Skip to content

Commit b298811

Browse files
authored
feat: introduce headindIds option to disable auto generated id for headings (#284)
1 parent 5a8234f commit b298811

9 files changed

Lines changed: 72 additions & 13 deletions

File tree

docs/content/2.syntax/3.attributes.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ A paragraph {attr="value"}
204204

205205
### Headings
206206

207-
`{...}` after a heading attaches to that heading (alongside its auto-generated id):
207+
`{...}` after a heading attaches to that heading (alongside its auto-generated id when `headingIds` is enabled):
208208

209209
::code-group
210210
```mdc [Syntax]

docs/content/5.api/1.parse.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,7 @@ Both `parse()` and `createParse()` accept the same `ParseOptions`:
365365
| `autoClose` | `boolean` | `true` | Auto-close incomplete markdown syntax |
366366
| `html` | `boolean` | `true` | Parse embedded HTML tags into AST nodes |
367367
| `linkify` | `boolean` | `true` | Auto-convert URL-like text into links. Set `false` to disable |
368+
| `headingIds` | `boolean` | `true` | Auto-generate `id` attributes for `h1``h6` headings. Set `false` to disable |
368369
| `plugins` | `ComarkPlugin[]` | `[]` | Array of plugins to apply |
369370

370371
---

docs/content/5.api/3.reference.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,8 @@ interface ParseOptions {
273273
autoUnwrap?: boolean // default: true
274274
autoClose?: boolean // default: true
275275
html?: boolean // default: true
276+
linkify?: boolean // default: true
277+
headingIds?: boolean // default: true
276278
plugins?: ComarkPlugin[]
277279
}
278280
```

docs/skills/comark/references/markdown-syntax.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Comark supports all standard CommonMark and GitHub Flavored Markdown (GFM) featu
2929
###### Heading 6
3030
```
3131

32-
**Note:** All headings automatically get ID attributes generated from their content for linking (e.g., `# Hello World` becomes `<h1 id="hello-world">`).
32+
**Note:** All headings automatically get ID attributes generated from their content for linking (e.g., `# Hello World` becomes `<h1 id="hello-world">`). Set `headingIds: false` in parse options to disable auto-generated ids.
3333

3434
### Text Formatting
3535

packages/comark/src/internal/parse/token-processor.ts

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,21 +31,27 @@ interface ProcessState {
3131
headingSlugCounts: Map<string, number>
3232
headingStack: Array<{ level: number; id: string }>
3333
preservePositions: boolean
34+
headingIds: boolean
3435
}
3536

3637
// ─── main entry point ───────────────────────────────────────────────────────
3738

39+
interface TokenProcessorOptions {
40+
startLine?: number
41+
preservePositions?: boolean
42+
headingIds?: boolean
43+
}
44+
3845
/**
3946
* Convert Markdown-It tokens to a Comark tree
4047
*/
41-
export function marmdownItTokensToComarkTree(
42-
tokens: any[],
43-
options: { startLine: number; preservePositions: boolean } = { startLine: 0, preservePositions: false }
44-
): ComarkNode[] {
48+
export function marmdownItTokensToComarkTree(tokens: any[], opts?: TokenProcessorOptions): ComarkNode[] {
49+
const options = { startLine: 0, preservePositions: false, headingIds: true, ...opts }
4550
const state: ProcessState = {
4651
headingSlugCounts: new Map<string, number>(),
4752
headingStack: [],
4853
preservePositions: options.preservePositions,
54+
headingIds: options.headingIds ?? true,
4955
}
5056
const nodes: ComarkNode[] = []
5157

@@ -418,12 +424,15 @@ function processBlockToken(
418424
state
419425
)
420426
if (children.nodes.length > 0) {
421-
// Always generate ID for all headings, no exceptions
422-
const textContent = extractTextContent(children.nodes)
423-
const headingId = uniqueSlug(slugify(textContent), level, state)
424-
425-
// Merge user-supplied attrs with the auto-generated id; user `id` wins.
426-
const attrs: Record<string, unknown> = { id: headingId, ...userAttrs }
427+
let attrs: Record<string, unknown>
428+
if (state?.headingIds) {
429+
const textContent = extractTextContent(children.nodes)
430+
const headingId = uniqueSlug(slugify(textContent), level, state)
431+
// Merge user-supplied attrs with the auto-generated id; user `id` wins.
432+
attrs = { id: headingId, ...userAttrs }
433+
} else {
434+
attrs = userAttrs
435+
}
427436

428437
return {
429438
node: [headingTag, attrs, ...children.nodes] as ComarkNode,

packages/comark/src/parse.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ export function createParse<const TPlugins extends readonly ComarkPlugin<any, an
141141
let nodes = marmdownItTokensToComarkTree(state.tokens, {
142142
startLine: state.parsedLines,
143143
preservePositions: opts.streaming ?? false,
144+
headingIds: options.headingIds ?? true,
144145
})
145146

146147
if (autoUnwrap) {

packages/comark/src/types.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,21 @@ export interface ParseOptions<TPlugins extends readonly ComarkPlugin<any, any>[]
417417
*/
418418
linkify?: boolean
419419

420+
/**
421+
* Whether to auto-generate `id` attributes for `h1`–`h6` headings from their text content.
422+
* Set `false` to skip auto-generated ids; user-supplied `id` attributes are still preserved.
423+
*
424+
* @default true
425+
* @example
426+
* // With headingIds: true (default)
427+
* // # Hello World → ['h1', { id: 'hello-world' }, 'Hello World']
428+
*
429+
* // With headingIds: false
430+
* // # Hello World → ['h1', {}, 'Hello World']
431+
* // # Hello {id="custom"} → ['h1', { id: 'custom' }, 'Hello']
432+
*/
433+
headingIds?: boolean
434+
420435
/**
421436
* Additional plugins to use
422437
* @default []
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { parse } from '../src/parse'
3+
4+
describe('headingIds option', () => {
5+
it('generates id attributes by default', async () => {
6+
const tree = await parse('# Hello World')
7+
8+
expect(tree.nodes).toEqual([['h1', { id: 'hello-world' }, 'Hello World']])
9+
})
10+
11+
it('skips auto-generated ids when headingIds is false', async () => {
12+
const tree = await parse('# Hello World', { headingIds: false })
13+
14+
expect(tree.nodes).toEqual([['h1', {}, 'Hello World']])
15+
})
16+
17+
it('preserves user-supplied id when headingIds is false', async () => {
18+
const tree = await parse('# Hello {id="custom"}', { headingIds: false })
19+
20+
expect(tree.nodes).toEqual([['h1', { id: 'custom' }, 'Hello']])
21+
})
22+
23+
it('still generates hierarchical ids when headingIds is true', async () => {
24+
const tree = await parse('# Title\n\n## Section')
25+
26+
expect(tree.nodes).toEqual([
27+
['h1', { id: 'title' }, 'Title'],
28+
['h2', { id: 'section' }, 'Section'],
29+
])
30+
})
31+
})

test/bundle.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ describe('package bundle size', { timeout: 60_000 }, () => {
6767
"@comark/react": "39.9k (58 files)",
6868
"@comark/svelte": "40.1k (66 files)",
6969
"@comark/vue": "56.0k (62 files)",
70-
"comark": "363k (134 files)",
70+
"comark": "364k (134 files)",
7171
}
7272
`)
7373
})

0 commit comments

Comments
 (0)