@jxsuite/parser 0.19.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/parser",
3
- "version": "0.19.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.18.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 {any[]}
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 {any[]} children
18
- * @returns {any[]}
19
+ * @param {HastNode[]} children
20
+ * @returns {(JxElement | string)[]}
19
21
  */
20
22
  function convertHastChildren(children) {
21
- /** @type {any[]} */
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 {any} node
32
- * @returns {any}
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 {Record<string, any>} */
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, any>} properties
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 {any} tree
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 {any} */ node) {
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 {any} node
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 {any} */ node) => {
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 {any} */
104
+ /** @type {MdastNode | null} */
105
105
  let firstParagraph = null;
106
- visit(tree, "paragraph", (/** @type {any} */ node) => {
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 {any} config - Processing options
121
- * @returns {object} MarkdownFileResult
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
- processor.runSync(tree, /** @type {any} */ (vfile));
138
+ // @ts-ignore - vfile shape satisfies unified's internal contract
139
+ processor.runSync(tree, vfile);
137
140
 
138
- const frontmatter = /** @type {any} */ (vfile.data).frontmatter ?? {};
139
- const plainText = mdastToString(tree);
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 = tree.children.filter(
145
- (/** @type {any} */ n) => n.type !== "yaml" && n.type !== "toml",
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 {any} obj
173
+ * @param {Record<string, unknown> | MarkdownFileResult} obj
165
174
  * @param {string} path
166
- * @returns {any}
175
+ * @returns {unknown}
167
176
  */
168
177
  function getNestedValue(obj, path) {
169
- return path.split(".").reduce((/** @type {any} */ o, k) => o?.[k], obj);
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 {any[]} [config.remarkPlugins] Default is `[]`
185
- * @param {any[]} [config.rehypePlugins] Default is `[]`
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 {object} MarkdownFileResult
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 {any[]} [config.remarkPlugins] Default is `[]`
224
- * @param {any[]} [config.rehypePlugins] Default is `[]`
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<object[]>} Array of MarkdownFileResult
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 {any} */ (filter));
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, any>} Nested object
69
+ * @returns {Record<string, unknown>} Nested object
70
70
  */
71
71
  export function expandDotPaths(attrs) {
72
- /** @type {Record<string, any>} */
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, any>} obj - Nested object
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, any>} */ node, /** @type {string} */ prefix) {
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, any>} styleObj
152
- * @returns {Record<string, any>}
151
+ * @param {Record<string, unknown>} styleObj
152
+ * @returns {Record<string, unknown>}
153
153
  */
154
154
  export function applyStyleKeyMapping(styleObj) {
155
- /** @type {Record<string, any>} */
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, any>}
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, any>} styleObj
188
+ * @param {Record<string, unknown>} styleObj
189
189
  * @returns {Record<string, string>}
190
190
  */
191
191
  export function collapseStylePaths(styleObj) {
192
- /** @type {Record<string, any>} */
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, any>; attributes: Record<string, 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, any>} */
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: any) => string>}
303
+ * @type {Record<string, (n: MdastNode) => string>}
304
304
  */
305
305
  const JX_TAG_MAP = {
306
- heading: (/** @type {any} */ n) => `h${n.depth}`,
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 {any} */ n) => (n.ordered ? "ol" : "ul"),
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 {any} */ n) => (n.isHeader ? "th" : "td"),
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 {any} node
329
- * @returns {any} Jx element or null
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 {Record<string, any>} */
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?.length > 0) {
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 {any} node
440
- * @returns {any}
439
+ * @param {MdastNode} node
440
+ * @returns {JxElement}
441
441
  */
442
442
  function directiveToJx(node) {
443
- /** @type {Record<string, any>} */
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?.length > 0) {
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?.length > 0) {
519
- /** @type {any[]} */
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 != null) jxChildren.push(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 != null) jxChildren.push(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 {any[]} children
554
- * @returns {any[]}
557
+ * @param {MdastNode[]} children
558
+ * @returns {(JxElement | string)[]}
555
559
  */
556
560
  export function convertChildren(children) {
557
561
  if (!children) return [];
558
- return children.flatMap(mdastNodeToJx).filter((c) => c != null);
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 {any} */ (vfile.data)?.frontmatter ?? {};
588
+ const frontmatter = /** @type {Record<string, unknown>} */ (/** @type {unknown} */ (vfile.data))
589
+ ?.frontmatter ?? {};
583
590
 
584
- /** @type {Record<string, any>} */
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 = tree.children.filter(
592
- (/** @type {any} */ n) => n.type !== "yaml" && n.type !== "toml",
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 {any[]} */
606
+ /** @type {(JxElement | string)[]} */
596
607
  const children = [];
597
608
 
598
609
  for (const node of bodyNodes) {