PageSourceSearch

https://littlebirdelectronics.com.au/assets/controllers/courses/code_highlight_controller-54b5cf87.js

js littlebirdelectronics.com.au collected 2026-09-25 14:09:09 UTC 5,534 bytes, 140 lines download raw bytes

1import { Controller } from "@hotwired/stimulus"
2
3// Tiny regex-based syntax highlighter — direct port of `hl()` from
4// tmp/Code Explainer/shared.jsx, extended to recognise C / C++ / Arduino
5// so the same controller handles every language we ship in courses.
6//
7// We deliberately don't pull in Prism or Highlight.js: the input is short
8// (rarely more than ~60 lines) and our authors hand-write samples, so a
9// 60-line tokeniser is enough to make Python and Arduino read clean
10// without paying the bundle cost or the importmap churn.
11//
12// Token classes (`.tok-*`) live in app/views/layouts/courses/_styles.html.erb
13// and swap colour under `data-theme="dark"` so dark mode rebinds wholesale.
14
15const PY_KEYWORDS = new Set([
16  "from", "import", "while", "for", "in", "if", "else", "elif", "def",
17  "return", "True", "False", "None", "and", "or", "not", "as", "with",
18  "try", "except", "finally", "raise", "pass", "break", "continue",
19  "class", "lambda", "yield", "global", "nonlocal", "is", "assert", "del"
20])
21const PY_BUILTINS = new Set([
22  "range", "print", "len", "int", "float", "str", "list", "dict", "tuple",
23  "set", "bool", "open", "abs", "min", "max", "sum", "round", "input"
24])
25
26const C_KEYWORDS = new Set([
27  "auto", "break", "case", "char", "const", "continue", "default", "do",
28  "double", "else", "enum", "extern", "float", "for", "goto", "if",
29  "int", "long", "register", "return", "short", "signed", "sizeof",
30  "static", "struct", "switch", "typedef", "union", "unsigned", "void",
31  "volatile", "while", "true", "false", "bool",
32  // C++ on top
33  "class", "namespace", "new", "delete", "public", "private", "protected",
34  "template", "typename", "using", "virtual", "this", "operator",
35  "nullptr", "constexpr", "noexcept", "override", "final", "try", "catch", "throw"
36])
37const ARDUINO_BUILTINS = new Set([
38  "setup", "loop",
39  "pinMode", "digitalWrite", "digitalRead", "analogWrite", "analogRead",
40  "delay", "delayMicroseconds", "millis", "micros",
41  "Serial", "begin", "print", "println", "write", "read", "available",
42  "HIGH", "LOW", "INPUT", "OUTPUT", "INPUT_PULLUP",
43  "LED_BUILTIN", "A0", "A1", "A2", "A3", "A4", "A5",
44  "map", "constrain", "tone", "noTone", "attachInterrupt", "detachInterrupt"
45])
46
47export default class extends Controller {
48  static values = { language: { type: String, default: "python" } }
49
50  connect() {
51    // Idempotent — Turbo can re-attach the controller on a back-nav and
52    // we don't want to re-wrap already-tokenised spans.
53    if (this.element.dataset.codeHighlighted === "true") return
54    const raw = this.element.textContent
55    const html = this.highlight(raw, this.languageValue)
56    this.element.innerHTML = html
57    this.element.dataset.codeHighlighted = "true"
58  }
59
60  highlight(src, lang) {
61    const lines = src.split("\n")
62    return lines.map(line => this.highlightLine(line, lang)).join("\n")
63  }
64
65  highlightLine(line, lang) {
66    const { keywords, builtins, commentStart } = this.profileFor(lang)
67    const out = []
68    let i = 0
69    while (i < line.length) {
70      const rest = line.slice(i)
71
72      // Line comment — Python `#…`, C/C++/Arduino `//…`.
73      if (commentStart && rest.startsWith(commentStart)) {
74        out.push(`<span class="tok-comment">${escape(rest)}</span>`)
75        return out.join("")
76      }
77      // Block comment for C-family: /* … */ (single-line only — the
78      // tokeniser is line-by-line and we don't fight multiline comments).
79      if (commentStart === "//" && rest.startsWith("/*")) {
80        const end = rest.indexOf("*/")
81        const chunk = end >= 0 ? rest.slice(0, end + 2) : rest
82        out.push(`<span class="tok-comment">${escape(chunk)}</span>`)
83        i += chunk.length
84        continue
85      }
86
87      // Strings: triple-quoted, single, double.
88      const st = rest.match(/^(?:f|r|b|u)?(?:'''[\s\S]*?'''|"""[\s\S]*?"""|'(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*")/)
89      if (st) {
90        out.push(`<span class="tok-string">${escape(st[0])}</span>`)
91        i += st[0].length
92        continue
93      }
94
95      // Numbers (ints / floats / hex / 0b…).
96      const nm = rest.match(/^(?:0x[0-9a-fA-F]+|0b[01]+|\d+(?:\.\d+)?)/)
97      if (nm) {
98        out.push(`<span class="tok-number">${escape(nm[0])}</span>`)
99        i += nm[0].length
100        continue
101      }
102
103      // Identifiers — keyword / builtin / plain.
104      const id = rest.match(/^[A-Za-z_][A-Za-z0-9_]*/)
105      if (id) {
106        const w = id[0]
107        if (keywords.has(w)) out.push(`<span class="tok-keyword">${escape(w)}</span>`)
108        else if (builtins.has(w)) out.push(`<span class="tok-builtin">${escape(w)}</span>`)
109        else out.push(escape(w))
110        i += w.length
111        continue
112      }
113
114      // Everything else (operators, punctuation, whitespace) — passthrough.
115      out.push(escape(line[i]))
116      i++
117    }
118    return out.join("")
119  }
120
121  profileFor(lang) {
122    const l = (lang || "").toLowerCase()
123    if (l === "python" || l === "micropython") {
124      return { keywords: PY_KEYWORDS, builtins: PY_BUILTINS, commentStart: "#" }
125    }
126    if (l === "c" || l === "cpp" || l === "c++" || l === "arduino") {
127      const builtins = l === "arduino" ? ARDUINO_BUILTINS : new Set()
128      return { keywords: C_KEYWORDS, builtins, commentStart: "//" }
129    }
130    // Fallback — no highlighting, but still safe to render.
131    return { keywords: new Set(), builtins: new Set(), commentStart: null }
132  }
133}
134
135function escape(str) {
136  return str
137    .replace(/&/g, "&amp;")
138    .replace(/</g, "&lt;")
139    .replace(/>/g, "&gt;")
140}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.