Developing Plugins
In Expressive Code, all processing of your code blocks and their metadata is performed by plugins and annotations. To render markup around lines or inline ranges of characters, plugins create annotations and attach them to the target lines.
Adding existing plugins
To add a plugin to Expressive Code that is not installed by default, install its package using your package manager, import its initialization function into your config file, and call it inside the plugins
array property.
The following example shows how to add the Collapsible Sections plugin to your Expressive Code configuration. After installing the plugin using your package manager, make the following changes to your Expressive Code configuration:
import { defineConfig } from 'astro/config'import astroExpressiveCode from 'astro-expressive-code'// Import the plugin's initialization functionimport { pluginCollapsibleSections } from '@expressive-code/plugin-collapsible-sections'
export default defineConfig({ integrations: [ astroExpressiveCode({ plugins: [ // Call the plugin initialization function inside the `plugins` array pluginCollapsibleSections(), ], }), ],})
import { defineConfig } from 'astro/config'import starlight from '@astrojs/starlight'// Import the plugin's initialization functionimport { pluginCollapsibleSections } from '@expressive-code/plugin-collapsible-sections'
export default defineConfig({ integrations: [ starlight({ title: 'My Starlight site', expressiveCode: { plugins: [ // Call the plugin initialization function inside the `plugins` array pluginCollapsibleSections(), ], }, }), ],})
import createMDX from '@next/mdx'import rehypeExpressiveCode from 'rehype-expressive-code'// Import the plugin's initialization functionimport { pluginCollapsibleSections } from '@expressive-code/plugin-collapsible-sections'
/** @type {import('rehype-expressive-code').RehypeExpressiveCodeOptions} */const rehypeExpressiveCodeOptions = { plugins: [ // Call the plugin initialization function inside the `plugins` array pluginCollapsibleSections(), ],}
/** @type {import('next').NextConfig} */const nextConfig = { reactStrictMode: true, pageExtensions: ["js", "jsx", "ts", "tsx", "md", "mdx"],}
const withMDX = createMDX({ extension: /\.mdx?$/, options: { remarkPlugins: [], rehypePlugins: [ // The nested array structure is required to pass options // to a rehype plugin [rehypeExpressiveCode, rehypeExpressiveCodeOptions], ], },})
export default withMDX(nextConfig)
Writing your own plugin
To write a new plugin, you need to create a plugin initialization function that returns an object matching the interface ExpressiveCodePlugin
.
The easiest way to do this is by importing the definePlugin
helper function from the @expressive-code/core
package, which will provide your editor with the required type information to help define the plugin correctly. This function takes a single argument, an object containing the properties of your plugin:
import { definePlugin } from '@expressive-code/core'
export function pluginExample() { return definePlugin({ // The only required property is `name` name: 'Example that does nothing', // Add more properties of `ExpressiveCodePlugin` to make your plugin // actually do something (e.g. `baseStyles`, `hooks`, etc.) hooks: { // Add hooks to perform actions during the plugin's lifecycle }, })}
Then, add the plugin to your Expressive Code configuration just like any other plugin:
import { defineConfig } from 'astro/config'import astroExpressiveCode from 'astro-expressive-code'// Import the plugin's initialization function// (adding the file extension is recommended for best compatibility)import { pluginExample } from './plugins/my-example-plugin.js'
export default defineConfig({ integrations: [ astroExpressiveCode({ plugins: [ // Call the plugin initialization function inside the `plugins` array pluginExample(), ], }), ],})
import { defineConfig } from 'astro/config'import starlight from '@astrojs/starlight'// Import the plugin's initialization function// (adding the file extension is recommended for best compatibility)import { pluginExample } from './plugins/my-example-plugin.js'
export default defineConfig({ integrations: [ starlight({ title: 'My Starlight site', expressiveCode: { plugins: [ // Call the plugin initialization function inside the `plugins` array pluginExample(), ], }, }), ],})
import createMDX from '@next/mdx'import rehypeExpressiveCode from 'rehype-expressive-code'// Import the plugin's initialization function// (adding the file extension is recommended for best compatibility)import { pluginExample } from './plugins/my-example-plugin.js'
/** @type {import('rehype-expressive-code').RehypeExpressiveCodeOptions} */const rehypeExpressiveCodeOptions = { plugins: [ // Call the plugin initialization function inside the `plugins` array pluginExample(), ],}
/** @type {import('next').NextConfig} */const nextConfig = { reactStrictMode: true, pageExtensions: ["js", "jsx", "ts", "tsx", "md", "mdx"],}
const withMDX = createMDX({ extension: /\.mdx?$/, options: { remarkPlugins: [], rehypePlugins: [ // The nested array structure is required to pass options // to a rehype plugin [rehypeExpressiveCode, rehypeExpressiveCodeOptions], ], },})
export default withMDX(nextConfig)
🎉 Success! From now on, your plugin will be loaded and executed whenever you run your site generator. If you add any hooks, it will be able to process your code blocks.
Adding options to your plugin
If your plugin needs configuration options, you can add a single options
argument to your initialization function. This argument must be an object type containing the desired configuration properties.
import { definePlugin } from '@expressive-code/core'
export function pluginWithOptionsExample(options) { // Extract the options from the `options` object, // and provide sensible default values const { option1 = true, option2 = 'hello' } = options || {} return definePlugin({ name: 'Example with options that does nothing', hooks: { // Add hooks to perform actions during the plugin's lifecycle // (you can use the options anywhere in your plugin) }, })}
When adding the plugin to your Expressive Code configuration, pass the desired options as an object to the plugin’s initialization function:
import { defineConfig } from 'astro/config'import astroExpressiveCode from 'astro-expressive-code'// Import the plugin's initialization function// (adding the file extension is recommended for best compatibility)import { pluginWithOptionsExample } from './plugins/plugin-with-options.js'
export default defineConfig({ integrations: [ astroExpressiveCode({ plugins: [ // Call the plugin initialization function inside the `plugins` array pluginWithOptionsExample({ // Pass any desired options as object properties option1: false, option2: 'world', }), ], }), ],})
import { defineConfig } from 'astro/config'import starlight from '@astrojs/starlight'// Import the plugin's initialization function// (adding the file extension is recommended for best compatibility)import { pluginWithOptionsExample } from './plugins/plugin-with-options.js'
export default defineConfig({ integrations: [ starlight({ title: 'My Starlight site', expressiveCode: { plugins: [ // Call the plugin initialization function inside the `plugins` array pluginWithOptionsExample({ // Pass any desired options as object properties option1: false, option2: 'world', }), ], }, }), ],})
import createMDX from '@next/mdx'import rehypeExpressiveCode from 'rehype-expressive-code'// Import the plugin's initialization function// (adding the file extension is recommended for best compatibility)import { pluginWithOptionsExample } from './plugins/plugin-with-options.js'
/** @type {import('rehype-expressive-code').RehypeExpressiveCodeOptions} */const rehypeExpressiveCodeOptions = { plugins: [ // Call the plugin initialization function inside the `plugins` array pluginWithOptionsExample({ // Pass any desired options as object properties option1: false, option2: 'world', }), ],}
/** @type {import('next').NextConfig} */const nextConfig = { reactStrictMode: true, pageExtensions: ["js", "jsx", "ts", "tsx", "md", "mdx"],}
const withMDX = createMDX({ extension: /\.mdx?$/, options: { remarkPlugins: [], rehypePlugins: [ // The nested array structure is required to pass options // to a rehype plugin [rehypeExpressiveCode, rehypeExpressiveCodeOptions], ], },})
export default withMDX(nextConfig)
Adding custom annotations to code
In Expressive Code, annotations are used by plugins to attach semantic information to lines or inline ranges of code. They are used to represent things like syntax highlighting, text markers, comments, errors, warnings, and other semantic information.
Expressive Code provides a built-in InlineStyleAnnotation
that allows applying simple inline styles to code. See its documentation for a usage example.
To provide full styling flexibility, plugins can also create their own annotations. In the following example, you will create a plugin that uses two custom annotations:
- a
SquigglesAnnotation
to render squiggly red underlines under words surrounded by a pair of~~
characters, and - an
ErrorMessageAnnotation
to render error messages in red and italic with a semitransparent background.
For both custom annotations, you will create a class that extends the abstract ExpressiveCodeAnnotation
class, and provide an implementation for the render
function that transforms its contained AST nodes, e.g. by wrapping them in HTML tags. This function is called by the engine when it’s time to render the line the annotation has been attached to.
Now, let’s create the plugin:
// @ts-checkimport { definePlugin, ExpressiveCodeAnnotation } from '@expressive-code/core'import { h } from '@expressive-code/core/hast'
class SquigglesAnnotation extends ExpressiveCodeAnnotation { /** @param {import('@expressive-code/core').AnnotationRenderOptions} context */ render({ nodesToTransform }) { return nodesToTransform.map((node) => { return h('span.error-squiggles', node) }) }}
class ErrorMessageAnnotation extends ExpressiveCodeAnnotation { /** @param {import('@expressive-code/core').AnnotationRenderOptions} context */ render({ nodesToTransform }) { return nodesToTransform.map((node) => { return h('span.error-message', node) }) }}
export function pluginErrorPreview() { return definePlugin({ name: 'Error Preview', baseStyles: ` .error-squiggles { text-decoration-style: wavy; text-decoration-color: #f22; text-decoration-line: underline; & span { text-decoration: unset; } } .error-message { color: #f22; font-style: italic; background: #f002; padding-inline: 0.4rem; border-radius: 0.2rem; /* Prevent inline annotations from overriding our styles */ & span { color: inherit; font-style: inherit; } } `, hooks: { preprocessCode: (context) => { // Only apply this to code blocks with the `error-preview` meta if (!context.codeBlock.meta.includes('error-preview')) return
context.codeBlock.getLines().forEach((line) => { // Find all squiggles markup in the line const matches = [...line.text.matchAll(/~~[^~]+~~/g)].reverse() matches.forEach((match) => { // Add a squiggles annotation to the match const from = match.index || 0 const to = from + match[0].length line.addAnnotation( new SquigglesAnnotation({ inlineRange: { columnStart: from, columnEnd: to, }, }) ) // Remove the squiggle markup from the code plaintext line.editText(from, to, match[0].slice(2, -2)) }) }) }, postprocessAnalyzedCode: (context) => { // Only apply this to code blocks with the `error-preview` meta if (!context.codeBlock.meta.includes('error-preview')) return
context.codeBlock.getLines().forEach((line) => { // Find a `//!` comment surrounded by spaces const messageIdx = line.text.match(/(?<=^|\s)\/\/!\s/)?.index if (messageIdx !== undefined) { // Add an error message annotation to the match line.addAnnotation( new ErrorMessageAnnotation({ inlineRange: { columnStart: messageIdx, columnEnd: line.text.length, }, }) ) // Remove the comment markup from the code plaintext line.editText(messageIdx, messageIdx + 4, '') } }) }, }, })}
After saving your new plugin, you need to add it to your Expressive Code configuration:
import { defineConfig } from 'astro/config'import astroExpressiveCode from 'astro-expressive-code'// Import the plugin's initialization function// (adding the file extension is recommended for best compatibility)import { pluginErrorPreview } from './plugins/plugin-error-preview.js'
export default defineConfig({ integrations: [ astroExpressiveCode({ plugins: [ // Call the plugin initialization function inside the `plugins` array pluginErrorPreview(), ], }), ],})
import { defineConfig } from 'astro/config'import starlight from '@astrojs/starlight'// Import the plugin's initialization function// (adding the file extension is recommended for best compatibility)import { pluginErrorPreview } from './plugins/plugin-error-preview.js'
export default defineConfig({ integrations: [ starlight({ title: 'My Starlight site', expressiveCode: { plugins: [ // Call the plugin initialization function inside the `plugins` array pluginErrorPreview(), ], }, }), ],})
import createMDX from '@next/mdx'import rehypeExpressiveCode from 'rehype-expressive-code'// Import the plugin's initialization function// (adding the file extension is recommended for best compatibility)import { pluginErrorPreview } from './plugins/plugin-error-preview.js'
/** @type {import('rehype-expressive-code').RehypeExpressiveCodeOptions} */const rehypeExpressiveCodeOptions = { plugins: [ // Call the plugin initialization function inside the `plugins` array pluginErrorPreview(), ],}
/** @type {import('next').NextConfig} */const nextConfig = { reactStrictMode: true, pageExtensions: ["js", "jsx", "ts", "tsx", "md", "mdx"],}
const withMDX = createMDX({ extension: /\.mdx?$/, options: { remarkPlugins: [], rehypePlugins: [ // The nested array structure is required to pass options // to a rehype plugin [rehypeExpressiveCode, rehypeExpressiveCodeOptions], ], },})
export default withMDX(nextConfig)
You can now use the new syntax in code blocks to add error annotations to lines! For example, you can use the following markup:
```js error-preview~~consore~~.log('Hello world!') //! Error: 'consore' is not defined```
This will render the following result:
consore.log('Hello world!')
🎉 Success! You have created a plugin that uses custom annotations to render squiggly underlines and error messages in your code blocks.
Adding CSS styles to your plugin
Plugins can add CSS styles by providing a baseStyles
property in the plugin object returned by the plugin’s initialization function.
You can see an example of this in the previous section, where the baseStyles
property is used to add styles for the squiggly underline and error message annotations.
Automatic style scoping
All provided styles will be scoped by default to Expressive Code, so they will not affect the rest of the page. This means that you can define styles like font-weight: 800
, or del { text-decoration: line-through }
, and they will only apply to code blocks. SASS-like nesting is also supported.
If you explicitly want to add global styles, you can use the @at-root
rule or target :root
, html
or body
in your selectors. Please be careful with global styles, as users may not expect your plugin to contain a style like body { color: red }
that changes the look of the entire page.
Making your styles theme-dependent
If you want your CSS styles to use colors from the configured themes, you can add a styleSettings
property to your plugin object, which will be used to generate CSS variables automatically.
To use the generated CSS variables in your baseStyles
property, set its value to a function instead of a string. The function will be called by the engine with a context
argument of the type ResolverContext
that provides access to the names of all generated CSS variables.