Skip to content

Commit

Permalink
Adds StringSyntaxHighlighter (#363)
Browse files Browse the repository at this point in the history
* Adds default tab stops

* Fixes SwiftLint warning

* Adds StringSyntaxHighlighter

* Adds documentation

* Fixes SwiftLint warnings

* Improves formatting
  • Loading branch information
simonbs committed Mar 23, 2024
1 parent 78983bc commit 9a48654
Show file tree
Hide file tree
Showing 4 changed files with 200 additions and 0 deletions.
2 changes: 2 additions & 0 deletions Sources/Runestone/Documentation.docc/Documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,14 @@ Syntax highlighting is based on GitHub's [Tree-sitter](https://github.com/tree-s

- <doc:Syntax-Highlighting-the-Text>
- <doc:AddingATreeSitterLanguage>
- <doc:SyntaxHighlightingAString>
- ``LanguageMode``
- ``PlainTextLanguageMode``
- ``TreeSitterLanguageMode``
- ``TreeSitterLanguage``
- ``TreeSitterLanguageProvider``
- ``SyntaxNode``
- ``StringSyntaxHighlighter``

### Indentation

Expand Down
Original file line number Diff line number Diff line change
@@ -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

- <doc:SyntaxHighlightingAString>
- ``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``
48 changes: 48 additions & 0 deletions Sources/Runestone/Documentation.docc/SyntaxHighlightingAString.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Syntax Highlighting a String

Learn how to syntax hightlight a string without needing to create a TextView.

## Overview

The <doc:StringSyntaxHighlighter> can be used to syntax highlight a string without needing to create a <doc:TextView>.

Before reading this article, make sure that you have follow the guides on <doc:AddingATreeSitterLanguage> and <doc:CreatingATheme>.


## Creating an Attributed String

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.

```swift
let syntaxHighlighter = StringSyntaxHighlighter(
theme: TomorrowTheme(),
language: .javaScript
)
```

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.

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.
105 changes: 105 additions & 0 deletions Sources/Runestone/StringSyntaxHighlighter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
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 scaledLineHeight = theme.font.totalLineHeight * lineHeightMultiplier
let mutableParagraphStyle = getMutableParagraphStyle(from: attributedString)
mutableParagraphStyle.lineSpacing = scaledLineHeight - 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()
}

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
}
}

0 comments on commit 9a48654

Please sign in to comment.