Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Expressive Code to Starlight #742

Merged
merged 28 commits into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e11490d
Add Expressive Code to Starlight
hippotastic Sep 22, 2023
66dc164
Add min color contrast option, fix a11y issues
hippotastic Sep 22, 2023
6eac143
Merge branch 'main' into pr/742
delucis Oct 26, 2023
63a9c11
Merge branch 'main' into add-expressive-code
delucis Nov 1, 2023
cb40752
Refactor EC to use new fs translations system
delucis Oct 28, 2023
5e1ff49
Migrate to Expressive Code v0.27
hippotastic Nov 7, 2023
3e2abbb
Merge branch 'main' into add-expressive-code
hippotastic Nov 7, 2023
91848a1
Add MD authoring examples for EC features
hippotastic Nov 7, 2023
1bc73ed
Change EC default theme to "Night Owl"
hippotastic Nov 8, 2023
8ba9dcf
Update EC to make code block CSS cacheable
hippotastic Nov 11, 2023
f122453
Update EC dependency to fix APIRoute warning
hippotastic Nov 16, 2023
485772b
Merge branch 'main' into add-expressive-code
delucis Nov 17, 2023
af00d1d
Refactor `pathToLocale` helper for re-use
delucis Nov 18, 2023
5c614f9
Re-use `pathToLocale` in Expressive Code
delucis Nov 18, 2023
f2d9077
Rework EC docs in Markdown guide & Configuration reference
delucis Nov 18, 2023
1f7ca13
Add changeset
delucis Nov 18, 2023
6d9c7ff
Merge pull request #2 from withastro/chris/ec-docs
hippotastic Nov 19, 2023
6ef1ef7
Merge pull request #1 from withastro/chris/ec-refactoring
hippotastic Nov 19, 2023
7759b2a
Update authoring content with Sarah’s suggestions
delucis Nov 19, 2023
5026515
Implement suggestions from code review
hippotastic Nov 20, 2023
fa015f6
Update docs/src/content/docs/reference/configuration.mdx
hippotastic Nov 20, 2023
e26ed40
Update .changeset/sweet-berries-begin.md
hippotastic Nov 20, 2023
4d766e0
Apply configuration reference suggestions
delucis Nov 20, 2023
8c67efa
Update astro-expressive-code to 0.29.0
hippotastic Nov 20, 2023
efd1f63
Fix sentence
delucis Nov 20, 2023
44d3717
Update packages/starlight/integrations/expressive-code/index.ts
hippotastic Nov 20, 2023
597a46f
Update docs/src/content/docs/reference/configuration.mdx
hippotastic Nov 20, 2023
46103c7
Merge branch 'main' into add-expressive-code
delucis Nov 20, 2023
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
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`:
delucis marked this conversation as resolved.
Show resolved Hide resolved

- 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
delucis marked this conversation as resolved.
Show resolved Hide resolved

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}
hippotastic marked this conversation as resolved.
Show resolved Hide resolved
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):
delucis marked this conversation as resolved.
Show resolved Hide resolved

```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';
Loading