@jxsuite/parser 2.0.1 → 3.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/dist/content-loader.js +608 -284
- package/dist/content.js +183 -30
- package/dist/markdown.js +394 -352
- package/dist/serialize.js +262 -232
- package/package.json +5 -5
- package/src/alerts.ts +22 -7
- package/src/content-data.ts +40 -0
- package/src/content-links.ts +15 -7
- package/src/content-loader.ts +681 -221
- package/src/content-routes.ts +23 -10
- package/src/content-rules.ts +40 -29
- package/src/content.ts +108 -70
- package/src/csv.ts +18 -14
- package/src/dates.ts +18 -13
- package/src/highlight.ts +6 -5
- package/src/inert.ts +7 -5
- package/src/markdown.ts +27 -23
- package/src/md.ts +133 -64
- package/src/serialize.ts +367 -266
- package/src/transpile.ts +182 -152
- package/src/types.ts +43 -11
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 {
|
|
106
|
+
* @returns {ExpandedAttributes} Nested object
|
|
91
107
|
*/
|
|
92
108
|
export function expandDotPaths(attrs: Record<string, string>) {
|
|
93
|
-
const result:
|
|
109
|
+
const result: ExpandedAttributes = {};
|
|
94
110
|
|
|
95
111
|
for (const [key, value] of Object.entries(attrs)) {
|
|
96
|
-
const
|
|
97
|
-
if (
|
|
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 (
|
|
105
|
-
const seg = jxKey(
|
|
106
|
-
|
|
107
|
-
|
|
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(
|
|
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 {
|
|
139
|
+
* @param {JsonObject} obj - Nested object
|
|
121
140
|
* @returns {Record<string, string>} Flat attribute map
|
|
122
141
|
*/
|
|
123
|
-
export function collapseDotPaths(obj:
|
|
142
|
+
export function collapseDotPaths(obj: JsonObject) {
|
|
124
143
|
const result: Record<string, string> = {};
|
|
125
144
|
|
|
126
|
-
function walk(node:
|
|
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 (
|
|
130
|
-
walk(value
|
|
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,
|
|
194
|
-
* @returns {Record<string,
|
|
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,
|
|
197
|
-
const result: Record<string,
|
|
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,
|
|
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 {
|
|
250
|
+
* @param {JsonObject} styleObj
|
|
232
251
|
* @returns {Record<string, string>}
|
|
233
252
|
*/
|
|
234
|
-
export function collapseStylePaths(styleObj:
|
|
235
|
-
const normalized:
|
|
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:
|
|
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
|
-
|
|
329
|
-
|
|
347
|
+
const { style } = expanded;
|
|
348
|
+
if (isNonNullObject(style)) {
|
|
349
|
+
expanded.style = applyStyleKeyMapping(style);
|
|
330
350
|
}
|
|
331
351
|
|
|
332
|
-
const props:
|
|
333
|
-
const attributes: Record<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
|
|
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
|
-
*
|
|
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
|
-
* @
|
|
405
|
+
* @param {JxElement} el - Mutated in place
|
|
406
|
+
* @param {(JxElement | string)[]} children
|
|
350
407
|
*/
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
444
|
-
|
|
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
|
-
|
|
464
|
-
alt: node.alt ?? "",
|
|
465
|
-
src: node.url ?? "",
|
|
466
|
-
};
|
|
494
|
+
el.attributes = { alt: node.alt ?? "", src: node.url ?? "" };
|
|
467
495
|
if (node.title) {
|
|
468
|
-
|
|
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
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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 &&
|
|
530
|
+
if (isNonNullObject(headerRow) && Array.isArray(headerRow.children)) {
|
|
506
531
|
for (const cell of headerRow.children) {
|
|
507
|
-
if (cell &&
|
|
508
|
-
|
|
532
|
+
if (isNonNullObject(cell) && cell.tagName === "td") {
|
|
533
|
+
cell.tagName = "th";
|
|
509
534
|
}
|
|
510
535
|
}
|
|
511
536
|
}
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
|
|
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
|
|
544
|
-
|
|
545
|
-
|
|
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
|
-
|
|
558
|
-
|
|
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(
|
|
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:
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
698
|
-
|
|
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:
|
|
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
|
|
753
|
+
el[key] = value;
|
|
728
754
|
}
|
|
729
755
|
}
|
|
730
756
|
|
|
731
757
|
const children = convertChildren(node.children ?? []);
|
|
732
|
-
const template = children.find((c)
|
|
758
|
+
const template = children.find((c): c is JxElement => !isString(c));
|
|
733
759
|
if (template) {
|
|
734
|
-
el.map = template
|
|
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
|
|
771
|
+
export function convertChildren(children?: MdastNode[]): (JxElement | string)[] {
|
|
746
772
|
if (!children) {
|
|
747
773
|
return [];
|
|
748
774
|
}
|
|
749
|
-
return children.flatMap((n) => mdastNodeToJx(n)
|
|
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
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
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 =
|
|
804
|
+
const frontmatter = vfile.data.frontmatter ?? {};
|
|
777
805
|
|
|
778
806
|
const doc: JxDocument = {};
|
|
779
807
|
|
|
780
|
-
|
|
781
|
-
|
|
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 (
|
|
885
|
+
if (isString(node)) {
|
|
856
886
|
return node;
|
|
857
887
|
}
|
|
858
|
-
let out =
|
|
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
|
|
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 (
|
|
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 =
|
|
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 =
|
|
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
|
|
933
|
+
walk(node.children);
|
|
904
934
|
}
|
|
905
935
|
}
|
|
906
936
|
}
|