@jxsuite/parser 0.28.4 → 0.29.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/dist/content.js +6 -0
- package/dist/markdown.js +7135 -7065
- package/dist/serialize.js +10977 -261
- package/package.json +4 -3
- package/src/content.ts +6 -0
- package/src/csv.ts +3 -3
- package/src/md.ts +6 -4
- package/src/serialize.ts +106 -30
- package/src/transpile.ts +47 -6
- package/src/types.ts +15 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/parser",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
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.
|
|
43
|
+
"@jxsuite/schema": "^0.29.0",
|
|
43
44
|
"glob": "^13.0.6",
|
|
44
45
|
"hast-util-from-html": "^2.0.3",
|
|
45
46
|
"hast-util-whitespace": "^3.0.0",
|
|
@@ -59,7 +60,7 @@
|
|
|
59
60
|
},
|
|
60
61
|
"devDependencies": {
|
|
61
62
|
"@happy-dom/global-registrator": "^20.10.2",
|
|
62
|
-
"@jxsuite/runtime": "^0.
|
|
63
|
+
"@jxsuite/runtime": "^0.29.0",
|
|
63
64
|
"@types/hast": "^3.0.4",
|
|
64
65
|
"@types/mdast": "^4.0.4"
|
|
65
66
|
}
|
package/src/content.ts
CHANGED
|
@@ -63,9 +63,15 @@ export function evaluateFilterRule(
|
|
|
63
63
|
return actual != null && actual !== "" && !(Array.isArray(actual) && actual.length === 0);
|
|
64
64
|
}
|
|
65
65
|
case "contains": {
|
|
66
|
+
if (Array.isArray(actual)) {
|
|
67
|
+
return actual.includes(rule.value);
|
|
68
|
+
}
|
|
66
69
|
return typeof actual === "string" && actual.includes(String(rule.value ?? ""));
|
|
67
70
|
}
|
|
68
71
|
case "not contains": {
|
|
72
|
+
if (Array.isArray(actual)) {
|
|
73
|
+
return !actual.includes(rule.value);
|
|
74
|
+
}
|
|
69
75
|
return typeof actual === "string" && !actual.includes(String(rule.value ?? ""));
|
|
70
76
|
}
|
|
71
77
|
case ">": {
|
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]
|
|
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
|
|
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(
|
|
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]
|
|
289
|
-
el.textContent = node.children[0]
|
|
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]
|
|
395
|
-
el.textContent = node.children[0]
|
|
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
|
/**
|
|
@@ -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
|
|
|
@@ -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]
|
|
1268
|
-
const children = parseInlineHtml(hMatch[2]);
|
|
1346
|
+
const depth = Number.parseInt(hMatch[1]!.slice(1), 10);
|
|
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]
|
|
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]
|
|
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
|
|
1643
|
+
const processor = (unified as unknown as () => UnifiedProcessor)()
|
|
1568
1644
|
.use(remarkGfm)
|
|
1569
1645
|
.use(remarkDirective)
|
|
1570
|
-
.use(remarkStringify, { bullet: "-", emphasis: "*", strong: "*" })
|
|
1571
|
-
|
|
1646
|
+
.use(remarkStringify, { bullet: "-", emphasis: "*", strong: "*" });
|
|
1647
|
+
const md = processor.stringify(mdast as unknown as Root);
|
|
1572
1648
|
|
|
1573
|
-
lines.push(md
|
|
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
|
|
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
|
-
|
|
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
|
|
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;
|