When writing an article that includes code snippets, it is often useful to have the complete code together in a file or a section at the end. But using markdown’s regular code blocks would mean a lot of duplicated text and possible discrepancies between the snippets and the final code (if I forget to update the code in both places).
There is already a Remark plugin to do this, but I wanted to be able to specify the code files in the frontmatter, which this plugin doesn’t support.
RemarkCodeSnippets
With my own plugin I can specify one or more code files in the frontmatter, as a string, a list of strings, or a dictionary.
---
code: "path/to/code.py"
---
<!-- or -->
---
code:
- "path/to/code.py"
- "path/to/code.R"
---
<!-- or -->
---
code:
py: "path/to/code.py"
r: "path/to/code.R"
---
And then I can reference the code in the markdown using :code:
.
<!-- Gives the only/first code file -->
:code:
<!-- Gives the first code file -->
:code[0]:
<!-- Gives the code file named py -->
:code[py]:
It is also possible to reference a range of lines in the code file.
<!-- Gives lines 5-10 in the only/first code file -->
:code[5,10]:
<!-- Gives lines 5-10 in the first code file -->
:code[0][5,10]:
<!-- Gives lines 5-10 in the code file named py -->
:code[py][5,10]:
Example (this page)
---
title: A Remark Plugin for Easier Code Snippets
date: 2024-05-29
code:
markdown: "src/content/notes/remark-code-snippets.md"
plugin: "src/plugins/remark-code-snippets.ts"
---
## Example (this page)
:code[markdown][0,7]:
:code[markdown][72,79]:
## Plugin Code
:code[plugin]:
Plugin Code
import { readFile } from 'fs/promises'
import type { Root, Code, Text } from 'mdast'
import type { VFile } from 'vfile'
import { visit } from 'unist-util-visit'
export default function remarkCodeSnippets() {
async function transform(tree: Root, file: VFile): Promise<void> {
// Get code file/s from Astro frontmatter
const fmCode = file.data.astro?.frontmatter?.code as string | string[] | Record<string, string>
// If no code file/s are specified, do nothing
if (!fmCode) return
// Create an array to store the code file/s
const codeList: { name: string, path: string, lines: string[], ext: string }[] = []
const readCodeFile = async (path: string, name?: string) => {
const fileContent = await readFile(path, 'utf8')
return {
name: name || 'default',
path: path,
ext: path.split('.').pop()?.toLowerCase() || '',
lines: fileContent.split('\n'),
}
}
if (typeof fmCode === 'string') {
codeList.push(await readCodeFile(fmCode))
} else if (Array.isArray(fmCode)) {
codeList.push(...await Promise.all(fmCode.map((item) => readCodeFile(item))))
} else {
codeList.push(...await Promise.all(Object.entries(fmCode).map(([name, path]) => readCodeFile(path, name))))
}
const getSnippet = (listIndex?: string, start?: string, end?: string) => {
let codeItem
if (!listIndex) {
// No index specified, use first
codeItem = codeList[0]
} else if (Number.isInteger(Number(listIndex)) && Number(listIndex) > 0) {
// Index is an integer
codeItem = codeList?.[Number(listIndex)] || codeList[0]
} else {
// Index is a string, use first item that matches, if no match use first
codeItem = codeList.find(item => item.name == listIndex) || codeList[0]
}
if (!start || !end) return { snippet: codeItem.lines.join('\n'), ext: codeItem.ext }
const startNum = Math.max(0, +start - 1)
const endNum = Math.min(codeItem.lines.length, +end)
const snippet = startNum < endNum ? codeItem.lines.slice(startNum, endNum).join('\n') : ''
return { snippet, ext: codeItem.ext }
}
visit(tree, 'text', (node: Text, index, parent) => {
const regex = /:code(?:\[(\w+)\])?(?:\[(\d+),(\d+)\])?:/g
if (!node.value.match(regex) || !parent || index === undefined) return
const parts = node.value.split(regex)
const newNodes: Array<Text | Code> = []
for (let i = 0; i < parts.length; i += 4) {
if (parts[i]) newNodes.push({ type: 'text', value: parts[i] })
if (i + 1 < parts.length) {
const { snippet, ext } = getSnippet(parts[i + 1], parts[i + 2], parts[i + 3])
newNodes.push({
type: 'code',
value: snippet,
lang: ext
})
}
}
if (newNodes.length > 0) {
parent.children.splice(index, 1, ...newNodes)
}
})
}
return transform
}