Skip to content
Closed
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
17 changes: 17 additions & 0 deletions docs/content/5.reference/1.parse.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,23 @@ console.timeEnd('createMarkdownParser x1000')

## Options

### Heading IDs after tree transforms

Parse with `headingIds: false` when a transform removes headings. After selecting the output,
use `createHeadingIdGenerator` from `comark/utils` in document order. It shares the parser's
slug, nesting, and duplicate rules. Pass any authored ID as the third argument; that ID wins
and is reserved against later collisions. Create one generator per document and pass only
headings that participate in the output. The generator does not traverse the tree or assign
IDs to raw HTML or code samples.

```ts
import { createHeadingIdGenerator } from 'comark/utils'

const nextId = createHeadingIdGenerator('nested')
nextId('Setup', 2) // 'setup'
nextId('Install', 3) // 'setup-install'
```

Both `parseMarkdown()` and `createMarkdownParser()` accept the same `ParserOptions`:

| Option | Type | Default | Description |
Expand Down
15 changes: 15 additions & 0 deletions packages/comark/src/internal/heading-id.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
import type { HeadingIdsOption } from 'comark'

/**
* Generate heading ids in document order using the parser's slug, hierarchy, and collision rules.
* Use with trees parsed with `headingIds: false`; pass authored ids to reserve their destinations.
* Each generator owns one document. Call it only for headings participating in the output.
*/
export function createHeadingIdGenerator(option: HeadingIdsOption = true) {
const tracker = createHeadingIdTracker(option)
return (text: string, level: number, authoredId?: string): string | undefined => {
if (!tracker) return authoredId
const generated = nextHeadingId(text, level, tracker)
reserveHeadingId(tracker, authoredId)
return authoredId ?? generated
}
}

/**
* Auto-generated heading ids. Shared by the parser (to assign ids) and the
* markdown stringifier (to tell whether a heading's id is the implicit one or
Expand Down
2 changes: 2 additions & 0 deletions packages/comark/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,3 +354,5 @@ export type { ResolveAttributesOptions } from '../internal/stringify/attributes.
// The security plugin's frontmatter rules, for objects the parser never sees.
export { sanitizeData } from '../internal/sanitize.ts'
export type { SanitizeDataOptions } from '../internal/sanitize.ts'

export { createHeadingIdGenerator } from '../internal/heading-id.ts'
25 changes: 25 additions & 0 deletions packages/comark/test/heading-id-generator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { describe, expect, it } from 'vitest'
import { parseMarkdown } from '../src/parse.ts'
import { createHeadingIdGenerator, textContent } from '../src/utils/index.ts'
import type { ElementNode, HeadingIdsOption } from '../src/types.ts'

describe('heading id generator', () => {
it.each<HeadingIdsOption>([true, false, 'nested'])('matches parser allocation with mode %s', async (mode) => {
const source = '# Title\n\n## Café {#reserved}\n\n### Child\n\n## Café\n\n## Café-1\n\n## !!!\n\n## 123'
const parsed = await parseMarkdown(source, { headingIds: mode })
const authored = await parseMarkdown(source, { headingIds: false })
const nextId = createHeadingIdGenerator(mode)
const ids = authored.nodes.map((node) => {
const heading = node as ElementNode
return nextId(textContent(heading), Number(heading[0].slice(1)), heading[1].id as string | undefined)
})
expect(ids).toEqual(parsed.nodes.map((node) => (node as ElementNode)[1].id))
})

it('allocates only selected headings and keeps document state isolated', () => {
const nextId = createHeadingIdGenerator('nested')
expect(nextId('Visible', 3)).toBe('visible')
expect(nextId('Visible', 3)).toBe('visible-1')
expect(createHeadingIdGenerator('nested')('Visible', 3)).toBe('visible')
})
})
Loading