@jxsuite/parser 0.18.0 → 0.20.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 +5 -2
- package/src/html-to-jx.js +14 -12
- package/src/md.js +44 -30
- package/src/transpile.js +74 -52
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/parser",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "Jx markdown parser and external class integration",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -28,7 +28,9 @@
|
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"glob": "^13.0.6",
|
|
30
30
|
"hast-util-from-html": "^2.0.3",
|
|
31
|
+
"hast-util-whitespace": "^3.0.0",
|
|
31
32
|
"mdast-util-to-string": "^4.0.0",
|
|
33
|
+
"property-information": "^7.1.0",
|
|
32
34
|
"rehype-stringify": "^10.0.1",
|
|
33
35
|
"remark-directive": "^4.0.0",
|
|
34
36
|
"remark-frontmatter": "^5.0.0",
|
|
@@ -41,6 +43,7 @@
|
|
|
41
43
|
},
|
|
42
44
|
"devDependencies": {
|
|
43
45
|
"@happy-dom/global-registrator": "^20.9.0",
|
|
44
|
-
"@jxsuite/runtime": "^0.
|
|
46
|
+
"@jxsuite/runtime": "^0.20.0",
|
|
47
|
+
"@types/hast": "^3.0.4"
|
|
45
48
|
}
|
|
46
49
|
}
|
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
|
@@ -29,13 +29,21 @@ export { htmlToJx };
|
|
|
29
29
|
const JX_DOLLAR_KEYS = new Set(["prototype", "ref", "component", "props", "switch", "elements"]);
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
*
|
|
32
|
+
* Annotation keys written as `--key` in markdown directives, mapped to `$key` in JX JSON. These use
|
|
33
|
+
* `--` prefix to avoid collision with HTML attributes like `title`.
|
|
34
|
+
*/
|
|
35
|
+
const JX_ANNOTATION_KEYS = new Set(["title", "description"]);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Re-add `$` prefix to known Jx reserved keywords and annotation keys.
|
|
33
39
|
*
|
|
34
40
|
* @param {string} key
|
|
35
41
|
* @returns {string}
|
|
36
42
|
*/
|
|
37
43
|
export function jxKey(key) {
|
|
38
|
-
|
|
44
|
+
if (JX_DOLLAR_KEYS.has(key)) return `$${key}`;
|
|
45
|
+
if (key.startsWith("--") && JX_ANNOTATION_KEYS.has(key.slice(2))) return `$${key.slice(2)}`;
|
|
46
|
+
return key;
|
|
39
47
|
}
|
|
40
48
|
|
|
41
49
|
/**
|
|
@@ -48,6 +56,9 @@ export function mdKey(key) {
|
|
|
48
56
|
if (key.startsWith("$") && JX_DOLLAR_KEYS.has(key.slice(1))) {
|
|
49
57
|
return key.slice(1);
|
|
50
58
|
}
|
|
59
|
+
if (key.startsWith("$") && JX_ANNOTATION_KEYS.has(key.slice(1))) {
|
|
60
|
+
return `--${key.slice(1)}`;
|
|
61
|
+
}
|
|
51
62
|
return key;
|
|
52
63
|
}
|
|
53
64
|
|
|
@@ -55,10 +66,10 @@ export function mdKey(key) {
|
|
|
55
66
|
* Expand dot-path attribute keys into nested objects.
|
|
56
67
|
*
|
|
57
68
|
* @param {Record<string, string>} attrs - Flat attribute map from remark-directive
|
|
58
|
-
* @returns {Record<string,
|
|
69
|
+
* @returns {Record<string, unknown>} Nested object
|
|
59
70
|
*/
|
|
60
71
|
export function expandDotPaths(attrs) {
|
|
61
|
-
/** @type {Record<string,
|
|
72
|
+
/** @type {Record<string, unknown>} */
|
|
62
73
|
const result = {};
|
|
63
74
|
|
|
64
75
|
for (const [key, value] of Object.entries(attrs)) {
|
|
@@ -75,7 +86,7 @@ export function expandDotPaths(attrs) {
|
|
|
75
86
|
if (!(seg in target) || typeof target[seg] !== "object") {
|
|
76
87
|
target[seg] = {};
|
|
77
88
|
}
|
|
78
|
-
target = target[seg];
|
|
89
|
+
target = /** @type {Record<string, unknown>} */ (target[seg]);
|
|
79
90
|
}
|
|
80
91
|
target[jxKey(segments[segments.length - 1])] = value;
|
|
81
92
|
}
|
|
@@ -86,18 +97,18 @@ export function expandDotPaths(attrs) {
|
|
|
86
97
|
/**
|
|
87
98
|
* Collapse a nested object back to dot-path flat attributes (inverse of expandDotPaths).
|
|
88
99
|
*
|
|
89
|
-
* @param {Record<string,
|
|
100
|
+
* @param {Record<string, unknown>} obj - Nested object
|
|
90
101
|
* @returns {Record<string, string>} Flat attribute map
|
|
91
102
|
*/
|
|
92
103
|
export function collapseDotPaths(obj) {
|
|
93
104
|
/** @type {Record<string, string>} */
|
|
94
105
|
const result = {};
|
|
95
106
|
|
|
96
|
-
function walk(/** @type {Record<string,
|
|
107
|
+
function walk(/** @type {Record<string, unknown>} */ node, /** @type {string} */ prefix) {
|
|
97
108
|
for (const [key, value] of Object.entries(node)) {
|
|
98
109
|
const path = prefix ? `${prefix}.${key}` : key;
|
|
99
110
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
100
|
-
walk(value, path);
|
|
111
|
+
walk(/** @type {Record<string, unknown>} */ (value), path);
|
|
101
112
|
} else {
|
|
102
113
|
result[path] = String(value);
|
|
103
114
|
}
|
|
@@ -137,11 +148,11 @@ const CSS_PSEUDO_NAMES = new Set([
|
|
|
137
148
|
* `:hover` (for known CSS pseudo-class names) - `--dark` → `@--dark` (for custom property / media
|
|
138
149
|
* query keys)
|
|
139
150
|
*
|
|
140
|
-
* @param {Record<string,
|
|
141
|
-
* @returns {Record<string,
|
|
151
|
+
* @param {Record<string, unknown>} styleObj
|
|
152
|
+
* @returns {Record<string, unknown>}
|
|
142
153
|
*/
|
|
143
154
|
export function applyStyleKeyMapping(styleObj) {
|
|
144
|
-
/** @type {Record<string,
|
|
155
|
+
/** @type {Record<string, unknown>} */
|
|
145
156
|
const result = {};
|
|
146
157
|
for (const [key, value] of Object.entries(styleObj)) {
|
|
147
158
|
if (CSS_PSEUDO_NAMES.has(key)) {
|
|
@@ -162,7 +173,7 @@ export function applyStyleKeyMapping(styleObj) {
|
|
|
162
173
|
* cannot appear at the start of remark-directive attribute keys.
|
|
163
174
|
*
|
|
164
175
|
* @param {Record<string, string>} attrs
|
|
165
|
-
* @returns {Record<string,
|
|
176
|
+
* @returns {Record<string, unknown>}
|
|
166
177
|
*/
|
|
167
178
|
export function expandStylePaths(attrs) {
|
|
168
179
|
return applyStyleKeyMapping(expandDotPaths(attrs));
|
|
@@ -174,11 +185,11 @@ export function expandStylePaths(attrs) {
|
|
|
174
185
|
* Strips `:` prefix from pseudo-class keys and `@` prefix from media keys before flattening with
|
|
175
186
|
* collapseDotPaths.
|
|
176
187
|
*
|
|
177
|
-
* @param {Record<string,
|
|
188
|
+
* @param {Record<string, unknown>} styleObj
|
|
178
189
|
* @returns {Record<string, string>}
|
|
179
190
|
*/
|
|
180
191
|
export function collapseStylePaths(styleObj) {
|
|
181
|
-
/** @type {Record<string,
|
|
192
|
+
/** @type {Record<string, unknown>} */
|
|
182
193
|
const normalized = {};
|
|
183
194
|
|
|
184
195
|
for (const [key, value] of Object.entries(styleObj)) {
|
|
@@ -260,24 +271,24 @@ const PHRASING_ELEMENTS = new Set([
|
|
|
260
271
|
* Route directive attributes to their correct Jx locations.
|
|
261
272
|
*
|
|
262
273
|
* @param {Record<string, string>} attrs
|
|
263
|
-
* @returns {{ props: Record<string,
|
|
274
|
+
* @returns {{ props: Record<string, unknown>; attributes: Record<string, string> }}
|
|
264
275
|
*/
|
|
265
276
|
function routeAttributes(attrs) {
|
|
266
277
|
const expanded = expandDotPaths(attrs);
|
|
267
278
|
|
|
268
279
|
// Apply style-key mapping (pseudo-classes, media queries) to the style sub-object
|
|
269
280
|
if (expanded.style && typeof expanded.style === "object") {
|
|
270
|
-
expanded.style = applyStyleKeyMapping(expanded.style);
|
|
281
|
+
expanded.style = applyStyleKeyMapping(/** @type {Record<string, unknown>} */ (expanded.style));
|
|
271
282
|
}
|
|
272
283
|
|
|
273
|
-
/** @type {Record<string,
|
|
284
|
+
/** @type {Record<string, unknown>} */
|
|
274
285
|
const props = {};
|
|
275
286
|
/** @type {Record<string, string>} */
|
|
276
287
|
const attributes = {};
|
|
277
288
|
|
|
278
289
|
for (const [key, value] of Object.entries(expanded)) {
|
|
279
290
|
if (HTML_ATTR_PATTERN.test(key)) {
|
|
280
|
-
attributes[key] = value;
|
|
291
|
+
attributes[key] = /** @type {string} */ (value);
|
|
281
292
|
} else {
|
|
282
293
|
props[key] = value;
|
|
283
294
|
}
|
|
@@ -289,10 +300,10 @@ function routeAttributes(attrs) {
|
|
|
289
300
|
/**
|
|
290
301
|
* Mdast node-type → Jx tagName mapping.
|
|
291
302
|
*
|
|
292
|
-
* @type {Record<string, (n:
|
|
303
|
+
* @type {Record<string, (n: MdastNode) => string>}
|
|
293
304
|
*/
|
|
294
305
|
const JX_TAG_MAP = {
|
|
295
|
-
heading: (/** @type {
|
|
306
|
+
heading: (/** @type {MdastNode} */ n) => `h${n.depth}`,
|
|
296
307
|
paragraph: () => "p",
|
|
297
308
|
emphasis: () => "em",
|
|
298
309
|
strong: () => "strong",
|
|
@@ -301,21 +312,21 @@ const JX_TAG_MAP = {
|
|
|
301
312
|
link: () => "a",
|
|
302
313
|
image: () => "img",
|
|
303
314
|
blockquote: () => "blockquote",
|
|
304
|
-
list: (/** @type {
|
|
315
|
+
list: (/** @type {MdastNode} */ n) => (n.ordered ? "ol" : "ul"),
|
|
305
316
|
listItem: () => "li",
|
|
306
317
|
code: () => "pre",
|
|
307
318
|
thematicBreak: () => "hr",
|
|
308
319
|
break: () => "br",
|
|
309
320
|
table: () => "table",
|
|
310
321
|
tableRow: () => "tr",
|
|
311
|
-
tableCell: (/** @type {
|
|
322
|
+
tableCell: (/** @type {MdastNode} */ n) => (n.isHeader ? "th" : "td"),
|
|
312
323
|
};
|
|
313
324
|
|
|
314
325
|
/**
|
|
315
326
|
* Convert a standard mdast node to a Jx element definition.
|
|
316
327
|
*
|
|
317
|
-
* @param {
|
|
318
|
-
* @returns {
|
|
328
|
+
* @param {MdastNode} node
|
|
329
|
+
* @returns {JxElement | string | (JxElement | string)[] | null} Jx element or null
|
|
319
330
|
*/
|
|
320
331
|
export function mdastNodeToJx(node) {
|
|
321
332
|
if (!node || typeof node !== "object") return null;
|
|
@@ -331,7 +342,7 @@ export function mdastNodeToJx(node) {
|
|
|
331
342
|
}
|
|
332
343
|
|
|
333
344
|
if (node.type === "text") {
|
|
334
|
-
return node.value;
|
|
345
|
+
return node.value ?? null;
|
|
335
346
|
}
|
|
336
347
|
|
|
337
348
|
if (node.type === "html") {
|
|
@@ -343,7 +354,7 @@ export function mdastNodeToJx(node) {
|
|
|
343
354
|
if (!tagFn) return null;
|
|
344
355
|
|
|
345
356
|
const tag = tagFn(node);
|
|
346
|
-
/** @type {
|
|
357
|
+
/** @type {JxElement} */
|
|
347
358
|
const el = { tagName: tag };
|
|
348
359
|
|
|
349
360
|
switch (node.type) {
|
|
@@ -356,7 +367,7 @@ export function mdastNodeToJx(node) {
|
|
|
356
367
|
case "listItem":
|
|
357
368
|
case "tableRow":
|
|
358
369
|
case "tableCell": {
|
|
359
|
-
const children = convertChildren(node.children);
|
|
370
|
+
const children = convertChildren(node.children ?? []);
|
|
360
371
|
if (children.length === 1 && typeof children[0] === "string") {
|
|
361
372
|
el.textContent = children[0];
|
|
362
373
|
} else if (children.length > 0) {
|
|
@@ -373,7 +384,7 @@ export function mdastNodeToJx(node) {
|
|
|
373
384
|
el.attributes = { href: node.url };
|
|
374
385
|
if (node.title) el.attributes.title = node.title;
|
|
375
386
|
{
|
|
376
|
-
const children = convertChildren(node.children);
|
|
387
|
+
const children = convertChildren(node.children ?? []);
|
|
377
388
|
if (children.length === 1 && typeof children[0] === "string") {
|
|
378
389
|
el.textContent = children[0];
|
|
379
390
|
} else if (children.length > 0) {
|
|
@@ -388,7 +399,7 @@ export function mdastNodeToJx(node) {
|
|
|
388
399
|
break;
|
|
389
400
|
|
|
390
401
|
case "list":
|
|
391
|
-
if (node.children
|
|
402
|
+
if (node.children && node.children.length > 0) {
|
|
392
403
|
el.children = convertChildren(node.children);
|
|
393
404
|
}
|
|
394
405
|
if (node.start != null && node.start !== 1) {
|
|
@@ -411,10 +422,10 @@ export function mdastNodeToJx(node) {
|
|
|
411
422
|
break;
|
|
412
423
|
|
|
413
424
|
case "table": {
|
|
414
|
-
const rows = convertChildren(node.children);
|
|
425
|
+
const rows = convertChildren(node.children ?? []);
|
|
415
426
|
const thead = rows.length > 0 ? { tagName: "thead", children: [rows[0]] } : null;
|
|
416
427
|
const tbody = rows.length > 1 ? { tagName: "tbody", children: rows.slice(1) } : null;
|
|
417
|
-
el.children = [thead, tbody].filter(Boolean);
|
|
428
|
+
el.children = /** @type {JxElement[]} */ ([thead, tbody].filter(Boolean));
|
|
418
429
|
break;
|
|
419
430
|
}
|
|
420
431
|
}
|
|
@@ -425,16 +436,16 @@ export function mdastNodeToJx(node) {
|
|
|
425
436
|
/**
|
|
426
437
|
* Convert a directive mdast node to a Jx element.
|
|
427
438
|
*
|
|
428
|
-
* @param {
|
|
429
|
-
* @returns {
|
|
439
|
+
* @param {MdastNode} node
|
|
440
|
+
* @returns {JxElement}
|
|
430
441
|
*/
|
|
431
442
|
function directiveToJx(node) {
|
|
432
|
-
/** @type {
|
|
433
|
-
const el = { tagName: node.name };
|
|
443
|
+
/** @type {JxElement} */
|
|
444
|
+
const el = { tagName: /** @type {string} */ (node.name) };
|
|
434
445
|
|
|
435
446
|
if (node.attributes && Object.keys(node.attributes).length > 0) {
|
|
436
447
|
const { props, attributes } = routeAttributes(node.attributes);
|
|
437
|
-
const isCustomElement = node.name.includes("-");
|
|
448
|
+
const isCustomElement = /** @type {string} */ (node.name).includes("-");
|
|
438
449
|
if (isCustomElement) {
|
|
439
450
|
// For custom elements:
|
|
440
451
|
// - style, children, textContent, innerHTML, $-prefixed → element-level
|
|
@@ -450,7 +461,7 @@ function directiveToJx(node) {
|
|
|
450
461
|
) {
|
|
451
462
|
el[key] = value;
|
|
452
463
|
} else if (key === "props") {
|
|
453
|
-
el.$props = value;
|
|
464
|
+
el.$props = /** @type {Record<string, unknown>} */ (value);
|
|
454
465
|
} else {
|
|
455
466
|
if (!el.attributes) el.attributes = {};
|
|
456
467
|
el.attributes[key] = value;
|
|
@@ -489,8 +500,8 @@ function directiveToJx(node) {
|
|
|
489
500
|
}
|
|
490
501
|
|
|
491
502
|
if (node.type === "textDirective") {
|
|
492
|
-
if (node.children
|
|
493
|
-
const children = convertChildren(node.children);
|
|
503
|
+
if (node.children && node.children.length > 0) {
|
|
504
|
+
const children = convertChildren(node.children ?? []);
|
|
494
505
|
if (children.length === 1 && typeof children[0] === "string") {
|
|
495
506
|
el.textContent = children[0];
|
|
496
507
|
} else if (children.length > 0) {
|
|
@@ -504,21 +515,25 @@ function directiveToJx(node) {
|
|
|
504
515
|
return el;
|
|
505
516
|
}
|
|
506
517
|
|
|
507
|
-
if (node.children
|
|
508
|
-
/** @type {
|
|
518
|
+
if (node.children && node.children.length > 0) {
|
|
519
|
+
/** @type {(JxElement | string)[]} */
|
|
509
520
|
const jxChildren = [];
|
|
510
|
-
const isPhrasingParent = PHRASING_ELEMENTS.has(node.name);
|
|
521
|
+
const isPhrasingParent = PHRASING_ELEMENTS.has(/** @type {string} */ (node.name));
|
|
511
522
|
|
|
512
523
|
for (const child of node.children) {
|
|
513
524
|
if (isPhrasingParent && child.type === "paragraph") {
|
|
514
525
|
// Unwrap: promote paragraph's inline children directly
|
|
515
526
|
for (const inline of child.children ?? []) {
|
|
516
527
|
const converted = mdastNodeToJx(inline);
|
|
517
|
-
if (converted
|
|
528
|
+
if (converted == null) continue;
|
|
529
|
+
if (Array.isArray(converted)) jxChildren.push(...converted);
|
|
530
|
+
else jxChildren.push(converted);
|
|
518
531
|
}
|
|
519
532
|
} else {
|
|
520
533
|
const converted = mdastNodeToJx(child);
|
|
521
|
-
if (converted
|
|
534
|
+
if (converted == null) continue;
|
|
535
|
+
if (Array.isArray(converted)) jxChildren.push(...converted);
|
|
536
|
+
else jxChildren.push(converted);
|
|
522
537
|
}
|
|
523
538
|
}
|
|
524
539
|
|
|
@@ -539,12 +554,14 @@ function directiveToJx(node) {
|
|
|
539
554
|
/**
|
|
540
555
|
* Convert an array of mdast children to Jx elements/strings.
|
|
541
556
|
*
|
|
542
|
-
* @param {
|
|
543
|
-
* @returns {
|
|
557
|
+
* @param {MdastNode[]} children
|
|
558
|
+
* @returns {(JxElement | string)[]}
|
|
544
559
|
*/
|
|
545
560
|
export function convertChildren(children) {
|
|
546
561
|
if (!children) return [];
|
|
547
|
-
return
|
|
562
|
+
return /** @type {(JxElement | string)[]} */ (
|
|
563
|
+
children.flatMap(mdastNodeToJx).filter((c) => c != null)
|
|
564
|
+
);
|
|
548
565
|
}
|
|
549
566
|
|
|
550
567
|
/**
|
|
@@ -568,20 +585,25 @@ export function transpileJxMarkdown(source) {
|
|
|
568
585
|
const vfile = { data: {} };
|
|
569
586
|
processor.runSync(tree, vfile);
|
|
570
587
|
|
|
571
|
-
const frontmatter = /** @type {
|
|
588
|
+
const frontmatter = /** @type {Record<string, unknown>} */ (/** @type {unknown} */ (vfile.data))
|
|
589
|
+
?.frontmatter ?? {};
|
|
572
590
|
|
|
573
|
-
/** @type {
|
|
591
|
+
/** @type {JxDocument} */
|
|
574
592
|
const doc = {};
|
|
575
593
|
|
|
576
594
|
for (const [key, value] of Object.entries(frontmatter)) {
|
|
577
595
|
doc[key] = value;
|
|
578
596
|
}
|
|
579
597
|
|
|
580
|
-
const bodyNodes =
|
|
581
|
-
|
|
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
|
+
)
|
|
582
604
|
);
|
|
583
605
|
|
|
584
|
-
/** @type {
|
|
606
|
+
/** @type {(JxElement | string)[]} */
|
|
585
607
|
const children = [];
|
|
586
608
|
|
|
587
609
|
for (const node of bodyNodes) {
|