@jxsuite/parser 0.23.1 → 0.24.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.23.1",
3
+ "version": "0.24.0",
4
4
  "description": "Jx markdown parser and external class integration",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -13,29 +13,31 @@
13
13
  ],
14
14
  "type": "module",
15
15
  "exports": {
16
- ".": "./src/md.js",
17
- "./content": "./src/content.js",
16
+ ".": "./src/md.ts",
17
+ "./content": "./src/content.ts",
18
18
  "./ContentCollection.class.json": "./src/ContentCollection.class.json",
19
19
  "./ContentEntry.class.json": "./src/ContentEntry.class.json",
20
20
  "./MarkdownCollection.class.json": "./src/MarkdownCollection.class.json",
21
21
  "./MarkdownFile.class.json": "./src/MarkdownFile.class.json",
22
- "./transpile": "./src/transpile.js",
23
- "./html-to-jx": "./src/html-to-jx.js"
22
+ "./transpile": "./src/transpile.ts",
23
+ "./html-to-jx": "./src/html-to-jx.ts",
24
+ "./types": "./src/types.ts"
24
25
  },
25
26
  "publishConfig": {
26
27
  "provenance": true
27
28
  },
28
29
  "scripts": {
29
- "test": "bun test",
30
- "test:coverage": "bun test --coverage",
30
+ "test": "bun test --isolate",
31
+ "test:coverage": "bun test --isolate --coverage",
31
32
  "upgrade": "bunx npm-check-updates -u && bun install"
32
33
  },
33
34
  "dependencies": {
35
+ "@jxsuite/schema": "^0.24.0",
34
36
  "glob": "^13.0.6",
35
37
  "hast-util-from-html": "^2.0.3",
36
38
  "hast-util-whitespace": "^3.0.0",
37
39
  "mdast-util-to-string": "^4.0.0",
38
- "property-information": "^7.1.0",
40
+ "property-information": "^7.2.0",
39
41
  "rehype-stringify": "^10.0.1",
40
42
  "remark-directive": "^4.0.0",
41
43
  "remark-frontmatter": "^5.0.0",
@@ -48,7 +50,7 @@
48
50
  },
49
51
  "devDependencies": {
50
52
  "@happy-dom/global-registrator": "^20.9.0",
51
- "@jxsuite/runtime": "^0.23.1",
53
+ "@jxsuite/runtime": "^0.24.0",
52
54
  "@types/hast": "^3.0.4"
53
55
  }
54
56
  }
@@ -4,7 +4,7 @@
4
4
  "description": "Query a project content type with filter, sort, and limit",
5
5
  "$prototype": "Class",
6
6
  "extends": "Object",
7
- "$implementation": "./content.js",
7
+ "$implementation": "./content.ts",
8
8
 
9
9
  "$defs": {
10
10
  "parameters": {
@@ -105,6 +105,7 @@
105
105
  "scope": "instance",
106
106
  "identifier": "resolve",
107
107
  "parameters": [],
108
+ "returns": { "type": "array" },
108
109
  "description": "Query the content type entries with filter, sort, and limit"
109
110
  }
110
111
  }
@@ -6,6 +6,26 @@
6
6
  * queryContentType, findEntry) used by both the classes and the server endpoint.
7
7
  */
8
8
 
9
+ import type { ContentLoaderEntry } from "./types.ts";
10
+
11
+ interface CollectionConfig {
12
+ contentType?: string;
13
+ filter?: unknown;
14
+ sort?: unknown;
15
+ limit?: number;
16
+ _project?: { contentTypes?: Map<string, ContentLoaderEntry[]>; [k: string]: unknown };
17
+ [k: string]: unknown;
18
+ }
19
+
20
+ interface EntryConfig {
21
+ contentType?: string;
22
+ id?: unknown;
23
+ field?: string;
24
+ _project?: { contentTypes?: Map<string, ContentLoaderEntry[]>; [k: string]: unknown };
25
+ _document?: { route?: { _pathParams?: Record<string, string> }; [k: string]: unknown };
26
+ [k: string]: unknown;
27
+ }
28
+
9
29
  // ─── Query Functions ────────────────────────────────────────────────────────
10
30
 
11
31
  /**
@@ -15,7 +35,10 @@
15
35
  * @param {ContentLoaderEntry} entry
16
36
  * @returns {boolean}
17
37
  */
18
- export function evaluateFilterRule(rule, entry) {
38
+ export function evaluateFilterRule(
39
+ rule: { field: string; op: string; value?: unknown },
40
+ entry: ContentLoaderEntry,
41
+ ) {
19
42
  const actual = rule.field === "id" ? entry.id : entry.data[rule.field];
20
43
  switch (rule.op) {
21
44
  case "==":
@@ -31,13 +54,13 @@ export function evaluateFilterRule(rule, entry) {
31
54
  case "not contains":
32
55
  return typeof actual === "string" && !actual.includes(String(rule.value ?? ""));
33
56
  case ">":
34
- return /** @type {any} */ (actual) > /** @type {any} */ (rule.value);
57
+ return Number(actual) > Number(rule.value);
35
58
  case "<":
36
- return /** @type {any} */ (actual) < /** @type {any} */ (rule.value);
59
+ return Number(actual) < Number(rule.value);
37
60
  case ">=":
38
- return /** @type {any} */ (actual) >= /** @type {any} */ (rule.value);
61
+ return Number(actual) >= Number(rule.value);
39
62
  case "<=":
40
- return /** @type {any} */ (actual) <= /** @type {any} */ (rule.value);
63
+ return Number(actual) <= Number(rule.value);
41
64
  default:
42
65
  return true;
43
66
  }
@@ -54,13 +77,19 @@ export function evaluateFilterRule(rule, entry) {
54
77
  * }} [query]
55
78
  * @returns {ContentLoaderEntry[]} Filtered, sorted, limited entries
56
79
  */
57
- export function queryContentType(entries, query = {}) {
80
+ export function queryContentType(
81
+ entries: ContentLoaderEntry[],
82
+ query: {
83
+ filter?: Record<string, unknown> | Array<{ field: string; op: string; value?: unknown }>;
84
+ sort?: { field: string; order?: string } | Array<{ field: string; order?: string }>;
85
+ limit?: number;
86
+ } = {},
87
+ ) {
58
88
  let result = [...entries];
59
89
 
60
90
  // Filter
61
91
  if (query.filter) {
62
- /** @type {{ field: string; op: string; value?: unknown }[]} */
63
- let rules;
92
+ let rules: { field: string; op: string; value?: unknown }[];
64
93
  if (Array.isArray(query.filter)) {
65
94
  rules = query.filter;
66
95
  } else if (typeof query.filter === "object") {
@@ -76,8 +105,8 @@ export function queryContentType(entries, query = {}) {
76
105
  const sortRules = Array.isArray(query.sort) ? query.sort : [query.sort];
77
106
  result.sort((a, b) => {
78
107
  for (const { field, order = "asc" } of sortRules) {
79
- const aVal = /** @type {string | number} */ (field === "id" ? a.id : (a.data[field] ?? ""));
80
- const bVal = /** @type {string | number} */ (field === "id" ? b.id : (b.data[field] ?? ""));
108
+ const aVal = field === "id" ? a.id : ((a.data[field] ?? "") as string | number);
109
+ const bVal = field === "id" ? b.id : ((b.data[field] ?? "") as string | number);
81
110
  if (aVal < bVal) return order === "asc" ? -1 : 1;
82
111
  if (aVal > bVal) return order === "asc" ? 1 : -1;
83
112
  }
@@ -100,35 +129,16 @@ export function queryContentType(entries, query = {}) {
100
129
  * @param {string} id - Entry ID to find
101
130
  * @returns {ContentLoaderEntry | null} The matching entry or null
102
131
  */
103
- export function findEntry(entries, id) {
132
+ export function findEntry(entries: ContentLoaderEntry[], id: string) {
104
133
  return entries.find((e) => e.id === id) ?? null;
105
134
  }
106
135
 
107
136
  // ─── Class Implementations ──────────────────────────────────────────────────
108
137
 
109
- /**
110
- * @typedef {{
111
- * contentType?: string;
112
- * filter?: unknown;
113
- * sort?: unknown;
114
- * limit?: number;
115
- * _project?: { contentTypes?: Map<string, ContentLoaderEntry[]>; [k: string]: unknown };
116
- * [k: string]: unknown;
117
- * }} CollectionConfig
118
- *
119
- * @typedef {{
120
- * contentType?: string;
121
- * id?: unknown;
122
- * field?: string;
123
- * _project?: { contentTypes?: Map<string, ContentLoaderEntry[]>; [k: string]: unknown };
124
- * _document?: { route?: { _pathParams?: Record<string, string> }; [k: string]: unknown };
125
- * [k: string]: unknown;
126
- * }} EntryConfig
127
- */
128
-
129
138
  export class ContentCollection {
139
+ config: CollectionConfig;
130
140
  /** @param {CollectionConfig} config */
131
- constructor(config) {
141
+ constructor(config: CollectionConfig) {
132
142
  this.config = config;
133
143
  }
134
144
 
@@ -136,13 +146,24 @@ export class ContentCollection {
136
146
  const { contentType, filter, sort, limit, _project } = this.config;
137
147
  const entries = _project?.contentTypes?.get(contentType ?? "");
138
148
  if (!entries) return [];
139
- return queryContentType(entries, /** @type {any} */ ({ filter, sort, limit }));
149
+ return queryContentType(entries, {
150
+ ...(filter != null && {
151
+ filter: filter as
152
+ | Record<string, unknown>
153
+ | Array<{ field: string; op: string; value?: unknown }>,
154
+ }),
155
+ ...(sort != null && {
156
+ sort: sort as { field: string; order?: string } | Array<{ field: string; order?: string }>,
157
+ }),
158
+ ...(limit != null && { limit }),
159
+ });
140
160
  }
141
161
  }
142
162
 
143
163
  export class ContentEntry {
164
+ config: EntryConfig;
144
165
  /** @param {EntryConfig} config */
145
- constructor(config) {
166
+ constructor(config: EntryConfig) {
146
167
  this.config = config;
147
168
  }
148
169
 
@@ -155,16 +176,16 @@ export class ContentEntry {
155
176
  if (
156
177
  resolvedId &&
157
178
  typeof resolvedId === "object" &&
158
- /** @type {{ $ref?: string }} */ (resolvedId).$ref?.startsWith("#/$params/")
179
+ (resolvedId as { $ref?: string }).$ref?.startsWith("#/$params/")
159
180
  ) {
160
- const paramName = /** @type {{ $ref: string }} */ (resolvedId).$ref.replace("#/$params/", "");
181
+ const paramName = (resolvedId as { $ref: string }).$ref.replace("#/$params/", "");
161
182
  resolvedId = _document?.route?._pathParams?.[paramName];
162
183
  }
163
184
  if (!resolvedId) return null;
164
185
 
165
186
  if (field && field !== "id") {
166
- return entries.find((e) => e.data[field] === resolvedId) ?? null;
187
+ return entries.find((e: ContentLoaderEntry) => e.data[field] === resolvedId) ?? null;
167
188
  }
168
- return findEntry(entries, /** @type {string} */ (resolvedId));
189
+ return findEntry(entries, resolvedId as string);
169
190
  }
170
191
  }
@@ -2,7 +2,8 @@ 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 */
5
+ import type { Nodes as HastNode } from "hast";
6
+ import type { JxElement } from "@jxsuite/schema/types";
6
7
 
7
8
  /**
8
9
  * Convert an HTML string into an array of Jx tree nodes.
@@ -10,18 +11,17 @@ import { find, html as htmlInfo } from "property-information";
10
11
  * @param {string} htmlString
11
12
  * @returns {(JxElement | string)[]}
12
13
  */
13
- export function htmlToJx(htmlString) {
14
+ export function htmlToJx(htmlString: string) {
14
15
  const hast = fromHtml(htmlString, { fragment: true });
15
- return convertHastChildren(/** @type {HastNode[]} */ (hast.children));
16
+ return convertHastChildren(hast.children as HastNode[]);
16
17
  }
17
18
 
18
19
  /**
19
20
  * @param {HastNode[]} children
20
21
  * @returns {(JxElement | string)[]}
21
22
  */
22
- function convertHastChildren(children) {
23
- /** @type {(JxElement | string)[]} */
24
- const result = [];
23
+ function convertHastChildren(children: HastNode[]) {
24
+ const result: (JxElement | string)[] = [];
25
25
  for (const child of children) {
26
26
  const converted = convertHastNode(child);
27
27
  if (converted != null) result.push(converted);
@@ -33,15 +33,14 @@ function convertHastChildren(children) {
33
33
  * @param {HastNode} node
34
34
  * @returns {JxElement | string | null}
35
35
  */
36
- function convertHastNode(node) {
36
+ function convertHastNode(node: HastNode) {
37
37
  if (node.type === "text") {
38
- if (whitespace(/** @type {HastNode} */ (/** @type {unknown} */ (node)))) return null;
38
+ if (whitespace(node as unknown as HastNode)) return null;
39
39
  return node.value ?? null;
40
40
  }
41
41
 
42
42
  if (node.type === "element") {
43
- /** @type {JxElement} */
44
- const el = { tagName: node.tagName };
43
+ const el: JxElement = { tagName: node.tagName };
45
44
 
46
45
  if (node.properties && Object.keys(node.properties).length > 0) {
47
46
  const { style, attrs } = hastPropsToJx(node.properties);
@@ -54,7 +53,7 @@ function convertHastNode(node) {
54
53
  if (kids.length === 1 && typeof kids[0] === "string") {
55
54
  el.textContent = kids[0];
56
55
  } else if (kids.length > 0) {
57
- el.children = /** @type {JxElement[]} */ (kids);
56
+ el.children = kids as JxElement[];
58
57
  }
59
58
 
60
59
  return el;
@@ -67,11 +66,9 @@ function convertHastNode(node) {
67
66
  * @param {Record<string, unknown>} properties
68
67
  * @returns {{ style: Record<string, string>; attrs: Record<string, string> }}
69
68
  */
70
- function hastPropsToJx(properties) {
71
- /** @type {Record<string, string>} */
72
- const attrs = {};
73
- /** @type {Record<string, string>} */
74
- const style = {};
69
+ function hastPropsToJx(properties: Record<string, unknown>) {
70
+ const attrs: Record<string, string> = {};
71
+ const style: Record<string, string> = {};
75
72
 
76
73
  for (const [key, value] of Object.entries(properties)) {
77
74
  if (value === false || value === undefined || value === null) continue;
@@ -99,7 +96,7 @@ function hastPropsToJx(properties) {
99
96
  * @param {string} styleStr
100
97
  * @param {Record<string, string>} out
101
98
  */
102
- function parseInlineStyle(styleStr, out) {
99
+ function parseInlineStyle(styleStr: string, out: Record<string, string>) {
103
100
  for (const decl of styleStr.split(";")) {
104
101
  const colon = decl.indexOf(":");
105
102
  if (colon === -1) continue;
@@ -20,7 +20,9 @@ import remarkDirective from "remark-directive";
20
20
  import { readFileSync } from "node:fs";
21
21
  import { basename, extname, resolve as resolvePath } from "node:path";
22
22
  import { globSync } from "glob";
23
- import { mdastNodeToJx } from "./transpile.js";
23
+ import { mdastNodeToJx } from "./transpile.ts";
24
+ import type { MarkdownFileResult, TocEntry, MdastNode } from "./types.ts";
25
+ import type { JxElement } from "@jxsuite/schema/types";
24
26
 
25
27
  // ─── Tree utilities (inline to avoid Bun ESM resolution issues with unist-util-*) ──
26
28
 
@@ -31,18 +33,22 @@ import { mdastNodeToJx } from "./transpile.js";
31
33
  * @param {string | function} typeOrVisitor
32
34
  * @param {function} [maybeVisitor]
33
35
  */
34
- function visit(tree, typeOrVisitor, maybeVisitor) {
36
+ function visit(
37
+ tree: MdastNode,
38
+ typeOrVisitor: string | ((node: MdastNode) => void),
39
+ maybeVisitor?: (node: MdastNode) => void,
40
+ ) {
35
41
  const type = typeof typeOrVisitor === "string" ? typeOrVisitor : null;
36
42
  const visitor = type ? maybeVisitor : typeOrVisitor;
37
43
 
38
- function walk(/** @type {MdastNode} */ node) {
44
+ function walk(node: MdastNode) {
39
45
  if (!node || typeof node !== "object") return;
40
- if (!type || node.type === type) /** @type {Function} */ (visitor)(node);
46
+ if (!type || node.type === type) (visitor as Function)(node);
41
47
  if (Array.isArray(node.children)) {
42
48
  for (const child of node.children) walk(child);
43
49
  }
44
50
  }
45
- walk(/** @type {MdastNode} */ (tree));
51
+ walk(tree as MdastNode);
46
52
  }
47
53
 
48
54
  /**
@@ -51,7 +57,7 @@ function visit(tree, typeOrVisitor, maybeVisitor) {
51
57
  * @param {MdastNode} node
52
58
  * @returns {string}
53
59
  */
54
- function mdastToString(node) {
60
+ function mdastToString(node: MdastNode): string {
55
61
  if (!node) return "";
56
62
  if (typeof node === "string") return node;
57
63
  if (node.value) return node.value;
@@ -67,7 +73,7 @@ function mdastToString(node) {
67
73
  * @param {string} text
68
74
  * @returns {number} Minutes (rounded up, minimum 1)
69
75
  */
70
- function readingTime(text) {
76
+ function readingTime(text: string) {
71
77
  const words = text.split(/\s+/).filter(Boolean).length;
72
78
  return Math.max(1, Math.ceil(words / 200));
73
79
  }
@@ -78,10 +84,10 @@ function readingTime(text) {
78
84
  * @param {object} tree - Mdast AST
79
85
  * @returns {{ depth: number; text: string; id: string }[]}
80
86
  */
81
- function extractToc(tree) {
87
+ function extractToc(tree: MdastNode) {
82
88
  /** @type {{ depth: number; text: string; id: string }[]} */
83
- const entries = [];
84
- visit(tree, "heading", (/** @type {MdastNode} */ node) => {
89
+ const entries: TocEntry[] = [];
90
+ visit(tree, "heading", (node: MdastNode) => {
85
91
  const text = mdastToString(node);
86
92
  const id = text
87
93
  .toLowerCase()
@@ -89,7 +95,7 @@ function extractToc(tree) {
89
95
  .replace(/\s+/g, "-")
90
96
  .replace(/-+/g, "-")
91
97
  .replace(/^-|-$/g, "");
92
- entries.push({ depth: /** @type {number} */ (node.depth), text, id });
98
+ entries.push({ depth: node.depth as number, text, id });
93
99
  });
94
100
  return entries;
95
101
  }
@@ -100,10 +106,9 @@ function extractToc(tree) {
100
106
  * @param {object} tree - Mdast AST
101
107
  * @returns {string} Plain text of first paragraph, or empty string
102
108
  */
103
- function extractExcerpt(tree) {
104
- /** @type {MdastNode | null} */
105
- let firstParagraph = null;
106
- visit(tree, "paragraph", (/** @type {MdastNode} */ node) => {
109
+ function extractExcerpt(tree: MdastNode) {
110
+ let firstParagraph: MdastNode | null = null;
111
+ visit(tree, "paragraph", (node: MdastNode) => {
107
112
  if (!firstParagraph) firstParagraph = node;
108
113
  });
109
114
  if (!firstParagraph) return "";
@@ -122,7 +127,14 @@ function extractExcerpt(tree) {
122
127
  * @param {unknown} [config.directiveOptions] - Directive plugin options
123
128
  * @returns {MarkdownFileResult}
124
129
  */
125
- function processMarkdown(source, filePath, config = {}) {
130
+ function processMarkdown(
131
+ source: string,
132
+ filePath: string,
133
+ config: {
134
+ directives?: boolean;
135
+ directiveOptions?: unknown;
136
+ } = {},
137
+ ) {
126
138
  let processor = unified()
127
139
  .use(remarkParse)
128
140
  .use(remarkFrontmatter, ["yaml"])
@@ -138,22 +150,20 @@ function processMarkdown(source, filePath, config = {}) {
138
150
  // @ts-ignore - vfile shape satisfies unified's internal contract
139
151
  processor.runSync(tree, vfile);
140
152
 
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));
153
+ const vfileData = vfile.data as Record<string, unknown>;
154
+ const frontmatter = (vfileData.frontmatter ?? {}) as Record<string, unknown>;
155
+ const mdTree = tree as unknown as MdastNode;
144
156
  const plainText = mdastToString(mdTree);
145
157
  const toc = extractToc(tree);
146
158
  const excerpt = extractExcerpt(tree);
147
159
  const slug = basename(filePath, extname(filePath));
148
160
 
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)
156
- );
161
+ const bodyNodes = tree.children.filter(
162
+ (n: MdastNode) => n.type !== "yaml" && n.type !== "toml",
163
+ ) as MdastNode[];
164
+ const $children = bodyNodes
165
+ .map(/** @type {(n: MdastNode) => any} */ (mdastNodeToJx))
166
+ .filter(Boolean) as (JxElement | string)[];
157
167
 
158
168
  return {
159
169
  slug,
@@ -174,11 +184,11 @@ function processMarkdown(source, filePath, config = {}) {
174
184
  * @param {string} path
175
185
  * @returns {unknown}
176
186
  */
177
- function getNestedValue(obj, path) {
187
+ function getNestedValue(obj: Record<string, unknown> | MarkdownFileResult, path: string) {
178
188
  return path
179
189
  .split(".")
180
190
  .reduce(
181
- (/** @type {unknown} */ o, k) => /** @type {Record<string, unknown> | undefined} */ (o)?.[k],
191
+ (o: unknown, k) => (o as Record<string, unknown> | undefined)?.[k],
182
192
  /** @type {unknown} */ (obj),
183
193
  );
184
194
  }
@@ -192,6 +202,13 @@ function getNestedValue(obj, path) {
192
202
  * { "$prototype": "MarkdownFile", "$src": "@jxsuite/md", "src": "./content/about.md" }
193
203
  */
194
204
  export class MarkdownFile {
205
+ config: {
206
+ src: string;
207
+ remarkPlugins?: unknown[];
208
+ rehypePlugins?: unknown[];
209
+ basePath?: string;
210
+ directives?: boolean;
211
+ };
195
212
  /**
196
213
  * @param {object} config
197
214
  * @param {string} config.src - File path to markdown file
@@ -200,7 +217,13 @@ export class MarkdownFile {
200
217
  * @param {string} [config.basePath] - Base path for resolving src
201
218
  * @param {boolean} [config.directives] - Enable directive support
202
219
  */
203
- constructor(config) {
220
+ constructor(config: {
221
+ src: string;
222
+ remarkPlugins?: unknown[];
223
+ rehypePlugins?: unknown[];
224
+ basePath?: string;
225
+ directives?: boolean;
226
+ }) {
204
227
  this.config = config;
205
228
  }
206
229
 
@@ -227,19 +250,40 @@ export class MarkdownFile {
227
250
  * { "$prototype": "MarkdownCollection", "$src": "@jxsuite/md", "src": "./posts/*.md" }
228
251
  */
229
252
  export class MarkdownCollection {
253
+ config: {
254
+ src: string;
255
+ sortBy?: string;
256
+ sortOrder?: string;
257
+ limit?: number;
258
+ filter?: (result: MarkdownFileResult) => boolean;
259
+ remarkPlugins?: unknown[];
260
+ rehypePlugins?: unknown[];
261
+ basePath?: string;
262
+ directives?: boolean;
263
+ };
230
264
  /**
231
265
  * @param {object} config
232
266
  * @param {string} config.src - Glob pattern or directory path
233
267
  * @param {string} [config.sortBy] Default is `'frontmatter.date'`
234
268
  * @param {string} [config.sortOrder] Default is `'desc'`
235
269
  * @param {number} [config.limit]
236
- * @param {Function} [config.filter] - Filter function
270
+ * @param {(result: MarkdownFileResult) => boolean} [config.filter] - Filter function
237
271
  * @param {unknown[]} [config.remarkPlugins] Default is `[]`
238
272
  * @param {unknown[]} [config.rehypePlugins] Default is `[]`
239
273
  * @param {string} [config.basePath] - Base path for resolving glob
240
274
  * @param {boolean} [config.directives] - Enable directive support
241
275
  */
242
- constructor(config) {
276
+ constructor(config: {
277
+ src: string;
278
+ sortBy?: string;
279
+ sortOrder?: string;
280
+ limit?: number;
281
+ filter?: (result: MarkdownFileResult) => boolean;
282
+ remarkPlugins?: unknown[];
283
+ rehypePlugins?: unknown[];
284
+ basePath?: string;
285
+ directives?: boolean;
286
+ }) {
243
287
  this.config = config;
244
288
  }
245
289
 
@@ -264,7 +308,7 @@ export class MarkdownCollection {
264
308
  const pattern = resolved.split("\\").join("/");
265
309
  const files = globSync(pattern, { absolute: true });
266
310
 
267
- const results = files.map((/** @type {string} */ filePath) => {
311
+ const results = files.map((filePath: string) => {
268
312
  const source = readFileSync(filePath, "utf-8");
269
313
  return processMarkdown(source, filePath, processorConfig);
270
314
  });
@@ -276,9 +320,9 @@ export class MarkdownCollection {
276
320
  }
277
321
 
278
322
  // Sort
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) ?? "");
323
+ filtered.sort((a: MarkdownFileResult, b: MarkdownFileResult) => {
324
+ const aVal = getNestedValue(a, sortBy) ?? ("" as string | number);
325
+ const bVal = getNestedValue(b, sortBy) ?? ("" as string | number);
282
326
  if (aVal < bVal) return sortOrder === "asc" ? -1 : 1;
283
327
  if (aVal > bVal) return sortOrder === "asc" ? 1 : -1;
284
328
  return 0;
@@ -307,4 +351,4 @@ export {
307
351
  convertChildren,
308
352
  jxKey,
309
353
  mdKey,
310
- } from "./transpile.js";
354
+ } from "./transpile.ts";
@@ -17,7 +17,9 @@ 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";
20
+ import { htmlToJx } from "./html-to-jx.ts";
21
+ import type { MdastNode } from "./types.ts";
22
+ import type { JxDocument, JxElement } from "@jxsuite/schema/types";
21
23
  export { htmlToJx };
22
24
 
23
25
  // ─── Dot-path expansion ─────────────────────────────────────────────────────
@@ -40,7 +42,7 @@ const JX_ANNOTATION_KEYS = new Set(["title", "description"]);
40
42
  * @param {string} key
41
43
  * @returns {string}
42
44
  */
43
- export function jxKey(key) {
45
+ export function jxKey(key: string) {
44
46
  if (JX_DOLLAR_KEYS.has(key)) return `$${key}`;
45
47
  if (key.startsWith("--") && JX_ANNOTATION_KEYS.has(key.slice(2))) return `$${key.slice(2)}`;
46
48
  return key;
@@ -52,7 +54,7 @@ export function jxKey(key) {
52
54
  * @param {string} key
53
55
  * @returns {string}
54
56
  */
55
- export function mdKey(key) {
57
+ export function mdKey(key: string) {
56
58
  if (key.startsWith("$") && JX_DOLLAR_KEYS.has(key.slice(1))) {
57
59
  return key.slice(1);
58
60
  }
@@ -68,9 +70,8 @@ export function mdKey(key) {
68
70
  * @param {Record<string, string>} attrs - Flat attribute map from remark-directive
69
71
  * @returns {Record<string, unknown>} Nested object
70
72
  */
71
- export function expandDotPaths(attrs) {
72
- /** @type {Record<string, unknown>} */
73
- const result = {};
73
+ export function expandDotPaths(attrs: Record<string, string>) {
74
+ const result: Record<string, unknown> = {};
74
75
 
75
76
  for (const [key, value] of Object.entries(attrs)) {
76
77
  const dotIndex = key.indexOf(".");
@@ -86,7 +87,7 @@ export function expandDotPaths(attrs) {
86
87
  if (!(seg in target) || typeof target[seg] !== "object") {
87
88
  target[seg] = {};
88
89
  }
89
- target = /** @type {Record<string, unknown>} */ (target[seg]);
90
+ target = target[seg] as Record<string, unknown>;
90
91
  }
91
92
  target[jxKey(segments[segments.length - 1])] = value;
92
93
  }
@@ -100,15 +101,14 @@ export function expandDotPaths(attrs) {
100
101
  * @param {Record<string, unknown>} obj - Nested object
101
102
  * @returns {Record<string, string>} Flat attribute map
102
103
  */
103
- export function collapseDotPaths(obj) {
104
- /** @type {Record<string, string>} */
105
- const result = {};
104
+ export function collapseDotPaths(obj: Record<string, unknown>) {
105
+ const result: Record<string, string> = {};
106
106
 
107
- function walk(/** @type {Record<string, unknown>} */ node, /** @type {string} */ prefix) {
107
+ function walk(node: Record<string, unknown>, prefix: string) {
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(/** @type {Record<string, unknown>} */ (value), path);
111
+ walk(value as Record<string, unknown>, path);
112
112
  } else {
113
113
  result[path] = String(value);
114
114
  }
@@ -151,9 +151,8 @@ const CSS_PSEUDO_NAMES = new Set([
151
151
  * @param {Record<string, unknown>} styleObj
152
152
  * @returns {Record<string, unknown>}
153
153
  */
154
- export function applyStyleKeyMapping(styleObj) {
155
- /** @type {Record<string, unknown>} */
156
- const result = {};
154
+ export function applyStyleKeyMapping(styleObj: Record<string, unknown>) {
155
+ const result: Record<string, unknown> = {};
157
156
  for (const [key, value] of Object.entries(styleObj)) {
158
157
  if (CSS_PSEUDO_NAMES.has(key)) {
159
158
  result[`:${key}`] = value;
@@ -175,7 +174,7 @@ export function applyStyleKeyMapping(styleObj) {
175
174
  * @param {Record<string, string>} attrs
176
175
  * @returns {Record<string, unknown>}
177
176
  */
178
- export function expandStylePaths(attrs) {
177
+ export function expandStylePaths(attrs: Record<string, string>) {
179
178
  return applyStyleKeyMapping(expandDotPaths(attrs));
180
179
  }
181
180
 
@@ -188,9 +187,8 @@ export function expandStylePaths(attrs) {
188
187
  * @param {Record<string, unknown>} styleObj
189
188
  * @returns {Record<string, string>}
190
189
  */
191
- export function collapseStylePaths(styleObj) {
192
- /** @type {Record<string, unknown>} */
193
- const normalized = {};
190
+ export function collapseStylePaths(styleObj: Record<string, unknown>) {
191
+ const normalized: Record<string, unknown> = {};
194
192
 
195
193
  for (const [key, value] of Object.entries(styleObj)) {
196
194
  if (key.startsWith(":") && CSS_PSEUDO_NAMES.has(key.slice(1))) {
@@ -214,7 +212,7 @@ export function collapseStylePaths(styleObj) {
214
212
  * @param {string} source - Raw markdown string
215
213
  * @returns {boolean}
216
214
  */
217
- export function isJxMarkdown(source) {
215
+ export function isJxMarkdown(source: string) {
218
216
  const fmMatch = source.match(/^---\r?\n([\s\S]*?)\r?\n---/);
219
217
  if (!fmMatch) return false;
220
218
  return /^tagName:\s*.+-.+/m.test(fmMatch[1]);
@@ -273,22 +271,20 @@ const PHRASING_ELEMENTS = new Set([
273
271
  * @param {Record<string, string>} attrs
274
272
  * @returns {{ props: Record<string, unknown>; attributes: Record<string, string> }}
275
273
  */
276
- function routeAttributes(attrs) {
274
+ function routeAttributes(attrs: Record<string, string>) {
277
275
  const expanded = expandDotPaths(attrs);
278
276
 
279
277
  // Apply style-key mapping (pseudo-classes, media queries) to the style sub-object
280
278
  if (expanded.style && typeof expanded.style === "object") {
281
- expanded.style = applyStyleKeyMapping(/** @type {Record<string, unknown>} */ (expanded.style));
279
+ expanded.style = applyStyleKeyMapping(expanded.style as Record<string, unknown>);
282
280
  }
283
281
 
284
- /** @type {Record<string, unknown>} */
285
- const props = {};
286
- /** @type {Record<string, string>} */
287
- const attributes = {};
282
+ const props: Record<string, unknown> = {};
283
+ const attributes: Record<string, string> = {};
288
284
 
289
285
  for (const [key, value] of Object.entries(expanded)) {
290
286
  if (HTML_ATTR_PATTERN.test(key)) {
291
- attributes[key] = /** @type {string} */ (value);
287
+ attributes[key] = value as string;
292
288
  } else {
293
289
  props[key] = value;
294
290
  }
@@ -302,8 +298,8 @@ function routeAttributes(attrs) {
302
298
  *
303
299
  * @type {Record<string, (n: MdastNode) => string>}
304
300
  */
305
- const JX_TAG_MAP = {
306
- heading: (/** @type {MdastNode} */ n) => `h${n.depth}`,
301
+ const JX_TAG_MAP: Record<string, (n: MdastNode) => string> = {
302
+ heading: (n: MdastNode) => `h${n.depth}`,
307
303
  paragraph: () => "p",
308
304
  emphasis: () => "em",
309
305
  strong: () => "strong",
@@ -312,14 +308,14 @@ const JX_TAG_MAP = {
312
308
  link: () => "a",
313
309
  image: () => "img",
314
310
  blockquote: () => "blockquote",
315
- list: (/** @type {MdastNode} */ n) => (n.ordered ? "ol" : "ul"),
311
+ list: (n: MdastNode) => (n.ordered ? "ol" : "ul"),
316
312
  listItem: () => "li",
317
313
  code: () => "pre",
318
314
  thematicBreak: () => "hr",
319
315
  break: () => "br",
320
316
  table: () => "table",
321
317
  tableRow: () => "tr",
322
- tableCell: (/** @type {MdastNode} */ n) => (n.isHeader ? "th" : "td"),
318
+ tableCell: (n: MdastNode) => (n.isHeader ? "th" : "td"),
323
319
  };
324
320
 
325
321
  /**
@@ -328,7 +324,7 @@ const JX_TAG_MAP = {
328
324
  * @param {MdastNode} node
329
325
  * @returns {JxElement | string | (JxElement | string)[] | null} Jx element or null
330
326
  */
331
- export function mdastNodeToJx(node) {
327
+ export function mdastNodeToJx(node: MdastNode) {
332
328
  if (!node || typeof node !== "object") return null;
333
329
 
334
330
  if (node.type === "yaml" || node.type === "toml") return null;
@@ -354,8 +350,7 @@ export function mdastNodeToJx(node) {
354
350
  if (!tagFn) return null;
355
351
 
356
352
  const tag = tagFn(node);
357
- /** @type {JxElement} */
358
- const el = { tagName: tag };
353
+ const el: JxElement = { tagName: tag };
359
354
 
360
355
  switch (node.type) {
361
356
  case "heading":
@@ -377,7 +372,7 @@ export function mdastNodeToJx(node) {
377
372
  }
378
373
 
379
374
  case "inlineCode":
380
- el.textContent = node.value;
375
+ el.textContent = node.value ?? null;
381
376
  break;
382
377
 
383
378
  case "link":
@@ -411,7 +406,7 @@ export function mdastNodeToJx(node) {
411
406
  el.children = [
412
407
  {
413
408
  tagName: "code",
414
- textContent: node.value,
409
+ textContent: node.value ?? null,
415
410
  ...(node.lang ? { className: `language-${node.lang}` } : {}),
416
411
  },
417
412
  ];
@@ -425,7 +420,7 @@ export function mdastNodeToJx(node) {
425
420
  const rows = convertChildren(node.children ?? []);
426
421
  const thead = rows.length > 0 ? { tagName: "thead", children: [rows[0]] } : null;
427
422
  const tbody = rows.length > 1 ? { tagName: "tbody", children: rows.slice(1) } : null;
428
- el.children = /** @type {JxElement[]} */ ([thead, tbody].filter(Boolean));
423
+ el.children = [thead, tbody].filter(Boolean) as JxElement[];
429
424
  break;
430
425
  }
431
426
  }
@@ -439,13 +434,12 @@ export function mdastNodeToJx(node) {
439
434
  * @param {MdastNode} node
440
435
  * @returns {JxElement}
441
436
  */
442
- function directiveToJx(node) {
443
- /** @type {JxElement} */
444
- const el = { tagName: /** @type {string} */ (node.name) };
437
+ function directiveToJx(node: MdastNode) {
438
+ const el: JxElement = { tagName: node.name as string };
445
439
 
446
440
  if (node.attributes && Object.keys(node.attributes).length > 0) {
447
441
  const { props, attributes } = routeAttributes(node.attributes);
448
- const isCustomElement = /** @type {string} */ (node.name).includes("-");
442
+ const isCustomElement = (node.name as string).includes("-");
449
443
  if (isCustomElement) {
450
444
  // For custom elements:
451
445
  // - style, children, textContent, innerHTML, $-prefixed → element-level
@@ -461,7 +455,7 @@ function directiveToJx(node) {
461
455
  ) {
462
456
  el[key] = value;
463
457
  } else if (key === "props") {
464
- el.$props = /** @type {Record<string, unknown>} */ (value);
458
+ el.$props = value as Record<string, unknown>;
465
459
  } else {
466
460
  if (!el.attributes) el.attributes = {};
467
461
  el.attributes[key] = value;
@@ -516,9 +510,8 @@ function directiveToJx(node) {
516
510
  }
517
511
 
518
512
  if (node.children && node.children.length > 0) {
519
- /** @type {(JxElement | string)[]} */
520
- const jxChildren = [];
521
- const isPhrasingParent = PHRASING_ELEMENTS.has(/** @type {string} */ (node.name));
513
+ const jxChildren: (JxElement | string)[] = [];
514
+ const isPhrasingParent = PHRASING_ELEMENTS.has(node.name as string);
522
515
 
523
516
  for (const child of node.children) {
524
517
  if (isPhrasingParent && child.type === "paragraph") {
@@ -557,11 +550,9 @@ function directiveToJx(node) {
557
550
  * @param {MdastNode[]} children
558
551
  * @returns {(JxElement | string)[]}
559
552
  */
560
- export function convertChildren(children) {
553
+ export function convertChildren(children: MdastNode[]) {
561
554
  if (!children) return [];
562
- return /** @type {(JxElement | string)[]} */ (
563
- children.flatMap(mdastNodeToJx).filter((c) => c != null)
564
- );
555
+ return children.flatMap(mdastNodeToJx).filter((c) => c != null) as (JxElement | string)[];
565
556
  }
566
557
 
567
558
  /**
@@ -573,7 +564,7 @@ export function convertChildren(children) {
573
564
  * @param {string} source - Raw markdown string
574
565
  * @returns {object} Complete Jx JSON document
575
566
  */
576
- export function transpileJxMarkdown(source) {
567
+ export function transpileJxMarkdown(source: string) {
577
568
  const processor = unified()
578
569
  .use(remarkParse)
579
570
  .use(remarkFrontmatter, ["yaml"])
@@ -585,26 +576,19 @@ export function transpileJxMarkdown(source) {
585
576
  const vfile = { data: {} };
586
577
  processor.runSync(tree, vfile);
587
578
 
588
- const frontmatter = /** @type {Record<string, unknown>} */ (/** @type {unknown} */ (vfile.data))
589
- ?.frontmatter ?? {};
579
+ const frontmatter = (vfile.data as unknown as Record<string, unknown>)?.frontmatter ?? {};
590
580
 
591
- /** @type {JxDocument} */
592
- const doc = {};
581
+ const doc: JxDocument = {};
593
582
 
594
583
  for (const [key, value] of Object.entries(frontmatter)) {
595
584
  doc[key] = value;
596
585
  }
597
586
 
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
- )
604
- );
587
+ const bodyNodes = tree.children.filter(
588
+ (n: MdastNode) => n.type !== "yaml" && n.type !== "toml",
589
+ ) as unknown as MdastNode[];
605
590
 
606
- /** @type {(JxElement | string)[]} */
607
- const children = [];
591
+ const children: (JxElement | string)[] = [];
608
592
 
609
593
  for (const node of bodyNodes) {
610
594
  const converted = mdastNodeToJx(node);
package/src/types.ts ADDED
@@ -0,0 +1,65 @@
1
+ import type { JxElement } from "@jxsuite/schema/types";
2
+
3
+ export interface MarkdownFileResult {
4
+ slug: string;
5
+ path: string;
6
+ frontmatter: Record<string, unknown>;
7
+ $children: (JxElement | string)[];
8
+ $excerpt?: string;
9
+ $toc?: TocEntry[];
10
+ $readingTime?: number;
11
+ $wordCount?: number;
12
+ [key: string]: unknown;
13
+ }
14
+
15
+ export interface TocEntry {
16
+ depth: number;
17
+ text: string;
18
+ id: string;
19
+ }
20
+
21
+ export interface ContentEntry extends MarkdownFileResult {
22
+ [key: string]: unknown;
23
+ }
24
+
25
+ export interface ContentLoaderEntry {
26
+ id: string;
27
+ data: Record<string, unknown>;
28
+ body: string | null;
29
+ $children?: (JxElement | string)[];
30
+ _meta?: {
31
+ excerpt?: string;
32
+ toc?: TocEntry[];
33
+ readingTime?: number;
34
+ wordCount?: number;
35
+ };
36
+ }
37
+
38
+ export interface MdastNode {
39
+ type: string;
40
+ children?: MdastNode[];
41
+ value?: string;
42
+ data?: unknown;
43
+ position?: unknown;
44
+ depth?: number;
45
+ ordered?: boolean | null;
46
+ start?: number | null;
47
+ spread?: boolean;
48
+ isHeader?: boolean;
49
+ name?: string;
50
+ attributes?: Record<string, string>;
51
+ url?: string;
52
+ alt?: string;
53
+ title?: string | null;
54
+ lang?: string | null;
55
+ meta?: string | null;
56
+ align?: (string | null)[] | null;
57
+ }
58
+
59
+ export interface HastNode {
60
+ type: string;
61
+ tagName?: string;
62
+ properties?: Record<string, unknown>;
63
+ children?: HastNode[];
64
+ value?: string;
65
+ }