comark 0.0.1 → 0.1.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 +104 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +6 -0
- package/dist/internal/frontmatter.d.ts +16 -0
- package/dist/internal/frontmatter.js +43 -0
- package/dist/internal/parse/auto-close/index.d.ts +12 -0
- package/dist/internal/parse/auto-close/index.js +457 -0
- package/dist/internal/parse/auto-close/table.d.ts +4 -0
- package/dist/internal/parse/auto-close/table.js +161 -0
- package/dist/internal/parse/auto-unwrap.d.ts +20 -0
- package/dist/internal/parse/auto-unwrap.js +42 -0
- package/dist/internal/parse/html/html_block_rule.d.ts +2 -0
- package/dist/internal/parse/html/html_block_rule.js +60 -0
- package/dist/internal/parse/html/html_blocks.d.ts +2 -0
- package/dist/internal/parse/html/html_blocks.js +66 -0
- package/dist/internal/parse/html/html_inline_rule.d.ts +2 -0
- package/dist/internal/parse/html/html_inline_rule.js +43 -0
- package/dist/internal/parse/html/html_re.d.ts +3 -0
- package/dist/internal/parse/html/html_re.js +18 -0
- package/dist/internal/parse/html/index.d.ts +18 -0
- package/dist/internal/parse/html/index.js +122 -0
- package/dist/internal/parse/incremental.d.ts +12 -0
- package/dist/internal/parse/incremental.js +39 -0
- package/dist/internal/parse/token-processor.d.ts +9 -0
- package/dist/internal/parse/token-processor.js +803 -0
- package/dist/internal/props-validation.d.ts +12 -0
- package/dist/internal/props-validation.js +112 -0
- package/dist/internal/stringify/attributes.d.ts +21 -0
- package/dist/internal/stringify/attributes.js +67 -0
- package/dist/internal/stringify/handlers/a.d.ts +3 -0
- package/dist/internal/stringify/handlers/a.js +11 -0
- package/dist/internal/stringify/handlers/blockquote.d.ts +3 -0
- package/dist/internal/stringify/handlers/blockquote.js +18 -0
- package/dist/internal/stringify/handlers/br.d.ts +3 -0
- package/dist/internal/stringify/handlers/br.js +3 -0
- package/dist/internal/stringify/handlers/code.d.ts +3 -0
- package/dist/internal/stringify/handlers/code.js +11 -0
- package/dist/internal/stringify/handlers/comment.d.ts +3 -0
- package/dist/internal/stringify/handlers/comment.js +6 -0
- package/dist/internal/stringify/handlers/del.d.ts +3 -0
- package/dist/internal/stringify/handlers/del.js +4 -0
- package/dist/internal/stringify/handlers/emphesis.d.ts +3 -0
- package/dist/internal/stringify/handlers/emphesis.js +13 -0
- package/dist/internal/stringify/handlers/heading.d.ts +3 -0
- package/dist/internal/stringify/handlers/heading.js +7 -0
- package/dist/internal/stringify/handlers/hr.d.ts +3 -0
- package/dist/internal/stringify/handlers/hr.js +3 -0
- package/dist/internal/stringify/handlers/html.d.ts +3 -0
- package/dist/internal/stringify/handlers/html.js +73 -0
- package/dist/internal/stringify/handlers/img.d.ts +3 -0
- package/dist/internal/stringify/handlers/img.js +9 -0
- package/dist/internal/stringify/handlers/index.d.ts +2 -0
- package/dist/internal/stringify/handlers/index.js +56 -0
- package/dist/internal/stringify/handlers/li.d.ts +3 -0
- package/dist/internal/stringify/handlers/li.js +43 -0
- package/dist/internal/stringify/handlers/math.d.ts +3 -0
- package/dist/internal/stringify/handlers/math.js +8 -0
- package/dist/internal/stringify/handlers/mdc.d.ts +3 -0
- package/dist/internal/stringify/handlers/mdc.js +47 -0
- package/dist/internal/stringify/handlers/mermaid.d.ts +3 -0
- package/dist/internal/stringify/handlers/mermaid.js +8 -0
- package/dist/internal/stringify/handlers/ol.d.ts +3 -0
- package/dist/internal/stringify/handlers/ol.js +18 -0
- package/dist/internal/stringify/handlers/p.d.ts +3 -0
- package/dist/internal/stringify/handlers/p.js +8 -0
- package/dist/internal/stringify/handlers/pre.d.ts +3 -0
- package/dist/internal/stringify/handlers/pre.js +60 -0
- package/dist/internal/stringify/handlers/strong.d.ts +3 -0
- package/dist/internal/stringify/handlers/strong.js +13 -0
- package/dist/internal/stringify/handlers/table.d.ts +8 -0
- package/dist/internal/stringify/handlers/table.js +180 -0
- package/dist/internal/stringify/handlers/template.d.ts +3 -0
- package/dist/internal/stringify/handlers/template.js +14 -0
- package/dist/internal/stringify/handlers/ul.d.ts +3 -0
- package/dist/internal/stringify/handlers/ul.js +18 -0
- package/dist/internal/stringify/indent.d.ts +4 -0
- package/dist/internal/stringify/indent.js +8 -0
- package/dist/internal/stringify/state.d.ts +13 -0
- package/dist/internal/stringify/state.js +121 -0
- package/dist/internal/yaml.d.ts +12 -0
- package/dist/internal/yaml.js +51 -0
- package/dist/parse.d.ts +66 -0
- package/dist/parse.js +163 -0
- package/dist/plugins/alert.d.ts +2 -0
- package/dist/plugins/alert.js +66 -0
- package/dist/plugins/emoji.d.ts +3 -0
- package/dist/plugins/emoji.js +438 -0
- package/dist/plugins/headings.d.ts +48 -0
- package/dist/plugins/headings.js +85 -0
- package/dist/plugins/highlight.d.ts +63 -0
- package/dist/plugins/highlight.js +235 -0
- package/dist/plugins/math.d.ts +59 -0
- package/dist/plugins/math.js +263 -0
- package/dist/plugins/mermaid.d.ts +38 -0
- package/dist/plugins/mermaid.js +185 -0
- package/dist/plugins/security.d.ts +11 -0
- package/dist/plugins/security.js +32 -0
- package/dist/plugins/summary.d.ts +2 -0
- package/dist/plugins/summary.js +22 -0
- package/dist/plugins/task-list.d.ts +8 -0
- package/dist/plugins/task-list.js +117 -0
- package/dist/plugins/toc.d.ts +15 -0
- package/dist/plugins/toc.js +118 -0
- package/dist/render.d.ts +18 -0
- package/dist/render.js +29 -0
- package/dist/types.d.ts +258 -0
- package/dist/types.js +1 -0
- package/dist/utils/caret.d.ts +7 -0
- package/dist/utils/caret.js +36 -0
- package/dist/utils/index.d.ts +38 -0
- package/dist/utils/index.js +149 -0
- package/package.json +73 -9
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import katex from 'katex';
|
|
2
|
+
/**
|
|
3
|
+
* Render LaTeX math expression to HTML using KaTeX
|
|
4
|
+
*
|
|
5
|
+
* @param code LaTeX math expression
|
|
6
|
+
* @param displayMode Whether to render in display mode
|
|
7
|
+
* @param config KaTeX configuration
|
|
8
|
+
* @returns HTML string
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* const html = renderMath('E = mc^2', false)
|
|
13
|
+
* const display = renderMath('x^2', true)
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export function renderMath(code, displayMode, config = {}) {
|
|
17
|
+
try {
|
|
18
|
+
const options = {
|
|
19
|
+
displayMode,
|
|
20
|
+
throwOnError: config.throwOnError ?? false,
|
|
21
|
+
...config.options,
|
|
22
|
+
};
|
|
23
|
+
return katex.renderToString(code, options);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
console.error('Math rendering error:', error);
|
|
27
|
+
if (config.throwOnError) {
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
return `<span class="math-error">${error instanceof Error ? error.message : 'Failed to render math'}</span>`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Check if code is valid LaTeX math syntax
|
|
35
|
+
*
|
|
36
|
+
* @param code LaTeX math expression
|
|
37
|
+
* @returns true if valid, false otherwise
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* validateMath('E = mc^2') // true
|
|
42
|
+
* validateMath('\\invalid') // false
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export function validateMath(code) {
|
|
46
|
+
try {
|
|
47
|
+
katex.renderToString(code, { throwOnError: true });
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* markdown-it plugin for inline display math ($$...$$)
|
|
56
|
+
* This handles $$...$$ within a paragraph (same line)
|
|
57
|
+
*/
|
|
58
|
+
function mathInlineDisplayRule(state, silent, _config) {
|
|
59
|
+
const start = state.pos;
|
|
60
|
+
const max = state.posMax;
|
|
61
|
+
// Check if we start with $$
|
|
62
|
+
if (start + 1 >= max)
|
|
63
|
+
return false;
|
|
64
|
+
if (state.src.charCodeAt(start) !== 0x24 /* $ */)
|
|
65
|
+
return false;
|
|
66
|
+
if (state.src.charCodeAt(start + 1) !== 0x24 /* $ */)
|
|
67
|
+
return false;
|
|
68
|
+
// Find closing $$
|
|
69
|
+
let pos = start + 2;
|
|
70
|
+
while (pos + 1 < max) {
|
|
71
|
+
// Stop at newline
|
|
72
|
+
if (state.src.charCodeAt(pos) === 0x0A /* \n */) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
// Check for $$
|
|
76
|
+
if (state.src.charCodeAt(pos) === 0x24 && state.src.charCodeAt(pos + 1) === 0x24) {
|
|
77
|
+
// Found closing $$
|
|
78
|
+
const content = state.src.slice(start + 2, pos);
|
|
79
|
+
if (!silent) {
|
|
80
|
+
const token = state.push('math_inline', 'math', 0);
|
|
81
|
+
token.content = content;
|
|
82
|
+
token.markup = '$$';
|
|
83
|
+
token.meta = { display: true }; // Mark as display mode
|
|
84
|
+
}
|
|
85
|
+
state.pos = pos + 2;
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
pos++;
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* markdown-it plugin for inline math ($...$)
|
|
94
|
+
*/
|
|
95
|
+
function mathInlineRule(state, silent, _config) {
|
|
96
|
+
const start = state.pos;
|
|
97
|
+
const max = state.posMax;
|
|
98
|
+
// Check if we start with $
|
|
99
|
+
if (state.src.charCodeAt(start) !== 0x24 /* $ */) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
// Don't match $$ (that's display math)
|
|
103
|
+
if (start + 1 < max && state.src.charCodeAt(start + 1) === 0x24) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
// Find closing $
|
|
107
|
+
let pos = start + 1;
|
|
108
|
+
let foundClose = false;
|
|
109
|
+
while (pos < max) {
|
|
110
|
+
const char = state.src.charCodeAt(pos);
|
|
111
|
+
// Stop at newline - $ must close on same line
|
|
112
|
+
if (char === 0x0A /* \n */) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
if (char === 0x24 /* $ */) {
|
|
116
|
+
// Found potential closing $
|
|
117
|
+
// Make sure it's not escaped, we have content (at least 1 char),
|
|
118
|
+
// it's not preceded by another $ (which would make it $$),
|
|
119
|
+
// and it's not followed by another $ (which would make it $$)
|
|
120
|
+
const hasContent = pos > start + 1;
|
|
121
|
+
const notEscaped = pos === start + 1 || state.src.charCodeAt(pos - 1) !== 0x5C; /* \ */
|
|
122
|
+
const notPrecededByDollar = pos === start + 1 || state.src.charCodeAt(pos - 1) !== 0x24;
|
|
123
|
+
const notFollowedByDollar = pos + 1 >= max || state.src.charCodeAt(pos + 1) !== 0x24;
|
|
124
|
+
if (hasContent && notEscaped && notPrecededByDollar && notFollowedByDollar) {
|
|
125
|
+
foundClose = true;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
pos++;
|
|
130
|
+
}
|
|
131
|
+
if (!foundClose) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
// Extract math content
|
|
135
|
+
const content = state.src.slice(start + 1, pos);
|
|
136
|
+
// Don't match if content looks like a price or number (starts with digits and contains "or" or ends at end of sentence)
|
|
137
|
+
if (/^\d/.test(content)) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
if (!silent) {
|
|
141
|
+
const token = state.push('math_inline', 'math', 0);
|
|
142
|
+
token.content = content;
|
|
143
|
+
token.markup = '$';
|
|
144
|
+
}
|
|
145
|
+
state.pos = pos + 1;
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* markdown-it plugin for display math ($$...$$)
|
|
150
|
+
*/
|
|
151
|
+
function mathBlockRule(state, startLine, endLine, silent) {
|
|
152
|
+
const start = state.bMarks[startLine] + state.tShift[startLine];
|
|
153
|
+
const max = state.eMarks[startLine];
|
|
154
|
+
// Check if line starts with $$
|
|
155
|
+
if (start + 2 > max)
|
|
156
|
+
return false;
|
|
157
|
+
if (state.src.charCodeAt(start) !== 0x24 /* $ */)
|
|
158
|
+
return false;
|
|
159
|
+
if (state.src.charCodeAt(start + 1) !== 0x24 /* $ */)
|
|
160
|
+
return false;
|
|
161
|
+
const marker = state.src.slice(start, start + 2);
|
|
162
|
+
const pos = start + 2;
|
|
163
|
+
// Check if it's inline $$ (closing on same line)
|
|
164
|
+
const firstLine = state.src.slice(pos, max);
|
|
165
|
+
const closePos = firstLine.indexOf('$$');
|
|
166
|
+
if (closePos !== -1) {
|
|
167
|
+
// Inline display math on single line
|
|
168
|
+
if (silent)
|
|
169
|
+
return true;
|
|
170
|
+
const content = firstLine.slice(0, closePos);
|
|
171
|
+
const token = state.push('math_block', 'div', 0);
|
|
172
|
+
token.content = content;
|
|
173
|
+
token.markup = marker;
|
|
174
|
+
token.block = true;
|
|
175
|
+
token.map = [startLine, startLine + 1];
|
|
176
|
+
state.line = startLine + 1;
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
// Multi-line display math
|
|
180
|
+
let nextLine = startLine;
|
|
181
|
+
let autoClose = false;
|
|
182
|
+
// Search for closing $$
|
|
183
|
+
while (nextLine < endLine) {
|
|
184
|
+
nextLine++;
|
|
185
|
+
if (nextLine >= endLine)
|
|
186
|
+
break;
|
|
187
|
+
const lineStart = state.bMarks[nextLine] + state.tShift[nextLine];
|
|
188
|
+
const lineMax = state.eMarks[nextLine];
|
|
189
|
+
if (lineStart < lineMax && state.sCount[nextLine] < state.blkIndent) {
|
|
190
|
+
// Non-empty line with less indent - might close the block
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
const lineText = state.src.slice(lineStart, lineMax);
|
|
194
|
+
if (lineText.trim() === '$$') {
|
|
195
|
+
autoClose = true;
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
// Check if line contains $$
|
|
199
|
+
if (lineText.includes('$$')) {
|
|
200
|
+
autoClose = true;
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (!autoClose) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
if (silent)
|
|
208
|
+
return true;
|
|
209
|
+
// Extract content between $$ markers
|
|
210
|
+
const contentLines = [];
|
|
211
|
+
for (let i = startLine + 1; i < nextLine; i++) {
|
|
212
|
+
const lineStart = state.bMarks[i] + state.tShift[i];
|
|
213
|
+
const lineMax = state.eMarks[i];
|
|
214
|
+
contentLines.push(state.src.slice(lineStart, lineMax));
|
|
215
|
+
}
|
|
216
|
+
const content = contentLines.join('\n');
|
|
217
|
+
const token = state.push('math_block', 'div', 0);
|
|
218
|
+
token.content = content;
|
|
219
|
+
token.markup = marker;
|
|
220
|
+
token.block = true;
|
|
221
|
+
token.map = [startLine, nextLine + 1];
|
|
222
|
+
state.line = nextLine + 1;
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Create markdown-it plugin for math support
|
|
227
|
+
*/
|
|
228
|
+
function markdownItMath(md, config = {}) {
|
|
229
|
+
// Add inline display math rule ($$...$$) - must come before inline math ($...$)
|
|
230
|
+
md.inline.ruler.before('escape', 'math_inline_display', (state, silent) => mathInlineDisplayRule(state, silent, config));
|
|
231
|
+
// Add inline math rule ($...$)
|
|
232
|
+
md.inline.ruler.before('escape', 'math_inline', (state, silent) => mathInlineRule(state, silent, config));
|
|
233
|
+
// Add block math rule ($$...$$)
|
|
234
|
+
md.block.ruler.before('fence', 'math_block', mathBlockRule, {
|
|
235
|
+
alt: ['paragraph', 'reference', 'blockquote', 'list'],
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Create math plugin for comark
|
|
240
|
+
*
|
|
241
|
+
* This plugin processes markdown-it tokens and converts math expressions
|
|
242
|
+
* with $ and $$ delimiters to HTML using KaTeX.
|
|
243
|
+
*
|
|
244
|
+
* @param config Math rendering configuration
|
|
245
|
+
*
|
|
246
|
+
* @example
|
|
247
|
+
* ```ts
|
|
248
|
+
* import { parse } from 'comark'
|
|
249
|
+
* import math from 'comark/plugins/math'
|
|
250
|
+
*
|
|
251
|
+
* const result = await parse('Inline $x^2$ and display $$E = mc^2$$', {
|
|
252
|
+
* plugins: [math({ throwOnError: false })]
|
|
253
|
+
* })
|
|
254
|
+
* ```
|
|
255
|
+
*/
|
|
256
|
+
export default function math(config) {
|
|
257
|
+
return {
|
|
258
|
+
name: 'math',
|
|
259
|
+
markdownItPlugins: [
|
|
260
|
+
((md) => markdownItMath(md, config ?? {})),
|
|
261
|
+
],
|
|
262
|
+
};
|
|
263
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ComarkPlugin } from 'comark';
|
|
2
|
+
export type ThemeNames = 'zinc-light' | 'zinc-dark' | 'tokyo-night' | 'tokyo-night-storm' | 'tokyo-night-light' | 'catppuccin-mocha' | 'catppuccin-latte' | 'nord' | 'nord-light' | 'dracula' | 'github-light' | 'github-dark' | 'solarized-light' | 'solarized-dark' | 'one-dark';
|
|
3
|
+
export interface MermaidConfig {
|
|
4
|
+
/**
|
|
5
|
+
* Theme for mermaid diagrams
|
|
6
|
+
* @default undefined
|
|
7
|
+
*/
|
|
8
|
+
theme?: ThemeNames;
|
|
9
|
+
/**
|
|
10
|
+
* Theme for mermaid diagrams in dark mode
|
|
11
|
+
* @default undefined
|
|
12
|
+
*/
|
|
13
|
+
themeDark?: ThemeNames;
|
|
14
|
+
}
|
|
15
|
+
export declare function searchProps(content: string, index?: number): {
|
|
16
|
+
props: [string, string][];
|
|
17
|
+
index: number;
|
|
18
|
+
} | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Create mermaid plugin for comark
|
|
21
|
+
*
|
|
22
|
+
* This plugin processes markdown-it tokens and identifies mermaid code blocks
|
|
23
|
+
* for rendering by Vue/React components.
|
|
24
|
+
*
|
|
25
|
+
* @param config Mermaid rendering configuration
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* import { parse } from 'comark'
|
|
30
|
+
* import { createMermaidPlugin } from 'comark/plugins/mermaid'
|
|
31
|
+
*
|
|
32
|
+
* const mermaid = createMermaidPlugin({ theme: 'dark' })
|
|
33
|
+
* const result = await parse('```mermaid\ngraph TD; A-->B;\n```', {
|
|
34
|
+
* plugins: [mermaid()]
|
|
35
|
+
* })
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export default function comarkMermaid(config?: MermaidConfig): ComarkPlugin;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* markdown-it plugin for mermaid diagrams
|
|
3
|
+
* This handles ```mermaid code blocks
|
|
4
|
+
*/
|
|
5
|
+
function markdownItMermaid(md, config) {
|
|
6
|
+
md.core.ruler.after('block', 'replace-pre', (state) => {
|
|
7
|
+
for (const token of state.tokens) {
|
|
8
|
+
if (token.type === 'fence' && token.info?.startsWith('mermaid')) {
|
|
9
|
+
let info = token.info.substring(7).trim();
|
|
10
|
+
let props = [];
|
|
11
|
+
// extract props from info
|
|
12
|
+
const curlyBraceIndex = info.indexOf('{');
|
|
13
|
+
if (curlyBraceIndex !== -1) {
|
|
14
|
+
const result = searchProps(info.substring(curlyBraceIndex));
|
|
15
|
+
if (result) {
|
|
16
|
+
props = result.props;
|
|
17
|
+
info = info.substring(result.index);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
// set type, tag, props and content
|
|
21
|
+
token.type = 'mermaid';
|
|
22
|
+
token.tag = 'mermaid';
|
|
23
|
+
for (const prop of props) {
|
|
24
|
+
token.attrJoin(prop[0], prop[1]);
|
|
25
|
+
}
|
|
26
|
+
if (config?.theme) {
|
|
27
|
+
token.attrSet('theme', config.theme);
|
|
28
|
+
}
|
|
29
|
+
if (config?.themeDark) {
|
|
30
|
+
token.attrSet('theme-dark', config.themeDark);
|
|
31
|
+
}
|
|
32
|
+
token.info = info;
|
|
33
|
+
token.attrSet('content', token.content);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
export function searchProps(content, index = 0) {
|
|
39
|
+
const bracketPairs = {
|
|
40
|
+
'[': ']',
|
|
41
|
+
'{': '}',
|
|
42
|
+
'(': ')',
|
|
43
|
+
};
|
|
44
|
+
const quotePairs = {
|
|
45
|
+
'\'': '\'',
|
|
46
|
+
'"': '"',
|
|
47
|
+
'`': '`',
|
|
48
|
+
};
|
|
49
|
+
if (content[index] !== '{')
|
|
50
|
+
throw new Error(`Invalid props, expected \`{\` but got '${content[index]}'`);
|
|
51
|
+
const props = [];
|
|
52
|
+
// Vue's mustache {{ }} syntax
|
|
53
|
+
if (content[index + 1] === '{')
|
|
54
|
+
return undefined;
|
|
55
|
+
index += 1;
|
|
56
|
+
while (index < content.length) {
|
|
57
|
+
if (content[index] === '\\') {
|
|
58
|
+
index += 2;
|
|
59
|
+
}
|
|
60
|
+
else if (content[index] === '}') {
|
|
61
|
+
index += 1;
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
else if (content[index] === ' ') {
|
|
65
|
+
index += 1;
|
|
66
|
+
}
|
|
67
|
+
else if (content[index] === '.') {
|
|
68
|
+
index += 1;
|
|
69
|
+
props.push([
|
|
70
|
+
'class',
|
|
71
|
+
searchUntil(' #.}'),
|
|
72
|
+
]);
|
|
73
|
+
}
|
|
74
|
+
else if (content[index] === '#') {
|
|
75
|
+
index += 1;
|
|
76
|
+
props.push([
|
|
77
|
+
'id',
|
|
78
|
+
searchUntil(' #.}'),
|
|
79
|
+
]);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
const start = index;
|
|
83
|
+
while (index < content.length) {
|
|
84
|
+
index += 1;
|
|
85
|
+
if (' }='.includes(content[index]))
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
const char = content[index];
|
|
89
|
+
if (start !== index) {
|
|
90
|
+
const key = content.slice(start, index).trim();
|
|
91
|
+
if (char === '=') {
|
|
92
|
+
index += 1;
|
|
93
|
+
props.push([
|
|
94
|
+
key,
|
|
95
|
+
searchValue(),
|
|
96
|
+
]);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
props.push([
|
|
100
|
+
key,
|
|
101
|
+
'true',
|
|
102
|
+
]);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function searchUntil(str) {
|
|
108
|
+
const start = index;
|
|
109
|
+
while (index < content.length) {
|
|
110
|
+
index += 1;
|
|
111
|
+
if (content[index] === '\\')
|
|
112
|
+
index += 2;
|
|
113
|
+
if (str.includes(content[index]))
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
return content.slice(start, index);
|
|
117
|
+
}
|
|
118
|
+
function searchValue() {
|
|
119
|
+
const start = index;
|
|
120
|
+
if (content[index] in bracketPairs) {
|
|
121
|
+
searchBracket(bracketPairs[content[index]]);
|
|
122
|
+
index += 1;
|
|
123
|
+
return content.slice(start, index);
|
|
124
|
+
}
|
|
125
|
+
else if (content[index] in quotePairs) {
|
|
126
|
+
searchString(quotePairs[content[index]]);
|
|
127
|
+
index += 1;
|
|
128
|
+
return content.slice(start, index);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
// unquoted value
|
|
132
|
+
return searchUntil(' }');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function searchBracket(end) {
|
|
136
|
+
while (index < content.length) {
|
|
137
|
+
index++;
|
|
138
|
+
if (content[index] in quotePairs)
|
|
139
|
+
searchString(quotePairs[content[index]]);
|
|
140
|
+
else if (content[index] in bracketPairs)
|
|
141
|
+
searchBracket(bracketPairs[content[index]]);
|
|
142
|
+
else if (content[index] === end)
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function searchString(end) {
|
|
147
|
+
return searchUntil(end);
|
|
148
|
+
}
|
|
149
|
+
// Escale quotes
|
|
150
|
+
props.forEach((v) => {
|
|
151
|
+
if (v[1].match(/^(['"`]).*\1$/))
|
|
152
|
+
v[1] = v[1].slice(1, -1);
|
|
153
|
+
});
|
|
154
|
+
return {
|
|
155
|
+
props,
|
|
156
|
+
index,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Create mermaid plugin for comark
|
|
161
|
+
*
|
|
162
|
+
* This plugin processes markdown-it tokens and identifies mermaid code blocks
|
|
163
|
+
* for rendering by Vue/React components.
|
|
164
|
+
*
|
|
165
|
+
* @param config Mermaid rendering configuration
|
|
166
|
+
*
|
|
167
|
+
* @example
|
|
168
|
+
* ```ts
|
|
169
|
+
* import { parse } from 'comark'
|
|
170
|
+
* import { createMermaidPlugin } from 'comark/plugins/mermaid'
|
|
171
|
+
*
|
|
172
|
+
* const mermaid = createMermaidPlugin({ theme: 'dark' })
|
|
173
|
+
* const result = await parse('```mermaid\ngraph TD; A-->B;\n```', {
|
|
174
|
+
* plugins: [mermaid()]
|
|
175
|
+
* })
|
|
176
|
+
* ```
|
|
177
|
+
*/
|
|
178
|
+
export default function comarkMermaid(config) {
|
|
179
|
+
return {
|
|
180
|
+
name: 'mermaid',
|
|
181
|
+
markdownItPlugins: [
|
|
182
|
+
((md) => markdownItMermaid(md, config)),
|
|
183
|
+
],
|
|
184
|
+
};
|
|
185
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ComarkPlugin } from 'comark';
|
|
2
|
+
import type { PropsValidationOptions } from '../internal/props-validation';
|
|
3
|
+
interface SecurityOptions extends PropsValidationOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Tags to remove entirely from the output tree.
|
|
6
|
+
* @default []
|
|
7
|
+
*/
|
|
8
|
+
blockedTags?: string[];
|
|
9
|
+
}
|
|
10
|
+
export default function security(options: SecurityOptions): ComarkPlugin;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { visit } from 'comark/utils';
|
|
2
|
+
import { validateProps } from '../internal/props-validation';
|
|
3
|
+
export default function security(options) {
|
|
4
|
+
const { blockedTags = [], allowedLinkPrefixes, allowedImagePrefixes, allowedProtocols, defaultOrigin, allowDataImages, } = options;
|
|
5
|
+
const dropSet = new Set(blockedTags.map(t => t.toLowerCase()));
|
|
6
|
+
const propsOptions = {
|
|
7
|
+
allowedLinkPrefixes,
|
|
8
|
+
allowedImagePrefixes,
|
|
9
|
+
allowedProtocols,
|
|
10
|
+
defaultOrigin,
|
|
11
|
+
allowDataImages,
|
|
12
|
+
};
|
|
13
|
+
return {
|
|
14
|
+
name: 'security',
|
|
15
|
+
post(state) {
|
|
16
|
+
visit(state.tree, node => typeof node !== 'string' && node[0] !== null, (node) => {
|
|
17
|
+
const element = node;
|
|
18
|
+
// return false to remove the node from the tree
|
|
19
|
+
if (dropSet.has(element[0].toLowerCase())) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
const keys = Object.keys(element[1]);
|
|
23
|
+
/**
|
|
24
|
+
* If the element has any props, validate them
|
|
25
|
+
*/
|
|
26
|
+
if (keys.length) {
|
|
27
|
+
element[1] = validateProps(element[0], element[1], propsOptions);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { applyAutoUnwrap } from '../internal/parse/auto-unwrap';
|
|
2
|
+
import { marmdownItTokensToComarkTree } from '../internal/parse/token-processor';
|
|
3
|
+
export default function summary(delimiter = '<!-- more -->') {
|
|
4
|
+
return {
|
|
5
|
+
name: 'summary',
|
|
6
|
+
post(state) {
|
|
7
|
+
let summary;
|
|
8
|
+
const delimiterIndex = state.tokens.findIndex((token) => token.type === 'html_block' && token.content?.includes(delimiter));
|
|
9
|
+
if (delimiterIndex !== -1) {
|
|
10
|
+
const summaryTokens = state.tokens.slice(0, delimiterIndex);
|
|
11
|
+
summary = marmdownItTokensToComarkTree(summaryTokens);
|
|
12
|
+
// Apply auto-unwrap to summary as well
|
|
13
|
+
if (state.options.autoUnwrap) {
|
|
14
|
+
summary = summary?.map((child) => applyAutoUnwrap(child));
|
|
15
|
+
}
|
|
16
|
+
if (summary) {
|
|
17
|
+
state.tree.meta.summary = summary;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom task list plugin for markdown-it that works with @comark/markdown-it
|
|
3
|
+
*
|
|
4
|
+
* This plugin runs before inline parsing to prevent Comark from interpreting
|
|
5
|
+
* task list markers [X] and [ ] as Comark inline span syntax.
|
|
6
|
+
*/
|
|
7
|
+
import type { ComarkPlugin } from 'comark';
|
|
8
|
+
export default function comarkTaskList(): ComarkPlugin;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom task list plugin for markdown-it that works with @comark/markdown-it
|
|
3
|
+
*
|
|
4
|
+
* This plugin runs before inline parsing to prevent Comark from interpreting
|
|
5
|
+
* task list markers [X] and [ ] as Comark inline span syntax.
|
|
6
|
+
*/
|
|
7
|
+
function attrSet(token, name, value) {
|
|
8
|
+
const index = token.attrIndex(name);
|
|
9
|
+
const attr = [name, value];
|
|
10
|
+
if (index < 0) {
|
|
11
|
+
if (!token.attrs) {
|
|
12
|
+
token.attrs = [];
|
|
13
|
+
}
|
|
14
|
+
token.attrs.push(attr);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
token.attrs[index] = attr;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function findParentListItem(tokens, index) {
|
|
21
|
+
// Look backwards for list_item_open
|
|
22
|
+
for (let i = index - 1; i >= 0; i--) {
|
|
23
|
+
if (tokens[i].type === 'list_item_open') {
|
|
24
|
+
return i;
|
|
25
|
+
}
|
|
26
|
+
if (tokens[i].type === 'list_item_close') {
|
|
27
|
+
// We've gone past the current list item
|
|
28
|
+
return -1;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return -1;
|
|
32
|
+
}
|
|
33
|
+
function findParentList(tokens, listItemIndex) {
|
|
34
|
+
const targetLevel = tokens[listItemIndex].level - 1;
|
|
35
|
+
// Look backwards for the list (ul/ol) that contains this list item
|
|
36
|
+
for (let i = listItemIndex - 1; i >= 0; i--) {
|
|
37
|
+
if (tokens[i].level === targetLevel
|
|
38
|
+
&& (tokens[i].type === 'bullet_list_open' || tokens[i].type === 'ordered_list_open')) {
|
|
39
|
+
return i;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return -1;
|
|
43
|
+
}
|
|
44
|
+
function markdownItTaskList(md, options) {
|
|
45
|
+
const disableCheckboxes = !(options?.enabled ?? false);
|
|
46
|
+
// Run BEFORE inline parsing to prevent Comark from processing task list markers
|
|
47
|
+
md.core.ruler.before('inline', 'task-lists-mdc', (state) => {
|
|
48
|
+
const tokens = state.tokens;
|
|
49
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
50
|
+
const token = tokens[i];
|
|
51
|
+
// Look for list items that might contain task lists
|
|
52
|
+
if (token.type === 'inline' && token.content) {
|
|
53
|
+
const parentIdx = findParentListItem(tokens, i);
|
|
54
|
+
if (parentIdx >= 0) {
|
|
55
|
+
// Check if content starts with task list marker
|
|
56
|
+
const match = token.content.match(/^(\[[ x]\])\s+/i);
|
|
57
|
+
if (match) {
|
|
58
|
+
const isChecked = match[1].toLowerCase() === '[x]';
|
|
59
|
+
// Mark the list item with task-list-item class
|
|
60
|
+
attrSet(tokens[parentIdx], 'class', 'task-list-item');
|
|
61
|
+
// Mark the parent list with contains-task-list class
|
|
62
|
+
const listIdx = findParentList(tokens, parentIdx);
|
|
63
|
+
if (listIdx >= 0) {
|
|
64
|
+
attrSet(tokens[listIdx], 'class', 'contains-task-list');
|
|
65
|
+
}
|
|
66
|
+
// Replace the task marker with a placeholder that won't be processed by Comark
|
|
67
|
+
// We use a special format that we can detect later
|
|
68
|
+
// Keep one space after the placeholder to match expected output
|
|
69
|
+
const checkboxPlaceholder = `{{TASK_CHECKBOX_${isChecked ? 'CHECKED' : 'UNCHECKED'}}} `;
|
|
70
|
+
token.content = token.content.replace(/^\[[ x]\]\s+/i, checkboxPlaceholder);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
// Run AFTER inline parsing to replace placeholders with actual checkbox HTML
|
|
77
|
+
// @ts-expect-error - after is not a valid method on the ruler types
|
|
78
|
+
md.core.ruler.after('inline', 'task-lists-mdc-post', (state) => {
|
|
79
|
+
const tokens = state.tokens;
|
|
80
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
81
|
+
const token = tokens[i];
|
|
82
|
+
if (token.type === 'inline' && token.children) {
|
|
83
|
+
for (let j = 0; j < token.children.length; j++) {
|
|
84
|
+
const child = token.children[j];
|
|
85
|
+
if (child.type === 'text' && child.content) {
|
|
86
|
+
// Check for our checkbox placeholder
|
|
87
|
+
const checkedMatch = child.content.match(/^\{\{TASK_CHECKBOX_CHECKED\}\}/);
|
|
88
|
+
const uncheckedMatch = child.content.match(/^\{\{TASK_CHECKBOX_UNCHECKED\}\}/);
|
|
89
|
+
if (checkedMatch || uncheckedMatch) {
|
|
90
|
+
const isChecked = !!checkedMatch;
|
|
91
|
+
// Create checkbox token
|
|
92
|
+
const checkbox = new state.Token('mdc_inline_component', 'input', 0);
|
|
93
|
+
checkbox.attrs = [['class', 'task-list-item-checkbox'], ['type', 'checkbox']];
|
|
94
|
+
if (disableCheckboxes) {
|
|
95
|
+
checkbox.attrs.push([':disabled', 'true']);
|
|
96
|
+
}
|
|
97
|
+
if (isChecked) {
|
|
98
|
+
checkbox.attrs.push([':checked', 'true']);
|
|
99
|
+
}
|
|
100
|
+
// Remove placeholder from text
|
|
101
|
+
child.content = child.content.replace(/^\{\{TASK_CHECKBOX_(CHECKED|UNCHECKED)\}\}/, '');
|
|
102
|
+
// Insert checkbox before the text
|
|
103
|
+
token.children.splice(j, 0, checkbox);
|
|
104
|
+
j++; // Skip the newly inserted checkbox
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
export default function comarkTaskList() {
|
|
113
|
+
return {
|
|
114
|
+
name: 'task-list',
|
|
115
|
+
markdownItPlugins: [markdownItTaskList],
|
|
116
|
+
};
|
|
117
|
+
}
|