Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ packages/comark/
│ └── internal/ # Internal implementation (not exported)
│ ├── shiki.ts # Shared Shiki runtime used by both entry points
│ ├── frontmatter.ts # parseFrontmatter() / renderFrontmatter()
│ ├── heading-id.ts # Auto heading id generation (shared by parser + stringifier)
│ ├── yaml.ts # YAML helpers
│ ├── props-validation.ts # Component props validation
│ ├── parse/ # Parsing pipeline
Expand Down
20 changes: 17 additions & 3 deletions docs/content/2.syntax/1.markdown.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,31 @@ All headings automatically get an `id` from their visible text, for linking. Set

The id is the heading's visible text, lowercased, with spaces turned into `-` and other punctuation removed. A leading digit is prefixed with `_`. `**bold**`, `` `code` ``, and link labels contribute their text, not the tag name. An inline component name is omitted (`## Star :icon-star here` → `star-here`).

Two headings with the same id get a numeric suffix: the second `## Options` is `options-1`.
Ids are unique within a document. A heading whose id is already taken gets a numeric suffix, at any level and in any section:

From `h3` down, the id is prefixed with the nearest `h2` or deeper ancestor, not the `h1`:
```md
## Npm
### Install
<!-- id="install" -->

## Yarn
### Install
<!-- id="install-1" -->

## Options
## Options
<!-- id="options", then "options-1" -->
```

Ids are flat by default. Set `headingIds: 'nested'` in the [parse options](/reference/parse#options) to prefix each id from `h3` down with the nearest `h2` or deeper ancestor (never the `h1`):

```md
# Guide

## Ending space

### Slash in title
<!-- id="ending-space-slash-in-title" -->
<!-- headingIds: 'nested' → id="ending-space-slash-in-title" -->
```

## Text formatting
Expand Down
12 changes: 12 additions & 0 deletions docs/content/2.syntax/3.attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,18 @@ A paragraph {attr="value"}
```
::

A `{#id}` overrides the auto-generated id. When rendering back to markdown, the id is written out only if it differs from the auto-generated one, so custom ids survive a round trip:

::code-group
```mdc [Syntax]
# Product Documentation {#prod-doc}
```

```html [Output]
<h1 id="prod-doc">Product Documentation</h1>
```
::

### List items

Bullet, ordered, and task-list items all accept trailing attributes:
Expand Down
1 change: 1 addition & 0 deletions docs/content/5.reference/0.render.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ For the complete parsed document and node types, see the [Document Model](/getti
| [`maxInlineAttributes`](#options-maxinlineattributes) | `number` | `3` | Maximum attributes before switching to YAML block syntax. Set to `0` to always use block syntax |
| `blockAttributesStyle` | `'frontmatter' \| 'codeblock'` | `'codeblock'` | Syntax style for block attributes when they exceed `maxInlineAttributes` |
| [`frontmatterOptions`](#options-frontmatteroptions) | `DumpOptions` | `{ indent: 2, lineWidth: -1 }` | js-yaml [DumpOptions](https://github.com/nodeca/js-yaml#dump-object---options-) passed to the frontmatter serializer |
| `headingIds` | `boolean \| 'nested' \| 'flat'` | `true` | The [`headingIds`](/reference/parse#options) mode the document was parsed with. A heading id equal to the auto-generated one is omitted; any other id is written as `{#id}` |
| [`components`](#options-components) | `Record<string, NodeHandler \| ConditionalNodeHandler>` | `{}` | Custom render handlers for specific elements |
| [`data`](#options-data) | `Record<string, any>` | `{}` | Additional data passed to render handlers |

Expand Down
2 changes: 1 addition & 1 deletion docs/content/5.reference/1.parse.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ Both `parseMarkdown()` and `createMarkdownParser()` accept the same `ParserOptio
| `unwrap` | `boolean \| string \| string[]` | `false` | Remove wrapper tags from the tree, hoisting their children (MDC `unwrap` behaviour). `true` unwraps `p`; a comma/whitespace-separated string or array unwraps the listed tags; `'*'` matches any tag. Tags apply sequentially (each descends one level), and adjacent text is merged into a single string. |
| `html` | `boolean` | `true` | **Deprecated** (warns). Prefer `registerDefaultPlugins: false` and register `html()` explicitly. `html: false` still skips the default html plugin. |
| `linkify` | `boolean` | `true` | Auto-convert URL-like text into links. Set `false` to disable |
| `headingIds` | `boolean` | `true` | Auto-generate `id` attributes for `h1`–`h6` from visible heading text (not tag or component names). Unicode letters, marks, and numbers are kept (`## Café` → `café`); punctuation and symbols are dropped, and a heading that slugifies to nothing gets no id. An explicit `id` attribute wins. Set `false` to disable. See [Headings](/syntax/markdown#headings). |
| `headingIds` | `boolean \| 'nested' \| 'flat'` | `true` | Auto-generate `id` attributes for `h1`–`h6` from visible heading text (not tag or component names). `true`/`'flat'` uses the heading's own text only (`install`); `'nested'` prefixes each id with its parent heading's id (`setup-install`); `false` disables auto ids. Unicode letters, marks, and numbers are kept (`## Café` → `café`); punctuation and symbols are dropped, and a heading that slugifies to nothing gets no id. Ids are unique per document: duplicates get a `-1`, `-2`, ... suffix (`options`, `options-1`, `options-2`). An explicit `id` attribute wins. See [Headings](/syntax/markdown#headings). |
| `registerDefaultPlugins` | `boolean` | `true` | Register the built-in default plugins (`frontmatter`, `html`, `alert`, `task-list`, `components`, `attributes`). Set `false` to disable them. |
| `plugins` | `ComarkPlugin[]` | `[]` | Ordered plugins to run after the defaults. A same-name plugin replaces its default; duplicate explicit names keep the first instance. See [Default plugins](/plugins#default-plugins). |
| `tracer` | `ComarkTracer` | `undefined` | Timing recorder for the parse pipeline — see [Timing the parse](#timing-the-parse) |
Expand Down
2 changes: 1 addition & 1 deletion docs/content/5.reference/3.reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ interface ParserOptions {
/** @deprecated Prefer registerDefaultPlugins: false */
html?: boolean // default: true
linkify?: boolean // default: true
headingIds?: boolean // default: true — slug from visible text (keeps Unicode letters/numbers, omits symbol-only ids); explicit id wins
headingIds?: boolean | 'nested' | 'flat' // default: true ('flat') — slug from visible text; duplicates get -1, -2, … (keeps Unicode letters/numbers, omits symbol-only ids); explicit id wins
registerDefaultPlugins?: boolean // default: true
plugins?: ComarkPlugin[]
tracer?: ComarkTracer // OpenTelemetry-style tracer for parse spans
Expand Down
3 changes: 2 additions & 1 deletion docs/skills/comark/references/markdown-syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ The standard features:
###### Heading 6
```

**Note:** All headings automatically get an `id` from their visible text (e.g., `# Hello World` becomes `<h1 id="hello-world">`). Unicode letters, marks, and numbers are kept (`## Café` → `café`); punctuation and symbols are stripped, and a heading that slugifies to nothing gets no id. A leading digit is prefixed with `_` (`## 1. Introduction` → `_1-introduction`), and a duplicate gets a numeric suffix (`options`, `options-1`). From `h3` down, the id is prefixed with the nearest `h2`+ ancestor, not the `h1`. Inline marks and link labels contribute their text; an inline component name is omitted (`:icon-star` does not appear in the slug). An explicit `{id="..."}` replaces the generated id. Set `headingIds: false` to disable generation; a user-supplied `id` is still kept.

**Note:** All headings automatically get an `id` from their visible text (e.g., `# Hello World` becomes `<h1 id="hello-world">`). Unicode letters, marks, and numbers are kept (`## Café` → `café`); punctuation and symbols are stripped, and a heading that slugifies to nothing gets no id. A leading digit is prefixed with `_` (`## 1. Introduction` → `_1-introduction`), and a duplicate anywhere in the document gets a numeric suffix (`options`, `options-1`, `options-2`). Ids are flat by default (`## Setup` / `### Install` → `setup`, `install`); set `headingIds: 'nested'` in parse options to prefix each id from `h3` down with the nearest `h2`+ ancestor (`setup-install`). Inline marks and link labels contribute their text; an inline component name is omitted (`:icon-star` does not appear in the slug). An explicit `{id="..."}` replaces the generated id. Set `headingIds: false` to disable generation; a user-supplied `id` is still kept.

### Text Formatting

Expand Down
16 changes: 8 additions & 8 deletions packages/comark/SPEC/COMARK/attributes/headings.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,31 +40,31 @@
[
"h3",
{
"id": "h2-h3",
"id": "h3",
"attr": "value"
},
"h3"
],
[
"h4",
{
"id": "h2-h3-h4",
"id": "h4",
"attr": "value"
},
"h4"
],
[
"h5",
{
"id": "h2-h3-h4-h5",
"id": "h5",
"attr": "value"
},
"h5"
],
[
"h6",
{
"id": "h2-h3-h4-h5-h6",
"id": "h6",
"attr": "value"
},
"h6"
Expand All @@ -78,10 +78,10 @@
```html
<h1 id="h1" attr="value">h1</h1>
<h2 id="h2" attr="value">h2</h2>
<h3 id="h2-h3" attr="value">h3</h3>
<h4 id="h2-h3-h4" attr="value">h4</h4>
<h5 id="h2-h3-h4-h5" attr="value">h5</h5>
<h6 id="h2-h3-h4-h5-h6" attr="value">h6</h6>
<h3 id="h3" attr="value">h3</h3>
<h4 id="h4" attr="value">h4</h4>
<h5 id="h5" attr="value">h5</h5>
<h6 id="h6" attr="value">h6</h6>
```

## Markdown
Expand Down
50 changes: 50 additions & 0 deletions packages/comark/SPEC/COMARK/heading-attributes-disabled.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
options:
headingIds: false
---

## Input

```md
# Product Documentation {#prod-doc}

## Installation
```

## AST

```json
{
"frontmatter": {},
"meta": {},
"nodes": [
[
"h1",
{
"id": "prod-doc"
},
"Product Documentation"
],
[
"h2",
{},
"Installation"
]
]
}
```

## HTML

```html
<h1 id="prod-doc">Product Documentation</h1>
<h2>Installation</h2>
```

## Markdown

```md
# Product Documentation {#prod-doc}

## Installation
```
100 changes: 100 additions & 0 deletions packages/comark/SPEC/COMARK/heading-attributes-flat-collision.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
---
options:
headingIds: flat
---

## Input

```md
## Foo

### Foo

### Foo-1

## Bar-1

## Bar

## Bar
```

## AST

```json
{
"frontmatter": {},
"meta": {},
"nodes": [
[
"h2",
{
"id": "foo"
},
"Foo"
],
[
"h3",
{
"id": "foo-1"
},
"Foo"
],
[
"h3",
{
"id": "foo-1-1"
},
"Foo-1"
],
[
"h2",
{
"id": "bar-1"
},
"Bar-1"
],
[
"h2",
{
"id": "bar"
},
"Bar"
],
[
"h2",
{
"id": "bar-2"
},
"Bar"
]
]
}
```

## HTML

```html
<h2 id="foo">Foo</h2>
<h3 id="foo-1">Foo</h3>
<h3 id="foo-1-1">Foo-1</h3>
<h2 id="bar-1">Bar-1</h2>
<h2 id="bar">Bar</h2>
<h2 id="bar-2">Bar</h2>
```

## Markdown

```md
## Foo

### Foo

### Foo-1

## Bar-1

## Bar

## Bar
```
Loading
Loading