Skip to content

Commit c6a4bcb

Browse files
hippotasticsarah11918delucisHiDeookevinzunigacuellar
authored
Add Expressive Code to Starlight (#742)
Co-authored-by: Sarah Rainsberger <[email protected]> Co-authored-by: Chris Swithinbank <[email protected]> Co-authored-by: HiDeoo <[email protected]> Co-authored-by: Kevin Zuniga Cuellar <[email protected]> Co-authored-by: Lorenzo Lewis <[email protected]> Co-authored-by: Genteure <[email protected]> Co-authored-by: trueberryless <[email protected]>
1 parent 6d14b4f commit c6a4bcb

File tree

18 files changed

+4298
-33
lines changed

18 files changed

+4298
-33
lines changed

.changeset/sweet-berries-begin.md

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
'@astrojs/starlight': minor
3+
---
4+
5+
Adds Expressive Code as Starlight’s default code block renderer
6+
7+
⚠️ **Potentially breaking change:**
8+
This addition changes how Markdown code blocks are rendered. By default, Starlight will now use [Expressive Code](https://github.com/expressive-code/expressive-code/tree/main/packages/astro-expressive-code).
9+
If you were already customizing how code blocks are rendered and don't want to use the [features provided by Expressive Code](https://starlight.astro.build/guides/authoring-content/#expressive-code-features), you can preserve the previous behavior by setting the new config option `expressiveCode` to `false`.
10+
11+
If you had previously added Expressive Code manually to your Starlight project, you can now remove the manual set-up in `astro.config.mjs`:
12+
13+
- Move your configuration to Starlight’s new `expressiveCode` option.
14+
- Remove the `astro-expressive-code` integration.
15+
16+
For example:
17+
18+
```diff
19+
import starlight from '@astrojs/starlight';
20+
import { defineConfig } from 'astro/config';
21+
- import expressiveCode from 'astro-expressive-code';
22+
23+
export default defineConfig({
24+
integrations: [
25+
- expressiveCode({
26+
- themes: ['rose-pine'],
27+
- }),
28+
starlight({
29+
title: 'My docs',
30+
+ expressiveCode: {
31+
+ themes: ['rose-pine'],
32+
+ },
33+
}),
34+
],
35+
});
36+
```
37+
38+
Note that the built-in Starlight version of Expressive Code sets some opinionated defaults that are different from the `astro-expressive-code` defaults. You may need to set some `styleOverrides` if you wish to keep styles exactly the same.

docs/src/content/docs/guides/authoring-content.md

+142-3
Original file line numberDiff line numberDiff line change
@@ -202,9 +202,148 @@ var fun = function lang(l) {
202202
```
203203
````
204204

205-
```md
206-
Long, single-line code blocks should not wrap. They should horizontally scroll if they are too long. This line should be long enough to demonstrate this.
207-
```
205+
### Expressive Code features
206+
207+
Starlight uses [Expressive Code](https://github.com/expressive-code/expressive-code/tree/main/packages/astro-expressive-code) to extend formatting possibilities for code blocks.
208+
Expressive Code’s text markers and window frames plugins are enabled by default.
209+
Code block rendering can be configured using Starlight’s [`expressiveCode` configuration option](/reference/configuration/#expressivecode).
210+
211+
#### Text markers
212+
213+
You can highlight specific lines or parts of your code blocks using [Expressive Code text markers](https://github.com/expressive-code/expressive-code/blob/main/packages/%40expressive-code/plugin-text-markers/README.md#usage-in-markdown--mdx-documents) on the opening line of your code block.
214+
Use curly braces (`{ }`) to highlight entire lines, and quotation marks to highlight strings of text.
215+
216+
There are three highlighting styles: neutral for calling attention to code, green for indicating inserted code, and red for indicating deleted code.
217+
Both text and entire lines can be marked using the default marker, or in combination with `ins=` and `del=` to produce the desired highlighting.
218+
219+
Expressive Code provides several options for customizing the visual appearance of your code samples.
220+
Many of these can be combined, for highly illustrative code samples.
221+
Please explore the [Expressive Code documentation](https://github.com/expressive-code/expressive-code/blob/main/packages/%40expressive-code/plugin-text-markers/README.md) for the extensive options available.
222+
Some of the most common examples are shown below:
223+
224+
- [Mark entire lines & line ranges using the `{ }` marker](https://github.com/expressive-code/expressive-code/blob/main/packages/%40expressive-code/plugin-text-markers/README.md#marking-entire-lines--line-ranges):
225+
226+
```js {2-3}
227+
function demo() {
228+
// This line (#2) and the next one are highlighted
229+
return 'This is line #3 of this snippet';
230+
}
231+
```
232+
233+
````md
234+
```js {2-3}
235+
function demo() {
236+
// This line (#2) and the next one are highlighted
237+
return 'This is line #3 of this snippet';
238+
}
239+
```
240+
````
241+
242+
- [Mark selections of text using the `" "` marker or regular expressions](https://github.com/expressive-code/expressive-code/blob/main/packages/%40expressive-code/plugin-text-markers/README.md#marking-entire-lines--line-ranges):
243+
244+
```js "Individual terms" /Even.*supported/
245+
// Individual terms can be highlighted, too
246+
function demo() {
247+
return 'Even regular expressions are supported';
248+
}
249+
```
250+
251+
````md
252+
```js "Individual terms" /Even.*supported/
253+
// Individual terms can be highlighted, too
254+
function demo() {
255+
return 'Even regular expressions are supported';
256+
}
257+
```
258+
````
259+
260+
- [Mark text or lines as inserted or deleted with `ins` or `del`](https://github.com/expressive-code/expressive-code/blob/main/packages/%40expressive-code/plugin-text-markers/README.md#selecting-marker-types-mark-ins-del):
261+
262+
```js "return true;" ins="inserted" del="deleted"
263+
function demo() {
264+
console.log('These are inserted and deleted marker types');
265+
// The return statement uses the default marker type
266+
return true;
267+
}
268+
```
269+
270+
````md
271+
```js "return true;" ins="inserted" del="deleted"
272+
function demo() {
273+
console.log('These are inserted and deleted marker types');
274+
// The return statement uses the default marker type
275+
return true;
276+
}
277+
```
278+
````
279+
280+
- [Combine syntax highlighting with `diff`-like syntax](https://github.com/expressive-code/expressive-code/blob/main/packages/%40expressive-code/plugin-text-markers/README.md#combining-syntax-highlighting-with-diff-like-syntax):
281+
282+
```diff lang="js"
283+
function thisIsJavaScript() {
284+
// This entire block gets highlighted as JavaScript,
285+
// and we can still add diff markers to it!
286+
- console.log('Old code to be removed')
287+
+ console.log('New and shiny code!')
288+
}
289+
```
290+
291+
````md
292+
```diff lang="js"
293+
function thisIsJavaScript() {
294+
// This entire block gets highlighted as JavaScript,
295+
// and we can still add diff markers to it!
296+
- console.log('Old code to be removed')
297+
+ console.log('New and shiny code!')
298+
}
299+
```
300+
````
301+
302+
#### Frames and titles
303+
304+
Code blocks can be rendered inside a window-like frame.
305+
A frame that looks like a terminal window will be used for shell scripting languages (e.g. `bash` or `sh`).
306+
Other languages display inside a code editor-style frame if they include a title.
307+
308+
A code block’s optional title can be set either with a `title="..."` attribute following the code block's opening backticks and language identifier, or with a file name comment in the first lines of the code.
309+
310+
- [Add a file name tab with a comment](https://github.com/expressive-code/expressive-code/blob/main/packages/%40expressive-code/plugin-frames/README.md#code-editor-window-frames)
311+
312+
```js
313+
// my-test-file.js
314+
console.log('Hello World!');
315+
```
316+
317+
````md
318+
```js
319+
// my-test-file.js
320+
console.log('Hello World!');
321+
```
322+
````
323+
324+
- [Add a title to a Terminal window](https://github.com/expressive-code/expressive-code/blob/main/packages/%40expressive-code/plugin-frames/README.md#terminal-window-frames)
325+
326+
```bash title="Installing dependencies…"
327+
npm install
328+
```
329+
330+
````md
331+
```bash title="Installing dependencies…"
332+
npm install
333+
```
334+
````
335+
336+
- [Disable window frames with `frame="none"`](https://github.com/expressive-code/expressive-code/blob/main/packages/%40expressive-code/plugin-frames/README.md#overriding-frame-types)
337+
338+
```bash frame="none"
339+
echo "This is not rendered as a terminal despite using the bash language"
340+
```
341+
342+
````md
343+
```bash frame="none"
344+
echo "This is not rendered as a terminal despite using the bash language"
345+
```
346+
````
208347

209348
## Other common Markdown features
210349

docs/src/content/docs/guides/i18n.mdx

+11
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,17 @@ You can provide translations for additional languages you support — or overrid
205205
}
206206
```
207207

208+
Starlight’s code blocks are powered by the [Expressive Code](https://github.com/expressive-code/expressive-code) library.
209+
You can set translations for its UI strings in the same JSON file using `expressiveCode` keys:
210+
211+
```json
212+
{
213+
"expressiveCode.copyButtonCopied": "Copied!",
214+
"expressiveCode.copyButtonTooltip": "Copy to clipboard",
215+
"expressiveCode.terminalWindowFallbackTitle": "Terminal window"
216+
}
217+
```
218+
208219
Starlight’s search modal is powered by the [Pagefind](https://pagefind.app/) library.
209220
You can set translations for Pagefind’s UI in the same JSON file using `pagefind` keys:
210221

docs/src/content/docs/reference/configuration.mdx

+66
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,72 @@ starlight({
346346
});
347347
```
348348

349+
### `expressiveCode`
350+
351+
**type:** `StarlightExpressiveCodeOptions | boolean`
352+
**default:** `true`
353+
354+
Starlight uses [Expressive Code](https://github.com/expressive-code/expressive-code/tree/main/packages/astro-expressive-code) to render code blocks and add support for highlighting parts of code examples, adding filenames to code blocks, and more.
355+
See the [“Code blocks” guide](/guides/authoring-content/#code-blocks) to learn how to use Expressive Code syntax in your Markdown and MDX content.
356+
357+
You can use any of the standard [Expressive Code configuration options](https://github.com/expressive-code/expressive-code/blob/main/packages/astro-expressive-code/README.md#configuration) as well as some Starlight-specific properties, by setting them in Starlight’s `expressiveCode` option.
358+
For example, set Expressive Code’s `styleOverrides` option to override the default CSS. This enables customizations like giving your code blocks rounded corners:
359+
360+
```js ins={2-4}
361+
starlight({
362+
expressiveCode: {
363+
styleOverrides: { borderRadius: '0.5rem' },
364+
},
365+
});
366+
```
367+
368+
If you want to disable Expressive Code, set `expressiveCode: false` in your Starlight config:
369+
370+
```js ins={2}
371+
starlight({
372+
expressiveCode: false,
373+
});
374+
```
375+
376+
In addition to the standard Expressive Code options, you can also set the following Starlight-specific properties in your `expressiveCode` config to further customize theme behavior for your code blocks :
377+
378+
#### `themes`
379+
380+
**type:** `Array<string | ThemeObject | ExpressiveCodeTheme>`
381+
**default:** `['starlight-dark', 'starlight-light']`
382+
383+
Set the themes used to style code blocks.
384+
See the [Expressive Code `themes` documentation](https://github.com/expressive-code/expressive-code/blob/main/packages/astro-expressive-code/README.md#themes) for details of the supported theme formats.
385+
386+
Starlight uses the dark and light variants of Sarah Drasner’s [Night Owl theme](https://github.com/sdras/night-owl-vscode-theme) by default.
387+
388+
If you provide at least one dark and one light theme, Starlight will automatically keep the active code block theme in sync with the current site theme.
389+
Configure this behavior with the [`useStarlightDarkModeSwitch`](#usestarlightdarkmodeswitch) option.
390+
391+
#### `useStarlightDarkModeSwitch`
392+
393+
**type:** `boolean`
394+
**default:** `true`
395+
396+
When `true`, code blocks automatically switch between light and dark themes when the site theme changes.
397+
When `false`, you must manually add CSS to handle switching between multiple themes.
398+
399+
:::note
400+
When setting `themes`, you must provide at least one dark and one light theme for the Starlight dark mode switch to work.
401+
:::
402+
403+
#### `useStarlightUiThemeColors`
404+
405+
**type:** `boolean`
406+
**default:** `true` if `themes` is not set, otherwise `false`
407+
408+
When `true`, Starlight's CSS variables are used for the colors of code block UI elements (backgrounds, buttons, shadows etc.), matching the [site color theme](/guides/css-and-tailwind/#theming).
409+
When `false`, the colors provided by the active syntax highlighting theme are used for these elements.
410+
411+
:::note
412+
When using custom themes and setting this to `true`, you must provide at least one dark and one light theme to ensure proper color contrast.
413+
:::
414+
349415
### `head`
350416

351417
**type:** [`HeadConfig[]`](#headconfig)

packages/starlight/index.ts

+10
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { spawn } from 'node:child_process';
44
import { dirname, relative } from 'node:path';
55
import { fileURLToPath } from 'node:url';
66
import { starlightAsides } from './integrations/asides';
7+
import { starlightExpressiveCode } from './integrations/expressive-code';
78
import { starlightSitemap } from './integrations/sitemap';
89
import { vitePluginStarlightUserConfig } from './integrations/virtual-user-config';
910
import { errorMap } from './utils/error-map';
@@ -37,6 +38,15 @@ export default function StarlightIntegration(opts: StarlightUserConfig): AstroIn
3738
entryPoint: '@astrojs/starlight/index.astro',
3839
});
3940
const integrations: AstroIntegration[] = [];
41+
if (!config.integrations.find(({ name }) => name === 'astro-expressive-code')) {
42+
integrations.push(
43+
...starlightExpressiveCode({
44+
starlightConfig: userConfig,
45+
astroConfig: config,
46+
useTranslations,
47+
})
48+
);
49+
}
4050
if (!config.integrations.find(({ name }) => name === '@astrojs/sitemap')) {
4151
integrations.push(starlightSitemap(userConfig));
4252
}

packages/starlight/integrations/asides.ts

+2-28
Original file line numberDiff line numberDiff line change
@@ -7,40 +7,14 @@ import { remove } from 'unist-util-remove';
77
import { visit } from 'unist-util-visit';
88
import type { StarlightConfig } from '../types';
99
import type { createTranslationSystemFromFs } from '../utils/translations-fs';
10+
import { pathToLocale } from './shared/pathToLocale';
1011

1112
interface AsidesOptions {
1213
starlightConfig: { locales: StarlightConfig['locales'] };
1314
astroConfig: { root: AstroConfig['root']; srcDir: AstroConfig['srcDir'] };
1415
useTranslations: ReturnType<typeof createTranslationSystemFromFs>;
1516
}
1617

17-
function pathToLocale(
18-
slug: string | undefined,
19-
config: AsidesOptions['starlightConfig']
20-
): string | undefined {
21-
const locales = Object.keys(config.locales || {});
22-
const baseSegment = slug?.split('/')[0];
23-
if (baseSegment && locales.includes(baseSegment)) return baseSegment;
24-
return undefined;
25-
}
26-
27-
/** get current lang from file full path */
28-
function getLocaleFromPath(
29-
unformattedPath: string | undefined,
30-
{ starlightConfig, astroConfig }: AsidesOptions
31-
): string | undefined {
32-
const srcDir = new URL(astroConfig.srcDir, astroConfig.root);
33-
const docsDir = new URL('content/docs/', srcDir);
34-
const path = unformattedPath
35-
// Format path to unix style path.
36-
?.replace(/\\/g, '/')
37-
// Strip docs path leaving only content collection file ID.
38-
// Example: /Users/houston/repo/src/content/docs/en/guide.md => en/guide.md
39-
.replace(docsDir.pathname, '');
40-
const locale = pathToLocale(path, starlightConfig);
41-
return locale;
42-
}
43-
4418
/** Hacky function that generates an mdast HTML tree ready for conversion to HTML by rehype. */
4519
function h(el: string, attrs: Properties = {}, children: any[] = []): P {
4620
const { tagName, properties } = _h(el, attrs);
@@ -123,7 +97,7 @@ function remarkAsides(options: AsidesOptions): Plugin<[], Root> {
12397
};
12498

12599
const transformer: Transformer<Root> = (tree, file) => {
126-
const locale = getLocaleFromPath(file.history[0], options);
100+
const locale = pathToLocale(file.history[0], options);
127101
const t = options.useTranslations(locale);
128102
visit(tree, (node, index, parent) => {
129103
if (!parent || index === null || node.type !== 'containerDirective') {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* @file This file is exported by Starlight as `@astrojs/starlight/expressive-code`
3+
* and can be used in your site's configuration to customize Expressive Code.
4+
*
5+
* It provides access to all of the Expressive Code classes and functions without having
6+
* to install `astro-expressive-code` as an additional dependency into your project
7+
* (and thereby risiking version conflicts).
8+
*
9+
* For example, you can use this to load custom themes from a JSONC file (JSON with comments)
10+
* that would otherwise be difficult to import, and pass them to the `themes` option:
11+
*
12+
* @example
13+
* ```js
14+
* // astro.config.mjs
15+
* import fs from 'node:fs';
16+
* import { defineConfig } from 'astro/config';
17+
* import starlight from '@astrojs/starlight';
18+
* import { ExpressiveCodeTheme } from '@astrojs/starlight/expressive-code';
19+
*
20+
* const jsoncString = fs.readFileSync(new URL(`./my-theme.jsonc`, import.meta.url), 'utf-8');
21+
* const myTheme = ExpressiveCodeTheme.fromJSONString(jsoncString);
22+
*
23+
* export default defineConfig({
24+
* integrations: [
25+
* starlight({
26+
* title: 'My Starlight site',
27+
* expressiveCode: {
28+
* themes: [myTheme],
29+
* },
30+
* }),
31+
* ],
32+
* });
33+
* ```
34+
*/
35+
36+
export * from 'astro-expressive-code';

0 commit comments

Comments
 (0)