From 9afd3e87eeff89e1b512a401a1f6e636ffc3596a Mon Sep 17 00:00:00 2001 From: Feror Date: Sun, 31 May 2026 20:53:20 +0200 Subject: [PATCH] perf(renderer): guard markdown parser work --- .../HybridMarkdownLineRenderer.swift | 153 ++++++++++++------ 1 file changed, 100 insertions(+), 53 deletions(-) diff --git a/Sources/SaplingEditor/HybridMarkdownLineRenderer.swift b/Sources/SaplingEditor/HybridMarkdownLineRenderer.swift index 0ad4d2c..d0198a1 100644 --- a/Sources/SaplingEditor/HybridMarkdownLineRenderer.swift +++ b/Sources/SaplingEditor/HybridMarkdownLineRenderer.swift @@ -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,48 +274,63 @@ public struct HybridMarkdownLineRenderer: Sendable { var spans: [HybridMarkdownSpan] = [] var excludedRanges: [NSRange] = [] - collectMatches("`([^`\\n]+)`", 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) + 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 - 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("**") { + 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 - 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("__") { + 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) + } } - collectLinkMatches(in: line, excluding: excludedRanges).forEach { match in - spans.append(HybridMarkdownSpan(range: match.titleRange, kind: .link)) - spans.append(HybridMarkdownSpan(range: match.urlRange, kind: .markdownDelimiter)) - spans.append(contentsOf: match.delimiterRanges.map { - HybridMarkdownSpan(range: $0, kind: .markdownDelimiter) - }) - 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)) + spans.append(contentsOf: match.delimiterRanges.map { + HybridMarkdownSpan(range: $0, kind: .markdownDelimiter) + }) + excludedRanges.append(match.fullRange) + } } - collectMatches("(https?://[^\\s<>()]+)", in: line, excluding: excludedRanges).forEach { match in - spans.append(HybridMarkdownSpan(range: match.fullRange, kind: .automaticLink)) - excludedRanges.append(match.fullRange) + 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("(? [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: "(? 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("(?()]+)") + static let italicAsterisk = regex("(? NSRegularExpression { + do { + return try NSRegularExpression(pattern: pattern) + } catch { + preconditionFailure("Invalid Markdown renderer regex: \(pattern)") + } + } +} + private extension NSRange { var upperBound: Int { location + length