@jxsuite/parser 0.19.0 → 0.21.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/package.json +6 -2
- package/src/html-to-jx.js +14 -12
- package/src/md.js +44 -30
- package/src/transpile.js +61 -50
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/parser",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "Jx markdown parser and external class integration",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -23,12 +23,15 @@
|
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
25
|
"test": "bun test",
|
|
26
|
+
"test:coverage": "bun test --coverage",
|
|
26
27
|
"upgrade": "bunx npm-check-updates -u && bun install"
|
|
27
28
|
},
|
|
28
29
|
"dependencies": {
|
|
29
30
|
"glob": "^13.0.6",
|
|
30
31
|
"hast-util-from-html": "^2.0.3",
|
|
32
|
+
"hast-util-whitespace": "^3.0.0",
|
|
31
33
|
"mdast-util-to-string": "^4.0.0",
|
|
34
|
+
"property-information": "^7.1.0",
|
|
32
35
|
"rehype-stringify": "^10.0.1",
|
|
33
36
|
"remark-directive": "^4.0.0",
|
|
34
37
|
"remark-frontmatter": "^5.0.0",
|
|
@@ -41,6 +44,7 @@
|
|
|
41
44
|
},
|
|
42
45
|
"devDependencies": {
|
|
43
46
|
"@happy-dom/global-registrator": "^20.9.0",
|
|
44
|
-
"@jxsuite/runtime": "^0.
|
|
47
|
+
"@jxsuite/runtime": "^0.21.0",
|
|
48
|
+
"@types/hast": "^3.0.4"
|
|
45
49
|
}
|
|
46
50
|
}
|
package/src/html-to-jx.js
CHANGED
|
@@ -2,23 +2,25 @@ import { fromHtml } from "hast-util-from-html";
|
|
|
2
2
|
import { whitespace } from "hast-util-whitespace";
|
|
3
3
|
import { find, html as htmlInfo } from "property-information";
|
|
4
4
|
|
|
5
|
+
/** @typedef {import("hast").Nodes} HastNode */
|
|
6
|
+
|
|
5
7
|
/**
|
|
6
8
|
* Convert an HTML string into an array of Jx tree nodes.
|
|
7
9
|
*
|
|
8
10
|
* @param {string} htmlString
|
|
9
|
-
* @returns {
|
|
11
|
+
* @returns {(JxElement | string)[]}
|
|
10
12
|
*/
|
|
11
13
|
export function htmlToJx(htmlString) {
|
|
12
14
|
const hast = fromHtml(htmlString, { fragment: true });
|
|
13
|
-
return convertHastChildren(hast.children);
|
|
15
|
+
return convertHastChildren(/** @type {HastNode[]} */ (hast.children));
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
|
-
* @param {
|
|
18
|
-
* @returns {
|
|
19
|
+
* @param {HastNode[]} children
|
|
20
|
+
* @returns {(JxElement | string)[]}
|
|
19
21
|
*/
|
|
20
22
|
function convertHastChildren(children) {
|
|
21
|
-
/** @type {
|
|
23
|
+
/** @type {(JxElement | string)[]} */
|
|
22
24
|
const result = [];
|
|
23
25
|
for (const child of children) {
|
|
24
26
|
const converted = convertHastNode(child);
|
|
@@ -28,17 +30,17 @@ function convertHastChildren(children) {
|
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
/**
|
|
31
|
-
* @param {
|
|
32
|
-
* @returns {
|
|
33
|
+
* @param {HastNode} node
|
|
34
|
+
* @returns {JxElement | string | null}
|
|
33
35
|
*/
|
|
34
36
|
function convertHastNode(node) {
|
|
35
37
|
if (node.type === "text") {
|
|
36
|
-
if (whitespace(node)) return null;
|
|
37
|
-
return node.value;
|
|
38
|
+
if (whitespace(/** @type {HastNode} */ (/** @type {unknown} */ (node)))) return null;
|
|
39
|
+
return node.value ?? null;
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
if (node.type === "element") {
|
|
41
|
-
/** @type {
|
|
43
|
+
/** @type {JxElement} */
|
|
42
44
|
const el = { tagName: node.tagName };
|
|
43
45
|
|
|
44
46
|
if (node.properties && Object.keys(node.properties).length > 0) {
|
|
@@ -52,7 +54,7 @@ function convertHastNode(node) {
|
|
|
52
54
|
if (kids.length === 1 && typeof kids[0] === "string") {
|
|
53
55
|
el.textContent = kids[0];
|
|
54
56
|
} else if (kids.length > 0) {
|
|
55
|
-
el.children = kids;
|
|
57
|
+
el.children = /** @type {JxElement[]} */ (kids);
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
return el;
|
|
@@ -62,7 +64,7 @@ function convertHastNode(node) {
|
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
/**
|
|
65
|
-
* @param {Record<string,
|
|
67
|
+
* @param {Record<string, unknown>} properties
|
|
66
68
|
* @returns {{ style: Record<string, string>; attrs: Record<string, string> }}
|
|
67
69
|
*/
|
|
68
70
|
function hastPropsToJx(properties) {
|
package/src/md.js
CHANGED
|
@@ -27,7 +27,7 @@ import { mdastNodeToJx } from "./transpile.js";
|
|
|
27
27
|
/**
|
|
28
28
|
* Walk an AST tree, calling visitor for nodes matching the given type.
|
|
29
29
|
*
|
|
30
|
-
* @param {
|
|
30
|
+
* @param {object} tree
|
|
31
31
|
* @param {string | function} typeOrVisitor
|
|
32
32
|
* @param {function} [maybeVisitor]
|
|
33
33
|
*/
|
|
@@ -35,20 +35,20 @@ function visit(tree, typeOrVisitor, maybeVisitor) {
|
|
|
35
35
|
const type = typeof typeOrVisitor === "string" ? typeOrVisitor : null;
|
|
36
36
|
const visitor = type ? maybeVisitor : typeOrVisitor;
|
|
37
37
|
|
|
38
|
-
function walk(/** @type {
|
|
38
|
+
function walk(/** @type {MdastNode} */ node) {
|
|
39
39
|
if (!node || typeof node !== "object") return;
|
|
40
40
|
if (!type || node.type === type) /** @type {Function} */ (visitor)(node);
|
|
41
41
|
if (Array.isArray(node.children)) {
|
|
42
42
|
for (const child of node.children) walk(child);
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
-
walk(tree);
|
|
45
|
+
walk(/** @type {MdastNode} */ (tree));
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
49
|
* Serialize an mdast tree to plain text.
|
|
50
50
|
*
|
|
51
|
-
* @param {
|
|
51
|
+
* @param {MdastNode} node
|
|
52
52
|
* @returns {string}
|
|
53
53
|
*/
|
|
54
54
|
function mdastToString(node) {
|
|
@@ -81,7 +81,7 @@ function readingTime(text) {
|
|
|
81
81
|
function extractToc(tree) {
|
|
82
82
|
/** @type {{ depth: number; text: string; id: string }[]} */
|
|
83
83
|
const entries = [];
|
|
84
|
-
visit(tree, "heading", (/** @type {
|
|
84
|
+
visit(tree, "heading", (/** @type {MdastNode} */ node) => {
|
|
85
85
|
const text = mdastToString(node);
|
|
86
86
|
const id = text
|
|
87
87
|
.toLowerCase()
|
|
@@ -89,7 +89,7 @@ function extractToc(tree) {
|
|
|
89
89
|
.replace(/\s+/g, "-")
|
|
90
90
|
.replace(/-+/g, "-")
|
|
91
91
|
.replace(/^-|-$/g, "");
|
|
92
|
-
entries.push({ depth: node.depth, text, id });
|
|
92
|
+
entries.push({ depth: /** @type {number} */ (node.depth), text, id });
|
|
93
93
|
});
|
|
94
94
|
return entries;
|
|
95
95
|
}
|
|
@@ -101,9 +101,9 @@ function extractToc(tree) {
|
|
|
101
101
|
* @returns {string} Plain text of first paragraph, or empty string
|
|
102
102
|
*/
|
|
103
103
|
function extractExcerpt(tree) {
|
|
104
|
-
/** @type {
|
|
104
|
+
/** @type {MdastNode | null} */
|
|
105
105
|
let firstParagraph = null;
|
|
106
|
-
visit(tree, "paragraph", (/** @type {
|
|
106
|
+
visit(tree, "paragraph", (/** @type {MdastNode} */ node) => {
|
|
107
107
|
if (!firstParagraph) firstParagraph = node;
|
|
108
108
|
});
|
|
109
109
|
if (!firstParagraph) return "";
|
|
@@ -117,8 +117,10 @@ function extractExcerpt(tree) {
|
|
|
117
117
|
*
|
|
118
118
|
* @param {string} source - Raw markdown string
|
|
119
119
|
* @param {string} filePath - File path (for slug derivation)
|
|
120
|
-
* @param {
|
|
121
|
-
* @
|
|
120
|
+
* @param {object} config - Processing options
|
|
121
|
+
* @param {boolean} [config.directives] - Enable directive support
|
|
122
|
+
* @param {unknown} [config.directiveOptions] - Directive plugin options
|
|
123
|
+
* @returns {MarkdownFileResult}
|
|
122
124
|
*/
|
|
123
125
|
function processMarkdown(source, filePath, config = {}) {
|
|
124
126
|
let processor = unified()
|
|
@@ -133,18 +135,25 @@ function processMarkdown(source, filePath, config = {}) {
|
|
|
133
135
|
|
|
134
136
|
const tree = processor.parse(source);
|
|
135
137
|
const vfile = { data: {} };
|
|
136
|
-
|
|
138
|
+
// @ts-ignore - vfile shape satisfies unified's internal contract
|
|
139
|
+
processor.runSync(tree, vfile);
|
|
137
140
|
|
|
138
|
-
const
|
|
139
|
-
const
|
|
141
|
+
const vfileData = /** @type {Record<string, unknown>} */ (vfile.data);
|
|
142
|
+
const frontmatter = /** @type {Record<string, unknown>} */ (vfileData.frontmatter ?? {});
|
|
143
|
+
const mdTree = /** @type {MdastNode} */ (/** @type {unknown} */ (tree));
|
|
144
|
+
const plainText = mdastToString(mdTree);
|
|
140
145
|
const toc = extractToc(tree);
|
|
141
146
|
const excerpt = extractExcerpt(tree);
|
|
142
147
|
const slug = basename(filePath, extname(filePath));
|
|
143
148
|
|
|
144
|
-
const bodyNodes =
|
|
145
|
-
|
|
149
|
+
const bodyNodes = /** @type {MdastNode[]} */ (
|
|
150
|
+
tree.children.filter(
|
|
151
|
+
(/** @type {{ type: string }} */ n) => n.type !== "yaml" && n.type !== "toml",
|
|
152
|
+
)
|
|
153
|
+
);
|
|
154
|
+
const $children = /** @type {(JxElement | string)[]} */ (
|
|
155
|
+
bodyNodes.map(/** @type {(n: MdastNode) => any} */ (mdastNodeToJx)).filter(Boolean)
|
|
146
156
|
);
|
|
147
|
-
const $children = bodyNodes.map(mdastNodeToJx).filter(Boolean);
|
|
148
157
|
|
|
149
158
|
return {
|
|
150
159
|
slug,
|
|
@@ -161,12 +170,17 @@ function processMarkdown(source, filePath, config = {}) {
|
|
|
161
170
|
/**
|
|
162
171
|
* Resolve a dot-notation path within an object.
|
|
163
172
|
*
|
|
164
|
-
* @param {
|
|
173
|
+
* @param {Record<string, unknown> | MarkdownFileResult} obj
|
|
165
174
|
* @param {string} path
|
|
166
|
-
* @returns {
|
|
175
|
+
* @returns {unknown}
|
|
167
176
|
*/
|
|
168
177
|
function getNestedValue(obj, path) {
|
|
169
|
-
return path
|
|
178
|
+
return path
|
|
179
|
+
.split(".")
|
|
180
|
+
.reduce(
|
|
181
|
+
(/** @type {unknown} */ o, k) => /** @type {Record<string, unknown> | undefined} */ (o)?.[k],
|
|
182
|
+
/** @type {unknown} */ (obj),
|
|
183
|
+
);
|
|
170
184
|
}
|
|
171
185
|
|
|
172
186
|
// ─── MarkdownFile ─────────────────────────────────────────────────────────────
|
|
@@ -181,8 +195,8 @@ export class MarkdownFile {
|
|
|
181
195
|
/**
|
|
182
196
|
* @param {object} config
|
|
183
197
|
* @param {string} config.src - File path to markdown file
|
|
184
|
-
* @param {
|
|
185
|
-
* @param {
|
|
198
|
+
* @param {unknown[]} [config.remarkPlugins] Default is `[]`
|
|
199
|
+
* @param {unknown[]} [config.rehypePlugins] Default is `[]`
|
|
186
200
|
* @param {string} [config.basePath] - Base path for resolving src
|
|
187
201
|
* @param {boolean} [config.directives] - Enable directive support
|
|
188
202
|
*/
|
|
@@ -193,7 +207,7 @@ export class MarkdownFile {
|
|
|
193
207
|
/**
|
|
194
208
|
* Parse and resolve the markdown file.
|
|
195
209
|
*
|
|
196
|
-
* @returns {
|
|
210
|
+
* @returns {MarkdownFileResult}
|
|
197
211
|
*/
|
|
198
212
|
resolve() {
|
|
199
213
|
const { src, basePath, ...processorConfig } = this.config;
|
|
@@ -220,8 +234,8 @@ export class MarkdownCollection {
|
|
|
220
234
|
* @param {string} [config.sortOrder] Default is `'desc'`
|
|
221
235
|
* @param {number} [config.limit]
|
|
222
236
|
* @param {Function} [config.filter] - Filter function
|
|
223
|
-
* @param {
|
|
224
|
-
* @param {
|
|
237
|
+
* @param {unknown[]} [config.remarkPlugins] Default is `[]`
|
|
238
|
+
* @param {unknown[]} [config.rehypePlugins] Default is `[]`
|
|
225
239
|
* @param {string} [config.basePath] - Base path for resolving glob
|
|
226
240
|
* @param {boolean} [config.directives] - Enable directive support
|
|
227
241
|
*/
|
|
@@ -232,7 +246,7 @@ export class MarkdownCollection {
|
|
|
232
246
|
/**
|
|
233
247
|
* Glob files, parse each, sort, filter, and limit.
|
|
234
248
|
*
|
|
235
|
-
* @returns {Promise<
|
|
249
|
+
* @returns {Promise<MarkdownFileResult[]>} Array of MarkdownFileResult
|
|
236
250
|
*/
|
|
237
251
|
async resolve() {
|
|
238
252
|
const {
|
|
@@ -250,7 +264,7 @@ export class MarkdownCollection {
|
|
|
250
264
|
const pattern = resolved.split("\\").join("/");
|
|
251
265
|
const files = globSync(pattern, { absolute: true });
|
|
252
266
|
|
|
253
|
-
const results = files.map((filePath) => {
|
|
267
|
+
const results = files.map((/** @type {string} */ filePath) => {
|
|
254
268
|
const source = readFileSync(filePath, "utf-8");
|
|
255
269
|
return processMarkdown(source, filePath, processorConfig);
|
|
256
270
|
});
|
|
@@ -258,13 +272,13 @@ export class MarkdownCollection {
|
|
|
258
272
|
// Filter
|
|
259
273
|
let filtered = results;
|
|
260
274
|
if (typeof filter === "function") {
|
|
261
|
-
filtered = results.filter(/** @type {
|
|
275
|
+
filtered = results.filter(/** @type {(r: MarkdownFileResult) => boolean} */ (filter));
|
|
262
276
|
}
|
|
263
277
|
|
|
264
278
|
// Sort
|
|
265
|
-
filtered.sort((a, b) => {
|
|
266
|
-
const aVal = getNestedValue(a, sortBy) ?? "";
|
|
267
|
-
const bVal = getNestedValue(b, sortBy) ?? "";
|
|
279
|
+
filtered.sort((/** @type {MarkdownFileResult} */ a, /** @type {MarkdownFileResult} */ b) => {
|
|
280
|
+
const aVal = /** @type {string | number} */ (getNestedValue(a, sortBy) ?? "");
|
|
281
|
+
const bVal = /** @type {string | number} */ (getNestedValue(b, sortBy) ?? "");
|
|
268
282
|
if (aVal < bVal) return sortOrder === "asc" ? -1 : 1;
|
|
269
283
|
if (aVal > bVal) return sortOrder === "asc" ? 1 : -1;
|
|
270
284
|
return 0;
|
package/src/transpile.js
CHANGED
|
@@ -66,10 +66,10 @@ export function mdKey(key) {
|
|
|
66
66
|
* Expand dot-path attribute keys into nested objects.
|
|
67
67
|
*
|
|
68
68
|
* @param {Record<string, string>} attrs - Flat attribute map from remark-directive
|
|
69
|
-
* @returns {Record<string,
|
|
69
|
+
* @returns {Record<string, unknown>} Nested object
|
|
70
70
|
*/
|
|
71
71
|
export function expandDotPaths(attrs) {
|
|
72
|
-
/** @type {Record<string,
|
|
72
|
+
/** @type {Record<string, unknown>} */
|
|
73
73
|
const result = {};
|
|
74
74
|
|
|
75
75
|
for (const [key, value] of Object.entries(attrs)) {
|
|
@@ -86,7 +86,7 @@ export function expandDotPaths(attrs) {
|
|
|
86
86
|
if (!(seg in target) || typeof target[seg] !== "object") {
|
|
87
87
|
target[seg] = {};
|
|
88
88
|
}
|
|
89
|
-
target = target[seg];
|
|
89
|
+
target = /** @type {Record<string, unknown>} */ (target[seg]);
|
|
90
90
|
}
|
|
91
91
|
target[jxKey(segments[segments.length - 1])] = value;
|
|
92
92
|
}
|
|
@@ -97,18 +97,18 @@ export function expandDotPaths(attrs) {
|
|
|
97
97
|
/**
|
|
98
98
|
* Collapse a nested object back to dot-path flat attributes (inverse of expandDotPaths).
|
|
99
99
|
*
|
|
100
|
-
* @param {Record<string,
|
|
100
|
+
* @param {Record<string, unknown>} obj - Nested object
|
|
101
101
|
* @returns {Record<string, string>} Flat attribute map
|
|
102
102
|
*/
|
|
103
103
|
export function collapseDotPaths(obj) {
|
|
104
104
|
/** @type {Record<string, string>} */
|
|
105
105
|
const result = {};
|
|
106
106
|
|
|
107
|
-
function walk(/** @type {Record<string,
|
|
107
|
+
function walk(/** @type {Record<string, unknown>} */ node, /** @type {string} */ prefix) {
|
|
108
108
|
for (const [key, value] of Object.entries(node)) {
|
|
109
109
|
const path = prefix ? `${prefix}.${key}` : key;
|
|
110
110
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
111
|
-
walk(value, path);
|
|
111
|
+
walk(/** @type {Record<string, unknown>} */ (value), path);
|
|
112
112
|
} else {
|
|
113
113
|
result[path] = String(value);
|
|
114
114
|
}
|
|
@@ -148,11 +148,11 @@ const CSS_PSEUDO_NAMES = new Set([
|
|
|
148
148
|
* `:hover` (for known CSS pseudo-class names) - `--dark` → `@--dark` (for custom property / media
|
|
149
149
|
* query keys)
|
|
150
150
|
*
|
|
151
|
-
* @param {Record<string,
|
|
152
|
-
* @returns {Record<string,
|
|
151
|
+
* @param {Record<string, unknown>} styleObj
|
|
152
|
+
* @returns {Record<string, unknown>}
|
|
153
153
|
*/
|
|
154
154
|
export function applyStyleKeyMapping(styleObj) {
|
|
155
|
-
/** @type {Record<string,
|
|
155
|
+
/** @type {Record<string, unknown>} */
|
|
156
156
|
const result = {};
|
|
157
157
|
for (const [key, value] of Object.entries(styleObj)) {
|
|
158
158
|
if (CSS_PSEUDO_NAMES.has(key)) {
|
|
@@ -173,7 +173,7 @@ export function applyStyleKeyMapping(styleObj) {
|
|
|
173
173
|
* cannot appear at the start of remark-directive attribute keys.
|
|
174
174
|
*
|
|
175
175
|
* @param {Record<string, string>} attrs
|
|
176
|
-
* @returns {Record<string,
|
|
176
|
+
* @returns {Record<string, unknown>}
|
|
177
177
|
*/
|
|
178
178
|
export function expandStylePaths(attrs) {
|
|
179
179
|
return applyStyleKeyMapping(expandDotPaths(attrs));
|
|
@@ -185,11 +185,11 @@ export function expandStylePaths(attrs) {
|
|
|
185
185
|
* Strips `:` prefix from pseudo-class keys and `@` prefix from media keys before flattening with
|
|
186
186
|
* collapseDotPaths.
|
|
187
187
|
*
|
|
188
|
-
* @param {Record<string,
|
|
188
|
+
* @param {Record<string, unknown>} styleObj
|
|
189
189
|
* @returns {Record<string, string>}
|
|
190
190
|
*/
|
|
191
191
|
export function collapseStylePaths(styleObj) {
|
|
192
|
-
/** @type {Record<string,
|
|
192
|
+
/** @type {Record<string, unknown>} */
|
|
193
193
|
const normalized = {};
|
|
194
194
|
|
|
195
195
|
for (const [key, value] of Object.entries(styleObj)) {
|
|
@@ -271,24 +271,24 @@ const PHRASING_ELEMENTS = new Set([
|
|
|
271
271
|
* Route directive attributes to their correct Jx locations.
|
|
272
272
|
*
|
|
273
273
|
* @param {Record<string, string>} attrs
|
|
274
|
-
* @returns {{ props: Record<string,
|
|
274
|
+
* @returns {{ props: Record<string, unknown>; attributes: Record<string, string> }}
|
|
275
275
|
*/
|
|
276
276
|
function routeAttributes(attrs) {
|
|
277
277
|
const expanded = expandDotPaths(attrs);
|
|
278
278
|
|
|
279
279
|
// Apply style-key mapping (pseudo-classes, media queries) to the style sub-object
|
|
280
280
|
if (expanded.style && typeof expanded.style === "object") {
|
|
281
|
-
expanded.style = applyStyleKeyMapping(expanded.style);
|
|
281
|
+
expanded.style = applyStyleKeyMapping(/** @type {Record<string, unknown>} */ (expanded.style));
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
-
/** @type {Record<string,
|
|
284
|
+
/** @type {Record<string, unknown>} */
|
|
285
285
|
const props = {};
|
|
286
286
|
/** @type {Record<string, string>} */
|
|
287
287
|
const attributes = {};
|
|
288
288
|
|
|
289
289
|
for (const [key, value] of Object.entries(expanded)) {
|
|
290
290
|
if (HTML_ATTR_PATTERN.test(key)) {
|
|
291
|
-
attributes[key] = value;
|
|
291
|
+
attributes[key] = /** @type {string} */ (value);
|
|
292
292
|
} else {
|
|
293
293
|
props[key] = value;
|
|
294
294
|
}
|
|
@@ -300,10 +300,10 @@ function routeAttributes(attrs) {
|
|
|
300
300
|
/**
|
|
301
301
|
* Mdast node-type → Jx tagName mapping.
|
|
302
302
|
*
|
|
303
|
-
* @type {Record<string, (n:
|
|
303
|
+
* @type {Record<string, (n: MdastNode) => string>}
|
|
304
304
|
*/
|
|
305
305
|
const JX_TAG_MAP = {
|
|
306
|
-
heading: (/** @type {
|
|
306
|
+
heading: (/** @type {MdastNode} */ n) => `h${n.depth}`,
|
|
307
307
|
paragraph: () => "p",
|
|
308
308
|
emphasis: () => "em",
|
|
309
309
|
strong: () => "strong",
|
|
@@ -312,21 +312,21 @@ const JX_TAG_MAP = {
|
|
|
312
312
|
link: () => "a",
|
|
313
313
|
image: () => "img",
|
|
314
314
|
blockquote: () => "blockquote",
|
|
315
|
-
list: (/** @type {
|
|
315
|
+
list: (/** @type {MdastNode} */ n) => (n.ordered ? "ol" : "ul"),
|
|
316
316
|
listItem: () => "li",
|
|
317
317
|
code: () => "pre",
|
|
318
318
|
thematicBreak: () => "hr",
|
|
319
319
|
break: () => "br",
|
|
320
320
|
table: () => "table",
|
|
321
321
|
tableRow: () => "tr",
|
|
322
|
-
tableCell: (/** @type {
|
|
322
|
+
tableCell: (/** @type {MdastNode} */ n) => (n.isHeader ? "th" : "td"),
|
|
323
323
|
};
|
|
324
324
|
|
|
325
325
|
/**
|
|
326
326
|
* Convert a standard mdast node to a Jx element definition.
|
|
327
327
|
*
|
|
328
|
-
* @param {
|
|
329
|
-
* @returns {
|
|
328
|
+
* @param {MdastNode} node
|
|
329
|
+
* @returns {JxElement | string | (JxElement | string)[] | null} Jx element or null
|
|
330
330
|
*/
|
|
331
331
|
export function mdastNodeToJx(node) {
|
|
332
332
|
if (!node || typeof node !== "object") return null;
|
|
@@ -342,7 +342,7 @@ export function mdastNodeToJx(node) {
|
|
|
342
342
|
}
|
|
343
343
|
|
|
344
344
|
if (node.type === "text") {
|
|
345
|
-
return node.value;
|
|
345
|
+
return node.value ?? null;
|
|
346
346
|
}
|
|
347
347
|
|
|
348
348
|
if (node.type === "html") {
|
|
@@ -354,7 +354,7 @@ export function mdastNodeToJx(node) {
|
|
|
354
354
|
if (!tagFn) return null;
|
|
355
355
|
|
|
356
356
|
const tag = tagFn(node);
|
|
357
|
-
/** @type {
|
|
357
|
+
/** @type {JxElement} */
|
|
358
358
|
const el = { tagName: tag };
|
|
359
359
|
|
|
360
360
|
switch (node.type) {
|
|
@@ -367,7 +367,7 @@ export function mdastNodeToJx(node) {
|
|
|
367
367
|
case "listItem":
|
|
368
368
|
case "tableRow":
|
|
369
369
|
case "tableCell": {
|
|
370
|
-
const children = convertChildren(node.children);
|
|
370
|
+
const children = convertChildren(node.children ?? []);
|
|
371
371
|
if (children.length === 1 && typeof children[0] === "string") {
|
|
372
372
|
el.textContent = children[0];
|
|
373
373
|
} else if (children.length > 0) {
|
|
@@ -384,7 +384,7 @@ export function mdastNodeToJx(node) {
|
|
|
384
384
|
el.attributes = { href: node.url };
|
|
385
385
|
if (node.title) el.attributes.title = node.title;
|
|
386
386
|
{
|
|
387
|
-
const children = convertChildren(node.children);
|
|
387
|
+
const children = convertChildren(node.children ?? []);
|
|
388
388
|
if (children.length === 1 && typeof children[0] === "string") {
|
|
389
389
|
el.textContent = children[0];
|
|
390
390
|
} else if (children.length > 0) {
|
|
@@ -399,7 +399,7 @@ export function mdastNodeToJx(node) {
|
|
|
399
399
|
break;
|
|
400
400
|
|
|
401
401
|
case "list":
|
|
402
|
-
if (node.children
|
|
402
|
+
if (node.children && node.children.length > 0) {
|
|
403
403
|
el.children = convertChildren(node.children);
|
|
404
404
|
}
|
|
405
405
|
if (node.start != null && node.start !== 1) {
|
|
@@ -422,10 +422,10 @@ export function mdastNodeToJx(node) {
|
|
|
422
422
|
break;
|
|
423
423
|
|
|
424
424
|
case "table": {
|
|
425
|
-
const rows = convertChildren(node.children);
|
|
425
|
+
const rows = convertChildren(node.children ?? []);
|
|
426
426
|
const thead = rows.length > 0 ? { tagName: "thead", children: [rows[0]] } : null;
|
|
427
427
|
const tbody = rows.length > 1 ? { tagName: "tbody", children: rows.slice(1) } : null;
|
|
428
|
-
el.children = [thead, tbody].filter(Boolean);
|
|
428
|
+
el.children = /** @type {JxElement[]} */ ([thead, tbody].filter(Boolean));
|
|
429
429
|
break;
|
|
430
430
|
}
|
|
431
431
|
}
|
|
@@ -436,16 +436,16 @@ export function mdastNodeToJx(node) {
|
|
|
436
436
|
/**
|
|
437
437
|
* Convert a directive mdast node to a Jx element.
|
|
438
438
|
*
|
|
439
|
-
* @param {
|
|
440
|
-
* @returns {
|
|
439
|
+
* @param {MdastNode} node
|
|
440
|
+
* @returns {JxElement}
|
|
441
441
|
*/
|
|
442
442
|
function directiveToJx(node) {
|
|
443
|
-
/** @type {
|
|
444
|
-
const el = { tagName: node.name };
|
|
443
|
+
/** @type {JxElement} */
|
|
444
|
+
const el = { tagName: /** @type {string} */ (node.name) };
|
|
445
445
|
|
|
446
446
|
if (node.attributes && Object.keys(node.attributes).length > 0) {
|
|
447
447
|
const { props, attributes } = routeAttributes(node.attributes);
|
|
448
|
-
const isCustomElement = node.name.includes("-");
|
|
448
|
+
const isCustomElement = /** @type {string} */ (node.name).includes("-");
|
|
449
449
|
if (isCustomElement) {
|
|
450
450
|
// For custom elements:
|
|
451
451
|
// - style, children, textContent, innerHTML, $-prefixed → element-level
|
|
@@ -461,7 +461,7 @@ function directiveToJx(node) {
|
|
|
461
461
|
) {
|
|
462
462
|
el[key] = value;
|
|
463
463
|
} else if (key === "props") {
|
|
464
|
-
el.$props = value;
|
|
464
|
+
el.$props = /** @type {Record<string, unknown>} */ (value);
|
|
465
465
|
} else {
|
|
466
466
|
if (!el.attributes) el.attributes = {};
|
|
467
467
|
el.attributes[key] = value;
|
|
@@ -500,8 +500,8 @@ function directiveToJx(node) {
|
|
|
500
500
|
}
|
|
501
501
|
|
|
502
502
|
if (node.type === "textDirective") {
|
|
503
|
-
if (node.children
|
|
504
|
-
const children = convertChildren(node.children);
|
|
503
|
+
if (node.children && node.children.length > 0) {
|
|
504
|
+
const children = convertChildren(node.children ?? []);
|
|
505
505
|
if (children.length === 1 && typeof children[0] === "string") {
|
|
506
506
|
el.textContent = children[0];
|
|
507
507
|
} else if (children.length > 0) {
|
|
@@ -515,21 +515,25 @@ function directiveToJx(node) {
|
|
|
515
515
|
return el;
|
|
516
516
|
}
|
|
517
517
|
|
|
518
|
-
if (node.children
|
|
519
|
-
/** @type {
|
|
518
|
+
if (node.children && node.children.length > 0) {
|
|
519
|
+
/** @type {(JxElement | string)[]} */
|
|
520
520
|
const jxChildren = [];
|
|
521
|
-
const isPhrasingParent = PHRASING_ELEMENTS.has(node.name);
|
|
521
|
+
const isPhrasingParent = PHRASING_ELEMENTS.has(/** @type {string} */ (node.name));
|
|
522
522
|
|
|
523
523
|
for (const child of node.children) {
|
|
524
524
|
if (isPhrasingParent && child.type === "paragraph") {
|
|
525
525
|
// Unwrap: promote paragraph's inline children directly
|
|
526
526
|
for (const inline of child.children ?? []) {
|
|
527
527
|
const converted = mdastNodeToJx(inline);
|
|
528
|
-
if (converted
|
|
528
|
+
if (converted == null) continue;
|
|
529
|
+
if (Array.isArray(converted)) jxChildren.push(...converted);
|
|
530
|
+
else jxChildren.push(converted);
|
|
529
531
|
}
|
|
530
532
|
} else {
|
|
531
533
|
const converted = mdastNodeToJx(child);
|
|
532
|
-
if (converted
|
|
534
|
+
if (converted == null) continue;
|
|
535
|
+
if (Array.isArray(converted)) jxChildren.push(...converted);
|
|
536
|
+
else jxChildren.push(converted);
|
|
533
537
|
}
|
|
534
538
|
}
|
|
535
539
|
|
|
@@ -550,12 +554,14 @@ function directiveToJx(node) {
|
|
|
550
554
|
/**
|
|
551
555
|
* Convert an array of mdast children to Jx elements/strings.
|
|
552
556
|
*
|
|
553
|
-
* @param {
|
|
554
|
-
* @returns {
|
|
557
|
+
* @param {MdastNode[]} children
|
|
558
|
+
* @returns {(JxElement | string)[]}
|
|
555
559
|
*/
|
|
556
560
|
export function convertChildren(children) {
|
|
557
561
|
if (!children) return [];
|
|
558
|
-
return
|
|
562
|
+
return /** @type {(JxElement | string)[]} */ (
|
|
563
|
+
children.flatMap(mdastNodeToJx).filter((c) => c != null)
|
|
564
|
+
);
|
|
559
565
|
}
|
|
560
566
|
|
|
561
567
|
/**
|
|
@@ -579,20 +585,25 @@ export function transpileJxMarkdown(source) {
|
|
|
579
585
|
const vfile = { data: {} };
|
|
580
586
|
processor.runSync(tree, vfile);
|
|
581
587
|
|
|
582
|
-
const frontmatter = /** @type {
|
|
588
|
+
const frontmatter = /** @type {Record<string, unknown>} */ (/** @type {unknown} */ (vfile.data))
|
|
589
|
+
?.frontmatter ?? {};
|
|
583
590
|
|
|
584
|
-
/** @type {
|
|
591
|
+
/** @type {JxDocument} */
|
|
585
592
|
const doc = {};
|
|
586
593
|
|
|
587
594
|
for (const [key, value] of Object.entries(frontmatter)) {
|
|
588
595
|
doc[key] = value;
|
|
589
596
|
}
|
|
590
597
|
|
|
591
|
-
const bodyNodes =
|
|
592
|
-
|
|
598
|
+
const bodyNodes = /** @type {MdastNode[]} */ (
|
|
599
|
+
/** @type {unknown} */ (
|
|
600
|
+
tree.children.filter(
|
|
601
|
+
(/** @type {{ type: string }} */ n) => n.type !== "yaml" && n.type !== "toml",
|
|
602
|
+
)
|
|
603
|
+
)
|
|
593
604
|
);
|
|
594
605
|
|
|
595
|
-
/** @type {
|
|
606
|
+
/** @type {(JxElement | string)[]} */
|
|
596
607
|
const children = [];
|
|
597
608
|
|
|
598
609
|
for (const node of bodyNodes) {
|