Skip to content

Commit 9a48654

Browse files
authored
Adds StringSyntaxHighlighter (#363)
* Adds default tab stops * Fixes SwiftLint warning * Adds StringSyntaxHighlighter * Adds documentation * Fixes SwiftLint warnings * Improves formatting
1 parent 78983bc commit 9a48654

File tree

4 files changed

+200
-0
lines changed

4 files changed

+200
-0
lines changed

Sources/Runestone/Documentation.docc/Documentation.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,14 @@ Syntax highlighting is based on GitHub's [Tree-sitter](https://github.com/tree-s
6161

6262
- <doc:Syntax-Highlighting-the-Text>
6363
- <doc:AddingATreeSitterLanguage>
64+
- <doc:SyntaxHighlightingAString>
6465
- ``LanguageMode``
6566
- ``PlainTextLanguageMode``
6667
- ``TreeSitterLanguageMode``
6768
- ``TreeSitterLanguage``
6869
- ``TreeSitterLanguageProvider``
6970
- ``SyntaxNode``
71+
- ``StringSyntaxHighlighter``
7072

7173
### Indentation
7274

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# ``StringSyntaxHighlighter``
2+
3+
## Example
4+
5+
Create a syntax highlighter by passing a theme and language, and then call the ``StringSyntaxHighlighter/syntaxHighlight(_:)`` method to syntax highlight the provided text.
6+
7+
```swift
8+
let syntaxHighlighter = StringSyntaxHighlighter(
9+
theme: TomorrowTheme(),
10+
language: .javaScript
11+
)
12+
let attributedString = syntaxHighlighter.syntaxHighlight(
13+
"""
14+
function fibonacci(num) {
15+
if (num <= 1) {
16+
return 1
17+
}
18+
return fibonacci(num - 1) + fibonacci(num - 2)
19+
}
20+
"""
21+
)
22+
```
23+
24+
## Topics
25+
26+
### Essentials
27+
28+
- <doc:SyntaxHighlightingAString>
29+
- ``StringSyntaxHighlighter/syntaxHighlight(_:)``
30+
31+
### Initialing the Syntax Highlighter
32+
33+
- ``StringSyntaxHighlighter/init(theme:language:languageProvider:)``
34+
35+
### Configuring the Appearance
36+
37+
- ``StringSyntaxHighlighter/theme``
38+
- ``StringSyntaxHighlighter/kern``
39+
- ``StringSyntaxHighlighter/lineHeightMultiplier``
40+
- ``StringSyntaxHighlighter/tabLength``
41+
42+
### Specifying the Language
43+
44+
- ``StringSyntaxHighlighter/language``
45+
- ``StringSyntaxHighlighter/languageProvider``
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Syntax Highlighting a String
2+
3+
Learn how to syntax hightlight a string without needing to create a TextView.
4+
5+
## Overview
6+
7+
The <doc:StringSyntaxHighlighter> can be used to syntax highlight a string without needing to create a <doc:TextView>.
8+
9+
Before reading this article, make sure that you have follow the guides on <doc:AddingATreeSitterLanguage> and <doc:CreatingATheme>.
10+
11+
12+
## Creating an Attributed String
13+
14+
Create an instance of <doc:StringSyntaxHighlighter> 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.
15+
16+
```swift
17+
let syntaxHighlighter = StringSyntaxHighlighter(
18+
theme: TomorrowTheme(),
19+
language: .javaScript
20+
)
21+
```
22+
23+
If the language has any embedded languages, you will need to pass an object conforming to <doc:TreeSitterLanguageProvider>, which provides the syntax highlighter with additional languages.
24+
25+
Apply customizations to the syntax highlighter as needed.
26+
27+
```swift
28+
syntaxHighlighter.kern = 0.3
29+
syntaxHighlighter.lineHeightMultiplier = 1.2
30+
syntaxHighlighter.tabLength = 2
31+
```
32+
33+
With the syntax highlighter created and configured, we can syntax highlight the text.
34+
35+
```swift
36+
let attributedString = syntaxHighlighter.syntaxHighlight(
37+
"""
38+
function fibonacci(num) {
39+
if (num <= 1) {
40+
return 1
41+
}
42+
return fibonacci(num - 1) + fibonacci(num - 2)
43+
}
44+
"""
45+
)
46+
```
47+
48+
The attributed string can be displayed using a UILabel or UITextView.
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import UIKit
2+
3+
/// Syntax highlights a string.
4+
///
5+
/// An instance of `StringSyntaxHighlighter` can be used to syntax highlight a string without needing to create a `TextView`.
6+
public final class StringSyntaxHighlighter {
7+
/// The theme to use when syntax highlighting the text.
8+
public var theme: Theme
9+
/// The language to use when parsing the text.
10+
public var language: TreeSitterLanguage
11+
/// Object that can provide embedded languages on demand. A strong reference will be stored to the language provider.
12+
public var languageProvider: TreeSitterLanguageProvider?
13+
/// The number of points by which to adjust kern.
14+
///
15+
/// The default value is 0 meaning that kerning is disabled.
16+
public var kern: CGFloat = 0
17+
/// The tab length determines the width of the tab measured in space characers.
18+
///
19+
/// The default value is 4 meaning that a tab is four spaces wide.
20+
public var tabLength: Int = 4
21+
/// The line-height is multiplied with the value.
22+
public var lineHeightMultiplier: CGFloat = 1
23+
24+
/// Creates an object that can syntax highlight a text.
25+
/// - Parameters:
26+
/// - theme: The theme to use when syntax highlighting the text.
27+
/// - language: The language to use when parsing the text
28+
/// - languageProvider: Object that can provide embedded languages on demand. A strong reference will be stored to the language provider..
29+
public init(
30+
theme: Theme = DefaultTheme(),
31+
language: TreeSitterLanguage,
32+
languageProvider: TreeSitterLanguageProvider? = nil
33+
) {
34+
self.theme = theme
35+
self.language = language
36+
self.languageProvider = languageProvider
37+
}
38+
39+
required init?(coder: NSCoder) {
40+
fatalError("init(coder:) has not been implemented")
41+
}
42+
43+
/// Syntax highlights the text using the configured syntax highlighter.
44+
/// - Parameter text: The text to be syntax highlighted.
45+
/// - Returns: An attributed string containing the syntax highlighted text.
46+
public func syntaxHighlight(_ text: String) -> NSAttributedString {
47+
let mutableString = NSMutableString(string: text)
48+
let stringView = StringView(string: mutableString)
49+
let lineManager = LineManager(stringView: stringView)
50+
lineManager.rebuild()
51+
let languageMode = TreeSitterLanguageMode(language: language, languageProvider: languageProvider)
52+
let internalLanguageMode = languageMode.makeInternalLanguageMode(
53+
stringView: stringView,
54+
lineManager: lineManager
55+
)
56+
internalLanguageMode.parse(mutableString)
57+
let tabWidth = TabWidthMeasurer.tabWidth(tabLength: tabLength, font: theme.font)
58+
let mutableAttributedString = NSMutableAttributedString(string: text)
59+
let defaultAttributes = DefaultStringAttributes(
60+
textColor: theme.textColor,
61+
font: theme.font,
62+
kern: kern,
63+
tabWidth: tabWidth
64+
)
65+
defaultAttributes.apply(to: mutableAttributedString)
66+
applyLineHeightMultiplier(to: mutableAttributedString)
67+
let byteRange = ByteRange(from: 0, to: text.byteCount)
68+
let syntaxHighlighter = internalLanguageMode.createLineSyntaxHighlighter()
69+
syntaxHighlighter.theme = theme
70+
let syntaxHighlighterInput = LineSyntaxHighlighterInput(
71+
attributedString: mutableAttributedString,
72+
byteRange: byteRange
73+
)
74+
syntaxHighlighter.syntaxHighlight(syntaxHighlighterInput)
75+
return mutableAttributedString
76+
}
77+
}
78+
79+
private extension StringSyntaxHighlighter {
80+
private func applyLineHeightMultiplier(to attributedString: NSMutableAttributedString) {
81+
let scaledLineHeight = theme.font.totalLineHeight * lineHeightMultiplier
82+
let mutableParagraphStyle = getMutableParagraphStyle(from: attributedString)
83+
mutableParagraphStyle.lineSpacing = scaledLineHeight - theme.font.totalLineHeight
84+
let range = NSRange(location: 0, length: attributedString.length)
85+
attributedString.beginEditing()
86+
attributedString.removeAttribute(.paragraphStyle, range: range)
87+
attributedString.addAttribute(.paragraphStyle, value: mutableParagraphStyle, range: range)
88+
attributedString.endEditing()
89+
}
90+
91+
private func getMutableParagraphStyle(
92+
from attributedString: NSMutableAttributedString
93+
) -> NSMutableParagraphStyle {
94+
guard let attributeValue = attributedString.attribute(.paragraphStyle, at: 0, effectiveRange: nil) else {
95+
return NSMutableParagraphStyle()
96+
}
97+
guard let paragraphStyle = attributeValue as? NSParagraphStyle else {
98+
fatalError("Expected .paragraphStyle attribute to be instance of NSParagraphStyle")
99+
}
100+
guard let mutableParagraphStyle = paragraphStyle.mutableCopy() as? NSMutableParagraphStyle else {
101+
fatalError("Expected mutableCopy() to return an instance of NSMutableParagraphStyle")
102+
}
103+
return mutableParagraphStyle
104+
}
105+
}

0 commit comments

Comments
 (0)