Pudu programming language
Menu
Package

@chrismichaelps / pudu-lang-docgen

Documentation publishing for Pudu: articles, API references, navigation, search, and static websites

0.1.0Apache-2.01

InstallClose

Highlight.pudu

Pudu161 lines12.6 KB

GitHub ↗
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 }1213/// Keywords shared by languages with C-like syntax.14const 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]1920/// Lexical profiles by language name and common alias.21const 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  ])4344/// Language identities that share another language's profile.45const 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  ])5051/// Escaped HTML of each line, with tokens of known languages wrapped in `hl-*` spans.52/// Unknown languages are escaped without coloring.53export 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}7778/// The profile a language name or alias selects, or the empty text.79fn 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}8485/// Whether text continues with a given prefix at an index.86fn 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}9596/// Text of the characters in a half-open range.97fn 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}102103/// Classified pieces: `c` comment, `s` string, `n` number, `k` keyword, `l` literal, `t` type.104fn 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}158159/// Whether a character may appear in an identifier.160fn identifier(character: Char) -> Bool { Char.isAlphanumeric(character) || character == '_' }161