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.
- 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/hr.d.ts +1 -1
- package/dist/internal/stringify/handlers/hr.js +4 -1
- package/dist/internal/stringify/handlers/html.js +13 -2
- 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 +3 -2
- 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.d.ts +2 -2
- package/dist/plugins/highlight.js +7 -5
- package/dist/plugins/json-render.d.ts +53 -0
- package/dist/plugins/json-render.js +99 -0
- 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 -23
- 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
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ const chatMessage = ...
|
|
|
48
48
|
```bash
|
|
49
49
|
npm install @comark/react katex
|
|
50
50
|
# or
|
|
51
|
-
pnpm add @comark/react katex
|
|
51
|
+
pnpm add @comark/react katex
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
```tsx
|
|
@@ -97,8 +97,18 @@ const html = await render(chatMessage)
|
|
|
97
97
|
```
|
|
98
98
|
|
|
99
99
|
|
|
100
|
+
## Agent skill
|
|
101
|
+
|
|
102
|
+
Coding agents can install the Comark skill from the docs site:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
npx skills add https://comark.dev
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
See [Installation](https://comark.dev/getting-started/installation) on comark.dev for details.
|
|
109
|
+
|
|
100
110
|
## License
|
|
101
111
|
|
|
102
112
|
Made with ❤️
|
|
103
113
|
|
|
104
|
-
Published under [MIT License](
|
|
114
|
+
Published under [MIT License](https://github.com/comarkdown/comark/blob/main/LICENSE).
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DumpOptions } from 'js-yaml';
|
|
1
2
|
/**
|
|
2
3
|
* Parse frontmatter from content
|
|
3
4
|
* @param content - The content to parse
|
|
@@ -13,4 +14,4 @@ export declare function parseFrontmatter(content: string): {
|
|
|
13
14
|
* @param content - The content to render
|
|
14
15
|
* @returns The rendered content
|
|
15
16
|
*/
|
|
16
|
-
export declare function renderFrontmatter(data: Record<string, any> | undefined | null, content?: string): string;
|
|
17
|
+
export declare function renderFrontmatter(data: Record<string, any> | undefined | null, content?: string, yamlOptions?: DumpOptions): string;
|
|
@@ -31,11 +31,11 @@ export function parseFrontmatter(content) {
|
|
|
31
31
|
* @param content - The content to render
|
|
32
32
|
* @returns The rendered content
|
|
33
33
|
*/
|
|
34
|
-
export function renderFrontmatter(data, content) {
|
|
34
|
+
export function renderFrontmatter(data, content, yamlOptions) {
|
|
35
35
|
if (!data || Object.keys(data).length === 0) {
|
|
36
36
|
return (content?.trim() || '');
|
|
37
37
|
}
|
|
38
|
-
const fm = stringifyYaml(data).trim();
|
|
38
|
+
const fm = stringifyYaml(data, yamlOptions).trim();
|
|
39
39
|
if (content) {
|
|
40
40
|
return FRONTMATTER_DELIMITER_DEFAULT + LF + fm + LF + FRONTMATTER_DELIMITER_DEFAULT + LF + LF + content.trim();
|
|
41
41
|
}
|
|
@@ -196,8 +196,55 @@ function closeInlineMarkersLinear(line) {
|
|
|
196
196
|
const doubleAsteriskPositions = [];
|
|
197
197
|
const doubleUnderscorePositions = [];
|
|
198
198
|
// Single-pass scan through the line - O(n)
|
|
199
|
+
// Skip markers inside attribute scopes {...} and link text [...] / link URL (...)
|
|
200
|
+
let inAttributes = 0;
|
|
201
|
+
let inLinkText = 0;
|
|
202
|
+
let inLinkUrl = 0;
|
|
199
203
|
for (let i = 0; i < len; i++) {
|
|
204
|
+
const prevCh = i > 0 ? line[i - 1] : '';
|
|
200
205
|
const ch = line[i];
|
|
206
|
+
if (ch === '{' && prevCh !== ' ') {
|
|
207
|
+
inAttributes++;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (ch === '}') {
|
|
211
|
+
if (inAttributes > 0)
|
|
212
|
+
inAttributes--;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (inAttributes > 0)
|
|
216
|
+
continue;
|
|
217
|
+
if (ch === '[') {
|
|
218
|
+
bracketBalance++;
|
|
219
|
+
lastBracketPos = i;
|
|
220
|
+
inLinkText++;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (ch === ']') {
|
|
224
|
+
bracketBalance--;
|
|
225
|
+
lastBracketPos = i;
|
|
226
|
+
if (inLinkText > 0)
|
|
227
|
+
inLinkText--;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (ch === '(') {
|
|
231
|
+
if (lastBracketPos >= 0 && i > lastBracketPos) {
|
|
232
|
+
parenBalance++;
|
|
233
|
+
if (prevCh === ']')
|
|
234
|
+
inLinkUrl++;
|
|
235
|
+
}
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (ch === ')') {
|
|
239
|
+
if (lastBracketPos >= 0 && i > lastBracketPos) {
|
|
240
|
+
parenBalance--;
|
|
241
|
+
if (inLinkUrl > 0)
|
|
242
|
+
inLinkUrl--;
|
|
243
|
+
}
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (inLinkText > 0 || inLinkUrl > 0)
|
|
247
|
+
continue;
|
|
201
248
|
if (ch === '*') {
|
|
202
249
|
asteriskCount++;
|
|
203
250
|
// Track ** positions (not part of ***)
|
|
@@ -209,10 +256,16 @@ function closeInlineMarkersLinear(line) {
|
|
|
209
256
|
}
|
|
210
257
|
}
|
|
211
258
|
else if (ch === '_') {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
259
|
+
// Skip intra-word underscores (not emphasis delimiters per CommonMark)
|
|
260
|
+
const nextCh = i + 1 < len ? line[i + 1] : '';
|
|
261
|
+
const prevIsWord = (prevCh >= 'a' && prevCh <= 'z') || (prevCh >= 'A' && prevCh <= 'Z') || (prevCh >= '0' && prevCh <= '9');
|
|
262
|
+
const nextIsWord = (nextCh >= 'a' && nextCh <= 'z') || (nextCh >= 'A' && nextCh <= 'Z') || (nextCh >= '0' && nextCh <= '9');
|
|
263
|
+
if (!(prevIsWord && nextIsWord)) {
|
|
264
|
+
underscoreCount++;
|
|
265
|
+
// Track __ positions (for bold)
|
|
266
|
+
if (nextCh === '_') {
|
|
267
|
+
doubleUnderscorePositions.push(i);
|
|
268
|
+
}
|
|
216
269
|
}
|
|
217
270
|
}
|
|
218
271
|
else if (ch === '~') {
|
|
@@ -221,7 +274,7 @@ function closeInlineMarkersLinear(line) {
|
|
|
221
274
|
else if (ch === '`') {
|
|
222
275
|
backtickCount++;
|
|
223
276
|
}
|
|
224
|
-
else if (ch === '$') {
|
|
277
|
+
else if (ch === '$' && prevCh !== '\\') {
|
|
225
278
|
// Count $$ pairs for block/display math
|
|
226
279
|
if (i + 1 < len && line[i + 1] === '$') {
|
|
227
280
|
dollarPairCount++;
|
|
@@ -232,24 +285,6 @@ function closeInlineMarkersLinear(line) {
|
|
|
232
285
|
dollarCount++; // Single $ for inline math
|
|
233
286
|
}
|
|
234
287
|
}
|
|
235
|
-
else if (ch === '[') {
|
|
236
|
-
bracketBalance++;
|
|
237
|
-
lastBracketPos = i;
|
|
238
|
-
}
|
|
239
|
-
else if (ch === ']') {
|
|
240
|
-
bracketBalance--;
|
|
241
|
-
lastBracketPos = i;
|
|
242
|
-
}
|
|
243
|
-
else if (ch === '(') {
|
|
244
|
-
if (lastBracketPos >= 0 && i > lastBracketPos) {
|
|
245
|
-
parenBalance++;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
else if (ch === ')') {
|
|
249
|
-
if (lastBracketPos >= 0 && i > lastBracketPos) {
|
|
250
|
-
parenBalance--;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
288
|
}
|
|
254
289
|
// Check for complete ** pairs in O(1) - pairs are matched left to right
|
|
255
290
|
const hasCompleteBoldPair = doubleAsteriskPositions.length >= 2;
|
|
@@ -26,6 +26,7 @@ const INLINE_TAG_MAP = {
|
|
|
26
26
|
export function marmdownItTokensToComarkTree(tokens, options = { startLine: 0, preservePositions: false }) {
|
|
27
27
|
const state = {
|
|
28
28
|
headingSlugCounts: new Map(),
|
|
29
|
+
headingStack: [],
|
|
29
30
|
preservePositions: options.preservePositions,
|
|
30
31
|
};
|
|
31
32
|
const nodes = [];
|
|
@@ -302,14 +303,14 @@ function processBlockToken(tokens, startIndex, insideNestedContext = false, stat
|
|
|
302
303
|
return { node: pre, nextIndex: startIndex + 1 };
|
|
303
304
|
}
|
|
304
305
|
if (token.type === 'heading_open') {
|
|
305
|
-
const level = token.tag.replace('h', '');
|
|
306
|
+
const level = Number.parseInt(token.tag.replace('h', ''), 10);
|
|
306
307
|
const headingTag = `h${level}`;
|
|
307
308
|
// Process heading children with inHeading flag for Comark component handling
|
|
308
309
|
const children = processBlockChildren(tokens, startIndex + 1, 'heading_close', true, true, insideNestedContext, state);
|
|
309
310
|
if (children.nodes.length > 0) {
|
|
310
311
|
// Always generate ID for all headings, no exceptions
|
|
311
312
|
const textContent = extractTextContent(children.nodes);
|
|
312
|
-
const headingId = uniqueSlug(slugify(textContent), state);
|
|
313
|
+
const headingId = uniqueSlug(slugify(textContent), level, state);
|
|
313
314
|
// Always attach ID to the heading element itself
|
|
314
315
|
return { node: [headingTag, { id: headingId }, ...children.nodes], nextIndex: children.nextIndex + 1 };
|
|
315
316
|
}
|
|
@@ -522,9 +523,23 @@ function slugify(text) {
|
|
|
522
523
|
/**
|
|
523
524
|
* Return a unique slug by appending a numeric suffix for duplicates
|
|
524
525
|
*/
|
|
525
|
-
function uniqueSlug(slug, state) {
|
|
526
|
+
function uniqueSlug(slug, level, state) {
|
|
526
527
|
if (!state)
|
|
527
528
|
return slug;
|
|
529
|
+
// Build hierarchical ID: pop headings at same or deeper level, then prefix with parent's ID
|
|
530
|
+
// Pop headings at same level or deeper
|
|
531
|
+
while (state.headingStack.length > 0 && state.headingStack[state.headingStack.length - 1].level >= level) {
|
|
532
|
+
state.headingStack.pop();
|
|
533
|
+
}
|
|
534
|
+
// Use parent's full ID as prefix (h1 doesn't prefix children)
|
|
535
|
+
if (state.headingStack.length > 0) {
|
|
536
|
+
const parent = state.headingStack[state.headingStack.length - 1];
|
|
537
|
+
if (parent.level >= 2) {
|
|
538
|
+
slug = parent.id + '-' + slug;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
// Push onto stack for child headings to reference
|
|
542
|
+
state.headingStack.push({ level, id: slug });
|
|
528
543
|
const count = state.headingSlugCounts.get(slug) ?? 0;
|
|
529
544
|
state.headingSlugCounts.set(slug, count + 1);
|
|
530
545
|
return count === 0 ? slug : `${slug}-${count}`;
|
|
@@ -1,3 +1,39 @@
|
|
|
1
|
+
import type { NodeRenderData } from '../../types.ts';
|
|
2
|
+
export interface ResolveAttributesOptions {
|
|
3
|
+
/**
|
|
4
|
+
* When true, every `:prefixed` string value is JSON-parsed first and the
|
|
5
|
+
* `:` prefix is always stripped. Non-JSON strings fall back to a dot-path
|
|
6
|
+
* lookup in `renderData`; unresolved paths yield `undefined`.
|
|
7
|
+
*
|
|
8
|
+
* This matches the Vue/React/Svelte renderer semantics, which always
|
|
9
|
+
* normalize bindings into real JS values suitable for typed component props.
|
|
10
|
+
*
|
|
11
|
+
* When false (default) only dot-path lookups are applied — literals and
|
|
12
|
+
* unresolved paths are preserved verbatim so string-based serializers
|
|
13
|
+
* (like HTML attribute emitters) can apply their own `:prefix` handling.
|
|
14
|
+
*/
|
|
15
|
+
parseJson?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Resolve `:prefixed` attributes against the render context.
|
|
19
|
+
*
|
|
20
|
+
* Default behavior: a `:prefixed` string value that matches a dot-path in
|
|
21
|
+
* `{ frontmatter, meta, data, props }` is replaced with the resolved value
|
|
22
|
+
* (and the `:` prefix is stripped). Anything that doesn't resolve — literals
|
|
23
|
+
* like `"5"` / `"true"`, unknown paths, or already-parsed object values — is
|
|
24
|
+
* left untouched and keeps its `:` prefix.
|
|
25
|
+
*
|
|
26
|
+
* With `parseJson: true`, every `:prefixed` string is JSON-parsed first and
|
|
27
|
+
* the `:` prefix is always stripped, falling back to the dot-path lookup.
|
|
28
|
+
* The `$` metadata key is never forwarded.
|
|
29
|
+
*/
|
|
30
|
+
export declare function resolveAttributes(attrs: Record<string, unknown>, renderData: NodeRenderData, options?: ResolveAttributesOptions): Record<string, unknown>;
|
|
31
|
+
/**
|
|
32
|
+
* Read a named attribute, preferring its `:prefixed` binding (resolved against
|
|
33
|
+
* `renderData`) over the literal `key`. Falls back to the raw value when the
|
|
34
|
+
* binding doesn't resolve.
|
|
35
|
+
*/
|
|
36
|
+
export declare function resolveAttribute(attrs: Record<string, unknown>, renderData: NodeRenderData, key: string): unknown;
|
|
1
37
|
/**
|
|
2
38
|
* Convert attributes to a string of Comark attributes
|
|
3
39
|
*
|
|
@@ -18,4 +54,4 @@ export declare function htmlAttributes(attributes: Record<string, unknown>): str
|
|
|
18
54
|
* @param attributes - The attributes to stringify
|
|
19
55
|
* @returns The stringified attributes
|
|
20
56
|
*/
|
|
21
|
-
export declare function comarkYamlAttributes(attributes: Record<string, unknown
|
|
57
|
+
export declare function comarkYamlAttributes(attributes: Record<string, unknown>, style?: 'frontmatter' | 'codeblock'): string;
|
|
@@ -1,4 +1,72 @@
|
|
|
1
1
|
import { stringifyYaml } from "../yaml.js";
|
|
2
|
+
import { get } from "../../utils/index.js";
|
|
3
|
+
/**
|
|
4
|
+
* Resolve `:prefixed` attributes against the render context.
|
|
5
|
+
*
|
|
6
|
+
* Default behavior: a `:prefixed` string value that matches a dot-path in
|
|
7
|
+
* `{ frontmatter, meta, data, props }` is replaced with the resolved value
|
|
8
|
+
* (and the `:` prefix is stripped). Anything that doesn't resolve — literals
|
|
9
|
+
* like `"5"` / `"true"`, unknown paths, or already-parsed object values — is
|
|
10
|
+
* left untouched and keeps its `:` prefix.
|
|
11
|
+
*
|
|
12
|
+
* With `parseJson: true`, every `:prefixed` string is JSON-parsed first and
|
|
13
|
+
* the `:` prefix is always stripped, falling back to the dot-path lookup.
|
|
14
|
+
* The `$` metadata key is never forwarded.
|
|
15
|
+
*/
|
|
16
|
+
export function resolveAttributes(attrs, renderData, options = {}) {
|
|
17
|
+
const result = {};
|
|
18
|
+
for (const key in attrs) {
|
|
19
|
+
if (key === '$')
|
|
20
|
+
continue;
|
|
21
|
+
const value = attrs[key];
|
|
22
|
+
const isBinding = key.charCodeAt(0) === 58; /* ':' */
|
|
23
|
+
if (options.parseJson && isBinding) {
|
|
24
|
+
// Framework mode: always strip `:` and hand components real JS values.
|
|
25
|
+
if (typeof value === 'string') {
|
|
26
|
+
try {
|
|
27
|
+
result[key.slice(1)] = JSON.parse(value);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// not JSON — fall through to dot-path lookup
|
|
32
|
+
}
|
|
33
|
+
result[key.slice(1)] = get(renderData, value);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
// Non-string binding value (e.g. an object literal the parser already
|
|
37
|
+
// decoded) — pass through with the prefix stripped.
|
|
38
|
+
result[key.slice(1)] = value;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (isBinding && typeof value === 'string') {
|
|
42
|
+
const resolved = get(renderData, value);
|
|
43
|
+
if (resolved !== undefined) {
|
|
44
|
+
result[key.slice(1)] = resolved;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
result[key] = value;
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Read a named attribute, preferring its `:prefixed` binding (resolved against
|
|
54
|
+
* `renderData`) over the literal `key`. Falls back to the raw value when the
|
|
55
|
+
* binding doesn't resolve.
|
|
56
|
+
*/
|
|
57
|
+
export function resolveAttribute(attrs, renderData, key) {
|
|
58
|
+
const bindKey = `:${key}`;
|
|
59
|
+
if (bindKey in attrs) {
|
|
60
|
+
const value = attrs[bindKey];
|
|
61
|
+
if (typeof value === 'string') {
|
|
62
|
+
const resolved = get(renderData, value);
|
|
63
|
+
if (resolved !== undefined)
|
|
64
|
+
return resolved;
|
|
65
|
+
}
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
return attrs[key];
|
|
69
|
+
}
|
|
2
70
|
/**
|
|
3
71
|
* Convert attributes to a string of Comark attributes
|
|
4
72
|
*
|
|
@@ -32,22 +100,33 @@ export function comarkAttributes(attributes) {
|
|
|
32
100
|
* @returns The stringified attributes
|
|
33
101
|
*/
|
|
34
102
|
export function htmlAttributes(attributes) {
|
|
35
|
-
|
|
36
|
-
|
|
103
|
+
const parts = [];
|
|
104
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
37
105
|
if (key.startsWith(':')) {
|
|
38
106
|
if (value === 'true') {
|
|
39
|
-
|
|
107
|
+
parts.push(key.slice(1));
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (typeof value === 'object' && value !== null) {
|
|
111
|
+
parts.push(`${key.slice(1)}="${JSON.stringify(value).replace(/"/g, '\\"')}"`);
|
|
112
|
+
continue;
|
|
40
113
|
}
|
|
41
|
-
|
|
114
|
+
parts.push(`${key.slice(1)}="${value}"`);
|
|
115
|
+
continue;
|
|
42
116
|
}
|
|
43
|
-
if (value === 'true')
|
|
44
|
-
|
|
117
|
+
if (value === true || value === 'true') {
|
|
118
|
+
parts.push(key);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (value === false || value === null || value === undefined)
|
|
122
|
+
continue;
|
|
45
123
|
if (typeof value === 'object') {
|
|
46
|
-
|
|
124
|
+
parts.push(`${key}="${JSON.stringify(value).replace(/"/g, '\\"')}"`);
|
|
125
|
+
continue;
|
|
47
126
|
}
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
|
|
127
|
+
parts.push(`${key}="${value}"`);
|
|
128
|
+
}
|
|
129
|
+
return parts.join(' ');
|
|
51
130
|
}
|
|
52
131
|
/**
|
|
53
132
|
* Convert attributes to a string of YAML attributes
|
|
@@ -55,7 +134,7 @@ export function htmlAttributes(attributes) {
|
|
|
55
134
|
* @param attributes - The attributes to stringify
|
|
56
135
|
* @returns The stringified attributes
|
|
57
136
|
*/
|
|
58
|
-
export function comarkYamlAttributes(attributes) {
|
|
137
|
+
export function comarkYamlAttributes(attributes, style = 'codeblock') {
|
|
59
138
|
// Normalize boolean attributes to remove the colon prefix
|
|
60
139
|
const normalized = Object.fromEntries(Object.entries(attributes).map(([key, value]) => {
|
|
61
140
|
if (key.startsWith(':') && (value === 'true' || value === 'false')) {
|
|
@@ -63,5 +142,10 @@ export function comarkYamlAttributes(attributes) {
|
|
|
63
142
|
}
|
|
64
143
|
return [key, value];
|
|
65
144
|
}));
|
|
66
|
-
|
|
145
|
+
const yamlContent = stringifyYaml(normalized).trim();
|
|
146
|
+
if (style === 'frontmatter') {
|
|
147
|
+
return `---\n${yamlContent}\n---`;
|
|
148
|
+
}
|
|
149
|
+
const fence = yamlContent.includes('```') ? '~~~' : '```';
|
|
150
|
+
return `${fence}yaml [props]\n${yamlContent}\n${fence}`;
|
|
67
151
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { comarkAttributes } from "../attributes.js";
|
|
2
|
-
import { textContent } from
|
|
2
|
+
import { textContent } from "../../../utils/index.js";
|
|
3
3
|
export function code(node, _state) {
|
|
4
4
|
const [_, attrs] = node;
|
|
5
5
|
const attrsString = Object.keys(attrs).length > 0
|
|
@@ -6,7 +6,18 @@ const inlineTags = new Set(['strong', 'em', 'code', 'a', 'br', 'span', 'img']);
|
|
|
6
6
|
const blockTags = new Set(['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'ul', 'ol', 'blockquote', 'hr', 'table', 'td', 'th']);
|
|
7
7
|
export async function html(node, state, parent) {
|
|
8
8
|
const [tag, attr, ...children] = node;
|
|
9
|
-
const { $ = {}, ...
|
|
9
|
+
const { $ = {}, ...rawAttributes } = attr;
|
|
10
|
+
// In text/html mode, `one()` has already resolved this element's `:prefix`
|
|
11
|
+
// bindings against the parent's render context and stored the result in
|
|
12
|
+
// `state.renderData.props` — but only when the element has its own attrs.
|
|
13
|
+
// If it doesn't, `state.renderData.props` still holds the enclosing scope
|
|
14
|
+
// (so that `{{ props.* }}` in nested children keeps working), so we fall
|
|
15
|
+
// back to the raw (empty) attrs to avoid leaking parent props onto native
|
|
16
|
+
// wrappers like `<p>` or `<ul>`.
|
|
17
|
+
const rawHasAttrs = Object.keys(rawAttributes).length > 0;
|
|
18
|
+
const attributes = state.context.html
|
|
19
|
+
? (rawHasAttrs ? state.renderData.props : rawAttributes)
|
|
20
|
+
: rawAttributes;
|
|
10
21
|
const hasOnlyTextChildren = children.every(child => typeof child === 'string' || inlineTags.has(String(child?.[0])));
|
|
11
22
|
const hasTextSibling = children.some(child => typeof child === 'string');
|
|
12
23
|
const isBlock = textBlocks.has(String(tag));
|
|
@@ -37,7 +48,7 @@ export async function html(node, state, parent) {
|
|
|
37
48
|
for (let i = 0; i < children.length; i++) {
|
|
38
49
|
const childContent = childrenContent[i];
|
|
39
50
|
const child = children[i];
|
|
40
|
-
const isBlock = blockTags.has(String(child?.[0])) || (!inlineTags.has(String(child?.[0])) && !hasTextSibling);
|
|
51
|
+
const isBlock = typeof child !== 'string' && (blockTags.has(String(child?.[0])) || (!inlineTags.has(String(child?.[0])) && !hasTextSibling));
|
|
41
52
|
if (i > 0 && !isPrevBlock && isBlock) {
|
|
42
53
|
content += state.context.blockSeparator;
|
|
43
54
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { comarkAttributes } from "../attributes.js";
|
|
2
2
|
export function img(node, _state) {
|
|
3
3
|
const [_, attrs] = node;
|
|
4
|
-
const { title, src, alt, ...rest } = attrs;
|
|
4
|
+
const { title, src, alt = '', ...rest } = attrs;
|
|
5
5
|
const attrsString = Object.keys(rest).length > 0
|
|
6
6
|
? comarkAttributes(rest)
|
|
7
7
|
: '';
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { indent } from "../indent.js";
|
|
2
|
+
// Block elements that need explicit indentation in list items.
|
|
3
|
+
// Note: ol/ul are handled by their own handlers which manage indentation via listIndent context.
|
|
4
|
+
const blockElements = new Set(['pre', 'blockquote', 'table']);
|
|
1
5
|
export async function li(node, state) {
|
|
2
6
|
const children = node.slice(2);
|
|
3
7
|
const order = state.context.order;
|
|
@@ -10,9 +14,18 @@ export async function li(node, state) {
|
|
|
10
14
|
const input = children.shift();
|
|
11
15
|
prefix += input[1].checked || input[1][':checked'] ? '[x] ' : '[ ] ';
|
|
12
16
|
}
|
|
17
|
+
const prefixWidth = prefix.length;
|
|
13
18
|
let result = '';
|
|
14
19
|
for (const child of children) {
|
|
15
|
-
|
|
20
|
+
const rendered = await state.one(child, state, node);
|
|
21
|
+
if (Array.isArray(child) && blockElements.has(child[0])) {
|
|
22
|
+
// Block-level child: put on its own line and indent to align with list prefix
|
|
23
|
+
const indented = indent(rendered, { width: prefixWidth });
|
|
24
|
+
result = result.trimEnd() + '\n' + indented.trimEnd() + '\n';
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
result += rendered;
|
|
28
|
+
}
|
|
16
29
|
}
|
|
17
30
|
result = result.trim();
|
|
18
31
|
if (!order) {
|
|
@@ -28,7 +28,8 @@ export async function mdc(node, state, parent) {
|
|
|
28
28
|
content = content.trimEnd();
|
|
29
29
|
const attrs = attributeEntries.length > 0 ? comarkAttributes(attributes) : '';
|
|
30
30
|
if (tag === 'span') {
|
|
31
|
-
return `[${content}]${attrs}
|
|
31
|
+
return `[${content}]${attrs}`
|
|
32
|
+
+ (inline ? '' : state.context.blockSeparator);
|
|
32
33
|
}
|
|
33
34
|
const fence = ':'.repeat((state.nodeDepthInTree || 0) + 2);
|
|
34
35
|
let result = `:${tag}${content && `[${content}]`}${attrs}` + (!parent ? state.context.blockSeparator : '');
|
|
@@ -36,7 +37,7 @@ export async function mdc(node, state, parent) {
|
|
|
36
37
|
const maxInlineAttributes = state.context.maxInlineAttributes ?? 3;
|
|
37
38
|
const useYaml = hasObjectAttributes || maxInlineAttributes === 0 || attributeEntries.length > maxInlineAttributes;
|
|
38
39
|
if (useYaml) {
|
|
39
|
-
const yamlAttrs = comarkYamlAttributes(attributes);
|
|
40
|
+
const yamlAttrs = comarkYamlAttributes(attributes, state.context.blockAttributesStyle);
|
|
40
41
|
result = `${fence}${tag}\n${yamlAttrs}${content ? `\n${content}` : ''}\n${fence}` + state.context.blockSeparator;
|
|
41
42
|
}
|
|
42
43
|
else {
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { indent } from "../indent.js";
|
|
2
2
|
export async function ol(node, state) {
|
|
3
3
|
const children = node.slice(2);
|
|
4
|
-
const revert = state.applyContext({ list: true, order: 1 });
|
|
4
|
+
const revert = state.applyContext({ list: true, order: 1, listIndent: 3 });
|
|
5
5
|
let result = '';
|
|
6
6
|
for (const child of children) {
|
|
7
7
|
result += await state.one(child, state);
|
|
8
8
|
}
|
|
9
9
|
result = result.trim();
|
|
10
10
|
if (revert.list) {
|
|
11
|
-
result = '\n' + indent(result);
|
|
11
|
+
result = '\n' + indent(result, { width: revert.listIndent || 2 });
|
|
12
12
|
}
|
|
13
13
|
else {
|
|
14
14
|
result = result + state.context.blockSeparator;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// slot template
|
|
2
2
|
export async function template(node, state, parent) {
|
|
3
3
|
const [_, attrs] = node;
|
|
4
|
-
const content = (await state.flow(node, state)).
|
|
4
|
+
const content = (await state.flow(node, state)).trimEnd();
|
|
5
5
|
// Omit #default marker if this is the only slot
|
|
6
6
|
if (attrs.name === 'default') {
|
|
7
7
|
const siblings = parent ? parent.slice(2) : [];
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { indent } from "../indent.js";
|
|
2
2
|
export async function ul(node, state) {
|
|
3
3
|
const children = node.slice(2);
|
|
4
|
-
const revert = state.applyContext({ list: true, order: false });
|
|
4
|
+
const revert = state.applyContext({ list: true, order: false, listIndent: 2 });
|
|
5
5
|
let result = '';
|
|
6
6
|
for (const child of children) {
|
|
7
7
|
result += await state.one(child, state);
|
|
8
8
|
}
|
|
9
9
|
result = result.trim();
|
|
10
10
|
if (revert.list) {
|
|
11
|
-
result = '\n' + indent(result);
|
|
11
|
+
result = '\n' + indent(result, { width: revert.listIndent || 2 });
|
|
12
12
|
}
|
|
13
13
|
else {
|
|
14
14
|
result = result + state.context.blockSeparator;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
export function indent(text, { ignoreFirstLine = false, level = 1 } = {}) {
|
|
1
|
+
export function indent(text, { ignoreFirstLine = false, level = 1, width } = {}) {
|
|
2
|
+
const pad = width ? ' '.repeat(width) : ' '.repeat(level);
|
|
2
3
|
return text.split('\n').map((line, index) => {
|
|
3
4
|
if (ignoreFirstLine && index === 0) {
|
|
4
5
|
return line;
|
|
5
6
|
}
|
|
6
|
-
return line ?
|
|
7
|
+
return line ? pad + line : line;
|
|
7
8
|
}).join('\n');
|
|
8
9
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { State
|
|
2
|
-
import type { ComarkElement, ComarkNode } from 'comark';
|
|
1
|
+
import type { State } from 'comark/render';
|
|
2
|
+
import type { ComarkElement, ComarkNode, CreateContext } from 'comark';
|
|
3
3
|
/**
|
|
4
4
|
* Render a single node
|
|
5
5
|
* @param node - The node to render
|
|
@@ -9,5 +9,5 @@ import type { ComarkElement, ComarkNode } from 'comark';
|
|
|
9
9
|
*/
|
|
10
10
|
export declare function one(node: ComarkNode, state: State, parent?: ComarkElement): Promise<string>;
|
|
11
11
|
export declare function flow(node: ComarkElement, state: State, parent?: ComarkElement): Promise<string>;
|
|
12
|
-
export declare function createState(ctx?: Partial<
|
|
12
|
+
export declare function createState(ctx?: Partial<CreateContext>): State;
|
|
13
13
|
export declare const state: State;
|