Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Benchmarks/ChromaBenchmarks/ChromaBenchmarks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ let curatedSwiftTokens = try! BenchmarkSupport.tokenize(curatedSwift, language:
let curatedDiffTokens = try! BenchmarkSupport.tokenize(curatedDiff, language: .swift, registry: registry)

let defaultOptions = HighlightOptions()
let diffOptions = HighlightOptions(diff: .patch)
let diffOptions = HighlightOptions(diff: .patch())

let shouldPrintMetrics = ProcessInfo.processInfo.environment["CHROMA_BENCH_PRINT_METRICS"] == "1"

Expand Down
34 changes: 32 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ let output = try Chroma.highlight(

### Diff highlighting (unified patch)

`Chroma` can highlight `+` / `-` lines with green / red background, following the common `git diff` patch rules.
`Chroma` can highlight `+` / `-` lines in unified patches, following the common `git diff` patch rules.

```swift
let patch = """
Expand All @@ -111,11 +111,41 @@ index 1111111..2222222 100644
let output = try Chroma.highlight(
patch,
language: .swift,
options: .init(diff: .patch)
options: .init(diff: .patch())
)
print(output)
```

Foreground-only diff (plain text, no token styling):

```swift
let output = try Chroma.highlight(
patch,
language: .swift,
options: .init(diff: .patch(style: .foreground()))
)
```

Foreground diff with syntax-highlighted context lines:

```swift
let output = try Chroma.highlight(
patch,
language: .swift,
options: .init(diff: .patch(style: .foreground(contextCode: .syntax)))
)
```

Background diff without code styling:

```swift
let output = try Chroma.highlight(
patch,
language: .swift,
options: .init(diff: .patch(style: .background(diffCode: .plain)))
)
```

## Custom languages

`Chroma` uses a regex-based tokenizer (similar to Prism / highlight.js) so new languages can be defined by rules.
Expand Down
26 changes: 25 additions & 1 deletion Sources/Chroma/Diff.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,31 @@ enum DiffLineKind {

enum DiffDetector {
static func looksLikePatch(_ code: String) -> Bool {
return looksLikePatch(lines: splitLines(code))
var lineStart = code.startIndex
var index = lineStart

func lineLooksLikePatch(start: String.Index, end: String.Index) -> Bool {
guard start < end else { return false }
let line = code[start..<end]
if line.hasPrefix("diff --git ") { return true }
if line.hasPrefix("@@") { return true }
if line.hasPrefix("--- ") || line.hasPrefix("+++ ") { return true }
return false
}

while index < code.endIndex {
if code[index] == "\n" {
if lineLooksLikePatch(start: lineStart, end: index) {
return true
}
index = code.index(after: index)
lineStart = index
continue
}
index = code.index(after: index)
}

return lineLooksLikePatch(start: lineStart, end: code.endIndex)
}

static func looksLikePatch(lines: [Substring]) -> Bool {
Expand Down
87 changes: 84 additions & 3 deletions Sources/Chroma/HighlightOptions.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
public struct HighlightOptions: Equatable {
public enum DiffCodeStyle: Equatable {
case syntax
case plain
}

public enum DiffStyle: Equatable {
case background(diffCode: DiffCodeStyle = .syntax, contextCode: DiffCodeStyle = .syntax)
case foreground(contextCode: DiffCodeStyle = .plain)
}

public enum DiffHighlight: Equatable {
case none
case auto
case patch
case auto(style: DiffStyle = .background())
case patch(style: DiffStyle = .background())
}

public var theme: Theme?
Expand All @@ -18,7 +28,7 @@ public struct HighlightOptions: Equatable {

public init(
theme: Theme? = nil,
diff: DiffHighlight = .auto,
diff: DiffHighlight = .auto(),
highlightLines: LineRangeSet = .init(),
indent: Int = 0
) {
Expand All @@ -28,3 +38,74 @@ public struct HighlightOptions: Equatable {
self.indent = max(0, indent)
}
}

extension HighlightOptions {
struct DiffRendering: Equatable {
let style: DiffStyle
}

var maySkipTokenization: Bool {
switch diff {
case .none:
return false
case let .auto(style), let .patch(style):
return style.diffCodeStyle == .plain && style.contextCodeStyle == .plain
}
}

func diffRendering(for code: String) -> DiffRendering? {
diff.rendering(for: code)
}

func shouldSkipTokenization(for code: String) -> Bool {
guard let rendering = diffRendering(for: code) else { return false }
let style = rendering.style
return style.diffCodeStyle == .plain && style.contextCodeStyle == .plain
}
}

extension HighlightOptions.DiffHighlight {
func rendering(for code: String) -> HighlightOptions.DiffRendering? {
switch self {
case .none:
return nil
case let .patch(style):
return HighlightOptions.DiffRendering(style: style)
case let .auto(style):
guard DiffDetector.looksLikePatch(code) else { return nil }
return HighlightOptions.DiffRendering(style: style)
}
}

func rendering(for lines: [Substring]) -> HighlightOptions.DiffRendering? {
switch self {
case .none:
return nil
case let .patch(style):
return HighlightOptions.DiffRendering(style: style)
case let .auto(style):
guard DiffDetector.looksLikePatch(lines: lines) else { return nil }
return HighlightOptions.DiffRendering(style: style)
}
}
}

extension HighlightOptions.DiffStyle {
var diffCodeStyle: HighlightOptions.DiffCodeStyle {
switch self {
case let .background(diffCode, _):
return diffCode
case .foreground:
return .plain
}
}

var contextCodeStyle: HighlightOptions.DiffCodeStyle {
switch self {
case let .background(_, contextCode):
return contextCode
case let .foreground(contextCode):
return contextCode
}
}
}
7 changes: 6 additions & 1 deletion Sources/Chroma/Highlighter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,13 @@ public final class Highlighter {
}

let theme = options.theme ?? self.theme
let tokenizer = RegexTokenizer(rules: language.rules, fastPath: language.fastPath)
let renderer = Renderer(theme: theme, options: options)
if options.maySkipTokenization && options.shouldSkipTokenization(for: code) {
let ns = code as NSString
let tokens = [Token(kind: .plain, range: NSRange(location: 0, length: ns.length))]
return renderer.render(code: code, tokens: tokens)
}
let tokenizer = RegexTokenizer(rules: language.rules, fastPath: language.fastPath)
return renderer.render(code: code) { emit in
tokenizer.scan(code, emit: emit)
}
Expand Down
Loading