Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .agents/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ The choices that the code does not explain, with the options that were rejected.
- **Every directive works at the end of its line or on a line of its own above it.** A comment with other text keeps that text, and its directives apply to the comment line. *Rejected:* own-line-only directives for callouts, links and footnotes (a rule to learn, for no rendering gain).
- **Features declare their directives on their plugin**, so each feature stays in its own file. *Rejected:* a central directive list.
- **`notation.comments` maps a language to several syntaxes**; C-family, Vue, Svelte and Astro also accept `//` and `/* */`. Languages resolve through Shiki ids and aliases. The bracket scanner uses the same map.
- **A known directive outside every comment that the block reads warns**, and names the syntaxes to use. A directive in a string, or escaped, does not: a string can show one on purpose, and `[\!` unescapes only in a comment. A language with no comment syntax, or set to `[]`, reads no directives and never warns. *Rejected:* a new opt-out attribute.
- **Code tabs are a Sätteri container directive**, not an MDX component: it works in `.md` and `.mdx`, and each variant stays its own EC block with its own title, frame and copy text.
- **Code tabs show editor tabs by default, and a menu with `control="menu"`.** The user found the menu clumsy; tabs also make a group of files look like an editor. Each variant renders EC's own editor tab (a variant with no title gets its label as the title, and the editor frame), and the client module turns it into a row with every variant's tab, cloned from their titles. So the selected tab is the one EC draws, a file name comment works as a title, and without JavaScript the first variant shows with its own tab and no dead buttons. A label is not a file name, so its tab drops the automatic file icon; `icon="…"` still adds one. *Rejected:* building the row at build time (EC renders each block alone, so a block cannot see the other titles); keeping the terminal frame for shell variants (it has no tabs); the old name "code switcher".
- **Inline highlighting:** the suffix inside the backticks is the documented form (`` `x{:js}` ``), as rehype-pretty-code; it needs no MDX escape and works with plugins that read `.md` as MDX. The form after the backtick still works. `{:txt}`, `{:plain}` and similar keep code plain. The inline engine reuses the site engine's captured themes with contrast correction off, and Astro's `shikiConfig` languages.
Expand Down
5 changes: 3 additions & 2 deletions docs/src/content/docs/comment-notation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -261,9 +261,10 @@ 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 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 stays in the code as written, so you can see it on the page.
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

Expand Down
17 changes: 15 additions & 2 deletions packages/starlight-codeblocks/src/expressive-code/notation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,11 +201,24 @@ export function parseLine(
const unchanged = { text, removed: false, directives: [] };
// A `[!word]` in code or in a string, such as a Markdown alert, comes before the comment that holds the directives.
let comment: ReturnType<typeof findComment>;
for (const token of scanTokens(text)) {
const tokens = scanTokens(text);
for (const token of tokens) {
comment = findComment(text, token.start, syntaxes);
if (comment) break;
}
if (!comment) return unchanged;
if (!comment) {
// A string can show a directive on purpose, and `[\!` only unescapes inside a comment.
for (const token of tokens) {
if (token.escaped || inString(text, token.start, token.start, [])) continue;
if ('problem' in interpret(token, specs, sourceLine)) continue;
const use = syntaxes.map(({ open, close }) => `\`${close ? `${open} ${close}` : open}\``).join(' or ');
report(
`\`${text.slice(token.start, token.end)}\` is not in a comment that this block reads. Use ${use}. It shows as text.`,
sourceLine,
);
}
return unchanged;
}
const [start, bodyStart, bodyEnd, end] = comment;
const body = text.slice(bodyStart, bodyEnd);
// Expressive Code strips a diff prefix only later, so `+ // [!code …]` still holds only directives.
Expand Down
25 changes: 25 additions & 0 deletions packages/starlight-codeblocks/test/notation-render.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,31 @@ test('warns about unknown directives with the file and line, and keeps them', as
expect((await render(block('constructor', 'a() // [!code focus]'))).html).toContain('[!code focus]');
});

test('warns about a directive that is not in a comment the block reads, and keeps it', async () => {
const mdx = await render(
block('mdx', '<!-- [!callout /import/] Import the component once. -->', "import X from 'x'"),
);
expect(mdx.copyText).toBe("<!-- [!callout /import/] Import the component once. -->\nimport X from 'x'");
expect(mdx.warnings).toEqual([
'src/content/docs/example.md, mdx code block, line 1: `[!callout /import/]` is not in a comment that this block reads. Use `{/* */}`. It shows as text.',
]);
expect((await render(block('py', 'x = 1 // [!code focus]'))).warnings).toEqual([
'src/content/docs/example.md, py code block, line 1: `[!code focus]` is not in a comment that this block reads. Use `#`. It shows as text.',
]);
});

test.each([
['py', 'x = 1 # [!code focus]', {}],
['js', 'a() // [\\!code focus]', {}],
['py', 'x = 1 // [\\!code focus]', {}],
['js', 'const s = "[!code focus]"', {}],
['md', '> [!NOTE]', {}],
['txt', 'a() # [!code focus]', {}],
['mdx', '<!-- [!code focus] -->', { notation: { comments: { mdx: [] } } }],
])('does not warn about %s %j', async (lang, line, options) => {
expect((await render(block(lang, line), options)).warnings).toEqual([]);
});

test('works with diff syntax and diff-prefixed directive lines', async () => {
const diff = await render(block('diff lang="js"', '-a() // [!code focus]', '+b() // [!code ++]'));
expect(diff.copyText).toBe('a()\nb()');
Expand Down
10 changes: 9 additions & 1 deletion packages/starlight-codeblocks/test/notation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,19 @@ describe('parseLine', () => {
'/* a */ x = "[!code focus]"',
'const s = "// [!code focus]"',
'const a = "http://x", b = "[!code focus]";',
'fetch("http://x") [!code focus]',
'arr[!flag]',
])('leaves %j unchanged', (input) => {
expect(parse(input)).toMatchObject({ text: input, directives: [], problems: [] });
});

test('reports a known directive outside a comment, and keeps it', () => {
expect(parse('fetch("http://x") [!code focus]')).toMatchObject({
text: 'fetch("http://x") [!code focus]',
directives: [],
problems: ['`[!code focus]` is not in a comment that this block reads. Use `//` or `/* */`. It shows as text.'],
});
});

test('reads a count, a message, a name and literal text, and ends a message at the next directive', () => {
expect(parse('x // [!code focus:3]').directives[0]).toMatchObject({ count: 3 });
expect(parse('x // [!code error] Missing `await`').directives[0]).toMatchObject({ text: 'Missing `await`' });
Expand Down
2 changes: 1 addition & 1 deletion skills/starlight-codeblocks/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ If a page shows one feature, keep the others out of its examples with these attr
- `<CodeWalkthrough>` and `<Scrollycoding>` work in MDX files only. Import them with `import { CodeWalkthrough, Scrollycoding, Step } from 'starlight-codeblocks/components';`. Put an empty line after the opening tag and before the closing tag.
- A `:::code-tabs` directive can contain only code blocks. A paragraph inside it fails the build.
- For inline code highlighting, put the suffix inside the backticks: `` `res.ok{:js}` ``. MDX reads `` `res.ok`{:js} `` as a JavaScript expression and the build fails.
- A directive with an unknown name, or of a feature that is off, stays in the code and logs a build warning. Read the build warnings after every change: each gives the file, the block and the line.
- A directive with an unknown name, or of a feature that is off, stays in the code and logs a build warning. So does a directive outside a comment that the block's language reads. Read the build warnings after every change: each gives the file, the block and the line.
- Each `id` for line permalinks must be unique on the page, and must not match a heading id.
- Hidden lines still run in the copied code, in playgrounds and with `runnable`. Code for a playground or the **Run code** button must be complete.
- Fill-in placeholder values stay in the browser's `localStorage` by default. For secrets such as API tokens, set `placeholders: { storage: 'session' }`.
Expand Down
Loading