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-elevation="flat" 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-elevation="flat" 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 | ✓ | Path to the source file, relative to the project root. Rejected if it escapes the root. |
lines | string | — | Line range. Formats: "10-25", "10-" (to EOF), "-20" (from start), "10" (single line). 1-indexed, inclusive. |
lang | string | — | Syntax-highlighting language hint. Overrides the extension-based inference. |
linenumbers | boolean | — | Show line numbers in the gutter. Starting number derives from the `lines` range start (e.g. lines="74-125" → first line is 74), so numbers reflect the file's real offsets. WORK-304. |
highlight | string | — | Range(s) to emphasize without cropping — Shiki-style format: "74-78", "74-78,82,90-92". Indices are file coordinates (same frame as `lines=`). Use this when you want full context visible but want to draw the eye to specific lines. WORK-304. |
Universal attributes
bg
The background layer (SPEC-088): image, video, gradient, flat overlay wash and legibility scrim, in a single injected layer behind the rune's content.
| Attribute | Type | Required | Description |
|---|---|---|---|
bg | string | — | Background preset applied to this block |
bg-from | string | — | Gradient start colour — a semantic token name (→ var(--rf-color-*)) |
bg-gradient | "to-t" | "to-b" | "to-l" | "to-r" | "to-tr" | "to-br" | "to-bl" | "to-tl" | — | Gradient direction (bounded named set) |
bg-gradient-type | "linear" | "radial" | "conic" | — | Gradient type |
bg-to | string | — | Gradient end colour — a semantic token name |
bg-via | string | — | Optional middle gradient stop — a semantic token name |
scrim | "top" | "bottom" | "left" | "right" | "none" | — | Scrim direction (heaviest edge); presence turns the scrim on, "none" opts out of the default cover scrim |
scrim-blur | "none" | "sm" | "md" | "lg" | — | Frost scrim blur amount |
scrim-strength | "sm" | "md" | "lg" | — | Gradient scrim strength |
scrim-tone | "dark" | "light" | — | Whether the scrim darkens (for light text) or lightens (for dark text) |
scrim-type | "gradient" | "frost" | — | Scrim treatment: gradient (default) or frost (backdrop blur) |
elevation
The chrome/depth ladder (SPEC-107). The skin maps each rung to a chrome bundle by attribute, so there is no BEM class.
| Attribute | Type | Required | Description |
|---|---|---|---|
elevation | "sunken" | "flush" | "flat" | "raised" | "floating" | "overlay" | "none" | "sm" | "md" | "lg" | — | Surface depth on the SPEC-107 ladder (sunken→overlay); none/sm/md/lg are deprecated aliases |
inset
Internal padding override.
| Attribute | Type | Required | Description |
|---|---|---|---|
inset | "flush" | "tight" | "default" | "loose" | "breathe" | — | Inner padding of this block |
motion
Scroll-reveal entrance (SPEC-105). The author declares the character, the theme owns the choreography, a behaviour owns the timing.
| Attribute | Type | Required | Description |
|---|---|---|---|
reveal | "none" | "fade" | "slide" | "scale" | "blur" | — | Scroll-reveal entrance character (none|fade|slide|scale|blur); the theme owns the choreography |
stagger | boolean | — | Cascade this block's items in as it reveals (no-op on single-child runes) |
spacing
Block-level rhythm override.
| Attribute | Type | Required | Description |
|---|---|---|---|
spacing | "flush" | "tight" | "default" | "loose" | "breathe" | — | Vertical spacing above and below this block |
substrate
Generated pattern fills (SPEC-087). Markers only — the engine sets the attributes and cell/opacity custom properties, CSS draws the pattern.
| Attribute | Type | Required | Description |
|---|---|---|---|
substrate | "dots" | "grid" | "lines" | "cross" | "checker" | "none" | — | Generated surface pattern |
substrate-fill | "inherit" | "inset" | — | Surface fill the pattern sits on (full colour stays with tint) |
substrate-opacity | "sm" | "md" | "lg" | — | Pattern ink strength |
substrate-size | "sm" | "md" | "lg" | — | Pattern cell size |
substrate-target | "self" | "media" | — | Which surface the pattern fills (overrides the rune/theme default) |
tint
Per-rune colour override (SPEC-053): a named tint from the theme registry, with inline per-token overrides layered on top.
| Attribute | Type | Required | Description |
|---|---|---|---|
tint | string | — | Color tint preset applied to this block |
tint-mode | "auto" | "dark" | "light" | — | Whether the tint adapts to auto, dark, or light mode |
width
The track a block rune occupies.
| Attribute | Type | Required | Description |
|---|---|---|---|
width | "compact" | "narrow" | "content" | "wide" | "full" | — | Maximum width constraint for this block |
| Not available | Why |
|---|---|
| dropcap, reading | this rune declares no prose body |
| frame | this rune declares neither a `frameTarget` nor a media section |
| prominence | this rune has no page-section header |
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
{% include file="rune-attributes.md" variables={r: "rune:snippet"} /%}
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 "/docs/authoring/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](/docs/authoring/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.