Files
i7t5--edmund/Sources/EdmundCore/Parsing/CodeHighlighter.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

136 lines
6.3 KiB
Swift

import Foundation
// MARK: - Code Highlighter
//
// A small, language-agnostic tokenizer for fenced code blocks. It is not a full
// parser — it recognizes the lexical tokens that carry most of the visual
// signal (comments, strings, numbers, keywords, types, function calls) across
// the common C-family / script languages. The block's info string picks the
// comment style (`#` vs `//`), everything else is shared.
enum CodeHighlighter {
enum TokenType: Equatable {
case keyword, type, string, number, comment, function
}
struct Token: Equatable {
let range: NSRange
let type: TokenType
}
/// Languages whose line comments start with `#` rather than `//`.
private static let hashCommentLanguages: Set<String> = [
"python", "py", "ruby", "rb", "sh", "bash", "shell", "zsh", "fish",
"yaml", "yml", "toml", "ini", "perl", "pl", "r", "makefile", "make",
"dockerfile", "docker", "elixir", "ex", "nim", "julia", "jl", "tcl",
]
/// A union of frequent keywords across popular languages. Over-inclusive is
/// fine: a keyword that doesn't apply to the current language simply won't
/// appear in its code.
private static let keywords: Set<String> = [
// declarations / control flow (shared across many languages)
"func", "function", "fn", "def", "let", "var", "val", "const", "static",
"final", "class", "struct", "enum", "interface", "protocol", "trait",
"impl", "extends", "implements", "namespace", "package", "module", "mod",
"import", "export", "from", "use", "using", "include", "require",
"public", "private", "protected", "internal", "fileprivate", "open",
"if", "else", "elif", "for", "while", "do", "switch", "case", "default",
"break", "continue", "return", "yield", "goto", "match", "when", "where",
"try", "catch", "except", "finally", "throw", "throws", "raise", "rescue",
"guard", "defer", "async", "await", "go", "chan", "select", "with", "as",
"is", "in", "of", "new", "delete", "typeof", "instanceof", "sizeof",
"void", "int", "long", "short", "char", "float", "double", "bool",
"boolean", "string", "unsigned", "signed", "auto", "typedef", "template",
"virtual", "override", "abstract", "extension", "init", "self", "this",
"super", "nil", "null", "none", "undefined", "true", "false", "and",
"or", "not", "lambda", "pass", "global", "nonlocal", "mut", "pub", "dyn",
"type", "object", "end", "begin", "then", "elsif", "unless", "until",
]
static func tokenize(_ code: String, language: String?) -> [Token] {
let ns = code as NSString
let n = ns.length
guard n > 0 else { return [] }
let lang = language?
.trimmingCharacters(in: .whitespaces).lowercased() ?? ""
let hashComments = hashCommentLanguages.contains(lang)
func isIdentStart(_ c: unichar) -> Bool {
(c >= 65 && c <= 90) || (c >= 97 && c <= 122) || c == 95
}
func isIdentChar(_ c: unichar) -> Bool { isIdentStart(c) || (c >= 48 && c <= 57) }
func isDigit(_ c: unichar) -> Bool { c >= 48 && c <= 57 }
var tokens: [Token] = []
var i = 0
while i < n {
let c = ns.character(at: i)
// Line comment: `//` always, `#` for hash-comment languages.
if (c == 0x2F && i + 1 < n && ns.character(at: i + 1) == 0x2F)
|| (c == 0x23 && hashComments) {
let start = i
while i < n && ns.character(at: i) != 0x0A { i += 1 }
tokens.append(Token(range: NSRange(location: start, length: i - start), type: .comment))
continue
}
// Block comment `/* … */`.
if c == 0x2F && i + 1 < n && ns.character(at: i + 1) == 0x2A {
let start = i; i += 2
while i + 1 < n && !(ns.character(at: i) == 0x2A && ns.character(at: i + 1) == 0x2F) { i += 1 }
i = min(n, i + 2)
tokens.append(Token(range: NSRange(location: start, length: i - start), type: .comment))
continue
}
// String: "…", '…', `…` with backslash escapes; stops at end of line.
if c == 0x22 || c == 0x27 || c == 0x60 {
let quote = c, start = i; i += 1
while i < n {
let d = ns.character(at: i)
if d == 0x5C { i += 2; continue } // escape
if d == quote { i += 1; break }
if d == 0x0A { break }
i += 1
}
tokens.append(Token(range: NSRange(location: start, length: min(i, n) - start), type: .string))
continue
}
// Number (incl. trailing hex/exponent/dot chars).
if isDigit(c) {
let start = i; i += 1
while i < n {
let d = ns.character(at: i)
if isIdentChar(d) || d == 0x2E { i += 1 } else { break }
}
tokens.append(Token(range: NSRange(location: start, length: i - start), type: .number))
continue
}
// Identifier → keyword / type / function call / plain.
if isIdentStart(c) {
let start = i; i += 1
while i < n && isIdentChar(ns.character(at: i)) { i += 1 }
let word = ns.substring(with: NSRange(location: start, length: i - start))
let range = NSRange(location: start, length: i - start)
if keywords.contains(word) {
tokens.append(Token(range: range, type: .keyword))
} else if let first = word.unicodeScalars.first, first.properties.isUppercase {
tokens.append(Token(range: range, type: .type))
} else {
// Function call: identifier immediately followed by `(`.
var j = i
while j < n && ns.character(at: j) == 0x20 { j += 1 }
if j < n && ns.character(at: j) == 0x28 {
tokens.append(Token(range: range, type: .function))
}
}
continue
}
i += 1
}
return tokens
}
}