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 {
|
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
|
return fence
|
||||||
}
|
}
|
||||||
|
|
||||||
if isHorizontalRule(line.source) {
|
if (firstToken == "-" || firstToken == "*" || firstToken == "_"), isHorizontalRule(line.source) {
|
||||||
return .horizontalRule(range: line.range)
|
return .horizontalRule(range: line.range)
|
||||||
}
|
}
|
||||||
|
|
||||||
if let heading = heading(in: line) {
|
if firstToken == "#", let heading = heading(in: line) {
|
||||||
return heading
|
return heading
|
||||||
}
|
}
|
||||||
|
|
||||||
if let task = taskList(in: line) {
|
if (firstToken == "-" || firstToken == "*"), let task = taskList(in: line) {
|
||||||
return task
|
return task
|
||||||
}
|
}
|
||||||
|
|
||||||
if let unorderedList = unorderedList(in: line) {
|
if (firstToken == "-" || firstToken == "*"), let unorderedList = unorderedList(in: line) {
|
||||||
return unorderedList
|
return unorderedList
|
||||||
}
|
}
|
||||||
|
|
||||||
if let orderedList = orderedList(in: line) {
|
if firstToken?.isNumber == true, let orderedList = orderedList(in: line) {
|
||||||
return orderedList
|
return orderedList
|
||||||
}
|
}
|
||||||
|
|
||||||
if let blockquote = blockquote(in: line) {
|
if firstToken == ">", let blockquote = blockquote(in: line) {
|
||||||
return blockquote
|
return blockquote
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -171,7 +173,7 @@ public struct HybridMarkdownLineRenderer: Sendable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func unorderedList(in line: EditorLine) -> HybridMarkdownLineKind? {
|
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
|
match.range(at: 3).length > 0
|
||||||
else { return nil }
|
else { return nil }
|
||||||
|
|
||||||
|
|
@ -188,7 +190,7 @@ public struct HybridMarkdownLineRenderer: Sendable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func orderedList(in line: EditorLine) -> HybridMarkdownLineKind? {
|
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
|
match.range(at: 3).length > 0
|
||||||
else { return nil }
|
else { return nil }
|
||||||
|
|
||||||
|
|
@ -205,7 +207,7 @@ public struct HybridMarkdownLineRenderer: Sendable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func taskList(in line: EditorLine) -> HybridMarkdownLineKind? {
|
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
|
match.range(at: 4).length > 0
|
||||||
else { return nil }
|
else { return nil }
|
||||||
|
|
||||||
|
|
@ -230,7 +232,7 @@ public struct HybridMarkdownLineRenderer: Sendable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func fencedCodeFence(in line: EditorLine) -> HybridMarkdownLineKind? {
|
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
|
let languageRange = match.range(at: 3).length > 0
|
||||||
? NSRange(location: line.range.location + match.range(at: 3).location, length: match.range(at: 3).length)
|
? NSRange(location: line.range.location + match.range(at: 3).location, length: match.range(at: 3).length)
|
||||||
: nil
|
: nil
|
||||||
|
|
@ -248,7 +250,7 @@ public struct HybridMarkdownLineRenderer: Sendable {
|
||||||
let trimmed = line.source.trimmingCharacters(in: .whitespaces)
|
let trimmed = line.source.trimmingCharacters(in: .whitespaces)
|
||||||
guard trimmed.contains("|"), trimmed.count > 2 else { return nil }
|
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)
|
NSRange(location: line.range.location + $0.range.location, length: $0.range.length)
|
||||||
}
|
}
|
||||||
guard separatorRanges.count >= 1 else { return nil }
|
guard separatorRanges.count >= 1 else { return nil }
|
||||||
|
|
@ -272,24 +274,31 @@ public struct HybridMarkdownLineRenderer: Sendable {
|
||||||
var spans: [HybridMarkdownSpan] = []
|
var spans: [HybridMarkdownSpan] = []
|
||||||
var excludedRanges: [NSRange] = []
|
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(HybridMarkdownSpan(range: match.contentRange, kind: .inlineCode))
|
||||||
spans.append(contentsOf: delimiterRanges(match.fullRange, leading: 1, trailing: 1))
|
spans.append(contentsOf: delimiterRanges(match.fullRange, leading: 1, trailing: 1))
|
||||||
excludedRanges.append(match.fullRange)
|
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(HybridMarkdownSpan(range: match.contentRange, kind: .bold))
|
||||||
spans.append(contentsOf: delimiterRanges(match.fullRange, leading: 2, trailing: 2))
|
spans.append(contentsOf: delimiterRanges(match.fullRange, leading: 2, trailing: 2))
|
||||||
excludedRanges.append(match.fullRange)
|
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(HybridMarkdownSpan(range: match.contentRange, kind: .bold))
|
||||||
spans.append(contentsOf: delimiterRanges(match.fullRange, leading: 2, trailing: 2))
|
spans.append(contentsOf: delimiterRanges(match.fullRange, leading: 2, trailing: 2))
|
||||||
excludedRanges.append(match.fullRange)
|
excludedRanges.append(match.fullRange)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if line.source.contains("](") {
|
||||||
collectLinkMatches(in: line, excluding: excludedRanges).forEach { match in
|
collectLinkMatches(in: line, excluding: excludedRanges).forEach { match in
|
||||||
spans.append(HybridMarkdownSpan(range: match.titleRange, kind: .link))
|
spans.append(HybridMarkdownSpan(range: match.titleRange, kind: .link))
|
||||||
spans.append(HybridMarkdownSpan(range: match.urlRange, kind: .markdownDelimiter))
|
spans.append(HybridMarkdownSpan(range: match.urlRange, kind: .markdownDelimiter))
|
||||||
|
|
@ -298,23 +307,31 @@ public struct HybridMarkdownLineRenderer: Sendable {
|
||||||
})
|
})
|
||||||
excludedRanges.append(match.fullRange)
|
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))
|
spans.append(HybridMarkdownSpan(range: match.fullRange, kind: .automaticLink))
|
||||||
excludedRanges.append(match.fullRange)
|
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(HybridMarkdownSpan(range: match.contentRange, kind: .italic))
|
||||||
spans.append(contentsOf: delimiterRanges(match.fullRange, leading: 1, trailing: 1))
|
spans.append(contentsOf: delimiterRanges(match.fullRange, leading: 1, trailing: 1))
|
||||||
excludedRanges.append(match.fullRange)
|
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(HybridMarkdownSpan(range: match.contentRange, kind: .italic))
|
||||||
spans.append(contentsOf: delimiterRanges(match.fullRange, leading: 1, trailing: 1))
|
spans.append(contentsOf: delimiterRanges(match.fullRange, leading: 1, trailing: 1))
|
||||||
excludedRanges.append(match.fullRange)
|
excludedRanges.append(match.fullRange)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return spans.sorted {
|
return spans.sorted {
|
||||||
if $0.range.location == $1.range.location {
|
if $0.range.location == $1.range.location {
|
||||||
|
|
@ -325,11 +342,10 @@ public struct HybridMarkdownLineRenderer: Sendable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func collectMatches(
|
private func collectMatches(
|
||||||
_ pattern: String,
|
_ regex: NSRegularExpression,
|
||||||
in line: EditorLine,
|
in line: EditorLine,
|
||||||
excluding excludedRanges: [NSRange]
|
excluding excludedRanges: [NSRange]
|
||||||
) -> [InlineMatch] {
|
) -> [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))
|
return regex.matches(in: line.source, range: NSRange(location: 0, length: line.source.utf16.count))
|
||||||
.compactMap { match in
|
.compactMap { match in
|
||||||
guard match.numberOfRanges > 1 else { return nil }
|
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] {
|
private func collectLinkMatches(in line: EditorLine, excluding excludedRanges: [NSRange]) -> [LinkMatch] {
|
||||||
guard let regex = try? NSRegularExpression(pattern: "(?<!!)\\[([^\\]\\n]+)\\]\\(([^)\\s\\n]+)\\)") else {
|
Patterns.markdownLink.matches(in: line.source, range: NSRange(location: 0, length: line.source.utf16.count))
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
return regex.matches(in: line.source, range: NSRange(location: 0, length: line.source.utf16.count))
|
|
||||||
.compactMap { match in
|
.compactMap { match in
|
||||||
guard match.numberOfRanges > 2 else { return nil }
|
guard match.numberOfRanges > 2 else { return nil }
|
||||||
let fullRange = NSRange(location: line.range.location + match.range.location, length: match.range.length)
|
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? {
|
private func firstMatch(_ regex: NSRegularExpression, in source: String) -> NSTextCheckingResult? {
|
||||||
regexMatches(pattern, in: source).first
|
regexMatches(regex, in: source).first
|
||||||
}
|
}
|
||||||
|
|
||||||
private func regexMatches(_ pattern: String, in source: String) -> [NSTextCheckingResult] {
|
private func regexMatches(_ regex: NSRegularExpression, in source: String) -> [NSTextCheckingResult] {
|
||||||
guard let regex = try? NSRegularExpression(pattern: pattern) else { return [] }
|
|
||||||
return regex.matches(in: source, range: NSRange(location: 0, length: source.utf16.count))
|
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 {
|
private func isWhitespace(_ character: unichar) -> Bool {
|
||||||
character == 32 || character == 9
|
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 {
|
private struct InlineMatch {
|
||||||
|
|
@ -470,6 +494,29 @@ private struct LinkMatch {
|
||||||
var delimiterRanges: [NSRange]
|
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 {
|
private extension NSRange {
|
||||||
var upperBound: Int {
|
var upperBound: Int {
|
||||||
location + length
|
location + length
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue