comark 0.3.2 → 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 (57) hide show
  1. package/dist/internal/parse/auto-close/index.js +44 -18
  2. package/dist/internal/parse/auto-unwrap.js +5 -1
  3. package/dist/internal/parse/html/html_block_rule.js +9 -15
  4. package/dist/internal/parse/html/index.d.ts +1 -0
  5. package/dist/internal/parse/html/index.js +1 -1
  6. package/dist/internal/parse/token-processor.js +70 -32
  7. package/dist/internal/stringify/attributes.d.ts +7 -0
  8. package/dist/internal/stringify/attributes.js +52 -0
  9. package/dist/internal/stringify/handlers/blockquote.js +17 -0
  10. package/dist/internal/stringify/handlers/heading.js +6 -1
  11. package/dist/internal/stringify/handlers/html.js +8 -2
  12. package/dist/internal/stringify/handlers/li.js +4 -1
  13. package/dist/internal/stringify/handlers/mdc.js +1 -1
  14. package/dist/internal/stringify/handlers/ol.js +11 -1
  15. package/dist/internal/stringify/handlers/p.js +4 -0
  16. package/dist/internal/stringify/handlers/pre.js +11 -2
  17. package/dist/internal/stringify/handlers/table.js +7 -0
  18. package/dist/internal/stringify/handlers/template.js +4 -1
  19. package/dist/internal/stringify/handlers/ul.js +11 -1
  20. package/dist/parse.d.ts +4 -4
  21. package/dist/parse.js +6 -2
  22. package/dist/plugins/alert.d.ts +1 -1
  23. package/dist/plugins/binding.d.ts +1 -1
  24. package/dist/plugins/breaks.d.ts +1 -1
  25. package/dist/plugins/emoji.d.ts +1 -1
  26. package/dist/plugins/footnotes.d.ts +1 -1
  27. package/dist/plugins/headings.d.ts +19 -8
  28. package/dist/plugins/headings.js +25 -15
  29. package/dist/plugins/highlight.d.ts +1 -1
  30. package/dist/plugins/highlight.js +4 -1
  31. package/dist/plugins/json-render.d.ts +1 -1
  32. package/dist/plugins/math.d.ts +1 -1
  33. package/dist/plugins/mermaid.d.ts +1 -1
  34. package/dist/plugins/punctuation.d.ts +1 -1
  35. package/dist/plugins/security.d.ts +1 -1
  36. package/dist/plugins/summary.d.ts +4 -1
  37. package/dist/plugins/syntax.d.ts +1 -1
  38. package/dist/plugins/syntax.js +70 -34
  39. package/dist/plugins/task-list.d.ts +1 -1
  40. package/dist/plugins/toc.d.ts +3 -1
  41. package/dist/types.d.ts +56 -12
  42. package/dist/utils/helpers.d.ts +16 -4
  43. package/dist/utils/helpers.js +15 -3
  44. package/package.json +3 -2
  45. package/skills/comark/references/rendering-svelte.md +51 -7
  46. package/dist/devtools/index.d.ts +0 -1
  47. package/dist/devtools/index.js +0 -1
  48. package/dist/devtools/register.d.ts +0 -1
  49. package/dist/devtools/register.js +0 -1
  50. package/dist/devtools/registry.d.ts +0 -1
  51. package/dist/devtools/registry.js +0 -1
  52. package/dist/devtools/vite.d.ts +0 -1
  53. package/dist/devtools/vite.js +0 -1
  54. package/dist/internal/stringify/indent.d.ts +0 -1
  55. package/dist/internal/stringify/indent.js +0 -1
  56. package/dist/vite.d.ts +0 -1
  57. package/dist/vite.js +0 -1
@@ -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;
@@ -10,5 +11,7 @@ export async function template(node, state, parent) {
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
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
  }
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
@@ -44,7 +44,10 @@ 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] : [];
48
51
  plugins.unshift(syntax());
49
52
  plugins.unshift(taskList());
50
53
  plugins.unshift(alert());
@@ -65,7 +68,7 @@ export function createParse(options = {}) {
65
68
  }
66
69
  let lastOutput = null;
67
70
  let lastInput = null;
68
- return async (markdown, opts = {}) => {
71
+ const parseFn = async (markdown, opts = {}) => {
69
72
  const state = {
70
73
  options,
71
74
  tokens: [],
@@ -143,6 +146,7 @@ export function createParse(options = {}) {
143
146
  }
144
147
  return state.tree;
145
148
  };
149
+ return parseFn;
146
150
  }
147
151
  /**
148
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;
@@ -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
@@ -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;
@@ -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;
@@ -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.
@@ -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;
@@ -35,19 +35,25 @@ function flattenNodeText(node) {
35
35
  * content is written to `tree.meta.description`. When no title was found,
36
36
  * this check starts from the very first content node.
37
37
  *
38
- * Both nodes are removed from the tree by default so they are not rendered
39
- * twice. Set `remove: false` to keep them in place.
38
+ * By default the extracted nodes are kept in the tree. Set `remove: true`
39
+ * to strip them so they are not rendered twice.
40
40
  *
41
41
  * @example
42
42
  * ```ts
43
- * // Default — h1 as title, first paragraph as description
43
+ * // Default — h1 as title, first paragraph as description, nodes kept in tree
44
44
  * headings()
45
45
  *
46
46
  * // Use a blockquote as the description instead of a paragraph
47
47
  * headings({ descriptionTag: 'blockquote' })
48
48
  *
49
- * // Extract metadata without removing the nodes from the tree
50
- * headings({ remove: false })
49
+ * // Extract metadata and remove the matched nodes from the tree
50
+ * headings({ remove: true })
51
+ *
52
+ * // Disable title extraction, only extract description
53
+ * headings({ titleTag: false })
54
+ *
55
+ * // Disable description extraction, only extract title
56
+ * headings({ descriptionTag: false })
51
57
  * ```
52
58
  */
53
59
  export default defineComarkPlugin((options = {}) => {
@@ -60,17 +66,21 @@ export default defineComarkPlugin((options = {}) => {
60
66
  const contentNodes = nodes.filter((node) => Array.isArray(node) && getTag(node) !== 'hr');
61
67
  let titleNodeIndex = -1;
62
68
  let descriptionNodeIndex = -1;
63
- const first = contentNodes[0];
64
- if (first && getTag(first) === titleTag) {
65
- titleNodeIndex = nodes.indexOf(first);
66
- state.tree.meta.title = flattenNodeText(first);
69
+ let nextContentIndex = 0;
70
+ if (titleTag !== false) {
71
+ const first = contentNodes[0];
72
+ if (first && getTag(first) === titleTag) {
73
+ titleNodeIndex = nodes.indexOf(first);
74
+ state.tree.meta.title = flattenNodeText(first);
75
+ nextContentIndex = 1;
76
+ }
67
77
  }
68
- // Description is the first content node after the (optional) title
69
- const afterTitle = titleNodeIndex !== -1 ? contentNodes.slice(1) : contentNodes;
70
- const second = afterTitle[0];
71
- if (second && getTag(second) === descriptionTag) {
72
- descriptionNodeIndex = nodes.indexOf(second);
73
- state.tree.meta.description = flattenNodeText(second);
78
+ if (descriptionTag !== false) {
79
+ const candidate = contentNodes[nextContentIndex];
80
+ if (candidate && getTag(candidate) === descriptionTag) {
81
+ descriptionNodeIndex = nodes.indexOf(candidate);
82
+ state.tree.meta.description = flattenNodeText(candidate);
83
+ }
74
84
  }
75
85
  if (remove) {
76
86
  // Remove in reverse order to preserve indices
@@ -56,5 +56,5 @@ export declare function highlightCodeBlocks(tree: ComarkTree, options?: Highligh
56
56
  * Useful for testing or when you want to reconfigure
57
57
  */
58
58
  export declare function resetHighlighter(): void;
59
- declare const _default: import("comark").ComarkPluginFactory<HighlightOptions>;
59
+ declare const _default: import("comark").ComarkPluginFactory<HighlightOptions, {}, {}>;
60
60
  export default _default;
@@ -278,9 +278,12 @@ export async function highlightCodeBlocks(tree, options = {}) {
278
278
  line += 1;
279
279
  }
280
280
  }
281
+ // Merge highlighter class with any user-supplied class (e.g. from
282
+ // `::pre{.user-class}`) so the wrapper's class isn't lost.
283
+ const userClass = typeof preAttrs.class === 'string' ? preAttrs.class.trim() : '';
281
284
  const newPreAttrs = {
282
285
  ...preAttrs,
283
- class: classStr,
286
+ class: userClass ? `${classStr} . ${userClass}` : classStr,
284
287
  tabindex: '0',
285
288
  };
286
289
  if (options.preStyles) {
@@ -49,5 +49,5 @@ interface JsonRenderConfig {
49
49
  * </template>
50
50
  * ```
51
51
  */
52
- declare const _default: import("../types").ComarkPluginFactory<JsonRenderConfig>;
52
+ declare const _default: import("../types").ComarkPluginFactory<JsonRenderConfig, {}, {}>;
53
53
  export default _default;
@@ -55,5 +55,5 @@ export declare function validateMath(code: string): boolean;
55
55
  * })
56
56
  * ```
57
57
  */
58
- declare const _default: import("comark").ComarkPluginFactory<MathConfig>;
58
+ declare const _default: import("comark").ComarkPluginFactory<MathConfig, {}, {}>;
59
59
  export default _default;
@@ -34,5 +34,5 @@ export declare function searchProps(content: string, index?: number): {
34
34
  * })
35
35
  * ```
36
36
  */
37
- declare const _default: import("comark").ComarkPluginFactory<MermaidConfig>;
37
+ declare const _default: import("comark").ComarkPluginFactory<MermaidConfig, {}, {}>;
38
38
  export default _default;
@@ -63,5 +63,5 @@ export interface PunctuationOptions {
63
63
  * })
64
64
  * ```
65
65
  */
66
- declare const _default: import("comark").ComarkPluginFactory<PunctuationOptions>;
66
+ declare const _default: import("comark").ComarkPluginFactory<PunctuationOptions, {}, {}>;
67
67
  export default _default;
@@ -6,5 +6,5 @@ interface SecurityOptions extends PropsValidationOptions {
6
6
  */
7
7
  blockedTags?: string[];
8
8
  }
9
- declare const _default: import("comark").ComarkPluginFactory<SecurityOptions>;
9
+ declare const _default: import("comark").ComarkPluginFactory<SecurityOptions, {}, {}>;
10
10
  export default _default;
@@ -1,4 +1,7 @@
1
+ import type { ComarkNode } from 'comark';
1
2
  declare const _default: import("comark").ComarkPluginFactory<{
2
3
  delimiter?: string;
3
- }>;
4
+ }, {
5
+ summary: ComarkNode[];
6
+ }, {}>;
4
7
  export default _default;
@@ -44,6 +44,6 @@ export interface SyntaxOptions {
44
44
  */
45
45
  bindingTag?: string;
46
46
  }
47
- declare const _default: import("../types.ts").ComarkPluginFactory<SyntaxOptions>;
47
+ declare const _default: import("../types.ts").ComarkPluginFactory<SyntaxOptions, {}, {}>;
48
48
  export default _default;
49
49
  export declare const markdownItComark: MarkdownItPluginWithOptions<SyntaxOptions>;
@@ -201,6 +201,12 @@ const markdownItComarkBlock = (md) => {
201
201
  const blockAttributesClosingFence = blockYamlLines[line] || '';
202
202
  if (!blockAttributesClosingFence)
203
203
  return false;
204
+ // The `---` fence is only valid on the line immediately after the component opener. Any other `---` is a thematic break.
205
+ if (line === '---') {
206
+ const parentOpenLine = state.env.comarkBlockTokens[0].map?.[0];
207
+ if (parentOpenLine === undefined || startLine !== parentOpenLine + 1)
208
+ return false;
209
+ }
204
210
  let lineEnd = startLine + 1;
205
211
  let found = false;
206
212
  while (lineEnd < endLine) {
@@ -237,8 +243,38 @@ const markdownItComarkBlock = (md) => {
237
243
  const line = state.src.slice(start, state.eMarks[startLine]);
238
244
  const { name, props } = parseBlockParams(line.slice(1));
239
245
  let lineEnd = startLine + 1;
246
+ let inCodeFence = false;
247
+ let codeFenceChar = '';
248
+ let codeFenceCount = 0;
240
249
  while (lineEnd < endLine) {
241
250
  const inner = state.src.slice(state.bMarks[lineEnd] + state.tShift[startLine], state.eMarks[lineEnd]);
251
+ if (inCodeFence) {
252
+ // Look for matching closing fence (same char, >= opening count, nothing but spaces after)
253
+ if (inner[0] === codeFenceChar) {
254
+ let fencePos = 1;
255
+ while (fencePos < inner.length && inner[fencePos] === codeFenceChar)
256
+ fencePos++;
257
+ if (fencePos >= codeFenceCount && inner.slice(fencePos).trim() === '') {
258
+ inCodeFence = false;
259
+ }
260
+ }
261
+ lineEnd += 1;
262
+ continue;
263
+ }
264
+ // Detect opening code fence (``` or ~~~, length >= 3)
265
+ if (inner[0] === '`' || inner[0] === '~') {
266
+ const ch = inner[0];
267
+ let fencePos = 1;
268
+ while (fencePos < inner.length && inner[fencePos] === ch)
269
+ fencePos++;
270
+ if (fencePos >= 3) {
271
+ inCodeFence = true;
272
+ codeFenceChar = ch;
273
+ codeFenceCount = fencePos;
274
+ lineEnd += 1;
275
+ continue;
276
+ }
277
+ }
242
278
  if (/^#\w+/.test(inner) || inner.startsWith('::'))
243
279
  break;
244
280
  lineEnd += 1;
@@ -397,52 +433,52 @@ const markdownItInlineProps = (md) => {
397
433
  const _parse = md.parse;
398
434
  md.parse = function (src, env) {
399
435
  const tokens = _parse.call(this, src, env);
400
- // When the props token is the only inline child of a heading/paragraph/list_item,
401
- // apply it to the parent block tag instead of producing a `span`
436
+ // When the trailing inline child is a props token directly after a text
437
+ // node, lift the props onto the surrounding heading/paragraph/list_item.
438
+ // (If the props follow a closing tag, they belong to that inline tag, not
439
+ // the parent — leave them alone.)
402
440
  tokens.forEach((token, index) => {
403
441
  const prev = tokens[index - 1];
404
442
  const next = tokens[index + 1];
405
443
  if (!prev || !['heading_open', 'paragraph_open', 'list_item_open'].includes(prev.type) || prev.hidden)
406
444
  return;
407
- // list item handling
445
+ // Tight-list paragraph: the inline lives one slot ahead
408
446
  if (token.hidden && next?.type === 'inline')
409
447
  token = next;
410
- if (token.type === 'inline' &&
411
- token.children?.length === 2 &&
412
- token.children[0].type === 'text' &&
413
- token.children[1].type === 'mdc_inline_props') {
414
- const props = token.children[1].attrs;
415
- token.children.splice(1, 1);
416
- props?.forEach(([key, value]) => {
417
- if (key === 'class')
418
- prev.attrJoin('class', value);
419
- else
420
- prev.attrSet(key, value);
421
- });
422
- }
423
- });
424
- // Deduplicate `ul` wrapping when `::ul` is used and contains exactly one bullet list
425
- tokens.forEach((tokenOpen, index) => {
426
- if (tokenOpen.type !== 'bullet_list_open')
448
+ if (token.type !== 'inline' || !token.children?.length)
427
449
  return;
428
- const prev = tokens[index - 1];
429
- if (!prev || prev.type !== 'mdc_block_open' || prev.tag !== 'ul')
450
+ const last = token.children[token.children.length - 1];
451
+ if (last.type !== 'mdc_inline_props')
430
452
  return;
431
- let closeIndex = index + 1;
432
- while (closeIndex < tokens.length) {
433
- const close = tokens[closeIndex];
434
- if (close.type === 'bullet_list_close' && close.level === tokenOpen.level)
435
- break;
436
- closeIndex += 1;
453
+ // Find the previous non-empty child. Markdown-it's emphasis tokenizer
454
+ // can leave an empty text token between the closing delimiter and the
455
+ // props skipping it lets us distinguish "props on the parent" from
456
+ // "props on the preceding inline tag".
457
+ let beforeIdx = token.children.length - 2;
458
+ while (beforeIdx >= 0) {
459
+ const child = token.children[beforeIdx];
460
+ if (child.type === 'text' && !child.content) {
461
+ beforeIdx--;
462
+ continue;
463
+ }
464
+ break;
437
465
  }
438
- const tokenClose = tokens[closeIndex];
439
- if (tokenClose?.type !== 'bullet_list_close')
466
+ const beforeProps = beforeIdx >= 0 ? token.children[beforeIdx] : undefined;
467
+ if (!beforeProps || beforeProps.type !== 'text')
440
468
  return;
441
- const next = tokens[closeIndex + 1];
442
- if (next?.type === 'mdc_block_close' && next.tag === 'ul') {
443
- tokenOpen.hidden = true;
444
- tokenClose.hidden = true;
469
+ // Strip the trailing space the text picked up before the `{...}` token.
470
+ if (typeof beforeProps.content === 'string') {
471
+ beforeProps.content = beforeProps.content.replace(/[ \t]+$/, '');
445
472
  }
473
+ const props = last.attrs;
474
+ // Drop the props token (last) plus any empty text tokens it left behind.
475
+ token.children.length = beforeProps.content ? beforeIdx + 1 : beforeIdx;
476
+ props?.forEach(([key, value]) => {
477
+ if (key === 'class')
478
+ prev.attrJoin('class', value);
479
+ else
480
+ prev.attrSet(key, value);
481
+ });
446
482
  });
447
483
  return tokens;
448
484
  };
@@ -4,5 +4,5 @@
4
4
  * This plugin runs before inline parsing to prevent Comark from interpreting
5
5
  * task list markers [X] and [ ] as Comark inline span syntax.
6
6
  */
7
- declare const _default: import("../types.ts").ComarkPluginFactory<unknown>;
7
+ declare const _default: import("../types.ts").ComarkPluginFactory<unknown, {}, {}>;
8
8
  export default _default;
@@ -12,5 +12,7 @@ export interface Toc {
12
12
  links: TocLink[];
13
13
  }
14
14
  export declare function generateFlatToc(body: ComarkTree, options: Toc): Toc;
15
- declare const _default: import("comark").ComarkPluginFactory<Partial<Toc>>;
15
+ declare const _default: import("comark").ComarkPluginFactory<Partial<Toc>, {
16
+ toc: Toc;
17
+ }, {}>;
16
18
  export default _default;
package/dist/types.d.ts CHANGED
@@ -1,6 +1,12 @@
1
1
  import type { DumpOptions } from 'js-yaml';
2
2
  import type MarkdownExit from 'markdown-exit';
3
3
  import type MarkdownIt from 'markdown-it';
4
+ /**
5
+ * The `[keyof T] extends [never]` form (rather than `keyof T extends never`)
6
+ * is the standard trick to prevent TS from distributing the check over a
7
+ * union — we want to test "is T's keyset empty?" as one yes/no question.
8
+ */
9
+ type Writable<T> = [keyof T] extends [never] ? Record<string, any> : T;
4
10
  /**
5
11
  * The Comark text
6
12
  * @param string - The text content
@@ -43,11 +49,14 @@ export type ComarkNode = ComarkElement | ComarkText | ComarkComment;
43
49
  * @param nodes - The nodes of the tree
44
50
  * @param frontmatter - The frontmatter data which is the data at the top of the file
45
51
  * @param meta - The meta data of tree, it can be used to store additional data for the tree
52
+ *
53
+ * The `TMeta` and `TFrontmatter` type parameters allow `parse` / `createParse`
54
+ * to surface plugin-contributed keys with narrow types (see `MergePluginMeta`).
46
55
  */
47
- export interface ComarkTree {
56
+ export interface ComarkTree<TMeta = Record<string, any>, TFrontmatter = Record<string, any>> {
48
57
  nodes: ComarkNode[];
49
- frontmatter: Record<string, any>;
50
- meta: Record<string, any>;
58
+ frontmatter: TFrontmatter;
59
+ meta: TMeta;
51
60
  }
52
61
  export interface ContextBase {
53
62
  /**
@@ -234,26 +243,60 @@ export type ComarkParsePreState = {
234
243
  options: ParseOptions;
235
244
  [key: string]: any;
236
245
  };
237
- export type ComarkParsePostState = {
246
+ export type ComarkParsePostState<TMeta = Record<string, any>, TFrontmatter = Record<string, any>> = {
238
247
  markdown: string;
239
- tree: ComarkTree;
248
+ tree: ComarkTree<TMeta, TFrontmatter>;
240
249
  options: ParseOptions;
241
250
  tokens: unknown[];
242
251
  [key: string]: any;
243
252
  };
244
- export type ComarkPlugin = {
253
+ /**
254
+ * A Comark plugin.
255
+ *
256
+ * `TMeta` / `TFrontmatter` are phantom type parameters that record what this
257
+ * plugin contributes to `tree.meta` / `tree.frontmatter`. They are surfaced
258
+ * only via the optional `__meta` / `__frontmatter` markers — implementations
259
+ * never set these at runtime; they exist purely so the contribution survives
260
+ * `ReturnType<typeof factory>` inference and can be merged in `createParse`.
261
+ */
262
+ export type ComarkPlugin<TMeta = {}, TFrontmatter = {}> = {
245
263
  name: string;
246
264
  markdownItPlugins?: MarkdownItPlugin[];
247
265
  pre?: (state: ComarkParsePreState) => Promise<void> | void;
248
- post?: (state: ComarkParsePostState) => Promise<void> | void;
266
+ post?: (state: ComarkParsePostState<Writable<TMeta>, Writable<TFrontmatter>>) => Promise<void> | void;
267
+ /** Phantom — used for type inference only. Never set at runtime. */
268
+ __meta?: TMeta;
269
+ /** Phantom — used for type inference only. Never set at runtime. */
270
+ __frontmatter?: TFrontmatter;
249
271
  };
250
- export type ComarkPluginFactory<Options> = (opts?: Options) => ComarkPlugin;
251
- export type ComponentManifest = (name: string) => Promise<unknown> | undefined | null;
272
+ export type ComarkPluginFactory<Options, TMeta = {}, TFrontmatter = {}> = (opts?: Options) => ComarkPlugin<TMeta, TFrontmatter>;
273
+ type PluginMetaOf<P> = P extends ComarkPlugin<infer M, any> ? M : {};
274
+ type PluginFrontmatterOf<P> = P extends ComarkPlugin<any, infer F> ? F : {};
275
+ /**
276
+ * Walk a tuple of plugins and intersect their meta contributions.
277
+ * Returns `{}` when the tuple is empty or when nothing was contributed.
278
+ */
279
+ export type MergePluginMeta<TPlugins extends readonly unknown[]> = TPlugins extends readonly [infer Head, ...infer Rest] ? PluginMetaOf<Head> & MergePluginMeta<Rest extends readonly unknown[] ? Rest : []> : {};
280
+ /**
281
+ * Walk a tuple of plugins and intersect their frontmatter contributions.
282
+ */
283
+ export type MergePluginFrontmatter<TPlugins extends readonly unknown[]> = TPlugins extends readonly [
284
+ infer Head,
285
+ ...infer Rest
286
+ ] ? PluginFrontmatterOf<Head> & MergePluginFrontmatter<Rest extends readonly unknown[] ? Rest : []> : {};
287
+ /**
288
+ * When no plugin contributed meta keys, fall back to the permissive
289
+ * `Record<string, any>` (backwards-compatible). Otherwise, preserve narrow
290
+ * keys and type unknown accesses as `unknown` (safer than `any`).
291
+ */
292
+ export type ResolvedMeta<T> = [keyof T] extends [never] ? Record<string, any> : T & Record<string, unknown>;
293
+ export type ResolvedFrontmatter<T> = [keyof T] extends [never] ? Record<string, any> : T & Record<string, unknown>;
294
+ export type ComponentManifest = (name: string) => unknown | Promise<unknown> | undefined | null;
252
295
  export interface ComarkContextProvider {
253
296
  components: Record<string, any>;
254
297
  componentManifest: ComponentManifest;
255
298
  }
256
- export interface ParseOptions {
299
+ export interface ParseOptions<TPlugins extends readonly ComarkPlugin<any, any>[] = readonly ComarkPlugin<any, any>[]> {
257
300
  /**
258
301
  * Whether to automatically unwrap single paragraphs in container components.
259
302
  * When enabled, if a container component (alert, card, callout, note, warning, tip, info)
@@ -300,7 +343,7 @@ export interface ParseOptions {
300
343
  * Additional plugins to use
301
344
  * @default []
302
345
  */
303
- plugins?: ComarkPlugin[];
346
+ plugins?: TPlugins;
304
347
  }
305
348
  /**
306
349
  * Type signature for the options object passed to the Comark parser function returned by createParse().
@@ -312,4 +355,5 @@ export type ComarkParseFnOptions = {
312
355
  * Type signature for the async Comark parser function returned by createParse().
313
356
  * Accepts a markdown string and optional parsing options, and returns a Promise of ComarkTree.
314
357
  */
315
- export type ComarkParseFn = (markdown: string, opts?: ComarkParseFnOptions) => Promise<ComarkTree>;
358
+ export type ComarkParseFn<TMeta = Record<string, any>, TFrontmatter = Record<string, any>> = (markdown: string, opts?: ComarkParseFnOptions) => Promise<ComarkTree<TMeta, TFrontmatter>>;
359
+ export {};
@@ -5,8 +5,20 @@ import type { ComarkPluginFactory } from '../types.ts';
5
5
  */
6
6
  export declare function createSerializedTask<TArgs extends unknown[], TResult>(fn: (...args: TArgs) => Promise<TResult>): (...args: TArgs) => Promise<TResult>;
7
7
  /**
8
- * Define a Comark plugin
9
- * @param fn - The plugin factory function
10
- * @returns The defined plugin
8
+ * Define a Comark plugin.
9
+ *
10
+ * `TMeta` and `TFrontmatter` declare what the plugin contributes to
11
+ * `tree.meta` / `tree.frontmatter`. They are inferred from the factory's
12
+ * return type when set via the `__meta` / `__frontmatter` phantom markers,
13
+ * or can be passed explicitly. Plugins that don't contribute typed keys can
14
+ * omit them entirely.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * defineComarkPlugin<Options, { toc: Toc }>((opts) => ({
19
+ * name: 'toc',
20
+ * post(state) { state.tree.meta.toc = ... },
21
+ * }))
22
+ * ```
11
23
  */
12
- export declare function defineComarkPlugin<Options>(fn: ComarkPluginFactory<Options>): ComarkPluginFactory<Options>;
24
+ export declare function defineComarkPlugin<Options, TMeta = {}, TFrontmatter = {}>(fn: ComarkPluginFactory<Options, TMeta, TFrontmatter>): ComarkPluginFactory<Options, TMeta, TFrontmatter>;