@jxsuite/parser 2.0.1 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/transpile.ts CHANGED
@@ -18,10 +18,18 @@ import remarkParseFrontmatter from "remark-parse-frontmatter";
18
18
  import remarkGfm from "remark-gfm";
19
19
  import remarkDirective from "remark-directive";
20
20
  import { htmlToJx } from "@jxsuite/markup/html-to-jx";
21
+ import {
22
+ isBoolean,
23
+ isJsonObject,
24
+ isNonNullObject,
25
+ isNumber,
26
+ isString,
27
+ } from "@jxsuite/schema/guards";
21
28
  import { ALERT_NODE, DEFAULT_ALERT_TITLES, inlineText } from "./alerts.ts";
22
29
  import type { AlertNode } from "./alerts.ts";
23
- import type { MdastNode, UnifiedProcessor } from "./types.ts";
30
+ import type { MdastNode, UnifiedFile, UnifiedPlugin, UnifiedProcessor } from "./types.ts";
24
31
  import type {
32
+ JsonObject,
25
33
  JsonValue,
26
34
  JxAttributeValue,
27
35
  JxDocument,
@@ -83,32 +91,43 @@ export function mdKey(key: string) {
83
91
  return key;
84
92
  }
85
93
 
94
+ /**
95
+ * A directive's attribute map after dot-path expansion: every leaf is the attribute string the
96
+ * author wrote, and every dotted segment is a nested object.
97
+ */
98
+ export interface ExpandedAttributes {
99
+ [key: string]: string | ExpandedAttributes;
100
+ }
101
+
86
102
  /**
87
103
  * Expand dot-path attribute keys into nested objects.
88
104
  *
89
105
  * @param {Record<string, string>} attrs - Flat attribute map from remark-directive
90
- * @returns {Record<string, unknown>} Nested object
106
+ * @returns {ExpandedAttributes} Nested object
91
107
  */
92
108
  export function expandDotPaths(attrs: Record<string, string>) {
93
- const result: Record<string, unknown> = {};
109
+ const result: ExpandedAttributes = {};
94
110
 
95
111
  for (const [key, value] of Object.entries(attrs)) {
96
- const dotIndex = key.indexOf(".");
97
- if (dotIndex === -1) {
112
+ const lastDot = key.lastIndexOf(".");
113
+ if (lastDot === -1) {
98
114
  result[jxKey(key)] = value;
99
115
  continue;
100
116
  }
101
117
 
102
- const segments = key.split(".");
103
118
  let target = result;
104
- for (let i = 0; i < segments.length - 1; i++) {
105
- const seg = jxKey(segments[i]!);
106
- if (!(seg in target) || typeof target[seg] !== "object") {
107
- target[seg] = {};
119
+ for (const segment of key.slice(0, lastDot).split(".")) {
120
+ const seg = jxKey(segment);
121
+ const existing = seg in target ? target[seg] : undefined;
122
+ if (isNonNullObject(existing)) {
123
+ target = existing;
124
+ } else {
125
+ const nested: ExpandedAttributes = {};
126
+ target[seg] = nested;
127
+ target = nested;
108
128
  }
109
- target = target[seg] as Record<string, unknown>;
110
129
  }
111
- target[jxKey(segments.at(-1) as string)] = value;
130
+ target[jxKey(key.slice(lastDot + 1))] = value;
112
131
  }
113
132
 
114
133
  return result;
@@ -117,17 +136,17 @@ export function expandDotPaths(attrs: Record<string, string>) {
117
136
  /**
118
137
  * Collapse a nested object back to dot-path flat attributes (inverse of expandDotPaths).
119
138
  *
120
- * @param {Record<string, unknown>} obj - Nested object
139
+ * @param {JsonObject} obj - Nested object
121
140
  * @returns {Record<string, string>} Flat attribute map
122
141
  */
123
- export function collapseDotPaths(obj: Record<string, unknown>) {
142
+ export function collapseDotPaths(obj: JsonObject) {
124
143
  const result: Record<string, string> = {};
125
144
 
126
- function walk(node: Record<string, unknown>, prefix: string) {
145
+ function walk(node: JsonObject, prefix: string) {
127
146
  for (const [key, value] of Object.entries(node)) {
128
147
  const path = prefix ? `${prefix}.${key}` : key;
129
- if (value && typeof value === "object" && !Array.isArray(value)) {
130
- walk(value as Record<string, unknown>, path);
148
+ if (isJsonObject(value)) {
149
+ walk(value, path);
131
150
  } else {
132
151
  result[path] = String(value);
133
152
  }
@@ -190,11 +209,11 @@ const CSS_PSEUDO_ELEMENTS = new Set(["backdrop"]);
190
209
  * `:hover` (for known CSS pseudo-class names) - `--dark` → `@--dark` (for custom property / media
191
210
  * query keys)
192
211
  *
193
- * @param {Record<string, unknown>} styleObj
194
- * @returns {Record<string, unknown>}
212
+ * @param {Record<string, V>} styleObj
213
+ * @returns {Record<string, V>} The same values under their mapped keys
195
214
  */
196
- export function applyStyleKeyMapping(styleObj: Record<string, unknown>) {
197
- const result: Record<string, unknown> = {};
215
+ export function applyStyleKeyMapping<V>(styleObj: Record<string, V>) {
216
+ const result: Record<string, V> = {};
198
217
  for (const [key, value] of Object.entries(styleObj)) {
199
218
  if (CSS_PSEUDO_ELEMENTS.has(key)) {
200
219
  result[`::${key}`] = value;
@@ -216,7 +235,7 @@ export function applyStyleKeyMapping(styleObj: Record<string, unknown>) {
216
235
  * cannot appear at the start of remark-directive attribute keys.
217
236
  *
218
237
  * @param {Record<string, string>} attrs
219
- * @returns {Record<string, unknown>}
238
+ * @returns {Record<string, string | ExpandedAttributes>}
220
239
  */
221
240
  export function expandStylePaths(attrs: Record<string, string>) {
222
241
  return applyStyleKeyMapping(expandDotPaths(attrs));
@@ -228,11 +247,11 @@ export function expandStylePaths(attrs: Record<string, string>) {
228
247
  * Strips `:` prefix from pseudo-class keys and `@` prefix from media keys before flattening with
229
248
  * collapseDotPaths.
230
249
  *
231
- * @param {Record<string, unknown>} styleObj
250
+ * @param {JsonObject} styleObj
232
251
  * @returns {Record<string, string>}
233
252
  */
234
- export function collapseStylePaths(styleObj: Record<string, unknown>) {
235
- const normalized: Record<string, unknown> = {};
253
+ export function collapseStylePaths(styleObj: JsonObject) {
254
+ const normalized: JsonObject = {};
236
255
 
237
256
  for (const [key, value] of Object.entries(styleObj)) {
238
257
  /* `::` before `:`, or a pseudo-element would match the pseudo-class arm on its second colon
@@ -319,22 +338,23 @@ const PHRASING_ELEMENTS = new Set([
319
338
  * Route directive attributes to their correct Jx locations.
320
339
  *
321
340
  * @param {Record<string, string>} attrs
322
- * @returns {{ props: Record<string, unknown>; attributes: Record<string, string> }}
341
+ * @returns {{ props: ExpandedAttributes; attributes: Record<string, JxAttributeValue> }}
323
342
  */
324
343
  function routeAttributes(attrs: Record<string, string>) {
325
344
  const expanded = expandDotPaths(attrs);
326
345
 
327
346
  // Apply style-key mapping (pseudo-classes, media queries) to the style sub-object
328
- if (expanded.style && typeof expanded.style === "object") {
329
- expanded.style = applyStyleKeyMapping(expanded.style as Record<string, unknown>);
347
+ const { style } = expanded;
348
+ if (isNonNullObject(style)) {
349
+ expanded.style = applyStyleKeyMapping(style);
330
350
  }
331
351
 
332
- const props: Record<string, unknown> = {};
333
- const attributes: Record<string, string> = {};
352
+ const props: ExpandedAttributes = {};
353
+ const attributes: Record<string, JxAttributeValue> = {};
334
354
 
335
355
  for (const [key, value] of Object.entries(expanded)) {
336
356
  if (HTML_ATTR_PATTERN.test(key)) {
337
- attributes[key] = value as string;
357
+ attributes[key] = attributeValue(value);
338
358
  } else {
339
359
  props[key] = value;
340
360
  }
@@ -344,38 +364,64 @@ function routeAttributes(attrs: Record<string, string>) {
344
364
  }
345
365
 
346
366
  /**
347
- * Mdast node-type → Jx tagName mapping.
367
+ * A routed directive attribute as an element's `attributes` map holds it.
368
+ *
369
+ * @param {string | ExpandedAttributes} value
370
+ * @returns {JxAttributeValue}
371
+ */
372
+ function attributeValue(value: string | ExpandedAttributes): JxAttributeValue {
373
+ /* SAFETY: a directive attribute is a string unless its key was dotted. The dotted form that
374
+ means something here is `name.ref="#/…"`, which `jxKey` expands to `{ $ref }`, a JxRef
375
+ binding. Any other nested object is carried into the map verbatim, as it always has been, so
376
+ the serializer can write the same dotted key back; the attribute type does not model it. */
377
+ return value as JxAttributeValue;
378
+ }
379
+
380
+ /** Mdast node-type → Jx tagName mapping. */
381
+ const JX_TAG_MAP = new Map<string, (n: MdastNode) => string>([
382
+ ["blockquote", () => "blockquote"],
383
+ ["break", () => "br"],
384
+ ["code", () => "pre"],
385
+ ["delete", () => "del"],
386
+ ["emphasis", () => "em"],
387
+ ["heading", (n: MdastNode) => `h${n.depth}`],
388
+ ["image", () => "img"],
389
+ ["inlineCode", () => "code"],
390
+ ["link", () => "a"],
391
+ ["list", (n: MdastNode) => (n.ordered ? "ol" : "ul")],
392
+ ["listItem", () => "li"],
393
+ ["paragraph", () => "p"],
394
+ ["strong", () => "strong"],
395
+ ["table", () => "table"],
396
+ ["tableCell", (n: MdastNode) => (n.isHeader ? "th" : "td")],
397
+ ["tableRow", () => "tr"],
398
+ ["thematicBreak", () => "hr"],
399
+ ]);
400
+
401
+ /**
402
+ * Give an element its converted content: a lone string becomes `textContent`, anything else
403
+ * non-empty becomes `children`.
348
404
  *
349
- * @type {Record<string, (n: MdastNode) => string>}
405
+ * @param {JxElement} el - Mutated in place
406
+ * @param {(JxElement | string)[]} children
350
407
  */
351
- const JX_TAG_MAP: Record<string, (n: MdastNode) => string> = {
352
- blockquote: () => "blockquote",
353
- break: () => "br",
354
- code: () => "pre",
355
- delete: () => "del",
356
- emphasis: () => "em",
357
- heading: (n: MdastNode) => `h${n.depth}`,
358
- image: () => "img",
359
- inlineCode: () => "code",
360
- link: () => "a",
361
- list: (n: MdastNode) => (n.ordered ? "ol" : "ul"),
362
- listItem: () => "li",
363
- paragraph: () => "p",
364
- strong: () => "strong",
365
- table: () => "table",
366
- tableCell: (n: MdastNode) => (n.isHeader ? "th" : "td"),
367
- tableRow: () => "tr",
368
- thematicBreak: () => "hr",
369
- };
408
+ function setContent(el: JxElement, children: (JxElement | string)[]) {
409
+ const [only] = children;
410
+ if (children.length === 1 && isString(only)) {
411
+ el.textContent = only;
412
+ } else if (children.length > 0) {
413
+ el.children = children;
414
+ }
415
+ }
370
416
 
371
417
  /**
372
418
  * Convert a standard mdast node to a Jx element definition.
373
419
  *
374
- * @param {MdastNode} node
420
+ * @param {MdastNode | null | undefined} node - A missing node converts to null
375
421
  * @returns {JxElement | string | (JxElement | string)[] | null} Jx element or null
376
422
  */
377
- export function mdastNodeToJx(node: MdastNode) {
378
- if (!node || typeof node !== "object") {
423
+ export function mdastNodeToJx(node: MdastNode | null | undefined) {
424
+ if (!node) {
379
425
  return null;
380
426
  }
381
427
 
@@ -392,6 +438,8 @@ export function mdastNodeToJx(node: MdastNode) {
392
438
  }
393
439
 
394
440
  if (node.type === ALERT_NODE) {
441
+ // SAFETY: `transformAlerts` is the only producer of a `jxAlert` node, and it builds each one as
442
+ // A complete AlertNode (`alert`, `element` and `titleNodes` all set).
395
443
  return alertToJx(node as AlertNode);
396
444
  }
397
445
 
@@ -406,7 +454,7 @@ export function mdastNodeToJx(node: MdastNode) {
406
454
  return null;
407
455
  }
408
456
 
409
- const tagFn = JX_TAG_MAP[node.type];
457
+ const tagFn = JX_TAG_MAP.get(node.type);
410
458
  if (!tagFn) {
411
459
  return null;
412
460
  }
@@ -424,12 +472,7 @@ export function mdastNodeToJx(node: MdastNode) {
424
472
  case "listItem":
425
473
  case "tableRow":
426
474
  case "tableCell": {
427
- const children = convertChildren(node.children ?? []);
428
- if (children.length === 1 && typeof children[0] === "string") {
429
- [el.textContent] = children;
430
- } else if (children.length > 0) {
431
- el.children = children;
432
- }
475
+ setContent(el, convertChildren(node.children ?? []));
433
476
  break;
434
477
  }
435
478
 
@@ -439,35 +482,19 @@ export function mdastNodeToJx(node: MdastNode) {
439
482
  }
440
483
 
441
484
  case "link": {
442
- {
443
- const linkAttrs: Record<string, JxAttributeValue> = {
444
- href: node.url ?? "",
445
- };
446
- if (node.title) {
447
- linkAttrs.title = node.title;
448
- }
449
- el.attributes = linkAttrs;
450
- }
451
- {
452
- const children = convertChildren(node.children ?? []);
453
- if (children.length === 1 && typeof children[0] === "string") {
454
- [el.textContent] = children;
455
- } else if (children.length > 0) {
456
- el.children = children;
457
- }
485
+ el.attributes = { href: node.url ?? "" };
486
+ if (node.title) {
487
+ el.attributes.title = node.title;
458
488
  }
489
+ setContent(el, convertChildren(node.children ?? []));
459
490
  break;
460
491
  }
461
492
 
462
493
  case "image": {
463
- const imageAttrs: Record<string, JxAttributeValue> = {
464
- alt: node.alt ?? "",
465
- src: node.url ?? "",
466
- };
494
+ el.attributes = { alt: node.alt ?? "", src: node.url ?? "" };
467
495
  if (node.title) {
468
- imageAttrs.title = node.title;
496
+ el.attributes.title = node.title;
469
497
  }
470
- el.attributes = imageAttrs;
471
498
  break;
472
499
  }
473
500
 
@@ -482,13 +509,11 @@ export function mdastNodeToJx(node: MdastNode) {
482
509
  }
483
510
 
484
511
  case "code": {
485
- el.children = [
486
- {
487
- tagName: "code",
488
- textContent: node.value ?? null,
489
- ...(node.lang ? { className: `language-${node.lang}` } : {}),
490
- },
491
- ];
512
+ const code: JxElement = { tagName: "code", textContent: node.value ?? null };
513
+ if (node.lang) {
514
+ code.className = `language-${node.lang}`;
515
+ }
516
+ el.children = [code];
492
517
  break;
493
518
  }
494
519
 
@@ -502,16 +527,21 @@ export function mdastNodeToJx(node: MdastNode) {
502
527
  // GFM's first table row is the header row, but mdast does not mark its cells — retag them
503
528
  // To <th> so the <thead> carries real column headers (assistive tech relies on this).
504
529
  const [headerRow] = rows;
505
- if (headerRow && typeof headerRow === "object" && Array.isArray(headerRow.children)) {
530
+ if (isNonNullObject(headerRow) && Array.isArray(headerRow.children)) {
506
531
  for (const cell of headerRow.children) {
507
- if (cell && typeof cell === "object" && (cell as JxElement).tagName === "td") {
508
- (cell as JxElement).tagName = "th";
532
+ if (isNonNullObject(cell) && cell.tagName === "td") {
533
+ cell.tagName = "th";
509
534
  }
510
535
  }
511
536
  }
512
- const thead = rows.length > 0 ? { children: [rows[0]], tagName: "thead" } : null;
513
- const tbody = rows.length > 1 ? { children: rows.slice(1), tagName: "tbody" } : null;
514
- el.children = [thead, tbody].filter(Boolean) as JxElement[];
537
+ const sections: JxElement[] = [];
538
+ if (headerRow !== undefined) {
539
+ sections.push({ children: [headerRow], tagName: "thead" });
540
+ }
541
+ if (rows.length > 1) {
542
+ sections.push({ children: rows.slice(1), tagName: "tbody" });
543
+ }
544
+ el.children = sections;
515
545
  break;
516
546
  }
517
547
  default: {
@@ -540,10 +570,9 @@ function alertToJx(node: AlertNode): JxElement {
540
570
  const body = convertChildren(node.children ?? []);
541
571
 
542
572
  if (node.element) {
543
- const attributes: Record<string, JxAttributeValue> = { "data-alert": node.alert };
544
- if (node.titleNodes) {
545
- attributes["data-title"] = inlineText(node.titleNodes).trim();
546
- }
573
+ const attributes = node.titleNodes
574
+ ? { "data-alert": node.alert, "data-title": inlineText(node.titleNodes).trim() }
575
+ : { "data-alert": node.alert };
547
576
  const el: JxElement = { attributes, tagName: node.element };
548
577
  if (body.length > 0) {
549
578
  el.children = body;
@@ -554,8 +583,9 @@ function alertToJx(node: AlertNode): JxElement {
554
583
  const title: JxElement = { className: "jx-alert-title", tagName: "p" };
555
584
  if (node.titleNodes) {
556
585
  const inline = convertChildren(node.titleNodes);
557
- if (inline.length === 1 && typeof inline[0] === "string") {
558
- [title.textContent] = inline;
586
+ const [only] = inline;
587
+ if (inline.length === 1 && isString(only)) {
588
+ title.textContent = only;
559
589
  } else {
560
590
  title.children = inline;
561
591
  }
@@ -579,16 +609,20 @@ function alertToJx(node: AlertNode): JxElement {
579
609
  * @returns {JxElement}
580
610
  */
581
611
  function directiveToJx(node: MdastNode) {
612
+ /* SAFETY: mdastNodeToJx routes only container, leaf and text directive nodes here, and
613
+ remark-directive gives every one of them a `name`: the syntax has no nameless directive. */
614
+ const name = node.name as string;
615
+
582
616
  // Prototype directive (e.g. `:::Array`) → `{ $prototype: name, ... }` with no tagName.
583
- if (PROTOTYPE_DIRECTIVE_NAMES.has(node.name as string)) {
584
- return prototypeDirectiveToJx(node);
617
+ if (PROTOTYPE_DIRECTIVE_NAMES.has(name)) {
618
+ return prototypeDirectiveToJx(node, name);
585
619
  }
586
620
 
587
- const el: JxElement = { tagName: node.name as string };
621
+ const el: JxElement = { tagName: name };
588
622
 
589
623
  if (node.attributes && Object.keys(node.attributes).length > 0) {
590
624
  const { props, attributes } = routeAttributes(node.attributes);
591
- const isCustomElement = (node.name as string).includes("-");
625
+ const isCustomElement = name.includes("-");
592
626
  if (isCustomElement) {
593
627
  // For custom elements:
594
628
  // - style, children, textContent, innerHTML, $-prefixed → element-level
@@ -608,7 +642,7 @@ function directiveToJx(node: MdastNode) {
608
642
  if (!el.attributes) {
609
643
  el.attributes = {};
610
644
  }
611
- el.attributes[key] = value as JxAttributeValue;
645
+ el.attributes[key] = attributeValue(value);
612
646
  }
613
647
  }
614
648
  } else {
@@ -636,7 +670,7 @@ function directiveToJx(node: MdastNode) {
636
670
  if (!el.attributes) {
637
671
  el.attributes = {};
638
672
  }
639
- el.attributes[key] = value as JxAttributeValue;
673
+ el.attributes[key] = attributeValue(value);
640
674
  }
641
675
  }
642
676
  }
@@ -647,12 +681,7 @@ function directiveToJx(node: MdastNode) {
647
681
 
648
682
  if (node.type === "textDirective") {
649
683
  if (node.children && node.children.length > 0) {
650
- const children = convertChildren(node.children ?? []);
651
- if (children.length === 1 && typeof children[0] === "string") {
652
- [el.textContent] = children;
653
- } else if (children.length > 0) {
654
- el.children = children;
655
- }
684
+ setContent(el, convertChildren(node.children));
656
685
  }
657
686
  return el;
658
687
  }
@@ -663,7 +692,7 @@ function directiveToJx(node: MdastNode) {
663
692
 
664
693
  if (node.children && node.children.length > 0) {
665
694
  const jxChildren: (JxElement | string)[] = [];
666
- const isPhrasingParent = PHRASING_ELEMENTS.has(node.name as string);
695
+ const isPhrasingParent = PHRASING_ELEMENTS.has(name);
667
696
 
668
697
  for (const child of node.children) {
669
698
  if (isPhrasingParent && child.type === "paragraph") {
@@ -693,13 +722,9 @@ function directiveToJx(node: MdastNode) {
693
722
  }
694
723
 
695
724
  // Don't overwrite children if already set as an object by dot-path attributes
696
- // (e.g. children.prototype="Array" children.items.ref="...")
697
- if (el.children && typeof el.children === "object" && !Array.isArray(el.children)) {
698
- // Children was set to a descriptor object by dot-path expansion — keep it
699
- } else if (jxChildren.length === 1 && typeof jxChildren[0] === "string") {
700
- [el.textContent] = jxChildren;
701
- } else if (jxChildren.length > 0) {
702
- el.children = jxChildren;
725
+ // (e.g. children.prototype="Array" children.items.ref="..."): a descriptor object stays.
726
+ if (!isNonNullObject(el.children) || Array.isArray(el.children)) {
727
+ setContent(el, jxChildren);
703
728
  }
704
729
  }
705
730
 
@@ -712,10 +737,11 @@ function directiveToJx(node: MdastNode) {
712
737
  * `map` template.
713
738
  *
714
739
  * @param {MdastNode} node
740
+ * @param {string} name - The directive's name, which is the prototype's
715
741
  * @returns {JxElement}
716
742
  */
717
- function prototypeDirectiveToJx(node: MdastNode) {
718
- const el: JxElement = { $prototype: node.name as string };
743
+ function prototypeDirectiveToJx(node: MdastNode, name: string) {
744
+ const el: JxElement = { $prototype: name };
719
745
 
720
746
  if (node.attributes && Object.keys(node.attributes).length > 0) {
721
747
  const expanded = expandDotPaths(node.attributes);
@@ -724,14 +750,14 @@ function prototypeDirectiveToJx(node: MdastNode) {
724
750
  if (key === "$prototype") {
725
751
  continue;
726
752
  }
727
- el[key] = value as JsonValue;
753
+ el[key] = value;
728
754
  }
729
755
  }
730
756
 
731
757
  const children = convertChildren(node.children ?? []);
732
- const template = children.find((c) => c != null && typeof c === "object");
758
+ const template = children.find((c): c is JxElement => !isString(c));
733
759
  if (template) {
734
- el.map = template as JxElement;
760
+ el.map = template;
735
761
  }
736
762
  return el;
737
763
  }
@@ -739,17 +765,14 @@ function prototypeDirectiveToJx(node: MdastNode) {
739
765
  /**
740
766
  * Convert an array of mdast children to Jx elements/strings.
741
767
  *
742
- * @param {MdastNode[]} children
768
+ * @param {MdastNode[]} [children] - Absent converts to no children
743
769
  * @returns {(JxElement | string)[]}
744
770
  */
745
- export function convertChildren(children: MdastNode[]) {
771
+ export function convertChildren(children?: MdastNode[]): (JxElement | string)[] {
746
772
  if (!children) {
747
773
  return [];
748
774
  }
749
- return children.flatMap((n) => mdastNodeToJx(n)).filter((c) => c != null) as (
750
- | JxElement
751
- | string
752
- )[];
775
+ return children.flatMap<JxElement | string>((n) => mdastNodeToJx(n) ?? []);
753
776
  }
754
777
 
755
778
  /**
@@ -762,28 +785,35 @@ export function convertChildren(children: MdastNode[]) {
762
785
  * @returns {object} Complete Jx JSON document
763
786
  */
764
787
  export function transpileJxMarkdown(source: string) {
765
- const processor = (unified as unknown as () => UnifiedProcessor)()
766
- .use(remarkParse)
767
- .use(remarkFrontmatter, ["yaml"])
768
- .use(remarkParseFrontmatter)
788
+ /* SAFETY: UnifiedProcessor declares only the four methods this pipeline calls (use, parse, runSync,
789
+ stringify) with the signatures unified 11 gives them; it is the processor narrowed, not changed.
790
+ `remark-parse`, `remark-frontmatter` and `remark-parse-frontmatter` are shorthand
791
+ `declare module` entries in the root types.d.ts, so their default imports are `any`; each
792
+ default-exports a unified attacher taking optional JSON settings, the UnifiedPlugin shape. */
793
+ const processor = (unified as () => UnifiedProcessor)()
794
+ .use(remarkParse as UnifiedPlugin)
795
+ .use(remarkFrontmatter as UnifiedPlugin, ["yaml"])
796
+ .use(remarkParseFrontmatter as UnifiedPlugin)
769
797
  .use(remarkGfm)
770
798
  .use(remarkDirective);
771
799
 
772
800
  const tree = processor.parse(source);
773
- const vfile = { data: {} };
801
+ const vfile: UnifiedFile = { data: {} };
774
802
  processor.runSync(tree, vfile);
775
803
 
776
- const frontmatter = (vfile.data as unknown as Record<string, unknown>)?.frontmatter ?? {};
804
+ const frontmatter = vfile.data.frontmatter ?? {};
777
805
 
778
806
  const doc: JxDocument = {};
779
807
 
780
- for (const [key, value] of Object.entries(frontmatter)) {
781
- doc[key] = value;
808
+ // A scalar or sequence front matter spreads exactly as `Object.entries` spreads it; numbers and
809
+ // Booleans have no entries.
810
+ if (!isNumber(frontmatter) && !isBoolean(frontmatter)) {
811
+ for (const [key, value] of Object.entries<JsonValue>(frontmatter)) {
812
+ doc[key] = value;
813
+ }
782
814
  }
783
815
 
784
- const bodyNodes = tree.children!.filter(
785
- (n: MdastNode) => n.type !== "yaml" && n.type !== "toml",
786
- ) as unknown as MdastNode[];
816
+ const bodyNodes = tree.children!.filter((n) => n.type !== "yaml" && n.type !== "toml");
787
817
 
788
818
  const children: (JxElement | string)[] = [];
789
819
 
@@ -852,13 +882,13 @@ const HEADING_TAGS = new Set(["h1", "h2", "h3", "h4", "h5", "h6"]);
852
882
  * @returns {string}
853
883
  */
854
884
  function jxNodeText(node: JxElement | string): string {
855
- if (typeof node === "string") {
885
+ if (isString(node)) {
856
886
  return node;
857
887
  }
858
- let out = typeof node.textContent === "string" ? node.textContent : "";
888
+ let out = isString(node.textContent) ? node.textContent : "";
859
889
  if (Array.isArray(node.children)) {
860
890
  for (const child of node.children) {
861
- out += jxNodeText(child as JxElement | string);
891
+ out += jxNodeText(child);
862
892
  }
863
893
  }
864
894
  return out;
@@ -879,15 +909,15 @@ export function assignHeadingIds(children?: (JxElement | string)[]): TocEntry[]
879
909
 
880
910
  function walk(nodes: (JxElement | string)[]) {
881
911
  for (const node of nodes) {
882
- if (typeof node !== "object" || node === null) {
912
+ if (!isNonNullObject(node)) {
883
913
  continue;
884
914
  }
885
915
  // Heading extraction reads a literal tag; a chosen one is never a heading (every candidate
886
916
  // Would have to be, and the construct exists to vary a WRAPPER), so it is simply not one.
887
- const tag = typeof node.tagName === "string" ? node.tagName : "";
917
+ const tag = isString(node.tagName) ? node.tagName : "";
888
918
  if (tag && HEADING_TAGS.has(tag)) {
889
919
  const text = jxNodeText(node).trim();
890
- let id = typeof node.id === "string" && node.id.length > 0 ? node.id : "";
920
+ let id = isString(node.id) && node.id.length > 0 ? node.id : "";
891
921
  if (id) {
892
922
  seen.set(id, (seen.get(id) ?? 0) + 1);
893
923
  } else {
@@ -900,7 +930,7 @@ export function assignHeadingIds(children?: (JxElement | string)[]): TocEntry[]
900
930
  entries.push({ depth: Number(tag.slice(1)), id, text });
901
931
  }
902
932
  if (Array.isArray(node.children)) {
903
- walk(node.children as (JxElement | string)[]);
933
+ walk(node.children);
904
934
  }
905
935
  }
906
936
  }