Skip to content

Commit

Permalink
Add Expressive Code to Starlight (#742)
Browse files Browse the repository at this point in the history
Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>
Co-authored-by: HiDeoo <494699+HiDeoo@users.noreply.github.com>
Co-authored-by: Kevin Zuniga Cuellar <46791833+kevinzunigacuellar@users.noreply.github.com>
Co-authored-by: Lorenzo Lewis <15347255+lorenzolewis@users.noreply.github.com>
Co-authored-by: Genteure <11240579+Genteure@users.noreply.github.com>
Co-authored-by: trueberryless <99918022+trueberryless@users.noreply.github.com>
  • Loading branch information
8 people committed Nov 20, 2023
1 parent 6d14b4f commit c6a4bcb
Show file tree
Hide file tree
Showing 18 changed files with 4,298 additions and 33 deletions.
38 changes: 38 additions & 0 deletions .changeset/sweet-berries-begin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
'@astrojs/starlight': minor
---

Adds Expressive Code as Starlight’s default code block renderer

⚠️ **Potentially breaking change:**
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).
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`.

If you had previously added Expressive Code manually to your Starlight project, you can now remove the manual set-up in `astro.config.mjs`:

- Move your configuration to Starlight’s new `expressiveCode` option.
- Remove the `astro-expressive-code` integration.

For example:

```diff
import starlight from '@astrojs/starlight';
import { defineConfig } from 'astro/config';
- import expressiveCode from 'astro-expressive-code';

export default defineConfig({
integrations: [
- expressiveCode({
- themes: ['rose-pine'],
- }),
starlight({
title: 'My docs',
+ expressiveCode: {
+ themes: ['rose-pine'],
+ },
}),
],
});
```

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.
145 changes: 142 additions & 3 deletions docs/src/content/docs/guides/authoring-content.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,9 +202,148 @@ var fun = function lang(l) {
```
````

```md
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.
```
### Expressive Code features

Starlight uses [Expressive Code](https://github.com/expressive-code/expressive-code/tree/main/packages/astro-expressive-code) to extend formatting possibilities for code blocks.
Expressive Code’s text markers and window frames plugins are enabled by default.
Code block rendering can be configured using Starlight’s [`expressiveCode` configuration option](/reference/configuration/#expressivecode).

#### Text markers

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.
Use curly braces (`{ }`) to highlight entire lines, and quotation marks to highlight strings of text.

There are three highlighting styles: neutral for calling attention to code, green for indicating inserted code, and red for indicating deleted code.
Both text and entire lines can be marked using the default marker, or in combination with `ins=` and `del=` to produce the desired highlighting.

Expressive Code provides several options for customizing the visual appearance of your code samples.
Many of these can be combined, for highly illustrative code samples.
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.
Some of the most common examples are shown below:

- [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):

```js {2-3}
function demo() {
// This line (#2) and the next one are highlighted
return 'This is line #3 of this snippet';
}
```

````md
```js {2-3}
function demo() {
// This line (#2) and the next one are highlighted
return 'This is line #3 of this snippet';
}
```
````

- [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):

```js "Individual terms" /Even.*supported/
// Individual terms can be highlighted, too
function demo() {
return 'Even regular expressions are supported';
}
```

````md
```js "Individual terms" /Even.*supported/
// Individual terms can be highlighted, too
function demo() {
return 'Even regular expressions are supported';
}
```
````

- [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):

```js "return true;" ins="inserted" del="deleted"
function demo() {
console.log('These are inserted and deleted marker types');
// The return statement uses the default marker type
return true;
}
```

````md
```js "return true;" ins="inserted" del="deleted"
function demo() {
console.log('These are inserted and deleted marker types');
// The return statement uses the default marker type
return true;
}
```
````

- [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):

```diff lang="js"
function thisIsJavaScript() {
// This entire block gets highlighted as JavaScript,
// and we can still add diff markers to it!
- console.log('Old code to be removed')
+ console.log('New and shiny code!')
}
```

````md
```diff lang="js"
function thisIsJavaScript() {
// This entire block gets highlighted as JavaScript,
// and we can still add diff markers to it!
- console.log('Old code to be removed')
+ console.log('New and shiny code!')
}
```
````

#### Frames and titles

Code blocks can be rendered inside a window-like frame.
A frame that looks like a terminal window will be used for shell scripting languages (e.g. `bash` or `sh`).
Other languages display inside a code editor-style frame if they include a title.

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.

- [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)

```js
// my-test-file.js
console.log('Hello World!');
```

````md
```js
// my-test-file.js
console.log('Hello World!');
```
````

- [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)

```bash title="Installing dependencies…"
npm install
```

````md
```bash title="Installing dependencies…"
npm install
```
````

- [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)

```bash frame="none"
echo "This is not rendered as a terminal despite using the bash language"
```

````md
```bash frame="none"
echo "This is not rendered as a terminal despite using the bash language"
```
````

## Other common Markdown features

Expand Down
11 changes: 11 additions & 0 deletions docs/src/content/docs/guides/i18n.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,17 @@ You can provide translations for additional languages you support — or overrid
}
```

Starlight’s code blocks are powered by the [Expressive Code](https://github.com/expressive-code/expressive-code) library.
You can set translations for its UI strings in the same JSON file using `expressiveCode` keys:

```json
{
"expressiveCode.copyButtonCopied": "Copied!",
"expressiveCode.copyButtonTooltip": "Copy to clipboard",
"expressiveCode.terminalWindowFallbackTitle": "Terminal window"
}
```

Starlight’s search modal is powered by the [Pagefind](https://pagefind.app/) library.
You can set translations for Pagefind’s UI in the same JSON file using `pagefind` keys:

Expand Down
66 changes: 66 additions & 0 deletions docs/src/content/docs/reference/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,72 @@ starlight({
});
```

### `expressiveCode`

**type:** `StarlightExpressiveCodeOptions | boolean`
**default:** `true`

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.
See the [“Code blocks” guide](/guides/authoring-content/#code-blocks) to learn how to use Expressive Code syntax in your Markdown and MDX content.

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.
For example, set Expressive Code’s `styleOverrides` option to override the default CSS. This enables customizations like giving your code blocks rounded corners:

```js ins={2-4}
starlight({
expressiveCode: {
styleOverrides: { borderRadius: '0.5rem' },
},
});
```

If you want to disable Expressive Code, set `expressiveCode: false` in your Starlight config:

```js ins={2}
starlight({
expressiveCode: false,
});
```

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 :

#### `themes`

**type:** `Array<string | ThemeObject | ExpressiveCodeTheme>`
**default:** `['starlight-dark', 'starlight-light']`

Set the themes used to style code blocks.
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.

Starlight uses the dark and light variants of Sarah Drasner’s [Night Owl theme](https://github.com/sdras/night-owl-vscode-theme) by default.

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.
Configure this behavior with the [`useStarlightDarkModeSwitch`](#usestarlightdarkmodeswitch) option.

#### `useStarlightDarkModeSwitch`

**type:** `boolean`
**default:** `true`

When `true`, code blocks automatically switch between light and dark themes when the site theme changes.
When `false`, you must manually add CSS to handle switching between multiple themes.

:::note
When setting `themes`, you must provide at least one dark and one light theme for the Starlight dark mode switch to work.
:::

#### `useStarlightUiThemeColors`

**type:** `boolean`
**default:** `true` if `themes` is not set, otherwise `false`

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).
When `false`, the colors provided by the active syntax highlighting theme are used for these elements.

:::note
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.
:::

### `head`

**type:** [`HeadConfig[]`](#headconfig)
Expand Down
10 changes: 10 additions & 0 deletions packages/starlight/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { spawn } from 'node:child_process';
import { dirname, relative } from 'node:path';
import { fileURLToPath } from 'node:url';
import { starlightAsides } from './integrations/asides';
import { starlightExpressiveCode } from './integrations/expressive-code';
import { starlightSitemap } from './integrations/sitemap';
import { vitePluginStarlightUserConfig } from './integrations/virtual-user-config';
import { errorMap } from './utils/error-map';
Expand Down Expand Up @@ -37,6 +38,15 @@ export default function StarlightIntegration(opts: StarlightUserConfig): AstroIn
entryPoint: '@astrojs/starlight/index.astro',
});
const integrations: AstroIntegration[] = [];
if (!config.integrations.find(({ name }) => name === 'astro-expressive-code')) {
integrations.push(
...starlightExpressiveCode({
starlightConfig: userConfig,
astroConfig: config,
useTranslations,
})
);
}
if (!config.integrations.find(({ name }) => name === '@astrojs/sitemap')) {
integrations.push(starlightSitemap(userConfig));
}
Expand Down
30 changes: 2 additions & 28 deletions packages/starlight/integrations/asides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,40 +7,14 @@ import { remove } from 'unist-util-remove';
import { visit } from 'unist-util-visit';
import type { StarlightConfig } from '../types';
import type { createTranslationSystemFromFs } from '../utils/translations-fs';
import { pathToLocale } from './shared/pathToLocale';

interface AsidesOptions {
starlightConfig: { locales: StarlightConfig['locales'] };
astroConfig: { root: AstroConfig['root']; srcDir: AstroConfig['srcDir'] };
useTranslations: ReturnType<typeof createTranslationSystemFromFs>;
}

function pathToLocale(
slug: string | undefined,
config: AsidesOptions['starlightConfig']
): string | undefined {
const locales = Object.keys(config.locales || {});
const baseSegment = slug?.split('/')[0];
if (baseSegment && locales.includes(baseSegment)) return baseSegment;
return undefined;
}

/** get current lang from file full path */
function getLocaleFromPath(
unformattedPath: string | undefined,
{ starlightConfig, astroConfig }: AsidesOptions
): string | undefined {
const srcDir = new URL(astroConfig.srcDir, astroConfig.root);
const docsDir = new URL('content/docs/', srcDir);
const path = unformattedPath
// Format path to unix style path.
?.replace(/\\/g, '/')
// Strip docs path leaving only content collection file ID.
// Example: /Users/houston/repo/src/content/docs/en/guide.md => en/guide.md
.replace(docsDir.pathname, '');
const locale = pathToLocale(path, starlightConfig);
return locale;
}

/** Hacky function that generates an mdast HTML tree ready for conversion to HTML by rehype. */
function h(el: string, attrs: Properties = {}, children: any[] = []): P {
const { tagName, properties } = _h(el, attrs);
Expand Down Expand Up @@ -123,7 +97,7 @@ function remarkAsides(options: AsidesOptions): Plugin<[], Root> {
};

const transformer: Transformer<Root> = (tree, file) => {
const locale = getLocaleFromPath(file.history[0], options);
const locale = pathToLocale(file.history[0], options);
const t = options.useTranslations(locale);
visit(tree, (node, index, parent) => {
if (!parent || index === null || node.type !== 'containerDirective') {
Expand Down
36 changes: 36 additions & 0 deletions packages/starlight/integrations/expressive-code/exports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* @file This file is exported by Starlight as `@astrojs/starlight/expressive-code`
* and can be used in your site's configuration to customize Expressive Code.
*
* It provides access to all of the Expressive Code classes and functions without having
* to install `astro-expressive-code` as an additional dependency into your project
* (and thereby risiking version conflicts).
*
* For example, you can use this to load custom themes from a JSONC file (JSON with comments)
* that would otherwise be difficult to import, and pass them to the `themes` option:
*
* @example
* ```js
* // astro.config.mjs
* import fs from 'node:fs';
* import { defineConfig } from 'astro/config';
* import starlight from '@astrojs/starlight';
* import { ExpressiveCodeTheme } from '@astrojs/starlight/expressive-code';
*
* const jsoncString = fs.readFileSync(new URL(`./my-theme.jsonc`, import.meta.url), 'utf-8');
* const myTheme = ExpressiveCodeTheme.fromJSONString(jsoncString);
*
* export default defineConfig({
* integrations: [
* starlight({
* title: 'My Starlight site',
* expressiveCode: {
* themes: [myTheme],
* },
* }),
* ],
* });
* ```
*/

export * from 'astro-expressive-code';

0 comments on commit c6a4bcb

Please sign in to comment.