Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
189f9d6
feat(shiki): highlight inline code
benjamincanac Sep 10, 2026
59fe6b4
chore: drop unused test imports
benjamincanac Sep 10, 2026
ca32d40
chore: refresh the bundle size snapshot
benjamincanac Sep 10, 2026
90bff33
fix(shiki): avoid Object.hasOwn for older lib targets
benjamincanac Sep 10, 2026
2700035
fix(stringify): match highlighter classes on whole tokens
benjamincanac Sep 10, 2026
8f3f2d4
Merge remote-tracking branch 'origin/main' into HEAD
benjamincanac Sep 11, 2026
069d685
fix(stringify): collapse the highlighter class sentinel for every ren…
benjamincanac Sep 11, 2026
bca3da0
fix(shiki): let a registered grammar win over a built-in context
benjamincanac Sep 11, 2026
de431ed
refactor(shiki): trim speculative surface flagged in review
benjamincanac Sep 11, 2026
809726d
chore: refresh the bundle size snapshot
benjamincanac Sep 11, 2026
2d734f3
fix(shiki): leave raw html inline code alone
benjamincanac Sep 11, 2026
5c96072
refactor(stringify): record the user class in $ instead of a sentinel
benjamincanac Sep 11, 2026
6f96a78
test(render): pin that streaming metadata stays out of markdown output
benjamincanac Sep 11, 2026
36ebbb0
Merge remote-tracking branch 'origin/main' into feat/shiki-inline-code
benjamincanac Sep 15, 2026
6a810cb
refactor(shiki): reduce the inline code change to the minimum
benjamincanac Sep 15, 2026
ef294cd
chore(shiki): tidy the new comments
benjamincanac Sep 15, 2026
5e502b1
fix(shiki): skip inline code whose grammar is not loaded
benjamincanac Sep 15, 2026
60819c4
feat: inline highlight for rangi
farnabaz Sep 22, 2026
71197bb
Merge branch 'main' into feat/shiki-inline-code
farnabaz Sep 22, 2026
06a01bf
Update index.test.ts
farnabaz Sep 22, 2026
26d70de
test: update bundle size snapshot
github-actions[bot] Sep 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion docs/content/4.plugins/1.built-in/rangi.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Rangi (Highlight)
description: Lightweight syntax highlighting for code blocks using rangi.
description: Lightweight syntax highlighting for code blocks and language-tagged inline code using rangi.
seo:
title: Syntax Highlighting using Rangi
navigation:
Expand Down Expand Up @@ -146,6 +146,22 @@ const tokens = tokenize(source, {

Rangi ships aliases built-in (`javascript`→`js`, `typescript`→`ts`, `python`→`py`, `yml`→`yaml`, …). Comark passes the fence info string straight through. Unknown languages fall back to plain text (no throw).

### Inline code

Inline code is highlighted when it declares a language with the attributes syntax. `lang` wins over `language`:

```markdown
The type is `Ref<HTMLInputElement | null>`{lang="ts-type"} and the component is `<UButton />`{lang="vue-html"}.
```

Two fragment languages ship out of the box: `ts-type` tokenizes through the TypeScript grammar seeded with `let a:`, and `vue-html` through the Vue grammar seeded with `<template>`. Without that seed a bare type or a bare tag can fall through to weaker highlighting.

The `<code>` element gets the same `shj shiki shj-lang-…` class a highlighted `<pre>` gets, so dual-theme CSS hooks shared with the Shiki plugin cover it with no extra rules. Inline code does not wrap lines, so `lineNumbers` and `preStyles` do not apply to it.

Inline code naming a grammar that is not known (bundled, Comark, or custom via `languages`) is left exactly as it was written, with no class and no spans. `lang` is a real HTML attribute for natural language, so `` `Bonjour`{lang="fr"} `` must not be treated as code. A fenced block behaves differently and still falls back to an unhighlighted `.shiki` block, because a `<pre>` is unambiguously code.

Set `inlineCode: false` to turn this off.

### Line highlighting

Fence info `{2-3,5}` wraps the code in line spans and marks the selected lines with the `.highlight` class — same as the Shiki plugin. No `lineNumbers` option is required.
Expand Down Expand Up @@ -173,6 +189,7 @@ Returns a `ComarkPlugin` that enables rangi syntax highlighting.
| [`classPrefix`](#classprefix) | `string` | `'shj'` | Class prefix on the highlighted `<pre>` |
| [`languages`](#languages) | `Record<string, grammar>` | — | Extra custom grammars, merged over the Comark ones |
| [`preStyles`](#prestyles) | `boolean` | `false` | Add inline background/foreground styles to `<pre>` |
| [`inlineCode`](#inlinecode) | `boolean` | `true` | Highlight inline code that declares a language |

### `theme`

Expand Down Expand Up @@ -239,6 +256,16 @@ rangi({ preStyles: true })

**Default:** `false`

### `inlineCode`

Whether to highlight inline code that declares a language, e.g. `` `Ref<T>`{lang="ts-type"} ``. See [Inline code](#inline-code).

```typescript
rangi({ inlineCode: false })
```

**Default:** `true`

---

## Examples
Expand Down
30 changes: 29 additions & 1 deletion docs/content/4.plugins/1.built-in/shiki.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,22 @@ const x: number = 42
```
````

### Inline code

Inline code is highlighted when it declares a language with the attributes syntax. `lang` wins over `language`:

```markdown
The type is `Ref<HTMLInputElement | null>`{lang="ts-type"} and the component is `<UButton />`{lang="vue-html"}.
```

Two fragment languages ship out of the box: `ts-type` tokenizes through the TypeScript grammar seeded with `let a:`, and `vue-html` through the Vue grammar seeded with `<template>`. Without that seed a bare type or a bare tag falls through to plain text.

The `<code>` element gets the same `shiki` class a `<pre>` gets, so the dual-theme CSS in [Styling](#styling) covers it with no extra rules. Inline code uses the fast token path, so `transformers` and `preStyles` do not apply to it.

Inline code naming a grammar that is not registered is left exactly as it was written, with no class and no spans. `lang` is a real HTML attribute for natural language, so `` `Bonjour`{lang="fr"} `` must not be treated as code. A fenced block behaves differently and still falls back to an unhighlighted `.shiki` block, because a `<pre>` is unambiguously code.

Set `inlineCode: false` to turn this off.

### Line highlighting

Highlight specific lines using `{line-numbers}` syntax:
Expand Down Expand Up @@ -304,6 +320,7 @@ Two option types, one per entry:
| [`languages`](#options-languages) | `Array<LanguageRegistration \| LanguageRegistration[]>` | `undefined` | Extra languages (merged onto the default set) |
| [`transformers`](#options-transformers) | `ShikiTransformer[]` | `undefined` | Shiki transformers applied to every block |
| [`preStyles`](#options-prestyles) | `boolean` | `false` | Add inline background/foreground styles to `<pre>` |
| [`inlineCode`](#options-inlinecode) | `boolean` | `true` | Highlight inline code that declares a language |
| [`registerDefaultLanguages`](#options-registerdefaultlanguages) | `boolean` | `true` | Register the built-in default language set |
| [`registerDefaultThemes`](#options-registerdefaultthemes) | `boolean` | `true` | Register the built-in Material themes |

Expand All @@ -315,6 +332,7 @@ Two option types, one per entry:
| `languages` | `Array<LanguageRegistration \| LanguageRegistration[]>` | **required** | Languages to register |
| `transformers` | `ShikiTransformer[]` | `undefined` | Shiki transformers applied to every block |
| `preStyles` | `boolean` | `false` | Add inline background/foreground styles to `<pre>` |
| `inlineCode` | `boolean` | `true` | Highlight inline code that declares a language |

### `themes`

Expand Down Expand Up @@ -384,6 +402,16 @@ shiki({ preStyles: true })

**Default:** `false`

### `inlineCode`

Whether to highlight inline code that declares a language, e.g. `` `Ref<T>`{lang="ts-type"} ``. See [Inline code](#inline-code).

```typescript
shiki({ inlineCode: false })
```

**Default:** `true`

### `registerDefaultLanguages`

Standard entry only. When `true`, these languages are pre-registered: `vue`, `tsx`, `svelte`, `astro`, `typescript`, `javascript`, `bash`, `json`, `yaml` (plus the built-in Comark/`mdc` grammar). Set to `false` to control the language set entirely via `languages`.
Expand Down Expand Up @@ -486,7 +514,7 @@ Browser-side twoslash with CDN-fetched TypeScript types and interactive type pop

## Styling

Shiki outputs tokens as `<span class="line">` elements inside a `<pre class="shiki">` block.
Shiki outputs tokens as `<span class="line">` elements inside a `<pre class="shiki">` block. Highlighted inline code gets the same `shiki` class on the `<code>` element, with the token spans directly inside it and no `.line` wrapper.

### Line highlight

Expand Down
77 changes: 77 additions & 0 deletions packages/comark/SPEC/COMARK/rangi-inline-code-lang.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
---
timeout:
parse: 500ms
html: 5ms
markdown: 5ms
options:
plugins:
- rangi
---

## Input

```md
Use `const a = 1`{lang="ts"} here.
```

## AST

```json
{
"frontmatter": {},
"meta": {},
"nodes": [
[
"p",
{},
"Use ",
[
"code",
{
"lang": "ts",
"class": "shj shiki shj-lang-ts"
},
[
"span",
{
"class": "shj-syn-kwd shj-kwd",
"style": "color:#e16;--shiki-dark:#ff7cc6"
},
"const"
],
" a ",
[
"span",
{
"class": "shj-syn-oper shj-oper",
"style": "color:#5af;--shiki-dark:#80c6ff"
},
"="
],
" ",
[
"span",
{
"class": "shj-syn-num shj-num",
"style": "color:#f60;--shiki-dark:#b581fd"
},
"1"
]
],
" here."
]
]
}
```

## HTML

```html
<p>Use <code lang="ts" class="shj shiki shj-lang-ts"><span class="shj-syn-kwd shj-kwd" style="color:#e16;--shiki-dark:#ff7cc6">const</span> a <span class="shj-syn-oper shj-oper" style="color:#5af;--shiki-dark:#80c6ff">=</span> <span class="shj-syn-num shj-num" style="color:#f60;--shiki-dark:#b581fd">1</span></code> here.</p>
```

## Markdown

```md
Use `const a = 1`{lang="ts"} here.
```
80 changes: 80 additions & 0 deletions packages/comark/SPEC/COMARK/shiki-inline-code-lang.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
---
timeout:
parse: 500ms
html: 5ms
markdown: 5ms
options:
highlight:
themes:
light: 'github-dark'
---

## Input

```md
Use `Ref<T>`{lang="ts-type"} here.
```

## AST

```json
{
"frontmatter": {},
"meta": {},
"nodes": [
[
"p",
{},
"Use ",
[
"code",
{
"lang": "ts-type",
"class": "shiki shiki-themes github-dark dark:github-dark"
},
[
"span",
{
"style": "color:#B392F0"
},
"Ref"
],
[
"span",
{
"style": "color:#E1E4E8"
},
"<"
],
[
"span",
{
"style": "color:#B392F0"
},
"T"
],
[
"span",
{
"style": "color:#E1E4E8"
},
">"
]
],
" here."
]
]
}
```

## HTML

```html
<p>Use <code lang="ts-type" class="shiki shiki-themes github-dark dark:github-dark"><span style="color:#B392F0">Ref</span><span style="color:#E1E4E8">&lt;</span><span style="color:#B392F0">T</span><span style="color:#E1E4E8">&gt;</span></code> here.</p>
```

## Markdown

```md
Use `Ref<T>`{lang="ts-type"} here.
```
22 changes: 22 additions & 0 deletions packages/comark/src/internal/inline-code-lang.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { ElementNodeAttributes } from 'comark'

/**
* Fragment languages: a name authors write in `{lang="…"}` mapped onto a real
* grammar plus source that seeds the grammar state and is then discarded.
* Named after the `@nuxtjs/mdc` conventions so `ts-type` and `vue-html` keep
* working for sites migrating off it. Shared by the Shiki and rangi plugins.
*/
export const GRAMMAR_CONTEXTS = new Map<string, { lang: string; grammarContextCode: string }>([
['ts-type', { lang: 'typescript', grammarContextCode: 'let a:' }],
['vue-html', { lang: 'vue', grammarContextCode: '<template>' }],
])

/**
* Read the language an inline `<code>` declares. `lang` wins over `language`:
* `lang` is what authors type, `language` is what the fence path already uses.
*/
export function inlineCodeLanguage(attrs: ElementNodeAttributes): string | undefined {
const raw = attrs?.lang ?? attrs?.language
if (typeof raw !== 'string') return undefined
return raw.trim() || undefined
}
Loading
Loading