comark 0.3.2 → 0.5.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 (78) hide show
  1. package/README.md +25 -1
  2. package/dist/context.d.ts +78 -0
  3. package/dist/context.js +127 -0
  4. package/dist/devtools/bridge.d.ts +1 -0
  5. package/dist/devtools/bridge.js +1 -0
  6. package/dist/devtools/constants.d.ts +1 -0
  7. package/dist/devtools/constants.js +1 -0
  8. package/dist/devtools/renderer/dom.d.ts +1 -0
  9. package/dist/devtools/renderer/dom.js +1 -0
  10. package/dist/devtools/renderer/index.d.ts +2 -0
  11. package/dist/devtools/renderer/index.js +2 -0
  12. package/dist/devtools/renderer/output.d.ts +1 -0
  13. package/dist/devtools/renderer/output.js +1 -0
  14. package/dist/devtools/renderer/panel.d.ts +1 -0
  15. package/dist/devtools/renderer/panel.js +1 -0
  16. package/dist/devtools/renderer/styles.d.ts +1 -0
  17. package/dist/devtools/renderer/styles.js +1 -0
  18. package/dist/devtools/renderer/theme.d.ts +1 -0
  19. package/dist/devtools/renderer/theme.js +1 -0
  20. package/dist/devtools/types.d.ts +1 -0
  21. package/dist/devtools/types.js +1 -0
  22. package/dist/index.d.ts +2 -0
  23. package/dist/index.js +2 -0
  24. package/dist/internal/parse/auto-close/index.js +96 -31
  25. package/dist/internal/parse/auto-unwrap.js +5 -1
  26. package/dist/internal/parse/html/html_block_rule.js +9 -15
  27. package/dist/internal/parse/html/index.d.ts +1 -0
  28. package/dist/internal/parse/html/index.js +1 -1
  29. package/dist/internal/parse/token-processor.js +70 -32
  30. package/dist/internal/stringify/attributes.d.ts +8 -1
  31. package/dist/internal/stringify/attributes.js +53 -0
  32. package/dist/internal/stringify/handlers/blockquote.js +17 -0
  33. package/dist/internal/stringify/handlers/heading.js +6 -1
  34. package/dist/internal/stringify/handlers/html.js +8 -2
  35. package/dist/internal/stringify/handlers/li.js +19 -9
  36. package/dist/internal/stringify/handlers/mdc.js +1 -1
  37. package/dist/internal/stringify/handlers/ol.js +15 -2
  38. package/dist/internal/stringify/handlers/p.js +4 -0
  39. package/dist/internal/stringify/handlers/pre.js +11 -2
  40. package/dist/internal/stringify/handlers/table.js +7 -0
  41. package/dist/internal/stringify/handlers/template.js +4 -1
  42. package/dist/internal/stringify/handlers/ul.js +11 -1
  43. package/dist/internal/stringify/state.js +13 -1
  44. package/dist/parse.d.ts +4 -4
  45. package/dist/parse.js +7 -3
  46. package/dist/plugins/alert.d.ts +1 -1
  47. package/dist/plugins/binding.d.ts +1 -1
  48. package/dist/plugins/breaks.d.ts +1 -1
  49. package/dist/plugins/emoji.d.ts +1 -1
  50. package/dist/plugins/footnotes.d.ts +1 -1
  51. package/dist/plugins/headings.d.ts +19 -8
  52. package/dist/plugins/headings.js +25 -15
  53. package/dist/plugins/highlight.d.ts +1 -1
  54. package/dist/plugins/highlight.js +4 -2
  55. package/dist/plugins/json-render.d.ts +1 -1
  56. package/dist/plugins/math.d.ts +1 -1
  57. package/dist/plugins/mermaid.d.ts +1 -1
  58. package/dist/plugins/punctuation.d.ts +1 -1
  59. package/dist/plugins/security.d.ts +12 -1
  60. package/dist/plugins/security.js +13 -6
  61. package/dist/plugins/summary.d.ts +4 -1
  62. package/dist/plugins/syntax.d.ts +1 -1
  63. package/dist/plugins/syntax.js +95 -36
  64. package/dist/plugins/task-list.d.ts +1 -1
  65. package/dist/plugins/toc.d.ts +3 -1
  66. package/dist/render.d.ts +6 -2
  67. package/dist/render.js +2 -2
  68. package/dist/types.d.ts +61 -12
  69. package/dist/utils/helpers.d.ts +16 -4
  70. package/dist/utils/helpers.js +15 -3
  71. package/dist/utils/index.d.ts +3 -1
  72. package/dist/utils/index.js +30 -14
  73. package/package.json +6 -3
  74. package/skills/comark/references/rendering-svelte.md +51 -7
  75. package/dist/internal/stringify/indent.d.ts +0 -1
  76. package/dist/internal/stringify/indent.js +0 -1
  77. package/dist/vite.d.ts +0 -1
  78. package/dist/vite.js +0 -1
package/dist/parse.js CHANGED
@@ -44,13 +44,16 @@ 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());
51
54
  const parser = new MarkdownExit({
52
55
  html: false,
53
- linkify: true,
56
+ linkify: options.linkify ?? true,
54
57
  }).enable(['table', 'strikethrough']);
55
58
  if (options.html !== false) {
56
59
  parser.inline.ruler.before('text', 'comark_html_inline', html_inline);
@@ -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,10 +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,
284
- tabindex: '0',
286
+ class: userClass ? `${classStr} . ${userClass}` : classStr,
285
287
  };
286
288
  if (options.preStyles) {
287
289
  const lightTheme = options.themes?.light;
@@ -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;
@@ -1,3 +1,4 @@
1
+ import type { ComarkElement, ComarkNode } from 'comark';
1
2
  import type { PropsValidationOptions } from '../internal/props-validation.ts';
2
3
  interface SecurityOptions extends PropsValidationOptions {
3
4
  /**
@@ -5,6 +6,16 @@ interface SecurityOptions extends PropsValidationOptions {
5
6
  * @default []
6
7
  */
7
8
  blockedTags?: string[];
9
+ /**
10
+ * Tags to allow only in the output tree.
11
+ * @default []
12
+ */
13
+ allowedTags?: string[];
14
+ /**
15
+ * Behavior when encountering an unallowed or blocked tag.
16
+ * @default undefined
17
+ */
18
+ tagFallback?: (element: ComarkElement) => false | ComarkNode | Promise<false | ComarkNode>;
8
19
  }
9
- declare const _default: import("comark").ComarkPluginFactory<SecurityOptions>;
20
+ declare const _default: import("comark").ComarkPluginFactory<SecurityOptions, {}, {}>;
10
21
  export default _default;
@@ -1,9 +1,10 @@
1
1
  import { defineComarkPlugin } from "../utils/helpers.js";
2
- import { visit } from "../utils/index.js";
2
+ import { visitAsync } from "../utils/index.js";
3
3
  import { validateProps } from "../internal/props-validation.js";
4
4
  export default defineComarkPlugin((options = {}) => {
5
- const { blockedTags = [], allowedLinkPrefixes, allowedImagePrefixes, allowedProtocols, defaultOrigin, allowDataImages, } = options;
5
+ const { blockedTags = [], allowedTags = [], tagFallback = undefined, allowedLinkPrefixes, allowedImagePrefixes, allowedProtocols, defaultOrigin, allowDataImages, } = options;
6
6
  const dropSet = new Set(blockedTags.map((t) => t.toLowerCase()));
7
+ const allowSet = new Set(allowedTags.map((t) => t.toLowerCase()));
7
8
  const propsOptions = {
8
9
  allowedLinkPrefixes,
9
10
  allowedImagePrefixes,
@@ -13,11 +14,17 @@ export default defineComarkPlugin((options = {}) => {
13
14
  };
14
15
  return {
15
16
  name: 'security',
16
- post(state) {
17
- visit(state.tree, (node) => typeof node !== 'string' && node[0] !== null, (node) => {
17
+ async post(state) {
18
+ await visitAsync(state.tree, (node) => typeof node !== 'string' && node[0] !== null, async (node) => {
18
19
  const element = node;
19
- // return false to remove the node from the tree
20
- if (dropSet.has(element[0].toLowerCase())) {
20
+ const tagName = element[0].toLowerCase();
21
+ const isBlocked = dropSet.has(tagName);
22
+ const isNotAllowed = allowSet.size > 0 && !allowSet.has(tagName);
23
+ if (isNotAllowed || isBlocked) {
24
+ if (typeof tagFallback === 'function') {
25
+ return await tagFallback(element);
26
+ }
27
+ // return false to remove the node from the tree
21
28
  return false;
22
29
  }
23
30
  const keys = Object.keys(element[1]);
@@ -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>;
@@ -4,6 +4,22 @@ import { parseBracketContent } from "../internal/parse/syntax/brackets.js";
4
4
  import { searchProps } from "../internal/parse/syntax/props.js";
5
5
  import { parseBlockParams } from "../internal/parse/syntax/block-params.js";
6
6
  import { parseYaml } from "../internal/yaml.js";
7
+ /**
8
+ * A component name must start with a letter or `$`, followed by word chars,
9
+ * `$` or `-`. Mirrors the block name grammar (`RE_BLOCK_NAME = /^[a-z$]/i`).
10
+ */
11
+ const RE_COMPONENT_NAME = /^[a-z$][\w$-]*/i;
12
+ /**
13
+ * Whether `name` begins with a syntactically valid component name.
14
+ *
15
+ * This prevents sequences such as `:8100` or `::30` from being treated as
16
+ * components — a purely numeric name is not a valid component and would
17
+ * otherwise produce invalid output like `createElement('8100')` (inline) or
18
+ * throw `Invalid block params` (block).
19
+ */
20
+ function isValidComponentName(name) {
21
+ return RE_COMPONENT_NAME.test(name);
22
+ }
7
23
  // #region Block component plugin (`::name` and `::name ... ::`)
8
24
  const blockYamlLines = {
9
25
  '---': '---',
@@ -18,7 +34,7 @@ const markdownItComarkBlock = (md) => {
18
34
  const marker_char = marker_str.charCodeAt(0);
19
35
  md.block.ruler.before('fence', 'comark_block_shorthand', function comark_block_shorthand(state, startLine, _endLine, silent) {
20
36
  const line = state.src.slice(state.bMarks[startLine] + state.tShift[startLine], state.eMarks[startLine]);
21
- if (!/^:\w/.test(line))
37
+ if (line[0] !== ':' || !isValidComponentName(line.slice(1)))
22
38
  return false;
23
39
  const { name, content, props, remaining } = parseBlockParams(line.slice(1));
24
40
  // If there's unparsed remaining content, treat it as inline component in a paragraph
@@ -78,6 +94,11 @@ const markdownItComarkBlock = (md) => {
78
94
  if (marker_count < min_markers)
79
95
  return false;
80
96
  const markup = state.src.slice(start, pos);
97
+ // Bail out (plain text) on an invalid name instead of letting
98
+ // parseBlockParams throw on e.g. `::8100`.
99
+ const nameStart = state.skipSpaces(pos);
100
+ if (nameStart < max && !isValidComponentName(state.src.slice(nameStart, max)))
101
+ return false;
81
102
  const params = parseBlockParams(state.src.slice(pos, max));
82
103
  if (!params.name)
83
104
  return false;
@@ -201,6 +222,12 @@ const markdownItComarkBlock = (md) => {
201
222
  const blockAttributesClosingFence = blockYamlLines[line] || '';
202
223
  if (!blockAttributesClosingFence)
203
224
  return false;
225
+ // The `---` fence is only valid on the line immediately after the component opener. Any other `---` is a thematic break.
226
+ if (line === '---') {
227
+ const parentOpenLine = state.env.comarkBlockTokens[0].map?.[0];
228
+ if (parentOpenLine === undefined || startLine !== parentOpenLine + 1)
229
+ return false;
230
+ }
204
231
  let lineEnd = startLine + 1;
205
232
  let found = false;
206
233
  while (lineEnd < endLine) {
@@ -237,8 +264,38 @@ const markdownItComarkBlock = (md) => {
237
264
  const line = state.src.slice(start, state.eMarks[startLine]);
238
265
  const { name, props } = parseBlockParams(line.slice(1));
239
266
  let lineEnd = startLine + 1;
267
+ let inCodeFence = false;
268
+ let codeFenceChar = '';
269
+ let codeFenceCount = 0;
240
270
  while (lineEnd < endLine) {
241
271
  const inner = state.src.slice(state.bMarks[lineEnd] + state.tShift[startLine], state.eMarks[lineEnd]);
272
+ if (inCodeFence) {
273
+ // Look for matching closing fence (same char, >= opening count, nothing but spaces after)
274
+ if (inner[0] === codeFenceChar) {
275
+ let fencePos = 1;
276
+ while (fencePos < inner.length && inner[fencePos] === codeFenceChar)
277
+ fencePos++;
278
+ if (fencePos >= codeFenceCount && inner.slice(fencePos).trim() === '') {
279
+ inCodeFence = false;
280
+ }
281
+ }
282
+ lineEnd += 1;
283
+ continue;
284
+ }
285
+ // Detect opening code fence (``` or ~~~, length >= 3)
286
+ if (inner[0] === '`' || inner[0] === '~') {
287
+ const ch = inner[0];
288
+ let fencePos = 1;
289
+ while (fencePos < inner.length && inner[fencePos] === ch)
290
+ fencePos++;
291
+ if (fencePos >= 3) {
292
+ inCodeFence = true;
293
+ codeFenceChar = ch;
294
+ codeFenceCount = fencePos;
295
+ lineEnd += 1;
296
+ continue;
297
+ }
298
+ }
242
299
  if (/^#\w+/.test(inner) || inner.startsWith('::'))
243
300
  break;
244
301
  lineEnd += 1;
@@ -347,10 +404,12 @@ const markdownItInlineComponent = (md) => {
347
404
  // Empty name
348
405
  if (nameEnd <= start + 1)
349
406
  return false;
407
+ const name = state.src.slice(start + 1, nameEnd);
408
+ if (!isValidComponentName(name))
409
+ return false;
350
410
  state.pos = index;
351
411
  if (silent)
352
412
  return true;
353
- const name = state.src.slice(start + 1, nameEnd);
354
413
  if (contentStart !== -1) {
355
414
  state.push('mdc_inline_component', name, 1);
356
415
  const oldPos = state.pos;
@@ -397,52 +456,52 @@ const markdownItInlineProps = (md) => {
397
456
  const _parse = md.parse;
398
457
  md.parse = function (src, env) {
399
458
  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`
459
+ // When the trailing inline child is a props token directly after a text
460
+ // node, lift the props onto the surrounding heading/paragraph/list_item.
461
+ // (If the props follow a closing tag, they belong to that inline tag, not
462
+ // the parent — leave them alone.)
402
463
  tokens.forEach((token, index) => {
403
464
  const prev = tokens[index - 1];
404
465
  const next = tokens[index + 1];
405
466
  if (!prev || !['heading_open', 'paragraph_open', 'list_item_open'].includes(prev.type) || prev.hidden)
406
467
  return;
407
- // list item handling
468
+ // Tight-list paragraph: the inline lives one slot ahead
408
469
  if (token.hidden && next?.type === 'inline')
409
470
  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')
471
+ if (token.type !== 'inline' || !token.children?.length)
427
472
  return;
428
- const prev = tokens[index - 1];
429
- if (!prev || prev.type !== 'mdc_block_open' || prev.tag !== 'ul')
473
+ const last = token.children[token.children.length - 1];
474
+ if (last.type !== 'mdc_inline_props')
430
475
  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;
476
+ // Find the previous non-empty child. Markdown-it's emphasis tokenizer
477
+ // can leave an empty text token between the closing delimiter and the
478
+ // props skipping it lets us distinguish "props on the parent" from
479
+ // "props on the preceding inline tag".
480
+ let beforeIdx = token.children.length - 2;
481
+ while (beforeIdx >= 0) {
482
+ const child = token.children[beforeIdx];
483
+ if (child.type === 'text' && !child.content) {
484
+ beforeIdx--;
485
+ continue;
486
+ }
487
+ break;
437
488
  }
438
- const tokenClose = tokens[closeIndex];
439
- if (tokenClose?.type !== 'bullet_list_close')
489
+ const beforeProps = beforeIdx >= 0 ? token.children[beforeIdx] : undefined;
490
+ if (!beforeProps || beforeProps.type !== 'text')
440
491
  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;
492
+ // Strip the trailing space the text picked up before the `{...}` token.
493
+ if (typeof beforeProps.content === 'string') {
494
+ beforeProps.content = beforeProps.content.replace(/[ \t]+$/, '');
445
495
  }
496
+ const props = last.attrs;
497
+ // Drop the props token (last) plus any empty text tokens it left behind.
498
+ token.children.length = beforeProps.content ? beforeIdx + 1 : beforeIdx;
499
+ props?.forEach(([key, value]) => {
500
+ if (key === 'class')
501
+ prev.attrJoin('class', value);
502
+ else
503
+ prev.attrSet(key, value);
504
+ });
446
505
  });
447
506
  return tokens;
448
507
  };
@@ -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/render.d.ts CHANGED
@@ -8,7 +8,9 @@ export { resolveAttributes, resolveAttribute } from './internal/stringify/attrib
8
8
  * @param context - The context of the renderer
9
9
  * @returns The string representation of the Comark tree
10
10
  */
11
- export declare function render(tree: ComarkTree, context?: RenderOptions): Promise<string>;
11
+ export declare function render(tree: ComarkTree | {
12
+ nodes: ComarkTree['nodes'];
13
+ }, context?: RenderOptions): Promise<string>;
12
14
  /**
13
15
  * Render Comark tree to markdown
14
16
  *
@@ -16,4 +18,6 @@ export declare function render(tree: ComarkTree, context?: RenderOptions): Promi
16
18
  * @param options - Optional rendering options
17
19
  * @returns The markdown string with optional frontmatter
18
20
  */
19
- export declare function renderMarkdown(tree: ComarkTree, options?: RenderMarkdownOptions): Promise<string>;
21
+ export declare function renderMarkdown(tree: ComarkTree | {
22
+ nodes: ComarkTree['nodes'];
23
+ }, options?: RenderMarkdownOptions): Promise<string>;
package/dist/render.js CHANGED
@@ -11,7 +11,7 @@ export { resolveAttributes, resolveAttribute } from "./internal/stringify/attrib
11
11
  * @returns The string representation of the Comark tree
12
12
  */
13
13
  export async function render(tree, context = {}) {
14
- const state = createState({ ...context, tree, handlers: context.components });
14
+ const state = createState({ ...context, tree: tree, handlers: context.components });
15
15
  let result = '';
16
16
  for (const child of tree.nodes) {
17
17
  result += await one(child, state);
@@ -27,5 +27,5 @@ export async function render(tree, context = {}) {
27
27
  */
28
28
  export async function renderMarkdown(tree, options) {
29
29
  const content = await render(tree, { format: 'markdown/comark', ...options });
30
- return renderFrontmatter(tree.frontmatter, content, options?.frontmatterOptions);
30
+ return renderFrontmatter(tree.frontmatter || {}, content, options?.frontmatterOptions);
31
31
  }