Expressive Code in your Astro project.
I also use the line numbers plugin, which is a separate package.
1#installs expressive code and line numbers plugin2npm i astro-expressive-code @expressive-code/plugin-line-numbers
Obviously, the most basic way to show code in Astro, is to just write it in a code block, in the mdx
file where the content lives and define the language after the ```
.
1```js2console.log('example');3```
which then renders as:
1console.log('example');
This is all well and good, but what if you want to show one of the files that actually lives in your site and don’t want to copy and paste it into the mdx
file?
Let’s look at one of my example files from my The Math of Calligraphy Grids article.
I made a file for my DotGrid Example, which looks like this
1<svg2 role="img"3 style="max-width: 100%; border: 1px solid red"4 viewBox="0 0 219 100"5 xmlns="http://www.w3.org/2000/svg"6>7 <title>dot Grid with pattern</title>8 <defs>9 <pattern10 id="horizontal-dotted-line"11 viewBox="0 0 219 0.5"12 width="100%"13 height="4%"14 >15 <line16 x1="1.5"17 y1="0"18 x2="219"19 y2="0"20 stroke="currentColor"21 stroke-width="0.5"22 stroke-dasharray="0,4"23 stroke-linecap="round"24 ></line>25 </pattern>26 </defs>27 <rect28 x="0"29 y="0"30 width="219"31 height="100"32 fill="url(#horizontal-dotted-line)"33 ></rect>34</svg>
Now, if I wanted to display the code of that, I’d import the Code
component as well as the Components raw
content and render it like this.
1import ExampleDotCode from "@components/article-content/svg/ExampleDot.astro?raw";2import { Code } from "astro-expressive-code/components";3
4<Code code={ExampleDotCode} lang="astro" title="ExampleDot.astro"/>
Which renders as:
1---2import Example from "./Example.astro";3import SVG from "./SVG.astro";4
5const docWidth = 219;6const docHeight = 100;7const cellSize = 4;8const strokeColor = "currentColor";9const gridYStart = (docHeight % cellSize) / 2;10const gridXStart = (docWidth % cellSize) / 2;11const dotSize = .5;12const patternHeight = (cellSize / docHeight) * 100;13---14
15<Example>16 <SVG17 w={null}18 dW={docWidth}19 dH={docHeight}20 title="dot Grid with pattern"21 >22 <defs>23 <pattern24 id="horizontal-dotted-line"25 viewBox={`0 0 ${docWidth} ${dotSize}`}26 width="100%"27 height={`${patternHeight}%`}28 >29 <line30 x1={gridXStart}31 y1={gridYStart}32 x2={docWidth}33 y2={gridYStart}34 stroke={strokeColor}35 stroke-width={dotSize}36 stroke-dasharray={`0,${cellSize}`}37 stroke-linecap="round"38 ></line>39 </pattern>40 </defs>41 <rect42 x="0"43 y="0"44 width={docWidth}45 height={docHeight}46 fill="url(#horizontal-dotted-line)"47 ></rect>48 </SVG>49</Example>
This is great, but what if I just wanted to get a part of that file?
For that, I wrote a new Astro Component, which takes a code string as well as a partial
prop.
In that prop, I can specify wheter or not not I want to show the frontmatter, the template, or a specific part of the file.
1---2import { Code } from "astro-expressive-code/components";3
4type Partial = 'frontmatter' | 'template' | { lineStart: number; lineEnd: number };5
6interface Props {7 codeTitle?: string;8 rawFile?: string;9 lang?: string;10 partial: Partial;11}12
13const { codeTitle, rawFile = '', lang, partial} = Astro.props;14
15const title = codeTitle ? `${codeTitle}.${lang}` : undefined;16const lines = rawFile.split("\n");17
18const getCodeString = (lines: string[], partial: Partial) => {19 if (partial === 'frontmatter') {20 const start = lines.findIndex((line) => line === '---');21 const end = lines.findIndex((line, index) => index > start && line === '---') + 1;22 return lines.slice(start, end).join('\n');23 }24 if (partial === 'template') {25 const start = lines.findIndex((line) => line === '---');26 const end = lines.findIndex((line, index) => index > start && line === '---');27 return lines.slice(end + 1).join('\n');28 }29 if (typeof partial === 'object') {30 return lines.slice(partial.lineStart, partial.lineEnd).join('\n');31 }32 return lines.join('\n');33};34
35const codeString = getCodeString(lines, partial);36---37<Code38 code={codeString}39 lang={lang}40 title={title}41/>
So if, I wanted to just show the SVG
part of the ExampleDot
file, I’d use the PartialFile
component like this:
1<PartialFile codeTitle="frontmatter" rawFile={ExampleDotCode} lang="astro" partial="frontmatter" />2
3<PartialFile codeTitle="template" rawFile={ExampleDotCode} lang="astro" partial="template" />4
5<PartialFile codeTitle="partial" rawFile={ExampleDotCode} lang="astro" partial={{lineStart: 40, lineEnd: 47}} />
Which renders like this:
1---2import Example from "./Example.astro";3import SVG from "./SVG.astro";4
5const docWidth = 219;6const docHeight = 100;7const cellSize = 4;8const strokeColor = "currentColor";9const gridYStart = (docHeight % cellSize) / 2;10const gridXStart = (docWidth % cellSize) / 2;11const dotSize = .5;12const patternHeight = (cellSize / docHeight) * 100;13---
1<Example>2 <SVG3 w={null}4 dW={docWidth}5 dH={docHeight}6 title="dot Grid with pattern"7 >8 <defs>9 <pattern10 id="horizontal-dotted-line"11 viewBox={`0 0 ${docWidth} ${dotSize}`}12 width="100%"13 height={`${patternHeight}%`}14 >15 <line16 x1={gridXStart}17 y1={gridYStart}18 x2={docWidth}19 y2={gridYStart}20 stroke={strokeColor}21 stroke-width={dotSize}22 stroke-dasharray={`0,${cellSize}`}23 stroke-linecap="round"24 ></line>25 </pattern>26 </defs>27 <rect28 x="0"29 y="0"30 width={docWidth}31 height={docHeight}32 fill="url(#horizontal-dotted-line)"33 ></rect>34 </SVG>35</Example>
1 <rect2 x="0"3 y="0"4 width={docWidth}5 height={docHeight}6 fill="url(#horizontal-dotted-line)"7 ></rect>
Okay, so this is if we’re passing in files in the original syntax. But what if we want to get the resulting HTML of that template?
This is where the ShowCode
component comes in.
Let’s have a look at how the Example file works first though.
If you go back to the preview of the Dot, you see there is a summary with Show SVG Code, which expands the SVG code.
In the Example.astro
Component, the Code is actually added twice.
1---2import ShowCode from "@components/ShowCode.astro";3---4
5<div class="svg-example">6 <div class="svg-wrap">7 <slot />8 </div>9 <ShowCode10 codeTitle="rendered-svg"11 toggleText="Show SVG Code"12 >13 <slot />14 </ShowCode>15</div>
So, once is when we render the actual SVG and the second is then the Code we’d like to render.
Since that HTML is rendered and potentially no longer nicely formatted, I personally like to use the prettier
package to format the HTML.
We can access the slot content which is rendered and then run prettier over it and then get a code string from it.
Astro may add some script tags or data-astro attributes which we don’t want, so we can use a quick replace to get rid of those.
And that’s all the magic there is to it:
1---2interface Props {3 codeTitle?: string;4 lang?: string;5 toggleText?: string;6}7
8const { codeTitle = "rendered-example.html", lang = "html", toggleText = "Show Code" } = Astro.props;9
10import * as prettier from "prettier";11import { Code } from "astro-expressive-code/components";12import Icon from "@components/Icon.astro";13// note no <slot/> is placed in content, we manually add the formatted code.14const defaultSlotContent = await Astro.slots.render("default");15// slot content will add script tags and astro-ids if there is custom css, we don't want these added16const scriptPattern = /<script[^>]*>[\s\S]*?<\/script>/g;17const astroPattern = /(\s*data-astro-cid-[^\s"'`>]+)/g;18const modifiedHtmlString = await defaultSlotContent.replace(scriptPattern, "").replace(astroPattern, "");19const codeString = await prettier.format(modifiedHtmlString, {20 htmlWhitespaceSensitivity: "ignore",21 singleAttributePerLine: true,22 parser: lang,23});24---25
26<details class="doc-show-code">27 <summary class="doc-show-code__summary t-mono">28 {toggleText}29 <Icon icon="angle-up" />30 </summary>31 {32 Astro.slots.has("default") && (33 <Code34 code={codeString}35 lang={lang}36 title={`${codeTitle}.${lang}`}37 />38 )39 }40</details>41
42<style lang="scss" is:global>43 .doc-show-code {44 margin-top: 0.5em;45 border: 1px solid var(--sys-c-line);46 transition: 300ms border ease;47
48 &:hover {49 border-color: var(--sys-c-interactive);50 }51
52 &[open] {53 .c-icon {54 transform: rotate(0deg);55 }56 }57
58 summary {59 padding: 0.2em 0.5em;60
61 &::marker {62 content: "";63 }64
65 .c-icon {66 transition: 300ms transform ease;67 transform: rotate(180deg);68 }69 }70
71 .expressive-code {72 margin-block: 0;73 }74 }75</style>
Displaying Code Blocks and keeping them in sync was always a big hassle in previous setups. So this is a huge win for me.
I hope this helps you in your code block endeavours as well, even if you aren’t using Astro, I hope you can take some inspiration from this and apply it to your own setup.