comark 0.2.0 → 0.3.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 (67) hide show
  1. package/README.md +12 -2
  2. package/dist/internal/frontmatter.d.ts +2 -1
  3. package/dist/internal/frontmatter.js +2 -2
  4. package/dist/internal/parse/auto-close/index.js +58 -23
  5. package/dist/internal/parse/token-processor.js +18 -3
  6. package/dist/internal/stringify/attributes.d.ts +37 -1
  7. package/dist/internal/stringify/attributes.js +96 -12
  8. package/dist/internal/stringify/handlers/a.js +3 -0
  9. package/dist/internal/stringify/handlers/code.js +1 -1
  10. package/dist/internal/stringify/handlers/del.js +1 -1
  11. package/dist/internal/stringify/handlers/hr.d.ts +1 -1
  12. package/dist/internal/stringify/handlers/hr.js +4 -1
  13. package/dist/internal/stringify/handlers/html.js +13 -2
  14. package/dist/internal/stringify/handlers/img.js +1 -1
  15. package/dist/internal/stringify/handlers/li.js +14 -1
  16. package/dist/internal/stringify/handlers/math.js +1 -1
  17. package/dist/internal/stringify/handlers/mdc.js +3 -2
  18. package/dist/internal/stringify/handlers/ol.js +2 -2
  19. package/dist/internal/stringify/handlers/pre.js +1 -1
  20. package/dist/internal/stringify/handlers/template.js +1 -1
  21. package/dist/internal/stringify/handlers/ul.js +2 -2
  22. package/dist/internal/stringify/indent.d.ts +2 -1
  23. package/dist/internal/stringify/indent.js +3 -2
  24. package/dist/internal/stringify/state.d.ts +3 -3
  25. package/dist/internal/stringify/state.js +71 -15
  26. package/dist/internal/yaml.d.ts +2 -1
  27. package/dist/internal/yaml.js +3 -1
  28. package/dist/parse.js +13 -2
  29. package/dist/plugins/alert.js +1 -1
  30. package/dist/plugins/binding.d.ts +20 -0
  31. package/dist/plugins/binding.js +61 -0
  32. package/dist/plugins/breaks.d.ts +2 -0
  33. package/dist/plugins/breaks.js +34 -0
  34. package/dist/plugins/footnotes.d.ts +61 -0
  35. package/dist/plugins/footnotes.js +187 -0
  36. package/dist/plugins/highlight.d.ts +2 -2
  37. package/dist/plugins/highlight.js +7 -5
  38. package/dist/plugins/json-render.d.ts +53 -0
  39. package/dist/plugins/json-render.js +99 -0
  40. package/dist/plugins/punctuation.d.ts +67 -0
  41. package/dist/plugins/punctuation.js +236 -0
  42. package/dist/plugins/security.js +1 -1
  43. package/dist/render.d.ts +2 -1
  44. package/dist/render.js +3 -1
  45. package/dist/types.d.ts +71 -16
  46. package/dist/utils/index.d.ts +9 -0
  47. package/dist/utils/index.js +24 -0
  48. package/dist/vite.d.ts +1 -0
  49. package/dist/vite.js +1 -0
  50. package/package.json +29 -23
  51. package/skills/comark/AGENTS.md +261 -0
  52. package/skills/comark/SKILL.md +489 -0
  53. package/skills/comark/references/markdown-syntax.md +599 -0
  54. package/skills/comark/references/parsing-ast.md +378 -0
  55. package/skills/comark/references/rendering-react.md +445 -0
  56. package/skills/comark/references/rendering-svelte.md +453 -0
  57. package/skills/comark/references/rendering-vue.md +462 -0
  58. package/skills/skills/comark/AGENTS.md +261 -0
  59. package/skills/skills/comark/SKILL.md +489 -0
  60. package/skills/skills/comark/references/markdown-syntax.md +599 -0
  61. package/skills/skills/comark/references/parsing-ast.md +378 -0
  62. package/skills/skills/comark/references/rendering-react.md +445 -0
  63. package/skills/skills/comark/references/rendering-svelte.md +453 -0
  64. package/skills/skills/comark/references/rendering-vue.md +462 -0
  65. package/skills/skills/migrate-mdc-to-comark/SKILL.md +191 -0
  66. package/dist/utils/serialized-task.d.ts +0 -1
  67. package/dist/utils/serialized-task.js +0 -1
@@ -1,5 +1,18 @@
1
- import { handlers } from "./handlers/index.js";
1
+ import { handlers as defaultHandlers } from "./handlers/index.js";
2
2
  import { pascalCase } from "../../utils/index.js";
3
+ import { resolveAttributes } from "./attributes.js";
4
+ function findHandler(ctx, node) {
5
+ const userHandler = ctx.handlers[node[0]] || ctx.handlers[pascalCase(node[0])];
6
+ if (typeof userHandler === 'function') {
7
+ return userHandler;
8
+ }
9
+ for (const handler of ctx.conditionalHandlers) {
10
+ if (handler?.match(node)) {
11
+ return handler.handler;
12
+ }
13
+ }
14
+ return userHandler;
15
+ }
3
16
  /**
4
17
  * Render a single node
5
18
  * @param node - The node to render
@@ -17,20 +30,39 @@ export async function one(node, state, parent) {
17
30
  if (node[0] === null) {
18
31
  return await state.handlers.comment(node, state);
19
32
  }
20
- const userHandler = state.context.handlers[node[0]] || state.context.handlers[pascalCase(node[0])];
21
- if (userHandler) {
22
- return await userHandler(node, state, parent);
33
+ // Scope `renderData.props` to the current element's resolved attributes so
34
+ // nested bindings like `:prop="props.x"` resolve against the enclosing
35
+ // element's values, regardless of which handler (html / ansi / user) runs.
36
+ // Elements with no attributes (e.g. an auto-generated `<p>` wrapper) must
37
+ // NOT shadow the parent's scope, otherwise `{{ props.* }}` inside them would
38
+ // resolve to nothing.
39
+ const prevRenderData = state.renderData;
40
+ if (state.renderData && node[1]) {
41
+ const resolved = resolveAttributes(node[1], prevRenderData);
42
+ if (Object.keys(resolved).length > 0) {
43
+ state.renderData = { ...prevRenderData, props: resolved };
44
+ }
23
45
  }
24
- if (state.context.html || node[1].$?.html === 1) {
25
- return await state.handlers.html(node, state, parent);
46
+ try {
47
+ const userHandler = findHandler(state.context, node);
48
+ if (userHandler) {
49
+ return await userHandler(node, state, parent);
50
+ }
51
+ if (state.context.html || node[1].$?.html === 1) {
52
+ return await state.handlers.html(node, state, parent);
53
+ }
54
+ // fallback to default handlers
55
+ const nodeHandler = state.handlers[node[0]];
56
+ if (nodeHandler) {
57
+ return await nodeHandler(node, state, parent);
58
+ }
59
+ return state.context.format === 'markdown/comark'
60
+ ? await state.handlers.mdc(node, state, parent)
61
+ : await state.handlers.html(node, state, parent);
26
62
  }
27
- const nodeHandler = state.handlers[node[0]];
28
- if (nodeHandler) {
29
- return await nodeHandler(node, state, parent);
63
+ finally {
64
+ state.renderData = prevRenderData;
30
65
  }
31
- return state.context.format === 'markdown/comark'
32
- ? await state.handlers.mdc(node, state, parent)
33
- : await state.handlers.html(node, state, parent);
34
66
  }
35
67
  export async function flow(node, state, parent) {
36
68
  const children = node.slice(2);
@@ -41,20 +73,40 @@ export async function flow(node, state, parent) {
41
73
  return result;
42
74
  }
43
75
  export function createState(ctx = {}) {
76
+ const conditionalHandlers = [];
77
+ const handlers = {};
78
+ for (const [key, value] of Object.entries(ctx.handlers || {})) {
79
+ if (typeof value === 'function') {
80
+ handlers[key] = value;
81
+ }
82
+ else {
83
+ conditionalHandlers.push(value);
84
+ }
85
+ }
44
86
  const context = {
45
87
  ...ctx,
46
88
  blockSeparator: ctx.blockSeparator || '\n\n',
47
89
  format: ctx.format || 'markdown/comark',
48
- handlers: ctx.handlers || {}, // user defined node handlers
90
+ handlers, // user defined node handlers
91
+ conditionalHandlers,
92
+ blockAttributesStyle: ctx.blockAttributesStyle || 'codeblock',
49
93
  // Enable html mode for text/html format
50
94
  html: ctx.format === 'text/html',
51
95
  };
96
+ const tree = ctx.tree;
97
+ const renderData = {
98
+ frontmatter: (tree?.frontmatter || {}),
99
+ meta: (tree?.meta || {}),
100
+ data: (ctx.data || {}),
101
+ props: {},
102
+ };
52
103
  const state = {
53
- handlers,
104
+ handlers: defaultHandlers,
54
105
  context,
55
106
  one,
56
107
  flow,
57
108
  data: ctx.data || {},
109
+ renderData,
58
110
  render: async (input) => {
59
111
  if (Array.isArray(input) && typeof input[0] === 'string' && input.length > 1) {
60
112
  return state.one(input, state);
@@ -77,12 +129,16 @@ export function createState(ctx = {}) {
77
129
  return state;
78
130
  }
79
131
  export const state = {
80
- handlers,
132
+ handlers: defaultHandlers,
133
+ conditionalHandlers: [],
81
134
  data: {},
135
+ renderData: { frontmatter: {}, meta: {}, data: {}, props: {} },
82
136
  context: {
83
137
  blockSeparator: '\n\n',
84
138
  format: 'markdown/comark',
85
139
  handlers: {}, // user defined node handlers
140
+ conditionalHandlers: [], // user defined conditional handlers
141
+ blockAttributesStyle: 'codeblock',
86
142
  },
87
143
  flow,
88
144
  one,
@@ -1,3 +1,4 @@
1
+ import { type DumpOptions } from 'js-yaml';
1
2
  /**
2
3
  * Parse YAML content
3
4
  * @param content - The content to parse
@@ -9,4 +10,4 @@ export declare function parseYaml(content: string): Record<string, unknown>;
9
10
  * @param data - The data to stringify
10
11
  * @returns The stringified data
11
12
  */
12
- export declare function stringifyYaml(data: Record<string, unknown>): string;
13
+ export declare function stringifyYaml(data: Record<string, unknown>, options?: DumpOptions): string;
@@ -12,9 +12,11 @@ export function parseYaml(content) {
12
12
  * @param data - The data to stringify
13
13
  * @returns The stringified data
14
14
  */
15
- export function stringifyYaml(data) {
15
+ export function stringifyYaml(data, options) {
16
16
  const yamlOutput = dump(data, {
17
17
  indent: 2,
18
+ lineWidth: -1,
19
+ ...options,
18
20
  replacer: (_key, value) => {
19
21
  if (value === 'true')
20
22
  return true;
package/dist/parse.js CHANGED
@@ -91,8 +91,19 @@ export function createParse(options = {}) {
91
91
  for (const plugin of options.plugins || []) {
92
92
  await plugin.pre?.(state);
93
93
  }
94
- const { content, data } = await parseFrontmatter(state.markdown);
95
- state.tokens = parser.parse(content, {});
94
+ const { content, data } = parseFrontmatter(state.markdown);
95
+ try {
96
+ state.tokens = parser.parse(content, {});
97
+ }
98
+ catch (e) {
99
+ // in case of streaming, return the previous output if parsing fails
100
+ // This is to avoid resetting the tree to an empty state on failure
101
+ // resetting the tree will re-redner whole tree
102
+ if (opts.streaming && prevOutput) {
103
+ return prevOutput;
104
+ }
105
+ throw e;
106
+ }
96
107
  // Convert tokens to Comark structure
97
108
  let nodes = marmdownItTokensToComarkTree(state.tokens, {
98
109
  startLine: state.parsedLines,
@@ -1,4 +1,4 @@
1
- import { visit } from 'comark/utils';
1
+ import { visit } from "../utils/index.js";
2
2
  import { defineComarkPlugin } from "../utils/helpers.js";
3
3
  const markers = {
4
4
  '!TIP': {
@@ -0,0 +1,20 @@
1
+ import type { NodeHandler } from '../types';
2
+ export interface MdcInlineBindingOptions {
3
+ /**
4
+ * The tag name used to render a binding.
5
+ *
6
+ * @default 'binding'
7
+ */
8
+ tag?: string;
9
+ }
10
+ declare const _default: import("../types").ComarkPluginFactory<MdcInlineBindingOptions>;
11
+ export default _default;
12
+ /**
13
+ * Markdown-format handler that renders a `binding` node back to the
14
+ * `{{ path || default }}` source form.
15
+ *
16
+ * Wire it via `renderMarkdown(tree, { components: { Binding } })`
17
+ * to round-trip faithfully to the authored shorthand instead of the generic
18
+ * `:binding{:value="..."}` component form.
19
+ */
20
+ export declare const Binding: NodeHandler;
@@ -0,0 +1,61 @@
1
+ import { defineComarkPlugin } from "../utils/helpers.js";
2
+ const markdownItInlineBinding = (md, options = {}) => {
3
+ const tag = options.tag || 'binding';
4
+ md.inline.ruler.after('entity', 'mdc_inline_binding', (state, silent) => {
5
+ const start = state.pos;
6
+ if (state.src[start] !== '{' || state.src[start + 1] !== '{')
7
+ return false;
8
+ // Find the closing `}}`
9
+ let end = start + 2;
10
+ while (end < state.posMax - 1) {
11
+ if (state.src[end] === '}' && state.src[end + 1] === '}')
12
+ break;
13
+ end += 1;
14
+ }
15
+ if (end >= state.posMax - 1)
16
+ return false;
17
+ const inner = state.src.slice(start + 2, end).trim();
18
+ if (!inner)
19
+ return false;
20
+ // Split on the first `||` to separate value and default
21
+ const separator = inner.indexOf('||');
22
+ const value = separator === -1 ? inner : inner.slice(0, separator).trim();
23
+ const defaultValue = separator === -1 ? '' : inner.slice(separator + 2).trim();
24
+ if (!value)
25
+ return false;
26
+ state.pos = end + 2;
27
+ if (silent)
28
+ return true;
29
+ const token = state.push('mdc_inline_component', tag, 0);
30
+ token.attrSet(':value', value);
31
+ if (defaultValue)
32
+ token.attrSet('defaultValue', defaultValue);
33
+ return true;
34
+ });
35
+ };
36
+ export default defineComarkPlugin((opts = {}) => {
37
+ return {
38
+ name: 'binding',
39
+ markdownItPlugins: [
40
+ ((md) => markdownItInlineBinding(md, opts)),
41
+ ],
42
+ };
43
+ });
44
+ /**
45
+ * Markdown-format handler that renders a `binding` node back to the
46
+ * `{{ path || default }}` source form.
47
+ *
48
+ * Wire it via `renderMarkdown(tree, { components: { Binding } })`
49
+ * to round-trip faithfully to the authored shorthand instead of the generic
50
+ * `:binding{:value="..."}` component form.
51
+ */
52
+ export const Binding = (node) => {
53
+ const attrs = (node[1] || {});
54
+ const path = attrs[':value'];
55
+ if (typeof path !== 'string' || !path)
56
+ return '';
57
+ const defaultValue = attrs.defaultValue;
58
+ return typeof defaultValue === 'string' && defaultValue.length > 0
59
+ ? `{{ ${path} || ${defaultValue} }}`
60
+ : `{{ ${path} }}`;
61
+ };
@@ -0,0 +1,2 @@
1
+ declare const _default: import("../types.ts").ComarkPluginFactory<unknown>;
2
+ export default _default;
@@ -0,0 +1,34 @@
1
+ import { defineComarkPlugin } from "../utils/helpers.js";
2
+ import { visit } from "../utils/index.js";
3
+ export default defineComarkPlugin(() => ({
4
+ name: 'breaks',
5
+ post(state) {
6
+ visit(state.tree, node => Array.isArray(node) && node.length > 2, (node) => {
7
+ const parent = node;
8
+ const newParent = [parent[0], parent[1]];
9
+ let hasModified = false;
10
+ for (let i = 2; i < parent.length; i++) {
11
+ const child = parent[i];
12
+ if (typeof child === 'string' && child.includes('\n')) {
13
+ hasModified = true;
14
+ const lines = child.split('\n');
15
+ lines.forEach((line, index) => {
16
+ if (line.length > 0) {
17
+ newParent.push(line);
18
+ }
19
+ if (index < lines.length - 1) {
20
+ newParent.push(['br', {}]);
21
+ }
22
+ });
23
+ }
24
+ else {
25
+ newParent.push(child);
26
+ }
27
+ }
28
+ if (hasModified) {
29
+ parent.length = 0;
30
+ parent.push(...newParent);
31
+ }
32
+ });
33
+ },
34
+ }));
@@ -0,0 +1,61 @@
1
+ import type { ConditionalNodeHandler } from 'comark';
2
+ export interface FootnotesConfig {
3
+ /**
4
+ * The label for the footnotes section
5
+ * @default 'Footnotes'
6
+ */
7
+ label?: string;
8
+ /**
9
+ * Whether to add a horizontal rule before the footnotes section
10
+ * @default true
11
+ */
12
+ hr?: boolean;
13
+ /**
14
+ * Back-reference symbol
15
+ * @default '↩'
16
+ */
17
+ backRef?: string;
18
+ }
19
+ /**
20
+ * Create footnotes plugin for comark
21
+ *
22
+ * This plugin adds support for footnote references `[^label]` and
23
+ * footnote definitions `[^label]: content`. Footnotes are collected
24
+ * and rendered as a numbered list at the end of the document.
25
+ *
26
+ * @param config Footnotes configuration
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * import { parse } from 'comark'
31
+ * import footnotes from 'comark/plugins/footnotes'
32
+ *
33
+ * const result = await parse('Hello[^1]\n\n[^1]: World', {
34
+ * plugins: [footnotes()]
35
+ * })
36
+ * ```
37
+ */
38
+ declare const _default: import("comark").ComarkPluginFactory<FootnotesConfig>;
39
+ export default _default;
40
+ /**
41
+ * Conditional stringify handler for footnotes.
42
+ *
43
+ * Converts footnote AST nodes back to standard markdown footnote syntax
44
+ * (`[^key]` for references, `[^key]: content` for definitions).
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * import { parse } from 'comark'
49
+ * import { renderMarkdown } from 'comark/render'
50
+ * import footnotes, { Footnote } from 'comark/plugins/footnotes'
51
+ *
52
+ * const tree = await parse('Hello[^1]\n\n[^1]: World', {
53
+ * plugins: [footnotes()]
54
+ * })
55
+ *
56
+ * const md = await renderMarkdown(tree, {
57
+ * components: { footnotes: Footnote },
58
+ * })
59
+ * ```
60
+ */
61
+ export declare const Footnote: ConditionalNodeHandler;
@@ -0,0 +1,187 @@
1
+ import { defineComarkPlugin } from "../utils/helpers.js";
2
+ import { visit } from "../utils/index.js";
3
+ // Regex to match footnote definitions at the start of a line:
4
+ // [^label]: content
5
+ const FOOTNOTE_DEF_RE = /^\[\^([^\s\]]+)\]:[ \t]?(.*)$/gm;
6
+ /**
7
+ * Quick structural check: is this a ['span', {…}, string] tuple?
8
+ * Used as the visit() checker to avoid running the full extraction
9
+ * on every node in the tree.
10
+ */
11
+ function maybeFootnoteRef(node) {
12
+ return Array.isArray(node)
13
+ && node[0] === 'span'
14
+ && node.length === 3
15
+ && typeof node[2] === 'string';
16
+ }
17
+ /**
18
+ * Check if a node is a footnote reference: ['span', {}, '^label']
19
+ * The MDC parser converts [^label] into ['span', {}, '^label']
20
+ * Returns the label string or null.
21
+ */
22
+ function isFootnoteRef(node) {
23
+ // Caller should pre-check with maybeFootnoteRef for fast rejection
24
+ const attrs = node[1];
25
+ // Check attrs has no keys other than '$' — avoid Object.keys() allocation
26
+ for (const k in attrs) {
27
+ if (k !== '$')
28
+ return null;
29
+ }
30
+ const child = node[2];
31
+ // Must start with '^' and have at least one label char
32
+ if (child.charCodeAt(0) !== 0x5E /* ^ */ || child.length < 2)
33
+ return null;
34
+ // Check for whitespace using charCode scanning (avoid regex)
35
+ for (let i = 1; i < child.length; i++) {
36
+ const c = child.charCodeAt(i);
37
+ if (c === 0x20 || c === 0x09 || c === 0x0A || c === 0x0D)
38
+ return null;
39
+ }
40
+ return child.slice(1);
41
+ }
42
+ /**
43
+ * Create footnotes plugin for comark
44
+ *
45
+ * This plugin adds support for footnote references `[^label]` and
46
+ * footnote definitions `[^label]: content`. Footnotes are collected
47
+ * and rendered as a numbered list at the end of the document.
48
+ *
49
+ * @param config Footnotes configuration
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * import { parse } from 'comark'
54
+ * import footnotes from 'comark/plugins/footnotes'
55
+ *
56
+ * const result = await parse('Hello[^1]\n\n[^1]: World', {
57
+ * plugins: [footnotes()]
58
+ * })
59
+ * ```
60
+ */
61
+ export default defineComarkPlugin((config = {}) => {
62
+ const { label = 'Footnotes', hr = true, backRef = '↩', } = config;
63
+ return {
64
+ name: 'footnotes',
65
+ // extract footnote definitions from markdown before MDC parsing
66
+ pre(state) {
67
+ const definitions = new Map();
68
+ // Extract and remove footnote definitions from the source
69
+ state.markdown = state.markdown.replace(FOOTNOTE_DEF_RE, (_match, defLabel, content) => {
70
+ definitions.set(defLabel, content.trim());
71
+ return ''; // Remove the definition line
72
+ });
73
+ // Store on state to avoid mixing definitions across parallel parses
74
+ state.footnote = definitions;
75
+ },
76
+ // replace [^ref] spans and build footnotes section
77
+ post(state) {
78
+ const definitions = state.footnote;
79
+ if (!definitions || definitions.size === 0)
80
+ return;
81
+ const refIndexMap = new Map();
82
+ // Replace footnote reference spans with sup > a elements
83
+ visit(state.tree, maybeFootnoteRef, (node) => {
84
+ const refLabel = isFootnoteRef(node);
85
+ if (!refLabel || !definitions.has(refLabel))
86
+ return;
87
+ if (!refIndexMap.has(refLabel)) {
88
+ refIndexMap.set(refLabel, refIndexMap.size + 1);
89
+ }
90
+ const refIndex = refIndexMap.get(refLabel);
91
+ return ['sup', { class: 'footnote-ref' },
92
+ ['a', {
93
+ href: `#fn-${refLabel}`,
94
+ id: `fnref-${refLabel}`,
95
+ }, `[${refIndex}]`],
96
+ ];
97
+ });
98
+ let nodes = state.tree.nodes;
99
+ // Remove empty paragraphs left after definition removal
100
+ nodes = nodes.filter((node) => {
101
+ if (!Array.isArray(node) || node[0] !== 'p')
102
+ return true;
103
+ // A paragraph with only whitespace children is considered empty
104
+ for (let i = 2; i < node.length; i++) {
105
+ const child = node[i];
106
+ if (typeof child === 'string') {
107
+ if (child.trim().length > 0)
108
+ return true;
109
+ }
110
+ else if (Array.isArray(child) && child[0] != null) {
111
+ return true;
112
+ }
113
+ }
114
+ return false;
115
+ });
116
+ // Build the footnotes section
117
+ if (refIndexMap.size === 0) {
118
+ state.tree.nodes = nodes;
119
+ return;
120
+ }
121
+ const footnoteItems = [];
122
+ for (const [refLabel] of refIndexMap) {
123
+ const content = definitions.get(refLabel);
124
+ footnoteItems.push(['li', { id: `fn-${refLabel}` },
125
+ content, ' ',
126
+ ['a', { href: `#fnref-${refLabel}`, class: 'footnote-backref' }, backRef],
127
+ ]);
128
+ }
129
+ const sectionChildren = [];
130
+ if (hr) {
131
+ sectionChildren.push(['hr', {}]);
132
+ }
133
+ if (label) {
134
+ sectionChildren.push(['h2', { id: 'footnotes' }, label]);
135
+ }
136
+ sectionChildren.push(['ol', { class: 'footnotes-list' }, ...footnoteItems]);
137
+ nodes.push(['section', { class: 'footnotes' }, ...sectionChildren]);
138
+ state.tree.nodes = nodes;
139
+ },
140
+ };
141
+ });
142
+ /**
143
+ * Conditional stringify handler for footnotes.
144
+ *
145
+ * Converts footnote AST nodes back to standard markdown footnote syntax
146
+ * (`[^key]` for references, `[^key]: content` for definitions).
147
+ *
148
+ * @example
149
+ * ```ts
150
+ * import { parse } from 'comark'
151
+ * import { renderMarkdown } from 'comark/render'
152
+ * import footnotes, { Footnote } from 'comark/plugins/footnotes'
153
+ *
154
+ * const tree = await parse('Hello[^1]\n\n[^1]: World', {
155
+ * plugins: [footnotes()]
156
+ * })
157
+ *
158
+ * const md = await renderMarkdown(tree, {
159
+ * components: { footnotes: Footnote },
160
+ * })
161
+ * ```
162
+ */
163
+ export const Footnote = {
164
+ match: (node) => {
165
+ return node[1].class === 'footnotes' || node[1].class === 'footnote-ref';
166
+ },
167
+ handler: (node) => {
168
+ if (node[1].class === 'footnotes') {
169
+ const ol = node.find(n => Array.isArray(n) && n[0] === 'ol');
170
+ if (!ol)
171
+ return '';
172
+ let result = '';
173
+ for (let i = 2; i < ol.length; i++) {
174
+ const key = String(ol[i][1]?.id)?.replace('fn-', '');
175
+ const value = ol[i][2];
176
+ result += `[^${key}]: ${value}\n`;
177
+ }
178
+ return result;
179
+ }
180
+ if (node[1].class === 'footnote-ref') {
181
+ const link = node[2];
182
+ const key = String(link[1]?.id)?.replace('fnref-', '');
183
+ return `[^${key}]`;
184
+ }
185
+ return '';
186
+ },
187
+ };
@@ -1,4 +1,4 @@
1
- import type { LanguageRegistration, ShikiTransformer, ShikiInternal, ThemeRegistration } from 'shiki';
1
+ import type { LanguageRegistration, ShikiTransformer, ShikiPrimitive, ThemeRegistration } from 'shiki';
2
2
  import type { ComarkNode, ComarkTree } from 'comark';
3
3
  export interface HighlightOptions {
4
4
  /**
@@ -45,7 +45,7 @@ export interface CodeBlockAttributes {
45
45
  * Get or create the Shiki highlighter instance
46
46
  * Uses a singleton pattern to avoid creating multiple highlighters
47
47
  */
48
- export declare function getHighlighter(options?: HighlightOptions): Promise<ShikiInternal>;
48
+ export declare function getHighlighter(options?: HighlightOptions): Promise<ShikiPrimitive>;
49
49
  /**
50
50
  * Highlight code using Shiki with codeToTokens
51
51
  * Returns comark nodes built from hast
@@ -1,5 +1,5 @@
1
1
  import { defineComarkPlugin } from "../utils/helpers.js";
2
- import { createShikiInternal } from 'shiki';
2
+ import { createShikiPrimitive } from 'shiki';
3
3
  import { createJavaScriptRegexEngine } from 'shiki/engine/javascript';
4
4
  import { codeToHast } from 'shiki/core';
5
5
  let highlighter = null;
@@ -24,13 +24,15 @@ export async function getHighlighter(options = {}) {
24
24
  try {
25
25
  highlighterPromise = (async () => {
26
26
  const { themes, languages } = await registerDefaults(options);
27
- const hl = await createShikiInternal({
27
+ const hl = createShikiPrimitive({
28
28
  themes: themes,
29
29
  langs: languages,
30
30
  langAlias: {
31
- md: 'mdc',
32
- markdown: 'mdc',
33
- comark: 'mdc',
31
+ 'md': 'mdc',
32
+ 'markdown': 'mdc',
33
+ 'comark': 'mdc',
34
+ 'json-render': 'json',
35
+ 'yaml-render': 'yaml',
34
36
  },
35
37
  engine: createJavaScriptRegexEngine({ forgiving: true }),
36
38
  });
@@ -0,0 +1,53 @@
1
+ interface JsonRenderConfig {
2
+ }
3
+ /**
4
+ * Plugin for rendering [JSON Render](https://json-render.dev/) specs as UI components.
5
+ *
6
+ * Transforms `json-render` fenced code blocks into Comark AST nodes at parse time.
7
+ * Supports both full specs (with `root` and `elements`) and single-element shorthand.
8
+ *
9
+ * @param config - Plugin configuration options
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import { parse } from 'comark'
14
+ * import jsonRender from 'comark/plugins/json-render'
15
+ *
16
+ * const result = await parse(`
17
+ * \`\`\`json-render
18
+ * {
19
+ * "root": "card",
20
+ * "elements": {
21
+ * "card": {
22
+ * "type": "Card",
23
+ * "props": { "title": "Hello" },
24
+ * "children": ["text"]
25
+ * },
26
+ * "text": {
27
+ * "type": "Text",
28
+ * "props": { "content": "World" }
29
+ * }
30
+ * }
31
+ * }
32
+ * \`\`\`
33
+ * `, {
34
+ * plugins: [jsonRender()]
35
+ * })
36
+ * ```
37
+ *
38
+ * @example
39
+ * ```vue
40
+ * <script setup>
41
+ * import { Comark } from '@comark/vue'
42
+ * import jsonRender from '@comark/vue/plugins/json-render'
43
+ * </script>
44
+ *
45
+ * <template>
46
+ * <Suspense>
47
+ * <Comark :plugins="[jsonRender()]">{{ content }}</Comark>
48
+ * </Suspense>
49
+ * </template>
50
+ * ```
51
+ */
52
+ declare const _default: import("../types").ComarkPluginFactory<JsonRenderConfig>;
53
+ export default _default;