Skip to content

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:

astro.config.mjs
import { defineConfig } from 'astro/config'
import astroExpressiveCode from 'astro-expressive-code'
// Import the plugin's initialization function
import { pluginCollapsibleSections } from '@expressive-code/plugin-collapsible-sections'
export default defineConfig({
integrations: [
astroExpressiveCode({
plugins: [
// Call the plugin initialization function inside the `plugins` array
pluginCollapsibleSections(),
],
}),
],
})

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:

plugins/my-example-plugin.js
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:

astro.config.mjs
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(),
],
}),
],
})

🎉 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.

plugins/plugin-with-options.js
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:

astro.config.mjs
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',
}),
],
}),
],
})

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:

plugins/plugin-error-preview.js
// @ts-check
import { 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:

astro.config.mjs
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(),
],
}),
],
})

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!') Error: 'consore' is not defined

🎉 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.