@jxsuite/parser 0.6.1 → 0.7.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 +3 -2
- package/src/MarkdownCollection.class.json +1 -1
- package/src/MarkdownFile.class.json +1 -1
- package/src/html-to-jx.js +84 -0
- package/src/md.js +37 -129
- package/src/transpile.js +10 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/parser",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Jx markdown parser and external class integration",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"glob": "^13.0.6",
|
|
30
|
+
"hast-util-from-html": "^2.0.3",
|
|
30
31
|
"mdast-util-to-string": "^4.0.0",
|
|
31
32
|
"rehype-stringify": "^10.0.1",
|
|
32
33
|
"remark-directive": "^4.0.0",
|
|
@@ -40,6 +41,6 @@
|
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
|
42
43
|
"@happy-dom/global-registrator": "^20.9.0",
|
|
43
|
-
"@jxsuite/runtime": "^0.
|
|
44
|
+
"@jxsuite/runtime": "^0.6.2"
|
|
44
45
|
}
|
|
45
46
|
}
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"type": "object",
|
|
60
60
|
"description": "Shape defined by itemSchema parameter"
|
|
61
61
|
},
|
|
62
|
-
"$
|
|
62
|
+
"$children": { "type": "array", "description": "JX node tree" },
|
|
63
63
|
"$excerpt": { "type": "string", "description": "First paragraph as plain text" },
|
|
64
64
|
"$toc": {
|
|
65
65
|
"type": "array",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"slug": { "type": "string", "description": "Filename without extension" },
|
|
36
36
|
"path": { "type": "string", "description": "Absolute file path" },
|
|
37
37
|
"frontmatter": { "type": "object", "description": "YAML frontmatter key-value pairs" },
|
|
38
|
-
"$
|
|
38
|
+
"$children": { "type": "array", "description": "JX node tree" },
|
|
39
39
|
"$excerpt": { "type": "string", "description": "First paragraph as plain text" },
|
|
40
40
|
"$toc": {
|
|
41
41
|
"type": "array",
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { fromHtml } from "hast-util-from-html";
|
|
2
|
+
import { whitespace } from "hast-util-whitespace";
|
|
3
|
+
import { find, html as htmlInfo } from "property-information";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Convert an HTML string into an array of Jx tree nodes.
|
|
7
|
+
*
|
|
8
|
+
* @param {string} htmlString
|
|
9
|
+
* @returns {any[]}
|
|
10
|
+
*/
|
|
11
|
+
export function htmlToJx(htmlString) {
|
|
12
|
+
const hast = fromHtml(htmlString, { fragment: true });
|
|
13
|
+
return convertHastChildren(hast.children);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {any[]} children
|
|
18
|
+
* @returns {any[]}
|
|
19
|
+
*/
|
|
20
|
+
function convertHastChildren(children) {
|
|
21
|
+
/** @type {any[]} */
|
|
22
|
+
const result = [];
|
|
23
|
+
for (const child of children) {
|
|
24
|
+
const converted = convertHastNode(child);
|
|
25
|
+
if (converted != null) result.push(converted);
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {any} node
|
|
32
|
+
* @returns {any}
|
|
33
|
+
*/
|
|
34
|
+
function convertHastNode(node) {
|
|
35
|
+
if (node.type === "text") {
|
|
36
|
+
if (whitespace(node)) return null;
|
|
37
|
+
return node.value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (node.type === "element") {
|
|
41
|
+
/** @type {Record<string, any>} */
|
|
42
|
+
const el = { tagName: node.tagName };
|
|
43
|
+
|
|
44
|
+
if (node.properties && Object.keys(node.properties).length > 0) {
|
|
45
|
+
el.attributes = hastPropsToAttributes(node.properties);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const kids = node.children ? convertHastChildren(node.children) : [];
|
|
49
|
+
|
|
50
|
+
if (kids.length === 1 && typeof kids[0] === "string") {
|
|
51
|
+
el.textContent = kids[0];
|
|
52
|
+
} else if (kids.length > 0) {
|
|
53
|
+
el.children = kids;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return el;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {Record<string, any>} properties
|
|
64
|
+
* @returns {Record<string, string>}
|
|
65
|
+
*/
|
|
66
|
+
function hastPropsToAttributes(properties) {
|
|
67
|
+
/** @type {Record<string, string>} */
|
|
68
|
+
const attrs = {};
|
|
69
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
70
|
+
if (value === false || value === undefined || value === null) continue;
|
|
71
|
+
|
|
72
|
+
const info = find(htmlInfo, key);
|
|
73
|
+
const name = info.attribute;
|
|
74
|
+
|
|
75
|
+
if (value === true) {
|
|
76
|
+
attrs[name] = "";
|
|
77
|
+
} else if (Array.isArray(value)) {
|
|
78
|
+
attrs[name] = value.join(info.commaSeparated ? ", " : " ");
|
|
79
|
+
} else {
|
|
80
|
+
attrs[name] = String(value);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return attrs;
|
|
84
|
+
}
|
package/src/md.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* jxsuite/md — Markdown integration for Jx
|
|
3
3
|
*
|
|
4
|
-
* Provides
|
|
4
|
+
* Provides two exports:
|
|
5
5
|
* - MarkdownFile — Parse a single markdown file (external class for $prototype)
|
|
6
6
|
* - MarkdownCollection — Parse a glob of markdown files as a content collection
|
|
7
|
-
* - MarkdownDirective — Remark plugin mapping directives to custom element tags
|
|
8
7
|
*
|
|
9
|
-
* Built on the unified/remark
|
|
8
|
+
* Built on the unified/remark ecosystem. Converts MDAST to JX node trees via mdastNodeToJx.
|
|
10
9
|
*
|
|
11
10
|
* @module @jxsuite/md
|
|
12
11
|
* @license MIT
|
|
@@ -18,11 +17,10 @@ import remarkFrontmatter from "remark-frontmatter";
|
|
|
18
17
|
import remarkParseFrontmatter from "remark-parse-frontmatter";
|
|
19
18
|
import remarkGfm from "remark-gfm";
|
|
20
19
|
import remarkDirective from "remark-directive";
|
|
21
|
-
import remarkRehype from "remark-rehype";
|
|
22
|
-
import rehypeStringify from "rehype-stringify";
|
|
23
20
|
import { readFileSync } from "node:fs";
|
|
24
21
|
import { basename, extname, resolve as resolvePath } from "node:path";
|
|
25
22
|
import { globSync } from "glob";
|
|
23
|
+
import { mdastNodeToJx } from "./transpile.js";
|
|
26
24
|
|
|
27
25
|
// ─── Tree utilities (inline to avoid Bun ESM resolution issues with unist-util-*) ──
|
|
28
26
|
|
|
@@ -97,91 +95,62 @@ function extractToc(tree) {
|
|
|
97
95
|
}
|
|
98
96
|
|
|
99
97
|
/**
|
|
100
|
-
* Extract first paragraph as
|
|
98
|
+
* Extract first paragraph as a JX text string from an mdast tree.
|
|
101
99
|
*
|
|
102
100
|
* @param {object} tree - Mdast AST
|
|
103
|
-
* @returns {
|
|
101
|
+
* @returns {string} Plain text of first paragraph, or empty string
|
|
104
102
|
*/
|
|
105
|
-
|
|
103
|
+
function extractExcerpt(tree) {
|
|
106
104
|
/** @type {any} */
|
|
107
105
|
let firstParagraph = null;
|
|
108
106
|
visit(tree, "paragraph", (/** @type {any} */ node) => {
|
|
109
107
|
if (!firstParagraph) firstParagraph = node;
|
|
110
108
|
});
|
|
111
109
|
if (!firstParagraph) return "";
|
|
112
|
-
|
|
113
|
-
const result = await unified()
|
|
114
|
-
.use(remarkRehype)
|
|
115
|
-
.use(rehypeStringify)
|
|
116
|
-
.stringify(
|
|
117
|
-
/** @type {any} */ (await unified().use(remarkRehype).run(/** @type {any} */ (excerptTree))),
|
|
118
|
-
);
|
|
119
|
-
return String(result);
|
|
110
|
+
return mdastToString(firstParagraph);
|
|
120
111
|
}
|
|
121
112
|
|
|
122
113
|
/**
|
|
123
|
-
*
|
|
114
|
+
* Process a single markdown source string into a MarkdownFileResult.
|
|
115
|
+
*
|
|
116
|
+
* Converts the MDAST directly to JX nodes via mdastNodeToJx — no rehype/HTML intermediary.
|
|
124
117
|
*
|
|
125
|
-
* @param {
|
|
126
|
-
* @param {
|
|
127
|
-
* @param {any
|
|
128
|
-
* @
|
|
129
|
-
* @returns {any} Unified processor
|
|
118
|
+
* @param {string} source - Raw markdown string
|
|
119
|
+
* @param {string} filePath - File path (for slug derivation)
|
|
120
|
+
* @param {any} config - Processing options
|
|
121
|
+
* @returns {object} MarkdownFileResult
|
|
130
122
|
*/
|
|
131
|
-
function
|
|
123
|
+
function processMarkdown(source, filePath, config = {}) {
|
|
132
124
|
let processor = unified()
|
|
133
125
|
.use(remarkParse)
|
|
134
126
|
.use(remarkFrontmatter, ["yaml"])
|
|
135
127
|
.use(remarkParseFrontmatter)
|
|
136
|
-
.use(remarkGfm)
|
|
137
|
-
.use(remarkDirective)
|
|
138
|
-
.use(/** @type {any} */ (MarkdownDirective), config.directiveOptions ?? {});
|
|
128
|
+
.use(remarkGfm);
|
|
139
129
|
|
|
140
|
-
|
|
141
|
-
processor =
|
|
130
|
+
if (config.directives || config.directiveOptions) {
|
|
131
|
+
processor = processor.use(remarkDirective);
|
|
142
132
|
}
|
|
143
133
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
processor = Array.isArray(plugin) ? processor.use(plugin[0], plugin[1]) : processor.use(plugin);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
processor = /** @type {any} */ (
|
|
151
|
-
processor.use(rehypeStringify, /** @type {any} */ ({ allowDangerousHtml: true }))
|
|
152
|
-
);
|
|
153
|
-
|
|
154
|
-
return processor;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Process a single markdown source string into a MarkdownFileResult.
|
|
159
|
-
*
|
|
160
|
-
* @param {string} source - Raw markdown string
|
|
161
|
-
* @param {string} filePath - File path (for slug derivation)
|
|
162
|
-
* @param {any} config - Processing options
|
|
163
|
-
* @returns {Promise<object>} MarkdownFileResult
|
|
164
|
-
*/
|
|
165
|
-
async function processMarkdown(source, filePath, config = {}) {
|
|
166
|
-
const processor = buildProcessor(config);
|
|
167
|
-
|
|
168
|
-
const vfile = await processor.process(source);
|
|
169
|
-
const frontmatter = /** @type {any} */ (vfile.data)?.frontmatter ?? {};
|
|
170
|
-
|
|
171
|
-
// Parse a separate tree for TOC/excerpt extraction (without rehype transform)
|
|
172
|
-
const mdProcessor = unified().use(remarkParse).use(remarkFrontmatter, ["yaml"]).use(remarkGfm);
|
|
173
|
-
const tree = mdProcessor.parse(source);
|
|
134
|
+
const tree = processor.parse(source);
|
|
135
|
+
const vfile = { data: {} };
|
|
136
|
+
processor.runSync(tree, /** @type {any} */ (vfile));
|
|
174
137
|
|
|
138
|
+
const frontmatter = /** @type {any} */ (vfile.data).frontmatter ?? {};
|
|
175
139
|
const plainText = mdastToString(tree);
|
|
176
140
|
const toc = extractToc(tree);
|
|
177
|
-
const excerpt =
|
|
141
|
+
const excerpt = extractExcerpt(tree);
|
|
178
142
|
const slug = basename(filePath, extname(filePath));
|
|
179
143
|
|
|
144
|
+
const bodyNodes = tree.children.filter(
|
|
145
|
+
(/** @type {any} */ n) => n.type !== "yaml" && n.type !== "toml",
|
|
146
|
+
);
|
|
147
|
+
const $children = bodyNodes.map(mdastNodeToJx).filter(Boolean);
|
|
148
|
+
|
|
180
149
|
return {
|
|
181
150
|
slug,
|
|
182
151
|
path: filePath,
|
|
183
152
|
frontmatter,
|
|
184
|
-
$
|
|
153
|
+
$children,
|
|
185
154
|
$excerpt: excerpt,
|
|
186
155
|
$toc: toc,
|
|
187
156
|
$readingTime: readingTime(plainText),
|
|
@@ -224,9 +193,9 @@ export class MarkdownFile {
|
|
|
224
193
|
/**
|
|
225
194
|
* Parse and resolve the markdown file.
|
|
226
195
|
*
|
|
227
|
-
* @returns {
|
|
196
|
+
* @returns {object} MarkdownFileResult
|
|
228
197
|
*/
|
|
229
|
-
|
|
198
|
+
resolve() {
|
|
230
199
|
const { src, basePath, ...processorConfig } = this.config;
|
|
231
200
|
const filePath = basePath ? resolvePath(basePath, src) : resolvePath(src);
|
|
232
201
|
const source = readFileSync(filePath, "utf-8");
|
|
@@ -281,12 +250,10 @@ export class MarkdownCollection {
|
|
|
281
250
|
const pattern = resolved.split("\\").join("/");
|
|
282
251
|
const files = globSync(pattern, { absolute: true });
|
|
283
252
|
|
|
284
|
-
const results =
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
}),
|
|
289
|
-
);
|
|
253
|
+
const results = files.map((filePath) => {
|
|
254
|
+
const source = readFileSync(filePath, "utf-8");
|
|
255
|
+
return processMarkdown(source, filePath, processorConfig);
|
|
256
|
+
});
|
|
290
257
|
|
|
291
258
|
// Filter
|
|
292
259
|
let filtered = results;
|
|
@@ -322,67 +289,8 @@ export {
|
|
|
322
289
|
applyStyleKeyMapping,
|
|
323
290
|
isJxMarkdown,
|
|
324
291
|
transpileJxMarkdown,
|
|
292
|
+
mdastNodeToJx,
|
|
293
|
+
convertChildren,
|
|
325
294
|
jxKey,
|
|
326
295
|
mdKey,
|
|
327
296
|
} from "./transpile.js";
|
|
328
|
-
|
|
329
|
-
// ─── MarkdownDirective ────────────────────────────────────────────────────────
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* Remark plugin: map markdown directives to custom element tags in HTML output.
|
|
333
|
-
*
|
|
334
|
-
* Works with `remark-directive` — must be placed after it in the pipeline. Converts
|
|
335
|
-
* ::directive-name{attrs} → <directive-name attrs> in hast output.
|
|
336
|
-
*
|
|
337
|
-
* @example
|
|
338
|
-
* unified()
|
|
339
|
-
* .use(remarkParse)
|
|
340
|
-
* .use(remarkDirective)
|
|
341
|
-
* .use(MarkdownDirective, { prefix: "jx-" })
|
|
342
|
-
* .use(remarkRehype)
|
|
343
|
-
* .use(rehypeStringify);
|
|
344
|
-
*
|
|
345
|
-
* @param {object} [options]
|
|
346
|
-
* @param {string} [options.prefix] - Prefix for directives without hyphens. Default is `'jx-'`
|
|
347
|
-
* @param {boolean} [options.passContent] - Pass container content as slot. Default is `true`
|
|
348
|
-
* @param {string[]} [options.allowedNames] - Whitelist of allowed directive names
|
|
349
|
-
* @returns {function} Remark plugin transform function
|
|
350
|
-
*/
|
|
351
|
-
export function MarkdownDirective(options = {}) {
|
|
352
|
-
const { prefix = "jx-", passContent = true, allowedNames } = options;
|
|
353
|
-
|
|
354
|
-
return (/** @type {any} */ tree) => {
|
|
355
|
-
visit(tree, (/** @type {any} */ node) => {
|
|
356
|
-
if (
|
|
357
|
-
node.type === "leafDirective" ||
|
|
358
|
-
node.type === "containerDirective" ||
|
|
359
|
-
node.type === "textDirective"
|
|
360
|
-
) {
|
|
361
|
-
const rawName = node.name;
|
|
362
|
-
|
|
363
|
-
// Check whitelist
|
|
364
|
-
if (allowedNames && !allowedNames.includes(rawName)) return;
|
|
365
|
-
|
|
366
|
-
// Custom element names must contain a hyphen per Web Components spec
|
|
367
|
-
const tagName = rawName.includes("-") ? rawName : `${prefix}${rawName}`;
|
|
368
|
-
|
|
369
|
-
// Set hast properties for remarkRehype
|
|
370
|
-
const data = node.data || (node.data = {});
|
|
371
|
-
data.hName = tagName;
|
|
372
|
-
const attrs = node.attributes;
|
|
373
|
-
data.hProperties =
|
|
374
|
-
attrs && Object.keys(attrs).length > 0 ? { "data-jx-props": JSON.stringify(attrs) } : {};
|
|
375
|
-
|
|
376
|
-
// For text directives, preserve label as children
|
|
377
|
-
if (node.type === "textDirective" && node.children?.length > 0) {
|
|
378
|
-
// Children are already part of the mdast node; remarkRehype handles them
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// For container directives, content is already in node.children
|
|
382
|
-
if (node.type === "containerDirective" && !passContent) {
|
|
383
|
-
node.children = [];
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
});
|
|
387
|
-
};
|
|
388
|
-
}
|
package/src/transpile.js
CHANGED
|
@@ -17,6 +17,8 @@ import remarkFrontmatter from "remark-frontmatter";
|
|
|
17
17
|
import remarkParseFrontmatter from "remark-parse-frontmatter";
|
|
18
18
|
import remarkGfm from "remark-gfm";
|
|
19
19
|
import remarkDirective from "remark-directive";
|
|
20
|
+
import { htmlToJx } from "./html-to-jx.js";
|
|
21
|
+
export { htmlToJx };
|
|
20
22
|
|
|
21
23
|
// ─── Dot-path expansion ─────────────────────────────────────────────────────
|
|
22
24
|
|
|
@@ -315,7 +317,7 @@ const JX_TAG_MAP = {
|
|
|
315
317
|
* @param {any} node
|
|
316
318
|
* @returns {any} Jx element or null
|
|
317
319
|
*/
|
|
318
|
-
function mdastNodeToJx(node) {
|
|
320
|
+
export function mdastNodeToJx(node) {
|
|
319
321
|
if (!node || typeof node !== "object") return null;
|
|
320
322
|
|
|
321
323
|
if (node.type === "yaml" || node.type === "toml") return null;
|
|
@@ -332,6 +334,11 @@ function mdastNodeToJx(node) {
|
|
|
332
334
|
return node.value;
|
|
333
335
|
}
|
|
334
336
|
|
|
337
|
+
if (node.type === "html") {
|
|
338
|
+
if (node.value) return htmlToJx(node.value);
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
|
|
335
342
|
const tagFn = JX_TAG_MAP[node.type];
|
|
336
343
|
if (!tagFn) return null;
|
|
337
344
|
|
|
@@ -535,9 +542,9 @@ function directiveToJx(node) {
|
|
|
535
542
|
* @param {any[]} children
|
|
536
543
|
* @returns {any[]}
|
|
537
544
|
*/
|
|
538
|
-
function convertChildren(children) {
|
|
545
|
+
export function convertChildren(children) {
|
|
539
546
|
if (!children) return [];
|
|
540
|
-
return children.
|
|
547
|
+
return children.flatMap(mdastNodeToJx).filter((c) => c != null);
|
|
541
548
|
}
|
|
542
549
|
|
|
543
550
|
/**
|