Skip to content
Open
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
17 changes: 13 additions & 4 deletions Sources/Splash/Output/AttributedStringOutputFormat.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,26 @@ public extension AttributedStringOutputFormat {
}

public mutating func addToken(_ token: String, ofType type: TokenType) {
let color = theme.tokenColors[type] ?? Color(red: 1, green: 1, blue: 1)
string.append(token, font: font, color: color)
if #available(iOS 13.0, macCatalyst 13.0, *) {
let color = theme.tokenColors[type] ?? Color.label
string.append(token, font: font, color: color)
} else {
string.append(token, font: font, color: Color(red: 1, green: 1, blue: 1))
}
}

public mutating func addPlainText(_ text: String) {
string.append(text, font: font, color: theme.plainTextColor)
}

public mutating func addWhitespace(_ whitespace: String) {
let color = Color(red: 1, green: 1, blue: 1)
string.append(whitespace, font: font, color: color)
if #available(iOS 13.0, macCatalyst 13.0, *) {
let color = Color.label
string.append(whitespace, font: font, color: color)
} else {
let color = Color(red: 1, green: 1, blue: 1)
string.append(whitespace, font: font, color: color)
}
}

public func build() -> NSAttributedString {
Expand Down
21 changes: 14 additions & 7 deletions Sources/Splash/Tokenizing/Tokenizer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ private extension Tokenizer {
}

mutating func next() -> Segment? {
var segment = nextSegment ?? iterator.next()
nextSegment = iterator.next()
var segment = nextSegment ?? iterator.controlledIterate(depth: 32)
nextSegment = iterator.controlledIterate(depth: 32)
segment?.tokens.next = nextSegment?.tokens.current
return segment
}
Expand Down Expand Up @@ -60,6 +60,12 @@ private extension Tokenizer {
}

mutating func next() -> Segment? {
controlledIterate(depth: 32)
}

mutating func controlledIterate(depth: Int) -> Segment? {
guard depth >= 0 else { return nil }

let nextIndex = makeNextIndex()

guard nextIndex != code.endIndex else {
Expand All @@ -75,11 +81,12 @@ private extension Tokenizer {
case .token, .delimiter:
guard var segment = segments.current else {
segments.current = makeSegment(with: component, at: nextIndex)
return next()
return controlledIterate(depth: depth - 1)
}

guard segment.trailingWhitespace == nil,
component.isDelimiter == segment.currentTokenIsDelimiter else {
component.isDelimiter == segment.currentTokenIsDelimiter
else {
return finish(segment, with: component, at: nextIndex)
}

Expand All @@ -95,14 +102,14 @@ private extension Tokenizer {

segment.tokens.current.append(component.character)
segments.current = segment
return next()
return controlledIterate(depth: depth - 1)
case .whitespace, .newline:
guard var segment = segments.current else {
var segment = makeSegment(with: component, at: nextIndex)
segment.trailingWhitespace = component.token
segment.isLastOnLine = component.isNewline
segments.current = segment
return next()
return controlledIterate(depth: depth - 1)
}

if var existingWhitespace = segment.trailingWhitespace {
Expand All @@ -117,7 +124,7 @@ private extension Tokenizer {
}

segments.current = segment
return next()
return controlledIterate(depth: depth - 1)
}
}

Expand Down
20 changes: 20 additions & 0 deletions Tests/SplashTests/Tests/StackOverflowTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// StackOverflowTests.swift
// Splash
//
// Created by 秋星桥 on 3/15/25.
//

import Foundation
import Splash
import XCTest

final class StackOverflowTests: SyntaxHighlighterTestCase {
func testStackOverflow() {
_ = highlighter.highlight(testDocument)
}
}

private let testDocument = """
U1LDE3LjU1LDAsMSwwLTM1LjwwLDEPHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMjMzLjQ1LDQwLjgxYTE3LjtMzUuMSwwVjMzMGExMS42MywxMS42MywwLDEsMC0yMy4yNiwwdjQwLjg2YTQwLjgxLDQwLjgxLDAsMCwwLDgxLjYyNTUsMTcuNTUsMCwwLDEtMzUuMSwwVjMzMGExMS42MywxMS42MywwLDEsMC0yMy4yNiwwdjQwLjg2YTQwLjgxLDQwLjgxLDAsMCwwLDgxLjYyLDBWNDAuODFjE1LjksNjMuNEE0MC44Niw0MC44NiwwLDAsMCw0aIi8hdGggY2xhc3M9ImNscy0xIiBkPSJNMjMzLjQ1LDQwLjgxYTE3LjU1LDE3LjU1LDAsMSwwLTM1LjEsMFYzMzEuNTZhNDAuODIsNDAuODIsMCwwLDEtODEuNjMsMFYxNDVhMTcuNTUsMTcu40MmExMS42MywxMS42MywwLDAsMSwyMy4yNiwwdjI4LjY2YTE3LjU1LDE3LjU1LDAsMCwwLDM1LjEsMFYxNDVBNDAuODIsNDAuODIsMCwwLDEsMTQwLDE0NVYzMzEuNTZhMTsMFYyODEuNTZhMTEuNjMsMTEuNjMsMCwxLDEtMjMuMjYsMFptMjE1LjksNjMuNEE0MC44Niw0MC44NiwwLDAsMCw0MDguNTMsMTQ1VjMwMC44NWExNy41NSwxNy41NSwwLDAsMS0zNS4wMDguNTMsMTQ1VjMwMC44NWExNy41NSwxNy41NSwwLDAsMS0zNS4wOSwwdi0yNjBhNDAuODIsNDAuODIsMCwwLDAtODEuNjMsMFYzNzAuODlhMTcuLjg1LDQwMCwxLDAtMzUuMDksMHY3ExLjYzLDExLjYzLDAsMCwwLDIzLjI2LDBWMTQ1QTQwLjg1LDQwLjg1LDAsMCwwLDQ0OS4zNSwxMDQuMjFaIi8NmE0MC44Miw0MC44MiwwLDAsMS04MS42MywwVjMFYxNDVBNDAuODIsNDAuODIsMCwwLDEsMTQwLDE0NVYzMzEuNTZhMTcuNTUsMTcuNTUsMCwwLDAsMzUuMSwwVjIxNy41aDBWNDAuODFhNDAuODEsNDAuODEsMCwxLDEsODEuNjAsMhMTcuNTUsMTcuNTUsMCwwLNTUsQTQwLDBWNDAuODFhMTcuNTUsMTcuNTUsMCwwLDEsMzUuMSwwdjI2MGE0MC44Miw0MC44MiwwLDAsMCw4MS42MywwVjE0NWExNy41NSwxNy41NSwwLDEsMSwzNS4xLDBWMjgxLjU2YTQuMjFOSwwdi0yNjBhNDAuODIsNDAEsMFYzMzEuNTZhNDAuODIsNDAuODIsMCwwLDEtODEuNjMsMFYxNDVhMTcuNTUsMTcuNTUsMCwxLDAtMzUuMDksMHY3OS4wPHBuODIsMCE5NSDEsMzUuMSwwdjI2MGE0MC44Miw0MC44MiwwLDAsMCw4MS42MywwVjE0NWExNy41NSwxNy41NSwwLDEsMSwzNS4xLDBWMjgxLjU2YTExLjYzLDExLjYzLDAsMCwwLDIzLjI2LDBWMTQ1Ljg1LDcuNTUsMTcuNTUsMCwwLDAsMzUuMSwwVjIxNy41aDBWNDAuODFhNDAuODEsNDAuODEsMCwxLDEsODEuNjIsMFYyODEuNTZhMTEuNjMsMTEuNjMsMCwxLDEtMjMuMjYsMFptMCwwLDQ0OS4zNSwxMDOS4wNmE0MC44Miw0MC44MiwwLDAsMS04MS42MywwVjE5NS40MmExMS42MywxMS42MywwLDAsMSwyMy4yNiwwdjI4LjY2YTE3LjU1LDE3LjU1LDAsMCwwLDM1LjEswwLDAtODEuNjMsMFYzNzAuODlhMTcuNTUsMTcuNTUsMC
"""