Files
i7t5--edmund/Sources/EdmundCore/Parsing/SyntaxHighlighter+WalkerInline.swift
T
wehub-resource-sync 5b99bf6bca
CI / test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:34:54 +08:00

203 lines
7.8 KiB
Swift

import Foundation
import Markdown
// Inline-construct visitors for SpanCollector: emphasis/strong (including the
// `***boldItalic***` nesting both cmark orderings produce), inline code,
// strikethrough, links, and images. The struct, its stored state, and the
// shared source-offset/delimiter helpers live in SyntaxHighlighter+Walker;
// block-level visitors (headings, code blocks, quotes, tables, lists) stay there
// too.
extension SyntaxHighlighter.SpanCollector {
mutating func visitEmphasis(_ emphasis: Emphasis) {
guard let range = emphasis.range else {
descendInto(emphasis)
return
}
let full = nsRange(for: range)
if insideStrong {
// Already inside Strong — parent will have emitted boldItalic
// or we're a nested emphasis. Just descend.
descendInto(emphasis)
return
}
// Check for ***...***: Emphasis wrapping a single Strong child
// with the same source range.
if emphasis.childCount == 1,
let strong = emphasis.children.first(where: { $0 is Strong }) as? Strong,
let strongRange = strong.range {
let strongNS = nsRange(for: strongRange)
if strongNS == full {
// This is boldItalic. Compute delimiters from the Strong's children
// (the text nodes inside), not from the Emphasis's children (the Strong).
let rawDelims = delimiterRanges(parent: full, children: strong.children)
let (trimmedFull, delims) = trimEmphasisDelimiters(
expectedWidth: 3, full: full, delims: rawDelims)
let content = contentRange(full: trimmedFull, delims: delims)
spans.append(SyntaxHighlighter.Span(
kind: .boldItalic,
fullRange: trimmedFull,
contentRange: content,
delimiterRanges: delims
))
// Don't descend — we've handled the whole subtree
return
}
}
// Regular italic
let rawDelims = delimiterRanges(parent: full, children: emphasis.children)
let (trimmedFull, delims) = trimEmphasisDelimiters(
expectedWidth: 1, full: full, delims: rawDelims)
let content = contentRange(full: trimmedFull, delims: delims)
spans.append(SyntaxHighlighter.Span(
kind: .italic,
fullRange: trimmedFull,
contentRange: content,
delimiterRanges: delims
))
insideEmphasis = true
descendInto(emphasis)
insideEmphasis = false
}
mutating func visitStrong(_ strong: Strong) {
guard let range = strong.range else {
descendInto(strong)
return
}
let full = nsRange(for: range)
if insideEmphasis {
// Already inside Emphasis — parent will have emitted boldItalic
// or we're nested. Just descend.
descendInto(strong)
return
}
// Check for ***...***: Strong wrapping a single Emphasis child
// with the same source range. (cmark can produce either nesting order.)
if strong.childCount == 1,
let emph = strong.children.first(where: { $0 is Emphasis }) as? Emphasis,
let emphRange = emph.range {
let emphNS = nsRange(for: emphRange)
if emphNS == full {
let rawDelims = delimiterRanges(parent: full, children: emph.children)
let (trimmedFull, delims) = trimEmphasisDelimiters(
expectedWidth: 3, full: full, delims: rawDelims)
let content = contentRange(full: trimmedFull, delims: delims)
spans.append(SyntaxHighlighter.Span(
kind: .boldItalic,
fullRange: trimmedFull,
contentRange: content,
delimiterRanges: delims
))
return
}
}
// Regular bold
let rawDelims = delimiterRanges(parent: full, children: strong.children)
let (trimmedFull, delims) = trimEmphasisDelimiters(
expectedWidth: 2, full: full, delims: rawDelims)
let content = contentRange(full: trimmedFull, delims: delims)
spans.append(SyntaxHighlighter.Span(
kind: .bold,
fullRange: trimmedFull,
contentRange: content,
delimiterRanges: delims
))
insideStrong = true
descendInto(strong)
insideStrong = false
}
mutating func visitInlineCode(_ code: InlineCode) {
guard let range = code.range else { return }
let full = nsRange(for: range)
guard full.length >= 2 else { return }
// GFM §6.3: the delimiters are equal-length backtick runs of ANY length.
// Measure the actual runs in the raw source (the AST doesn't carry them).
let ns = source as NSString
let backtick: unichar = 0x60
var open = 0
while full.location + open < full.upperBound,
ns.character(at: full.location + open) == backtick { open += 1 }
var close = 0
while full.upperBound - 1 - close > full.location + open - 1,
ns.character(at: full.upperBound - 1 - close) == backtick { close += 1 }
// cmark guarantees matching runs; clamp defensively so a surprise can't
// produce inverted ranges.
let d = max(1, min(min(open, close), full.length / 2))
// NOTE: the §6.3 one-space strip (`` ` `` → "`") is a render rule; in
// edit mode the padding spaces are source and stay in contentRange.
spans.append(SyntaxHighlighter.Span(
kind: .code,
fullRange: full,
contentRange: NSRange(location: full.location + d, length: max(0, full.length - 2 * d)),
delimiterRanges: [NSRange(location: full.location, length: d),
NSRange(location: full.upperBound - d, length: d)]
))
}
mutating func visitStrikethrough(_ strikethrough: Strikethrough) {
guard let range = strikethrough.range else {
descendInto(strikethrough)
return
}
let full = nsRange(for: range)
let delims = delimiterRanges(parent: full, children: strikethrough.children)
let content = contentRange(full: full, delims: delims)
spans.append(SyntaxHighlighter.Span(
kind: .strikethrough,
fullRange: full,
contentRange: content,
delimiterRanges: delims
))
descendInto(strikethrough)
}
mutating func visitLink(_ link: Link) {
guard let range = link.range else {
descendInto(link)
return
}
let full = nsRange(for: range)
let delims = delimiterRanges(parent: full, children: link.children)
let content = contentRange(full: full, delims: delims)
spans.append(SyntaxHighlighter.Span(
kind: .link(destination: link.destination ?? ""),
fullRange: full,
contentRange: content,
delimiterRanges: delims
))
descendInto(link)
}
mutating func visitImage(_ image: Image) {
guard let range = image.range else {
descendInto(image)
return
}
let full = nsRange(for: range)
let delims = delimiterRanges(parent: full, children: image.children)
let content = contentRange(full: full, delims: delims)
spans.append(SyntaxHighlighter.Span(
kind: .image(destination: image.source ?? "", width: nil, height: nil),
fullRange: full,
contentRange: content,
delimiterRanges: delims
))
descendInto(image)
}
}