turbo-editors/turbo-moonbitpublic Fork 0
main
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-moonbit.git
git clone ssh://git@rickub.com/turbo-editors/turbo-moonbit.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

📦 Turbo MoonBit cc1f595 · on main · k33g · 10h ago
languages.md · 296 lines · 19.4 KBmarkdown
Blame HistoryOpen raw

Reference: languages coloured

Neutral description of which files Turbo MoonBit colours, how it decides, and what each scanner recognises.

Recognition

A file's extension decides whenever it is one of these:

Extension Language
.mbt, .mbti, .mbtx MoonBit
.toml TOML
.yaml, .yml YAML
.md, .markdown Markdown
.js, .mjs, .cjs JavaScript
.html, .htm HTML
.xml, .xsd, .xsl, .xslt, .svg, .plist, .csproj, .pom XML
.sh, .bash, .zsh Shell
.dockerfile, .containerfile Dockerfile

Extensions are matched case-insensitively, and only the last one counts: README.mbt.md is Markdown, and main.mbt.backup is not MoonBit.

A file whose extension decides nothing is looked up by name next. Only files that carry no useful extension need this:

Name Language
Dockerfile, Containerfile Dockerfile

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.

moon.mod, moon.pkg and moon.work are not in that table. They are MoonBit's own configuration DSL rather than MoonBit, and their legacy JSON forms — moon.mod.json, moon.pkg.json — are not JSON that this editor colours either. All five open in plain text.

A file that neither table claims is read by its first line. A shebang naming a shell — sh, bash, zsh, dash or ksh — makes it a shell script, and the interpreter is recognised as a path element or as the argument to env. That is what colours a script in a bin directory, a git hook, or configure.

No shebang makes a file MoonBit. The language has no interpreter line: a file opening with #! would lex as an attribute named ! and fail. A file with no extension is not MoonBit, and claiming otherwise would take a shell script away from the scanner that can actually colour it.

First line Result
#!/bin/sh Shell
#!/usr/bin/env bash Shell
#!/usr/bin/env -S bash -e Shell
#!/usr/bin/env moon Not coloured
#!/usr/bin/env node Not coloured
Anything not starting #! Not coloured

The order is fixed — extension, then name, then first line — and the first to decide wins.

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.

Classes

Every scanner produces the same vocabulary of classes, and each maps to one theme key.

Class Theme key Produced by
identifier syntax.identifier MoonBit, TOML, JavaScript, shell, YAML, Dockerfile
keyword syntax.keyword MoonBit, JavaScript, shell, HTML (doctype), XML, Dockerfile
type syntax.type MoonBit (every capitalised name, and package qualifiers), TOML (table headers), YAML (tags)
builtin syntax.builtin MoonBit (the prelude), JavaScript, shell (builtins and expansions), YAML (anchors and aliases), Dockerfile (variables)
constant syntax.constant MoonBit, TOML, JavaScript, shell, YAML, HTML and XML (entities)
function syntax.function MoonBit, JavaScript, shell (the command)
string syntax.string all
char syntax.char MoonBit ('c' and b'c')
number syntax.number MoonBit, TOML, JavaScript, shell, YAML, Dockerfile
comment syntax.comment MoonBit, TOML, JavaScript, shell, HTML, YAML, XML, Dockerfile
operator syntax.operator MoonBit, TOML, JavaScript, shell, HTML, YAML (block scalar headers), XML, Dockerfile
punctuation syntax.punctuation MoonBit, TOML, JavaScript, shell, Markdown, YAML, Dockerfile
heading syntax.heading Markdown
tag syntax.tag HTML, XML
attribute syntax.attribute MoonBit (attributes and labelled arguments), HTML, XML, Dockerfile (flags)
emphasis syntax.emphasis Markdown
link syntax.link Markdown

In turbo-classic alone, syntax.attribute and syntax.identifier are both plain yellow, so a MoonBit attribute or label is not told apart from an ordinary name in that one theme. The other seven give them different colours. See how to write your own theme if you want to change it.

MoonBit

Hand-written, in internal/moonbitlang. Nothing crosses a line break, and that is a property of the language rather than a simplification: MoonBit has no block comment, a newline before a closing quote is an unterminated literal error, a multi-line string is a run of self-contained #| or $| lines, and an attribute is explicitly one line. So a stray quote colours to the end of its line and the next line is code again.

Recognised As
and, as, async, break, catch, const, continue, declare, defer, derive, else, enum, enumview, extend, extenum, extern, fn, for, guard, if, impl, import, in, is, let, letrec, lexscan, loop, match, mut, nobreak, nocancel, noraise, package, priv, proof_assert, proof_let, pub, raise, readonly, return, struct, suberror, test, throw, trait, try, type, using, where, while, with keyword
try! and guard!, mark included keyword
true, false, None, Some, Ok, Err constant
any name starting with an ASCII capital — Int, StringBuilder, Shape, Circle type
println, abort, panic, fail, ignore, inspect, debug, repr, hash, compare, null, assert_eq, assert_not_eq, assert_true, assert_false, debug_assert, debug_inspect, json_inspect, physical_equal builtin
any other lower-case name immediately before ( function
"…", b"…", re"…" string
'c', b'c' char
#| and $| the two-rune prefix as punctuation, the rest of the line as string
42, 1_000, 0xFF_FF, 0o17, 0b1010, 1.5, 1., 1.5e-3, 0x1.8p3F, 42U, 42L, 42UL, 42N, 1.0F number
// and /// to the end of the line comment
#deprecated("…"), #external, #custom.attribute(key="v") — the whole line attribute
name~ in a labelled argument, tilde included attribute
@json, @moonbitlang/core/builtin, @my-pkg — the @ included, as one span type
.0 in a tuple accessor the dot as punctuation, the digits as number
.., ..=, ..<, ... operator
runs of +-*/%=<>!&|^~?: operator
()[]{},;. punctuation

Nothing here is a table of built-in types, and nothing needs to be. MoonBit's identifier case is a lexical rule rather than a convention: the grammar says a uident "begins with an ASCII uppercase letter", and only a type, a trait or an enum constructor may be spelt that way. Int, StringBuilder and a type somebody wrote this morning are all coloured by the same line. Every other scanner in this family needs a table here; this one does not.

An integer ends before ... The grammar is explicit — "before .., an integer ends first, so 1..=2 begins with 1 and ..=" — so a dot is only part of a number when a second one does not follow it. Without that rule 1..=2 reads as the double 1. and then .=2, and every range in the file is miscoloured.

A number's suffix is upper case or it is not a suffix. 42UL is one number; 42u is the number 42 followed by the name u, which is what the compiler sees too.

An attribute takes the whole line. The grammar hands it everything after the dotted name: "everything through the next newline is the raw payload". Colouring less than the line would be inventing a structure the lexer does not have.

#| and #deprecated are told apart by the rune after the #. An attribute's name must start with a letter or an underscore; a multi-line string line has a bar there.

A doc comment is coloured like any other comment. ///, ///| and // all reach syntax.comment, because turbo-core's set of classes is closed on purpose — that is what lets one theme colour every language an editor will ever learn.

A name after a dot is never a keyword. MoonBit's dot-identifiers "use the identifier case rules without consulting the keyword table, so .if is valid" — a record with a field called type is ordinary MoonBit.

package is coloured as a keyword in a .mbt file too, although it is only a reserved word there. It is a real keyword in the .mbti interface files this editor also colours, and in a .mbt file the colour says exactly what the compiler is about to: this word is not yours to use. The rest of the reserved list — move, ref, static, unsafe, await and the forty others — is deliberately left alone, because those really are names you may use.

A tilde against the end of a lower-case name is a label, and against anything else it is not: the grammar says "ASCII-uppercase identifiers and keywords cannot form labels", so Foo~ is a type and a tilde.

Not recognised, each for a stated reason:

Not recognised Because
The expression inside \{…} The grammar matches it to "the matching }", with braces inside nested literals not counting — finding the end needs the parser. "a \{b} c" is therefore one string span, brace to brace. A string nested inside an interpolation is where that stops: the scanner takes the first unescaped quote as the closer, so "a \{f("x")} c" scans as string, then x as an identifier, then string. The spans stay in order and never overlap; the cost is a wrong colour inside a nested literal, which is rarer than the brace-counting bugs the alternative would cause
An enum constructor of your own, as anything but a type Nothing in the syntax separates Circle(1.0) from a type applied to arguments; inventing a separation means being wrong in both directions instead of one
.5 as a number MoonBit requires a digit before the point, so a leading dot is a tuple accessor or a dot-identifier and never a literal
A reserved word as a keyword move, ref and the rest are identifiers the compiler merely warns about, and colouring them would tell a reader they cannot write let ref = 1 when they can
An identifier holding non-ASCII letters MoonBit allows CJK and several other ranges in a name; the rune predicates this scanner is built on are ASCII, so such a name is stepped over uncoloured rather than guessed at
.mbt.md as MoonBit It is a Markdown document with MoonBit in its fences. Its extension is .md, and Markdown is what colours it
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

TOML

Recognised As
# comment comment
[table], [[array]] the name as a type, the brackets as punctuation
key = identifier, then operator
"basic", 'literal', """multi-line""", '''multi-line''' string
true, false constant
numbers, dates, times, inf, nan number

YAML

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.

Recognised As
# comment comment
key: before a space or the end of the line the key as an identifier, the colon as punctuation
"quoted": 1, 'quoted': 1 the quoted key as an identifier
- opening a sequence entry punctuation
"…", '…' string
true, false, yes, no, on, off, null constant, whatever their case
numbers, dates and times written without quotes number
&anchor, *alias builtin
!!str, !Custom type
---, ... the whole line as punctuation
{, }, [, ], , punctuation
|, >, with their chomping and indentation indicators the header as an operator, the body as a string

A colon is a separator only when a space or the end of the line follows it. image: nginx:1.27 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.

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.

A # needs a space before it to start a comment, so colour: ff#00aa is one scalar.

Not recognised Because
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
Multi-document streams as separate documents --- is coloured, but nothing is reset at it; nothing in the colouring depends on document boundaries
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

Markdown

Recognised As
# Heading###### Heading the whole line as a heading
**bold**, __bold__, *italic*, _italic_ emphasis
`code` string
[text](target), ![alt](src) the whole thing as a link
- , * , + , 1. , 1) the marker as punctuation
> punctuation
---, ***, ___ punctuation
``` and ~~~ fences the whole block, opening and closing lines included, as a string

A fenced block is one colour whatever language it announces: ```moonbit does not colour its contents as MoonBit. 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.

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.

JavaScript

Recognised As
const, let, function, class, async, await, import, export, … keyword
true, false, null, undefined, NaN, Infinity, this constant
console, document, window, Array, Object, Promise, Math, JSON, … builtin
a name immediately before ( function
"…", '…' string
`…`, interpolations included, across lines string
// to end of line, /* … */ across lines comment
42, 3.14, 0x1f, 0b1010, 0o777, 1_000_000, 1e6, 10n number
runs of `+-*/%=<>!& ^~?:`
()[]{},;. punctuation

Regular-expression literals are not recognised. Telling /x/g from a division needs to know whether the previous token could end an expression; a wrong guess colours the rest of a line as a string, which is worse than leaving a regex the colour of an operator.

Globals are recognised by name, so a file that shadows Math still has it coloured as a builtin — the same rule MoonBit's builtins follow here.

HTML

Recognised As
<tag, </tag, >, /> tag
attribute names, including data-*, xlink:href, @click, v-bind.prop attribute
= operator
"…", '…' string
<!-- … -->, across lines comment
&amp;, &#169; constant
<!DOCTYPE …> and other declarations keyword

Text between tags is not coloured. A bare & with no ; within 32 characters is left alone, because it is legal text.

The contents of <script> and <style> are not coloured as JavaScript and CSS.

XML

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.

Recognised As
<?xml version="1.0"?> and other processing instructions the target and ?> as keyword, the pairs between as attributes and strings
<!DOCTYPE …> and the other <! forms keyword
<!-- … -->, across lines comment
<![CDATA[ … ]]>, across lines string
<tag, </tag, >, /> tag
<ns:tag>, xsi:type the prefix and the local name as one span
attribute names attribute
= operator
"…", '…' string
&amp;, &#169; constant

A comment and a CDATA section close on different delimiters, and are carried separately: a --> inside a CDATA section does not end it.

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.

Text between tags is not coloured.

Shell

Applies to sh, bash and zsh alike: the keywords recognised are the ones they share.

Recognised As
if, then, fi, for, while, case, esac, function, return, … keyword
true, false constant
echo, printf, export, local, read, cd, set, source, … builtin
$NAME, ${…}, $(…), $1, $?, $@ builtin
the first bare word on a line function
every later bare word, and NAME in NAME=value identifier
'…', with nothing escaped or expanded inside string
"…", with the expansions inside it coloured as expansions string
# to end of line comment

$(a $(b) c) is one span: nesting is counted. An option such as -euo is one word, not a minus and a word.

Heredocs are not recognised. <<EOF and the text after it are coloured as ordinary shell.

Dockerfile

Recognised As
FROM, RUN, COPY, ADD, ARG, ENV, CMD, ENTRYPOINT, EXPOSE, LABEL, USER, VOLUME, WORKDIR, HEALTHCHECK, ONBUILD, SHELL, STOPSIGNAL, MAINTAINER keyword, in any case
AS, NONE keyword
# comment, including the # syntax= and # escape= directives comment
--from=builder, --chown=me:me the flag name as an attribute
$NAME, ${NAME}, ${NAME:-default} builtin, as one span to the closing brace
"…", '…' string
a trailing \ operator
numbers number
paths and image references — /usr/local/bin, golang:1.26-alpine identifier, as one span

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.

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.

Not recognised Because
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
Heredocs in a RUN The same reason the shell scanner does not recognise them
Which stage a --from names Nothing here reads the rest of the file

See also

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
# Reference: languages coloured

> Neutral description of which files Turbo MoonBit colours, how it decides, and what each scanner recognises.

## Recognition

A file's **extension** decides whenever it is one of these:

| Extension | Language |
| --- | --- |
| `.mbt`, `.mbti`, `.mbtx` | MoonBit |
| `.toml` | TOML |
| `.yaml`, `.yml` | YAML |
| `.md`, `.markdown` | Markdown |
| `.js`, `.mjs`, `.cjs` | JavaScript |
| `.html`, `.htm` | HTML |
| `.xml`, `.xsd`, `.xsl`, `.xslt`, `.svg`, `.plist`, `.csproj`, `.pom` | XML |
| `.sh`, `.bash`, `.zsh` | Shell |
| `.dockerfile`, `.containerfile` | Dockerfile |

Extensions are matched case-insensitively, and only the last one counts: `README.mbt.md` is Markdown, and `main.mbt.backup` is not MoonBit.

A file whose extension decides nothing is looked up by **name** next. Only files that carry no useful extension need this:

| Name | Language |
| --- | --- |
| `Dockerfile`, `Containerfile` | Dockerfile |

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.

`moon.mod`, `moon.pkg` and `moon.work` are **not** in that table. They are MoonBit's own configuration DSL rather than MoonBit, and their legacy JSON forms — `moon.mod.json`, `moon.pkg.json` — are not JSON that this editor colours either. All five open in plain text.

A file that neither table claims is read by its **first line**. A shebang naming a shell — `sh`, `bash`, `zsh`, `dash` or `ksh` — makes it a shell script, and the interpreter is recognised as a path element or as the argument to `env`. That is what colours a script in a `bin` directory, a git hook, or `configure`.

**No shebang makes a file MoonBit.** The language has no interpreter line: a file opening with `#!` would lex as an attribute named `!` and fail. A file with no extension is not MoonBit, and claiming otherwise would take a shell script away from the scanner that can actually colour it.

| First line | Result |
| --- | --- |
| `#!/bin/sh` | Shell |
| `#!/usr/bin/env bash` | Shell |
| `#!/usr/bin/env -S bash -e` | Shell |
| `#!/usr/bin/env moon` | Not coloured |
| `#!/usr/bin/env node` | Not coloured |
| Anything not starting `#!` | Not coloured |

The order is fixed — extension, then name, then first line — and the first to decide wins.

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.

## Classes

Every scanner produces the same vocabulary of classes, and each maps to one theme key.

| Class | Theme key | Produced by |
| --- | --- | --- |
| `identifier` | `syntax.identifier` | MoonBit, TOML, JavaScript, shell, YAML, Dockerfile |
| `keyword` | `syntax.keyword` | MoonBit, JavaScript, shell, HTML (doctype), XML, Dockerfile |
| `type` | `syntax.type` | MoonBit (every capitalised name, and package qualifiers), TOML (table headers), YAML (tags) |
| `builtin` | `syntax.builtin` | MoonBit (the prelude), JavaScript, shell (builtins and expansions), YAML (anchors and aliases), Dockerfile (variables) |
| `constant` | `syntax.constant` | MoonBit, TOML, JavaScript, shell, YAML, HTML and XML (entities) |
| `function` | `syntax.function` | MoonBit, JavaScript, shell (the command) |
| `string` | `syntax.string` | all |
| `char` | `syntax.char` | MoonBit (`'c'` and `b'c'`) |
| `number` | `syntax.number` | MoonBit, TOML, JavaScript, shell, YAML, Dockerfile |
| `comment` | `syntax.comment` | MoonBit, TOML, JavaScript, shell, HTML, YAML, XML, Dockerfile |
| `operator` | `syntax.operator` | MoonBit, TOML, JavaScript, shell, HTML, YAML (block scalar headers), XML, Dockerfile |
| `punctuation` | `syntax.punctuation` | MoonBit, TOML, JavaScript, shell, Markdown, YAML, Dockerfile |
| `heading` | `syntax.heading` | Markdown |
| `tag` | `syntax.tag` | HTML, XML |
| `attribute` | `syntax.attribute` | MoonBit (attributes and labelled arguments), HTML, XML, Dockerfile (flags) |
| `emphasis` | `syntax.emphasis` | Markdown |
| `link` | `syntax.link` | Markdown |

In `turbo-classic` alone, `syntax.attribute` and `syntax.identifier` are both plain yellow, so a MoonBit attribute or label is not told apart from an ordinary name in that one theme. The other seven give them different colours. See [how to write your own theme](../how-to/write-a-theme.md) if you want to change it.

## MoonBit

Hand-written, in `internal/moonbitlang`. **Nothing crosses a line break**, and that is a property of the language rather than a simplification: MoonBit has no block comment, a newline before a closing quote is an *unterminated literal* error, a multi-line string is a run of self-contained `#|` or `$|` lines, and an attribute is explicitly one line. So a stray quote colours to the end of its line and the next line is code again.

| Recognised | As |
| --- | --- |
| `and`, `as`, `async`, `break`, `catch`, `const`, `continue`, `declare`, `defer`, `derive`, `else`, `enum`, `enumview`, `extend`, `extenum`, `extern`, `fn`, `for`, `guard`, `if`, `impl`, `import`, `in`, `is`, `let`, `letrec`, `lexscan`, `loop`, `match`, `mut`, `nobreak`, `nocancel`, `noraise`, `package`, `priv`, `proof_assert`, `proof_let`, `pub`, `raise`, `readonly`, `return`, `struct`, `suberror`, `test`, `throw`, `trait`, `try`, `type`, `using`, `where`, `while`, `with` | keyword |
| `try!` and `guard!`, mark included | keyword |
| `true`, `false`, `None`, `Some`, `Ok`, `Err` | constant |
| any name starting with an ASCII capital — `Int`, `StringBuilder`, `Shape`, `Circle` | type |
| `println`, `abort`, `panic`, `fail`, `ignore`, `inspect`, `debug`, `repr`, `hash`, `compare`, `null`, `assert_eq`, `assert_not_eq`, `assert_true`, `assert_false`, `debug_assert`, `debug_inspect`, `json_inspect`, `physical_equal` | builtin |
| any other lower-case name immediately before `(` | function |
| `"…"`, `b"…"`, `re"…"` | string |
| `'c'`, `b'c'` | char |
| `#\|` and `$\|` | the two-rune prefix as punctuation, the rest of the line as string |
| `42`, `1_000`, `0xFF_FF`, `0o17`, `0b1010`, `1.5`, `1.`, `1.5e-3`, `0x1.8p3F`, `42U`, `42L`, `42UL`, `42N`, `1.0F` | number |
| `//` and `///` to the end of the line | comment |
| `#deprecated("…")`, `#external`, `#custom.attribute(key="v")` — the whole line | attribute |
| `name~` in a labelled argument, tilde included | attribute |
| `@json`, `@moonbitlang/core/builtin`, `@my-pkg` — the `@` included, as one span | type |
| `.0` in a tuple accessor | the dot as punctuation, the digits as number |
| `..`, `..=`, `..<`, `...` | operator |
| runs of `+-*/%=<>!&\|^~?:` | operator |
| `()[]{},;.` | punctuation |

**Nothing here is a table of built-in types, and nothing needs to be.** MoonBit's identifier case is a *lexical* rule rather than a convention: the grammar says a `uident` "begins with an ASCII uppercase letter", and only a type, a trait or an enum constructor may be spelt that way. `Int`, `StringBuilder` and a type somebody wrote this morning are all coloured by the same line. Every other scanner in this family needs a table here; this one does not.

**An integer ends before `..`.** The grammar is explicit — "before `..`, an integer ends first, so `1..=2` begins with `1` and `..=`" — so a dot is only part of a number when a second one does not follow it. Without that rule `1..=2` reads as the double `1.` and then `.=2`, and every range in the file is miscoloured.

**A number's suffix is upper case or it is not a suffix.** `42UL` is one number; `42u` is the number `42` followed by the name `u`, which is what the compiler sees too.

**An attribute takes the whole line.** The grammar hands it everything after the dotted name: "everything through the next newline is the raw payload". Colouring less than the line would be inventing a structure the lexer does not have.

**`#|` and `#deprecated` are told apart by the rune after the `#`.** An attribute's name must start with a letter or an underscore; a multi-line string line has a bar there.

**A doc comment is coloured like any other comment.** `///`, `///|` and `//` all reach `syntax.comment`, because turbo-core's set of classes is closed on purpose — that is what lets one theme colour every language an editor will ever learn.

**A name after a dot is never a keyword.** MoonBit's dot-identifiers "use the identifier case rules without consulting the keyword table, so `.if` is valid" — a record with a field called `type` is ordinary MoonBit.

**`package` is coloured as a keyword in a `.mbt` file too**, although it is only a *reserved* word there. It is a real keyword in the `.mbti` interface files this editor also colours, and in a `.mbt` file the colour says exactly what the compiler is about to: this word is not yours to use. The rest of the reserved list — `move`, `ref`, `static`, `unsafe`, `await` and the forty others — is deliberately left alone, because those really are names you may use.

**A tilde against the end of a lower-case name is a label**, and against anything else it is not: the grammar says "ASCII-uppercase identifiers and keywords cannot form labels", so `Foo~` is a type and a tilde.

**Not recognised**, each for a stated reason:

| Not recognised | Because |
| --- | --- |
| The expression inside `\{…}` | The grammar matches it to "the matching `}`", with braces inside nested literals not counting — finding the end needs the parser. `"a \{b} c"` is therefore one string span, brace to brace. **A string nested inside an interpolation is where that stops**: the scanner takes the first unescaped quote as the closer, so `"a \{f("x")} c"` scans as string, then `x` as an identifier, then string. The spans stay in order and never overlap; the cost is a wrong colour inside a nested literal, which is rarer than the brace-counting bugs the alternative would cause |
| An enum constructor of your own, as anything but a type | Nothing in the syntax separates `Circle(1.0)` from a type applied to arguments; inventing a separation means being wrong in both directions instead of one |
| `.5` as a number | MoonBit requires a digit before the point, so a leading dot is a tuple accessor or a dot-identifier and never a literal |
| A reserved word as a keyword | `move`, `ref` and the rest are identifiers the compiler merely warns about, and colouring them would tell a reader they cannot write `let ref = 1` when they can |
| An identifier holding non-ASCII letters | MoonBit allows CJK and several other ranges in a name; the rune predicates this scanner is built on are ASCII, so such a name is stepped over uncoloured rather than guessed at |
| `.mbt.md` as MoonBit | It is a Markdown document with MoonBit in its fences. Its extension is `.md`, and Markdown is what colours it |
| 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) |

## TOML

| Recognised | As |
| --- | --- |
| `# comment` | comment |
| `[table]`, `[[array]]` | the name as a type, the brackets as punctuation |
| `key =` | identifier, then operator |
| `"basic"`, `'literal'`, `"""multi-line"""`, `'''multi-line'''` | string |
| `true`, `false` | constant |
| numbers, dates, times, `inf`, `nan` | number |

## YAML

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.

| Recognised | As |
| --- | --- |
| `# comment` | comment |
| `key:` before a space or the end of the line | the key as an identifier, the colon as punctuation |
| `"quoted": 1`, `'quoted': 1` | the quoted key as an identifier |
| `- ` opening a sequence entry | punctuation |
| `"…"`, `'…'` | string |
| `true`, `false`, `yes`, `no`, `on`, `off`, `null` | constant, whatever their case |
| numbers, dates and times written without quotes | number |
| `&anchor`, `*alias` | builtin |
| `!!str`, `!Custom` | type |
| `---`, `...` | the whole line as punctuation |
| `{`, `}`, `[`, `]`, `,` | punctuation |
| `\|`, `>`, with their chomping and indentation indicators | the header as an operator, the body as a string |

**A colon is a separator only when a space or the end of the line follows it.** `image: nginx:1.27` 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.

**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.

**A `#` needs a space before it to start a comment**, so `colour: ff#00aa` is one scalar.

| Not recognised | Because |
| --- | --- |
| 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 |
| Multi-document streams as separate documents | `---` is coloured, but nothing is reset at it; nothing in the colouring depends on document boundaries |
| 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 |

## Markdown

| Recognised | As |
| --- | --- |
| `# Heading``###### Heading` | the whole line as a heading |
| `**bold**`, `__bold__`, `*italic*`, `_italic_` | emphasis |
| `` `code` `` | string |
| `[text](target)`, `![alt](src)` | the whole thing as a link |
| `- `, `* `, `+ `, `1. `, `1) ` | the marker as punctuation |
| `>` | punctuation |
| `---`, `***`, `___` | punctuation |
| ` ``` ` and `~~~` fences | the whole block, opening and closing lines included, as a string |

A fenced block is **one colour whatever language it announces**: ```` ```moonbit ```` does not colour its contents as MoonBit. 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.

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.

## JavaScript

| Recognised | As |
| --- | --- |
| `const`, `let`, `function`, `class`, `async`, `await`, `import`, `export`, … | keyword |
| `true`, `false`, `null`, `undefined`, `NaN`, `Infinity`, `this` | constant |
| `console`, `document`, `window`, `Array`, `Object`, `Promise`, `Math`, `JSON`, … | builtin |
| a name immediately before `(` | function |
| `"…"`, `'…'` | string |
| `` `` ``, interpolations included, across lines | string |
| `//` to end of line, `/* … */` across lines | comment |
| `42`, `3.14`, `0x1f`, `0b1010`, `0o777`, `1_000_000`, `1e6`, `10n` | number |
| runs of `+-*/%=<>!&|^~?:` | operator |
| `()[]{},;.` | punctuation |

**Regular-expression literals are not recognised.** Telling `/x/g` from a division needs to know whether the previous token could end an expression; a wrong guess colours the rest of a line as a string, which is worse than leaving a regex the colour of an operator.

Globals are recognised by name, so a file that shadows `Math` still has it coloured as a builtin — the same rule MoonBit's builtins follow here.

## HTML

| Recognised | As |
| --- | --- |
| `<tag`, `</tag`, `>`, `/>` | tag |
| attribute names, including `data-*`, `xlink:href`, `@click`, `v-bind.prop` | attribute |
| `=` | operator |
| `"…"`, `'…'` | string |
| `<!-- … -->`, across lines | comment |
| `&amp;`, `&#169;` | constant |
| `<!DOCTYPE …>` and other declarations | keyword |

Text between tags is not coloured. A bare `&` with no `;` within 32 characters is left alone, because it is legal text.

**The contents of `<script>` and `<style>` are not coloured** as JavaScript and CSS.

## XML

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.

| Recognised | As |
| --- | --- |
| `<?xml version="1.0"?>` and other processing instructions | the target and `?>` as keyword, the pairs between as attributes and strings |
| `<!DOCTYPE …>` and the other `<!` forms | keyword |
| `<!-- … -->`, across lines | comment |
| `<![CDATA[ … ]]>`, across lines | string |
| `<tag`, `</tag`, `>`, `/>` | tag |
| `<ns:tag>`, `xsi:type` | the prefix and the local name as **one** span |
| attribute names | attribute |
| `=` | operator |
| `"…"`, `'…'` | string |
| `&amp;`, `&#169;` | constant |

**A comment and a CDATA section close on different delimiters**, and are carried separately: a `-->` inside a CDATA section does not end it.

**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.

Text between tags is not coloured.

## Shell

Applies to `sh`, `bash` and `zsh` alike: the keywords recognised are the ones they share.

| Recognised | As |
| --- | --- |
| `if`, `then`, `fi`, `for`, `while`, `case`, `esac`, `function`, `return`, … | keyword |
| `true`, `false` | constant |
| `echo`, `printf`, `export`, `local`, `read`, `cd`, `set`, `source`, … | builtin |
| `$NAME`, `${…}`, `$(…)`, `$1`, `$?`, `$@` | builtin |
| the **first bare word on a line** | function |
| every later bare word, and `NAME` in `NAME=value` | identifier |
| `'…'`, with nothing escaped or expanded inside | string |
| `"…"`, with the expansions inside it coloured as expansions | string |
| `#` to end of line | comment |

`$(a $(b) c)` is one span: nesting is counted. An option such as `-euo` is one word, not a minus and a word.

**Heredocs are not recognised.** `<<EOF` and the text after it are coloured as ordinary shell.

## Dockerfile

| Recognised | As |
| --- | --- |
| `FROM`, `RUN`, `COPY`, `ADD`, `ARG`, `ENV`, `CMD`, `ENTRYPOINT`, `EXPOSE`, `LABEL`, `USER`, `VOLUME`, `WORKDIR`, `HEALTHCHECK`, `ONBUILD`, `SHELL`, `STOPSIGNAL`, `MAINTAINER` | keyword, in any case |
| `AS`, `NONE` | keyword |
| `# comment`, including the `# syntax=` and `# escape=` directives | comment |
| `--from=builder`, `--chown=me:me` | the flag name as an attribute |
| `$NAME`, `${NAME}`, `${NAME:-default}` | builtin, as one span to the closing brace |
| `"…"`, `'…'` | string |
| a trailing `\` | operator |
| numbers | number |
| paths and image references — `/usr/local/bin`, `golang:1.26-alpine` | identifier, as **one** span |

**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.

**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.

| Not recognised | Because |
| --- | --- |
| 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 |
| Heredocs in a `RUN` | The same reason the shell scanner does not recognise them |
| Which stage a `--from` names | Nothing here reads the rest of the file |

## See also

- [Theme file format](themes.md) — every key these classes resolve to
- [Colouring and completion](../explanation/colouring-and-completion.md) — why the scanners are written this way
- [How to write your own theme](../how-to/write-a-theme.md)