From 5daaa993d5996b14a2ee6354bd8046e789fb0ad1 Mon Sep 17 00:00:00 2001 From: Phil Ewels Date: Wed, 7 Oct 2026 00:56:21 +0200 Subject: [PATCH 1/3] astro: support Astro sites without Starlight, with an example site Add `starlight-codeblocks/astro`, an Astro integration that adds Expressive Code with every feature to a site without Starlight. Code tabs and inline code highlighting are opt-in there, because they change every Markdown page, and unclaimed directives go back to their source text. `@astrojs/starlight` is now an optional peer dependency. Add a minimal example site in `examples/astro/`, built into the docs at `/examples/astro/`, and an "Astro without Starlight" docs page. Describe the package as a plugin for Starlight and Astro. Co-Authored-By: Claude Opus 5.5 (1M context) --- .agents/DECISIONS.md | 4 +- AGENTS.md | 7 +- README.md | 4 +- docs/astro.config.mjs | 6 +- .../content/docs/astro-without-starlight.mdx | 96 ++++++++ docs/src/content/docs/features/code-tabs.mdx | 6 +- .../features/inline-code-highlighting.mdx | 10 +- docs/src/content/docs/features/run-code.mdx | 2 +- .../docs/features/visible-whitespace.mdx | 2 +- docs/src/content/docs/getting-started.mdx | 12 +- docs/src/content/docs/index.mdx | 4 +- .../reference/expressive-code-plugins.mdx | 7 +- docs/src/sidebar.mjs | 1 + examples/astro/README.md | 14 ++ examples/astro/astro.config.mjs | 50 ++++ examples/astro/package.json | 24 ++ examples/astro/src/layouts/Page.astro | 214 ++++++++++++++++++ examples/astro/src/pages/index.mdx | 84 +++++++ examples/astro/tsconfig.json | 5 + package.json | 4 +- packages/starlight-codeblocks/README.md | 4 +- packages/starlight-codeblocks/package.json | 17 +- packages/starlight-codeblocks/src/astro.ts | 99 ++++++++ .../src/expressive-code/swatches.ts | 11 +- packages/starlight-codeblocks/src/index.ts | 81 +------ .../starlight-codeblocks/src/integration.ts | 85 ++++++- .../starlight-codeblocks/src/satteri/index.ts | 18 ++ .../starlight-codeblocks/test/astro.test.ts | 113 +++++++++ .../starlight-codeblocks/tsdown.config.ts | 8 +- pnpm-lock.yaml | 138 ++++++++++- pnpm-workspace.yaml | 1 + scripts/og-cards.mjs | 2 +- skills/starlight-codeblocks/SKILL.md | 3 +- .../references/configuration.md | 20 +- .../references/copy-and-run.md | 2 +- 35 files changed, 1037 insertions(+), 121 deletions(-) create mode 100644 docs/src/content/docs/astro-without-starlight.mdx create mode 100644 examples/astro/README.md create mode 100644 examples/astro/astro.config.mjs create mode 100644 examples/astro/package.json create mode 100644 examples/astro/src/layouts/Page.astro create mode 100644 examples/astro/src/pages/index.mdx create mode 100644 examples/astro/tsconfig.json create mode 100644 packages/starlight-codeblocks/src/astro.ts create mode 100644 packages/starlight-codeblocks/test/astro.test.ts diff --git a/.agents/DECISIONS.md b/.agents/DECISIONS.md index ce515df..e666a61 100644 --- a/.agents/DECISIONS.md +++ b/.agents/DECISIONS.md @@ -19,6 +19,8 @@ The choices that the code does not explain, with the options that were rejected. - **All options go to `codeblocks()`** on Starlight sites, also when the preset is in `ec.config.mjs`; the preset reads them from the registry. - **`codeblocks()` sets `tabWidth: 0`** unless the Starlight config or `ec.config.mjs` sets it, including when `ec.config.mjs` has plugins. Otherwise real tabs never reach visible whitespace or the copied text, with no warning. - **Collapsible sections must come before `pluginCodeblocks()`.** The notation plugin throws otherwise, because our decorations shift the children that the other plugin picks by position. *Rejected:* moving every decoration to `postprocessRenderedBlockGroup` (touches most features for one optional plugin). +- **Astro sites without Starlight use `codeblocks()` from `starlight-codeblocks/astro`**, which returns `[setup, astroExpressiveCode()]`. Astro flattens the array, so Expressive Code stays where the author lists `codeblocks()`, before `mdx()`. It reuses the Starlight path: registry, ec-config override, Markdown plugin, runtimes; page CSS goes through `injectScript('page-ssr')` in place of `customCss`. It fails when the site also lists `expressiveCode()`. Code tabs and inline code highlighting are off by default there, because they change every Markdown page (directive syntax, a page stylesheet), and the API card page styles and script are never added; Starlight sites keep all three, since Starlight already turns on directives. `tabWidth: 0` stays, as on Starlight: it changes only code blocks. `astro-expressive-code` and `@astrojs/starlight` are optional peer dependencies, so each site installs only the one it uses. *Rejected:* adding Expressive Code through `updateConfig` (Astro appends it after `mdx()`, which fails EC's order check); asking the author to add the preset to their own `expressiveCode()` (no registry, so no code tabs, inline code or built-in runtimes). +- **Without Starlight, the integration turns on Sätteri directives for code tabs and gives each unclaimed directive back as its source text**, as Starlight does, in `astro:config:done` so that other integrations claim theirs first. Without it, `16:9` renders as `16`. It slices the source by position. *Rejected:* `mdast-util-to-markdown` as Starlight uses (two dependencies, and the output can differ from the source). - **The preset takes `{ base }`** for sites without Starlight, so site-relative `[!link]` URLs and adapter hrefs get the base. - **The registry is a public contract** for other plugins (starlight-pydocs): it exists after `codeblocks()` runs `config:setup`, and `options.inlineHighlighting` and `options.apiLinks` are `false` or a truthy object. Nothing else is public; `test/starlight.test.ts` guards it. - **Under `unified()`, one remark adapter runs the Sätteri plugin definition**, so each Markdown feature has one implementation. *Rejected:* a remark-native copy of each feature; a warning that the features are off. @@ -29,7 +31,7 @@ The choices that the code does not explain, with the options that were rejected. ## Client code - **One loader in `jsModules`, feature modules emitted as hashed assets next to `ec..js`.** `jsModules` load only on pages with code blocks. *Rejected:* `injectScript('page')` (loads on every page); all feature code inline (every page pays for every feature). -- **Sites without `codeblocks()` get a loader that carries every module's source** and imports from `blob:` URLs. It works with any EC integration, at the cost of a larger `ec..js` there. *Rejected:* an extra Astro integration to install; `data:` URLs (a third larger). +- **Sites without `codeblocks()` get a loader that carries every module's source** and imports from `blob:` URLs. It works with any EC integration, at the cost of a larger `ec..js` there. Sites without Starlight get the full set-up from `starlight-codeblocks/astro`. *Rejected:* making that integration a requirement; `data:` URLs (a third larger). - **Each client module builds on its own, with no shared chunks**, so a file works as a `blob:` module and the 3 kB budget is what a page downloads. The code walkthrough and runtimes are exempt. - **Runtime modules are emitted at a fixed path** (`scb-runtime-.js`, no hash), because code blocks render before the client build, so the URL must be known in advance. - **Anchor positioning with a small script fallback.** *Rejected:* Floating UI (a dependency, over budget). diff --git a/AGENTS.md b/AGENTS.md index a3f51e5..21d3284 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # Agent instructions -`starlight-codeblocks` is a Starlight plugin that adds 26 code block features on top of Expressive Code (EC), which renders every code block in Starlight. `packages/starlight-codeblocks/` is the plugin, `docs/` its Starlight docs site (it uses the plugin through `workspace:*`, and defines each feature's syntax and behaviour), and `skills/starlight-codeblocks/` an agent skill for people who use the plugin. +`starlight-codeblocks` is a Starlight plugin that adds 26 code block features on top of Expressive Code (EC), which renders every code block in Starlight. `packages/starlight-codeblocks/` is the plugin, `docs/` its Starlight docs site (it uses the plugin through `workspace:*`, and defines each feature's syntax and behaviour), `examples/astro/` a minimal Astro site without Starlight, and `skills/starlight-codeblocks/` an agent skill for people who use the plugin. - `.agents/DECISIONS.md`: design decisions and the rejected options. Search it before you change existing behaviour. - `.agents/WRITING-STYLE.md`: how the docs read, the feature page template and the glossary. @@ -16,10 +16,10 @@ ``` pnpm install pnpm build Build the package (tsdown) into packages/starlight-codeblocks/dist -pnpm test Build, then Vitest unit tests and the node:test checks in scripts/ +pnpm test Build, then Vitest unit tests, a build of examples/astro and the node:test checks in scripts/ pnpm lint Build, then Biome and type checks (tsc, astro check) pnpm lint:docs Writing-style lint for docs/, the READMEs and skills/ -pnpm docs:build Build the package and the docs site into docs/dist, then render the share cards +pnpm docs:build Build the package, the docs site into docs/dist and examples/astro into docs/dist/examples/astro, then render the share cards pnpm test:e2e docs:build, then Playwright against astro preview of docs/dist pnpm readme:media [slug...] docs:build, then regenerate the README images in .github/assets/readme/ ``` @@ -34,6 +34,7 @@ pnpm readme:media [slug...] docs:build, then regenerate the README images in . Paths are under `packages/starlight-codeblocks/src/` unless they start with `docs/`. - `index.ts`: `codeblocks()`, the Starlight plugin. It validates options (`options.ts`), fills the `globalThis` registry (`registry.ts`) and adds the integration (`integration.ts`). +- `astro.ts`: `codeblocks()` for Astro without Starlight (`starlight-codeblocks/astro`). It does the same and adds `astroExpressiveCode()` itself. `examples/astro/` uses it: `pnpm build && pnpm --filter example-astro build`. - `expressive-code/index.ts`: `createPlugins()`, the preset in order. Core first, notation second; the comments there give the other order constraints. One file per feature, `expressive-code/.ts`. Shared helpers are in `core.ts` and `styles.ts`. - `satteri/`: one mdast plugin for code tabs directive, `{:lang}` inline code, mention link checks and duplicate `id` checks. `remark.ts` runs it on `unified()`; add any context it lacks there, with a case in `test/remark.test.ts`. - `client/.ts`: browser modules, built to `dist/client/scb-..js` and loaded by one loader in `jsModules`. `api-card-page.ts` is the exception: a page script and CSS for API cards on links outside code blocks. diff --git a/README.md b/README.md index d9c56f7..e91392b 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ starlight-codeblocks -A Starlight plugin that adds 26 features to code blocks, such as focus, line states, annotations, links to API docs and runnable examples. It builds on Expressive Code, so the code blocks you already have keep working. +A Starlight and Astro plugin that adds 26 features to code blocks, such as focus, line states, annotations, links to API docs and runnable examples. It builds on Expressive Code, so the code blocks you already have keep working. Using an agent? [Point it at the bundled skill](https://ewels.github.io/starlight-codeblocks/agent-skill/). @@ -31,6 +31,8 @@ export default defineConfig({ }); ``` +On an Astro site without Starlight, add `codeblocks()` from `starlight-codeblocks/astro` to `integrations` instead. The [Astro without Starlight](https://ewels.github.io/starlight-codeblocks/astro-without-starlight/) page gives the details. + Most features start only when a code block uses their attribute, or a [comment notation](https://ewels.github.io/starlight-codeblocks/comment-notation/) directive such as `# [!code focus]`. A few, such as file icons and colour swatches, apply to every matching block. The [documentation](https://ewels.github.io/starlight-codeblocks/) has a page for each feature, with live examples, and a reference for every option. diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index ad457ae..b310270 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -76,7 +76,11 @@ export default defineConfig({ runnable: { timeout: 5000 }, lineStates: { states: { todo: { label: 'To do', colour: { dark: '#c792ea', light: '#7c3aed' } } } }, }), - starlightLinksValidator({ exclude: linksValidatorExclude }), + starlightLinksValidator({ + // The example site builds into docs/dist after the docs, so the validator cannot see it. + exclude: (context) => + linksValidatorExclude(context) || context.link.startsWith('/starlight-codeblocks/examples/'), + }), ], sidebar, }), diff --git a/docs/src/content/docs/astro-without-starlight.mdx b/docs/src/content/docs/astro-without-starlight.mdx new file mode 100644 index 0000000..48f9882 --- /dev/null +++ b/docs/src/content/docs/astro-without-starlight.mdx @@ -0,0 +1,96 @@ +--- +title: Astro without Starlight +description: Add the code block features to an Astro site that does not use Starlight. +--- + +import { LinkCard, Steps } from '@astrojs/starlight/components'; + +The plugin also works on Astro sites without Starlight. The `starlight-codeblocks/astro` subpath exports an Astro integration. It adds Expressive Code to the site, with the features of the code blocks. + +The source of the example site is in [`examples/astro` on GitHub](https://github.com/ewels/starlight-codeblocks/tree/main/examples/astro). + + + +## Requirements + +- Astro 7 or later. +- `astro-expressive-code` 0.44 or later. +- Node.js 22.12 or later. + +## Install the integration + + + +1. Add the packages to the site. + + ```sh + npm install starlight-codeblocks astro-expressive-code + ``` + +2. Open `astro.config.mjs`. + +3. Add `codeblocks()` from `starlight-codeblocks/astro` to `integrations`. If the site uses `mdx()`, put `codeblocks()` before it. + + ```js title="astro.config.mjs" ins={3,7} focus={3,7} + import mdx from '@astrojs/mdx'; + import { defineConfig } from 'astro/config'; + import codeblocks from 'starlight-codeblocks/astro'; + + export default defineConfig({ + integrations: [ + codeblocks(), + mdx(), + ], + }); + ``` + +4. If `integrations` has `expressiveCode()`, remove it. `codeblocks()` adds Expressive Code itself. + + + +## Options + +`codeblocks()` takes the same [options](/starlight-codeblocks/reference/options/) as on a Starlight site. It also takes `expressiveCode`, with the options of `astro-expressive-code`. + +Two features change Markdown outside the code blocks, so they are off until you turn them on: + +- [Code tabs](/starlight-codeblocks/features/code-tabs/) turn on directive syntax, such as `:::name`, in every Markdown page. Set `codeTabs: {}` to turn them on. +- [Inline code highlighting](/starlight-codeblocks/features/inline-code-highlighting/) adds a stylesheet to every page. Set `inlineHighlighting: {}` to turn it on. + +```js title="astro.config.mjs" focus={2-3} +codeblocks({ + codeTabs: {}, + inlineHighlighting: {}, +}), +``` + +An `ec.config.mjs` file works as on a Starlight site. Its options replace the options in `expressiveCode`. If the file has a `plugins` list, add `pluginCodeblocks()` to it, as [Getting started](/starlight-codeblocks/getting-started/#sites-with-an-ecconfigmjs-file) shows. + +## Differences from Starlight + +- Side annotations and scrollycoding stay inside the content column. On a Starlight page with no table of contents, they use the free space on each side of the column. +- Code blocks and inline code change theme with the Expressive Code theme selectors. By default, they follow `prefers-color-scheme`, and `data-theme` with the name of a theme on the `html` element. +- With code tabs on, the integration turns on directive syntax in Sätteri. It turns any other directive back into its text, so text such as `16:9` stays as it is. +- API cards show on links in code blocks only. Starlight sites also get them on links outside code blocks, such as those from `starlight-pydocs`. +- With `markdown: { processor: unified() }`, code tabs need `remark-directive` in the `remarkPlugins` of `unified()`. +- The **Copied** label of colour swatches in prose uses the system colours of the browser. + +## Expressive Code only + +A site that keeps its own `expressiveCode()` integration can add the plugins in `ec.config.mjs`. Give the options to `pluginCodeblocks()`: + +```js title="ec.config.mjs" focus={4-5} +import { pluginCodeblocks } from 'starlight-codeblocks/expressive-code'; + +export default { + tabWidth: 0, + plugins: [pluginCodeblocks({ focus: { style: 'dim' } })], +}; +``` + +The features inside code blocks then work, but these parts are not there: + +- Code tabs and inline code highlighting, which need the Markdown plugin. +- The built-in runtimes of the **Run code** button. Map each language to the URL of a runtime module, as in [Without codeblocks()](/starlight-codeblocks/reference/expressive-code-plugins/#without-codeblocks). + +`tabWidth: 0` keeps tabs in the code. Without it, Expressive Code turns each tab into spaces. diff --git a/docs/src/content/docs/features/code-tabs.mdx b/docs/src/content/docs/features/code-tabs.mdx index 7554937..f46c259 100644 --- a/docs/src/content/docs/features/code-tabs.mdx +++ b/docs/src/content/docs/features/code-tabs.mdx @@ -3,7 +3,7 @@ title: Code tabs description: Show several code blocks as one, with editor tabs in the title bar, such as the files of a project or the commands for each package manager. --- -import { LinkCard, TabItem, Tabs } from '@astrojs/starlight/components'; +import { Aside, LinkCard, TabItem, Tabs } from '@astrojs/starlight/components'; import Example from '../../../components/Example.astro'; import Options from '../../../components/Options.astro'; @@ -244,6 +244,10 @@ port = 8080 ## Options + + To use the menu on every block of the site, set `control` to `'menu'`. A directive with `control="tabs"` still shows tabs: diff --git a/docs/src/content/docs/features/inline-code-highlighting.mdx b/docs/src/content/docs/features/inline-code-highlighting.mdx index 35e5b2a..397a2e3 100644 --- a/docs/src/content/docs/features/inline-code-highlighting.mdx +++ b/docs/src/content/docs/features/inline-code-highlighting.mdx @@ -39,11 +39,7 @@ Each piece of inline code with a suffix gets the colours and the background of t | `` `{:}` `` | Inside the backticks, at the end of the code | | `` ``{:} `` | Directly after the closing backtick | -`` is a language name or alias that Expressive Code knows, as in a code block: `js`, `py`, `sh`, `css` and others. Languages that the site adds in the `shiki.langs` option of Expressive Code work too. The suffix must follow the code or the backtick with no space between them. - - +`` is a language name or alias that Expressive Code knows, as in a code block: `js`, `py`, `sh`, `css` and others. Languages that the site adds in the `shiki.langs` option of Expressive Code work too. The suffix must follow the code or the backtick with no space between them. If possible, use the first syntax, with the suffix inside the backticks. It is the same syntax as `rehype-pretty-code`, and it is valid in `.md` and `.mdx` files with no change.