---
title: "Comment notation"
description: "Mark lines with directives in code comments. The plugin applies each directive and removes it from the code that readers see and copy."
url: "https://ewels.github.io/starlight-codeblocks/comment-notation/"
markdown: "https://ewels.github.io/starlight-codeblocks/comment-notation.md"
section: "Start here"
site: "starlight-codeblocks documentation"
context: "This page is from the documentation of starlight-codeblocks. A Starlight plugin that adds focus, line states, annotations, API auto-linking and 22 more features to the code blocks of a site."
index: "https://ewels.github.io/starlight-codeblocks/llms.txt"
---

# Comment notation

> Mark lines with directives in code comments. The plugin applies each directive and removes it from the code that readers see and copy.

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.

````md
```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:_

````md
```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

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.

- [Directives reference](https://ewels.github.io/starlight-codeblocks/reference/directives/): See every directive and the feature it belongs to.

### 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:

````md
```js
const 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

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

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 `++`.

````md
```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

One comment can hold several directives. Each directive takes the text up to the next directive.

````md
```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](#mention:retries) with the first line.

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

````md
```py title="client.py" placeholder="YOUR_TOKEN" hidden={1-2}
import os
import requests

session = requests.Session()
# [!callout /headers/] Sent with every request
session.headers["Authorization"] = "Bearer YOUR_TOKEN"
# [!ref] The service closes idle connections after 30 s.
session.timeout = 30
session.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

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](#mention:health) and the [app](#mention:app) are mentions.

````md
```js title="server.js" id="kitchen-sink" placeholder="YOUR_API_KEY" hidden={1-2} whitespace="all" expandable={12} brackets
import '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 environment
app.use(express.json()); // [!code ++] Parse JSON bodies [!annotate] Before any route, or `req.body` is `undefined`.
// [!callout /x-api-key/] Checked on every request
app.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 handler
app.use('/api', api); // [!code highlight] [!annotate] Every route in `api` starts with `/api`.
app.listen(PORT, () => console.log('Listening on', PORT)); // [!mention app]
```
````

## 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 the `mark`, `ins` and `del` attributes of Expressive Code. Text after them shows as a message, as for [line states](https://ewels.github.io/starlight-codeblocks/features/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

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

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 an `mdx` block. 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

### `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`

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.

```js title="astro.config.mjs"
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` |

- [Configuration](https://ewels.github.io/starlight-codeblocks/configuration/): Learn how to set options for the whole site, or for a single code block.

## Comment syntax examples

Python and shell blocks use `#` comments:

````md
```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:

````md
```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:

````md
```js title="server.js"
const port = 8080; // [\!code highlight]
```
````

## Limitations

- JSON has no comments, so a `json` block cannot use directives. Use `jsonc`, 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

- [Focus](https://ewels.github.io/starlight-codeblocks/features/focus/): use `[!code focus]` to blur the lines that do not matter.
- [Line states](https://ewels.github.io/starlight-codeblocks/features/line-states/): use `[!code error]` and the other states to tint a line and add a message.
