Skip to content

Syntax Extensions#

View Markdown

Non-standard Markdown syntax is opt-in, so ordinary documents render the same everywhere until a site explicitly enables an extension.

Option Type Default
emojiShortcodes boolean / EmojiShortcodeOptions false
wikiLinks boolean / WikiLinkOptions false
attrs boolean / AttrsOptions false
cjkEmphasis boolean false

Emoji Shortcodes#

Expand GitHub-style :shortcode: aliases to Unicode emoji:

import { oxContent } from "@ox-content/vite-plugin";

export default {
  plugins: [
    oxContent({
      emojiShortcodes: true,
    }),
  ],
};

The built-in table covers hundreds of common aliases. Expansion happens outside fenced and inline code, and unknown shortcodes are left unchanged:

Ship it :rocket: :tada:

Status: :white_check_mark: passed, :warning: flaky, :x: failed

Unknown aliases like :no-such-emoji: stay untouched, and so does
inline code: `:rocket:`.

Rendered:

Ship it 🚀 🎉

Status: ✅ passed, ⚠️ flaky, ❌ failed

Unknown aliases like :no-such-emoji: stay untouched, and so does inline code: :rocket:.

Custom shortcodes#

Custom values are merged into the built-in table and override it on conflict. Keys are written without colons:

oxContent({
  emojiShortcodes: {
    custom: {
      shipit: "🚢",
      oxc: "🦀",
    },
  },
});

Resolve Obsidian-style [[target]] links into normal site links:

oxContent({
  wikiLinks: {
    // Defaults to the top-level `base` option.
    baseUrl: "/docs/",
  },
});

The expansion runs before Markdown parsing, and fenced code blocks and inline code spans are protected. Given this source:

See [[getting-started|Getting started]] and [[api/transform#options]].

the transform emits:

<p>
  See <a href="/docs/getting-started">Getting started</a> and
  <a href="/docs/api/transform#options">api/transform#options</a>.
</p>

[[target]] uses the target as the label, [[target|label]] overrides it, and #fragment parts are slugified. Site-relative targets are prefixed with baseUrl.

Wiki links also run before raw HTML is parsed, so [[...]] inside literal <code> tags in embedded HTML is expanded too — keep literal examples inside Markdown code spans or fences instead.

Attribute Syntax#

Add IDs, classes, and attributes with markdown-it-attrs syntax:

oxContent({
  attrs: true,
});

Supported tokens are #id, .class, and key=value. A trailing {...} block attaches to the element rendered from that line:

A lead paragraph. {.lead}

## Install {.section data-section=install}

produces:

<p class="lead">A lead paragraph.</p>

<h2 id="install" class="section" data-section="install">Install</h2>

The transform runs as a post-render HTML pass over the full document — raw HTML embedded in Markdown is affected as well, so literal {...} examples belong in code spans or fences.

CJK Emphasis#

Emphasis adjacent to CJK characters needs no configuration — CommonMark's delimiter rules already allow it, and no ASCII spaces are required:

これは**重要**です。次の文でも*強調*できます。

Rendered:

これは重要です。次の文でも強調できます。

What plain CommonMark rejects is a delimiter run sitting directly against punctuation on its outer side. Its flanking rules read Unicode punctuation as a whole, so East Asian punctuation blocks a run just like ASCII punctuation does, and A**強調。**B stays literal text. Latin prose rarely hits this because a space usually separates the two; CJK sets punctuation against the preceding word, so it comes up constantly.

cjkEmphasis classifies East Asian punctuation as an ordinary character for that decision only:

oxContent({
  cjkEmphasis: true,
});
A**強調。**B

renders as A<strong>強調。</strong>B with the option on, and as literal text with it off. Halfwidth ASCII punctuation is deliberately untouched, so a Latin document parses identically either way.

This is a deliberate deviation from the specification, which is why it is opt-in. See CJK Emphasis for the exact boundary and the reclassified character ranges.

Last updated: