| 📦 Turbo JS 91999d1 k33g 11h ago | 1 | # Reference: languages coloured |
| 2 | |
| 3 | > Neutral description of which files Turbo JS colours, how it decides, and what each scanner recognises. |
| 4 | |
| 5 | ## Recognition |
| 6 | |
| 7 | A file's **extension** decides whenever it is one of these: |
| 8 | |
| 9 | | Extension | Language | |
| 10 | | --- | --- | |
| 11 | | `.js`, `.mjs`, `.cjs` | JavaScript | |
| 12 | | `.json`, `.jsonc` | JSON | |
| 13 | | `.toml` | TOML | |
| 14 | | `.yaml`, `.yml` | YAML | |
| 15 | | `.md`, `.markdown` | Markdown | |
| 16 | | `.html`, `.htm` | HTML | |
| 17 | | `.xml`, `.xsd`, `.xsl`, `.xslt`, `.svg`, `.plist`, `.csproj`, `.pom` | XML | |
| 18 | | `.sh`, `.bash`, `.zsh` | Shell | |
| 19 | | `.dockerfile`, `.containerfile` | Dockerfile | |
| 20 | |
| 21 | Extensions are matched case-insensitively, and only the last one counts: `notes.js.md` is Markdown, `main.js.backup` is not JavaScript, and `main.test.js` is JavaScript. |
| 22 | |
| 23 | A file whose extension decides nothing is looked up by **name** next. Only files that carry no useful extension need this: |
| 24 | |
| 25 | | Name | Language | |
| 26 | | --- | --- | |
| 27 | | `Dockerfile`, `Containerfile` | Dockerfile | |
| 28 | |
| 29 | A name matches on the whole of it or on the part before the first dot, ignoring case — so `Dockerfile`, `dockerfile` and `Dockerfile.dev` are all recognised, while `Dockerfile.md` is Markdown, because the extension is consulted first. |
| 30 | |
| 31 | A file that neither table claims is read by its **first line**. A shebang naming `node` makes it JavaScript: a command-line tool written in JavaScript is a file with no extension whose first line is `#!/usr/bin/env node`, and Node strips that line before parsing. A shebang naming a shell — `sh`, `bash`, `zsh`, `dash` or `ksh` — makes it a shell script. The interpreter is recognised as a path element or as the argument to `env`. |
| 32 | |
| 33 | | First line | Result | |
| 34 | | --- | --- | |
| 35 | | `#!/usr/bin/env node` | JavaScript | |
| 36 | | `#!/usr/local/bin/node` | JavaScript | |
| 37 | | `#!/usr/bin/env -S node --no-warnings` | JavaScript | |
| 38 | | `#!/bin/sh` | Shell | |
| 39 | | `#!/usr/bin/env bash` | Shell | |
| 40 | | `#!/usr/bin/env python3` | Not coloured | |
| 41 | | Anything not starting `#!` | Not coloured | |
| 42 | |
| 43 | The order is fixed — extension, then name, then first line — and the first to decide wins. |
| 44 | |
| 45 | Everything else is shown in plain text. That is not an error — opening a PNG in the editor is not a mistake, it is just not coloured. In particular **TypeScript (`.ts`, `.tsx`), JSX (`.jsx`) and CSS are not coloured**: the language server serves `.ts` files all the same, but the scanner here is for JavaScript. |
| 46 | |
| 47 | ## Classes |
| 48 | |
| 49 | Every scanner produces the same vocabulary of classes, and each maps to one theme key. |
| 50 | |
| 51 | | Class | Theme key | Produced by | |
| 52 | | --- | --- | --- | |
| 53 | | `identifier` | `syntax.identifier` | JavaScript, JSON (a bare word, which is not JSON), TOML, shell, YAML, Dockerfile | |
| 54 | | `keyword` | `syntax.keyword` | JavaScript, shell, HTML (doctype), XML, Dockerfile | |
| 55 | | `type` | `syntax.type` | JavaScript (class names and capitalised names), TOML (table headers), YAML (tags) | |
| 56 | | `builtin` | `syntax.builtin` | JavaScript (the standard library's and Node's globals), shell (builtins and expansions), YAML (anchors and aliases), Dockerfile (variables) | |
| 57 | | `constant` | `syntax.constant` | JavaScript, JSON, TOML, shell, YAML, HTML and XML (entities) | |
| 58 | | `function` | `syntax.function` | JavaScript, shell (the command) | |
| 59 | | `string` | `syntax.string` | all | |
| 60 | | `char` | `syntax.char` | JavaScript (regular-expression literals) | |
| 61 | | `number` | `syntax.number` | JavaScript, JSON, TOML, shell, YAML, Dockerfile | |
| 62 | | `comment` | `syntax.comment` | JavaScript, JSON, TOML, shell, HTML, YAML, XML, Dockerfile | |
| 63 | | `operator` | `syntax.operator` | JavaScript, TOML, shell, HTML, YAML (block scalar headers), XML, Dockerfile | |
| 64 | | `punctuation` | `syntax.punctuation` | JavaScript, JSON, TOML, shell, Markdown, YAML, Dockerfile | |
| 65 | | `heading` | `syntax.heading` | Markdown | |
| 66 | | `tag` | `syntax.tag` | HTML, XML | |
| 67 | | `attribute` | `syntax.attribute` | JSON (keys), JavaScript (decorators), HTML, XML, Dockerfile (flags) | |
| 68 | | `emphasis` | `syntax.emphasis` | Markdown | |
| 69 | | `link` | `syntax.link` | Markdown | |
| 70 | |
| 71 | JavaScript produces no `heading`, `tag`, `emphasis` or `link` span. In `turbo-classic`, `syntax.number` and `syntax.constant` are both magenta and `syntax.char` is the green of `syntax.string`, so `42` and `true` share a colour there and so do `/re/` and `"re"`; other themes separate them. See [how to write your own theme](../how-to/write-a-theme.md) if you want to change it. |
| 72 | |
| 73 | ## JavaScript |
| 74 | |
| 75 | Hand-written, in `internal/jslang`, and registered under the same name as the scanner turbo-core ships for JavaScript, which it replaces. What it adds over the library's: regular-expression literals, the hashbang line, Node's globals, the name after `function` and `class`, private names, decorators, and a leading capital read as a class name. |
| 76 | |
| 77 | **Two constructs cross a line break**, and are carried to the next line: a `/* … */` block comment to its closing `*/`, and a `` `…` `` template literal to its closing backtick. Neither nests. An ordinary `'…'` or `"…"` string does **not** cross a line: an unterminated one is coloured to the end of its line, and the next line is code again. |
| 78 | |
| 79 | | Recognised | As | |
| 80 | | --- | --- | |
| 81 | | `as`, `async`, `await`, `break`, `case`, `catch`, `class`, `const`, `continue`, `debugger`, `default`, `delete`, `do`, `else`, `export`, `extends`, `finally`, `for`, `from`, `function`, `get`, `if`, `import`, `in`, `instanceof`, `let`, `new`, `of`, `return`, `set`, `static`, `super`, `switch`, `throw`, `try`, `typeof`, `var`, `void`, `while`, `with`, `yield`, and the reserved `enum`, `implements`, `interface`, `package`, `private`, `protected`, `public` | keyword | |
| 82 | | `true`, `false`, `null`, `undefined`, `NaN`, `Infinity`, `this` | constant | |
| 83 | | the standard library's globals — `Array`, `Object`, `Promise`, `Math`, `JSON`, `Map`, `Set`, `Symbol`, `BigInt`, `Error`, `TypeError`, `parseInt`, `setTimeout`, `structuredClone`, `fetch`, `URL`, … | builtin | |
| 84 | | Node's globals — `process`, `Buffer`, `console`, `require`, `module`, `exports`, `__dirname`, `__filename`, `setImmediate`, `performance`, `crypto` — and the browser's `document` and `window` | builtin | |
| 85 | | the name after `function`, `function*` or `async function` — `parse` in `function parse(input) {}` | function | |
| 86 | | the name after `class` — `widget` in `class widget extends Base {}` | type | |
| 87 | | any other name starting with an ASCII capital — `Greeter`, `EventEmitter`, `MyError` — even before a `(` | type | |
| 88 | | any other name immediately before `(` — `compute(3)`, `obj.method()`, `this.#reset()` | function | |
| 89 | | any word after `.` or `?.`, whatever it is spelt like — `map.get(k)` a function, `options.default` a name, `user?.class` a name | property, never a keyword | |
| 90 | | `get`, `set`, `static`, `of`, `from` and `as` before a `(` — `get(key)` | function | |
| 91 | | `#count`, a private member, the `#` included | identifier, or function before `(` | |
| 92 | | `@decorator`, `@observable.ref`, the dotted path included | attribute | |
| 93 | | any other name: a Unicode letter, `_` or `$`, then letters, digits and the same — `x`, `$el`, `_`, `café`, `名前` | identifier | |
| 94 | | `"…"`, `'…'` with backslash escapes, on one line | string | |
| 95 | | `` `…` ``, `${…}` interpolations included, across lines | string | |
| 96 | | `/…/gi`, a regular-expression literal, flags included, where the previous token allows one and a closing `/` exists on the line | char | |
| 97 | | `42`, `1_000`, `0xFF`, `0o17`, `0b1010`, `1.5e-3`, `2E+10`, `.5`, `10n`, `0xFFn` | number | |
| 98 | | `//` to the end of the line; `/* … */` and `/** … */` across lines | comment | |
| 99 | | `#!` on the first line of the file, to its end | comment | |
| 100 | | `...` | punctuation, as one span | |
| 101 | | `?.` | operator, as one span | |
| 102 | | runs of `+-*/%=<>!&\|^~?:` — including `=>`, `??`, `===`, `**`, `>>>=` | operator | |
| 103 | | `()[]{},;` and a lone `.` | punctuation | |
| 104 | |
| 105 | **A slash divides after a value and opens a regular expression everywhere else.** After a name, a number, a string, a template, a regular expression, `)` or `]`, a `/` is division. After an operator, `(`, `[`, `{`, `}`, `,`, `;`, `:`, a keyword, or at the start of a line, it opens a regular expression — **provided a closing `/` exists on the same line**; otherwise it divides, whatever came before it. A regular expression cannot cross a line, so that bound keeps a wrong guess to one line. |
| 106 | |
| 107 | **A point joins a number only when a digit follows it**, so `1.toString()` is the number `1`, a dot and a method. **The sign after an `e` is part of the number**, except in a hexadecimal literal, where `0xE+1` is a sum. |
| 108 | |
| 109 | **A capitalised name is a class by convention, not by rule.** JavaScript lets you write `const Count = 1`, and it is coloured as a class all the same, as is `MAX_SIZE`. Classes and constructors are what people capitalise, and the colour follows the people. |
| 110 | |
| 111 | **Not recognised**, each for a stated reason: |
| 112 | |
| 113 | | Not recognised | Because | |
| 114 | | --- | --- | |
| 115 | | The code inside `${…}` in a template | Colouring it means the scanner reaching back into itself with a nesting depth to carry, for a construct that is usually one short expression; the whole literal is a string | |
| 116 | | A backtick inside `${…}` | For the same reason: it ends the template early, and the code after it is coloured as code until the next backtick | |
| 117 | | A regular expression with no closing slash on its line | It cannot be one — the language forbids a newline in the literal — so the slash divides | |
| 118 | | JSX | `<div>` is read as an operator, a name and an operator; there is no HTML inside JavaScript here | |
| 119 | | TypeScript's annotations and types | A different language; `.ts` files are not coloured at all | |
| 120 | | Nested block comments | JavaScript ends a block comment at the first `*/`; a depth would be a claim about a different language | |
| 121 | | A string continued with a trailing backslash | The string stops at its line and the next line is code; the continuation is rare enough not to be worth carrying | |
| 122 | | Whether a `SCREAMING_SNAKE_CASE` name is a constant | The leading capital says class, and nothing in the spelling separates the two conventions | |
| 123 | | `#` as a comment | It is one only in the hashbang, on the first line; anywhere else `#` begins a private name or is stepped over | |
| 124 | | Whether a name is bound in this scope | Nothing here reads more than one line at a time; that is the language server's question, and [F1 answers it](../how-to/ask-about-code.md) | |
| 125 | |
| 126 | ## JSON |
| 127 | |
| 128 | Hand-written, in `internal/jslang/json.go`, thirty lines, because the language is thirty lines: strings, numbers, three words, six punctuation marks. It is the language every Node project's manifest is written in, and turbo-core does not colour it. |
| 129 | |
| 130 | | Recognised | As | |
| 131 | | --- | --- | |
| 132 | | `"name"` followed by a colon — a key | attribute | |
| 133 | | `"demo"` anywhere else — a value | string | |
| 134 | | `-12.5e+3`, `42`, `0.5` | number | |
| 135 | | `true`, `false`, `null` | constant | |
| 136 | | `{`, `}`, `[`, `]`, `,`, `:` | punctuation | |
| 137 | | `//` to the end of the line; `/* … */` across lines | comment | |
| 138 | | any bare word — `undefined`, `NaN`, a key without quotes | identifier | |
| 139 | |
| 140 | **A string is a key when a colon follows it**, past any spaces, and a value otherwise. `{"a" 1}` colours `"a"` as a value: it is not a key yet. |
| 141 | |
| 142 | **Comments are tolerated, not endorsed.** Strict JSON has none; `tsconfig.json`, `.jsonc` files and every editor's own settings file do, and a scanner that painted them as broken would be wrong in exactly the files most likely to hold one. |
| 143 | |
| 144 | | Not recognised | Because | |
| 145 | | --- | --- | |
| 146 | | Whether the document is valid JSON | The scanner colours tokens; a bare word comes out as an identifier rather than stopping the line, and a duplicated key looks like any other | |
| 147 | | Single-quoted strings, trailing commas and the rest of JSON5 | Not JSON; a `'` is stepped over uncoloured | |
| 148 | | An unterminated string stopping anywhere but its line | It is coloured to the end of the line, as a value, and the next line is JSON again | |
| 149 | |
| 150 | ## TOML |
| 151 | |
| 152 | | Recognised | As | |
| 153 | | --- | --- | |
| 154 | | `# comment` | comment | |
| 155 | | `[table]`, `[[array]]` | the name as a type, the brackets as punctuation | |
| 156 | | `key =` | identifier, then operator | |
| 157 | | `"basic"`, `'literal'`, `"""multi-line"""`, `'''multi-line'''` | string | |
| 158 | | `true`, `false` | constant | |
| 159 | | numbers, dates, times, `inf`, `nan` | number | |
| 160 | |
| 161 | ## YAML |
| 162 | |
| 163 | A compose file, a Kubernetes manifest and a CI workflow are all this: there is no separate dialect, because a dialect would be somebody else's schema to keep in step with. |
| 164 | |
| 165 | | Recognised | As | |
| 166 | | --- | --- | |
| 167 | | `# comment` | comment | |
| 168 | | `key:` before a space or the end of the line | the key as an identifier, the colon as punctuation | |
| 169 | | `"quoted": 1`, `'quoted': 1` | the quoted key as an identifier | |
| 170 | | `- ` opening a sequence entry | punctuation | |
| 171 | | `"…"`, `'…'` | string | |
| 172 | | `true`, `false`, `yes`, `no`, `on`, `off`, `null` | constant, whatever their case | |
| 173 | | numbers, dates and times written without quotes | number | |
| 174 | | `&anchor`, `*alias` | builtin | |
| 175 | | `!!str`, `!Custom` | type | |
| 176 | | `---`, `...` | the whole line as punctuation | |
| 177 | | `{`, `}`, `[`, `]`, `,` | punctuation | |
| 178 | | `\|`, `>`, with their chomping and indentation indicators | the header as an operator, the body as a string | |
| 179 | |
| 180 | **A colon is a separator only when a space or the end of the line follows it.** `image: node:24-alpine` is a key and one value, and `url: http://example.com/x` is a key and one URL — colouring the inner colons as separators would put every image tag and every URL in three colours. |
| 181 | |
| 182 | **A block scalar's extent is decided by indentation**, not by a delimiter. The first content line after `|` or `>` fixes the block's indentation; every line indented at least that far belongs to it, and the first line that is not ends it. **A blank line inside a block stays inside it**: a literal scalar keeps its empty lines, and ending the block at the first paragraph break would cut a shell script in a CI file in half. |
| 183 | |
| 184 | **A `#` needs a space before it to start a comment**, so `colour: ff#00aa` is one scalar. |
| 185 | |
| 186 | | Not recognised | Because | |
| 187 | | --- | --- | |
| 188 | | The schema of a compose file, a manifest or a workflow | Colouring `services:` differently from any other key means carrying somebody else's schema, and it goes stale the day they add a key | |
| 189 | | Multi-document streams as separate documents | `---` is coloured, but nothing is reset at it; nothing in the colouring depends on document boundaries | |
| 190 | | Whether a bare word is a string or a number to a parser | `1.2.3` is a version to a reader and a string to YAML; the scanner colours what it looks like | |
| 191 | |
| 192 | ## Markdown |
| 193 | |
| 194 | | Recognised | As | |
| 195 | | --- | --- | |
| 196 | | `# Heading` … `###### Heading` | the whole line as a heading | |
| 197 | | `**bold**`, `__bold__`, `*italic*`, `_italic_` | emphasis | |
| 198 | | `` `code` `` | string | |
| 199 | | `[text](target)`, `` | the whole thing as a link | |
| 200 | | `- `, `* `, `+ `, `1. `, `1) ` | the marker as punctuation | |
| 201 | | `>` | punctuation | |
| 202 | | `---`, `***`, `___` | punctuation | |
| 203 | | ` ``` ` and `~~~` fences | the whole block, opening and closing lines included, as a string | |
| 204 | |
| 205 | A fenced block is **one colour whatever language it announces**: ```` ```javascript ```` does not colour its contents as JavaScript in a Markdown file. The run of markers that opens a block must be matched by the same character to close it, so a backtick fence is not closed by a tilde one. An unclosed fence colours to the end of the file. |
| 206 | |
| 207 | The run of markers opening emphasis must be matched by a run of the same length, so `**bold**` is one span rather than two italics. |
| 208 | |
| 209 | ## HTML |
| 210 | |
| 211 | | Recognised | As | |
| 212 | | --- | --- | |
| 213 | | `<tag`, `</tag`, `>`, `/>` | tag | |
| 214 | | attribute names, including `data-*`, `xlink:href`, `@click`, `v-bind.prop` | attribute | |
| 215 | | `=` | operator | |
| 216 | | `"…"`, `'…'` | string | |
| 217 | | `<!-- … -->`, across lines | comment | |
| 218 | | `&`, `©` | constant | |
| 219 | | `<!DOCTYPE …>` and other declarations | keyword | |
| 220 | |
| 221 | Text between tags is not coloured. A bare `&` with no `;` within 32 characters is left alone, because it is legal text. |
| 222 | |
| 223 | **The contents of `<script>` and `<style>` are not coloured** as JavaScript and CSS, even in this editor: the HTML scanner is turbo-core's, and it does not hand a region of a line to another scanner. |
| 224 | |
| 225 | ## XML |
| 226 | |
| 227 | Its own scanner rather than HTML's, for one reason that matters: CDATA. The whole point of `<![CDATA[ … ]]>` is that its contents are *not* markup, and colouring the tags inside one as tags is exactly backwards. |
| 228 | |
| 229 | | Recognised | As | |
| 230 | | --- | --- | |
| 231 | | `<?xml version="1.0"?>` and other processing instructions | the target and `?>` as keyword, the pairs between as attributes and strings | |
| 232 | | `<!DOCTYPE …>` and the other `<!` forms | keyword | |
| 233 | | `<!-- … -->`, across lines | comment | |
| 234 | | `<![CDATA[ … ]]>`, across lines | string | |
| 235 | | `<tag`, `</tag`, `>`, `/>` | tag | |
| 236 | | `<ns:tag>`, `xsi:type` | the prefix and the local name as **one** span | |
| 237 | | attribute names | attribute | |
| 238 | | `=` | operator | |
| 239 | | `"…"`, `'…'` | string | |
| 240 | | `&`, `©` | constant | |
| 241 | |
| 242 | **A comment and a CDATA section close on different delimiters**, and are carried separately: a `-->` inside a CDATA section does not end it. |
| 243 | |
| 244 | **A bare `&` with no semicolon within 32 characters is left alone**, because it is legal text in plenty of documents and swallowing the rest of the line would be the bigger mistake. |
| 245 | |
| 246 | Text between tags is not coloured. |
| 247 | |
| 248 | ## Shell |
| 249 | |
| 250 | Applies to `sh`, `bash` and `zsh` alike: the keywords recognised are the ones they share. |
| 251 | |
| 252 | | Recognised | As | |
| 253 | | --- | --- | |
| 254 | | `if`, `then`, `fi`, `for`, `while`, `case`, `esac`, `function`, `return`, … | keyword | |
| 255 | | `true`, `false` | constant | |
| 256 | | `echo`, `printf`, `export`, `local`, `read`, `cd`, `set`, `source`, … | builtin | |
| 257 | | `$NAME`, `${…}`, `$(…)`, `$1`, `$?`, `$@` | builtin | |
| 258 | | the **first bare word on a line** | function | |
| 259 | | every later bare word, and `NAME` in `NAME=value` | identifier | |
| 260 | | `'…'`, with nothing escaped or expanded inside | string | |
| 261 | | `"…"`, with the expansions inside it coloured as expansions | string | |
| 262 | | `#` to end of line | comment | |
| 263 | |
| 264 | `$(a $(b) c)` is one span: nesting is counted. An option such as `-euo` is one word, not a minus and a word. |
| 265 | |
| 266 | **Heredocs are not recognised.** `<<EOF` and the text after it are coloured as ordinary shell. |
| 267 | |
| 268 | ## Dockerfile |
| 269 | |
| 270 | | Recognised | As | |
| 271 | | --- | --- | |
| 272 | | `FROM`, `RUN`, `COPY`, `ADD`, `ARG`, `ENV`, `CMD`, `ENTRYPOINT`, `EXPOSE`, `LABEL`, `USER`, `VOLUME`, `WORKDIR`, `HEALTHCHECK`, `ONBUILD`, `SHELL`, `STOPSIGNAL`, `MAINTAINER` | keyword, in any case | |
| 273 | | `AS`, `NONE` | keyword | |
| 274 | | `# comment`, including the `# syntax=` and `# escape=` directives | comment | |
| 275 | | `--from=builder`, `--chown=me:me` | the flag name as an attribute | |
| 276 | | `$NAME`, `${NAME}`, `${NAME:-default}` | builtin, as one span to the closing brace | |
| 277 | | `"…"`, `'…'` | string | |
| 278 | | a trailing `\` | operator | |
| 279 | | numbers | number | |
| 280 | | paths and image references — `/usr/local/bin`, `node:24-alpine` | identifier, as **one** span | |
| 281 | |
| 282 | **Only the first word of a line can be an instruction**, and a word that is not one is an argument — which is what keeps a continuation line's first word out of the keyword colour. |
| 283 | |
| 284 | **Nothing crosses a line break.** A `\` joins two lines for Docker, but each half still reads as a command and is coloured on its own. |
| 285 | |
| 286 | | Not recognised | Because | |
| 287 | | --- | --- | |
| 288 | | The shell inside a `RUN` | It would mean running the shell scanner over part of a line and mapping its columns out, and `RUN` may hold any language | |
| 289 | | Heredocs in a `RUN` | The same reason the shell scanner does not recognise them | |
| 290 | | Which stage a `--from` names | Nothing here reads the rest of the file | |
| 291 | |
| 292 | ## See also |
| 293 | |
| 294 | - [Theme file format](themes.md) — every key these classes resolve to |
| 295 | - [Colouring and completion](../explanation/colouring-and-completion.md) — why the scanners are written this way |
| 296 | - [How to write your own theme](../how-to/write-a-theme.md) |