Files
Puranjay Savar Mattas 6e1ffb96fd chore: vendor swift-markdown-engine as plain tracked source
Convert from git submodule to plain vendored copy. No push access to
upstream nodes-app/swift-markdown-engine meant our local fix commit
(05c1720) was stranded and unreachable from any remote on fresh
clones/CI. Vendoring as plain files folds it into normal repo history
instead.
2026-08-20 13:26:13 +01:00

171 lines
6.8 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// MarkdownDetection.swift
// MarkdownEngine
//
// Created by Luca Chen on 18.02.26.
//
// Helper checks for questions like "is the cursor inside code or LaTeX?"
// and "which Markdown part is currently active?".
import Foundation
enum MarkdownDetection {
// MARK: - Active Token Indices
static func computeActiveTokenIndices(
selectionRange: NSRange,
tokens: [MarkdownToken],
in text: NSString,
suppressed: Bool = false
) -> Set<Int> {
// Read-only mode (no caret) hides all tokens regardless of any trailing selection.
if suppressed { return [] }
var indices: Set<Int> = []
let caretLocation = selectionRange.location
for (index, token) in tokens.enumerated() {
let start = token.range.location
let end = NSMaxRange(token.range)
if selectionRange.length > 0 && (token.kind == .inlineLatex || token.kind == .blockLatex) && NSIntersectionRange(selectionRange, token.range).length > 0 {
indices.insert(index)
continue
}
if caretLocation >= start && caretLocation < end {
indices.insert(index)
continue
}
if caretLocation == end {
let lastIndex = end - 1
if lastIndex >= start && lastIndex < text.length {
let lastChar = text.substring(with: NSRange(location: lastIndex, length: 1))
if lastChar != "\n" {
indices.insert(index)
}
}
}
}
// When a container token (e.g. a table) is active, every inline token inside it becomes active too.
let activeContainers: [MarkdownToken] = indices.compactMap { idx in
let token = tokens[idx]
return token.kind == .table ? token : nil
}
if !activeContainers.isEmpty {
for (i, token) in tokens.enumerated() where !indices.contains(i) {
let tStart = token.range.location
let tEnd = NSMaxRange(token.range)
if activeContainers.contains(where: {
tStart >= $0.range.location && tEnd <= NSMaxRange($0.range)
}) {
indices.insert(i)
}
}
}
return indices
}
// MARK: - Code Block Detection
/// Slow: parses tokens each call. Pass the editor's registry so the parse
/// matches the styled document's grammar (an extension span can pre-claim
/// text a built-in would otherwise recognize).
static func isInsideCodeBlock(range: NSRange, in text: String, registry: ExtensionRegistry = .empty) -> Bool {
let codeTokens = MarkdownTokenizer.parseTokensViaAST(in: text, registry: registry).filter { $0.kind == .codeBlock || $0.kind == .inlineCode }
return isInsideCodeBlock(range: range, codeTokens: codeTokens)
}
static func isInsideCodeBlock(location: Int, in text: String, registry: ExtensionRegistry = .empty) -> Bool {
isInsideCodeBlock(range: NSRange(location: location, length: 0), in: text, registry: registry)
}
/// Fast: uses pre-parsed tokens
static func isInsideCodeBlock(range: NSRange, codeTokens: [MarkdownToken]) -> Bool {
guard !codeTokens.isEmpty else { return false }
for token in codeTokens {
let start = token.range.location
let end = start + token.range.length
if range.length == 0 {
if range.location >= start && range.location <= end { return true }
} else {
if range.location < end && range.location + range.length > start { return true }
}
}
return false
}
static func isInsideCodeBlock(location: Int, codeTokens: [MarkdownToken]) -> Bool {
isInsideCodeBlock(range: NSRange(location: location, length: 0), codeTokens: codeTokens)
}
/// Count of non-overlapping ``` occurrences, scanning left to right —
/// exactly `components(separatedBy: "```").count - 1`, but as one UTF-16
/// pass with no substring-array allocation.
static func tripleBacktickCount(in text: NSString) -> Int {
let length = text.length
guard length >= 3 else { return 0 }
var buffer = [unichar](repeating: 0, count: length)
text.getCharacters(&buffer, range: NSRange(location: 0, length: length))
var count = 0
var i = 0
while i + 2 < length { // i can reach length - 3
if buffer[i] == 0x60, buffer[i + 1] == 0x60, buffer[i + 2] == 0x60 {
count += 1
i += 3
} else {
i += 1
}
}
return count
}
/// The ``` count contributed by the backtick runs that intersect `range`.
/// The window expands through adjacent backticks on both sides, so every
/// run inside it is a MAXIMAL run of the whole text — and the greedy global
/// count is exactly Σ floor(runLen/3) over maximal runs, which makes these
/// window counts composable: full = fullBefore windowBefore + windowAfter.
static func backtickWindowCount(in text: NSString, around range: NSRange) -> Int {
let length = text.length
guard range.location >= 0, NSMaxRange(range) <= length else { return 0 }
var lo = range.location
while lo > 0, text.character(at: lo - 1) == 0x60 { lo -= 1 }
var hi = NSMaxRange(range)
while hi < length, text.character(at: hi) == 0x60 { hi += 1 }
var count = 0
var run = 0
var i = lo
while i < hi {
if text.character(at: i) == 0x60 {
run += 1
} else {
count += run / 3
run = 0
}
i += 1
}
return count + run / 3
}
// MARK: - LaTeX Detection
/// Slow: parses tokens each call. The registry matters here: a registered
/// extension (e.g. `==$==$`) can claim characters that would otherwise
/// pair into a phantom `$…$`, so parsing with `.empty` diverges from the
/// styled document.
static func isInsideLatex(location: Int, in text: String, registry: ExtensionRegistry = .empty) -> Bool {
let tokens = MarkdownTokenizer.parseTokensViaAST(in: text, registry: registry)
let latexTokens = tokens.filter { $0.kind == .inlineLatex || $0.kind == .blockLatex }
return isInsideLatex(location: location, latexTokens: latexTokens)
}
static func isInsideLatex(location: Int, latexTokens: [MarkdownToken]) -> Bool {
guard !latexTokens.isEmpty else { return false }
for token in latexTokens {
let start = token.range.location
let end = start + token.range.length
if location >= start && location <= end { return true }
}
return false
}
}