@jxsuite/parser 0.28.5 → 0.30.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/parser",
3
- "version": "0.28.5",
3
+ "version": "0.30.1",
4
4
  "description": "Jx markdown parser and external class integration",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -34,12 +34,13 @@
34
34
  "scripts": {
35
35
  "prepare": "bun build src/content.ts src/markdown.ts src/csv.ts src/serialize.ts --outdir=dist --format=esm --target=node",
36
36
  "build": "bun run prepare",
37
+ "build:metafile": "bun build src/content.ts src/markdown.ts src/csv.ts src/serialize.ts --outdir=dist --format=esm --target=node --metafile=metafile.json",
37
38
  "test": "bun test --isolate",
38
39
  "test:coverage": "bun test --isolate --coverage",
39
40
  "upgrade": "bunx npm-check-updates -u && bun install"
40
41
  },
41
42
  "dependencies": {
42
- "@jxsuite/schema": "^0.28.5",
43
+ "@jxsuite/schema": "^0.30.1",
43
44
  "glob": "^13.0.6",
44
45
  "hast-util-from-html": "^2.0.3",
45
46
  "hast-util-whitespace": "^3.0.0",
@@ -58,8 +59,8 @@
58
59
  "yaml": "^2.9.0"
59
60
  },
60
61
  "devDependencies": {
61
- "@happy-dom/global-registrator": "^20.10.2",
62
- "@jxsuite/runtime": "^0.28.5",
62
+ "@happy-dom/global-registrator": "^20.10.6",
63
+ "@jxsuite/runtime": "^0.30.1",
63
64
  "@types/hast": "^3.0.4",
64
65
  "@types/mdast": "^4.0.4"
65
66
  }
package/src/csv.ts CHANGED
@@ -78,12 +78,12 @@ export function parseCSV(csv: string): Record<string, string>[] {
78
78
  return fields;
79
79
  };
80
80
 
81
- const headers = parseRow(lines[0]);
81
+ const headers = parseRow(lines[0]!);
82
82
  for (let i = 1; i < lines.length; i++) {
83
- const fields = parseRow(lines[i]);
83
+ const fields = parseRow(lines[i]!);
84
84
  const obj: Record<string, string> = {};
85
85
  for (let j = 0; j < headers.length; j++) {
86
- obj[headers[j].trim()] = fields[j]?.trim() ?? "";
86
+ obj[headers[j]!.trim()] = fields[j]?.trim() ?? "";
87
87
  }
88
88
  rows.push(obj);
89
89
  }
package/src/md.ts CHANGED
@@ -21,7 +21,7 @@ import { readFileSync } from "node:fs";
21
21
  import { basename, extname, resolve as resolvePath } from "node:path";
22
22
  import { globSync } from "glob";
23
23
  import { mdastNodeToJx } from "./transpile.ts";
24
- import type { MarkdownFileResult, MdastNode, TocEntry } from "./types.ts";
24
+ import type { MarkdownFileResult, MdastNode, TocEntry, UnifiedProcessor } from "./types.ts";
25
25
  import type { JxElement } from "@jxsuite/schema/types";
26
26
 
27
27
  // ─── Tree utilities (inline to avoid Bun ESM resolution issues with unist-util-*) ──
@@ -153,7 +153,7 @@ export function processMarkdown(
153
153
  directiveOptions?: unknown;
154
154
  } = {},
155
155
  ) {
156
- let processor = unified()
156
+ let processor = (unified as unknown as () => UnifiedProcessor)()
157
157
  .use(remarkParse)
158
158
  .use(remarkFrontmatter, ["yaml"])
159
159
  .use(remarkParseFrontmatter)
@@ -175,7 +175,7 @@ export function processMarkdown(
175
175
  const excerpt = extractExcerpt(tree);
176
176
  const slug = basename(filePath, extname(filePath));
177
177
 
178
- const bodyNodes = tree.children.filter(
178
+ const bodyNodes = tree.children!.filter(
179
179
  (n: MdastNode) => n.type !== "yaml" && n.type !== "toml",
180
180
  ) as MdastNode[];
181
181
  const $children = bodyNodes.map((n: MdastNode) => mdastNodeToJx(n)).filter(Boolean) as (
@@ -280,7 +280,9 @@ export class MarkdownCollection {
280
280
  const resolved = basePath ? resolvePath(basePath, src) : src;
281
281
  // Normalize to forward slashes — glob requires POSIX paths on all platforms
282
282
  const pattern = resolved.split("\\").join("/");
283
- const files = globSync(pattern, { absolute: true });
283
+ const files = (globSync as (p: string, o: { absolute: boolean }) => string[])(pattern, {
284
+ absolute: true,
285
+ });
284
286
 
285
287
  const results = files.map((filePath: string) => {
286
288
  const source = readFileSync(filePath, "utf8");
package/src/serialize.ts CHANGED
@@ -25,6 +25,7 @@ import remarkDirective from "remark-directive";
25
25
  import remarkGfm from "remark-gfm";
26
26
  import { stringify as stringifyYaml } from "yaml";
27
27
  import { htmlToJx } from "./html-to-jx.ts";
28
+ import { expandDotPaths } from "./transpile.ts";
28
29
  import type {
29
30
  JsonValue,
30
31
  JxAttributeValue,
@@ -33,7 +34,7 @@ import type {
33
34
  JxMutableNode,
34
35
  JxStateDefinition,
35
36
  } from "@jxsuite/schema/types";
36
- import type { MdastNode } from "./types.ts";
37
+ import type { MdastNode, UnifiedProcessor } from "./types.ts";
37
38
  import type { Root } from "mdast";
38
39
 
39
40
  /** Static text content of a node — bound (`$ref`) text has no serializable form. */
@@ -285,8 +286,8 @@ function convertMdastNode(node: MdastNode): JxElement | null {
285
286
  | string
286
287
  )[];
287
288
  const flattenOrChildren = () => {
288
- if (node.children?.length === 1 && node.children[0].type === "text") {
289
- el.textContent = node.children[0].value ?? null;
289
+ if (node.children?.length === 1 && node.children[0]!.type === "text") {
290
+ el.textContent = node.children[0]!.value ?? null;
290
291
  } else if (node.children?.length) {
291
292
  el.children = childNodes();
292
293
  }
@@ -386,13 +387,18 @@ function convertMdastNode(node: MdastNode): JxElement | null {
386
387
  }
387
388
 
388
389
  function convertDirective(node: MdastNode): JxElement {
390
+ // Prototype directive (e.g. `:::Array`) → `{ $prototype: name, ... }`, tagName dropped.
391
+ if (node.name && PROTOTYPE_DIRECTIVE_NAMES.has(node.name)) {
392
+ return prototypeDirectiveToJx(node);
393
+ }
394
+
389
395
  const el: JxElement = { tagName: node.name ?? "div" };
390
396
  if (node.attributes && Object.keys(node.attributes).length > 0) {
391
397
  el.attributes = { ...node.attributes };
392
398
  }
393
399
  if (node.type === "textDirective") {
394
- if (node.children?.length === 1 && node.children[0].type === "text") {
395
- el.textContent = node.children[0].value ?? null;
400
+ if (node.children?.length === 1 && node.children[0]!.type === "text") {
401
+ el.textContent = node.children[0]!.value ?? null;
396
402
  } else if (node.children?.length) {
397
403
  el.children = node.children.flatMap((n) => convertMdastNode(n)).filter(Boolean) as (
398
404
  | JxElement
@@ -408,6 +414,34 @@ function convertDirective(node: MdastNode): JxElement {
408
414
  return el;
409
415
  }
410
416
 
417
+ /**
418
+ * Roundtrip md → Jx for a prototype directive (e.g. `:::Array`): name → `$prototype` (tagName
419
+ * dropped), dot-path attributes expanded (items/filter/sort), single nested child → `map`.
420
+ */
421
+ function prototypeDirectiveToJx(node: MdastNode): JxElement {
422
+ const el: JxElement = { $prototype: node.name as string };
423
+ if (node.attributes && Object.keys(node.attributes).length > 0) {
424
+ const expanded = expandDotPaths(node.attributes);
425
+ for (const [key, value] of Object.entries(expanded)) {
426
+ if (key === "$prototype") {
427
+ continue;
428
+ }
429
+ el[key] = value as JsonValue;
430
+ }
431
+ }
432
+ if (node.children?.length) {
433
+ const children = node.children.flatMap((n) => convertMdastNode(n)).filter(Boolean) as (
434
+ | JxElement
435
+ | string
436
+ )[];
437
+ const template = children.find((c) => c != null && typeof c === "object");
438
+ if (template) {
439
+ el.map = template as JxElement;
440
+ }
441
+ }
442
+ return el;
443
+ }
444
+
411
445
  // ─── Roundtrip: Jx → mdast ───────────────────────────────────────────────────
412
446
 
413
447
  /**
@@ -498,7 +532,7 @@ function convertJxNode(
498
532
  case "heading": {
499
533
  return {
500
534
  children: inline(el),
501
- depth: Number.parseInt(tag.slice(1), 10),
535
+ depth: Math.trunc(Number(tag.slice(1))),
502
536
  type: "heading",
503
537
  };
504
538
  }
@@ -550,7 +584,7 @@ function convertJxNode(
550
584
  .filter(Boolean) as MdastNode[],
551
585
  ordered: tag === "ol",
552
586
  spread: false,
553
- start: tag === "ol" ? Number.parseInt(el.attributes?.start as string, 10) || 1 : null,
587
+ start: tag === "ol" ? Math.trunc(Number(el.attributes?.start as string)) || 1 : null,
554
588
  type: "list",
555
589
  };
556
590
  }
@@ -656,6 +690,12 @@ const JX_DOLLAR_KEYS = new Set([
656
690
  "$elements",
657
691
  ]);
658
692
 
693
+ /**
694
+ * `$prototype` element types that serialize as a directive named after the prototype (no tagName),
695
+ * e.g. `:::Array`. Mirrors the set in transpile.ts.
696
+ */
697
+ const PROTOTYPE_DIRECTIVE_NAMES = new Set(["Array"]);
698
+
659
699
  const JX_ANNOTATION_KEYS = new Set(["$title", "$description"]);
660
700
 
661
701
  function collectDirectiveAttrs(el: JxElement) {
@@ -688,6 +728,16 @@ function convertToDirective(
688
728
  isBlock: boolean,
689
729
  allowlist: ReadonlySet<string>,
690
730
  ): MdastNode {
731
+ // Prototype pseudo-element (e.g. Array repeater) with no tagName → directive named after the
732
+ // Prototype; the `map` template is the directive body, items/filter/sort are attributes.
733
+ if (
734
+ !el.tagName &&
735
+ typeof el.$prototype === "string" &&
736
+ PROTOTYPE_DIRECTIVE_NAMES.has(el.$prototype)
737
+ ) {
738
+ return prototypeToDirective(el, isBlock, allowlist);
739
+ }
740
+
691
741
  const tag = (el.tagName as string) ?? "div";
692
742
  const attrs = collectDirectiveAttrs(el);
693
743
 
@@ -749,6 +799,35 @@ function convertToDirective(
749
799
  };
750
800
  }
751
801
 
802
+ /**
803
+ * Serialize a tagName-less `$prototype` node (e.g. an Array repeater) to a directive named after
804
+ * its prototype. items/filter/sort (and any other scalar props) become attributes; the `map`
805
+ * template is the single nested child. `$prototype` (carried by the name) and `map` (the body) are
806
+ * omitted from the attributes.
807
+ */
808
+ function prototypeToDirective(
809
+ el: JxElement,
810
+ isBlock: boolean,
811
+ allowlist: ReadonlySet<string>,
812
+ ): MdastNode {
813
+ const propsObj: Record<string, unknown> = {};
814
+ for (const [key, value] of Object.entries(el)) {
815
+ if (key === "$prototype" || key === "map" || key === "tagName") {
816
+ continue;
817
+ }
818
+ propsObj[key] = value;
819
+ }
820
+ const attrs = collapsePropsToAttrMap(propsObj);
821
+ const mapNode = el.map ? convertJxNode(el.map as JxElement, isBlock, allowlist) : null;
822
+ const children = mapNode ? [mapNode] : [];
823
+ return {
824
+ attributes: attrs,
825
+ children,
826
+ name: el.$prototype as string,
827
+ type: isBlock ? "containerDirective" : "textDirective",
828
+ };
829
+ }
830
+
752
831
  function collapsePropsToAttrMap(propsObj: Record<string, unknown>) {
753
832
  const result: Record<string, string> = {};
754
833
 
@@ -853,7 +932,7 @@ function nodeToMdast(
853
932
 
854
933
  switch (mdastType) {
855
934
  case "heading": {
856
- const depth = Number.parseInt(tag.slice(1), 10);
935
+ const depth = Math.trunc(Number(tag.slice(1)));
857
936
  const children =
858
937
  text != null ? [{ type: "text", value: text }] : exportChildren(node, ctx, scope);
859
938
  return [{ children, depth, type: "heading" }];
@@ -1264,14 +1343,14 @@ function splitHtmlBlocks(html: string) {
1264
1343
  function parseHtmlElement(html: string) {
1265
1344
  const hMatch = html.match(/^<(h[1-6])(?:\s[^>]*)?>(.+?)<\/\1>$/is);
1266
1345
  if (hMatch) {
1267
- const depth = Number.parseInt(hMatch[1].slice(1), 10);
1268
- const children = parseInlineHtml(hMatch[2]);
1346
+ const depth = Math.trunc(Number(hMatch[1]!.slice(1)));
1347
+ const children = parseInlineHtml(hMatch[2]!);
1269
1348
  return [{ children, depth, type: "heading" }];
1270
1349
  }
1271
1350
 
1272
1351
  const pMatch = html.match(/^<p(?:\s[^>]*)?>(.+?)<\/p>$/is);
1273
1352
  if (pMatch) {
1274
- const children = parseInlineHtml(pMatch[1]);
1353
+ const children = parseInlineHtml(pMatch[1]!);
1275
1354
  if (children.length === 0) {
1276
1355
  return null;
1277
1356
  }
@@ -1287,13 +1366,13 @@ function parseHtmlElement(html: string) {
1287
1366
  );
1288
1367
  if (preMatch) {
1289
1368
  const lang = preMatch[1] ?? null;
1290
- const value = decodeHtmlEntities(preMatch[2]);
1369
+ const value = decodeHtmlEntities(preMatch[2]!);
1291
1370
  return [{ lang, type: "code", value }];
1292
1371
  }
1293
1372
 
1294
1373
  const bqMatch = html.match(/^<blockquote(?:\s[^>]*)?>([^]*?)<\/blockquote>$/is);
1295
1374
  if (bqMatch) {
1296
- const inner = htmlToMdast(bqMatch[1]);
1375
+ const inner = htmlToMdast(bqMatch[1]!);
1297
1376
  const children = inner.map((c) =>
1298
1377
  c.type === "text" ? { children: [c], type: "paragraph" } : c,
1299
1378
  );
@@ -1302,7 +1381,7 @@ function parseHtmlElement(html: string) {
1302
1381
 
1303
1382
  const ulMatch = html.match(/^<ul(?:\s[^>]*)?>([^]*?)<\/ul>$/is);
1304
1383
  if (ulMatch) {
1305
- const items = parseListItems(ulMatch[1]);
1384
+ const items = parseListItems(ulMatch[1]!);
1306
1385
  if (items.length === 0) {
1307
1386
  return null;
1308
1387
  }
@@ -1311,7 +1390,7 @@ function parseHtmlElement(html: string) {
1311
1390
 
1312
1391
  const olMatch = html.match(/^<ol(?:\s[^>]*)?>([^]*?)<\/ol>$/is);
1313
1392
  if (olMatch) {
1314
- const items = parseListItems(olMatch[1]);
1393
+ const items = parseListItems(olMatch[1]!);
1315
1394
  if (items.length === 0) {
1316
1395
  return null;
1317
1396
  }
@@ -1320,14 +1399,14 @@ function parseHtmlElement(html: string) {
1320
1399
 
1321
1400
  const tableMatch = html.match(/^<table(?:\s[^>]*)?>([^]*?)<\/table>$/is);
1322
1401
  if (tableMatch) {
1323
- return parseHtmlTable(tableMatch[1]);
1402
+ return parseHtmlTable(tableMatch[1]!);
1324
1403
  }
1325
1404
 
1326
1405
  const wrapperMatch = html.match(
1327
1406
  /^<(?:div|section|article|aside|figure|nav|header|footer|main)(?:\s[^>]*)?>([^]*?)<\/(?:div|section|article|aside|figure|nav|header|footer|main)>$/is,
1328
1407
  );
1329
1408
  if (wrapperMatch) {
1330
- return htmlToMdast(wrapperMatch[1]);
1409
+ return htmlToMdast(wrapperMatch[1]!);
1331
1410
  }
1332
1411
 
1333
1412
  const text = stripHtmlTags(html).trim();
@@ -1382,7 +1461,7 @@ function parseInlineHtml(html: string) {
1382
1461
 
1383
1462
  const openMatch = html.slice(tagStart).match(/^<(a|em|strong|del|code|b|i|s)(\s[^>]*)?>/);
1384
1463
  if (openMatch) {
1385
- const tag = openMatch[1].toLowerCase();
1464
+ const tag = openMatch[1]!.toLowerCase();
1386
1465
  const attrs = openMatch[2] ?? "";
1387
1466
  const innerStart = tagStart + openMatch[0].length;
1388
1467
  const closeTag = `</${tag}>`;
@@ -1477,7 +1556,7 @@ function parseListItems(html: string) {
1477
1556
  const liPattern = /<li(?:\s[^>]*)?>([\s\S]*?)<\/li>/gi;
1478
1557
  let m;
1479
1558
  while ((m = liPattern.exec(html)) !== null) {
1480
- const inner = m[1].trim();
1559
+ const inner = m[1]!.trim();
1481
1560
  const innerNodes = /<(?:p|ul|ol|blockquote|pre)[\s>]/i.test(inner)
1482
1561
  ? htmlToMdast(inner)
1483
1562
  : [{ children: parseInlineHtml(inner), type: "paragraph" }];
@@ -1494,8 +1573,8 @@ function parseHtmlTable(html: string) {
1494
1573
  const cellPattern = /<(?:th|td)(?:\s[^>]*)?>([\s\S]*?)<\/(?:th|td)>/gi;
1495
1574
  const cells: MdastNode[] = [];
1496
1575
  let c;
1497
- while ((c = cellPattern.exec(m[1])) !== null) {
1498
- cells.push({ children: parseInlineHtml(c[1]), type: "tableCell" });
1576
+ while ((c = cellPattern.exec(m[1]!)) !== null) {
1577
+ cells.push({ children: parseInlineHtml(c[1]!), type: "tableCell" });
1499
1578
  }
1500
1579
  if (cells.length > 0) {
1501
1580
  rows.push({ children: cells, type: "tableRow" });
@@ -1555,22 +1634,19 @@ function serializeRoundtrip(doc: JxDocument, opts: SerializeOptions): string {
1555
1634
  }
1556
1635
 
1557
1636
  if (Object.keys(frontmatter).length > 0) {
1558
- lines.push("---");
1559
- lines.push(stringifyYaml(frontmatter).trim());
1560
- lines.push("---");
1561
- lines.push("");
1637
+ lines.push("---", stringifyYaml(frontmatter).trim(), "---", "");
1562
1638
  }
1563
1639
  }
1564
1640
 
1565
1641
  if (Array.isArray(doc.children) && doc.children.length > 0) {
1566
1642
  const mdast = jxToMdast(doc as JxElement, opts);
1567
- const md = unified()
1643
+ const processor = (unified as unknown as () => UnifiedProcessor)()
1568
1644
  .use(remarkGfm)
1569
1645
  .use(remarkDirective)
1570
- .use(remarkStringify, { bullet: "-", emphasis: "*", strong: "*" })
1571
- .stringify(mdast as unknown as Root);
1646
+ .use(remarkStringify, { bullet: "-", emphasis: "*", strong: "*" });
1647
+ const md = processor.stringify(mdast as unknown as Root);
1572
1648
 
1573
- lines.push(md as string);
1649
+ lines.push(md);
1574
1650
  }
1575
1651
 
1576
1652
  return `${lines
@@ -1586,7 +1662,7 @@ function serializeExport(doc: JxDocument, opts: SerializeOptions): string {
1586
1662
 
1587
1663
  const ctx: ExportContext = {
1588
1664
  buildScope: opts.buildScope,
1589
- componentDefs: opts.componentDefs ?? new Map(),
1665
+ componentDefs: opts.componentDefs ?? new Map<string, JxElement>(),
1590
1666
  evaluateTemplate: opts.evaluateTemplate,
1591
1667
  };
1592
1668
 
@@ -1623,15 +1699,15 @@ function serializeExport(doc: JxDocument, opts: SerializeOptions): string {
1623
1699
  type: "root",
1624
1700
  } as unknown as Root;
1625
1701
 
1626
- const md = unified()
1702
+ const processor = (unified as unknown as () => UnifiedProcessor)()
1627
1703
  .use(remarkGfm)
1628
1704
  .use(remarkStringify, {
1629
1705
  bullet: "-",
1630
1706
  emphasis: "*",
1631
1707
  setext: false,
1632
1708
  strong: "*",
1633
- })
1634
- .stringify(mdast);
1709
+ });
1710
+ const md = processor.stringify(mdast);
1635
1711
 
1636
1712
  return `${md.replaceAll(/\n{3,}/g, "\n\n").trim()}\n`;
1637
1713
  }
package/src/transpile.ts CHANGED
@@ -18,10 +18,10 @@ import remarkParseFrontmatter from "remark-parse-frontmatter";
18
18
  import remarkGfm from "remark-gfm";
19
19
  import remarkDirective from "remark-directive";
20
20
  import { htmlToJx } from "./html-to-jx.ts";
21
- import type { MdastNode } from "./types.ts";
21
+ import type { MdastNode, UnifiedProcessor } from "./types.ts";
22
22
  import type { JsonValue, JxAttributeValue, JxDocument, JxElement } from "@jxsuite/schema/types";
23
23
 
24
- export { htmlToJx };
24
+ export { htmlToJx } from "./html-to-jx.ts";
25
25
 
26
26
  // ─── Dot-path expansion ─────────────────────────────────────────────────────
27
27
 
@@ -31,6 +31,12 @@ export { htmlToJx };
31
31
  */
32
32
  const JX_DOLLAR_KEYS = new Set(["prototype", "ref", "component", "props", "switch", "elements"]);
33
33
 
34
+ /**
35
+ * `$prototype` element types that serialize as a directive named after the prototype (no tagName),
36
+ * e.g. `:::Array`. On parse the synthetic tagName is dropped and `$prototype` is restored.
37
+ */
38
+ const PROTOTYPE_DIRECTIVE_NAMES = new Set(["Array"]);
39
+
34
40
  /**
35
41
  * Annotation keys written as `--key` in markdown directives, mapped to `$key` in JX JSON. These use
36
42
  * `--` prefix to avoid collision with HTML attributes like `title`.
@@ -88,7 +94,7 @@ export function expandDotPaths(attrs: Record<string, string>) {
88
94
  const segments = key.split(".");
89
95
  let target = result;
90
96
  for (let i = 0; i < segments.length - 1; i++) {
91
- const seg = jxKey(segments[i]);
97
+ const seg = jxKey(segments[i]!);
92
98
  if (!(seg in target) || typeof target[seg] !== "object") {
93
99
  target[seg] = {};
94
100
  }
@@ -222,7 +228,7 @@ export function isJxMarkdown(source: string) {
222
228
  if (!fmMatch) {
223
229
  return false;
224
230
  }
225
- return /^tagName:\s*.+-.+/m.test(fmMatch[1]);
231
+ return /^tagName:\s*.+-.+/m.test(fmMatch[1]!);
226
232
  }
227
233
 
228
234
  // ─── Transpiler ─────────────────────────────────────────────────────────────
@@ -472,6 +478,11 @@ export function mdastNodeToJx(node: MdastNode) {
472
478
  * @returns {JxElement}
473
479
  */
474
480
  function directiveToJx(node: MdastNode) {
481
+ // Prototype directive (e.g. `:::Array`) → `{ $prototype: name, ... }` with no tagName.
482
+ if (PROTOTYPE_DIRECTIVE_NAMES.has(node.name as string)) {
483
+ return prototypeDirectiveToJx(node);
484
+ }
485
+
475
486
  const el: JxElement = { tagName: node.name as string };
476
487
 
477
488
  if (node.attributes && Object.keys(node.attributes).length > 0) {
@@ -595,6 +606,36 @@ function directiveToJx(node: MdastNode) {
595
606
  return el;
596
607
  }
597
608
 
609
+ /**
610
+ * Convert a prototype directive (e.g. `:::Array`) to a tagName-less `$prototype` node. Attributes
611
+ * (items/filter/sort, dot-path expanded) become element-level props; the single nested child is the
612
+ * `map` template.
613
+ *
614
+ * @param {MdastNode} node
615
+ * @returns {JxElement}
616
+ */
617
+ function prototypeDirectiveToJx(node: MdastNode) {
618
+ const el: JxElement = { $prototype: node.name as string };
619
+
620
+ if (node.attributes && Object.keys(node.attributes).length > 0) {
621
+ const expanded = expandDotPaths(node.attributes);
622
+ for (const [key, value] of Object.entries(expanded)) {
623
+ // The prototype is carried by the directive name; ignore any redundant attribute form.
624
+ if (key === "$prototype") {
625
+ continue;
626
+ }
627
+ el[key] = value as JsonValue;
628
+ }
629
+ }
630
+
631
+ const children = convertChildren(node.children ?? []);
632
+ const template = children.find((c) => c != null && typeof c === "object");
633
+ if (template) {
634
+ el.map = template as JxElement;
635
+ }
636
+ return el;
637
+ }
638
+
598
639
  /**
599
640
  * Convert an array of mdast children to Jx elements/strings.
600
641
  *
@@ -621,7 +662,7 @@ export function convertChildren(children: MdastNode[]) {
621
662
  * @returns {object} Complete Jx JSON document
622
663
  */
623
664
  export function transpileJxMarkdown(source: string) {
624
- const processor = unified()
665
+ const processor = (unified as unknown as () => UnifiedProcessor)()
625
666
  .use(remarkParse)
626
667
  .use(remarkFrontmatter, ["yaml"])
627
668
  .use(remarkParseFrontmatter)
@@ -640,7 +681,7 @@ export function transpileJxMarkdown(source: string) {
640
681
  doc[key] = value;
641
682
  }
642
683
 
643
- const bodyNodes = tree.children.filter(
684
+ const bodyNodes = tree.children!.filter(
644
685
  (n: MdastNode) => n.type !== "yaml" && n.type !== "toml",
645
686
  ) as unknown as MdastNode[];
646
687
 
package/src/types.ts CHANGED
@@ -54,6 +54,21 @@ export interface MdastNode {
54
54
  align?: (string | null)[] | null;
55
55
  }
56
56
 
57
+ /**
58
+ * Minimal structural type for the unified() processor chain.
59
+ *
60
+ * The unified/remark packages ship ESM-only types that oxlint's type-aware engine cannot resolve
61
+ * under `moduleResolution: bundler` (it sees them as `any`), even though tsgo resolves them
62
+ * correctly. We cast the `unified()` result to this hand-written surface once, at the construction
63
+ * boundary, so the rest of the pipeline is properly typed instead of `any`.
64
+ */
65
+ export interface UnifiedProcessor {
66
+ use: (plugin: unknown, ...options: unknown[]) => UnifiedProcessor;
67
+ parse: (source: string) => MdastNode;
68
+ runSync: (tree: MdastNode, file?: unknown) => MdastNode;
69
+ stringify: (tree: unknown) => string;
70
+ }
71
+
57
72
  export interface HastNode {
58
73
  type: string;
59
74
  tagName?: string;