Snippet
Embed the contents of a file as a syntax-highlighted code block. The file lives anywhere in your project tree (relative to the project root); refrakt reads it at build time, slices it by line range if you ask, and renders it like any other fenced code block. When the source file changes, the embedded version updates automatically on the next build — no copy-paste drift.
The snippet rune is implemented as an AST preprocessor: by the time the transform phase reaches it, every {% snippet %} tag has been replaced with a Markdoc fence node. This is what makes it compose transparently inside {% codegroup %}, {% diff %}, and any future container rune that consumes fence nodes — they see snippets exactly the same as triple-backtick code blocks.
Embed a file
The minimum case — a path attribute relative to the project root.
{% snippet path="packages/runes/src/lang-map.ts" lines="1-40" /%}
Renders as a live embed of refrakt's own language-map module (the one snippet itself uses for extension inference):
/**
* Extension → language map for code-block syntax highlighting.
*
* Shared across the snippet rune, the inspect tool, the contracts generator,
* and any future rune that needs to infer a syntax-highlighting language
* from a file extension. Lives in `@refrakt-md/runes` (not in a plugin)
* because every consumer already depends on this package and plugins
* cannot be depended on by core.
*
* Authors can always override inferred languages with explicit `lang=`
* attributes; inference is the default, not the only path.
*/
/** Lowercase file extension (with leading dot) → highlight-language identifier. */
export const LANG_MAP: Readonly<Record<string, string>> = Object.freeze({
'.ts': 'typescript',
'.tsx': 'typescript',
'.js': 'javascript',
'.jsx': 'javascript',
'.mjs': 'javascript',
'.cjs': 'javascript',
'.svelte': 'svelte',
'.vue': 'vue',
'.md': 'markdoc',
'.markdoc': 'markdoc',
'.json': 'json',
'.jsonc': 'jsonc',
'.html': 'html',
'.css': 'css',
'.yml': 'yaml',
'.yaml': 'yaml',
'.toml': 'toml',
'.sh': 'bash',
'.bash': 'bash',
});
/** Fallback language for extensions not covered by {@link LANG_MAP}. */
export const FALLBACK_LANG = 'text';
/**The reader is looking at the actual file in this repository, sliced to its first 40 lines. The build re-reads the file every time it runs, so this stays in sync.
Line ranges
Slice the file with the lines attribute. Four formats:
| Input | Meaning |
|---|---|
"10-25" | Lines 10 through 25, inclusive |
"10-" | Line 10 to end of file |
"-20" | Line 1 through line 20 |
"10" | Single line (line 10 only) — shorthand for "10-10" |
1-indexed (matches editor line numbers), inclusive on both ends. Out-of-range ends clamp to the file length with a build warning; out-of-range starts (entirely past EOF) are a build error.
{% snippet path="packages/runes/src/lang-map.ts" lines="15-35" title="LANG_MAP definition" /%}
export const LANG_MAP: Readonly<Record<string, string>> = Object.freeze({
'.ts': 'typescript',
'.tsx': 'typescript',
'.js': 'javascript',
'.jsx': 'javascript',
'.mjs': 'javascript',
'.cjs': 'javascript',
'.svelte': 'svelte',
'.vue': 'vue',
'.md': 'markdoc',
'.markdoc': 'markdoc',
'.json': 'json',
'.jsonc': 'jsonc',
'.html': 'html',
'.css': 'css',
'.yml': 'yaml',
'.yaml': 'yaml',
'.toml': 'toml',
'.sh': 'bash',
'.bash': 'bash',
});Captions and titles
Snippet itself has no title attribute — it's structurally a fence by the time the transform runs, and fences don't carry titles. For a labelled chrome, wrap the snippet in {% codegroup %}, which produces title-bar chrome around a single fence:
{% codegroup title="FALLBACK_LANG constant" %}
{% snippet path="packages/runes/src/lang-map.ts" lines="42-50" /%}
{% /codegroup %}
*
* Accepts a full path (`"src/lib/foo.ts"`), a bare extension with the dot
* (`".ts"`), or a bare extension without (`"ts"`). Unknown extensions
* return {@link FALLBACK_LANG}.
*
* @example
* ```ts
* inferLanguage('src/lib/foo.ts'); // 'typescript'
* inferLanguage('.svelte'); // 'svelte'Language inference
The language is inferred from the file extension via a shared map:
| Extension | Language |
|---|---|
.ts, .tsx | typescript |
.js, .jsx, .mjs, .cjs | javascript |
.svelte | svelte |
.vue | vue |
.md, .markdoc | markdoc |
.json | json |
.jsonc | jsonc |
.html | html |
.css | css |
.yml, .yaml | yaml |
.toml | toml |
.sh, .bash | bash |
| (others) | text (no highlighting) |
Override with lang= when the extension doesn't tell the full story (a .config file that's actually JSONC, for instance).
{% snippet path="packages/runes/src/lang-map.ts" lines="1-5" lang="javascript" /%}
/**
* Extension → language map for code-block syntax highlighting.
*
* Shared across the snippet rune, the inspect tool, the contracts generator,
* and any future rune that needs to infer a syntax-highlighting languageComposition
The pre-resolve model means snippets work inside any container rune that matches fence nodes. No special handling required on either side — the container sees a fence, it consumes a fence.
Inside {% codegroup %}
Multiple snippets become tabs in a codegroup. Tab labels come from the inferred language (or set labels on codegroup for custom names).
{% codegroup %}
{% snippet path="packages/runes/src/lang-map.ts" lines="1-5" /%}
{% snippet path="packages/runes/src/util.ts" lines="1-5" /%}
{% /codegroup %}<section data-rune="code-group">
<div role="tablist" data-name="tabs">
<button data-name="tab" role="tab">
<span>lang-map.ts:1-5</span>
</button>
<button data-name="tab" role="tab">
<span>util.ts:1-5</span>
</button>
</div>
<div data-name="panels">
<div role="tabpanel" data-name="panel">
<div class="rf-codeblock">
<pre data-language="typescript" data-source="packages/runes/src/lang-map.ts" data-lines="1-5" style="--rf-start-line: 0">
<code data-language="typescript" data-source="packages/runes/src/lang-map.ts" data-lines="1-5">/**
* Extension → language map for code-block syntax highlighting.
*
* Shared across the snippet rune, the inspect tool, the contracts generator,
* and any future rune that needs to infer a syntax-highlighting language</code>
</pre>
</div>
</div>
<div role="tabpanel" data-name="panel">
<div class="rf-codeblock">
<pre data-language="typescript" data-source="packages/runes/src/util.ts" data-lines="1-5" style="--rf-start-line: 0">
<code data-language="typescript" data-source="packages/runes/src/util.ts" data-lines="1-5">import Markdoc from '@markdoc/markdoc';
import type { Tag, Config, Node, RenderableTreeNode } from '@markdoc/markdoc';
const { Ast } = Markdoc;
import { NodeFilter } from './interfaces.js';
import { isFilterMatching } from './lib/node.js';</code>
</pre>
</div>
</div>
</div>
</section>/**
* Extension → language map for code-block syntax highlighting.
*
* Shared across the snippet rune, the inspect tool, the contracts generator,
* and any future rune that needs to infer a syntax-highlighting languageimport Markdoc from '@markdoc/markdoc';
import type { Tag, Config, Node, RenderableTreeNode } from '@markdoc/markdoc';
const { Ast } = Markdoc;
import { NodeFilter } from './interfaces.js';
import { isFilterMatching } from './lib/node.js';<section class="rf-codegroup rf-codegroup--scroll" data-overflow="scroll" data-rune="code-group" data-density="compact" data-code-host="true">
<div role="tablist" data-name="tabs" class="rf-codegroup__tabs">
<button data-name="tab" role="tab" class="rf-codegroup__tab">
<span>lang-map.ts:1-5</span>
</button>
<button data-name="tab" role="tab" class="rf-codegroup__tab">
<span>util.ts:1-5</span>
</button>
</div>
<div data-name="panels" class="rf-codegroup__panels">
<div role="tabpanel" data-name="panel" class="rf-codegroup__panel">
<div class="rf-codeblock">
<pre data-language="typescript" data-source="packages/runes/src/lang-map.ts" data-lines="1-5" style="--rf-start-line: 0"><code data-language="typescript" data-source="packages/runes/src/lang-map.ts" data-lines="1-5">/**
* Extension → language map for code-block syntax highlighting.
*
* Shared across the snippet rune, the inspect tool, the contracts generator,
* and any future rune that needs to infer a syntax-highlighting language</code></pre>
</div>
</div>
<div role="tabpanel" data-name="panel" class="rf-codegroup__panel">
<div class="rf-codeblock">
<pre data-language="typescript" data-source="packages/runes/src/util.ts" data-lines="1-5" style="--rf-start-line: 0"><code data-language="typescript" data-source="packages/runes/src/util.ts" data-lines="1-5">import Markdoc from '@markdoc/markdoc';
import type { Tag, Config, Node, RenderableTreeNode } from '@markdoc/markdoc';
const { Ast } = Markdoc;
import { NodeFilter } from './interfaces.js';
import { isFilterMatching } from './lib/node.js';</code></pre>
</div>
</div>
</div>
</section>Mixed children — snippets and triple-backtick fences in the same codegroup — work uniformly:
{% codegroup %}
{% snippet path="packages/runes/src/lang-map.ts" lines="1-5" /%}
```python
# An inline Python snippet alongside a real file
def hello(): pass
```
{% /codegroup %}<section data-rune="code-group">
<div role="tablist" data-name="tabs">
<button data-name="tab" role="tab">
<span>lang-map.ts:1-5</span>
</button>
<button data-name="tab" role="tab">
<span>Python</span>
</button>
</div>
<div data-name="panels">
<div role="tabpanel" data-name="panel">
<div class="rf-codeblock">
<pre data-language="typescript" data-source="packages/runes/src/lang-map.ts" data-lines="1-5" style="--rf-start-line: 0">
<code data-language="typescript" data-source="packages/runes/src/lang-map.ts" data-lines="1-5">/**
* Extension → language map for code-block syntax highlighting.
*
* Shared across the snippet rune, the inspect tool, the contracts generator,
* and any future rune that needs to infer a syntax-highlighting language</code>
</pre>
</div>
</div>
<div role="tabpanel" data-name="panel">
<div class="rf-codeblock">
<pre data-language="python">
<code data-language="python"># An inline Python snippet alongside a real file
def hello(): pass
</code>
</pre>
</div>
</div>
</div>
</section>/**
* Extension → language map for code-block syntax highlighting.
*
* Shared across the snippet rune, the inspect tool, the contracts generator,
* and any future rune that needs to infer a syntax-highlighting language# An inline Python snippet alongside a real file
def hello(): pass
<section class="rf-codegroup rf-codegroup--scroll" data-overflow="scroll" data-rune="code-group" data-density="compact" data-code-host="true">
<div role="tablist" data-name="tabs" class="rf-codegroup__tabs">
<button data-name="tab" role="tab" class="rf-codegroup__tab">
<span>lang-map.ts:1-5</span>
</button>
<button data-name="tab" role="tab" class="rf-codegroup__tab">
<span>Python</span>
</button>
</div>
<div data-name="panels" class="rf-codegroup__panels">
<div role="tabpanel" data-name="panel" class="rf-codegroup__panel">
<div class="rf-codeblock">
<pre data-language="typescript" data-source="packages/runes/src/lang-map.ts" data-lines="1-5" style="--rf-start-line: 0"><code data-language="typescript" data-source="packages/runes/src/lang-map.ts" data-lines="1-5">/**
* Extension → language map for code-block syntax highlighting.
*
* Shared across the snippet rune, the inspect tool, the contracts generator,
* and any future rune that needs to infer a syntax-highlighting language</code></pre>
</div>
</div>
<div role="tabpanel" data-name="panel" class="rf-codegroup__panel">
<div class="rf-codeblock">
<pre data-language="python"><code data-language="python"># An inline Python snippet alongside a real file
def hello(): pass
</code></pre>
</div>
</div>
</div>
</section>Inside {% diff %}
Two snippets become before/after for a diff comparison.
{% diff mode="split" %}
{% snippet path="packages/runes/src/lang-map.ts" lines="42-50" /%}
{% snippet path="packages/runes/src/util.ts" lines="42-50" /%}
{% /diff %}<div data-rune="diff" data-rune-fields="{"mode":"split","language":""}" data-code-host>
<div data-name="header">packages/runes/src/lang-map.ts → packages/runes/src/util.ts</div>
<div data-name="split-container">
<div data-name="panel">
<pre data-name="code" data-copy-selector="[data-name="line-content"]">
<div data-name="rows">
<span data-name="line" data-line-status="remove">
<span data-name="gutter-num" data-side="before">42</span>
<span data-name="line-content" data-language="typescript"> *</span>
</span>
<span data-name="line" data-line-status="remove">
<span data-name="gutter-num" data-side="before">43</span>
<span data-name="line-content" data-language="typescript"> * Accepts a full path (`"src/lib/foo.ts"`), a bare extension with the dot</span>
</span>
<span data-name="line" data-line-status="remove">
<span data-name="gutter-num" data-side="before">44</span>
<span data-name="line-content" data-language="typescript"> * (`".ts"`), or a bare extension without (`"ts"`). Unknown extensions</span>
</span>
<span data-name="line" data-line-status="remove">
<span data-name="gutter-num" data-side="before">45</span>
<span data-name="line-content" data-language="typescript"> * return {@link FALLBACK_LANG}.</span>
</span>
<span data-name="line" data-line-status="remove">
<span data-name="gutter-num" data-side="before">46</span>
<span data-name="line-content" data-language="typescript"> *</span>
</span>
<span data-name="line" data-line-status="remove">
<span data-name="gutter-num" data-side="before">47</span>
<span data-name="line-content" data-language="typescript"> * @example</span>
</span>
<span data-name="line" data-line-status="remove">
<span data-name="gutter-num" data-side="before">48</span>
<span data-name="line-content" data-language="typescript"> * ```ts</span>
</span>
<span data-name="line" data-line-status="remove">
<span data-name="gutter-num" data-side="before">49</span>
<span data-name="line-content" data-language="typescript"> * inferLanguage('src/lib/foo.ts'); // 'typescript'</span>
</span>
<span data-name="line" data-line-status="remove">
<span data-name="gutter-num" data-side="before">50</span>
<span data-name="line-content" data-language="typescript"> * inferLanguage('.svelte'); // 'svelte'</span>
</span>
</div>
</pre>
</div>
<div data-name="panel">
<pre data-name="code" data-copy-selector="[data-name="line-content"]">
<div data-name="rows">
<span data-name="line" data-line-status="add">
<span data-name="gutter-num" data-side="after">42</span>
<span data-name="line-content" data-language="typescript"></span>
</span>
<span data-name="line" data-line-status="add">
<span data-name="gutter-num" data-side="after">43</span>
<span data-name="line-content" data-language="typescript"> include?: NodeFilter[];</span>
</span>
<span data-name="line" data-line-status="add">
<span data-name="gutter-num" data-side="after">44</span>
<span data-name="line-content" data-language="typescript">}</span>
</span>
<span data-name="line" data-line-status="add">
<span data-name="gutter-num" data-side="after">45</span>
<span data-name="line-content" data-language="typescript"></span>
</span>
<span data-name="line" data-line-status="add">
<span data-name="gutter-num" data-side="after">46</span>
<span data-name="line-content" data-language="typescript">export interface HeadingInfo {</span>
</span>
<span data-name="line" data-line-status="add">
<span data-name="gutter-num" data-side="after">47</span>
<span data-name="line-content" data-language="typescript"> level: number;</span>
</span>
<span data-name="line" data-line-status="add">
<span data-name="gutter-num" data-side="after">48</span>
<span data-name="line-content" data-language="typescript"> text: string;</span>
</span>
<span data-name="line" data-line-status="add">
<span data-name="gutter-num" data-side="after">49</span>
<span data-name="line-content" data-language="typescript"> id: string;</span>
</span>
<span data-name="line" data-line-status="add">
<span data-name="gutter-num" data-side="after">50</span>
<span data-name="line-content" data-language="typescript"> /** Canonical name of the known section this heading belongs to, if any. */</span>
</span>
</div>
</pre>
</div>
</div>
</div>42 *43 * Accepts a full path (`"src/lib/foo.ts"`), a bare extension with the dot44 * (`".ts"`), or a bare extension without (`"ts"`). Unknown extensions45 * return {@link FALLBACK_LANG}.46 *47 * @example48 * ```ts49 * inferLanguage('src/lib/foo.ts'); // 'typescript'50 * inferLanguage('.svelte'); // 'svelte'
4243 include?: NodeFilter[];44}4546export interface HeadingInfo {47 level: number;48 text: string;49 id: string;50 /** Canonical name of the known section this heading belongs to, if any. */
<div class="rf-diff rf-diff--split" data-code-host data-mode="split" data-rune="diff" data-density="full">
<div data-name="header" class="rf-diff__header">packages/runes/src/lang-map.ts → packages/runes/src/util.ts</div>
<div data-name="split-container" class="rf-diff__split-container">
<div data-name="panel" class="rf-diff__panel">
<pre data-name="code" data-copy-selector="[data-name="line-content"]" class="rf-diff__code"><div data-name="rows" class="rf-diff__rows"><span data-name="line" data-line-status="remove" class="rf-diff__line"><span data-name="gutter-num" data-side="before" class="rf-diff__gutter-num">42</span><span data-name="line-content" data-language="typescript" class="rf-diff__line-content"> *</span></span><span data-name="line" data-line-status="remove" class="rf-diff__line"><span data-name="gutter-num" data-side="before" class="rf-diff__gutter-num">43</span><span data-name="line-content" data-language="typescript" class="rf-diff__line-content"> * Accepts a full path (`"src/lib/foo.ts"`), a bare extension with the dot</span></span><span data-name="line" data-line-status="remove" class="rf-diff__line"><span data-name="gutter-num" data-side="before" class="rf-diff__gutter-num">44</span><span data-name="line-content" data-language="typescript" class="rf-diff__line-content"> * (`".ts"`), or a bare extension without (`"ts"`). Unknown extensions</span></span><span data-name="line" data-line-status="remove" class="rf-diff__line"><span data-name="gutter-num" data-side="before" class="rf-diff__gutter-num">45</span><span data-name="line-content" data-language="typescript" class="rf-diff__line-content"> * return {@link FALLBACK_LANG}.</span></span><span data-name="line" data-line-status="remove" class="rf-diff__line"><span data-name="gutter-num" data-side="before" class="rf-diff__gutter-num">46</span><span data-name="line-content" data-language="typescript" class="rf-diff__line-content"> *</span></span><span data-name="line" data-line-status="remove" class="rf-diff__line"><span data-name="gutter-num" data-side="before" class="rf-diff__gutter-num">47</span><span data-name="line-content" data-language="typescript" class="rf-diff__line-content"> * @example</span></span><span data-name="line" data-line-status="remove" class="rf-diff__line"><span data-name="gutter-num" data-side="before" class="rf-diff__gutter-num">48</span><span data-name="line-content" data-language="typescript" class="rf-diff__line-content"> * ```ts</span></span><span data-name="line" data-line-status="remove" class="rf-diff__line"><span data-name="gutter-num" data-side="before" class="rf-diff__gutter-num">49</span><span data-name="line-content" data-language="typescript" class="rf-diff__line-content"> * inferLanguage('src/lib/foo.ts'); // 'typescript'</span></span><span data-name="line" data-line-status="remove" class="rf-diff__line"><span data-name="gutter-num" data-side="before" class="rf-diff__gutter-num">50</span><span data-name="line-content" data-language="typescript" class="rf-diff__line-content"> * inferLanguage('.svelte'); // 'svelte'</span></span></div></pre>
</div>
<div data-name="panel" class="rf-diff__panel">
<pre data-name="code" data-copy-selector="[data-name="line-content"]" class="rf-diff__code"><div data-name="rows" class="rf-diff__rows"><span data-name="line" data-line-status="add" class="rf-diff__line"><span data-name="gutter-num" data-side="after" class="rf-diff__gutter-num">42</span><span data-name="line-content" data-language="typescript" class="rf-diff__line-content"></span></span><span data-name="line" data-line-status="add" class="rf-diff__line"><span data-name="gutter-num" data-side="after" class="rf-diff__gutter-num">43</span><span data-name="line-content" data-language="typescript" class="rf-diff__line-content"> include?: NodeFilter[];</span></span><span data-name="line" data-line-status="add" class="rf-diff__line"><span data-name="gutter-num" data-side="after" class="rf-diff__gutter-num">44</span><span data-name="line-content" data-language="typescript" class="rf-diff__line-content">}</span></span><span data-name="line" data-line-status="add" class="rf-diff__line"><span data-name="gutter-num" data-side="after" class="rf-diff__gutter-num">45</span><span data-name="line-content" data-language="typescript" class="rf-diff__line-content"></span></span><span data-name="line" data-line-status="add" class="rf-diff__line"><span data-name="gutter-num" data-side="after" class="rf-diff__gutter-num">46</span><span data-name="line-content" data-language="typescript" class="rf-diff__line-content">export interface HeadingInfo {</span></span><span data-name="line" data-line-status="add" class="rf-diff__line"><span data-name="gutter-num" data-side="after" class="rf-diff__gutter-num">47</span><span data-name="line-content" data-language="typescript" class="rf-diff__line-content"> level: number;</span></span><span data-name="line" data-line-status="add" class="rf-diff__line"><span data-name="gutter-num" data-side="after" class="rf-diff__gutter-num">48</span><span data-name="line-content" data-language="typescript" class="rf-diff__line-content"> text: string;</span></span><span data-name="line" data-line-status="add" class="rf-diff__line"><span data-name="gutter-num" data-side="after" class="rf-diff__gutter-num">49</span><span data-name="line-content" data-language="typescript" class="rf-diff__line-content"> id: string;</span></span><span data-name="line" data-line-status="add" class="rf-diff__line"><span data-name="gutter-num" data-side="after" class="rf-diff__gutter-num">50</span><span data-name="line-content" data-language="typescript" class="rf-diff__line-content"> /** Canonical name of the known section this heading belongs to, if any. */</span></span></div></pre>
</div>
</div>
</div>The diff doesn't know or care that its children came from snippet tags — it sees the same fence nodes a triple-backtick block would produce.
Sandbox rules
Paths are resolved relative to the directory containing refrakt.config.json. The resolver enforces the snippet sandbox at build time:
| Pattern | Outcome |
|---|---|
path="/etc/passwd" | Rejected — absolute paths are not allowed. |
path="../../etc/passwd" | Rejected — paths must stay inside the project root after normalization. |
path="link-to-outside" (symlink that points outside the project root) | Rejected — fs.realpath is checked too. |
path="src/" (a directory) | Rejected — must be a regular file. |
path="missing.ts" (file doesn't exist) | Rejected — build error names the resolved path and the page that referenced it. |
These rules make snippet safe to use on sites that accept untrusted author content (a hosted authoring product, an external editor) — the worst a malicious author can do with snippet is point at a file inside the project root.
Line numbers
Set linenumbers=true to show a numbered gutter. The starting number derives from lines= so the gutter reflects the file's real offsets — lines="74-125" starts the count at 74, not 1. SPEC-062 / WORK-304.
{% snippet path="packages/runes/src/lang-map.ts" lines="15-25" linenumbers=true /%}
Renders with the gutter starting at 15 (file coordinates), matching what you'd see in your editor:
export const LANG_MAP: Readonly<Record<string, string>> = Object.freeze({
'.ts': 'typescript',
'.tsx': 'typescript',
'.js': 'javascript',
'.jsx': 'javascript',
'.mjs': 'javascript',
'.cjs': 'javascript',
'.svelte': 'svelte',
'.vue': 'vue',
'.md': 'markdoc',
'.markdoc': 'markdoc',The implementation is pure CSS — pre[data-linenumbers] gets a counter-reset seeded by an inlined --rf-start-line custom property the fence schema computes from data-lines. No JS, no DOM rewriting.
Highlighting specific lines
highlight="74-78" emphasizes specific lines without cropping. Unlike lines= (which slices), highlight keeps the surrounding context visible and draws the eye to the lines that matter. Use it for "look at this part" callouts inside a larger code block.
{% snippet path="packages/runes/src/lang-map.ts" lines="10-40" linenumbers=true highlight="18-22" /%}
Renders the slice 10-40 with file lines 18-22 emphasized via a neutral surface tint and a primary-coloured left rail:
* Authors can always override inferred languages with explicit `lang=`
* attributes; inference is the default, not the only path.
*/
/** Lowercase file extension (with leading dot) → highlight-language identifier. */
export const LANG_MAP: Readonly<Record<string, string>> = Object.freeze({
'.ts': 'typescript',
'.tsx': 'typescript',
'.js': 'javascript',
'.jsx': 'javascript',
'.mjs': 'javascript',
'.cjs': 'javascript',
'.svelte': 'svelte',
'.vue': 'vue',
'.md': 'markdoc',
'.markdoc': 'markdoc',
'.json': 'json',
'.jsonc': 'jsonc',
'.html': 'html',
'.css': 'css',
'.yml': 'yaml',
'.yaml': 'yaml',
'.toml': 'toml',
'.sh': 'bash',
'.bash': 'bash',
});
/** Fallback language for extensions not covered by {@link LANG_MAP}. */
export const FALLBACK_LANG = 'text';
/**Range syntax matches Shiki: single ranges ("74-78"), single lines ("82"), or comma-separated mixed ("74-78,82,90-92"). Indices are file coordinates — if you've set lines="10-40" highlight="20", the highlighted line is file line 20, not the 20th line of the slice. Less surprising for citing line numbers from a real file.
Both linenumbers and highlight are also fence-level annotations — ```ts {% linenumbers=true highlight="3-5" %} works on any hand-authored fence too, no snippet required.
Attributes
| Attribute | Type | Required | Description |
|---|---|---|---|
path | String | Yes | Path to the source file, relative to the project root. |
lines | String | No | Line range. "10-25" / "10-" / "-20" / "10". 1-indexed, inclusive. |
lang | String | No | Override the extension-inferred syntax-highlighting language. |
linenumbers | Boolean | No | Show line numbers in the gutter. Start counter derives from lines (file coordinates). |
highlight | String | No | Range(s) to emphasize without cropping. Shiki-style format: "74-78", "74-78,82,90-92". File coordinates. |
No title attribute. Snippet's output is a fence; for a labelled chrome wrap in {% codegroup title="..." %}.
View source — recursively
Snippet's killer trick: feed it $file.path from the page-variable surface (Content variables) to embed the page's own source.
{% snippet path=$file.path lang="markdoc" /%}
Renders as the snippet block below — at build time it reads the page you're looking at right now and inlines its content. The reader sees the markdown that generates the page they're reading. The "see it in action" pitch in one rune.
---
title: Snippet
description: Embed a project file as a syntax-highlighted code block — composes inside codegroup, diff, and any future fence-consuming container
category: "Code & Data"
plugin: core
status: stable
type: rune
---
# Snippet
Embed the contents of a file as a syntax-highlighted code block. The file lives anywhere in your project tree (relative to the project root); refrakt reads it at build time, slices it by line range if you ask, and renders it like any other fenced code block. When the source file changes, the embedded version updates automatically on the next build — no copy-paste drift.
{% hint type="note" %}
The snippet rune is implemented as an AST preprocessor: by the time the transform phase reaches it, every `{% snippet %}` tag has been replaced with a Markdoc `fence` node. This is what makes it compose transparently inside `{% codegroup %}`, `{% diff %}`, and any future container rune that consumes fence nodes — they see snippets exactly the same as triple-backtick code blocks.
{% /hint %}
## Embed a file
The minimum case — a `path` attribute relative to the project root.
```markdoc
{% snippet path="packages/runes/src/lang-map.ts" lines="1-40" /%}
```
Renders as a live embed of refrakt's own language-map module (the one snippet itself uses for extension inference):
{% snippet path="packages/runes/src/lang-map.ts" lines="1-40" /%}
The reader is looking at the actual file in this repository, sliced to its first 40 lines. The build re-reads the file every time it runs, so this stays in sync.
## Line ranges
Slice the file with the `lines` attribute. Four formats:
| Input | Meaning |
|-------|---------|
| `"10-25"` | Lines 10 through 25, inclusive |
| `"10-"` | Line 10 to end of file |
| `"-20"` | Line 1 through line 20 |
| `"10"` | Single line (line 10 only) — shorthand for `"10-10"` |
1-indexed (matches editor line numbers), inclusive on both ends. Out-of-range ends clamp to the file length with a build warning; out-of-range starts (entirely past EOF) are a build error.
```markdoc
{% snippet path="packages/runes/src/lang-map.ts" lines="15-35" title="LANG_MAP definition" /%}
```
{% snippet path="packages/runes/src/lang-map.ts" lines="15-35" title="LANG_MAP definition" /%}
## Captions and titles
Snippet itself has no `title` attribute — it's structurally a `fence` by the time the transform runs, and fences don't carry titles. For a labelled chrome, wrap the snippet in `{% codegroup %}`, which produces title-bar chrome around a single fence:
```markdoc
{% codegroup title="FALLBACK_LANG constant" %}
{% snippet path="packages/runes/src/lang-map.ts" lines="42-50" /%}
{% /codegroup %}
```
{% codegroup title="FALLBACK_LANG constant" %}
{% snippet path="packages/runes/src/lang-map.ts" lines="42-50" /%}
{% /codegroup %}
## Language inference
The language is inferred from the file extension via a shared map:
| Extension | Language |
|-----------|----------|
| `.ts`, `.tsx` | `typescript` |
| `.js`, `.jsx`, `.mjs`, `.cjs` | `javascript` |
| `.svelte` | `svelte` |
| `.vue` | `vue` |
| `.md`, `.markdoc` | `markdoc` |
| `.json` | `json` |
| `.jsonc` | `jsonc` |
| `.html` | `html` |
| `.css` | `css` |
| `.yml`, `.yaml` | `yaml` |
| `.toml` | `toml` |
| `.sh`, `.bash` | `bash` |
| (others) | `text` (no highlighting) |
Override with `lang=` when the extension doesn't tell the full story (a `.config` file that's actually JSONC, for instance).
```markdoc
{% snippet path="packages/runes/src/lang-map.ts" lines="1-5" lang="javascript" /%}
```
{% snippet path="packages/runes/src/lang-map.ts" lines="1-5" lang="javascript" /%}
## Composition
The pre-resolve model means snippets work inside any container rune that matches `fence` nodes. No special handling required on either side — the container sees a fence, it consumes a fence.
### Inside `{% codegroup %}`
Multiple snippets become tabs in a codegroup. Tab labels come from the inferred language (or set `labels` on codegroup for custom names).
{% preview source=true %}
{% codegroup %}
{% snippet path="packages/runes/src/lang-map.ts" lines="1-5" /%}
{% snippet path="packages/runes/src/util.ts" lines="1-5" /%}
{% /codegroup %}
{% /preview %}
Mixed children — snippets and triple-backtick fences in the same codegroup — work uniformly:
{% preview source=true %}
{% codegroup %}
{% snippet path="packages/runes/src/lang-map.ts" lines="1-5" /%}
```python
# An inline Python snippet alongside a real file
def hello(): pass
```
{% /codegroup %}
{% /preview %}
### Inside `{% diff %}`
Two snippets become before/after for a diff comparison.
{% preview source=true %}
{% diff mode="split" %}
{% snippet path="packages/runes/src/lang-map.ts" lines="42-50" /%}
{% snippet path="packages/runes/src/util.ts" lines="42-50" /%}
{% /diff %}
{% /preview %}
The diff doesn't know or care that its children came from snippet tags — it sees the same `fence` nodes a triple-backtick block would produce.
## Sandbox rules
Paths are resolved relative to the directory containing `refrakt.config.json`. The resolver enforces the snippet sandbox at build time:
| Pattern | Outcome |
|---------|---------|
| `path="/etc/passwd"` | **Rejected** — absolute paths are not allowed. |
| `path="../../etc/passwd"` | **Rejected** — paths must stay inside the project root after normalization. |
| `path="link-to-outside"` (symlink that points outside the project root) | **Rejected** — `fs.realpath` is checked too. |
| `path="src/"` (a directory) | **Rejected** — must be a regular file. |
| `path="missing.ts"` (file doesn't exist) | **Rejected** — build error names the resolved path and the page that referenced it. |
These rules make snippet safe to use on sites that accept untrusted author content (a hosted authoring product, an external editor) — the worst a malicious author can do with snippet is point at a file inside the project root.
## Line numbers
Set `linenumbers=true` to show a numbered gutter. The starting number derives from `lines=` so the gutter reflects the file's real offsets — `lines="74-125"` starts the count at 74, not 1. SPEC-062 / WORK-304.
```markdoc
{% snippet path="packages/runes/src/lang-map.ts" lines="15-25" linenumbers=true /%}
```
Renders with the gutter starting at 15 (file coordinates), matching what you'd see in your editor:
{% snippet path="packages/runes/src/lang-map.ts" lines="15-25" linenumbers=true /%}
The implementation is pure CSS — `pre[data-linenumbers]` gets a `counter-reset` seeded by an inlined `--rf-start-line` custom property the fence schema computes from `data-lines`. No JS, no DOM rewriting.
## Highlighting specific lines
`highlight="74-78"` emphasizes specific lines without cropping. Unlike `lines=` (which slices), highlight keeps the surrounding context visible and draws the eye to the lines that matter. Use it for "look at this part" callouts inside a larger code block.
```markdoc
{% snippet path="packages/runes/src/lang-map.ts" lines="10-40" linenumbers=true highlight="18-22" /%}
```
Renders the slice 10-40 with file lines 18-22 emphasized via a neutral surface tint and a primary-coloured left rail:
{% snippet path="packages/runes/src/lang-map.ts" lines="10-40" linenumbers=true highlight="18-22" /%}
Range syntax matches Shiki: single ranges (`"74-78"`), single lines (`"82"`), or comma-separated mixed (`"74-78,82,90-92"`). Indices are **file coordinates** — if you've set `lines="10-40" highlight="20"`, the highlighted line is file line 20, not the 20th line of the slice. Less surprising for citing line numbers from a real file.
Both `linenumbers` and `highlight` are also fence-level annotations — `` ```ts {% linenumbers=true highlight="3-5" %} `` works on any hand-authored fence too, no snippet required.
## Attributes
| Attribute | Type | Required | Description |
|-----------|------|----------|-------------|
| `path` | String | Yes | Path to the source file, relative to the project root. |
| `lines` | String | No | Line range. `"10-25"` / `"10-"` / `"-20"` / `"10"`. 1-indexed, inclusive. |
| `lang` | String | No | Override the extension-inferred syntax-highlighting language. |
| `linenumbers` | Boolean | No | Show line numbers in the gutter. Start counter derives from `lines` (file coordinates). |
| `highlight` | String | No | Range(s) to emphasize without cropping. Shiki-style format: `"74-78"`, `"74-78,82,90-92"`. File coordinates. |
No `title` attribute. Snippet's output is a fence; for a labelled chrome wrap in `{% codegroup title="..." %}`.
## View source — recursively
Snippet's killer trick: feed it `$file.path` from the page-variable surface ({% xref "/extend/variables" /%}) to embed the page's own source.
```markdoc
{% snippet path=$file.path lang="markdoc" /%}
```
Renders as the snippet block below — at build time it reads the page you're looking at right now and inlines its content. The reader sees the markdown that generates the page they're reading. The "see it in action" pitch in one rune.
{% snippet path=$file.path lang="markdoc" /%}
## See also
- [Codegroup](/runes/codegroup) — tabbed code blocks; consumes snippet children transparently.
- [Diff](/runes/diff) — before/after code comparison; consumes snippet children transparently.
- [Content variables](/extend/variables) — `$file.path` for the view-source-of-current-page pattern.
See also
- Codegroup — tabbed code blocks; consumes snippet children transparently.
- Diff — before/after code comparison; consumes snippet children transparently.
- Content variables —
$file.pathfor the view-source-of-current-page pattern.