
Highlight.pudu
Pudu161 lines12.6 KB
1/** @Docgen.Markdown.Highlight — static syntax coloring for code samples */2module PuduLangDocgen.Markdown.Highlight34import Std.Char as Char5import Std.Map as Map6import Std.Set as Set7import PuduLangDocgen.Markdown.Languages as Languages8import PuduLangDocgen.Paths as Paths910/** @Docgen.Markdown.Profile — lexical conventions of one language */11type Profile = { keywords: Set[Str], literals: Set[Str], comments: Array[Str], block: (Str, Str), quotes: Str, types: Bool }121314const C_FAMILY: Array[Str] = [15 "if", "else", "for", "while", "do", "switch", "case", "default", "break", "continue", "return", "goto",16 "struct", "enum", "union", "typedef", "static", "const", "extern", "void", "int", "char", "long",17 "short", "float", "double", "unsigned", "signed", "sizeof", "volatile", "inline"18]192021const PROFILES: Map[Str, Profile] = mapOf([22 ("pudu", Profile{keywords: setOf(["module", "import", "export", "as", "let", "var", "const", "mut", "fn", "async", "return", "if", "else", "match", "case", "for", "in", "while", "loop", "break", "continue", "type", "enum", "struct", "trait", "impl", "where", "await", "task", "spawn", "comptime", "macro", "unsafe", "with", "scope", "dynamic"]), literals: setOf(["true", "false", "Some", "None", "Ok", "Err"]), comments: ["//"], block: ("/*", "*/"), quotes: "\"'", types: true}),23 ("rust", Profile{keywords: setOf(["as", "async", "await", "break", "const", "continue", "crate", "dyn", "else", "enum", "extern", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref", "return", "self", "Self", "static", "struct", "super", "trait", "type", "unsafe", "use", "where", "while"]), literals: setOf(["true", "false", "Some", "None", "Ok", "Err"]), comments: ["//"], block: ("/*", "*/"), quotes: "\"", types: true}),24 ("go", Profile{keywords: setOf(["break", "case", "chan", "const", "continue", "default", "defer", "else", "fallthrough", "for", "func", "go", "goto", "if", "import", "interface", "map", "package", "range", "return", "select", "struct", "switch", "type", "var"]), literals: setOf(["true", "false", "nil", "iota"]), comments: ["//"], block: ("/*", "*/"), quotes: "\"'`", types: false}),25 ("javascript", Profile{keywords: setOf(["async", "await", "break", "case", "catch", "class", "const", "continue", "debugger", "default", "delete", "do", "else", "export", "extends", "finally", "for", "from", "function", "if", "import", "in", "instanceof", "let", "new", "of", "return", "static", "super", "switch", "this", "throw", "try", "typeof", "var", "void", "while", "yield"]), literals: setOf(["true", "false", "null", "undefined", "NaN", "Infinity"]), comments: ["//"], block: ("/*", "*/"), quotes: "\"'`", types: false}),26 ("typescript", Profile{keywords: setOf(["abstract", "as", "async", "await", "break", "case", "catch", "class", "const", "continue", "declare", "default", "do", "else", "enum", "export", "extends", "finally", "for", "from", "function", "if", "implements", "import", "in", "instanceof", "interface", "keyof", "let", "namespace", "new", "of", "private", "protected", "public", "readonly", "return", "static", "super", "switch", "this", "throw", "try", "type", "typeof", "var", "void", "while", "yield"]), literals: setOf(["true", "false", "null", "undefined"]), comments: ["//"], block: ("/*", "*/"), quotes: "\"'`", types: true}),27 ("python", Profile{keywords: setOf(["and", "as", "assert", "async", "await", "break", "class", "continue", "def", "del", "elif", "else", "except", "finally", "for", "from", "global", "if", "import", "in", "is", "lambda", "nonlocal", "not", "or", "pass", "raise", "return", "try", "while", "with", "yield"]), literals: setOf(["True", "False", "None"]), comments: ["#"], block: ("", ""), quotes: "\"'", types: false}),28 ("ruby", Profile{keywords: setOf(["begin", "class", "def", "do", "else", "elsif", "end", "ensure", "if", "module", "next", "rescue", "return", "self", "then", "unless", "until", "when", "while", "yield", "require"]), literals: setOf(["true", "false", "nil"]), comments: ["#"], block: ("", ""), quotes: "\"'", types: true}),29 ("java", Profile{keywords: setOf(["abstract", "boolean", "break", "byte", "case", "catch", "char", "class", "continue", "default", "do", "double", "else", "enum", "extends", "final", "finally", "float", "for", "if", "implements", "import", "instanceof", "int", "interface", "long", "new", "package", "private", "protected", "public", "return", "short", "static", "super", "switch", "this", "throw", "throws", "try", "var", "void", "while"]), literals: setOf(["true", "false", "null"]), comments: ["//"], block: ("/*", "*/"), quotes: "\"'", types: true}),30 ("kotlin", Profile{keywords: setOf(["as", "break", "class", "continue", "do", "else", "for", "fun", "if", "in", "interface", "is", "object", "package", "return", "super", "this", "throw", "try", "typealias", "val", "var", "when", "while", "import", "data", "sealed", "override", "private", "public"]), literals: setOf(["true", "false", "null"]), comments: ["//"], block: ("/*", "*/"), quotes: "\"'", types: true}),31 ("swift", Profile{keywords: setOf(["class", "deinit", "enum", "extension", "func", "import", "init", "let", "protocol", "struct", "subscript", "typealias", "var", "break", "case", "continue", "default", "defer", "do", "else", "for", "guard", "if", "in", "repeat", "return", "switch", "where", "while", "throws", "try", "self"]), literals: setOf(["true", "false", "nil"]), comments: ["//"], block: ("/*", "*/"), quotes: "\"", types: true}),32 ("c", Profile{keywords: setOf(C_FAMILY), literals: setOf(["NULL", "true", "false"]), comments: ["//", "#"], block: ("/*", "*/"), quotes: "\"'", types: false}),33 ("c++", Profile{keywords: setOf(C_FAMILY.concat(["class", "namespace", "template", "typename", "public", "private", "protected", "virtual", "override", "new", "delete", "using", "auto", "constexpr", "this", "throw", "try", "catch", "bool"])), literals: setOf(["nullptr", "true", "false"]), comments: ["//", "#"], block: ("/*", "*/"), quotes: "\"'", types: false}),34 ("json", Profile{keywords: setOf([]), literals: setOf(["true", "false", "null"]), comments: [], block: ("", ""), quotes: "\"", types: false}),35 ("yaml", Profile{keywords: setOf([]), literals: setOf(["true", "false", "null", "yes", "no", "on", "off"]), comments: ["#"], block: ("", ""), quotes: "\"'", types: false}),36 ("toml", Profile{keywords: setOf([]), literals: setOf(["true", "false"]), comments: ["#"], block: ("", ""), quotes: "\"'", types: false}),37 ("shell", Profile{keywords: setOf(["if", "then", "else", "elif", "fi", "for", "in", "do", "done", "while", "until", "case", "esac", "function", "return", "local", "export", "echo", "set", "unset", "cd", "exit"]), literals: setOf(["true", "false"]), comments: ["#"], block: ("", ""), quotes: "\"'", types: false}),38 ("powershell", Profile{keywords: setOf(["begin", "break", "catch", "class", "continue", "do", "else", "elseif", "end", "exit", "filter", "finally", "for", "foreach", "function", "if", "in", "param", "process", "return", "switch", "throw", "trap", "try", "until", "while"]), literals: setOf(["$true", "$false", "$null"]), comments: ["#"], block: ("<#", "#>"), quotes: "\"'", types: false}),39 ("sql", Profile{keywords: setOf(["select", "from", "where", "insert", "into", "values", "update", "set", "delete", "create", "table", "drop", "alter", "join", "left", "right", "inner", "outer", "on", "group", "by", "order", "having", "limit", "and", "or", "not", "as", "distinct", "union", "SELECT", "FROM", "WHERE", "INSERT", "INTO", "VALUES", "UPDATE", "SET", "DELETE", "CREATE", "TABLE", "JOIN", "ON", "GROUP", "BY", "ORDER", "AND", "OR", "NOT", "AS"]), literals: setOf(["null", "NULL", "true", "false", "TRUE", "FALSE"]), comments: ["--"], block: ("/*", "*/"), quotes: "'\"", types: false}),40 ("css", Profile{keywords: setOf(["@media", "@import", "@font-face", "@keyframes", "!important"]), literals: setOf([]), comments: [], block: ("/*", "*/"), quotes: "\"'", types: false}),41 ("html", Profile{keywords: setOf([]), literals: setOf([]), comments: [], block: ("<!--", "-->"), quotes: "\"'", types: false})42 ])434445const SHARED: Map[Str, Str] = mapOf([46 ("tsx", "typescript"), ("xml", "html"), ("svg", "html"), ("vue", "html"), ("scss", "css"), ("less", "css"),47 ("json-with-comments", "json"), ("json5", "json"), ("objective-c", "c"), ("objective-c++", "c++"),48 ("groovy", "java"), ("scala", "java"), ("dart", "java")49 ])50515253export fn lines(language: Str, text: Str) -> Array[Str] {54 let name = profileName(language)55 let tokens = match Map.get(&PROFILES, name) {56 case Some(profile) => tokenize(&profile, text)57 case None => [("", text)]58 }59 var result: Array[Str] = []60 var current: Array[Str] = []61 for (kind, written) in tokens {62 let parts = written.split("\n")63 var index = 064 for part in parts {65 if index > 0 {66 result = result.push(current.join(""))67 current = []68 }69 if !part.isEmpty() {70 current = current.push(if kind.isEmpty() { Paths.escape(part) } else { "<span class=\"hl-" + kind + "\">" + Paths.escape(part) + "</span>" })71 }72 index = index + 173 }74 }75 result.push(current.join(""))76}777879fn profileName(language: Str) -> Str {80 let identity = Languages.identify(language)81 let name = Map.getOr(&SHARED, identity, identity)82 if Map.containsKey(&PROFILES, name) { name } else { "" }83}848586fn startsAt(cs: &Array[Char], index: Int, prefix: Str) -> Bool {87 if prefix.isEmpty() { return false }88 let wanted = prefix.chars()89 if index + wanted.length() > cs.length() { return false }90 for offset in 0..wanted.length() {91 if cs[index + offset] != wanted[offset] { return false }92 }93 true94}959697fn slice(cs: &Array[Char], from: Int, to: Int) -> Str {98 var pieces: Array[Str] = []99 for index in from..to { pieces = pieces.push(cs[index].toText()) }100 pieces.join("")101}102103104fn tokenize(profile: &Profile, text: Str) -> Array[(Str, Str)] {105 let cs = text.chars()106 var tokens: Array[(Str, Str)] = []107 var plain: Array[Str] = []108 var index = 0109 let (open, close) = profile.block110 while index < cs.length() {111 let character = cs[index]112 var end = index113 var kind = ""114 if startsAt(&cs, index, open) {115 end = index + open.length()116 while end < cs.length() && !startsAt(&cs, end, close) { end = end + 1 }117 end = if end < cs.length() { end + close.length() } else { end }118 kind = "c"119 } else if profile.comments.filter(|leader: Str| startsAt(&cs, index, leader)).length() > 0 && (character != '#' || index == 0 || Char.isWhitespace(cs[index - 1])) {120 end = index121 while end < cs.length() && cs[end] != '\n' { end = end + 1 }122 kind = "c"123 } else if profile.quotes.contains(character.toText()) {124 end = index + 1125 while end < cs.length() && cs[end] != character && (cs[end] != '\n' || character == '`') {126 if cs[end] == '\\' { end = end + 1 }127 end = end + 1128 }129 end = if end < cs.length() && cs[end] == character { end + 1 } else { end }130 kind = "s"131 } else if Char.isDigit(character) && (index == 0 || !identifier(cs[index - 1])) {132 end = index133 while end < cs.length() && (Char.isAlphanumeric(cs[end]) || cs[end] == '.' || cs[end] == '_') { end = end + 1 }134 kind = "n"135 } else if identifier(character) || character == '$' || character == '@' {136 end = index + 1137 while end < cs.length() && (identifier(cs[end]) || cs[end] == '-' && profile.keywords.isEmpty()) { end = end + 1 }138 let written = slice(&cs, index, end)139 kind = if Set.contains(&profile.keywords, written) { "k" }140 else if Set.contains(&profile.literals, written) { "l" }141 else if profile.types && Char.isUpper(character) { "t" }142 else { "" }143 } else { end = index + 1 }144 if end <= index { end = index + 1 }145 if kind.isEmpty() {146 plain = plain.push(slice(&cs, index, end))147 } else {148 if !plain.isEmpty() {149 tokens = tokens.push(("", plain.join("")))150 plain = []151 }152 tokens = tokens.push((kind, slice(&cs, index, end)))153 }154 index = end155 }156 if plain.isEmpty() { tokens } else { tokens.push(("", plain.join(""))) }157}158159160fn identifier(character: Char) -> Bool { Char.isAlphanumeric(character) || character == '_' }161