comark 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/dist/internal/frontmatter.d.ts +1 -0
  2. package/dist/internal/frontmatter.js +4 -2
  3. package/dist/internal/parse/auto-close/index.js +69 -31
  4. package/dist/internal/parse/auto-close/table.js +12 -9
  5. package/dist/internal/parse/auto-unwrap.js +6 -10
  6. package/dist/internal/parse/html/html_block_rule.js +10 -16
  7. package/dist/internal/parse/html/html_inline_rule.js +3 -7
  8. package/dist/internal/parse/html/html_re.js +1 -1
  9. package/dist/internal/parse/html/index.d.ts +1 -0
  10. package/dist/internal/parse/html/index.js +15 -3
  11. package/dist/internal/parse/syntax/block-params.d.ts +9 -0
  12. package/dist/internal/parse/syntax/block-params.js +48 -0
  13. package/dist/internal/parse/syntax/brackets.d.ts +8 -0
  14. package/dist/internal/parse/syntax/brackets.js +20 -0
  15. package/dist/internal/parse/syntax/props.d.ts +5 -0
  16. package/dist/internal/parse/syntax/props.js +119 -0
  17. package/dist/internal/parse/token-processor.js +89 -50
  18. package/dist/internal/props-validation.js +4 -9
  19. package/dist/internal/stringify/attributes.d.ts +7 -0
  20. package/dist/internal/stringify/attributes.js +56 -1
  21. package/dist/internal/stringify/handlers/a.js +1 -3
  22. package/dist/internal/stringify/handlers/blockquote.js +19 -4
  23. package/dist/internal/stringify/handlers/code.js +1 -3
  24. package/dist/internal/stringify/handlers/emphesis.js +1 -3
  25. package/dist/internal/stringify/handlers/heading.js +6 -1
  26. package/dist/internal/stringify/handlers/html.js +34 -18
  27. package/dist/internal/stringify/handlers/img.js +1 -3
  28. package/dist/internal/stringify/handlers/li.js +18 -9
  29. package/dist/internal/stringify/handlers/mdc.js +3 -4
  30. package/dist/internal/stringify/handlers/ol.js +12 -2
  31. package/dist/internal/stringify/handlers/p.d.ts +1 -1
  32. package/dist/internal/stringify/handlers/p.js +8 -1
  33. package/dist/internal/stringify/handlers/pre.js +20 -14
  34. package/dist/internal/stringify/handlers/strong.js +1 -3
  35. package/dist/internal/stringify/handlers/table.js +14 -5
  36. package/dist/internal/stringify/handlers/template.js +5 -2
  37. package/dist/internal/stringify/handlers/ul.js +12 -2
  38. package/dist/internal/stringify/state.js +1 -1
  39. package/dist/internal/yaml.js +1 -1
  40. package/dist/parse.d.ts +4 -4
  41. package/dist/parse.js +20 -10
  42. package/dist/plugins/alert.d.ts +1 -1
  43. package/dist/plugins/alert.js +1 -1
  44. package/dist/plugins/binding.d.ts +1 -1
  45. package/dist/plugins/binding.js +1 -3
  46. package/dist/plugins/breaks.d.ts +1 -1
  47. package/dist/plugins/breaks.js +1 -1
  48. package/dist/plugins/emoji.d.ts +1 -1
  49. package/dist/plugins/emoji.js +8 -8
  50. package/dist/plugins/footnotes.d.ts +1 -1
  51. package/dist/plugins/footnotes.js +19 -13
  52. package/dist/plugins/headings.d.ts +19 -8
  53. package/dist/plugins/headings.js +27 -19
  54. package/dist/plugins/highlight.d.ts +2 -12
  55. package/dist/plugins/highlight.js +201 -103
  56. package/dist/plugins/json-render.d.ts +1 -1
  57. package/dist/plugins/json-render.js +5 -9
  58. package/dist/plugins/math.d.ts +1 -1
  59. package/dist/plugins/math.js +4 -6
  60. package/dist/plugins/mermaid.d.ts +1 -1
  61. package/dist/plugins/mermaid.js +6 -20
  62. package/dist/plugins/punctuation.d.ts +1 -1
  63. package/dist/plugins/punctuation.js +5 -6
  64. package/dist/plugins/security.d.ts +1 -1
  65. package/dist/plugins/security.js +2 -2
  66. package/dist/plugins/summary.d.ts +4 -1
  67. package/dist/plugins/syntax.d.ts +49 -0
  68. package/dist/plugins/syntax.js +558 -0
  69. package/dist/plugins/task-list.d.ts +2 -2
  70. package/dist/plugins/task-list.js +11 -8
  71. package/dist/plugins/toc.d.ts +3 -1
  72. package/dist/plugins/toc.js +1 -1
  73. package/dist/types.d.ts +57 -12
  74. package/dist/utils/comark.tmLanguage.d.ts +335 -0
  75. package/dist/utils/comark.tmLanguage.js +597 -0
  76. package/dist/utils/helpers.d.ts +16 -4
  77. package/dist/utils/helpers.js +16 -6
  78. package/dist/utils/index.d.ts +5 -0
  79. package/dist/utils/index.js +25 -3
  80. package/package.json +40 -40
  81. package/skills/comark/references/rendering-svelte.md +51 -7
  82. package/dist/internal/stringify/indent.d.ts +0 -5
  83. package/dist/internal/stringify/indent.js +0 -9
  84. package/dist/vite.d.ts +0 -1
  85. package/dist/vite.js +0 -1
  86. package/skills/skills/comark/AGENTS.md +0 -261
  87. package/skills/skills/comark/SKILL.md +0 -489
  88. package/skills/skills/comark/references/markdown-syntax.md +0 -599
  89. package/skills/skills/comark/references/parsing-ast.md +0 -378
  90. package/skills/skills/comark/references/rendering-react.md +0 -445
  91. package/skills/skills/comark/references/rendering-svelte.md +0 -453
  92. package/skills/skills/comark/references/rendering-vue.md +0 -462
  93. /package/skills/{skills/migrate-mdc-to-comark → migrate-mdc-to-comark}/SKILL.md +0 -0
@@ -1,11 +1,11 @@
1
- import { indent } from "../indent.js";
1
+ import { indent } from "../../../utils/index.js";
2
2
  import { comarkAttributes, comarkYamlAttributes } from "../attributes.js";
3
3
  import { html } from "./html.js";
4
4
  // HTML elements that always create an inline context for their children
5
5
  const INLINE_HTML_ELEMENTS = new Set(['a', 'strong', 'em', 'span']);
6
6
  export async function mdc(node, state, parent) {
7
7
  const [tag, attr, ...children] = node;
8
- const { $, ...attributes } = attr;
8
+ const { $: _, ...attributes } = attr;
9
9
  if (tag === 'table') {
10
10
  return html(node, state);
11
11
  }
@@ -28,8 +28,7 @@ export async function mdc(node, state, parent) {
28
28
  content = content.trimEnd();
29
29
  const attrs = attributeEntries.length > 0 ? comarkAttributes(attributes) : '';
30
30
  if (tag === 'span') {
31
- return `[${content}]${attrs}`
32
- + (inline ? '' : state.context.blockSeparator);
31
+ return `[${content}]${attrs}` + (inline ? '' : state.context.blockSeparator);
33
32
  }
34
33
  const fence = ':'.repeat((state.nodeDepthInTree || 0) + 2);
35
34
  let result = `:${tag}${content && `[${content}]`}${attrs}` + (!parent ? state.context.blockSeparator : '');
@@ -1,4 +1,5 @@
1
- import { indent } from "../indent.js";
1
+ import { indent } from "../../../utils/index.js";
2
+ import { comarkAttributes, userBlockAttrs } from "../attributes.js";
2
3
  export async function ol(node, state) {
3
4
  const children = node.slice(2);
4
5
  const revert = state.applyContext({ list: true, order: 1, listIndent: 3 });
@@ -7,12 +8,21 @@ export async function ol(node, state) {
7
8
  result += await state.one(child, state);
8
9
  }
9
10
  result = result.trim();
11
+ state.applyContext(revert);
12
+ // ol with user attrs round-trips via `::ol{attrs}\n1. …\n::` — the native
13
+ // markdown list syntax has no slot for list-level attrs.
14
+ const attrs = comarkAttributes(userBlockAttrs('ol', node[1]));
15
+ if (attrs) {
16
+ if (revert.list) {
17
+ return '\n' + indent(`::ol${attrs}\n${result}\n::`, { width: revert.listIndent || 2 });
18
+ }
19
+ return `::ol${attrs}\n${result}\n::` + state.context.blockSeparator;
20
+ }
10
21
  if (revert.list) {
11
22
  result = '\n' + indent(result, { width: revert.listIndent || 2 });
12
23
  }
13
24
  else {
14
25
  result = result + state.context.blockSeparator;
15
26
  }
16
- state.applyContext(revert);
17
27
  return result;
18
28
  }
@@ -1,3 +1,3 @@
1
1
  import type { State } from 'comark/render';
2
2
  import type { ComarkElement } from 'comark';
3
- export declare function p(node: ComarkElement, state: State): Promise<string>;
3
+ export declare function p(node: ComarkElement, state: State, parent?: ComarkElement): Promise<string>;
@@ -1,8 +1,15 @@
1
- export async function p(node, state) {
1
+ import { comarkAttributes } from "../attributes.js";
2
+ export async function p(node, state, parent) {
2
3
  const children = node.slice(2);
3
4
  let result = '';
4
5
  for (const child of children) {
5
6
  result += await state.one(child, state, node);
6
7
  }
8
+ const attrs = comarkAttributes(node[1]);
9
+ if (attrs)
10
+ result = `${result.replace(/[ \t]+$/, '')} ${attrs}`;
11
+ if (parent?.[0] === 'li') {
12
+ return result;
13
+ }
7
14
  return result + state.context.blockSeparator;
8
15
  }
@@ -1,23 +1,29 @@
1
1
  import { textContent } from "../../../utils/index.js";
2
+ import { comarkAttributes, userBlockAttrs } from "../attributes.js";
2
3
  export function pre(node, state) {
3
4
  const [_, attributes, ...children] = node;
4
5
  const codeClasses = children[0]?.[1]?.class;
5
- const language = (attributes.language || (codeClasses?.split(' ').find(cls => cls.startsWith('language-')))?.slice(9)) || '';
6
+ const language = attributes.language ||
7
+ codeClasses
8
+ ?.split(' ')
9
+ .find((cls) => cls.startsWith('language-'))
10
+ ?.slice(9) ||
11
+ '';
6
12
  // Escape ] in filename
7
- const filename = attributes.filename
8
- ? ' [' + String(attributes.filename).split(']').join('\\\\]') + ']'
9
- : '';
10
- const highlights = attributes.highlights
11
- ? ' {' + formatHighlights(attributes.highlights) + '}'
12
- : '';
13
+ const filename = attributes.filename ? ' [' + String(attributes.filename).split(']').join('\\\\]') + ']' : '';
14
+ const highlights = attributes.highlights ? ' {' + formatHighlights(attributes.highlights) + '}' : '';
13
15
  // Meta always has a leading space
14
- const meta = attributes.meta
15
- ? ' ' + attributes.meta
16
- : '';
17
- const result = '```' + language + filename + highlights + meta + '\n'
18
- + String(node[1]?.code || textContent(node)).trim()
19
- + '\n```';
20
- return result + state.context.blockSeparator;
16
+ const meta = attributes.meta ? ' ' + attributes.meta : '';
17
+ const code = String(node[1]?.code || textContent(node)).trim();
18
+ const fence = code.includes('```') ? '~~~' : '```';
19
+ const fenceBlock = fence + language + filename + highlights + meta + '\n' + code + '\n' + fence;
20
+ // Extra user attrs that can't ride on the fence info string round-trip via
21
+ // `::pre{attrs}\n```…```\n::` — mirrors the wrapper form for ul/ol/table/blockquote.
22
+ const attrs = comarkAttributes(userBlockAttrs('pre', attributes));
23
+ if (attrs) {
24
+ return `::pre${attrs}\n${fenceBlock}\n::` + state.context.blockSeparator;
25
+ }
26
+ return fenceBlock + state.context.blockSeparator;
21
27
  }
22
28
  function formatHighlights(highlights) {
23
29
  if (highlights.length === 0)
@@ -6,8 +6,6 @@ export async function strong(node, state) {
6
6
  content += await state.one(child, state, node);
7
7
  }
8
8
  content = content.trim();
9
- const attrsString = Object.keys(attrs).length > 0
10
- ? comarkAttributes(attrs)
11
- : '';
9
+ const attrsString = Object.keys(attrs).length > 0 ? comarkAttributes(attrs) : '';
12
10
  return `**${content}**${attrsString}`;
13
11
  }
@@ -1,3 +1,4 @@
1
+ import { comarkAttributes, userBlockAttrs } from "../attributes.js";
1
2
  // Helper function to extract alignment from style attribute
2
3
  function getAlignment(attributes) {
3
4
  const style = attributes.style;
@@ -52,14 +53,14 @@ function getRows(element) {
52
53
  }
53
54
  // If it's thead/tbody, extract tr elements
54
55
  if (tag === 'thead' || tag === 'tbody') {
55
- return children.filter(child => typeof child !== 'string' && child[0] === 'tr');
56
+ return children.filter((child) => typeof child !== 'string' && child[0] === 'tr');
56
57
  }
57
58
  return [];
58
59
  }
59
60
  // Helper function to get cells from a row
60
61
  function getCells(row) {
61
62
  const [, , ...children] = row;
62
- return children.filter(child => typeof child !== 'string' && (child[0] === 'th' || child[0] === 'td'));
63
+ return children.filter((child) => typeof child !== 'string' && (child[0] === 'th' || child[0] === 'td'));
63
64
  }
64
65
  export async function table(node, state) {
65
66
  const [, , ...children] = node;
@@ -110,7 +111,7 @@ export async function table(node, state) {
110
111
  return getAlignment(attributes);
111
112
  });
112
113
  // Calculate column widths (minimum 3 characters per column)
113
- const columnWidths = headerContent.map(content => Math.max(3, content.length));
114
+ const columnWidths = headerContent.map((content) => Math.max(3, content.length));
114
115
  // Update column widths based on body content
115
116
  for (const row of bodyRows) {
116
117
  const cells = getCells(row);
@@ -127,7 +128,8 @@ export async function table(node, state) {
127
128
  result += ' |\n';
128
129
  // Add separator row with alignment
129
130
  result += '| ';
130
- result += columnWidths.map((width, i) => {
131
+ result += columnWidths
132
+ .map((width, i) => {
131
133
  const alignment = alignments[i];
132
134
  if (alignment === 'left') {
133
135
  return ':' + '-'.repeat(width - 1);
@@ -139,7 +141,8 @@ export async function table(node, state) {
139
141
  return '-'.repeat(width - 1) + ':';
140
142
  }
141
143
  return '-'.repeat(width);
142
- }).join(' | ');
144
+ })
145
+ .join(' | ');
143
146
  result += ' |\n';
144
147
  // Add body rows
145
148
  for (const row of bodyRows) {
@@ -156,6 +159,12 @@ export async function table(node, state) {
156
159
  result += '| ' + cellContents.join(' | ') + ' |\n';
157
160
  }
158
161
  // result already ends with \n, so we only need to add one more \n
162
+ // table with user attrs round-trips via `::table{attrs}\n<table>\n::` —
163
+ // GFM table syntax has no slot for table-level attrs.
164
+ const attrs = comarkAttributes(userBlockAttrs('table', node[1]));
165
+ if (attrs) {
166
+ return `::table${attrs}\n${result.trimEnd()}\n::\n\n`;
167
+ }
159
168
  return result + '\n';
160
169
  }
161
170
  export function thead(_node, _state) {
@@ -1,3 +1,4 @@
1
+ import { comarkAttributes } from "../attributes.js";
1
2
  // slot template
2
3
  export async function template(node, state, parent) {
3
4
  const [_, attrs] = node;
@@ -5,10 +6,12 @@ export async function template(node, state, parent) {
5
6
  // Omit #default marker if this is the only slot
6
7
  if (attrs.name === 'default') {
7
8
  const siblings = parent ? parent.slice(2) : [];
8
- const templateCount = siblings.filter(child => Array.isArray(child) && child[0] === 'template').length;
9
+ const templateCount = siblings.filter((child) => Array.isArray(child) && child[0] === 'template').length;
9
10
  if (templateCount === 1) {
10
11
  return content + state.context.blockSeparator;
11
12
  }
12
13
  }
13
- return `#${attrs.name}\n${content}` + state.context.blockSeparator;
14
+ const { name: _name, $: _$, ...rest } = attrs;
15
+ const extraAttrs = comarkAttributes(rest);
16
+ return `#${attrs.name}${extraAttrs}\n${content}` + state.context.blockSeparator;
14
17
  }
@@ -1,4 +1,5 @@
1
- import { indent } from "../indent.js";
1
+ import { indent } from "../../../utils/index.js";
2
+ import { comarkAttributes, userBlockAttrs } from "../attributes.js";
2
3
  export async function ul(node, state) {
3
4
  const children = node.slice(2);
4
5
  const revert = state.applyContext({ list: true, order: false, listIndent: 2 });
@@ -7,12 +8,21 @@ export async function ul(node, state) {
7
8
  result += await state.one(child, state);
8
9
  }
9
10
  result = result.trim();
11
+ state.applyContext(revert);
12
+ // ul with user attrs round-trips via `::ul{attrs}\n- …\n::` — the native
13
+ // markdown list syntax has no slot for list-level attrs.
14
+ const attrs = comarkAttributes(userBlockAttrs('ul', node[1]));
15
+ if (attrs) {
16
+ if (revert.list) {
17
+ return '\n' + indent(`::ul${attrs}\n${result}\n::`, { width: revert.listIndent || 2 });
18
+ }
19
+ return `::ul${attrs}\n${result}\n::` + state.context.blockSeparator;
20
+ }
10
21
  if (revert.list) {
11
22
  result = '\n' + indent(result, { width: revert.listIndent || 2 });
12
23
  }
13
24
  else {
14
25
  result = result + state.context.blockSeparator;
15
26
  }
16
- state.applyContext(revert);
17
27
  return result;
18
28
  }
@@ -173,5 +173,5 @@ function escapeHtml(text) {
173
173
  '>': '&gt;',
174
174
  '&amp;': '&',
175
175
  };
176
- return text.replace(/[<>]/g, char => map[char]);
176
+ return text.replace(/[<>]/g, (char) => map[char]);
177
177
  }
@@ -36,7 +36,7 @@ export function stringifyYaml(data, options) {
36
36
  const line = lines[i];
37
37
  const trimmed = line.trimStart();
38
38
  // Check if line starts with a quote followed by colon
39
- if (trimmed[0] === '\'' || trimmed[0] === '"') {
39
+ if (trimmed[0] === "'" || trimmed[0] === '"') {
40
40
  const quote = trimmed[0];
41
41
  if (trimmed[1] === ':') {
42
42
  // Find the closing quote
package/dist/parse.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ComarkParseFn, ParseOptions, ComarkTree } from './types.ts';
1
+ import type { ComarkParseFn, ComarkPlugin, MergePluginFrontmatter, MergePluginMeta, ParseOptions, ResolvedFrontmatter, ResolvedMeta, ComarkTree } from './types.ts';
2
2
  export { parseFrontmatter } from './internal/frontmatter.ts';
3
3
  export { defineComarkPlugin } from './utils/helpers.ts';
4
4
  /**
@@ -30,7 +30,7 @@ export { defineComarkPlugin } from './utils/helpers.ts';
30
30
  * const parseNoHtml = createParse({ html: false })
31
31
  * ```
32
32
  */
33
- export declare function createParse(options?: ParseOptions): ComarkParseFn;
33
+ export declare function createParse<const TPlugins extends readonly ComarkPlugin<any, any>[] = []>(options?: ParseOptions<TPlugins>): ComarkParseFn<ResolvedMeta<MergePluginMeta<TPlugins>>, ResolvedFrontmatter<MergePluginFrontmatter<TPlugins>>>;
34
34
  /**
35
35
  * Parse Comark content from a string
36
36
  *
@@ -64,7 +64,7 @@ export declare function createParse(options?: ParseOptions): ComarkParseFn;
64
64
  * const tree2 = await parse(content, { autoUnwrap: false })
65
65
  * ```
66
66
  */
67
- export declare function parse(markdown: string, options?: ParseOptions): Promise<ComarkTree>;
67
+ export declare function parse<const TPlugins extends readonly ComarkPlugin<any, any>[] = []>(markdown: string, options?: ParseOptions<TPlugins>): Promise<ComarkTree<ResolvedMeta<MergePluginMeta<TPlugins>>, ResolvedFrontmatter<MergePluginFrontmatter<TPlugins>>>>;
68
68
  /**
69
69
  * Creates a serialized parser function for Comark content.
70
70
  * This is useful for parsing large files in a streaming manner.
@@ -80,4 +80,4 @@ export declare function parse(markdown: string, options?: ParseOptions): Promise
80
80
  * const tree = await parse(content)
81
81
  * console.log(tree.nodes)
82
82
  */
83
- export declare function createSerializedParse(options?: ParseOptions): ComarkParseFn;
83
+ export declare function createSerializedParse<const TPlugins extends readonly ComarkPlugin<any, any>[] = []>(options?: ParseOptions<TPlugins>): ComarkParseFn<ResolvedMeta<MergePluginMeta<TPlugins>>, ResolvedFrontmatter<MergePluginFrontmatter<TPlugins>>>;
package/dist/parse.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import MarkdownExit from 'markdown-exit';
2
- import pluginMdc from '@comark/markdown-it';
2
+ import syntax from "./plugins/syntax.js";
3
3
  import taskList from "./plugins/task-list.js";
4
4
  import alert from "./plugins/alert.js";
5
5
  import { applyAutoUnwrap } from "./internal/parse/auto-unwrap.js";
@@ -44,15 +44,17 @@ export { defineComarkPlugin } from "./utils/helpers.js";
44
44
  * ```
45
45
  */
46
46
  export function createParse(options = {}) {
47
- const { autoUnwrap = true, autoClose = true, plugins = [] } = options;
47
+ const { autoUnwrap = true, autoClose = true } = options;
48
+ // Make a mutable working copy so the inferred (possibly readonly) user tuple
49
+ // isn't mutated by the unshift calls below.
50
+ const plugins = options.plugins ? [...options.plugins] : [];
51
+ plugins.unshift(syntax());
48
52
  plugins.unshift(taskList());
49
53
  plugins.unshift(alert());
50
54
  const parser = new MarkdownExit({
51
55
  html: false,
52
56
  linkify: true,
53
- })
54
- .enable(['table', 'strikethrough'])
55
- .use(pluginMdc);
57
+ }).enable(['table', 'strikethrough']);
56
58
  if (options.html !== false) {
57
59
  parser.inline.ruler.before('text', 'comark_html_inline', html_inline);
58
60
  parser.block.ruler.before('html_block', 'comark_html_block', html_block, {
@@ -60,13 +62,13 @@ export function createParse(options = {}) {
60
62
  });
61
63
  }
62
64
  for (const plugin of plugins) {
63
- for (const markdownItPlugin of (plugin.markdownItPlugins || [])) {
65
+ for (const markdownItPlugin of plugin.markdownItPlugins || []) {
64
66
  parser.use(markdownItPlugin);
65
67
  }
66
68
  }
67
69
  let lastOutput = null;
68
70
  let lastInput = null;
69
- return async (markdown, opts = {}) => {
71
+ const parseFn = async (markdown, opts = {}) => {
70
72
  const state = {
71
73
  options,
72
74
  tokens: [],
@@ -76,7 +78,8 @@ export function createParse(options = {}) {
76
78
  reusableNodes: [],
77
79
  };
78
80
  const prevOutput = lastOutput;
79
- if (opts.streaming && prevOutput && markdown.startsWith(lastInput ?? '')) {
81
+ const isStartsWithLastInput = markdown.startsWith(lastInput ?? '');
82
+ if (opts.streaming && prevOutput && isStartsWithLastInput) {
80
83
  const { remainingMarkdownStartLine, reusedNodes, remainingMarkdown } = extractReusableNodes(markdown, prevOutput);
81
84
  // If there is no remaining markdown, return the previous output
82
85
  if (!remainingMarkdown)
@@ -91,7 +94,13 @@ export function createParse(options = {}) {
91
94
  for (const plugin of options.plugins || []) {
92
95
  await plugin.pre?.(state);
93
96
  }
94
- const { content, data } = parseFrontmatter(state.markdown);
97
+ const { content, data, frontmatterText } = parseFrontmatter(state.markdown);
98
+ // Count frontmatter lines for line number tracking
99
+ if (content && frontmatterText) {
100
+ state.parsedLines +=
101
+ frontmatterText.split('\n').length + // Number of lines in frontmatter
102
+ 1; // Separator line
103
+ }
95
104
  try {
96
105
  state.tokens = parser.parse(content, {});
97
106
  }
@@ -114,7 +123,7 @@ export function createParse(options = {}) {
114
123
  }
115
124
  if (opts.streaming) {
116
125
  state.tree = {
117
- frontmatter: state.parsedLines > 0 ? (prevOutput?.frontmatter ?? data) : data,
126
+ frontmatter: frontmatterText ? data : (prevOutput?.frontmatter ?? data),
118
127
  meta: {},
119
128
  nodes: [...state.reusableNodes, ...nodes],
120
129
  };
@@ -137,6 +146,7 @@ export function createParse(options = {}) {
137
146
  }
138
147
  return state.tree;
139
148
  };
149
+ return parseFn;
140
150
  }
141
151
  /**
142
152
  * Parse Comark content from a string
@@ -1,2 +1,2 @@
1
- declare const _default: import("comark").ComarkPluginFactory<unknown>;
1
+ declare const _default: import("comark").ComarkPluginFactory<unknown, {}, {}>;
2
2
  export default _default;
@@ -30,7 +30,7 @@ const markers = {
30
30
  export default defineComarkPlugin(() => ({
31
31
  name: 'alert',
32
32
  post(state) {
33
- visit(state.tree, node => Array.isArray(node) && node[0] === 'blockquote', (node) => {
33
+ visit(state.tree, (node) => Array.isArray(node) && node[0] === 'blockquote', (node) => {
34
34
  const element = node;
35
35
  if (node[2]?.[0] === 'span') {
36
36
  const content = String(node[2][2]).toUpperCase();
@@ -7,7 +7,7 @@ export interface MdcInlineBindingOptions {
7
7
  */
8
8
  tag?: string;
9
9
  }
10
- declare const _default: import("../types").ComarkPluginFactory<MdcInlineBindingOptions>;
10
+ declare const _default: import("../types").ComarkPluginFactory<MdcInlineBindingOptions, {}, {}>;
11
11
  export default _default;
12
12
  /**
13
13
  * Markdown-format handler that renders a `binding` node back to the
@@ -36,9 +36,7 @@ const markdownItInlineBinding = (md, options = {}) => {
36
36
  export default defineComarkPlugin((opts = {}) => {
37
37
  return {
38
38
  name: 'binding',
39
- markdownItPlugins: [
40
- ((md) => markdownItInlineBinding(md, opts)),
41
- ],
39
+ markdownItPlugins: [((md) => markdownItInlineBinding(md, opts))],
42
40
  };
43
41
  });
44
42
  /**
@@ -1,2 +1,2 @@
1
- declare const _default: import("../types.ts").ComarkPluginFactory<unknown>;
1
+ declare const _default: import("../types.ts").ComarkPluginFactory<unknown, {}, {}>;
2
2
  export default _default;
@@ -3,7 +3,7 @@ import { visit } from "../utils/index.js";
3
3
  export default defineComarkPlugin(() => ({
4
4
  name: 'breaks',
5
5
  post(state) {
6
- visit(state.tree, node => Array.isArray(node) && node.length > 2, (node) => {
6
+ visit(state.tree, (node) => Array.isArray(node) && node.length > 2, (node) => {
7
7
  const parent = node;
8
8
  const newParent = [parent[0], parent[1]];
9
9
  let hasModified = false;
@@ -1,4 +1,4 @@
1
1
  import type { MarkdownItPlugin } from 'comark';
2
2
  export declare const markdownItEmoji: MarkdownItPlugin;
3
- declare const _default: import("comark").ComarkPluginFactory<unknown>;
3
+ declare const _default: import("comark").ComarkPluginFactory<unknown, {}, {}>;
4
4
  export default _default;
@@ -386,7 +386,7 @@ const emojiRule = (state, silent) => {
386
386
  const max = state.posMax;
387
387
  const start = state.pos;
388
388
  // Quick check: must start with ':'
389
- if (state.src.charCodeAt(start) !== 0x3A /* : */) {
389
+ if (state.src.charCodeAt(start) !== 0x3a /* : */) {
390
390
  return false;
391
391
  }
392
392
  // Find the closing ':'
@@ -394,7 +394,7 @@ const emojiRule = (state, silent) => {
394
394
  while (pos < max) {
395
395
  const code = state.src.charCodeAt(pos);
396
396
  // Found closing ':'
397
- if (code === 0x3A /* : */) {
397
+ if (code === 0x3a /* : */) {
398
398
  const emojiName = state.src.slice(start + 1, pos);
399
399
  // Check if this is a valid emoji
400
400
  const emojiChar = EMOJI_MAP.get(emojiName);
@@ -412,12 +412,12 @@ const emojiRule = (state, silent) => {
412
412
  }
413
413
  // Only allow word characters, digits, underscores, hyphens, and plus
414
414
  // This matches the pattern of valid emoji names
415
- if ((code >= 0x61 && code <= 0x7A) // a-z
416
- || (code >= 0x41 && code <= 0x5A) // A-Z
417
- || (code >= 0x30 && code <= 0x39) // 0-9
418
- || code === 0x5F // _
419
- || code === 0x2D // -
420
- || code === 0x2B // +
415
+ if ((code >= 0x61 && code <= 0x7a) || // a-z
416
+ (code >= 0x41 && code <= 0x5a) || // A-Z
417
+ (code >= 0x30 && code <= 0x39) || // 0-9
418
+ code === 0x5f || // _
419
+ code === 0x2d || // -
420
+ code === 0x2b // +
421
421
  ) {
422
422
  pos++;
423
423
  continue;
@@ -35,7 +35,7 @@ export interface FootnotesConfig {
35
35
  * })
36
36
  * ```
37
37
  */
38
- declare const _default: import("comark").ComarkPluginFactory<FootnotesConfig>;
38
+ declare const _default: import("comark").ComarkPluginFactory<FootnotesConfig, {}, {}>;
39
39
  export default _default;
40
40
  /**
41
41
  * Conditional stringify handler for footnotes.
@@ -9,10 +9,7 @@ const FOOTNOTE_DEF_RE = /^\[\^([^\s\]]+)\]:[ \t]?(.*)$/gm;
9
9
  * on every node in the tree.
10
10
  */
11
11
  function maybeFootnoteRef(node) {
12
- return Array.isArray(node)
13
- && node[0] === 'span'
14
- && node.length === 3
15
- && typeof node[2] === 'string';
12
+ return Array.isArray(node) && node[0] === 'span' && node.length === 3 && typeof node[2] === 'string';
16
13
  }
17
14
  /**
18
15
  * Check if a node is a footnote reference: ['span', {}, '^label']
@@ -29,12 +26,12 @@ function isFootnoteRef(node) {
29
26
  }
30
27
  const child = node[2];
31
28
  // Must start with '^' and have at least one label char
32
- if (child.charCodeAt(0) !== 0x5E /* ^ */ || child.length < 2)
29
+ if (child.charCodeAt(0) !== 0x5e /* ^ */ || child.length < 2)
33
30
  return null;
34
31
  // Check for whitespace using charCode scanning (avoid regex)
35
32
  for (let i = 1; i < child.length; i++) {
36
33
  const c = child.charCodeAt(i);
37
- if (c === 0x20 || c === 0x09 || c === 0x0A || c === 0x0D)
34
+ if (c === 0x20 || c === 0x09 || c === 0x0a || c === 0x0d)
38
35
  return null;
39
36
  }
40
37
  return child.slice(1);
@@ -59,7 +56,7 @@ function isFootnoteRef(node) {
59
56
  * ```
60
57
  */
61
58
  export default defineComarkPlugin((config = {}) => {
62
- const { label = 'Footnotes', hr = true, backRef = '↩', } = config;
59
+ const { label = 'Footnotes', hr = true, backRef = '↩' } = config;
63
60
  return {
64
61
  name: 'footnotes',
65
62
  // extract footnote definitions from markdown before MDC parsing
@@ -88,11 +85,17 @@ export default defineComarkPlugin((config = {}) => {
88
85
  refIndexMap.set(refLabel, refIndexMap.size + 1);
89
86
  }
90
87
  const refIndex = refIndexMap.get(refLabel);
91
- return ['sup', { class: 'footnote-ref' },
92
- ['a', {
88
+ return [
89
+ 'sup',
90
+ { class: 'footnote-ref' },
91
+ [
92
+ 'a',
93
+ {
93
94
  href: `#fn-${refLabel}`,
94
95
  id: `fnref-${refLabel}`,
95
- }, `[${refIndex}]`],
96
+ },
97
+ `[${refIndex}]`,
98
+ ],
96
99
  ];
97
100
  });
98
101
  let nodes = state.tree.nodes;
@@ -121,8 +124,11 @@ export default defineComarkPlugin((config = {}) => {
121
124
  const footnoteItems = [];
122
125
  for (const [refLabel] of refIndexMap) {
123
126
  const content = definitions.get(refLabel);
124
- footnoteItems.push(['li', { id: `fn-${refLabel}` },
125
- content, ' ',
127
+ footnoteItems.push([
128
+ 'li',
129
+ { id: `fn-${refLabel}` },
130
+ content,
131
+ ' ',
126
132
  ['a', { href: `#fnref-${refLabel}`, class: 'footnote-backref' }, backRef],
127
133
  ]);
128
134
  }
@@ -166,7 +172,7 @@ export const Footnote = {
166
172
  },
167
173
  handler: (node) => {
168
174
  if (node[1].class === 'footnotes') {
169
- const ol = node.find(n => Array.isArray(n) && n[0] === 'ol');
175
+ const ol = node.find((n) => Array.isArray(n) && n[0] === 'ol');
170
176
  if (!ol)
171
177
  return '';
172
178
  let result = '';
@@ -1,15 +1,17 @@
1
1
  export interface HeadingsOptions {
2
2
  /**
3
3
  * Tag to extract as title and set to `tree.meta.title`.
4
+ * Set to `false` to disable title extraction.
4
5
  * @default 'h1'
5
6
  */
6
- titleTag?: string;
7
+ titleTag?: string | false;
7
8
  /**
8
9
  * Tag to extract as description and set to `tree.meta.description`.
9
10
  * Useful alternatives: `'blockquote'`
11
+ * Set to `false` to disable description extraction.
10
12
  * @default 'p'
11
13
  */
12
- descriptionTag?: string;
14
+ descriptionTag?: string | false;
13
15
  /**
14
16
  * Whether to remove the extracted nodes from the tree.
15
17
  * @default false
@@ -29,20 +31,29 @@ export interface HeadingsOptions {
29
31
  * content is written to `tree.meta.description`. When no title was found,
30
32
  * this check starts from the very first content node.
31
33
  *
32
- * Both nodes are removed from the tree by default so they are not rendered
33
- * twice. Set `remove: false` to keep them in place.
34
+ * By default the extracted nodes are kept in the tree. Set `remove: true`
35
+ * to strip them so they are not rendered twice.
34
36
  *
35
37
  * @example
36
38
  * ```ts
37
- * // Default — h1 as title, first paragraph as description
39
+ * // Default — h1 as title, first paragraph as description, nodes kept in tree
38
40
  * headings()
39
41
  *
40
42
  * // Use a blockquote as the description instead of a paragraph
41
43
  * headings({ descriptionTag: 'blockquote' })
42
44
  *
43
- * // Extract metadata without removing the nodes from the tree
44
- * headings({ remove: false })
45
+ * // Extract metadata and remove the matched nodes from the tree
46
+ * headings({ remove: true })
47
+ *
48
+ * // Disable title extraction, only extract description
49
+ * headings({ titleTag: false })
50
+ *
51
+ * // Disable description extraction, only extract title
52
+ * headings({ descriptionTag: false })
45
53
  * ```
46
54
  */
47
- declare const _default: import("comark").ComarkPluginFactory<HeadingsOptions>;
55
+ declare const _default: import("comark").ComarkPluginFactory<HeadingsOptions, {
56
+ title?: string;
57
+ description?: string;
58
+ }, {}>;
48
59
  export default _default;