@jxsuite/parser 0.28.0 → 0.28.3
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 +32 -16
- package/dist/csv.js +15 -11
- package/dist/markdown.js +516 -336
- package/dist/serialize.js +379 -254
- package/package.json +3 -3
- package/src/content.ts +41 -20
- package/src/csv.ts +18 -10
- package/src/html-to-jx.ts +23 -9
- package/src/markdown.ts +19 -10
- package/src/md.ts +57 -34
- package/src/serialize.ts +401 -246
- package/src/transpile.ts +114 -53
- package/src/types.ts +1 -3
package/src/serialize.ts
CHANGED
|
@@ -19,22 +19,31 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { unified } from "unified";
|
|
22
|
+
import { isRef } from "@jxsuite/schema/guards";
|
|
22
23
|
import remarkStringify from "remark-stringify";
|
|
23
24
|
import remarkDirective from "remark-directive";
|
|
24
25
|
import remarkGfm from "remark-gfm";
|
|
25
26
|
import { stringify as stringifyYaml } from "yaml";
|
|
26
27
|
import { htmlToJx } from "./html-to-jx.ts";
|
|
27
28
|
import type {
|
|
28
|
-
|
|
29
|
+
JsonValue,
|
|
30
|
+
JxAttributeValue,
|
|
29
31
|
JxDocument,
|
|
30
|
-
|
|
32
|
+
JxElement,
|
|
31
33
|
JxMutableNode,
|
|
34
|
+
JxStateDefinition,
|
|
32
35
|
} from "@jxsuite/schema/types";
|
|
33
36
|
import type { MdastNode } from "./types.ts";
|
|
37
|
+
import type { Root } from "mdast";
|
|
38
|
+
|
|
39
|
+
/** Static text content of a node — bound (`$ref`) text has no serializable form. */
|
|
40
|
+
function textOf(el: JxElement | undefined): string | undefined {
|
|
41
|
+
return el && typeof el.textContent === "string" ? el.textContent : undefined;
|
|
42
|
+
}
|
|
34
43
|
|
|
35
44
|
// ─── Markdown element sets ──────────────────────────────────────────────────
|
|
36
45
|
// Source of truth for the `$studio.elements` block in Markdown.class.json — a parser
|
|
37
|
-
//
|
|
46
|
+
// Test asserts the two stay in sync.
|
|
38
47
|
|
|
39
48
|
export const MD_ELEMENTS = {
|
|
40
49
|
block: [
|
|
@@ -59,33 +68,33 @@ export const MD_ELEMENTS = {
|
|
|
59
68
|
"td",
|
|
60
69
|
],
|
|
61
70
|
inline: ["em", "strong", "del", "code", "a", "img", "br"],
|
|
62
|
-
void: ["hr", "br", "img"],
|
|
63
|
-
textOnly: ["code"],
|
|
64
71
|
nesting: {
|
|
65
|
-
_root: { block: true,
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
_root: { block: true, directive: true, inline: false },
|
|
73
|
+
a: { block: false, directive: false, inline: true },
|
|
74
|
+
blockquote: { block: true, directive: true, inline: false },
|
|
75
|
+
del: { block: false, directive: false, inline: true },
|
|
76
|
+
em: { block: false, directive: false, inline: true },
|
|
77
|
+
h1: { block: false, directive: false, inline: true },
|
|
78
|
+
h2: { block: false, directive: false, inline: true },
|
|
79
|
+
h3: { block: false, directive: false, inline: true },
|
|
80
|
+
h4: { block: false, directive: false, inline: true },
|
|
81
|
+
h5: { block: false, directive: false, inline: true },
|
|
82
|
+
h6: { block: false, directive: false, inline: true },
|
|
83
|
+
li: { block: true, directive: true, inline: true },
|
|
75
84
|
ol: { only: ["li"] },
|
|
76
|
-
|
|
85
|
+
p: { block: false, directive: true, inline: true },
|
|
77
86
|
pre: { only: ["code"] },
|
|
87
|
+
strong: { block: false, directive: false, inline: true },
|
|
78
88
|
table: { only: ["thead", "tbody"] },
|
|
79
|
-
thead: { only: ["tr"] },
|
|
80
89
|
tbody: { only: ["tr"] },
|
|
90
|
+
td: { block: false, directive: false, inline: true },
|
|
91
|
+
th: { block: false, directive: false, inline: true },
|
|
92
|
+
thead: { only: ["tr"] },
|
|
81
93
|
tr: { only: ["th", "td"] },
|
|
82
|
-
|
|
83
|
-
td: { block: false, inline: true, directive: false },
|
|
84
|
-
em: { block: false, inline: true, directive: false },
|
|
85
|
-
strong: { block: false, inline: true, directive: false },
|
|
86
|
-
del: { block: false, inline: true, directive: false },
|
|
87
|
-
a: { block: false, inline: true, directive: false },
|
|
94
|
+
ul: { only: ["li"] },
|
|
88
95
|
} as Record<string, { block?: boolean; inline?: boolean; directive?: boolean; only?: string[] }>,
|
|
96
|
+
textOnly: ["code"],
|
|
97
|
+
void: ["hr", "br", "img"],
|
|
89
98
|
} as const;
|
|
90
99
|
|
|
91
100
|
/** Markdown-native block tags. */
|
|
@@ -103,33 +112,33 @@ export const MD_TEXT_ONLY: ReadonlySet<string> = new Set(MD_ELEMENTS.textOnly);
|
|
|
103
112
|
|
|
104
113
|
/** Jx tagName → mdast node-type. */
|
|
105
114
|
const TAG_MDAST_MAP: Record<string, string> = {
|
|
115
|
+
a: "link",
|
|
116
|
+
blockquote: "blockquote",
|
|
117
|
+
br: "break",
|
|
118
|
+
code: "inlineCode",
|
|
119
|
+
del: "delete",
|
|
120
|
+
em: "emphasis",
|
|
106
121
|
h1: "heading",
|
|
107
122
|
h2: "heading",
|
|
108
123
|
h3: "heading",
|
|
109
124
|
h4: "heading",
|
|
110
125
|
h5: "heading",
|
|
111
126
|
h6: "heading",
|
|
112
|
-
|
|
113
|
-
span: "text",
|
|
114
|
-
em: "emphasis",
|
|
115
|
-
strong: "strong",
|
|
116
|
-
del: "delete",
|
|
117
|
-
code: "inlineCode",
|
|
118
|
-
a: "link",
|
|
127
|
+
hr: "thematicBreak",
|
|
119
128
|
img: "image",
|
|
120
|
-
blockquote: "blockquote",
|
|
121
|
-
ul: "list",
|
|
122
|
-
ol: "list",
|
|
123
129
|
li: "listItem",
|
|
130
|
+
ol: "list",
|
|
131
|
+
p: "paragraph",
|
|
124
132
|
pre: "code",
|
|
125
|
-
|
|
126
|
-
|
|
133
|
+
span: "text",
|
|
134
|
+
strong: "strong",
|
|
127
135
|
table: "table",
|
|
128
|
-
thead: "thead",
|
|
129
136
|
tbody: "tbody",
|
|
130
|
-
tr: "tableRow",
|
|
131
|
-
th: "tableCell",
|
|
132
137
|
td: "tableCell",
|
|
138
|
+
th: "tableCell",
|
|
139
|
+
thead: "thead",
|
|
140
|
+
tr: "tableRow",
|
|
141
|
+
ul: "list",
|
|
133
142
|
};
|
|
134
143
|
|
|
135
144
|
/** Wrapper tags unwrapped in export mode — their children are promoted. */
|
|
@@ -205,24 +214,24 @@ export interface SerializeOptions {
|
|
|
205
214
|
|
|
206
215
|
/** Mdast node-type → Jx tagName mapping (canvas-targeted; text → span). */
|
|
207
216
|
const MDAST_TAG_MAP: Record<string, (n: MdastNode) => string> = {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
emphasis: () => "em",
|
|
212
|
-
strong: () => "strong",
|
|
217
|
+
blockquote: () => "blockquote",
|
|
218
|
+
break: () => "br",
|
|
219
|
+
code: () => "pre",
|
|
213
220
|
delete: () => "del",
|
|
221
|
+
emphasis: () => "em",
|
|
222
|
+
heading: (n) => `h${n.depth}`,
|
|
223
|
+
image: () => "img",
|
|
214
224
|
inlineCode: () => "code",
|
|
215
225
|
link: () => "a",
|
|
216
|
-
image: () => "img",
|
|
217
|
-
blockquote: () => "blockquote",
|
|
218
226
|
list: (n) => (n.ordered ? "ol" : "ul"),
|
|
219
227
|
listItem: () => "li",
|
|
220
|
-
|
|
221
|
-
|
|
228
|
+
paragraph: () => "p",
|
|
229
|
+
strong: () => "strong",
|
|
222
230
|
table: () => "table",
|
|
223
|
-
tableRow: () => "tr",
|
|
224
231
|
tableCell: (n) => (n.isHeader ? "th" : "td"),
|
|
225
|
-
|
|
232
|
+
tableRow: () => "tr",
|
|
233
|
+
text: () => "span",
|
|
234
|
+
thematicBreak: () => "hr",
|
|
226
235
|
};
|
|
227
236
|
|
|
228
237
|
/**
|
|
@@ -234,7 +243,7 @@ export function mdastToJx(mdast: MdastNode): JxElement {
|
|
|
234
243
|
return {
|
|
235
244
|
children: (mdast.children ?? [])
|
|
236
245
|
.filter((n) => n.type !== "yaml" && n.type !== "toml")
|
|
237
|
-
.flatMap(convertMdastNode)
|
|
246
|
+
.flatMap((n) => convertMdastNode(n))
|
|
238
247
|
.filter(Boolean) as (JxElement | string)[],
|
|
239
248
|
};
|
|
240
249
|
}
|
|
@@ -242,7 +251,9 @@ export function mdastToJx(mdast: MdastNode): JxElement {
|
|
|
242
251
|
}
|
|
243
252
|
|
|
244
253
|
function convertMdastNode(node: MdastNode): JxElement | null {
|
|
245
|
-
if (!node)
|
|
254
|
+
if (!node) {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
246
257
|
|
|
247
258
|
if (
|
|
248
259
|
node.type === "containerDirective" ||
|
|
@@ -253,19 +264,26 @@ function convertMdastNode(node: MdastNode): JxElement | null {
|
|
|
253
264
|
}
|
|
254
265
|
|
|
255
266
|
if (node.type === "html") {
|
|
256
|
-
if (!node.value)
|
|
267
|
+
if (!node.value) {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
257
270
|
const nodes = htmlToJx(node.value);
|
|
258
|
-
return nodes.length === 1 ? (nodes[0] as JxElement) : { tagName: "div"
|
|
271
|
+
return nodes.length === 1 ? (nodes[0] as JxElement) : { children: nodes, tagName: "div" };
|
|
259
272
|
}
|
|
260
273
|
|
|
261
274
|
const tagFn = MDAST_TAG_MAP[node.type];
|
|
262
|
-
if (!tagFn)
|
|
275
|
+
if (!tagFn) {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
263
278
|
|
|
264
279
|
const tag = tagFn(node);
|
|
265
280
|
const el: JxElement = { tagName: tag };
|
|
266
281
|
|
|
267
282
|
const childNodes = () =>
|
|
268
|
-
(node.children ?? []).flatMap(convertMdastNode).filter(Boolean) as (
|
|
283
|
+
(node.children ?? []).flatMap((n) => convertMdastNode(n)).filter(Boolean) as (
|
|
284
|
+
| JxElement
|
|
285
|
+
| string
|
|
286
|
+
)[];
|
|
269
287
|
const flattenOrChildren = () => {
|
|
270
288
|
if (node.children?.length === 1 && node.children[0].type === "text") {
|
|
271
289
|
el.textContent = node.children[0].value ?? null;
|
|
@@ -280,40 +298,54 @@ function convertMdastNode(node: MdastNode): JxElement | null {
|
|
|
280
298
|
case "emphasis":
|
|
281
299
|
case "strong":
|
|
282
300
|
case "delete":
|
|
283
|
-
case "tableCell":
|
|
301
|
+
case "tableCell": {
|
|
284
302
|
flattenOrChildren();
|
|
285
303
|
break;
|
|
304
|
+
}
|
|
286
305
|
|
|
287
306
|
case "text":
|
|
288
|
-
case "inlineCode":
|
|
307
|
+
case "inlineCode": {
|
|
289
308
|
el.textContent = node.value ?? null;
|
|
290
309
|
break;
|
|
310
|
+
}
|
|
291
311
|
|
|
292
|
-
case "link":
|
|
312
|
+
case "link": {
|
|
293
313
|
el.attributes = { href: node.url ?? "" };
|
|
294
|
-
if (node.title)
|
|
314
|
+
if (node.title) {
|
|
315
|
+
el.attributes.title = node.title;
|
|
316
|
+
}
|
|
295
317
|
flattenOrChildren();
|
|
296
318
|
break;
|
|
319
|
+
}
|
|
297
320
|
|
|
298
|
-
case "image":
|
|
299
|
-
el.attributes = {
|
|
300
|
-
if (node.title)
|
|
321
|
+
case "image": {
|
|
322
|
+
el.attributes = { alt: node.alt ?? "", src: node.url ?? "" };
|
|
323
|
+
if (node.title) {
|
|
324
|
+
el.attributes.title = node.title;
|
|
325
|
+
}
|
|
301
326
|
break;
|
|
327
|
+
}
|
|
302
328
|
|
|
303
329
|
case "blockquote":
|
|
304
330
|
case "listItem":
|
|
305
|
-
case "tableRow":
|
|
306
|
-
if (node.children?.length)
|
|
331
|
+
case "tableRow": {
|
|
332
|
+
if (node.children?.length) {
|
|
333
|
+
el.children = childNodes();
|
|
334
|
+
}
|
|
307
335
|
break;
|
|
336
|
+
}
|
|
308
337
|
|
|
309
|
-
case "list":
|
|
310
|
-
if (node.children?.length)
|
|
338
|
+
case "list": {
|
|
339
|
+
if (node.children?.length) {
|
|
340
|
+
el.children = childNodes();
|
|
341
|
+
}
|
|
311
342
|
if (node.start != null && node.start !== 1) {
|
|
312
343
|
el.attributes = { start: String(node.start) };
|
|
313
344
|
}
|
|
314
345
|
break;
|
|
346
|
+
}
|
|
315
347
|
|
|
316
|
-
case "code":
|
|
348
|
+
case "code": {
|
|
317
349
|
el.children = [
|
|
318
350
|
{
|
|
319
351
|
tagName: "code",
|
|
@@ -322,27 +354,32 @@ function convertMdastNode(node: MdastNode): JxElement | null {
|
|
|
322
354
|
},
|
|
323
355
|
];
|
|
324
356
|
break;
|
|
357
|
+
}
|
|
325
358
|
|
|
326
359
|
case "thematicBreak":
|
|
327
|
-
case "break":
|
|
360
|
+
case "break": {
|
|
328
361
|
break;
|
|
362
|
+
}
|
|
329
363
|
|
|
330
364
|
case "table": {
|
|
331
365
|
const rows = childNodes() as JxElement[];
|
|
332
366
|
const thead =
|
|
333
367
|
rows.length > 0
|
|
334
|
-
? {
|
|
368
|
+
? { children: [rows[0]] as (JxElement | string)[], tagName: "thead" }
|
|
335
369
|
: null;
|
|
336
370
|
const tbody =
|
|
337
371
|
rows.length > 1
|
|
338
372
|
? {
|
|
339
|
-
tagName: "tbody",
|
|
340
373
|
children: rows.slice(1) as (JxElement | string)[],
|
|
374
|
+
tagName: "tbody",
|
|
341
375
|
}
|
|
342
376
|
: null;
|
|
343
377
|
el.children = [thead, tbody].filter(Boolean) as (JxElement | string)[];
|
|
344
378
|
break;
|
|
345
379
|
}
|
|
380
|
+
default: {
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
346
383
|
}
|
|
347
384
|
|
|
348
385
|
return el;
|
|
@@ -357,13 +394,16 @@ function convertDirective(node: MdastNode): JxElement {
|
|
|
357
394
|
if (node.children?.length === 1 && node.children[0].type === "text") {
|
|
358
395
|
el.textContent = node.children[0].value ?? null;
|
|
359
396
|
} else if (node.children?.length) {
|
|
360
|
-
el.children = node.children.flatMap(convertMdastNode).filter(Boolean) as (
|
|
397
|
+
el.children = node.children.flatMap((n) => convertMdastNode(n)).filter(Boolean) as (
|
|
361
398
|
| JxElement
|
|
362
399
|
| string
|
|
363
400
|
)[];
|
|
364
401
|
}
|
|
365
402
|
} else if (node.type === "containerDirective" && node.children?.length) {
|
|
366
|
-
el.children = node.children.flatMap(convertMdastNode).filter(Boolean) as (
|
|
403
|
+
el.children = node.children.flatMap((n) => convertMdastNode(n)).filter(Boolean) as (
|
|
404
|
+
| JxElement
|
|
405
|
+
| string
|
|
406
|
+
)[];
|
|
367
407
|
}
|
|
368
408
|
return el;
|
|
369
409
|
}
|
|
@@ -381,11 +421,13 @@ export function jxToMdast(jx: JxElement, opts: SerializeOptions = {}): MdastNode
|
|
|
381
421
|
.map((child) => convertJxNode(child, true, allowlist))
|
|
382
422
|
.filter(Boolean) as MdastNode[];
|
|
383
423
|
|
|
384
|
-
return { type: "root"
|
|
424
|
+
return { children, type: "root" };
|
|
385
425
|
}
|
|
386
426
|
|
|
387
427
|
function normalizeAllowlist(allowlist?: ReadonlySet<string> | string[]): ReadonlySet<string> {
|
|
388
|
-
if (!allowlist)
|
|
428
|
+
if (!allowlist) {
|
|
429
|
+
return MD_ALL;
|
|
430
|
+
}
|
|
389
431
|
return allowlist instanceof Set ? allowlist : new Set(allowlist);
|
|
390
432
|
}
|
|
391
433
|
|
|
@@ -398,8 +440,9 @@ function hasJxProps(el: JxElement) {
|
|
|
398
440
|
key === "textContent" ||
|
|
399
441
|
key === "innerHTML" ||
|
|
400
442
|
key === "attributes"
|
|
401
|
-
)
|
|
443
|
+
) {
|
|
402
444
|
continue;
|
|
445
|
+
}
|
|
403
446
|
return true;
|
|
404
447
|
}
|
|
405
448
|
return false;
|
|
@@ -413,7 +456,9 @@ function convertJxNode(
|
|
|
413
456
|
if (typeof el === "string" || typeof el === "number") {
|
|
414
457
|
return { type: "text", value: String(el) };
|
|
415
458
|
}
|
|
416
|
-
if (!el || typeof el !== "object")
|
|
459
|
+
if (!el || typeof el !== "object") {
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
417
462
|
|
|
418
463
|
const tag = el.tagName ?? "div";
|
|
419
464
|
|
|
@@ -423,10 +468,14 @@ function convertJxNode(
|
|
|
423
468
|
}
|
|
424
469
|
|
|
425
470
|
const mdastType = TAG_MDAST_MAP[tag];
|
|
426
|
-
if (!mdastType)
|
|
471
|
+
if (!mdastType) {
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
427
474
|
|
|
428
475
|
const inline = (e: JxElement): MdastNode[] => {
|
|
429
|
-
if (e.textContent != null)
|
|
476
|
+
if (e.textContent != null) {
|
|
477
|
+
return [{ type: "text", value: String(e.textContent) }];
|
|
478
|
+
}
|
|
430
479
|
return ((e.children ?? []) as (JxElement | string)[])
|
|
431
480
|
.map((c) => convertJxNode(c, false, allowlist))
|
|
432
481
|
.filter(Boolean) as MdastNode[];
|
|
@@ -435,8 +484,8 @@ function convertJxNode(
|
|
|
435
484
|
if (e.textContent != null) {
|
|
436
485
|
return [
|
|
437
486
|
{
|
|
438
|
-
type: "paragraph",
|
|
439
487
|
children: [{ type: "text", value: String(e.textContent) }],
|
|
488
|
+
type: "paragraph",
|
|
440
489
|
},
|
|
441
490
|
];
|
|
442
491
|
}
|
|
@@ -446,107 +495,128 @@ function convertJxNode(
|
|
|
446
495
|
};
|
|
447
496
|
|
|
448
497
|
switch (mdastType) {
|
|
449
|
-
case "heading":
|
|
498
|
+
case "heading": {
|
|
450
499
|
return {
|
|
451
|
-
type: "heading",
|
|
452
|
-
depth: parseInt(tag.slice(1), 10),
|
|
453
500
|
children: inline(el),
|
|
501
|
+
depth: Number.parseInt(tag.slice(1), 10),
|
|
502
|
+
type: "heading",
|
|
454
503
|
};
|
|
504
|
+
}
|
|
455
505
|
|
|
456
|
-
case "paragraph":
|
|
457
|
-
return {
|
|
506
|
+
case "paragraph": {
|
|
507
|
+
return { children: inline(el), type: "paragraph" };
|
|
508
|
+
}
|
|
458
509
|
|
|
459
|
-
case "text":
|
|
460
|
-
return { type: "text", value: el
|
|
510
|
+
case "text": {
|
|
511
|
+
return { type: "text", value: textOf(el) ?? "" };
|
|
512
|
+
}
|
|
461
513
|
|
|
462
514
|
case "emphasis":
|
|
463
515
|
case "strong":
|
|
464
|
-
case "delete":
|
|
465
|
-
return {
|
|
516
|
+
case "delete": {
|
|
517
|
+
return { children: inline(el), type: mdastType };
|
|
518
|
+
}
|
|
466
519
|
|
|
467
|
-
case "inlineCode":
|
|
468
|
-
return { type: "inlineCode", value: el
|
|
520
|
+
case "inlineCode": {
|
|
521
|
+
return { type: "inlineCode", value: textOf(el) ?? "" };
|
|
522
|
+
}
|
|
469
523
|
|
|
470
|
-
case "link":
|
|
524
|
+
case "link": {
|
|
471
525
|
return {
|
|
526
|
+
children: inline(el),
|
|
527
|
+
title: (el.attributes?.title as string | null) ?? null,
|
|
472
528
|
type: "link",
|
|
473
529
|
url: (el.attributes?.href as string) ?? "",
|
|
474
|
-
title: (el.attributes?.title as string | null) ?? null,
|
|
475
|
-
children: inline(el),
|
|
476
530
|
};
|
|
531
|
+
}
|
|
477
532
|
|
|
478
|
-
case "image":
|
|
533
|
+
case "image": {
|
|
479
534
|
return {
|
|
480
|
-
type: "image",
|
|
481
|
-
url: (el.attributes?.src as string) ?? "",
|
|
482
535
|
alt: (el.attributes?.alt as string) ?? "",
|
|
483
536
|
title: (el.attributes?.title as string | null) ?? null,
|
|
537
|
+
type: "image",
|
|
538
|
+
url: (el.attributes?.src as string) ?? "",
|
|
484
539
|
};
|
|
540
|
+
}
|
|
485
541
|
|
|
486
|
-
case "blockquote":
|
|
487
|
-
return {
|
|
542
|
+
case "blockquote": {
|
|
543
|
+
return { children: block(el), type: "blockquote" };
|
|
544
|
+
}
|
|
488
545
|
|
|
489
|
-
case "list":
|
|
546
|
+
case "list": {
|
|
490
547
|
return {
|
|
491
|
-
type: "list",
|
|
492
|
-
ordered: tag === "ol",
|
|
493
|
-
start: tag === "ol" ? parseInt(el.attributes?.start as string, 10) || 1 : null,
|
|
494
|
-
spread: false,
|
|
495
548
|
children: ((el.children ?? []) as (JxElement | string)[])
|
|
496
549
|
.map((c) => convertJxNode(c, true, allowlist))
|
|
497
550
|
.filter(Boolean) as MdastNode[],
|
|
551
|
+
ordered: tag === "ol",
|
|
552
|
+
spread: false,
|
|
553
|
+
start: tag === "ol" ? Number.parseInt(el.attributes?.start as string, 10) || 1 : null,
|
|
554
|
+
type: "list",
|
|
498
555
|
};
|
|
556
|
+
}
|
|
499
557
|
|
|
500
|
-
case "listItem":
|
|
501
|
-
return {
|
|
558
|
+
case "listItem": {
|
|
559
|
+
return { children: block(el), spread: false, type: "listItem" };
|
|
560
|
+
}
|
|
502
561
|
|
|
503
562
|
case "code": {
|
|
504
563
|
const codeChild = Array.isArray(el.children)
|
|
505
564
|
? (el.children[0] as JxElement | undefined)
|
|
506
565
|
: undefined;
|
|
507
566
|
return {
|
|
508
|
-
type: "code",
|
|
509
567
|
lang: codeLang(codeChild),
|
|
510
|
-
|
|
568
|
+
type: "code",
|
|
569
|
+
value: textOf(codeChild) ?? textOf(el) ?? "",
|
|
511
570
|
};
|
|
512
571
|
}
|
|
513
572
|
|
|
514
|
-
case "thematicBreak":
|
|
573
|
+
case "thematicBreak": {
|
|
515
574
|
return { type: "thematicBreak" };
|
|
575
|
+
}
|
|
516
576
|
|
|
517
|
-
case "break":
|
|
577
|
+
case "break": {
|
|
518
578
|
return { type: "break" };
|
|
579
|
+
}
|
|
519
580
|
|
|
520
581
|
case "table": {
|
|
521
582
|
// Flatten thead/tbody back to rows
|
|
522
583
|
const rows: MdastNode[] = [];
|
|
523
584
|
for (const section of (el.children ?? []) as (JxElement | string)[]) {
|
|
524
|
-
if (typeof section === "string")
|
|
585
|
+
if (typeof section === "string") {
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
525
588
|
if (section.tagName === "thead" || section.tagName === "tbody") {
|
|
526
589
|
for (const row of (section.children ?? []) as (JxElement | string)[]) {
|
|
527
590
|
const mdRow = convertJxNode(row, true, allowlist);
|
|
528
591
|
if (mdRow) {
|
|
529
592
|
if (section.tagName === "thead") {
|
|
530
|
-
for (const cell of mdRow.children ?? [])
|
|
593
|
+
for (const cell of mdRow.children ?? []) {
|
|
594
|
+
cell.isHeader = true;
|
|
595
|
+
}
|
|
531
596
|
}
|
|
532
597
|
rows.push(mdRow);
|
|
533
598
|
}
|
|
534
599
|
}
|
|
535
600
|
}
|
|
536
601
|
}
|
|
537
|
-
return { type: "table"
|
|
602
|
+
return { children: rows, type: "table" };
|
|
538
603
|
}
|
|
539
604
|
|
|
540
|
-
case "tableRow":
|
|
605
|
+
case "tableRow": {
|
|
541
606
|
return {
|
|
542
|
-
type: "tableRow",
|
|
543
607
|
children: ((el.children ?? []) as (JxElement | string)[])
|
|
544
608
|
.map((c) => convertJxNode(c, false, allowlist))
|
|
545
609
|
.filter(Boolean) as MdastNode[],
|
|
610
|
+
type: "tableRow",
|
|
546
611
|
};
|
|
612
|
+
}
|
|
547
613
|
|
|
548
|
-
case "tableCell":
|
|
549
|
-
return {
|
|
614
|
+
case "tableCell": {
|
|
615
|
+
return { children: inline(el), type: "tableCell" };
|
|
616
|
+
}
|
|
617
|
+
default: {
|
|
618
|
+
break;
|
|
619
|
+
}
|
|
550
620
|
}
|
|
551
621
|
|
|
552
622
|
return null;
|
|
@@ -592,8 +662,9 @@ function collectDirectiveAttrs(el: JxElement) {
|
|
|
592
662
|
const propsObj: Record<string, unknown> = {};
|
|
593
663
|
|
|
594
664
|
for (const [key, value] of Object.entries(el)) {
|
|
595
|
-
if (key === "tagName" || key === "textContent" || key === "innerHTML" || key === "attributes")
|
|
665
|
+
if (key === "tagName" || key === "textContent" || key === "innerHTML" || key === "attributes") {
|
|
596
666
|
continue;
|
|
667
|
+
}
|
|
597
668
|
if (key === "children") {
|
|
598
669
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
599
670
|
propsObj.children = value;
|
|
@@ -622,8 +693,6 @@ function convertToDirective(
|
|
|
622
693
|
|
|
623
694
|
if (!isBlock) {
|
|
624
695
|
return {
|
|
625
|
-
type: "textDirective",
|
|
626
|
-
name: tag,
|
|
627
696
|
attributes: attrs,
|
|
628
697
|
children:
|
|
629
698
|
el.textContent != null
|
|
@@ -631,6 +700,8 @@ function convertToDirective(
|
|
|
631
700
|
: (((el.children ?? []) as (JxElement | string)[])
|
|
632
701
|
.map((c) => convertJxNode(c, false, allowlist))
|
|
633
702
|
.filter(Boolean) as MdastNode[]),
|
|
703
|
+
name: tag,
|
|
704
|
+
type: "textDirective",
|
|
634
705
|
};
|
|
635
706
|
}
|
|
636
707
|
|
|
@@ -642,10 +713,10 @@ function convertToDirective(
|
|
|
642
713
|
: (rawChildren as (JxElement | string)[] | undefined);
|
|
643
714
|
if (!childArray?.length && el.textContent == null) {
|
|
644
715
|
return {
|
|
645
|
-
type: "leafDirective",
|
|
646
|
-
name: tag,
|
|
647
716
|
attributes: attrs,
|
|
648
717
|
children: [],
|
|
718
|
+
name: tag,
|
|
719
|
+
type: "leafDirective",
|
|
649
720
|
};
|
|
650
721
|
}
|
|
651
722
|
|
|
@@ -653,8 +724,8 @@ function convertToDirective(
|
|
|
653
724
|
if (el.textContent != null) {
|
|
654
725
|
directiveChildren = [
|
|
655
726
|
{
|
|
656
|
-
type: "paragraph",
|
|
657
727
|
children: [{ type: "text", value: String(el.textContent) }],
|
|
728
|
+
type: "paragraph",
|
|
658
729
|
},
|
|
659
730
|
];
|
|
660
731
|
} else if (INLINE_CONTENT_TAGS.has(tag)) {
|
|
@@ -663,7 +734,7 @@ function convertToDirective(
|
|
|
663
734
|
.map((c) => convertJxNode(c, false, allowlist))
|
|
664
735
|
.filter(Boolean) as MdastNode[];
|
|
665
736
|
directiveChildren =
|
|
666
|
-
inlineNodes.length > 0 ? [{ type: "paragraph"
|
|
737
|
+
inlineNodes.length > 0 ? [{ children: inlineNodes, type: "paragraph" }] : [];
|
|
667
738
|
} else {
|
|
668
739
|
directiveChildren = ((el.children ?? []) as (JxElement | string)[])
|
|
669
740
|
.map((c) => convertJxNode(c, true, allowlist))
|
|
@@ -671,10 +742,10 @@ function convertToDirective(
|
|
|
671
742
|
}
|
|
672
743
|
|
|
673
744
|
return {
|
|
674
|
-
type: "containerDirective",
|
|
675
|
-
name: tag,
|
|
676
745
|
attributes: attrs,
|
|
677
746
|
children: directiveChildren,
|
|
747
|
+
name: tag,
|
|
748
|
+
type: "containerDirective",
|
|
678
749
|
};
|
|
679
750
|
}
|
|
680
751
|
|
|
@@ -739,7 +810,9 @@ function nodeToMdast(
|
|
|
739
810
|
if (typeof node === "number") {
|
|
740
811
|
return [{ type: "text", value: String(node) }];
|
|
741
812
|
}
|
|
742
|
-
if (!node || typeof node !== "object")
|
|
813
|
+
if (!node || typeof node !== "object") {
|
|
814
|
+
return [];
|
|
815
|
+
}
|
|
743
816
|
|
|
744
817
|
// Array descriptor — expand mapped arrays
|
|
745
818
|
if (node.$prototype === "Array") {
|
|
@@ -749,10 +822,12 @@ function nodeToMdast(
|
|
|
749
822
|
const tag = node.tagName ?? "div";
|
|
750
823
|
const text = resolveText(node.textContent, ctx, scope);
|
|
751
824
|
|
|
752
|
-
//
|
|
825
|
+
// InnerHTML — convert HTML content to mdast
|
|
753
826
|
if (typeof node.innerHTML === "string" && node.innerHTML.trim()) {
|
|
754
827
|
const htmlNodes = htmlToMdast(node.innerHTML);
|
|
755
|
-
if (htmlNodes.length > 0)
|
|
828
|
+
if (htmlNodes.length > 0) {
|
|
829
|
+
return htmlNodes;
|
|
830
|
+
}
|
|
756
831
|
}
|
|
757
832
|
|
|
758
833
|
// Custom elements — inline component content
|
|
@@ -763,7 +838,7 @@ function nodeToMdast(
|
|
|
763
838
|
// Wrapper tags — unwrap, promote children
|
|
764
839
|
if (WRAPPER_TAGS.has(tag)) {
|
|
765
840
|
if (text != null) {
|
|
766
|
-
return text.trim() ? [{
|
|
841
|
+
return text.trim() ? [{ children: [{ type: "text", value: text }], type: "paragraph" }] : [];
|
|
767
842
|
}
|
|
768
843
|
return exportChildren(node, ctx, scope);
|
|
769
844
|
}
|
|
@@ -771,24 +846,26 @@ function nodeToMdast(
|
|
|
771
846
|
const mdastType = TAG_MDAST_MAP[tag];
|
|
772
847
|
if (!mdastType) {
|
|
773
848
|
if (text != null) {
|
|
774
|
-
return text.trim() ? [{
|
|
849
|
+
return text.trim() ? [{ children: [{ type: "text", value: text }], type: "paragraph" }] : [];
|
|
775
850
|
}
|
|
776
851
|
return exportChildren(node, ctx, scope);
|
|
777
852
|
}
|
|
778
853
|
|
|
779
854
|
switch (mdastType) {
|
|
780
855
|
case "heading": {
|
|
781
|
-
const depth = parseInt(tag.slice(1), 10);
|
|
856
|
+
const depth = Number.parseInt(tag.slice(1), 10);
|
|
782
857
|
const children =
|
|
783
858
|
text != null ? [{ type: "text", value: text }] : exportChildren(node, ctx, scope);
|
|
784
|
-
return [{ type: "heading"
|
|
859
|
+
return [{ children, depth, type: "heading" }];
|
|
785
860
|
}
|
|
786
861
|
|
|
787
862
|
case "paragraph": {
|
|
788
863
|
const children =
|
|
789
864
|
text != null ? [{ type: "text", value: text }] : exportChildren(node, ctx, scope);
|
|
790
|
-
if (children.length === 0)
|
|
791
|
-
|
|
865
|
+
if (children.length === 0) {
|
|
866
|
+
return [];
|
|
867
|
+
}
|
|
868
|
+
return [{ children, type: "paragraph" }];
|
|
792
869
|
}
|
|
793
870
|
|
|
794
871
|
case "emphasis":
|
|
@@ -796,88 +873,96 @@ function nodeToMdast(
|
|
|
796
873
|
case "delete": {
|
|
797
874
|
const children =
|
|
798
875
|
text != null ? [{ type: "text", value: text }] : exportChildren(node, ctx, scope);
|
|
799
|
-
return [{ type: mdastType
|
|
876
|
+
return [{ children, type: mdastType }];
|
|
800
877
|
}
|
|
801
878
|
|
|
802
|
-
case "inlineCode":
|
|
879
|
+
case "inlineCode": {
|
|
803
880
|
return [{ type: "inlineCode", value: text ?? "" }];
|
|
881
|
+
}
|
|
804
882
|
|
|
805
883
|
case "link": {
|
|
806
884
|
const href = String(node.attributes?.href ?? "");
|
|
807
885
|
const title = (node.attributes?.title as string | null) ?? null;
|
|
808
886
|
const children =
|
|
809
887
|
text != null ? [{ type: "text", value: text }] : exportChildren(node, ctx, scope);
|
|
810
|
-
return [{ type: "link", url: href
|
|
888
|
+
return [{ children, title, type: "link", url: href }];
|
|
811
889
|
}
|
|
812
890
|
|
|
813
891
|
case "image": {
|
|
814
892
|
const src = String(node.attributes?.src ?? "");
|
|
815
893
|
const alt = String(node.attributes?.alt ?? "");
|
|
816
894
|
const title = (node.attributes?.title as string | null) ?? null;
|
|
817
|
-
return [{ type: "image", url: src
|
|
895
|
+
return [{ alt, title, type: "image", url: src }];
|
|
818
896
|
}
|
|
819
897
|
|
|
820
898
|
case "blockquote": {
|
|
821
899
|
const children = exportChildren(node, ctx, scope);
|
|
822
900
|
const wrapped = children.map((c) =>
|
|
823
|
-
c.type === "text" ? { type: "paragraph"
|
|
901
|
+
c.type === "text" ? { children: [c], type: "paragraph" } : c,
|
|
824
902
|
);
|
|
825
|
-
return [{ type: "blockquote"
|
|
903
|
+
return [{ children: wrapped, type: "blockquote" }];
|
|
826
904
|
}
|
|
827
905
|
|
|
828
906
|
case "list": {
|
|
829
907
|
const ordered = tag === "ol";
|
|
830
908
|
const children = exportChildren(node, ctx, scope);
|
|
831
909
|
const items = children.filter((c) => c.type === "listItem");
|
|
832
|
-
if (items.length === 0)
|
|
833
|
-
|
|
910
|
+
if (items.length === 0) {
|
|
911
|
+
return [];
|
|
912
|
+
}
|
|
913
|
+
return [{ children: items, ordered, spread: false, type: "list" }];
|
|
834
914
|
}
|
|
835
915
|
|
|
836
916
|
case "listItem": {
|
|
837
917
|
let children = exportChildren(node, ctx, scope);
|
|
838
918
|
if (children.length > 0 && children.every((c) => c.type === "text" || isInlineType(c.type))) {
|
|
839
|
-
children = [{ type: "paragraph"
|
|
919
|
+
children = [{ children, type: "paragraph" }];
|
|
840
920
|
}
|
|
841
|
-
return [{
|
|
921
|
+
return [{ children, spread: false, type: "listItem" }];
|
|
842
922
|
}
|
|
843
923
|
|
|
844
924
|
case "code": {
|
|
845
925
|
const codeChild = Array.isArray(node.children)
|
|
846
926
|
? node.children.find((c: JxElement | string) => (c as JxMutableNode)?.tagName === "code")
|
|
847
927
|
: null;
|
|
848
|
-
const value = (codeChild as JxElement | undefined)
|
|
928
|
+
const value = textOf(codeChild as JxElement | undefined) ?? text ?? "";
|
|
849
929
|
return [
|
|
850
930
|
{
|
|
851
|
-
type: "code",
|
|
852
931
|
lang: codeLang(codeChild as JxElement | undefined),
|
|
932
|
+
type: "code",
|
|
853
933
|
value,
|
|
854
934
|
},
|
|
855
935
|
];
|
|
856
936
|
}
|
|
857
937
|
|
|
858
|
-
case "thematicBreak":
|
|
938
|
+
case "thematicBreak": {
|
|
859
939
|
return [{ type: "thematicBreak" }];
|
|
940
|
+
}
|
|
860
941
|
|
|
861
|
-
case "break":
|
|
942
|
+
case "break": {
|
|
862
943
|
return [{ type: "break" }];
|
|
944
|
+
}
|
|
863
945
|
|
|
864
946
|
case "table": {
|
|
865
947
|
const rows = exportChildren(node, ctx, scope).filter((c) => c.type === "tableRow");
|
|
866
|
-
if (rows.length === 0)
|
|
867
|
-
|
|
948
|
+
if (rows.length === 0) {
|
|
949
|
+
return [];
|
|
950
|
+
}
|
|
951
|
+
return [{ children: rows, type: "table" }];
|
|
868
952
|
}
|
|
869
953
|
|
|
870
954
|
case "thead":
|
|
871
|
-
case "tbody":
|
|
955
|
+
case "tbody": {
|
|
872
956
|
// Unwrap — promote rows
|
|
873
957
|
return exportChildren(node, ctx, scope);
|
|
958
|
+
}
|
|
874
959
|
|
|
875
960
|
case "tableRow": {
|
|
876
961
|
const cells = exportChildren(node, ctx, scope);
|
|
877
962
|
return [
|
|
878
963
|
{
|
|
879
|
-
type: "tableRow",
|
|
880
964
|
children: cells.filter((c) => c.type === "tableCell"),
|
|
965
|
+
type: "tableRow",
|
|
881
966
|
},
|
|
882
967
|
];
|
|
883
968
|
}
|
|
@@ -885,7 +970,10 @@ function nodeToMdast(
|
|
|
885
970
|
case "tableCell": {
|
|
886
971
|
const children =
|
|
887
972
|
text != null ? [{ type: "text", value: text }] : exportChildren(node, ctx, scope);
|
|
888
|
-
return [{ type: "tableCell"
|
|
973
|
+
return [{ children, type: "tableCell" }];
|
|
974
|
+
}
|
|
975
|
+
default: {
|
|
976
|
+
break;
|
|
889
977
|
}
|
|
890
978
|
}
|
|
891
979
|
|
|
@@ -899,10 +987,14 @@ function exportChildren(
|
|
|
899
987
|
): MdastNode[] {
|
|
900
988
|
if (node.textContent != null) {
|
|
901
989
|
const text = resolveText(node.textContent, ctx, scope);
|
|
902
|
-
if (text)
|
|
990
|
+
if (text) {
|
|
991
|
+
return [{ type: "text", value: text }];
|
|
992
|
+
}
|
|
993
|
+
return [];
|
|
994
|
+
}
|
|
995
|
+
if (!Array.isArray(node.children)) {
|
|
903
996
|
return [];
|
|
904
997
|
}
|
|
905
|
-
if (!Array.isArray(node.children)) return [];
|
|
906
998
|
return node.children.flatMap((c: JxElement | string) => nodeToMdast(c, ctx, scope));
|
|
907
999
|
}
|
|
908
1000
|
|
|
@@ -920,16 +1012,13 @@ function inlineComponent(node: JxElement, tag: string, ctx: ExportContext): Mdas
|
|
|
920
1012
|
for (const [key, value] of Object.entries(props)) {
|
|
921
1013
|
if (key in stateDefs) {
|
|
922
1014
|
const existing = stateDefs[key];
|
|
923
|
-
|
|
1015
|
+
stateDefs[key] =
|
|
924
1016
|
existing &&
|
|
925
1017
|
typeof existing === "object" &&
|
|
926
1018
|
!Array.isArray(existing) &&
|
|
927
1019
|
"default" in existing
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
} else {
|
|
931
|
-
stateDefs[key] = value as JxStateDefinition;
|
|
932
|
-
}
|
|
1020
|
+
? { ...existing, default: value }
|
|
1021
|
+
: (value as JxStateDefinition);
|
|
933
1022
|
} else {
|
|
934
1023
|
stateDefs[key] = value as JxStateDefinition;
|
|
935
1024
|
}
|
|
@@ -937,7 +1026,9 @@ function inlineComponent(node: JxElement, tag: string, ctx: ExportContext): Mdas
|
|
|
937
1026
|
|
|
938
1027
|
const scope = ctx.buildScope?.(stateDefs) ?? null;
|
|
939
1028
|
|
|
940
|
-
if (!Array.isArray(def.children))
|
|
1029
|
+
if (!Array.isArray(def.children)) {
|
|
1030
|
+
return [];
|
|
1031
|
+
}
|
|
941
1032
|
|
|
942
1033
|
const resolved = deepResolve(def.children, scope ?? {}, ctx);
|
|
943
1034
|
|
|
@@ -956,7 +1047,9 @@ function deepResolve(
|
|
|
956
1047
|
scope: Record<string, unknown>,
|
|
957
1048
|
ctx: ExportContext,
|
|
958
1049
|
) {
|
|
959
|
-
if (!Array.isArray(nodes))
|
|
1050
|
+
if (!Array.isArray(nodes)) {
|
|
1051
|
+
return [];
|
|
1052
|
+
}
|
|
960
1053
|
return nodes.map((node) => resolveNode(node, scope, ctx));
|
|
961
1054
|
}
|
|
962
1055
|
|
|
@@ -969,24 +1062,33 @@ function resolveNode(
|
|
|
969
1062
|
const evaluated = ctx.evaluateTemplate?.(node, scope);
|
|
970
1063
|
return evaluated !== undefined ? String(evaluated) : node;
|
|
971
1064
|
}
|
|
972
|
-
if (!node || typeof node !== "object")
|
|
1065
|
+
if (!node || typeof node !== "object") {
|
|
1066
|
+
return node;
|
|
1067
|
+
}
|
|
973
1068
|
|
|
974
1069
|
const result = { ...node };
|
|
975
1070
|
|
|
976
1071
|
if (typeof result.textContent === "string") {
|
|
977
1072
|
const evaluated = ctx.evaluateTemplate?.(result.textContent, scope);
|
|
978
|
-
if (evaluated !== undefined)
|
|
1073
|
+
if (evaluated !== undefined) {
|
|
1074
|
+
result.textContent = String(evaluated);
|
|
1075
|
+
}
|
|
979
1076
|
}
|
|
980
1077
|
if (typeof result.innerHTML === "string") {
|
|
981
1078
|
const evaluated = ctx.evaluateTemplate?.(result.innerHTML, scope);
|
|
982
|
-
if (evaluated !== undefined)
|
|
1079
|
+
if (evaluated !== undefined) {
|
|
1080
|
+
result.innerHTML = String(evaluated);
|
|
1081
|
+
}
|
|
983
1082
|
}
|
|
984
1083
|
if (result.attributes) {
|
|
985
1084
|
result.attributes = { ...result.attributes };
|
|
986
1085
|
for (const [k, v] of Object.entries(result.attributes)) {
|
|
987
1086
|
if (typeof v === "string") {
|
|
988
|
-
|
|
989
|
-
|
|
1087
|
+
// Template evaluation yields a substituted scalar for attribute values.
|
|
1088
|
+
const evaluated = ctx.evaluateTemplate?.(v, scope) as JxAttributeValue | undefined;
|
|
1089
|
+
if (evaluated !== undefined) {
|
|
1090
|
+
result.attributes[k] = evaluated;
|
|
1091
|
+
}
|
|
990
1092
|
}
|
|
991
1093
|
}
|
|
992
1094
|
}
|
|
@@ -1004,14 +1106,21 @@ function expandArray(
|
|
|
1004
1106
|
ctx: ExportContext,
|
|
1005
1107
|
scope?: Record<string, unknown> | null,
|
|
1006
1108
|
): MdastNode[] {
|
|
1007
|
-
const
|
|
1008
|
-
|
|
1109
|
+
const itemsValue = (arrayDef as JxMutableNode).items;
|
|
1110
|
+
const itemsRef = isRef(itemsValue) ? itemsValue.$ref : undefined;
|
|
1111
|
+
if (!itemsRef || !scope) {
|
|
1112
|
+
return [];
|
|
1113
|
+
}
|
|
1009
1114
|
|
|
1010
1115
|
const items = resolveRef(itemsRef, scope);
|
|
1011
|
-
if (!Array.isArray(items))
|
|
1116
|
+
if (!Array.isArray(items)) {
|
|
1117
|
+
return [];
|
|
1118
|
+
}
|
|
1012
1119
|
|
|
1013
1120
|
const mapTemplate = (arrayDef as JxMutableNode).map;
|
|
1014
|
-
if (!mapTemplate)
|
|
1121
|
+
if (!mapTemplate) {
|
|
1122
|
+
return [];
|
|
1123
|
+
}
|
|
1015
1124
|
|
|
1016
1125
|
return items.flatMap((item: JxMutableNode): MdastNode[] => {
|
|
1017
1126
|
const resolved = resolveMapNode(mapTemplate, item);
|
|
@@ -1020,8 +1129,12 @@ function expandArray(
|
|
|
1020
1129
|
}
|
|
1021
1130
|
|
|
1022
1131
|
function resolveMapNode(node: JxMutableNode, item: Record<string, unknown>) {
|
|
1023
|
-
if (typeof node === "string")
|
|
1024
|
-
|
|
1132
|
+
if (typeof node === "string") {
|
|
1133
|
+
return node;
|
|
1134
|
+
}
|
|
1135
|
+
if (!node || typeof node !== "object") {
|
|
1136
|
+
return node;
|
|
1137
|
+
}
|
|
1025
1138
|
|
|
1026
1139
|
const result = { ...node };
|
|
1027
1140
|
|
|
@@ -1039,7 +1152,10 @@ function resolveMapNode(node: JxMutableNode, item: Record<string, unknown>) {
|
|
|
1039
1152
|
}
|
|
1040
1153
|
|
|
1041
1154
|
if (result.$props) {
|
|
1042
|
-
result.$props = resolveMapNode(
|
|
1155
|
+
result.$props = resolveMapNode(
|
|
1156
|
+
result.$props as unknown as JxMutableNode,
|
|
1157
|
+
item,
|
|
1158
|
+
) as unknown as Record<string, JsonValue>;
|
|
1043
1159
|
}
|
|
1044
1160
|
|
|
1045
1161
|
if (typeof result.textContent === "string" && result.textContent.startsWith("$map/")) {
|
|
@@ -1061,8 +1177,11 @@ function resolvePath(obj: unknown, path: string) {
|
|
|
1061
1177
|
const parts = path.split(/[/.]/);
|
|
1062
1178
|
let current = obj as JxMutableNode;
|
|
1063
1179
|
for (const part of parts) {
|
|
1064
|
-
if (current == null)
|
|
1065
|
-
|
|
1180
|
+
if (current == null) {
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
// Paths address nodes by construction; non-node leaves surface as undefined above.
|
|
1184
|
+
current = current[part] as JxMutableNode;
|
|
1066
1185
|
}
|
|
1067
1186
|
return current;
|
|
1068
1187
|
}
|
|
@@ -1079,11 +1198,15 @@ function resolveText(
|
|
|
1079
1198
|
ctx: ExportContext,
|
|
1080
1199
|
scope?: Record<string, unknown> | null,
|
|
1081
1200
|
): string | null {
|
|
1082
|
-
if (value == null)
|
|
1201
|
+
if (value == null) {
|
|
1202
|
+
return null;
|
|
1203
|
+
}
|
|
1083
1204
|
if (typeof value === "string") {
|
|
1084
1205
|
if (scope) {
|
|
1085
1206
|
const evaluated = ctx.evaluateTemplate?.(value, scope);
|
|
1086
|
-
if (evaluated !== undefined)
|
|
1207
|
+
if (evaluated !== undefined) {
|
|
1208
|
+
return String(evaluated);
|
|
1209
|
+
}
|
|
1087
1210
|
}
|
|
1088
1211
|
return value;
|
|
1089
1212
|
}
|
|
@@ -1098,9 +1221,13 @@ function htmlToMdast(html: string) {
|
|
|
1098
1221
|
const parts = splitHtmlBlocks(html);
|
|
1099
1222
|
for (const part of parts) {
|
|
1100
1223
|
const trimmed = part.trim();
|
|
1101
|
-
if (!trimmed)
|
|
1224
|
+
if (!trimmed) {
|
|
1225
|
+
continue;
|
|
1226
|
+
}
|
|
1102
1227
|
const parsed = parseHtmlElement(trimmed);
|
|
1103
|
-
if (parsed)
|
|
1228
|
+
if (parsed) {
|
|
1229
|
+
nodes.push(...parsed);
|
|
1230
|
+
}
|
|
1104
1231
|
}
|
|
1105
1232
|
|
|
1106
1233
|
return nodes;
|
|
@@ -1117,14 +1244,18 @@ function splitHtmlBlocks(html: string) {
|
|
|
1117
1244
|
while ((m = pattern.exec(trimmed)) !== null) {
|
|
1118
1245
|
if (m.index > lastIdx) {
|
|
1119
1246
|
const between = trimmed.slice(lastIdx, m.index).trim();
|
|
1120
|
-
if (between)
|
|
1247
|
+
if (between) {
|
|
1248
|
+
blocks.push(between);
|
|
1249
|
+
}
|
|
1121
1250
|
}
|
|
1122
1251
|
blocks.push(m[0]);
|
|
1123
1252
|
lastIdx = pattern.lastIndex;
|
|
1124
1253
|
}
|
|
1125
1254
|
if (lastIdx < trimmed.length) {
|
|
1126
1255
|
const tail = trimmed.slice(lastIdx).trim();
|
|
1127
|
-
if (tail)
|
|
1256
|
+
if (tail) {
|
|
1257
|
+
blocks.push(tail);
|
|
1258
|
+
}
|
|
1128
1259
|
}
|
|
1129
1260
|
|
|
1130
1261
|
return blocks;
|
|
@@ -1133,16 +1264,18 @@ function splitHtmlBlocks(html: string) {
|
|
|
1133
1264
|
function parseHtmlElement(html: string) {
|
|
1134
1265
|
const hMatch = html.match(/^<(h[1-6])(?:\s[^>]*)?>(.+?)<\/\1>$/is);
|
|
1135
1266
|
if (hMatch) {
|
|
1136
|
-
const depth = parseInt(hMatch[1].slice(1), 10);
|
|
1267
|
+
const depth = Number.parseInt(hMatch[1].slice(1), 10);
|
|
1137
1268
|
const children = parseInlineHtml(hMatch[2]);
|
|
1138
|
-
return [{ type: "heading"
|
|
1269
|
+
return [{ children, depth, type: "heading" }];
|
|
1139
1270
|
}
|
|
1140
1271
|
|
|
1141
1272
|
const pMatch = html.match(/^<p(?:\s[^>]*)?>(.+?)<\/p>$/is);
|
|
1142
1273
|
if (pMatch) {
|
|
1143
1274
|
const children = parseInlineHtml(pMatch[1]);
|
|
1144
|
-
if (children.length === 0)
|
|
1145
|
-
|
|
1275
|
+
if (children.length === 0) {
|
|
1276
|
+
return null;
|
|
1277
|
+
}
|
|
1278
|
+
return [{ children, type: "paragraph" }];
|
|
1146
1279
|
}
|
|
1147
1280
|
|
|
1148
1281
|
if (/^<hr\s*\/?>$/i.test(html)) {
|
|
@@ -1155,30 +1288,34 @@ function parseHtmlElement(html: string) {
|
|
|
1155
1288
|
if (preMatch) {
|
|
1156
1289
|
const lang = preMatch[1] ?? null;
|
|
1157
1290
|
const value = decodeHtmlEntities(preMatch[2]);
|
|
1158
|
-
return [{ type: "code",
|
|
1291
|
+
return [{ lang, type: "code", value }];
|
|
1159
1292
|
}
|
|
1160
1293
|
|
|
1161
1294
|
const bqMatch = html.match(/^<blockquote(?:\s[^>]*)?>([^]*?)<\/blockquote>$/is);
|
|
1162
1295
|
if (bqMatch) {
|
|
1163
1296
|
const inner = htmlToMdast(bqMatch[1]);
|
|
1164
1297
|
const children = inner.map((c) =>
|
|
1165
|
-
c.type === "text" ? { type: "paragraph"
|
|
1298
|
+
c.type === "text" ? { children: [c], type: "paragraph" } : c,
|
|
1166
1299
|
);
|
|
1167
|
-
return [{ type: "blockquote"
|
|
1300
|
+
return [{ children, type: "blockquote" }];
|
|
1168
1301
|
}
|
|
1169
1302
|
|
|
1170
1303
|
const ulMatch = html.match(/^<ul(?:\s[^>]*)?>([^]*?)<\/ul>$/is);
|
|
1171
1304
|
if (ulMatch) {
|
|
1172
1305
|
const items = parseListItems(ulMatch[1]);
|
|
1173
|
-
if (items.length === 0)
|
|
1174
|
-
|
|
1306
|
+
if (items.length === 0) {
|
|
1307
|
+
return null;
|
|
1308
|
+
}
|
|
1309
|
+
return [{ children: items, ordered: false, spread: false, type: "list" }];
|
|
1175
1310
|
}
|
|
1176
1311
|
|
|
1177
1312
|
const olMatch = html.match(/^<ol(?:\s[^>]*)?>([^]*?)<\/ol>$/is);
|
|
1178
1313
|
if (olMatch) {
|
|
1179
1314
|
const items = parseListItems(olMatch[1]);
|
|
1180
|
-
if (items.length === 0)
|
|
1181
|
-
|
|
1315
|
+
if (items.length === 0) {
|
|
1316
|
+
return null;
|
|
1317
|
+
}
|
|
1318
|
+
return [{ children: items, ordered: true, spread: false, type: "list" }];
|
|
1182
1319
|
}
|
|
1183
1320
|
|
|
1184
1321
|
const tableMatch = html.match(/^<table(?:\s[^>]*)?>([^]*?)<\/table>$/is);
|
|
@@ -1194,7 +1331,9 @@ function parseHtmlElement(html: string) {
|
|
|
1194
1331
|
}
|
|
1195
1332
|
|
|
1196
1333
|
const text = stripHtmlTags(html).trim();
|
|
1197
|
-
if (text)
|
|
1334
|
+
if (text) {
|
|
1335
|
+
return [{ children: parseInlineHtml(html), type: "paragraph" }];
|
|
1336
|
+
}
|
|
1198
1337
|
|
|
1199
1338
|
return null;
|
|
1200
1339
|
}
|
|
@@ -1207,13 +1346,17 @@ function parseInlineHtml(html: string) {
|
|
|
1207
1346
|
const tagStart = html.indexOf("<", pos);
|
|
1208
1347
|
if (tagStart === -1) {
|
|
1209
1348
|
const text = decodeHtmlEntities(html.slice(pos));
|
|
1210
|
-
if (text.trim())
|
|
1349
|
+
if (text.trim()) {
|
|
1350
|
+
nodes.push({ type: "text", value: text });
|
|
1351
|
+
}
|
|
1211
1352
|
break;
|
|
1212
1353
|
}
|
|
1213
1354
|
|
|
1214
1355
|
if (tagStart > pos) {
|
|
1215
1356
|
const text = decodeHtmlEntities(html.slice(pos, tagStart));
|
|
1216
|
-
if (text.trim())
|
|
1357
|
+
if (text.trim()) {
|
|
1358
|
+
nodes.push({ type: "text", value: text });
|
|
1359
|
+
}
|
|
1217
1360
|
}
|
|
1218
1361
|
|
|
1219
1362
|
const brMatch = html.slice(tagStart).match(/^<br\s*\/?>/i);
|
|
@@ -1229,9 +1372,9 @@ function parseInlineHtml(html: string) {
|
|
|
1229
1372
|
const src = attrs.match(/src="([^"]*)"/)?.[1] ?? "";
|
|
1230
1373
|
const alt = attrs.match(/alt="([^"]*)"/)?.[1] ?? "";
|
|
1231
1374
|
nodes.push({
|
|
1375
|
+
alt: decodeHtmlEntities(alt),
|
|
1232
1376
|
type: "image",
|
|
1233
1377
|
url: decodeHtmlEntities(src),
|
|
1234
|
-
alt: decodeHtmlEntities(alt),
|
|
1235
1378
|
});
|
|
1236
1379
|
pos = tagStart + imgMatch[0].length;
|
|
1237
1380
|
continue;
|
|
@@ -1256,41 +1399,45 @@ function parseInlineHtml(html: string) {
|
|
|
1256
1399
|
const href = attrs.match(/href="([^"]*)"/)?.[1] ?? "";
|
|
1257
1400
|
const title = attrs.match(/title="([^"]*)"/)?.[1] ?? null;
|
|
1258
1401
|
const children = parseInlineHtml(inner);
|
|
1259
|
-
if (children.length === 0)
|
|
1402
|
+
if (children.length === 0) {
|
|
1260
1403
|
children.push({ type: "text", value: decodeHtmlEntities(inner) });
|
|
1404
|
+
}
|
|
1261
1405
|
nodes.push({
|
|
1406
|
+
children,
|
|
1407
|
+
title,
|
|
1262
1408
|
type: "link",
|
|
1263
1409
|
url: decodeHtmlEntities(href),
|
|
1264
|
-
title,
|
|
1265
|
-
children,
|
|
1266
1410
|
});
|
|
1267
1411
|
break;
|
|
1268
1412
|
}
|
|
1269
1413
|
case "em":
|
|
1270
|
-
case "i":
|
|
1271
|
-
nodes.push({
|
|
1414
|
+
case "i": {
|
|
1415
|
+
nodes.push({ children: parseInlineHtml(inner), type: "emphasis" });
|
|
1272
1416
|
break;
|
|
1417
|
+
}
|
|
1273
1418
|
case "strong":
|
|
1274
|
-
case "b":
|
|
1275
|
-
nodes.push({
|
|
1419
|
+
case "b": {
|
|
1420
|
+
nodes.push({ children: parseInlineHtml(inner), type: "strong" });
|
|
1276
1421
|
break;
|
|
1422
|
+
}
|
|
1277
1423
|
case "del":
|
|
1278
|
-
case "s":
|
|
1279
|
-
nodes.push({
|
|
1424
|
+
case "s": {
|
|
1425
|
+
nodes.push({ children: parseInlineHtml(inner), type: "delete" });
|
|
1280
1426
|
break;
|
|
1281
|
-
|
|
1427
|
+
}
|
|
1428
|
+
case "code": {
|
|
1282
1429
|
nodes.push({ type: "inlineCode", value: decodeHtmlEntities(inner) });
|
|
1283
1430
|
break;
|
|
1431
|
+
}
|
|
1432
|
+
default: {
|
|
1433
|
+
break;
|
|
1434
|
+
}
|
|
1284
1435
|
}
|
|
1285
1436
|
continue;
|
|
1286
1437
|
}
|
|
1287
1438
|
|
|
1288
1439
|
const skipMatch = html.slice(tagStart).match(/^<[^>]*>/);
|
|
1289
|
-
|
|
1290
|
-
pos = tagStart + skipMatch[0].length;
|
|
1291
|
-
} else {
|
|
1292
|
-
pos = tagStart + 1;
|
|
1293
|
-
}
|
|
1440
|
+
pos = skipMatch ? tagStart + skipMatch[0].length : tagStart + 1;
|
|
1294
1441
|
}
|
|
1295
1442
|
|
|
1296
1443
|
return nodes;
|
|
@@ -1307,15 +1454,19 @@ function findMatchingClose(html: string, start: number, tag: string) {
|
|
|
1307
1454
|
const openMatch = openRe.exec(html);
|
|
1308
1455
|
const closeMatch = closeRe.exec(html);
|
|
1309
1456
|
|
|
1310
|
-
if (!closeMatch)
|
|
1457
|
+
if (!closeMatch) {
|
|
1458
|
+
return -1;
|
|
1459
|
+
}
|
|
1311
1460
|
|
|
1312
1461
|
if (openMatch && openMatch.index < closeMatch.index) {
|
|
1313
|
-
depth
|
|
1462
|
+
depth += 1;
|
|
1314
1463
|
openRe.lastIndex = openMatch.index + openMatch[0].length;
|
|
1315
|
-
closeRe.lastIndex = closeMatch.index; //
|
|
1464
|
+
closeRe.lastIndex = closeMatch.index; // Re-check this close
|
|
1316
1465
|
} else {
|
|
1317
|
-
depth
|
|
1318
|
-
if (depth === 0)
|
|
1466
|
+
depth -= 1;
|
|
1467
|
+
if (depth === 0) {
|
|
1468
|
+
return closeMatch.index;
|
|
1469
|
+
}
|
|
1319
1470
|
}
|
|
1320
1471
|
}
|
|
1321
1472
|
return -1;
|
|
@@ -1329,8 +1480,8 @@ function parseListItems(html: string) {
|
|
|
1329
1480
|
const inner = m[1].trim();
|
|
1330
1481
|
const innerNodes = /<(?:p|ul|ol|blockquote|pre)[\s>]/i.test(inner)
|
|
1331
1482
|
? htmlToMdast(inner)
|
|
1332
|
-
: [{
|
|
1333
|
-
items.push({
|
|
1483
|
+
: [{ children: parseInlineHtml(inner), type: "paragraph" }];
|
|
1484
|
+
items.push({ children: innerNodes, spread: false, type: "listItem" });
|
|
1334
1485
|
}
|
|
1335
1486
|
return items;
|
|
1336
1487
|
}
|
|
@@ -1344,27 +1495,31 @@ function parseHtmlTable(html: string) {
|
|
|
1344
1495
|
const cells: MdastNode[] = [];
|
|
1345
1496
|
let c;
|
|
1346
1497
|
while ((c = cellPattern.exec(m[1])) !== null) {
|
|
1347
|
-
cells.push({
|
|
1498
|
+
cells.push({ children: parseInlineHtml(c[1]), type: "tableCell" });
|
|
1499
|
+
}
|
|
1500
|
+
if (cells.length > 0) {
|
|
1501
|
+
rows.push({ children: cells, type: "tableRow" });
|
|
1348
1502
|
}
|
|
1349
|
-
if (cells.length > 0) rows.push({ type: "tableRow", children: cells });
|
|
1350
1503
|
}
|
|
1351
|
-
if (rows.length === 0)
|
|
1352
|
-
|
|
1504
|
+
if (rows.length === 0) {
|
|
1505
|
+
return [];
|
|
1506
|
+
}
|
|
1507
|
+
return [{ children: rows, type: "table" }];
|
|
1353
1508
|
}
|
|
1354
1509
|
|
|
1355
1510
|
function stripHtmlTags(html: string) {
|
|
1356
|
-
return html.
|
|
1511
|
+
return html.replaceAll(/<[^>]+>/g, "");
|
|
1357
1512
|
}
|
|
1358
1513
|
|
|
1359
1514
|
function decodeHtmlEntities(str: string) {
|
|
1360
1515
|
return str
|
|
1361
|
-
.
|
|
1362
|
-
.
|
|
1363
|
-
.
|
|
1364
|
-
.
|
|
1365
|
-
.
|
|
1366
|
-
.
|
|
1367
|
-
.
|
|
1516
|
+
.replaceAll("&", "&")
|
|
1517
|
+
.replaceAll("<", "<")
|
|
1518
|
+
.replaceAll(">", ">")
|
|
1519
|
+
.replaceAll(""", '"')
|
|
1520
|
+
.replaceAll("'", "'")
|
|
1521
|
+
.replaceAll("$", "$")
|
|
1522
|
+
.replaceAll(" ", " ");
|
|
1368
1523
|
}
|
|
1369
1524
|
|
|
1370
1525
|
// ═════════════════════════════════════════════════════════════════════════════
|
|
@@ -1393,7 +1548,9 @@ function serializeRoundtrip(doc: JxDocument, opts: SerializeOptions): string {
|
|
|
1393
1548
|
if (opts.frontmatter !== false) {
|
|
1394
1549
|
const frontmatter: Record<string, unknown> = {};
|
|
1395
1550
|
for (const [key, value] of Object.entries(doc)) {
|
|
1396
|
-
if (key === "children")
|
|
1551
|
+
if (key === "children") {
|
|
1552
|
+
continue;
|
|
1553
|
+
}
|
|
1397
1554
|
frontmatter[key] = value;
|
|
1398
1555
|
}
|
|
1399
1556
|
|
|
@@ -1411,17 +1568,15 @@ function serializeRoundtrip(doc: JxDocument, opts: SerializeOptions): string {
|
|
|
1411
1568
|
.use(remarkGfm)
|
|
1412
1569
|
.use(remarkDirective)
|
|
1413
1570
|
.use(remarkStringify, { bullet: "-", emphasis: "*", strong: "*" })
|
|
1414
|
-
.stringify(mdast as unknown as
|
|
1571
|
+
.stringify(mdast as unknown as Root);
|
|
1415
1572
|
|
|
1416
1573
|
lines.push(md as string);
|
|
1417
1574
|
}
|
|
1418
1575
|
|
|
1419
|
-
return
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
.trim() + "\n"
|
|
1424
|
-
);
|
|
1576
|
+
return `${lines
|
|
1577
|
+
.join("\n")
|
|
1578
|
+
.replaceAll(/\n{3,}/g, "\n\n")
|
|
1579
|
+
.trim()}\n`;
|
|
1425
1580
|
}
|
|
1426
1581
|
|
|
1427
1582
|
function serializeExport(doc: JxDocument, opts: SerializeOptions): string {
|
|
@@ -1430,9 +1585,9 @@ function serializeExport(doc: JxDocument, opts: SerializeOptions): string {
|
|
|
1430
1585
|
}
|
|
1431
1586
|
|
|
1432
1587
|
const ctx: ExportContext = {
|
|
1588
|
+
buildScope: opts.buildScope,
|
|
1433
1589
|
componentDefs: opts.componentDefs ?? new Map(),
|
|
1434
1590
|
evaluateTemplate: opts.evaluateTemplate,
|
|
1435
|
-
buildScope: opts.buildScope,
|
|
1436
1591
|
};
|
|
1437
1592
|
|
|
1438
1593
|
// Build scope from resolved state for any remaining template expressions
|
|
@@ -1448,7 +1603,7 @@ function serializeExport(doc: JxDocument, opts: SerializeOptions): string {
|
|
|
1448
1603
|
|
|
1449
1604
|
const flushInline = () => {
|
|
1450
1605
|
if (inlineBuf.length > 0) {
|
|
1451
|
-
cleaned.push({ type: "paragraph"
|
|
1606
|
+
cleaned.push({ children: inlineBuf, type: "paragraph" });
|
|
1452
1607
|
inlineBuf = [];
|
|
1453
1608
|
}
|
|
1454
1609
|
};
|
|
@@ -1464,19 +1619,19 @@ function serializeExport(doc: JxDocument, opts: SerializeOptions): string {
|
|
|
1464
1619
|
flushInline();
|
|
1465
1620
|
|
|
1466
1621
|
const mdast = {
|
|
1467
|
-
type: "root",
|
|
1468
1622
|
children: cleaned,
|
|
1469
|
-
|
|
1623
|
+
type: "root",
|
|
1624
|
+
} as unknown as Root;
|
|
1470
1625
|
|
|
1471
1626
|
const md = unified()
|
|
1472
1627
|
.use(remarkGfm)
|
|
1473
1628
|
.use(remarkStringify, {
|
|
1474
1629
|
bullet: "-",
|
|
1475
1630
|
emphasis: "*",
|
|
1476
|
-
strong: "*",
|
|
1477
1631
|
setext: false,
|
|
1632
|
+
strong: "*",
|
|
1478
1633
|
})
|
|
1479
1634
|
.stringify(mdast);
|
|
1480
1635
|
|
|
1481
|
-
return md.
|
|
1636
|
+
return `${md.replaceAll(/\n{3,}/g, "\n\n").trim()}\n`;
|
|
1482
1637
|
}
|