From cae92bfb6144966f8513f4a63d78ccb8e4fb2228 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Sat, 16 Mar 2024 16:51:48 +0100 Subject: [PATCH 1/6] Adds default tab stops --- Sources/Runestone/Library/DefaultStringAttributes.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/Runestone/Library/DefaultStringAttributes.swift b/Sources/Runestone/Library/DefaultStringAttributes.swift index 8784b828..fff834a4 100644 --- a/Sources/Runestone/Library/DefaultStringAttributes.swift +++ b/Sources/Runestone/Library/DefaultStringAttributes.swift @@ -9,7 +9,9 @@ struct DefaultStringAttributes { func apply(to attributedString: NSMutableAttributedString) { let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.tabStops = [] + paragraphStyle.tabStops = (0 ..< 20).map { + NSTextTab(textAlignment: .natural, location: CGFloat($0) * tabWidth) + } paragraphStyle.defaultTabInterval = tabWidth let range = NSRange(location: 0, length: attributedString.length) let attributes: [NSAttributedString.Key: Any] = [ From 84257dcfa3b07f7aabda1e20f9a4f4731d185e26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Sat, 16 Mar 2024 17:07:45 +0100 Subject: [PATCH 2/6] Fixes SwiftLint warning --- Sources/Runestone/Library/DefaultStringAttributes.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Runestone/Library/DefaultStringAttributes.swift b/Sources/Runestone/Library/DefaultStringAttributes.swift index fff834a4..aab529d4 100644 --- a/Sources/Runestone/Library/DefaultStringAttributes.swift +++ b/Sources/Runestone/Library/DefaultStringAttributes.swift @@ -9,8 +9,8 @@ struct DefaultStringAttributes { func apply(to attributedString: NSMutableAttributedString) { let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.tabStops = (0 ..< 20).map { - NSTextTab(textAlignment: .natural, location: CGFloat($0) * tabWidth) + paragraphStyle.tabStops = (0 ..< 20).map { index in + NSTextTab(textAlignment: .natural, location: CGFloat(index) * tabWidth) } paragraphStyle.defaultTabInterval = tabWidth let range = NSRange(location: 0, length: attributedString.length) From b03a98091835e9c709b4607f4bfd54d7faaaa21a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Sat, 16 Mar 2024 17:08:10 +0100 Subject: [PATCH 3/6] Adds StringSyntaxHighlighter --- .../Runestone/StringSyntaxHighlighter.swift | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 Sources/Runestone/StringSyntaxHighlighter.swift diff --git a/Sources/Runestone/StringSyntaxHighlighter.swift b/Sources/Runestone/StringSyntaxHighlighter.swift new file mode 100644 index 00000000..246624f0 --- /dev/null +++ b/Sources/Runestone/StringSyntaxHighlighter.swift @@ -0,0 +1,97 @@ +import UIKit + +/// Syntax highlights a string. +/// +/// An instance of `StringSyntaxHighlighter` can be used to syntax highlight a string without needing to create a `TextView`. +public final class StringSyntaxHighlighter { + /// The theme to use when syntax highlighting the text. + public var theme: Theme + /// The language to use when parsing the text. + public var language: TreeSitterLanguage + /// Object that can provide embedded languages on demand. A strong reference will be stored to the language provider. + public var languageProvider: TreeSitterLanguageProvider? + /// The number of points by which to adjust kern. + /// + /// The default value is 0 meaning that kerning is disabled. + public var kern: CGFloat = 0 + /// The tab length determines the width of the tab measured in space characers. + /// + /// The default value is 4 meaning that a tab is four spaces wide. + public var tabLength: Int = 4 + /// The line-height is multiplied with the value. + public var lineHeightMultiplier: CGFloat = 1 + + /// Creates an object that can syntax highlight a text. + /// - Parameters: + /// - theme: The theme to use when syntax highlighting the text. + /// - language: The language to use when parsing the text + /// - languageProvider: Object that can provide embedded languages on demand. A strong reference will be stored to the language provider.. + public init( + theme: Theme = DefaultTheme(), + language: TreeSitterLanguage, + languageProvider: TreeSitterLanguageProvider? = nil + ) { + self.theme = theme + self.language = language + self.languageProvider = languageProvider + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + /// Syntax highlights the text using the configured syntax highlighter. + /// - Parameter text: The text to be syntax highlighted. + /// - Returns: An attributed string containing the syntax highlighted text. + public func syntaxHighlight(_ text: String) -> NSAttributedString { + let mutableString = NSMutableString(string: text) + let stringView = StringView(string: mutableString) + let lineManager = LineManager(stringView: stringView) + lineManager.rebuild() + let languageMode = TreeSitterLanguageMode(language: language, languageProvider: languageProvider) + let internalLanguageMode = languageMode.makeInternalLanguageMode( + stringView: stringView, + lineManager: lineManager + ) + internalLanguageMode.parse(mutableString) + let tabWidth = TabWidthMeasurer.tabWidth(tabLength: tabLength, font: theme.font) + let mutableAttributedString = NSMutableAttributedString(string: text) + let defaultAttributes = DefaultStringAttributes( + textColor: theme.textColor, + font: theme.font, + kern: kern, + tabWidth: tabWidth + ) + defaultAttributes.apply(to: mutableAttributedString) + applyLineHeightMultiplier(to: mutableAttributedString) + let byteRange = ByteRange(from: 0, to: text.byteCount) + let syntaxHighlighter = internalLanguageMode.createLineSyntaxHighlighter() + syntaxHighlighter.theme = theme + let syntaxHighlighterInput = LineSyntaxHighlighterInput( + attributedString: mutableAttributedString, + byteRange: byteRange + ) + syntaxHighlighter.syntaxHighlight(syntaxHighlighterInput) + return mutableAttributedString + } +} + +private extension StringSyntaxHighlighter { + private func applyLineHeightMultiplier(to attributedString: NSMutableAttributedString) { + let mutableParagraphStyle = if let paragraphStyle = attributedString.attribute( + .paragraphStyle, + at: 0, + effectiveRange: nil + ) as? NSParagraphStyle { + paragraphStyle.mutableCopy() as! NSMutableParagraphStyle + } else { + NSMutableParagraphStyle() + } + mutableParagraphStyle.lineSpacing = (theme.font.totalLineHeight * lineHeightMultiplier) - theme.font.totalLineHeight + let range = NSRange(location: 0, length: attributedString.length) + attributedString.beginEditing() + attributedString.removeAttribute(.paragraphStyle, range: range) + attributedString.addAttribute(.paragraphStyle, value: mutableParagraphStyle, range: range) + attributedString.endEditing() + } +} From b60295db363d30557910e5ae0cbabeee2867e439 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Sat, 16 Mar 2024 17:08:16 +0100 Subject: [PATCH 4/6] Adds documentation --- .../Documentation.docc/Documentation.md | 2 + .../Extensions/StringSyntaxHighlighter.md | 45 +++++++++++++++++ .../SyntaxHighlightingAString.md | 48 +++++++++++++++++++ 3 files changed, 95 insertions(+) create mode 100644 Sources/Runestone/Documentation.docc/Extensions/StringSyntaxHighlighter.md create mode 100644 Sources/Runestone/Documentation.docc/SyntaxHighlightingAString.md diff --git a/Sources/Runestone/Documentation.docc/Documentation.md b/Sources/Runestone/Documentation.docc/Documentation.md index 058d6647..4be7c777 100644 --- a/Sources/Runestone/Documentation.docc/Documentation.md +++ b/Sources/Runestone/Documentation.docc/Documentation.md @@ -61,12 +61,14 @@ Syntax highlighting is based on GitHub's [Tree-sitter](https://github.com/tree-s - - +- - ``LanguageMode`` - ``PlainTextLanguageMode`` - ``TreeSitterLanguageMode`` - ``TreeSitterLanguage`` - ``TreeSitterLanguageProvider`` - ``SyntaxNode`` +- ``StringSyntaxHighlighter`` ### Indentation diff --git a/Sources/Runestone/Documentation.docc/Extensions/StringSyntaxHighlighter.md b/Sources/Runestone/Documentation.docc/Extensions/StringSyntaxHighlighter.md new file mode 100644 index 00000000..578b6528 --- /dev/null +++ b/Sources/Runestone/Documentation.docc/Extensions/StringSyntaxHighlighter.md @@ -0,0 +1,45 @@ +# ``StringSyntaxHighlighter`` + +## Example + +Create a syntax highlighter by passing a theme and language, and then call the ``StringSyntaxHighlighter/syntaxHighlight(_:)`` method to syntax highlight the provided text. + +```swift +let syntaxHighlighter = StringSyntaxHighlighter( + theme: TomorrowTheme(), + language: .javaScript +) +let attributedString = syntaxHighlighter.syntaxHighlight( + """ + function fibonacci(num) { + if (num <= 1) { + return 1 + } + return fibonacci(num - 1) + fibonacci(num - 2) + } + """ +) +``` + +## Topics + +### Essentials + +- +- ``StringSyntaxHighlighter/syntaxHighlight(_:)`` + +### Initialing the Syntax Highlighter + +- ``StringSyntaxHighlighter/init(theme:language:languageProvider:)`` + +### Configuring the Appearance + +- ``StringSyntaxHighlighter/theme`` +- ``StringSyntaxHighlighter/kern`` +- ``StringSyntaxHighlighter/lineHeightMultiplier`` +- ``StringSyntaxHighlighter/tabLength`` + +### Specifying the Language + +- ``StringSyntaxHighlighter/language`` +- ``StringSyntaxHighlighter/languageProvider`` diff --git a/Sources/Runestone/Documentation.docc/SyntaxHighlightingAString.md b/Sources/Runestone/Documentation.docc/SyntaxHighlightingAString.md new file mode 100644 index 00000000..f812e0f5 --- /dev/null +++ b/Sources/Runestone/Documentation.docc/SyntaxHighlightingAString.md @@ -0,0 +1,48 @@ +# Syntax Highlighting a String + +Learn how to syntax hightlight a string without needing to create a TextView. + +## Overview + +The can be used to syntax highlight a string without needing to create a . + +Before reading this article, make sure that you have follow the guides on and . + + +## Creating an Attributed String + +Create an instance of by supplying the theme containing the colors and fonts to be used for syntax highlighting the text, as well as the language to use when parsing the text. + +```swift +let syntaxHighlighter = StringSyntaxHighlighter( + theme: TomorrowTheme(), + language: .javaScript +) +``` + +If the language has any embedded languages, you will need to pass an object conforming to , which provides the syntax highlighter with additional languages. + +Apply customizations to the syntax highlighter as needed. + +```swift +syntaxHighlighter.kern = 0.3 +syntaxHighlighter.lineHeightMultiplier = 1.2 +syntaxHighlighter.tabLength = 2 +``` + +With the syntax highlighter created and configured, we can syntax highlight the text. + +```swift +let attributedString = syntaxHighlighter.syntaxHighlight( + """ + function fibonacci(num) { + if (num <= 1) { + return 1 + } + return fibonacci(num - 1) + fibonacci(num - 2) + } + """ +) +``` + +The attributed string can be displayed using a UILabel or UITextView. From 315f390db73f4ef816667e1b1a6b2fa9fbc549c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Sat, 16 Mar 2024 17:27:21 +0100 Subject: [PATCH 5/6] Fixes SwiftLint warnings --- .../Runestone/StringSyntaxHighlighter.swift | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/Sources/Runestone/StringSyntaxHighlighter.swift b/Sources/Runestone/StringSyntaxHighlighter.swift index 246624f0..769783bf 100644 --- a/Sources/Runestone/StringSyntaxHighlighter.swift +++ b/Sources/Runestone/StringSyntaxHighlighter.swift @@ -20,7 +20,7 @@ public final class StringSyntaxHighlighter { public var tabLength: Int = 4 /// The line-height is multiplied with the value. public var lineHeightMultiplier: CGFloat = 1 - + /// Creates an object that can syntax highlight a text. /// - Parameters: /// - theme: The theme to use when syntax highlighting the text. @@ -35,11 +35,11 @@ public final class StringSyntaxHighlighter { self.language = language self.languageProvider = languageProvider } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + /// Syntax highlights the text using the configured syntax highlighter. /// - Parameter text: The text to be syntax highlighted. /// - Returns: An attributed string containing the syntax highlighted text. @@ -78,15 +78,7 @@ public final class StringSyntaxHighlighter { private extension StringSyntaxHighlighter { private func applyLineHeightMultiplier(to attributedString: NSMutableAttributedString) { - let mutableParagraphStyle = if let paragraphStyle = attributedString.attribute( - .paragraphStyle, - at: 0, - effectiveRange: nil - ) as? NSParagraphStyle { - paragraphStyle.mutableCopy() as! NSMutableParagraphStyle - } else { - NSMutableParagraphStyle() - } + let mutableParagraphStyle = getMutableParagraphStyle(from: attributedString) mutableParagraphStyle.lineSpacing = (theme.font.totalLineHeight * lineHeightMultiplier) - theme.font.totalLineHeight let range = NSRange(location: 0, length: attributedString.length) attributedString.beginEditing() @@ -94,4 +86,19 @@ private extension StringSyntaxHighlighter { attributedString.addAttribute(.paragraphStyle, value: mutableParagraphStyle, range: range) attributedString.endEditing() } + + private func getMutableParagraphStyle( + from attributedString: NSMutableAttributedString + ) -> NSMutableParagraphStyle { + guard let attributeValue = attributedString.attribute(.paragraphStyle, at: 0, effectiveRange: nil) else { + return NSMutableParagraphStyle() + } + guard let paragraphStyle = attributeValue as? NSParagraphStyle else { + fatalError("Expected .paragraphStyle attribute to be instance of NSParagraphStyle") + } + guard let mutableParagraphStyle = paragraphStyle.mutableCopy() as? NSMutableParagraphStyle else { + fatalError("Expected mutableCopy() to return an instance of NSMutableParagraphStyle") + } + return mutableParagraphStyle + } } From d10471e001040f49622e75382fd5ca65887e09a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Sat, 16 Mar 2024 17:27:49 +0100 Subject: [PATCH 6/6] Improves formatting --- Sources/Runestone/StringSyntaxHighlighter.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/Runestone/StringSyntaxHighlighter.swift b/Sources/Runestone/StringSyntaxHighlighter.swift index 769783bf..000122d4 100644 --- a/Sources/Runestone/StringSyntaxHighlighter.swift +++ b/Sources/Runestone/StringSyntaxHighlighter.swift @@ -78,8 +78,9 @@ public final class StringSyntaxHighlighter { private extension StringSyntaxHighlighter { private func applyLineHeightMultiplier(to attributedString: NSMutableAttributedString) { + let scaledLineHeight = theme.font.totalLineHeight * lineHeightMultiplier let mutableParagraphStyle = getMutableParagraphStyle(from: attributedString) - mutableParagraphStyle.lineSpacing = (theme.font.totalLineHeight * lineHeightMultiplier) - theme.font.totalLineHeight + mutableParagraphStyle.lineSpacing = scaledLineHeight - theme.font.totalLineHeight let range = NSRange(location: 0, length: attributedString.length) attributedString.beginEditing() attributedString.removeAttribute(.paragraphStyle, range: range)