Comment notation
The standard way to activate Expressive Code features is with attributes on the fence line, such as {3} or ins={5}. The fence line is the first line of the code block, with the language.
This means you need to be careful when you edit the code in a code block.
If you add or remove any lines, the counts change and the attribute can point at the wrong code.
To avoid this, starlight-codeblocks also offers configuration via directives. A directive sits in a comment on or above the line that it marks. It moves with the code, so you don’t need to count line numbers.
Readers see
export const config = { host: 'localhost', protocol: 'https', port: 3000, port: Number(process.env.PORT ?? 3000), timeout: 5000,};You write
```ts title="config.ts"export const config = { // [!code highlight] host: 'localhost', protocol: 'https', port: 3000, // [!code --] port: Number(process.env.PORT ?? 3000), // [!code ++] timeout: 5000,};```Same example, built instead with attributes:
```ts title="config.ts" mark={2} del={4} ins={5}export const config = { host: 'localhost', protocol: 'https', port: 3000, port: Number(process.env.PORT ?? 3000), timeout: 5000,};```Each directive changes its line. Each comment holds only a directive, so the plugin removes the whole comment. The copy button copies the code without the directives.
Syntax
Section titled “Syntax”A directive is text in square brackets that starts with !.
It is written in a comment, using the appropriate comment syntax of the language of that code block.
Directives turn on many different features. <name> is the name of a directive, such as focus or ++. They all use one of these forms:
| Form | Meaning |
|---|---|
[!code <name>] |
Applies to the line it is on. |
[!code <name>:N] |
Applies to its line and the next N-1 lines. |
[!code <name> <name>] |
Applies each name to the line. |
[!<name>] <text> |
Shows <text> after the code, for example as a note or a message. The text stops at the next directive or at the end of the comment. |
[!code <name> <name>] <text> |
Applies each name to the line, and shows <text> for the first name that can show text. |
[!<name> /<text>/] |
Points at the first match of the literal text on its target line. |
[\!code <name>] |
Renders as the literal text [!code <name>]. |
For example, // [!code ++:3] marks its line and the next two lines as inserted. // [!code focus ++] Loads the plugin focuses its line and marks it as inserted, with “Loads the plugin” as the message of the inserted line.
Every directive can go at the end of the line it marks, or on its own line above it. The plugin removes each line that holds only directives, so it does not show on the page.
Sharing with ordinary comments
Section titled “Sharing with ordinary comments”A directive can share a comment with an ordinary comment. The plugin removes only the directive, and the rest of the comment stays in the code:
Readers see
const host = 'localhost'const port = 8080 // Default portYou write
```jsconst host = 'localhost'const port = 8080 // Default port [!code focus]```Text after a directive that takes text, such as [!code error] or [!callout], is the text of that directive. Put your own comment before the directive.
Combining directives
Section titled “Combining directives”One code block often needs several features: a focus, a changed line with a message, and a note to explain it. The features work together in one block, and each one keeps its own effect.
Several names in one directive
Section titled “Several names in one directive”One [!code …] directive can hold several names. Each name applies to the line. The text after the directive goes to the first name that takes text, here ++.
Readers see
import { defineConfig } from 'astro/config';import starlight from '@astrojs/starlight';
export default defineConfig({ integrations: [ starlight({ title: 'My docs', }), ],});You write
```js title="astro.config.mjs"import { defineConfig } from 'astro/config';import starlight from '@astrojs/starlight';import codeblocks from 'starlight-codeblocks'; // [!code focus ++] Import the plugin
export default defineConfig({ integrations: [ starlight({ title: 'My docs', plugins: [codeblocks()], // [!code focus ++] Load the plugin }), ],});```The two new lines are green, show their messages, and stay sharp. The other lines are blurred. To give two line states their own messages, use two lines.
Several directives in one comment
Section titled “Several directives in one comment”One comment can hold several directives. Each directive takes the text up to the next directive.
Readers see
import time
def fetch_with_retry(fetch, retries=3): for attempt in range(retries): try: return fetch()Warning: except TimeoutError:Warning Other errors fail at onceOnly a timeout is worth a retry.
time.sleep(2**attempt)Back off raise TimeoutError("no response")- Only a timeout is worth a retry.
You write
```py title="retry.py"import time
def fetch_with_retry(fetch, retries=3): # [!mention retries] for attempt in range(retries): try: return fetch() except TimeoutError: # [!code warning] Other errors fail at once [!annotate] Only a timeout is worth a retry. time.sleep(2**attempt) # [!code ++] Back off [!mention retries] raise TimeoutError("no response")```The except line has a warning and an annotation. The sleep line is new, and it is part of the retry loop with the first line.
Attributes and directives together
Section titled “Attributes and directives together”Attributes on the fence line and directives in comments add up. Here the fence line names a placeholder and hides the imports. The comments add a callout, a footnote and an error.
Readers see
You write
```py title="client.py" placeholder="YOUR_TOKEN" hidden={1-2}import osimport requests
session = requests.Session()# [!callout /headers/] Sent with every requestsession.headers["Authorization"] = "Bearer YOUR_TOKEN"# [!ref] The service closes idle connections after 30 s.session.timeout = 30session.verify = False # [!code error] Never turn off TLS checks```Readers can type their token into the field. The copy button copies their token, the hidden imports and none of the notes.
The kitchen sink
Section titled “The kitchen sink”This block turns on 15 features at once. It has focus, every line state and a word-level diff. It has annotations, mentions, a callout, a footnote and a code link. On the fence line, it adds a placeholder, hidden lines, visible whitespace, line permalinks, an expandable block and colourised brackets. Its title gets a file icon.
Do not publish a block like this. It shows that each feature keeps its own effect.
The health route and the app are mentions.
Readers see
import 'dotenv/config';import express from 'express';const app = express();const PORT = 3000;const PORT = Number(process.env.PORT ?? 3000);Read the port from the environmentapp.use(express.json());Parse JSON bodiesBefore any route, or req.body is undefined.
Checked on every requestapp.use((req, res, next) => (req.get('x-api-key') === 'YOUR_API_KEY' ? next() : res.sendStatus(401)));Success: res.json({ ok: true, uptime: process.uptime() });Success Always 200});Note:api.get('/users/:id', async (req, res) => {Note Needs a databaseError: const user = await db.users.find(req.params.id);Error db is not definedTo do: res.json(user ?? {});To do Return 404 when missingWarning:});Warning No error handlerapp.use('/api', api);Every route in api starts with /api.
app.listen(PORT, () => console.log('Listening on', PORT));- Before any route, or
req.bodyisundefined. - Every route in
apistarts with/api.
- 1.Load balancers call this route every 10 s.
You write
```js title="server.js" id="kitchen-sink" placeholder="YOUR_API_KEY" hidden={1-2} whitespace="all" expandable={12} bracketsimport 'dotenv/config';import express from 'express';
const app = express(); // [!mention app]const PORT = 3000; // [!code --]const PORT = Number(process.env.PORT ?? 3000); // [!code ++] Read the port from the environmentapp.use(express.json()); // [!code ++] Parse JSON bodies [!annotate] Before any route, or `req.body` is `undefined`.// [!callout /x-api-key/] Checked on every requestapp.use((req, res, next) => (req.get('x-api-key') === 'YOUR_API_KEY' ? next() : res.sendStatus(401)));// [!ref] Load balancers call this route every 10 s.app.get('/health', (req, res) => { // [!mention health] [!code focus:3] res.json({ ok: true, uptime: process.uptime() }); // [!code success] Always 200 [!mention health]});// [!link /express.Router/ https://expressjs.com/en/5x/api.html#router] Groups routes under one prefix.const api = express.Router(); // [!mention app]api.get('/users/:id', async (req, res) => { // [!code info] Needs a database const user = await db.users.find(req.params.id); // [!code error] `db` is not defined res.json(user ?? {}); // [!code todo] Return 404 when missing}); // [!code warning] No error handlerapp.use('/api', api); // [!code highlight] [!annotate] Every route in `api` starts with `/api`.app.listen(PORT, () => console.log('Listening on', PORT)); // [!mention app]```Behaviour
Section titled “Behaviour”The plugin reads the directives when the site builds. It needs no JavaScript in the browser.
- Removing directives:
- The plugin removes every directive from the rendered code and from the copied text.
- If a comment holds only directives, the plugin removes the whole comment and the whitespace before it. Other text in the comment stays.
- In JSX, TSX and MDX, the plugin also reads comments in braces,
{/* [!code focus] */}, and removes the braces with the comment. - A backslash after the bracket, as in
[\!code focus], turns a directive into plain text. The plugin removes the backslash.
- Expressive Code markers:
[!code highlight],[!code ++]and[!code --]give the same result as themark,insanddelattributes of Expressive Code. Text after them shows as a message, as for line states.
- Text and copy:
- Inline code, links and bold in the text of a directive render as HTML. Other Markdown stays as text.
- The copy button leaves out messages, annotations, callouts and footnotes.
Line numbers
Section titled “Line numbers”Line numbers in attributes count the lines that readers see. The plugin removes lines that hold only directives before it counts, so they do not change the numbers. This applies to the attributes of Expressive Code as well, such as {2} and ins={3}.
Warnings
Section titled “Warnings”The build logs a warning with the file, the code block and the line when:
- a directive has an unknown name, or the feature it belongs to is off;
- the
/<text>/of a directive has no match on its target line; - a directive on its own line has no line below it;
- a directive is not in a comment that the block reads, such as
<!-- -->in anmdxblock. A directive in a string does not give this warning.
The directive then has no effect. An unknown directive, or one outside a comment, stays in the code as written, so you can see it on the page.
Options
Section titled “Options”notation
Section titled “notation”Reads directives in code comments. Set it to false to turn the feature off. Every comment then renders as written.
- Type
false | object- Default
- On
notation.comments
Section titled “notation.comments”Comment syntax for each language, added to the built-in map. An empty list removes a language.
- Type
Record<string, string[]>- Default
{}
Each entry in notation.comments is a language and a list of comment syntaxes. A syntax with a space in it is a block comment: the opener, then the closer.
codeblocks({ notation: { comments: { cypher: ['//'], jinja: ['{# #}'], }, },});An entry replaces the built-in syntax of that language. A language name or its alias works: an entry for powershell also applies to ```pwsh. The built-in map:
| Comment syntax | Languages |
|---|---|
//, /* */ | c, c#, c++, cc, cjs, cpp, cs, csharp, cts, dart, go, groovy, h, hpp, java, javascript, js, json5, jsonc, kotlin, kt, kts, mjs, mts, nextflow, nf, php, rs, rust, scala, swift, ts, typescript |
//, /* */, {/* */} | jsx, tsx |
# | bash, console, docker, dockerfile, make, makefile, nix, perl, pl, powershell, ps, ps1, py, pycon, python, r, rb, ruby, sh, shell, shellscript, toml, yaml, yml, zsh |
-- | haskell, hs, lua, sql |
<!-- --> | html, markdown, md, svg, xml |
{/* */} | mdx |
<!-- -->, //, /* */ | astro, svelte, vue |
/* */ | css |
/* */, // | less, sass, scss |
% | erl, erlang, latex, tex |
; | clj, clojure, ini, lisp |
Comment syntax examples
Section titled “Comment syntax examples”Python and shell blocks use # comments:
Readers see
import json
data = json.loads(text)print(data["name"])You write
```py title="parse.py"import json
data = json.loads(text) # [!code highlight]print(data["name"])```HTML blocks use <!-- --> comments. :2 marks two lines with one directive:
Readers see
<nav> <a href="/">Home</a> <a href="/docs/">Docs</a></nav>You write
```html title="nav.html"<nav> <a href="/">Home</a> <!-- [!code ++:2] --> <a href="/docs/">Docs</a></nav>```To show a directive as text, for example in a page about this plugin, add a backslash:
Readers see
const port = 8080; // [!code highlight]You write
```js title="server.js"const port = 8080; // [\!code highlight]```Limitations
Section titled “Limitations”- JSON has no comments, so a
jsonblock cannot use directives. Usejsonc, or use attributes. - The plugin skips a comment opener inside a string that opens and closes on the same line, such as
"// [!code ++]". A string that continues on the next line, such as a template literal, is not skipped. Escape a directive in it with a backslash. - One comment on each line can hold directives. Text such as
[!NOTE]outside a comment, as in a Markdown alert, stays in the code. The plugin still reads the comment after it.
Related
Section titled “Related”- Focus: use
[!code focus]to blur the lines that do not matter. - Line states: use
[!code error]and the other states to tint a line and add a message.