comark 0.2.1 → 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.
- package/README.md +12 -2
- package/dist/internal/frontmatter.d.ts +2 -1
- package/dist/internal/frontmatter.js +2 -2
- package/dist/internal/parse/auto-close/index.js +58 -23
- package/dist/internal/parse/token-processor.js +18 -3
- package/dist/internal/stringify/attributes.d.ts +37 -1
- package/dist/internal/stringify/attributes.js +96 -12
- package/dist/internal/stringify/handlers/a.js +3 -0
- package/dist/internal/stringify/handlers/code.js +1 -1
- package/dist/internal/stringify/handlers/del.js +1 -1
- package/dist/internal/stringify/handlers/html.js +12 -1
- package/dist/internal/stringify/handlers/img.js +1 -1
- package/dist/internal/stringify/handlers/li.js +14 -1
- package/dist/internal/stringify/handlers/math.js +1 -1
- package/dist/internal/stringify/handlers/mdc.js +1 -1
- package/dist/internal/stringify/handlers/ol.js +2 -2
- package/dist/internal/stringify/handlers/pre.js +1 -1
- package/dist/internal/stringify/handlers/template.js +1 -1
- package/dist/internal/stringify/handlers/ul.js +2 -2
- package/dist/internal/stringify/indent.d.ts +2 -1
- package/dist/internal/stringify/indent.js +3 -2
- package/dist/internal/stringify/state.d.ts +3 -3
- package/dist/internal/stringify/state.js +71 -15
- package/dist/internal/yaml.d.ts +2 -1
- package/dist/internal/yaml.js +3 -1
- package/dist/parse.js +13 -2
- package/dist/plugins/alert.js +1 -1
- package/dist/plugins/binding.d.ts +20 -0
- package/dist/plugins/binding.js +61 -0
- package/dist/plugins/breaks.d.ts +2 -0
- package/dist/plugins/breaks.js +34 -0
- package/dist/plugins/footnotes.d.ts +61 -0
- package/dist/plugins/footnotes.js +187 -0
- package/dist/plugins/highlight.js +6 -4
- package/dist/plugins/json-render.d.ts +1 -1
- package/dist/plugins/json-render.js +3 -3
- package/dist/plugins/punctuation.d.ts +67 -0
- package/dist/plugins/punctuation.js +236 -0
- package/dist/plugins/security.js +1 -1
- package/dist/render.d.ts +2 -1
- package/dist/render.js +3 -1
- package/dist/types.d.ts +71 -16
- package/dist/utils/index.d.ts +9 -0
- package/dist/utils/index.js +24 -0
- package/dist/vite.d.ts +1 -0
- package/dist/vite.js +1 -0
- package/package.json +29 -24
- package/skills/comark/AGENTS.md +261 -0
- package/skills/comark/SKILL.md +489 -0
- package/skills/comark/references/markdown-syntax.md +599 -0
- package/skills/comark/references/parsing-ast.md +378 -0
- package/skills/comark/references/rendering-react.md +445 -0
- package/skills/comark/references/rendering-svelte.md +453 -0
- package/skills/comark/references/rendering-vue.md +462 -0
- package/skills/skills/comark/AGENTS.md +261 -0
- package/skills/skills/comark/SKILL.md +489 -0
- package/skills/skills/comark/references/markdown-syntax.md +599 -0
- package/skills/skills/comark/references/parsing-ast.md +378 -0
- package/skills/skills/comark/references/rendering-react.md +445 -0
- package/skills/skills/comark/references/rendering-svelte.md +453 -0
- package/skills/skills/comark/references/rendering-vue.md +462 -0
- package/skills/skills/migrate-mdc-to-comark/SKILL.md +191 -0
- package/dist/utils/serialized-task.d.ts +0 -1
- 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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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
|
-
|
|
28
|
-
|
|
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
|
|
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,
|
package/dist/internal/yaml.d.ts
CHANGED
|
@@ -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
|
|
13
|
+
export declare function stringifyYaml(data: Record<string, unknown>, options?: DumpOptions): string;
|
package/dist/internal/yaml.js
CHANGED
|
@@ -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 } =
|
|
95
|
-
|
|
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,
|
package/dist/plugins/alert.js
CHANGED
|
@@ -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,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
|
+
};
|
|
@@ -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 =
|
|
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
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { defineComarkPlugin } from
|
|
2
|
-
import { textContent, visit } from
|
|
3
|
-
import { parseYaml } from
|
|
1
|
+
import { defineComarkPlugin } from "../parse.js";
|
|
2
|
+
import { textContent, visit } from "../utils/index.js";
|
|
3
|
+
import { parseYaml } from "../internal/yaml.js";
|
|
4
4
|
function jsonRenderToAst(jrt) {
|
|
5
5
|
if (!jrt.root) {
|
|
6
6
|
jrt = {
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export interface PunctuationOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Convert straight quotes to smart (curly) quotes
|
|
4
|
+
* @default true
|
|
5
|
+
*/
|
|
6
|
+
quotes?: boolean | string | [string, string, string, string];
|
|
7
|
+
/**
|
|
8
|
+
* Convert -- to en-dash and --- to em-dash
|
|
9
|
+
* @default true
|
|
10
|
+
*/
|
|
11
|
+
dashes?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Convert ... to ellipsis character and normalize trailing dots after ? and !
|
|
14
|
+
* @default true
|
|
15
|
+
*/
|
|
16
|
+
ellipsis?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Convert (c), (r), (tm), +- to ©, ®, ™, ±
|
|
19
|
+
* @default true
|
|
20
|
+
*/
|
|
21
|
+
symbols?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Normalize repeated punctuation: ???? → ???, !!!! → !!!, ,, → ,
|
|
24
|
+
* @default true
|
|
25
|
+
*/
|
|
26
|
+
normalize?: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Punctuation plugin for comark
|
|
30
|
+
*
|
|
31
|
+
* Transforms common punctuation patterns into their typographically correct Unicode characters:
|
|
32
|
+
* - Smart (curly) quotes: "text" → \u201Ctext\u201D, 'text' → \u2018text\u2019
|
|
33
|
+
* - Dashes: -- → \u2013 (en-dash), --- → \u2014 (em-dash)
|
|
34
|
+
* - Ellipsis: ... → \u2026, ?.... → ?.., !.... → !..
|
|
35
|
+
* - Symbols: (c) → \u00A9, (r) → \u00AE, (tm) → \u2122, +- → \u00B1
|
|
36
|
+
* - Normalize: ???? → ???, !!!! → !!!, ,, → ,
|
|
37
|
+
*
|
|
38
|
+
* Does not transform text inside code, pre, math, kbd, script, or style elements.
|
|
39
|
+
*
|
|
40
|
+
* Supports locale-aware quote characters via the `quotes` option:
|
|
41
|
+
* - String of 4 characters: `'«»„"'` (open double, close double, open single, close single)
|
|
42
|
+
* - Array of 4 strings: `['«\xA0', '\xA0»', '‹\xA0', '\xA0›']` for French (with nbsp)
|
|
43
|
+
*
|
|
44
|
+
* @param options Punctuation configuration
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* import { parse } from 'comark'
|
|
49
|
+
* import punctuation from 'comark/plugins/punctuation'
|
|
50
|
+
*
|
|
51
|
+
* const result = await parse('"Hello" -- world...', {
|
|
52
|
+
* plugins: [punctuation()]
|
|
53
|
+
* })
|
|
54
|
+
*
|
|
55
|
+
* // Locale-aware quotes (Russian)
|
|
56
|
+
* const result2 = await parse('"Hello"', {
|
|
57
|
+
* plugins: [punctuation({ quotes: '«»„"' })]
|
|
58
|
+
* })
|
|
59
|
+
*
|
|
60
|
+
* // French quotes with non-breaking spaces
|
|
61
|
+
* const result3 = await parse('"Hello"', {
|
|
62
|
+
* plugins: [punctuation({ quotes: ['«\xA0', '\xA0»', '‹\xA0', '\xA0›'] })]
|
|
63
|
+
* })
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
declare const _default: import("comark").ComarkPluginFactory<PunctuationOptions>;
|
|
67
|
+
export default _default;
|