@jxsuite/parser 0.6.0 → 0.6.2

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.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Jx markdown parser and external class integration",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -59,7 +59,7 @@
59
59
  "type": "object",
60
60
  "description": "Shape defined by itemSchema parameter"
61
61
  },
62
- "$body": { "type": "string", "description": "Rendered HTML string" },
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
- "$body": { "type": "string", "description": "Rendered HTML string" },
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",
package/src/md.js CHANGED
@@ -1,12 +1,11 @@
1
1
  /**
2
2
  * jxsuite/md — Markdown integration for Jx
3
3
  *
4
- * Provides three exports:
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/rehype ecosystem. No framework dependency.
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 HTML excerpt from an mdast tree.
98
+ * Extract first paragraph as a JX text string from an mdast tree.
101
99
  *
102
100
  * @param {object} tree - Mdast AST
103
- * @returns {Promise<string>} HTML string of first paragraph, or empty string
101
+ * @returns {string} Plain text of first paragraph, or empty string
104
102
  */
105
- async function extractExcerpt(tree) {
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
- const excerptTree = { type: "root", children: [firstParagraph] };
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
- * Build the unified processing pipeline with standard plugins.
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 {object} config
126
- * @param {any} [config.directiveOptions]
127
- * @param {any[]} [config.remarkPlugins]
128
- * @param {any[]} [config.rehypePlugins]
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 buildProcessor(config = {}) {
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
- for (const plugin of config.remarkPlugins ?? []) {
141
- processor = Array.isArray(plugin) ? processor.use(plugin[0], plugin[1]) : processor.use(plugin);
130
+ if (config.directives || config.directiveOptions) {
131
+ processor = processor.use(remarkDirective);
142
132
  }
143
133
 
144
- processor = processor.use(remarkRehype, { allowDangerousHtml: true });
145
-
146
- for (const plugin of config.rehypePlugins ?? []) {
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 = await extractExcerpt(tree);
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
- $body: String(vfile),
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 {Promise<object>} MarkdownFileResult
196
+ * @returns {object} MarkdownFileResult
228
197
  */
229
- async resolve() {
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 = await Promise.all(
285
- files.map(async (filePath) => {
286
- const source = readFileSync(filePath, "utf-8");
287
- return processMarkdown(source, filePath, processorConfig);
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
@@ -315,7 +315,7 @@ const JX_TAG_MAP = {
315
315
  * @param {any} node
316
316
  * @returns {any} Jx element or null
317
317
  */
318
- function mdastNodeToJx(node) {
318
+ export function mdastNodeToJx(node) {
319
319
  if (!node || typeof node !== "object") return null;
320
320
 
321
321
  if (node.type === "yaml" || node.type === "toml") return null;
@@ -332,6 +332,11 @@ function mdastNodeToJx(node) {
332
332
  return node.value;
333
333
  }
334
334
 
335
+ if (node.type === "html") {
336
+ if (node.value) return { tagName: "div", innerHTML: node.value };
337
+ return null;
338
+ }
339
+
335
340
  const tagFn = JX_TAG_MAP[node.type];
336
341
  if (!tagFn) return null;
337
342
 
@@ -535,7 +540,7 @@ function directiveToJx(node) {
535
540
  * @param {any[]} children
536
541
  * @returns {any[]}
537
542
  */
538
- function convertChildren(children) {
543
+ export function convertChildren(children) {
539
544
  if (!children) return [];
540
545
  return children.map(mdastNodeToJx).filter((c) => c != null);
541
546
  }