@jxsuite/parser 0.28.2 → 0.28.4

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/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
- JxElement,
29
+ JsonValue,
30
+ JxAttributeValue,
29
31
  JxDocument,
30
- JxStateDefinition,
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
- // test asserts the two stay in sync.
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, inline: false, directive: true },
66
- h1: { block: false, inline: true, directive: false },
67
- h2: { block: false, inline: true, directive: false },
68
- h3: { block: false, inline: true, directive: false },
69
- h4: { block: false, inline: true, directive: false },
70
- h5: { block: false, inline: true, directive: false },
71
- h6: { block: false, inline: true, directive: false },
72
- p: { block: false, inline: true, directive: true },
73
- blockquote: { block: true, inline: false, directive: true },
74
- ul: { only: ["li"] },
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
- li: { block: true, inline: true, directive: true },
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
- th: { block: false, inline: true, directive: false },
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
- p: "paragraph",
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
- hr: "thematicBreak",
126
- br: "break",
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
- heading: (n) => `h${n.depth}`,
209
- paragraph: () => "p",
210
- text: () => "span",
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
- code: () => "pre",
221
- thematicBreak: () => "hr",
228
+ paragraph: () => "p",
229
+ strong: () => "strong",
222
230
  table: () => "table",
223
- tableRow: () => "tr",
224
231
  tableCell: (n) => (n.isHeader ? "th" : "td"),
225
- break: () => "br",
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) return null;
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) return null;
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", children: nodes };
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) return null;
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 (JxElement | string)[];
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) el.attributes.title = 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 = { src: node.url ?? "", alt: node.alt ?? "" };
300
- if (node.title) el.attributes.title = 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) el.children = childNodes();
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) el.children = childNodes();
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
- ? { tagName: "thead", children: [rows[0]] as (JxElement | string)[] }
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 (JxElement | string)[];
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", children };
424
+ return { children, type: "root" };
385
425
  }
386
426
 
387
427
  function normalizeAllowlist(allowlist?: ReadonlySet<string> | string[]): ReadonlySet<string> {
388
- if (!allowlist) return MD_ALL;
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") return null;
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) return null;
471
+ if (!mdastType) {
472
+ return null;
473
+ }
427
474
 
428
475
  const inline = (e: JxElement): MdastNode[] => {
429
- if (e.textContent != null) return [{ type: "text", value: String(e.textContent) }];
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 { type: "paragraph", children: inline(el) };
506
+ case "paragraph": {
507
+ return { children: inline(el), type: "paragraph" };
508
+ }
458
509
 
459
- case "text":
460
- return { type: "text", value: el.textContent ?? "" };
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 { type: mdastType, children: inline(el) };
516
+ case "delete": {
517
+ return { children: inline(el), type: mdastType };
518
+ }
466
519
 
467
- case "inlineCode":
468
- return { type: "inlineCode", value: el.textContent ?? "" };
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 { type: "blockquote", children: block(el) };
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 { type: "listItem", spread: false, children: block(el) };
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
- value: codeChild?.textContent ?? el.textContent ?? "",
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") continue;
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 ?? []) cell.isHeader = true;
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", children: rows };
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 { type: "tableCell", children: inline(el) };
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", children: inlineNodes }] : [];
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") return [];
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
- // innerHTML — convert HTML content to mdast
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) return htmlNodes;
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() ? [{ type: "paragraph", children: [{ type: "text", value: text }] }] : [];
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() ? [{ type: "paragraph", children: [{ type: "text", value: text }] }] : [];
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", depth, children }];
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) return [];
791
- return [{ type: "paragraph", children }];
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, children }];
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, title, children }];
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, alt, title }];
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", children: [c] } : c,
901
+ c.type === "text" ? { children: [c], type: "paragraph" } : c,
824
902
  );
825
- return [{ type: "blockquote", children: wrapped }];
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) return [];
833
- return [{ type: "list", ordered, spread: false, children: items }];
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", children }];
919
+ children = [{ children, type: "paragraph" }];
840
920
  }
841
- return [{ type: "listItem", spread: false, children }];
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)?.textContent ?? text ?? "";
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) return [];
867
- return [{ type: "table", children: rows }];
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", children }];
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) return [{ type: "text", value: 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
- if (
1015
+ stateDefs[key] =
924
1016
  existing &&
925
1017
  typeof existing === "object" &&
926
1018
  !Array.isArray(existing) &&
927
1019
  "default" in existing
928
- ) {
929
- stateDefs[key] = { ...existing, default: value };
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)) return [];
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)) return [];
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") return node;
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) result.textContent = String(evaluated);
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) result.innerHTML = String(evaluated);
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
- const evaluated = ctx.evaluateTemplate?.(v, scope);
989
- if (evaluated !== undefined) result.attributes[k] = evaluated;
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 itemsRef = (arrayDef as JxMutableNode).items?.$ref;
1008
- if (!itemsRef || !scope) return [];
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)) return [];
1116
+ if (!Array.isArray(items)) {
1117
+ return [];
1118
+ }
1012
1119
 
1013
1120
  const mapTemplate = (arrayDef as JxMutableNode).map;
1014
- if (!mapTemplate) return [];
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") return node;
1024
- if (!node || typeof node !== "object") return node;
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(result.$props, item);
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) return undefined;
1065
- current = current[part];
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) return 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) return String(evaluated);
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) continue;
1224
+ if (!trimmed) {
1225
+ continue;
1226
+ }
1102
1227
  const parsed = parseHtmlElement(trimmed);
1103
- if (parsed) nodes.push(...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) blocks.push(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) blocks.push(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", depth, children }];
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) return null;
1145
- return [{ type: "paragraph", children }];
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", lang, value }];
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", children: [c] } : c,
1298
+ c.type === "text" ? { children: [c], type: "paragraph" } : c,
1166
1299
  );
1167
- return [{ type: "blockquote", children }];
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) return null;
1174
- return [{ type: "list", ordered: false, spread: false, children: items }];
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) return null;
1181
- return [{ type: "list", ordered: true, spread: false, children: items }];
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) return [{ type: "paragraph", children: parseInlineHtml(html) }];
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()) nodes.push({ type: "text", value: text });
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()) nodes.push({ type: "text", value: text });
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({ type: "emphasis", children: parseInlineHtml(inner) });
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({ type: "strong", children: parseInlineHtml(inner) });
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({ type: "delete", children: parseInlineHtml(inner) });
1424
+ case "s": {
1425
+ nodes.push({ children: parseInlineHtml(inner), type: "delete" });
1280
1426
  break;
1281
- case "code":
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
- if (skipMatch) {
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) return -1;
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; // re-check this close
1464
+ closeRe.lastIndex = closeMatch.index; // Re-check this close
1316
1465
  } else {
1317
- depth--;
1318
- if (depth === 0) return closeMatch.index;
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
- : [{ type: "paragraph", children: parseInlineHtml(inner) }];
1333
- items.push({ type: "listItem", spread: false, children: innerNodes });
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({ type: "tableCell", children: parseInlineHtml(c[1]) });
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) return [];
1352
- return [{ type: "table", children: rows }];
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.replace(/<[^>]+>/g, "");
1511
+ return html.replaceAll(/<[^>]+>/g, "");
1357
1512
  }
1358
1513
 
1359
1514
  function decodeHtmlEntities(str: string) {
1360
1515
  return str
1361
- .replace(/&amp;/g, "&")
1362
- .replace(/&lt;/g, "<")
1363
- .replace(/&gt;/g, ">")
1364
- .replace(/&quot;/g, '"')
1365
- .replace(/&#39;/g, "'")
1366
- .replace(/&#36;/g, "$")
1367
- .replace(/&nbsp;/g, " ");
1516
+ .replaceAll("&amp;", "&")
1517
+ .replaceAll("&lt;", "<")
1518
+ .replaceAll("&gt;", ">")
1519
+ .replaceAll("&quot;", '"')
1520
+ .replaceAll("&#39;", "'")
1521
+ .replaceAll("&#36;", "$")
1522
+ .replaceAll("&nbsp;", " ");
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") continue;
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 import("mdast").Root);
1571
+ .stringify(mdast as unknown as Root);
1415
1572
 
1416
1573
  lines.push(md as string);
1417
1574
  }
1418
1575
 
1419
- return (
1420
- lines
1421
- .join("\n")
1422
- .replace(/\n{3,}/g, "\n\n")
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", children: inlineBuf });
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
- } as unknown as import("mdast").Root;
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.replace(/\n{3,}/g, "\n\n").trim() + "\n";
1636
+ return `${md.replaceAll(/\n{3,}/g, "\n\n").trim()}\n`;
1482
1637
  }