perf(renderer): guard markdown parser work

This commit is contained in:
Feror 2026-05-31 20:53:20 +02:00
parent d013b5328c
commit 9afd3e87ee

View file

@ -87,31 +87,33 @@ public struct HybridMarkdownLineRenderer: Sendable {
}
private func lineKind(for line: EditorLine) -> HybridMarkdownLineKind {
if let fence = fencedCodeFence(in: line) {
let firstToken = firstNonWhitespaceCharacter(in: line.source)
if (firstToken == "`" || firstToken == "~"), let fence = fencedCodeFence(in: line) {
return fence
}
if isHorizontalRule(line.source) {
if (firstToken == "-" || firstToken == "*" || firstToken == "_"), isHorizontalRule(line.source) {
return .horizontalRule(range: line.range)
}
if let heading = heading(in: line) {
if firstToken == "#", let heading = heading(in: line) {
return heading
}
if let task = taskList(in: line) {
if (firstToken == "-" || firstToken == "*"), let task = taskList(in: line) {
return task
}
if let unorderedList = unorderedList(in: line) {
if (firstToken == "-" || firstToken == "*"), let unorderedList = unorderedList(in: line) {
return unorderedList
}
if let orderedList = orderedList(in: line) {
if firstToken?.isNumber == true, let orderedList = orderedList(in: line) {
return orderedList
}
if let blockquote = blockquote(in: line) {
if firstToken == ">", let blockquote = blockquote(in: line) {
return blockquote
}
@ -171,7 +173,7 @@ public struct HybridMarkdownLineRenderer: Sendable {
}
private func unorderedList(in line: EditorLine) -> HybridMarkdownLineKind? {
guard let match = firstMatch("^([ \\t]*)([-*])\\s+(.*)$", in: line.source),
guard let match = firstMatch(Patterns.unorderedList, in: line.source),
match.range(at: 3).length > 0
else { return nil }
@ -188,7 +190,7 @@ public struct HybridMarkdownLineRenderer: Sendable {
}
private func orderedList(in line: EditorLine) -> HybridMarkdownLineKind? {
guard let match = firstMatch("^([ \\t]*)(\\d+[.)])\\s+(.*)$", in: line.source),
guard let match = firstMatch(Patterns.orderedList, in: line.source),
match.range(at: 3).length > 0
else { return nil }
@ -205,7 +207,7 @@ public struct HybridMarkdownLineRenderer: Sendable {
}
private func taskList(in line: EditorLine) -> HybridMarkdownLineKind? {
guard let match = firstMatch("^([ \\t]*)([-*])\\s+(\\[[ xX]\\])\\s+(.*)$", in: line.source),
guard let match = firstMatch(Patterns.taskList, in: line.source),
match.range(at: 4).length > 0
else { return nil }
@ -230,7 +232,7 @@ public struct HybridMarkdownLineRenderer: Sendable {
}
private func fencedCodeFence(in line: EditorLine) -> HybridMarkdownLineKind? {
guard let match = firstMatch("^([ \\t]*)(```|~~~)([^`~]*)$", in: line.source) else { return nil }
guard let match = firstMatch(Patterns.fencedCodeFence, in: line.source) else { return nil }
let languageRange = match.range(at: 3).length > 0
? NSRange(location: line.range.location + match.range(at: 3).location, length: match.range(at: 3).length)
: nil
@ -248,7 +250,7 @@ public struct HybridMarkdownLineRenderer: Sendable {
let trimmed = line.source.trimmingCharacters(in: .whitespaces)
guard trimmed.contains("|"), trimmed.count > 2 else { return nil }
let separatorRanges = regexMatches("\\|", in: line.source).map {
let separatorRanges = regexMatches(Patterns.tableSeparator, in: line.source).map {
NSRange(location: line.range.location + $0.range.location, length: $0.range.length)
}
guard separatorRanges.count >= 1 else { return nil }
@ -272,24 +274,31 @@ public struct HybridMarkdownLineRenderer: Sendable {
var spans: [HybridMarkdownSpan] = []
var excludedRanges: [NSRange] = []
collectMatches("`([^`\\n]+)`", in: line, excluding: excludedRanges).forEach { match in
if line.source.contains("`") {
collectMatches(Patterns.inlineCode, in: line, excluding: excludedRanges).forEach { match in
spans.append(HybridMarkdownSpan(range: match.contentRange, kind: .inlineCode))
spans.append(contentsOf: delimiterRanges(match.fullRange, leading: 1, trailing: 1))
excludedRanges.append(match.fullRange)
}
}
collectMatches("\\*\\*([^*\\n]+)\\*\\*", in: line, excluding: excludedRanges).forEach { match in
if line.source.contains("**") {
collectMatches(Patterns.boldAsterisk, in: line, excluding: excludedRanges).forEach { match in
spans.append(HybridMarkdownSpan(range: match.contentRange, kind: .bold))
spans.append(contentsOf: delimiterRanges(match.fullRange, leading: 2, trailing: 2))
excludedRanges.append(match.fullRange)
}
}
collectMatches("__([^_\\n]+)__", in: line, excluding: excludedRanges).forEach { match in
if line.source.contains("__") {
collectMatches(Patterns.boldUnderscore, in: line, excluding: excludedRanges).forEach { match in
spans.append(HybridMarkdownSpan(range: match.contentRange, kind: .bold))
spans.append(contentsOf: delimiterRanges(match.fullRange, leading: 2, trailing: 2))
excludedRanges.append(match.fullRange)
}
}
if line.source.contains("](") {
collectLinkMatches(in: line, excluding: excludedRanges).forEach { match in
spans.append(HybridMarkdownSpan(range: match.titleRange, kind: .link))
spans.append(HybridMarkdownSpan(range: match.urlRange, kind: .markdownDelimiter))
@ -298,23 +307,31 @@ public struct HybridMarkdownLineRenderer: Sendable {
})
excludedRanges.append(match.fullRange)
}
}
collectMatches("(https?://[^\\s<>()]+)", in: line, excluding: excludedRanges).forEach { match in
if !isReferenceStyleLinkDefinition(line.source),
line.source.contains("https://") || line.source.contains("http://") {
collectMatches(Patterns.automaticLink, in: line, excluding: excludedRanges).forEach { match in
spans.append(HybridMarkdownSpan(range: match.fullRange, kind: .automaticLink))
excludedRanges.append(match.fullRange)
}
}
collectMatches("(?<!\\*)\\*([^*\\n]+)\\*(?!\\*)", in: line, excluding: excludedRanges).forEach { match in
if line.source.contains("*") {
collectMatches(Patterns.italicAsterisk, in: line, excluding: excludedRanges).forEach { match in
spans.append(HybridMarkdownSpan(range: match.contentRange, kind: .italic))
spans.append(contentsOf: delimiterRanges(match.fullRange, leading: 1, trailing: 1))
excludedRanges.append(match.fullRange)
}
}
collectMatches("(?<!_)_([^_\\n]+)_(?!_)", in: line, excluding: excludedRanges).forEach { match in
if line.source.contains("_") {
collectMatches(Patterns.italicUnderscore, in: line, excluding: excludedRanges).forEach { match in
spans.append(HybridMarkdownSpan(range: match.contentRange, kind: .italic))
spans.append(contentsOf: delimiterRanges(match.fullRange, leading: 1, trailing: 1))
excludedRanges.append(match.fullRange)
}
}
return spans.sorted {
if $0.range.location == $1.range.location {
@ -325,11 +342,10 @@ public struct HybridMarkdownLineRenderer: Sendable {
}
private func collectMatches(
_ pattern: String,
_ regex: NSRegularExpression,
in line: EditorLine,
excluding excludedRanges: [NSRange]
) -> [InlineMatch] {
guard let regex = try? NSRegularExpression(pattern: pattern) else { return [] }
return regex.matches(in: line.source, range: NSRange(location: 0, length: line.source.utf16.count))
.compactMap { match in
guard match.numberOfRanges > 1 else { return nil }
@ -349,11 +365,7 @@ public struct HybridMarkdownLineRenderer: Sendable {
}
private func collectLinkMatches(in line: EditorLine, excluding excludedRanges: [NSRange]) -> [LinkMatch] {
guard let regex = try? NSRegularExpression(pattern: "(?<!!)\\[([^\\]\\n]+)\\]\\(([^)\\s\\n]+)\\)") else {
return []
}
return regex.matches(in: line.source, range: NSRange(location: 0, length: line.source.utf16.count))
Patterns.markdownLink.matches(in: line.source, range: NSRange(location: 0, length: line.source.utf16.count))
.compactMap { match in
guard match.numberOfRanges > 2 else { return nil }
let fullRange = NSRange(location: line.range.location + match.range.location, length: match.range.length)
@ -393,12 +405,11 @@ public struct HybridMarkdownLineRenderer: Sendable {
]
}
private func firstMatch(_ pattern: String, in source: String) -> NSTextCheckingResult? {
regexMatches(pattern, in: source).first
private func firstMatch(_ regex: NSRegularExpression, in source: String) -> NSTextCheckingResult? {
regexMatches(regex, in: source).first
}
private func regexMatches(_ pattern: String, in source: String) -> [NSTextCheckingResult] {
guard let regex = try? NSRegularExpression(pattern: pattern) else { return [] }
private func regexMatches(_ regex: NSRegularExpression, in source: String) -> [NSTextCheckingResult] {
return regex.matches(in: source, range: NSRange(location: 0, length: source.utf16.count))
}
@ -456,6 +467,19 @@ public struct HybridMarkdownLineRenderer: Sendable {
private func isWhitespace(_ character: unichar) -> Bool {
character == 32 || character == 9
}
private func isReferenceStyleLinkDefinition(_ source: String) -> Bool {
guard firstNonWhitespaceCharacter(in: source) == "[",
let closingBracket = source.firstIndex(of: "]")
else { return false }
let nextIndex = source.index(after: closingBracket)
return nextIndex < source.endIndex && source[nextIndex] == ":"
}
private func firstNonWhitespaceCharacter(in source: String) -> Character? {
source.first { $0 != " " && $0 != "\t" }
}
}
private struct InlineMatch {
@ -470,6 +494,29 @@ private struct LinkMatch {
var delimiterRanges: [NSRange]
}
private enum Patterns {
static let unorderedList = regex("^([ \\t]*)([-*])\\s+(.*)$")
static let orderedList = regex("^([ \\t]*)(\\d+[.)])\\s+(.*)$")
static let taskList = regex("^([ \\t]*)([-*])\\s+(\\[[ xX]\\])\\s+(.*)$")
static let fencedCodeFence = regex("^([ \\t]*)(```|~~~)([^`~]*)$")
static let tableSeparator = regex("\\|")
static let inlineCode = regex("`([^`\\n]+)`")
static let boldAsterisk = regex("\\*\\*([^*\\n]+)\\*\\*")
static let boldUnderscore = regex("__([^_\\n]+)__")
static let markdownLink = regex("(?<!!)\\[([^\\]\\n]+)\\]\\(([^)\\s\\n]+)\\)")
static let automaticLink = regex("(https?://[^\\s<>()]+)")
static let italicAsterisk = regex("(?<!\\*)\\*([^*\\n]+)\\*(?!\\*)")
static let italicUnderscore = regex("(?<!_)_([^_\\n]+)_(?!_)")
private static func regex(_ pattern: String) -> NSRegularExpression {
do {
return try NSRegularExpression(pattern: pattern)
} catch {
preconditionFailure("Invalid Markdown renderer regex: \(pattern)")
}
}
}
private extension NSRange {
var upperBound: Int {
location + length