Code Blocks#
Three opt-in features extend fenced code blocks: tree-sitter syntax highlighting, annotation syntax for highlighting and diff markers, and importing snippets from real source files. This site enables all three, so every example below is rendered live.
On-demand Run / Typecheck for samples is a separate package,
@ox-content/code-play. It is not part of
@ox-content/vite-plugin. See the Code Play example.
| Option | Type | Default |
|---|---|---|
highlight |
boolean |
false |
codeAnnotations |
boolean / CodeAnnotationsOptions |
false |
codeImports |
boolean / CodeImportOptions |
false |
codeGroups |
boolean / CodeGroupOptions |
false |
Syntax Highlighting#
Highlighting is opt-in. When enabled, fenced blocks and language-tagged inline
code go through the native tree-sitter engine. Languages with no native
grammar stay as ordinary <pre><code> — they are not highlighted.
Adjacent formats without their own bundled grammar use best-effort aliases
when the existing grammar keeps source text safe: jsonc / json5 /
webmanifest use JSON, vue / svelte / astro / angular use HTML,
flow / javascriptreact use JavaScript, and typescriptreact uses TSX.
Dotfile and config tags such as dotenv, .env, gitignore, npmrc,
ini, and conf render as escaped plain text.
Supported languages#
Native grammars tokenize the fence tags below. Aliases in a cell resolve to the same grammar. Vue, Svelte, Astro, and Angular still use the HTML grammar — crates.io does not currently ship maintained dedicated grammars that match this tree-sitter line.
| Language | Fence tags |
|---|---|
| TypeScript | typescript, ts, cts, mts |
| TSX | tsx, typescriptreact |
| JavaScript | javascript, js, cjs, mjs, jsx, javascriptreact, flow |
| Rust | rust, rs |
| JSON | json, jsonc, json5, webmanifest |
| CSS | css |
| Less | less |
| HTML | html, vue, svelte, astro, angular, mdx |
| XML | xml, svg, xsl, xslt, rss, atom, plist, xsd |
| Python | python, py |
| Go | go, golang |
| Java | java |
| C | c, h |
| C++ | cpp, c++, cc, hpp, cxx |
| YAML | yaml, yml |
| Markdown | markdown, md |
| Bash | bash, sh, shell, zsh, shellscript |
| Fish | fish |
| TOML | toml |
| WGSL | wgsl |
| SQL | sql |
| GraphQL | graphql, gql |
| Dockerfile | dockerfile, docker, containerfile |
| Ruby | ruby, rb |
| PHP | php |
| Nix | nix |
| Nushell | nu, nushell |
| C# | csharp, cs |
| Swift | swift |
| Kotlin | kotlin, kt |
| GLSL | glsl |
| Lua | lua |
| HCL | hcl, terraform, tf, tfvars |
| Make | make, makefile, mk |
| CMake | cmake |
| Vimscript | vimscript, vim |
| Diff | diff, patch, udiff |
| PowerShell | powershell, pwsh, ps1, psm1 |
| Zig | zig, zon |
| Haskell | haskell, hs |
| Elixir | elixir, ex, exs |
| Scala | scala, sc, sbt |
| R | r, rscript |
{
programs.nix-secure-enclave-key = {
enable = true;
identities.git-signing.keyFile = "~/.ssh/id_enclave_key";
};
}
let expensive = open usage.json
| where cost > 10
| get project
| uniq
function fish_prompt
set -l branch (git branch --show-current)
echo "$branch" | string upper
end
cmake_minimum_required(VERSION 3.28)
project(App)
add_executable(app main.cpp)
function! s:Run(cmd) abort
let l:output = execute(a:cmd)
echo "done"
endfunction
Unknown tags stay ordinary <pre><code> — for example perl, elm,
assembly, asm, llvm, clojure, and brainfuck. Do not alias those onto
an unrelated grammar. Plain tags such as
text, dotenv, and ini are escaped but not tokenized.
import { oxContent } from "@ox-content/vite-plugin";
export default {
plugins: [
oxContent({
highlight: true,
}),
],
};
Token colors are --octc-syntax-* CSS custom properties on
<pre class="ox-highlight css-variables">. Highlighting is tree-sitter only,
and @ox-content/theme-color-* packages resolve those variables. Without a
color scheme the properties fall back to GitHub Dark. After highlighting, code
block metadata (annotations, line numbers) is merged back into the native
output.
Code Annotations#
Annotations are opt-in so ordinary fences stay literal unless a site chooses an annotation syntax:
oxContent({
highlight: true,
codeAnnotations: {
// "attribute" (default) | "vitepress" | "both"
notation: "both",
// Attribute name used by the attribute syntax. Default: "annotate".
metaKey: "annotate",
// Render line numbers for every block. Default: false.
defaultLineNumbers: false,
},
});
Supported annotation kinds are highlight, warning, and error.
Attribute notation#
The default notation is a single fence attribute with kind:lines groups
separated by ;. Line selectors accept single lines (5) and ranges (3-4):
```ts annotate="highlight:1,6;warning:2;error:3"
export function loadUser(input: string) {
if (!input) console.warn("missing payload");
throw new Error("missing id");
}
const user = loadUser(payload);
console.log(user);
```
Rendered:
export function loadUser(input: string) {
if (!input) console.warn("missing payload");
throw new Error("missing id");
}
const user = loadUser(payload);
console.log(user);
VitePress notation#
notation: "vitepress" (or "both") enables VitePress-compatible fence
metadata and inline comment directives. The fence meta pieces compose
independently:
{1,3}— highlighted lines.[config.ts]— a filename label rendered above the block.:line-numbers/:line-numbers=7/:no-line-numbers— line numbers per block, with an optional start.
```ts:line-numbers=7 {1,3} [config.ts]
const token = readToken();
const expires = readExpiry(token);
refreshBefore(expires);
```
Rendered:
const token = readToken();
const expires = readExpiry(token);
refreshBefore(expires);
Inline comment directives annotate the line they sit on and are removed from
the output. This block is authored with // [!code warning] on the second
line and // [!code error] on the third:
const token = readToken();
console.warn("Token expires soon");
throw new Error("Token is invalid");
Diff notation uses // [!code --] for removed and // [!code ++] for added
lines — this block carries them on the two return lines:
export function resolve(id: string) {
return legacyResolve(id);
return nativeResolve(id);
}
// [!code focus] (or // [!code focus:3] for a range) dims everything but
the focused lines.
Dense blocks#
Dense API pages can opt a block into stable line fragments and wrapping without custom components:
```ts:line-numbers=27 :line-links=auth-loader :wrap [src/auth/load-user.ts]
export async function loadUserSession(request: Request) {
const token = request.headers.get("authorization") ?? request.headers.get("x-legacy-auth-token");
return fetchSession(token);
}
```
Rendered:
export async function loadUserSession(request: Request) {
const token = request.headers.get("authorization") ?? request.headers.get("x-legacy-auth-token");
return fetchSession(token);
}
:line-links adds id targets to rendered lines. With an explicit prefix,
line 27 above can be linked as #auth-loader-L27; without one, the prefix is
derived from the filename caption when present. :wrap wraps long lines inside
the code frame for mobile-heavy docs, while :no-wrap keeps the default
horizontal scrolling.
When ssg.readerChrome.copy is enabled, blocks whose VitePress inline
directives changed the visible code expose the original fence source to the
copy button. Plain and purely metadata-decorated blocks still copy their
visible code text.
Inline directives are consumed wherever they appear inside a code block — including fence examples nested in an outer fence — so use the escape directive below when a line needs to show annotation-looking text.
Escaping#
A standalone // [!code escape] comment is removed from the output and makes
the next line render literally. This block is authored with an escape comment
above the first console.warn line, so its // [!code warning] survives as
text while the second one becomes an annotation:
console.warn("literal"); // [!code warning]
console.warn("annotated");
Custom meta key#
Swap annotate for a more domain-specific attribute name:
oxContent({
codeAnnotations: {
metaKey: "markers",
},
});
```ts markers="highlight:2;warning:3"
const token = readToken();
refreshToken(token);
console.warn("Token expires soon");
```
Code Imports#
Import checked source files into Markdown instead of copy-pasting them:
oxContent({
codeImports: {
// Root for `@/` imports. Defaults to the Vite project root.
rootDir: process.cwd(),
},
});
The fence language is inferred from the file extension, and imported snippets go through the same highlighting and annotation pipeline as inline fences.
Writing <<< @/snippets/greet.ts on its own line imports the whole file:
export interface Greeting {
name: string;
message: string;
}
// #region greet
export function greet(name: string): Greeting {
return {
name,
message: `Hello, ${name}!`,
};
}
// #endregion greet
export function farewell(name: string): string {
return `Goodbye, ${name}.`;
}
A {1-4} suffix — <<< @/snippets/greet.ts{1-4} — imports a line range:
export interface Greeting {
name: string;
message: string;
}
A named suffix — <<< @/snippets/greet.ts{greet} — imports the region
delimited by #region greet / #endregion greet comments, with the markers
themselves stripped:
export function greet(name: string): Greeting {
return {
name,
message: `Hello, ${name}!`,
};
}
Because imports resolve at transform time, editing the source file updates every page that imports it, and stale docs snippets stop being possible.
<<< references are resolved inside fenced code blocks too, so quote the
syntax with inline code (as this page does) when you need to show it
literally.
Code Groups#
For adjacent JS/TS/shell alternatives, opt in to codeGroups and wrap the
fences in ::: code-group instead of hand-writing <tabs>. Titles come
from ```ts [label] or fence meta. See Code Groups.
Related#
- Code Groups — VitePress-style grouped fences.
- Quality Checks — lint, type-check, and test the code blocks themselves.
- Typed Hover — build-time TypeScript hover overlays on
twoslashfences. - Code Annotations example
- Code Imports example