Skip to content

Code Blocks#

View Markdown

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:

: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.

Last updated: