0: GFM markdown rendering — fenced code blocks, inline code, tables, lists, task lists, strikethrough, footnotes, horizontal rules
This commit is contained in:
parent
94b0965be2
commit
6b5404f679
|
|
@ -160,20 +160,36 @@ func applySyntaxHighlighting(to textStorage: NSTextStorage) {
|
||||||
]
|
]
|
||||||
textStorage.setAttributes(baseAttrs, range: fullRange)
|
textStorage.setAttributes(baseAttrs, range: fullRange)
|
||||||
|
|
||||||
|
let fencedRanges = highlightFencedCodeBlocks(textStorage: textStorage, palette: palette, baseFont: baseFont)
|
||||||
|
let tableHeaderLines = findTableHeaderLines(textStorage: textStorage, fencedRanges: fencedRanges)
|
||||||
|
|
||||||
let nsText = text as NSString
|
let nsText = text as NSString
|
||||||
var lineStart = 0
|
var lineStart = 0
|
||||||
|
|
||||||
while lineStart < nsText.length {
|
while lineStart < nsText.length {
|
||||||
let lineRange = nsText.lineRange(for: NSRange(location: lineStart, length: 0))
|
let lineRange = nsText.lineRange(for: NSRange(location: lineStart, length: 0))
|
||||||
|
|
||||||
|
if isInsideFencedBlock(lineRange, fencedRanges: fencedRanges) {
|
||||||
|
lineStart = NSMaxRange(lineRange)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
let line = nsText.substring(with: lineRange)
|
let line = nsText.substring(with: lineRange)
|
||||||
let trimmed = line.trimmingCharacters(in: .whitespaces)
|
let trimmed = line.trimmingCharacters(in: .whitespaces)
|
||||||
|
let isTableHeader = tableHeaderLines.contains(lineRange.location)
|
||||||
|
|
||||||
if highlightMarkdownLine(trimmed, lineRange: lineRange, textStorage: textStorage, baseFont: baseFont, palette: palette) {
|
if highlightMarkdownLine(trimmed, line: line, lineRange: lineRange, textStorage: textStorage, baseFont: baseFont, palette: palette, isTableHeader: isTableHeader) {
|
||||||
|
highlightInlineCode(textStorage: textStorage, lineRange: lineRange, palette: palette, baseFont: baseFont)
|
||||||
|
highlightStrikethrough(textStorage: textStorage, lineRange: lineRange, palette: palette)
|
||||||
|
highlightFootnoteRefs(textStorage: textStorage, lineRange: lineRange, palette: palette)
|
||||||
lineStart = NSMaxRange(lineRange)
|
lineStart = NSMaxRange(lineRange)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
highlightCodeLine(line, lineRange: lineRange, textStorage: textStorage, syn: syn)
|
highlightCodeLine(line, lineRange: lineRange, textStorage: textStorage, syn: syn)
|
||||||
|
highlightInlineCode(textStorage: textStorage, lineRange: lineRange, palette: palette, baseFont: baseFont)
|
||||||
|
highlightStrikethrough(textStorage: textStorage, lineRange: lineRange, palette: palette)
|
||||||
|
highlightFootnoteRefs(textStorage: textStorage, lineRange: lineRange, palette: palette)
|
||||||
|
|
||||||
lineStart = NSMaxRange(lineRange)
|
lineStart = NSMaxRange(lineRange)
|
||||||
}
|
}
|
||||||
|
|
@ -181,7 +197,7 @@ func applySyntaxHighlighting(to textStorage: NSTextStorage) {
|
||||||
highlightBlockComments(textStorage: textStorage, syn: syn, baseFont: baseFont)
|
highlightBlockComments(textStorage: textStorage, syn: syn, baseFont: baseFont)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func highlightMarkdownLine(_ trimmed: String, lineRange: NSRange, textStorage: NSTextStorage, baseFont: NSFont, palette: CatppuccinPalette) -> Bool {
|
private func highlightMarkdownLine(_ trimmed: String, line: String, lineRange: NSRange, textStorage: NSTextStorage, baseFont: NSFont, palette: CatppuccinPalette, isTableHeader: Bool = false) -> Bool {
|
||||||
if trimmed.hasPrefix("## ") {
|
if trimmed.hasPrefix("## ") {
|
||||||
let hashRange = (textStorage.string as NSString).range(of: "##", range: lineRange)
|
let hashRange = (textStorage.string as NSString).range(of: "##", range: lineRange)
|
||||||
if hashRange.location != NSNotFound {
|
if hashRange.location != NSNotFound {
|
||||||
|
|
@ -219,6 +235,48 @@ private func highlightMarkdownLine(_ trimmed: String, lineRange: NSRange, textSt
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Horizontal rule
|
||||||
|
if isHorizontalRule(trimmed) {
|
||||||
|
textStorage.addAttribute(.foregroundColor, value: palette.overlay0, range: lineRange)
|
||||||
|
textStorage.addAttribute(.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: lineRange)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Footnote definition
|
||||||
|
if trimmed.hasPrefix("[^") && trimmed.contains("]:") {
|
||||||
|
highlightFootnoteDefinition(textStorage: textStorage, lineRange: lineRange, palette: palette)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Task list (check before generic unordered list)
|
||||||
|
if trimmed.hasPrefix("- [ ] ") || trimmed.hasPrefix("- [x] ") || trimmed.hasPrefix("- [X] ") ||
|
||||||
|
trimmed.hasPrefix("* [ ] ") || trimmed.hasPrefix("* [x] ") || trimmed.hasPrefix("* [X] ") ||
|
||||||
|
trimmed.hasPrefix("+ [ ] ") || trimmed.hasPrefix("+ [x] ") || trimmed.hasPrefix("+ [X] ") {
|
||||||
|
highlightTaskList(trimmed, lineRange: lineRange, line: line, textStorage: textStorage, palette: palette)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Table rows
|
||||||
|
if trimmed.hasPrefix("|") {
|
||||||
|
let isSep = isTableSeparator(trimmed)
|
||||||
|
highlightTableLine(trimmed, lineRange: lineRange, textStorage: textStorage, palette: palette, baseFont: baseFont, isHeader: isTableHeader, isSeparator: isSep)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unordered list
|
||||||
|
if let regex = try? NSRegularExpression(pattern: "^\\s*[-*+] "),
|
||||||
|
regex.firstMatch(in: line, range: NSRange(location: 0, length: (line as NSString).length)) != nil {
|
||||||
|
highlightUnorderedList(trimmed, lineRange: lineRange, line: line, textStorage: textStorage, palette: palette)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ordered list
|
||||||
|
if let regex = try? NSRegularExpression(pattern: "^\\s*\\d+\\. "),
|
||||||
|
regex.firstMatch(in: line, range: NSRange(location: 0, length: (line as NSString).length)) != nil {
|
||||||
|
highlightOrderedList(trimmed, lineRange: lineRange, line: line, textStorage: textStorage, palette: palette)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -372,6 +430,288 @@ private func highlightBlockComments(textStorage: NSTextStorage, syn: Theme.Synta
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Fenced Code Blocks
|
||||||
|
|
||||||
|
private func highlightFencedCodeBlocks(textStorage: NSTextStorage, palette: CatppuccinPalette, baseFont: NSFont) -> [NSRange] {
|
||||||
|
let text = textStorage.string
|
||||||
|
let nsText = text as NSString
|
||||||
|
let monoFont = NSFont.monospacedSystemFont(ofSize: baseFont.pointSize, weight: .regular)
|
||||||
|
var fencedRanges: [NSRange] = []
|
||||||
|
var lineStart = 0
|
||||||
|
var openFence: Int? = nil
|
||||||
|
|
||||||
|
while lineStart < nsText.length {
|
||||||
|
let lineRange = nsText.lineRange(for: NSRange(location: lineStart, length: 0))
|
||||||
|
let line = nsText.substring(with: lineRange)
|
||||||
|
let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
|
||||||
|
if openFence == nil {
|
||||||
|
if trimmed.hasPrefix("```") {
|
||||||
|
openFence = lineRange.location
|
||||||
|
// Mute the fence line
|
||||||
|
textStorage.addAttribute(.foregroundColor, value: palette.overlay0, range: lineRange)
|
||||||
|
textStorage.addAttribute(.font, value: monoFont, range: lineRange)
|
||||||
|
// Language identifier after ```
|
||||||
|
if trimmed.count > 3 {
|
||||||
|
let langStart = (nsText as NSString).range(of: "```", range: lineRange)
|
||||||
|
if langStart.location != NSNotFound {
|
||||||
|
let after = langStart.location + langStart.length
|
||||||
|
let langRange = NSRange(location: after, length: NSMaxRange(lineRange) - after)
|
||||||
|
if langRange.length > 0 {
|
||||||
|
textStorage.addAttribute(.foregroundColor, value: palette.overlay0, range: langRange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if trimmed == "```" {
|
||||||
|
// Close fence
|
||||||
|
textStorage.addAttribute(.foregroundColor, value: palette.overlay0, range: lineRange)
|
||||||
|
textStorage.addAttribute(.font, value: monoFont, range: lineRange)
|
||||||
|
let blockRange = NSRange(location: openFence!, length: NSMaxRange(lineRange) - openFence!)
|
||||||
|
fencedRanges.append(blockRange)
|
||||||
|
openFence = nil
|
||||||
|
} else {
|
||||||
|
// Content inside fenced block
|
||||||
|
textStorage.addAttribute(.font, value: monoFont, range: lineRange)
|
||||||
|
textStorage.addAttribute(.backgroundColor, value: palette.surface0, range: lineRange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lineStart = NSMaxRange(lineRange)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fencedRanges
|
||||||
|
}
|
||||||
|
|
||||||
|
private func isInsideFencedBlock(_ lineRange: NSRange, fencedRanges: [NSRange]) -> Bool {
|
||||||
|
for fenced in fencedRanges {
|
||||||
|
if lineRange.location >= fenced.location && NSMaxRange(lineRange) <= NSMaxRange(fenced) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Inline Code
|
||||||
|
|
||||||
|
private func highlightInlineCode(textStorage: NSTextStorage, lineRange: NSRange, palette: CatppuccinPalette, baseFont: NSFont) {
|
||||||
|
guard let regex = try? NSRegularExpression(pattern: "`([^`]+)`") else { return }
|
||||||
|
let monoFont = NSFont.monospacedSystemFont(ofSize: baseFont.pointSize, weight: .regular)
|
||||||
|
let matches = regex.matches(in: textStorage.string, range: lineRange)
|
||||||
|
for match in matches {
|
||||||
|
let fullRange = match.range
|
||||||
|
let openTick = NSRange(location: fullRange.location, length: 1)
|
||||||
|
let closeTick = NSRange(location: fullRange.location + fullRange.length - 1, length: 1)
|
||||||
|
let contentRange = NSRange(location: fullRange.location + 1, length: fullRange.length - 2)
|
||||||
|
textStorage.addAttribute(.foregroundColor, value: palette.overlay0, range: openTick)
|
||||||
|
textStorage.addAttribute(.foregroundColor, value: palette.overlay0, range: closeTick)
|
||||||
|
if contentRange.length > 0 {
|
||||||
|
textStorage.addAttribute(.font, value: monoFont, range: contentRange)
|
||||||
|
textStorage.addAttribute(.backgroundColor, value: palette.surface0, range: contentRange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Strikethrough
|
||||||
|
|
||||||
|
private func highlightStrikethrough(textStorage: NSTextStorage, lineRange: NSRange, palette: CatppuccinPalette) {
|
||||||
|
guard let regex = try? NSRegularExpression(pattern: "~~(.+?)~~") else { return }
|
||||||
|
let matches = regex.matches(in: textStorage.string, range: lineRange)
|
||||||
|
for match in matches {
|
||||||
|
let fullRange = match.range
|
||||||
|
let openMarker = NSRange(location: fullRange.location, length: 2)
|
||||||
|
let closeMarker = NSRange(location: fullRange.location + fullRange.length - 2, length: 2)
|
||||||
|
let contentRange = NSRange(location: fullRange.location + 2, length: fullRange.length - 4)
|
||||||
|
textStorage.addAttribute(.foregroundColor, value: palette.overlay0, range: openMarker)
|
||||||
|
textStorage.addAttribute(.foregroundColor, value: palette.overlay0, range: closeMarker)
|
||||||
|
if contentRange.length > 0 {
|
||||||
|
textStorage.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.single.rawValue, range: contentRange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Footnotes
|
||||||
|
|
||||||
|
private func highlightFootnoteRefs(textStorage: NSTextStorage, lineRange: NSRange, palette: CatppuccinPalette) {
|
||||||
|
// Inline refs: [^label] (but not definitions which start line with [^label]:)
|
||||||
|
guard let regex = try? NSRegularExpression(pattern: "\\[\\^[^\\]]+\\](?!:)") else { return }
|
||||||
|
let matches = regex.matches(in: textStorage.string, range: lineRange)
|
||||||
|
for match in matches {
|
||||||
|
textStorage.addAttribute(.foregroundColor, value: palette.blue, range: match.range)
|
||||||
|
textStorage.addAttribute(.superscript, value: 1, range: match.range)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func highlightFootnoteDefinition(textStorage: NSTextStorage, lineRange: NSRange, palette: CatppuccinPalette) {
|
||||||
|
guard let regex = try? NSRegularExpression(pattern: "^\\[\\^[^\\]]+\\]:") else { return }
|
||||||
|
let nsText = textStorage.string as NSString
|
||||||
|
let lineContent = nsText.substring(with: lineRange)
|
||||||
|
let localRange = NSRange(location: 0, length: (lineContent as NSString).length)
|
||||||
|
let matches = regex.matches(in: lineContent, range: localRange)
|
||||||
|
for match in matches {
|
||||||
|
let absRange = NSRange(location: lineRange.location + match.range.location, length: match.range.length)
|
||||||
|
textStorage.addAttribute(.foregroundColor, value: palette.lavender, range: absRange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Tables
|
||||||
|
|
||||||
|
private func highlightTableLine(_ trimmed: String, lineRange: NSRange, textStorage: NSTextStorage, palette: CatppuccinPalette, baseFont: NSFont, isHeader: Bool, isSeparator: Bool) {
|
||||||
|
let monoFont = NSFont.monospacedSystemFont(ofSize: baseFont.pointSize, weight: .regular)
|
||||||
|
let boldMono = NSFontManager.shared.convert(monoFont, toHaveTrait: .boldFontMask)
|
||||||
|
|
||||||
|
textStorage.addAttribute(.font, value: monoFont, range: lineRange)
|
||||||
|
|
||||||
|
if isSeparator {
|
||||||
|
textStorage.addAttribute(.foregroundColor, value: palette.overlay0, range: lineRange)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if isHeader {
|
||||||
|
textStorage.addAttribute(.font, value: boldMono, range: lineRange)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mute pipe delimiters
|
||||||
|
guard let pipeRegex = try? NSRegularExpression(pattern: "\\|") else { return }
|
||||||
|
let matches = pipeRegex.matches(in: textStorage.string, range: lineRange)
|
||||||
|
for match in matches {
|
||||||
|
textStorage.addAttribute(.foregroundColor, value: palette.overlay0, range: match.range)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Lists and Horizontal Rules
|
||||||
|
|
||||||
|
private func highlightOrderedList(_ trimmed: String, lineRange: NSRange, line: String, textStorage: NSTextStorage, palette: CatppuccinPalette) {
|
||||||
|
guard let regex = try? NSRegularExpression(pattern: "^(\\s*)(\\d+\\.)( )") else { return }
|
||||||
|
let nsLine = line as NSString
|
||||||
|
let localRange = NSRange(location: 0, length: nsLine.length)
|
||||||
|
guard let match = regex.firstMatch(in: line, range: localRange) else { return }
|
||||||
|
let markerRange = match.range(at: 2)
|
||||||
|
let absMarker = NSRange(location: lineRange.location + markerRange.location, length: markerRange.length)
|
||||||
|
textStorage.addAttribute(.foregroundColor, value: palette.blue, range: absMarker)
|
||||||
|
|
||||||
|
applyListIndent(line: line, lineRange: lineRange, textStorage: textStorage)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func highlightUnorderedList(_ trimmed: String, lineRange: NSRange, line: String, textStorage: NSTextStorage, palette: CatppuccinPalette) {
|
||||||
|
guard let regex = try? NSRegularExpression(pattern: "^(\\s*)([-*+])( )") else { return }
|
||||||
|
let nsLine = line as NSString
|
||||||
|
let localRange = NSRange(location: 0, length: nsLine.length)
|
||||||
|
guard let match = regex.firstMatch(in: line, range: localRange) else { return }
|
||||||
|
let bulletRange = match.range(at: 2)
|
||||||
|
let absBullet = NSRange(location: lineRange.location + bulletRange.location, length: bulletRange.length)
|
||||||
|
textStorage.addAttribute(.foregroundColor, value: palette.blue, range: absBullet)
|
||||||
|
|
||||||
|
applyListIndent(line: line, lineRange: lineRange, textStorage: textStorage)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func highlightTaskList(_ trimmed: String, lineRange: NSRange, line: String, textStorage: NSTextStorage, palette: CatppuccinPalette) {
|
||||||
|
let checked: Bool
|
||||||
|
if trimmed.hasPrefix("- [ ] ") || trimmed.hasPrefix("* [ ] ") || trimmed.hasPrefix("+ [ ] ") {
|
||||||
|
checked = false
|
||||||
|
} else if trimmed.hasPrefix("- [x] ") || trimmed.hasPrefix("* [x] ") || trimmed.hasPrefix("+ [x] ") ||
|
||||||
|
trimmed.hasPrefix("- [X] ") || trimmed.hasPrefix("* [X] ") || trimmed.hasPrefix("+ [X] ") {
|
||||||
|
checked = true
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the checkbox in the actual line
|
||||||
|
guard let cbRegex = try? NSRegularExpression(pattern: "\\[[ xX]\\]") else { return }
|
||||||
|
guard let cbMatch = cbRegex.firstMatch(in: textStorage.string, range: lineRange) else { return }
|
||||||
|
let cbRange = cbMatch.range
|
||||||
|
|
||||||
|
if checked {
|
||||||
|
textStorage.addAttribute(.foregroundColor, value: palette.green, range: cbRange)
|
||||||
|
let afterCb = NSRange(location: cbRange.location + cbRange.length, length: NSMaxRange(lineRange) - (cbRange.location + cbRange.length))
|
||||||
|
if afterCb.length > 0 {
|
||||||
|
textStorage.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.single.rawValue, range: afterCb)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
textStorage.addAttribute(.foregroundColor, value: palette.overlay0, range: cbRange)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bullet coloring
|
||||||
|
guard let bulletRegex = try? NSRegularExpression(pattern: "^(\\s*)([-*+])") else { return }
|
||||||
|
let nsLine = line as NSString
|
||||||
|
guard let bMatch = bulletRegex.firstMatch(in: line, range: NSRange(location: 0, length: nsLine.length)) else { return }
|
||||||
|
let bulletRange = bMatch.range(at: 2)
|
||||||
|
let absBullet = NSRange(location: lineRange.location + bulletRange.location, length: bulletRange.length)
|
||||||
|
textStorage.addAttribute(.foregroundColor, value: palette.blue, range: absBullet)
|
||||||
|
|
||||||
|
applyListIndent(line: line, lineRange: lineRange, textStorage: textStorage)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applyListIndent(line: String, lineRange: NSRange, textStorage: NSTextStorage) {
|
||||||
|
let leading = line.prefix(while: { $0 == " " || $0 == "\t" })
|
||||||
|
let spaces = leading.filter { $0 == " " }.count
|
||||||
|
let tabs = leading.filter { $0 == "\t" }.count
|
||||||
|
let level = tabs + spaces / 2
|
||||||
|
if level > 0 {
|
||||||
|
let indent = NSMutableParagraphStyle()
|
||||||
|
let px = CGFloat(level) * 20.0
|
||||||
|
indent.headIndent = px
|
||||||
|
indent.firstLineHeadIndent = px
|
||||||
|
textStorage.addAttribute(.paragraphStyle, value: indent, range: lineRange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func isHorizontalRule(_ trimmed: String) -> Bool {
|
||||||
|
if trimmed.isEmpty { return false }
|
||||||
|
let stripped = trimmed.replacingOccurrences(of: " ", with: "")
|
||||||
|
if stripped.count < 3 { return false }
|
||||||
|
let allDash = stripped.allSatisfy { $0 == "-" }
|
||||||
|
let allStar = stripped.allSatisfy { $0 == "*" }
|
||||||
|
let allUnderscore = stripped.allSatisfy { $0 == "_" }
|
||||||
|
return allDash || allStar || allUnderscore
|
||||||
|
}
|
||||||
|
|
||||||
|
private func findTableHeaderLines(textStorage: NSTextStorage, fencedRanges: [NSRange]) -> Set<Int> {
|
||||||
|
var headerStarts = Set<Int>()
|
||||||
|
let nsText = textStorage.string as NSString
|
||||||
|
var lineStart = 0
|
||||||
|
var prevLineStart: Int? = nil
|
||||||
|
var prevTrimmed: String? = nil
|
||||||
|
|
||||||
|
while lineStart < nsText.length {
|
||||||
|
let lineRange = nsText.lineRange(for: NSRange(location: lineStart, length: 0))
|
||||||
|
|
||||||
|
if !isInsideFencedBlock(lineRange, fencedRanges: fencedRanges) {
|
||||||
|
let line = nsText.substring(with: lineRange)
|
||||||
|
let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
|
||||||
|
if trimmed.hasPrefix("|"), isTableSeparator(trimmed),
|
||||||
|
let pStart = prevLineStart, let pTrimmed = prevTrimmed,
|
||||||
|
pTrimmed.hasPrefix("|") {
|
||||||
|
headerStarts.insert(pStart)
|
||||||
|
}
|
||||||
|
|
||||||
|
prevLineStart = lineRange.location
|
||||||
|
prevTrimmed = trimmed
|
||||||
|
} else {
|
||||||
|
prevLineStart = nil
|
||||||
|
prevTrimmed = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lineStart = NSMaxRange(lineRange)
|
||||||
|
}
|
||||||
|
|
||||||
|
return headerStarts
|
||||||
|
}
|
||||||
|
|
||||||
|
private func isTableSeparator(_ trimmed: String) -> Bool {
|
||||||
|
guard trimmed.hasPrefix("|") else { return false }
|
||||||
|
let inner = trimmed.trimmingCharacters(in: CharacterSet(charactersIn: "| "))
|
||||||
|
guard !inner.isEmpty else { return false }
|
||||||
|
let cells = inner.components(separatedBy: "|")
|
||||||
|
return cells.allSatisfy { cell in
|
||||||
|
let c = cell.trimmingCharacters(in: .whitespaces)
|
||||||
|
guard let regex = try? NSRegularExpression(pattern: "^:?-{1,}:?$") else { return false }
|
||||||
|
return regex.firstMatch(in: c, range: NSRange(location: 0, length: (c as NSString).length)) != nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - LineNumberTextView
|
// MARK: - LineNumberTextView
|
||||||
|
|
||||||
class LineNumberTextView: NSTextView {
|
class LineNumberTextView: NSTextView {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue