// // SyntaxHighlighter.swift // Zyquo Cloud // // Author: Simon-Pierre Boucher // Mail: contact@spboucher.ai // // Lightweight scanner-based syntax highlighter for code blocks. Supports the // Phase 4 language set (Swift, Python, JS/TS, JSON, HTML, CSS, Bash, SQL, Go, // Rust, C/C++/Obj-C). Token colors are code-specific design tokens defined in // `CodeTheme` with the same dynamic light/dark pattern as `ZyquoColor`, // referencing existing semantic tokens where they fit. Unknown languages fall // back to plain text in the base color. // import SwiftUI // MARK: - Code theme /// Semantic colors for code tokens. Dynamic (light/dark) and coherent with the /// app palette: indigo/sky family for keywords, success-green strings, tertiary /// gray comments, amber numbers. struct CodeTheme { let keyword: Color let string: Color let comment: Color let number: Color let type: Color let functionCall: Color let property: Color let attribute: Color /// The default Zyquo Cloud code theme. static let zyquo = CodeTheme( keyword: ZyquoColor.accent, string: ZyquoColor.success, comment: ZyquoColor.textTertiary, number: dynamic(light: 0xB26A0B, dark: 0xE0A458), type: dynamic(light: 0x2380C2, dark: 0x62B7F0), functionCall: dynamic(light: 0x6E4FD4, dark: 0xA48CF2), property: dynamic(light: 0x2F6FBF, dark: 0x7FB4E8), attribute: ZyquoColor.warning ) /// Same dynamic-color pattern as `ZyquoColor` (resolved per appearance). private static func dynamic(light: UInt32, dark: UInt32) -> Color { Color(nsColor: NSColor(name: nil) { appearance in let hex = appearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua ? dark : light return NSColor(hex: hex) }) } } // MARK: - Highlighter enum SyntaxHighlighter { /// The active code theme. static let theme = CodeTheme.zyquo /// Highlights `code` for `language`, returning an `AttributedString` whose /// text is byte-for-byte identical to the input. Plain (unclassified) text /// is colored with `baseColor`. Unknown or nil languages return the whole /// string in `baseColor`. Results are memoized (streaming re-renders hit /// the cache for every already-completed block). static func highlight(_ code: String, language: String?, baseColor: Color) -> AttributedString { guard !code.isEmpty else { return AttributedString() } guard let profile = profile(for: language) else { var plain = AttributedString(code) plain.foregroundColor = baseColor return plain } let key = CacheKey( textHash: code.hashValue, length: code.count, language: language?.lowercased() ?? "", base: String(describing: baseColor) ) cacheLock.lock() if let hit = cache[key] { cacheLock.unlock() return hit } cacheLock.unlock() let result = tokenize(code, profile: profile, baseColor: baseColor) cacheLock.lock() if cache.count > cacheCapacity { cache.removeAll(keepingCapacity: true) } cache[key] = result cacheLock.unlock() return result } // MARK: Cache private struct CacheKey: Hashable { let textHash: Int let length: Int let language: String let base: String } private static let cacheLock = NSLock() private static let cacheCapacity = 128 private static var cache: [CacheKey: AttributedString] = [:] // MARK: Scanner private static func tokenize(_ code: String, profile: LanguageProfile, baseColor: Color) -> AttributedString { let chars = Array(code) var result = AttributedString() var i = 0 var pendingPlainStart = 0 var previousSignificant: Character? func matches(_ marker: [Character], at index: Int) -> Bool { guard index + marker.count <= chars.count else { return false } for (offset, ch) in marker.enumerated() where chars[index + offset] != ch { return false } return true } func nextSignificant(after index: Int) -> Character? { var j = index while j < chars.count, chars[j] == " " || chars[j] == "\t" { j += 1 } return j < chars.count ? chars[j] : nil } func flushPlain(upTo end: Int) { guard end > pendingPlainStart else { return } var segment = AttributedString(String(chars[pendingPlainStart ..< end])) segment.foregroundColor = baseColor result += segment pendingPlainStart = end } func emit(_ start: Int, _ end: Int, _ color: Color) { flushPlain(upTo: start) var segment = AttributedString(String(chars[start ..< end])) segment.foregroundColor = color result += segment pendingPlainStart = end } func isIdentifierStart(_ c: Character) -> Bool { c.isLetter || c == "_" || profile.identifierExtras.contains(c) } func isIdentifierBody(_ c: Character) -> Bool { c.isLetter || c.isNumber || c == "_" || profile.identifierExtras.contains(c) } while i < chars.count { let c = chars[i] // Block comments (unterminated ones run to EOF — streaming safe). if let block = profile.blockComments.first(where: { matches($0.open, at: i) }) { let start = i i += block.open.count while i < chars.count, !matches(block.close, at: i) { i += 1 } if i < chars.count { i += block.close.count } emit(start, i, theme.comment) previousSignificant = nil continue } // Line comments. if let line = profile.lineComments.first(where: { matches($0, at: i) }) { let start = i i += line.count while i < chars.count, chars[i] != "\n" { i += 1 } emit(start, i, theme.comment) previousSignificant = nil continue } // Strings (with backslash escapes; triple quotes for Python-style). if profile.stringDelimiters.contains(c) { let start = i let triple = [c, c, c] if matches(triple, at: i) { i += 3 while i < chars.count, !matches(triple, at: i) { i += 1 } if i < chars.count { i += 3 } } else { i += 1 while i < chars.count { if chars[i] == "\\" { i += 2; continue } if chars[i] == c { i += 1; break } i += 1 } i = min(i, chars.count) } let isKey = profile.stringKeyAsProperty && nextSignificant(after: i) == ":" emit(start, i, isKey ? theme.property : theme.string) previousSignificant = c continue } // Numbers (plus #hex colors for CSS). if c.isNumber || (profile.hashIsNumberPrefix && c == "#" && i + 1 < chars.count && chars[i + 1].isHexDigit) { let start = i i += 1 while i < chars.count, chars[i].isLetter || chars[i].isNumber || chars[i] == "." || chars[i] == "_" { i += 1 } emit(start, i, theme.number) previousSignificant = chars[i - 1] continue } // Attributes / decorators / directives (@escaping, #include, $VAR…). if profile.attributePrefixes.contains(c), i + 1 < chars.count, isIdentifierStart(chars[i + 1]) { let start = i i += 1 while i < chars.count, isIdentifierBody(chars[i]) { i += 1 } emit(start, i, theme.attribute) previousSignificant = chars[i - 1] continue } // Identifiers: keywords, types, calls, properties. if isIdentifierStart(c) { let start = i while i < chars.count, isIdentifierBody(chars[i]) { i += 1 } let word = String(chars[start ..< i]) let lookup = profile.caseInsensitiveKeywords ? word.lowercased() : word let next = nextSignificant(after: i) var color: Color? if profile.keywords.contains(lookup) { color = theme.keyword } else if profile.isMarkup { if let prev = previousSignificant, prev == "<" || prev == "/" || prev == "!" { color = theme.keyword // tag name } else if next == "=" { color = theme.property // tag attribute } } else if previousSignificant == "." { color = theme.property } else if next == "(" { color = theme.functionCall } else if let first = word.first, first.isUppercase { color = theme.type } else if profile.colonMeansProperty, next == ":" { color = theme.property } if let color { emit(start, i, color) } previousSignificant = chars[i - 1] continue } if !c.isWhitespace { previousSignificant = c } i += 1 } flushPlain(upTo: chars.count) return result } // MARK: Language profiles private struct LanguageProfile { var keywords: Set = [] var lineComments: [[Character]] = [] var blockComments: [(open: [Character], close: [Character])] = [] var stringDelimiters: Set = ["\""] var identifierExtras: Set = [] var attributePrefixes: Set = [] var caseInsensitiveKeywords = false var colonMeansProperty = false var hashIsNumberPrefix = false var stringKeyAsProperty = false var isMarkup = false } private static func profile(for language: String?) -> LanguageProfile? { guard let language else { return nil } let normalized = language.trimmingCharacters(in: .whitespaces).lowercased() return profiles[normalized] } private static let profiles: [String: LanguageProfile] = { let slashLine: [[Character]] = [Array("//")] let cBlock: [(open: [Character], close: [Character])] = [(Array("/*"), Array("*/"))] var table: [String: LanguageProfile] = [:] let swift = LanguageProfile( keywords: [ "func", "let", "var", "if", "else", "guard", "switch", "case", "default", "for", "while", "repeat", "in", "return", "import", "struct", "class", "enum", "protocol", "extension", "where", "as", "is", "try", "catch", "throw", "throws", "rethrows", "async", "await", "actor", "init", "deinit", "self", "Self", "super", "nil", "true", "false", "public", "private", "internal", "fileprivate", "open", "static", "final", "lazy", "weak", "unowned", "mutating", "nonmutating", "override", "defer", "typealias", "associatedtype", "some", "any", "break", "continue", "fallthrough", "do", "get", "set", "willSet", "didSet", "inout", "subscript", "operator", "indirect", "convenience", "required", "optional", "dynamic", ], lineComments: slashLine, blockComments: cBlock, attributePrefixes: ["@", "#"] ) table["swift"] = swift let python = LanguageProfile( keywords: [ "def", "class", "if", "elif", "else", "for", "while", "in", "return", "import", "from", "as", "with", "try", "except", "finally", "raise", "lambda", "pass", "break", "continue", "global", "nonlocal", "yield", "assert", "del", "not", "and", "or", "is", "None", "True", "False", "async", "await", "match", "case", "self", ], lineComments: [Array("#")], stringDelimiters: ["\"", "'"], attributePrefixes: ["@"] ) for alias in ["python", "py", "python3"] { table[alias] = python } let jsTs = LanguageProfile( keywords: [ "function", "const", "let", "var", "if", "else", "for", "while", "do", "switch", "case", "default", "return", "break", "continue", "new", "delete", "typeof", "instanceof", "in", "of", "class", "extends", "super", "this", "import", "export", "from", "as", "async", "await", "yield", "try", "catch", "finally", "throw", "void", "null", "undefined", "true", "false", "static", "get", "set", "interface", "type", "enum", "implements", "declare", "readonly", "namespace", "public", "private", "protected", "abstract", "satisfies", "keyof", "infer", "never", "unknown", "any", "string", "number", "boolean", "object", "symbol", "bigint", ], lineComments: slashLine, blockComments: cBlock, stringDelimiters: ["\"", "'", "`"], identifierExtras: ["$"], attributePrefixes: ["@"] ) for alias in ["javascript", "js", "jsx", "typescript", "ts", "tsx"] { table[alias] = jsTs } let json = LanguageProfile( keywords: ["true", "false", "null"], lineComments: slashLine, blockComments: cBlock, stringKeyAsProperty: true ) table["json"] = json table["jsonc"] = json let html = LanguageProfile( blockComments: [(Array(""))], stringDelimiters: ["\"", "'"], identifierExtras: ["-"], isMarkup: true ) for alias in ["html", "xml", "svg", "xhtml"] { table[alias] = html } let css = LanguageProfile( keywords: ["important", "inherit", "initial", "unset", "auto", "none", "revert"], blockComments: cBlock, stringDelimiters: ["\"", "'"], identifierExtras: ["-"], attributePrefixes: ["@"], colonMeansProperty: true, hashIsNumberPrefix: true ) for alias in ["css", "scss", "less"] { table[alias] = css } let bash = LanguageProfile( keywords: [ "if", "then", "else", "elif", "fi", "for", "while", "until", "do", "done", "case", "esac", "function", "in", "select", "time", "coproc", "echo", "cd", "export", "local", "return", "exit", "read", "set", "unset", "shift", "source", "alias", "eval", "exec", "printf", "test", "true", "false", "sudo", "trap", "declare", ], lineComments: [Array("#")], stringDelimiters: ["\"", "'"], identifierExtras: ["-"], attributePrefixes: ["$"] ) for alias in ["bash", "sh", "zsh", "shell", "console"] { table[alias] = bash } let sql = LanguageProfile( keywords: [ "select", "from", "where", "insert", "into", "values", "update", "delete", "set", "create", "table", "drop", "alter", "index", "view", "join", "inner", "left", "right", "outer", "full", "cross", "on", "as", "and", "or", "not", "null", "primary", "key", "foreign", "references", "group", "by", "order", "having", "limit", "offset", "distinct", "union", "all", "exists", "between", "like", "in", "is", "case", "when", "then", "else", "end", "count", "sum", "avg", "min", "max", "desc", "asc", "with", "constraint", "unique", "default", "begin", "commit", "rollback", "transaction", ], lineComments: [Array("--")], blockComments: cBlock, stringDelimiters: ["'", "\""], caseInsensitiveKeywords: true ) table["sql"] = sql let go = LanguageProfile( keywords: [ "func", "package", "import", "var", "const", "type", "struct", "interface", "map", "chan", "go", "defer", "if", "else", "for", "range", "switch", "case", "default", "return", "break", "continue", "fallthrough", "select", "goto", "true", "false", "nil", "iota", "make", "new", "len", "cap", "append", "copy", "delete", "panic", "recover", "error", "string", "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64", "bool", "byte", "rune", "float32", "float64", "complex64", "complex128", "any", ], lineComments: slashLine, blockComments: cBlock, stringDelimiters: ["\"", "'", "`"] ) table["go"] = go table["golang"] = go let rust = LanguageProfile( keywords: [ "fn", "let", "mut", "const", "static", "if", "else", "match", "for", "while", "loop", "in", "return", "break", "continue", "struct", "enum", "trait", "impl", "pub", "use", "mod", "crate", "self", "Self", "super", "where", "as", "ref", "move", "async", "await", "dyn", "unsafe", "extern", "type", "true", "false", "Some", "None", "Ok", "Err", "String", "str", "i8", "i16", "i32", "i64", "i128", "u8", "u16", "u32", "u64", "u128", "f32", "f64", "usize", "isize", "bool", "char", "Box", "Vec", "Option", "Result", ], lineComments: slashLine, blockComments: cBlock, attributePrefixes: ["#"] ) table["rust"] = rust table["rs"] = rust let cFamily = LanguageProfile( keywords: [ "int", "char", "float", "double", "void", "long", "short", "signed", "unsigned", "if", "else", "for", "while", "do", "switch", "case", "default", "return", "break", "continue", "struct", "union", "enum", "typedef", "const", "static", "extern", "inline", "sizeof", "goto", "volatile", "register", "auto", "bool", "true", "false", "class", "public", "private", "protected", "virtual", "override", "final", "template", "typename", "namespace", "using", "new", "delete", "this", "nullptr", "try", "catch", "throw", "constexpr", "noexcept", "friend", "operator", "explicit", "mutable", "id", "instancetype", "nonatomic", "strong", "weak", "copy", "readonly", "readwrite", "assign", "nil", "YES", "NO", ], lineComments: slashLine, blockComments: cBlock, stringDelimiters: ["\"", "'"], attributePrefixes: ["@", "#"] ) for alias in ["c", "cpp", "c++", "cc", "cxx", "h", "hpp", "objc", "objective-c", "objectivec", "m", "mm"] { table[alias] = cFamily } return table }() }