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.
Files changed (64) 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/html.js +12 -1
  12. package/dist/internal/stringify/handlers/img.js +1 -1
  13. package/dist/internal/stringify/handlers/li.js +14 -1
  14. package/dist/internal/stringify/handlers/math.js +1 -1
  15. package/dist/internal/stringify/handlers/mdc.js +1 -1
  16. package/dist/internal/stringify/handlers/ol.js +2 -2
  17. package/dist/internal/stringify/handlers/pre.js +1 -1
  18. package/dist/internal/stringify/handlers/template.js +1 -1
  19. package/dist/internal/stringify/handlers/ul.js +2 -2
  20. package/dist/internal/stringify/indent.d.ts +2 -1
  21. package/dist/internal/stringify/indent.js +3 -2
  22. package/dist/internal/stringify/state.d.ts +3 -3
  23. package/dist/internal/stringify/state.js +71 -15
  24. package/dist/internal/yaml.d.ts +2 -1
  25. package/dist/internal/yaml.js +3 -1
  26. package/dist/parse.js +13 -2
  27. package/dist/plugins/alert.js +1 -1
  28. package/dist/plugins/binding.d.ts +20 -0
  29. package/dist/plugins/binding.js +61 -0
  30. package/dist/plugins/breaks.d.ts +2 -0
  31. package/dist/plugins/breaks.js +34 -0
  32. package/dist/plugins/footnotes.d.ts +61 -0
  33. package/dist/plugins/footnotes.js +187 -0
  34. package/dist/plugins/highlight.js +6 -4
  35. package/dist/plugins/json-render.d.ts +1 -1
  36. package/dist/plugins/json-render.js +3 -3
  37. package/dist/plugins/punctuation.d.ts +67 -0
  38. package/dist/plugins/punctuation.js +236 -0
  39. package/dist/plugins/security.js +1 -1
  40. package/dist/render.d.ts +2 -1
  41. package/dist/render.js +3 -1
  42. package/dist/types.d.ts +71 -16
  43. package/dist/utils/index.d.ts +9 -0
  44. package/dist/utils/index.js +24 -0
  45. package/dist/vite.d.ts +1 -0
  46. package/dist/vite.js +1 -0
  47. package/package.json +29 -24
  48. package/skills/comark/AGENTS.md +261 -0
  49. package/skills/comark/SKILL.md +489 -0
  50. package/skills/comark/references/markdown-syntax.md +599 -0
  51. package/skills/comark/references/parsing-ast.md +378 -0
  52. package/skills/comark/references/rendering-react.md +445 -0
  53. package/skills/comark/references/rendering-svelte.md +453 -0
  54. package/skills/comark/references/rendering-vue.md +462 -0
  55. package/skills/skills/comark/AGENTS.md +261 -0
  56. package/skills/skills/comark/SKILL.md +489 -0
  57. package/skills/skills/comark/references/markdown-syntax.md +599 -0
  58. package/skills/skills/comark/references/parsing-ast.md +378 -0
  59. package/skills/skills/comark/references/rendering-react.md +445 -0
  60. package/skills/skills/comark/references/rendering-svelte.md +453 -0
  61. package/skills/skills/comark/references/rendering-vue.md +462 -0
  62. package/skills/skills/migrate-mdc-to-comark/SKILL.md +191 -0
  63. package/dist/utils/serialized-task.d.ts +0 -1
  64. 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](./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
- underscoreCount++;
213
- // Track __ positions (for bold)
214
- if (i + 1 < len && line[i + 1] === '_') {
215
- doubleUnderscorePositions.push(i);
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>): string;
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
- return Object.entries(attributes)
36
- .map(([key, value]) => {
103
+ const parts = [];
104
+ for (const [key, value] of Object.entries(attributes)) {
37
105
  if (key.startsWith(':')) {
38
106
  if (value === 'true') {
39
- return key.slice(1);
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
- return `${key.slice(1)}="${value}"`;
114
+ parts.push(`${key.slice(1)}="${value}"`);
115
+ continue;
42
116
  }
43
- if (value === 'true')
44
- return key;
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
- return `${key}="${JSON.stringify(value).replace(/"/g, '\\"')}"`;
124
+ parts.push(`${key}="${JSON.stringify(value).replace(/"/g, '\\"')}"`);
125
+ continue;
47
126
  }
48
- return `${key}="${value}"`;
49
- })
50
- .join(' ');
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
- return `---\n${stringifyYaml(normalized).trim()}\n---`;
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
  }
@@ -7,5 +7,8 @@ export async function a(node, state) {
7
7
  ? comarkAttributes(rest)
8
8
  : '';
9
9
  const content = await state.flow(node, state);
10
+ if (content === href && !attrsString) {
11
+ return `<${href}>`;
12
+ }
10
13
  return `[${content}](${href})${attrsString}`;
11
14
  }
@@ -1,5 +1,5 @@
1
1
  import { comarkAttributes } from "../attributes.js";
2
- import { textContent } from 'comark/utils';
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
@@ -1,4 +1,4 @@
1
- import { textContent } from 'comark/utils';
1
+ import { textContent } from "../../../utils/index.js";
2
2
  export function del(node, _) {
3
3
  return `~~${textContent(node)}~~`;
4
4
  }
@@ -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 { $ = {}, ...attributes } = attr;
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));
@@ -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
- result += await state.one(child, state, node);
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) {
@@ -1,4 +1,4 @@
1
- import { textContent } from 'comark/utils';
1
+ import { textContent } from "../../../utils/index.js";
2
2
  export function math(node, state, parent) {
3
3
  const content = textContent(node);
4
4
  if (parent?.some((child, index) => index > 1 && typeof child === 'string')) {
@@ -37,7 +37,7 @@ export async function mdc(node, state, parent) {
37
37
  const maxInlineAttributes = state.context.maxInlineAttributes ?? 3;
38
38
  const useYaml = hasObjectAttributes || maxInlineAttributes === 0 || attributeEntries.length > maxInlineAttributes;
39
39
  if (useYaml) {
40
- const yamlAttrs = comarkYamlAttributes(attributes);
40
+ const yamlAttrs = comarkYamlAttributes(attributes, state.context.blockAttributesStyle);
41
41
  result = `${fence}${tag}\n${yamlAttrs}${content ? `\n${content}` : ''}\n${fence}` + state.context.blockSeparator;
42
42
  }
43
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,4 +1,4 @@
1
- import { textContent } from 'comark/utils';
1
+ import { textContent } from "../../../utils/index.js";
2
2
  export function pre(node, state) {
3
3
  const [_, attributes, ...children] = node;
4
4
  const codeClasses = children[0]?.[1]?.class;
@@ -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)).trim();
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,4 +1,5 @@
1
- export declare function indent(text: string, { ignoreFirstLine, level }?: {
1
+ export declare function indent(text: string, { ignoreFirstLine, level, width }?: {
2
2
  ignoreFirstLine?: boolean;
3
3
  level?: number;
4
+ width?: number;
4
5
  }): string;
@@ -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 ? ' '.repeat(level) + line : line;
7
+ return line ? pad + line : line;
7
8
  }).join('\n');
8
9
  }
@@ -1,5 +1,5 @@
1
- import type { State, Context } from 'comark/render';
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<Context>): State;
12
+ export declare function createState(ctx?: Partial<CreateContext>): State;
13
13
  export declare const state: State;