perf(renderer): guard markdown parser work
This commit is contained in:
parent
d013b5328c
commit
9afd3e87ee
1 changed files with 100 additions and 53 deletions
|
|
@ -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("(?<!\\*)\\*([^*\\n]+)\\*(?!\\*)", 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)
|
||||
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
|
||||
spans.append(HybridMarkdownSpan(range: match.contentRange, kind: .italic))
|
||||
spans.append(contentsOf: delimiterRanges(match.fullRange, leading: 1, trailing: 1))
|
||||
excludedRanges.append(match.fullRange)
|
||||
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 {
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue