@jxsuite/parser 0.28.2 → 0.28.3
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/dist/content.js +32 -16
- package/dist/csv.js +15 -11
- package/dist/markdown.js +516 -336
- package/dist/serialize.js +379 -254
- package/package.json +3 -3
- package/src/content.ts +41 -20
- package/src/csv.ts +18 -10
- package/src/html-to-jx.ts +23 -9
- package/src/markdown.ts +19 -10
- package/src/md.ts +57 -34
- package/src/serialize.ts +401 -246
- package/src/transpile.ts +114 -53
- package/src/types.ts +1 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/parser",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.3",
|
|
4
4
|
"description": "Jx markdown parser and external class integration",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"upgrade": "bunx npm-check-updates -u && bun install"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@jxsuite/schema": "^0.28.
|
|
42
|
+
"@jxsuite/schema": "^0.28.3",
|
|
43
43
|
"glob": "^13.0.6",
|
|
44
44
|
"hast-util-from-html": "^2.0.3",
|
|
45
45
|
"hast-util-whitespace": "^3.0.0",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@happy-dom/global-registrator": "^20.10.2",
|
|
62
|
-
"@jxsuite/runtime": "^0.28.
|
|
62
|
+
"@jxsuite/runtime": "^0.28.3",
|
|
63
63
|
"@types/hast": "^3.0.4",
|
|
64
64
|
"@types/mdast": "^4.0.4"
|
|
65
65
|
}
|
package/src/content.ts
CHANGED
|
@@ -50,28 +50,39 @@ export function evaluateFilterRule(
|
|
|
50
50
|
) {
|
|
51
51
|
const actual = rule.field === "id" ? entry.id : entry.data[rule.field];
|
|
52
52
|
switch (rule.op) {
|
|
53
|
-
case "==":
|
|
53
|
+
case "==": {
|
|
54
54
|
return actual === rule.value;
|
|
55
|
-
|
|
55
|
+
}
|
|
56
|
+
case "!=": {
|
|
56
57
|
return actual !== rule.value;
|
|
57
|
-
|
|
58
|
+
}
|
|
59
|
+
case "empty": {
|
|
58
60
|
return actual == null || actual === "" || (Array.isArray(actual) && actual.length === 0);
|
|
59
|
-
|
|
61
|
+
}
|
|
62
|
+
case "not empty": {
|
|
60
63
|
return actual != null && actual !== "" && !(Array.isArray(actual) && actual.length === 0);
|
|
61
|
-
|
|
64
|
+
}
|
|
65
|
+
case "contains": {
|
|
62
66
|
return typeof actual === "string" && actual.includes(String(rule.value ?? ""));
|
|
63
|
-
|
|
67
|
+
}
|
|
68
|
+
case "not contains": {
|
|
64
69
|
return typeof actual === "string" && !actual.includes(String(rule.value ?? ""));
|
|
65
|
-
|
|
70
|
+
}
|
|
71
|
+
case ">": {
|
|
66
72
|
return Number(actual) > Number(rule.value);
|
|
67
|
-
|
|
73
|
+
}
|
|
74
|
+
case "<": {
|
|
68
75
|
return Number(actual) < Number(rule.value);
|
|
69
|
-
|
|
76
|
+
}
|
|
77
|
+
case ">=": {
|
|
70
78
|
return Number(actual) >= Number(rule.value);
|
|
71
|
-
|
|
79
|
+
}
|
|
80
|
+
case "<=": {
|
|
72
81
|
return Number(actual) <= Number(rule.value);
|
|
73
|
-
|
|
82
|
+
}
|
|
83
|
+
default: {
|
|
74
84
|
return true;
|
|
85
|
+
}
|
|
75
86
|
}
|
|
76
87
|
}
|
|
77
88
|
|
|
@@ -89,8 +100,8 @@ export function evaluateFilterRule(
|
|
|
89
100
|
export function queryContentType(
|
|
90
101
|
entries: ContentLoaderEntry[],
|
|
91
102
|
query: {
|
|
92
|
-
filter?: Record<string, unknown> |
|
|
93
|
-
sort?: { field: string; order?: string } |
|
|
103
|
+
filter?: Record<string, unknown> | { field: string; op: string; value?: unknown }[];
|
|
104
|
+
sort?: { field: string; order?: string } | { field: string; order?: string }[];
|
|
94
105
|
limit?: number;
|
|
95
106
|
} = {},
|
|
96
107
|
) {
|
|
@@ -120,8 +131,12 @@ export function queryContentType(
|
|
|
120
131
|
for (const { field, order = "asc" } of sortRules) {
|
|
121
132
|
const aVal = field === "id" ? a.id : ((a.data[field] ?? "") as string | number);
|
|
122
133
|
const bVal = field === "id" ? b.id : ((b.data[field] ?? "") as string | number);
|
|
123
|
-
if (aVal < bVal)
|
|
124
|
-
|
|
134
|
+
if (aVal < bVal) {
|
|
135
|
+
return order === "asc" ? -1 : 1;
|
|
136
|
+
}
|
|
137
|
+
if (aVal > bVal) {
|
|
138
|
+
return order === "asc" ? 1 : -1;
|
|
139
|
+
}
|
|
125
140
|
}
|
|
126
141
|
return 0;
|
|
127
142
|
});
|
|
@@ -158,15 +173,17 @@ export class ContentCollection {
|
|
|
158
173
|
resolve() {
|
|
159
174
|
const { contentType, filter, sort, limit, _project } = this.config;
|
|
160
175
|
const entries = _project?.contentTypes?.get(contentType ?? "");
|
|
161
|
-
if (!entries)
|
|
176
|
+
if (!entries) {
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
162
179
|
return queryContentType(entries, {
|
|
163
180
|
...(filter != null && {
|
|
164
181
|
filter: filter as
|
|
165
182
|
| Record<string, unknown>
|
|
166
|
-
|
|
|
183
|
+
| { field: string; op: string; value?: unknown }[],
|
|
167
184
|
}),
|
|
168
185
|
...(sort != null && {
|
|
169
|
-
sort: sort as { field: string; order?: string } |
|
|
186
|
+
sort: sort as { field: string; order?: string } | { field: string; order?: string }[],
|
|
170
187
|
}),
|
|
171
188
|
...(limit != null && { limit }),
|
|
172
189
|
});
|
|
@@ -183,7 +200,9 @@ export class ContentEntry {
|
|
|
183
200
|
resolve() {
|
|
184
201
|
const { contentType, id, field, _project, _document } = this.config;
|
|
185
202
|
const entries = _project?.contentTypes?.get(contentType ?? "");
|
|
186
|
-
if (!entries)
|
|
203
|
+
if (!entries) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
187
206
|
|
|
188
207
|
let resolvedId = id;
|
|
189
208
|
if (
|
|
@@ -194,7 +213,9 @@ export class ContentEntry {
|
|
|
194
213
|
const paramName = (resolvedId as { $ref: string }).$ref.replace("#/$params/", "");
|
|
195
214
|
resolvedId = _document?.route?._pathParams?.[paramName];
|
|
196
215
|
}
|
|
197
|
-
if (!resolvedId)
|
|
216
|
+
if (!resolvedId) {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
198
219
|
|
|
199
220
|
if (field && field !== "id") {
|
|
200
221
|
return entries.find((e: ContentLoaderEntry) => e.data[field] === resolvedId) ?? null;
|
package/src/csv.ts
CHANGED
|
@@ -32,21 +32,27 @@ export function parseCSV(csv: string): Record<string, string>[] {
|
|
|
32
32
|
current += ch;
|
|
33
33
|
if (inQuotes && csv[i + 1] === '"') {
|
|
34
34
|
current += csv[i + 1];
|
|
35
|
-
i
|
|
35
|
+
i += 1;
|
|
36
36
|
} else {
|
|
37
37
|
inQuotes = !inQuotes;
|
|
38
38
|
}
|
|
39
39
|
} else if ((ch === "\n" || (ch === "\r" && csv[i + 1] === "\n")) && !inQuotes) {
|
|
40
40
|
lines.push(current);
|
|
41
41
|
current = "";
|
|
42
|
-
if (ch === "\r")
|
|
42
|
+
if (ch === "\r") {
|
|
43
|
+
i += 1;
|
|
44
|
+
}
|
|
43
45
|
} else {
|
|
44
46
|
current += ch;
|
|
45
47
|
}
|
|
46
48
|
}
|
|
47
|
-
if (current.trim())
|
|
49
|
+
if (current.trim()) {
|
|
50
|
+
lines.push(current);
|
|
51
|
+
}
|
|
48
52
|
|
|
49
|
-
if (lines.length === 0)
|
|
53
|
+
if (lines.length === 0) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
50
56
|
|
|
51
57
|
const parseRow = (line: string) => {
|
|
52
58
|
const fields: string[] = [];
|
|
@@ -57,7 +63,7 @@ export function parseCSV(csv: string): Record<string, string>[] {
|
|
|
57
63
|
if (ch === '"') {
|
|
58
64
|
if (q && line[i + 1] === '"') {
|
|
59
65
|
field += '"';
|
|
60
|
-
i
|
|
66
|
+
i += 1;
|
|
61
67
|
} else {
|
|
62
68
|
q = !q;
|
|
63
69
|
}
|
|
@@ -107,15 +113,17 @@ export function coerceCSVRows(
|
|
|
107
113
|
const data: Record<string, unknown> = { ...row };
|
|
108
114
|
if (schema?.properties) {
|
|
109
115
|
for (const [key, def] of Object.entries(schema.properties)) {
|
|
110
|
-
if (!(key in data))
|
|
116
|
+
if (!(key in data)) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
111
119
|
if (def.type === "number") {
|
|
112
120
|
const raw = String(data[key] ?? "").trim();
|
|
113
121
|
if (raw === "") {
|
|
114
122
|
data[key] = null;
|
|
115
123
|
} else {
|
|
116
|
-
const cleaned = raw.
|
|
124
|
+
const cleaned = raw.replaceAll(/[$€£¥,\s]/g, "");
|
|
117
125
|
const n = Number(cleaned);
|
|
118
|
-
data[key] = isNaN(n) ? null : n;
|
|
126
|
+
data[key] = Number.isNaN(n) ? null : n;
|
|
119
127
|
}
|
|
120
128
|
} else if (def.type === "boolean") {
|
|
121
129
|
data[key] = data[key] === "true";
|
|
@@ -138,7 +146,7 @@ export function coerceCSVRows(
|
|
|
138
146
|
(data.slug as string | undefined) ??
|
|
139
147
|
(data.Slug as string | undefined) ??
|
|
140
148
|
String(i);
|
|
141
|
-
return {
|
|
149
|
+
return { body: null, data, id };
|
|
142
150
|
});
|
|
143
151
|
}
|
|
144
152
|
|
|
@@ -193,7 +201,7 @@ export class Csv {
|
|
|
193
201
|
return Csv.parse(await response.text(), options);
|
|
194
202
|
}
|
|
195
203
|
const { readFileSync } = await import("node:fs");
|
|
196
|
-
return Csv.parse(readFileSync(path, "
|
|
204
|
+
return Csv.parse(readFileSync(path, "utf8"), options);
|
|
197
205
|
}
|
|
198
206
|
|
|
199
207
|
/** Runtime on-demand access: load the configured source. */
|
package/src/html-to-jx.ts
CHANGED
|
@@ -24,7 +24,9 @@ function convertHastChildren(children: HastNode[]) {
|
|
|
24
24
|
const result: (JxElement | string)[] = [];
|
|
25
25
|
for (const child of children) {
|
|
26
26
|
const converted = convertHastNode(child);
|
|
27
|
-
if (converted != null)
|
|
27
|
+
if (converted != null) {
|
|
28
|
+
result.push(converted);
|
|
29
|
+
}
|
|
28
30
|
}
|
|
29
31
|
return result;
|
|
30
32
|
}
|
|
@@ -35,7 +37,9 @@ function convertHastChildren(children: HastNode[]) {
|
|
|
35
37
|
*/
|
|
36
38
|
function convertHastNode(node: HastNode) {
|
|
37
39
|
if (node.type === "text") {
|
|
38
|
-
if (whitespace(node as unknown as HastNode))
|
|
40
|
+
if (whitespace(node as unknown as HastNode)) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
39
43
|
return node.value ?? null;
|
|
40
44
|
}
|
|
41
45
|
|
|
@@ -44,14 +48,18 @@ function convertHastNode(node: HastNode) {
|
|
|
44
48
|
|
|
45
49
|
if (node.properties && Object.keys(node.properties).length > 0) {
|
|
46
50
|
const { style, attrs } = hastPropsToJx(node.properties);
|
|
47
|
-
if (Object.keys(attrs).length > 0)
|
|
48
|
-
|
|
51
|
+
if (Object.keys(attrs).length > 0) {
|
|
52
|
+
el.attributes = attrs;
|
|
53
|
+
}
|
|
54
|
+
if (Object.keys(style).length > 0) {
|
|
55
|
+
el.style = style;
|
|
56
|
+
}
|
|
49
57
|
}
|
|
50
58
|
|
|
51
59
|
const kids = node.children ? convertHastChildren(node.children) : [];
|
|
52
60
|
|
|
53
61
|
if (kids.length === 1 && typeof kids[0] === "string") {
|
|
54
|
-
el.textContent = kids
|
|
62
|
+
[el.textContent] = kids;
|
|
55
63
|
} else if (kids.length > 0) {
|
|
56
64
|
el.children = kids as JxElement[];
|
|
57
65
|
}
|
|
@@ -71,7 +79,9 @@ function hastPropsToJx(properties: Record<string, unknown>) {
|
|
|
71
79
|
const style: Record<string, string> = {};
|
|
72
80
|
|
|
73
81
|
for (const [key, value] of Object.entries(properties)) {
|
|
74
|
-
if (value === false || value === undefined || value === null)
|
|
82
|
+
if (value === false || value === undefined || value === null) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
75
85
|
|
|
76
86
|
const info = find(htmlInfo, key);
|
|
77
87
|
const name = info.attribute;
|
|
@@ -89,7 +99,7 @@ function hastPropsToJx(properties: Record<string, unknown>) {
|
|
|
89
99
|
attrs[name] = String(value);
|
|
90
100
|
}
|
|
91
101
|
}
|
|
92
|
-
return {
|
|
102
|
+
return { attrs, style };
|
|
93
103
|
}
|
|
94
104
|
|
|
95
105
|
/**
|
|
@@ -99,9 +109,13 @@ function hastPropsToJx(properties: Record<string, unknown>) {
|
|
|
99
109
|
function parseInlineStyle(styleStr: string, out: Record<string, string>) {
|
|
100
110
|
for (const decl of styleStr.split(";")) {
|
|
101
111
|
const colon = decl.indexOf(":");
|
|
102
|
-
if (colon === -1)
|
|
112
|
+
if (colon === -1) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
103
115
|
const prop = decl.slice(0, colon).trim();
|
|
104
116
|
const val = decl.slice(colon + 1).trim();
|
|
105
|
-
if (prop && val)
|
|
117
|
+
if (prop && val) {
|
|
118
|
+
out[prop] = val;
|
|
119
|
+
}
|
|
106
120
|
}
|
|
107
121
|
}
|
package/src/markdown.ts
CHANGED
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { transpileJxMarkdown } from "./transpile.ts";
|
|
19
|
-
import { serializeJxMarkdown
|
|
19
|
+
import { serializeJxMarkdown } from "./serialize.ts";
|
|
20
|
+
import type { SerializeOptions } from "./serialize.ts";
|
|
20
21
|
import type { JxDocument } from "@jxsuite/schema/types";
|
|
21
22
|
import type { ContentLoaderEntry, MarkdownFileResult } from "./types.ts";
|
|
22
23
|
|
|
@@ -76,7 +77,7 @@ export class Markdown {
|
|
|
76
77
|
): Promise<ContentLoaderEntry[]> {
|
|
77
78
|
const { processMarkdown } = await import("./md.ts");
|
|
78
79
|
const { readFileSync } = await import("node:fs");
|
|
79
|
-
const source = readFileSync(path, "
|
|
80
|
+
const source = readFileSync(path, "utf8");
|
|
80
81
|
const result = processMarkdown(source, path, {
|
|
81
82
|
...(options.directives !== undefined && { directives: options.directives }),
|
|
82
83
|
...(options.directiveOptions !== undefined && {
|
|
@@ -84,17 +85,25 @@ export class Markdown {
|
|
|
84
85
|
}),
|
|
85
86
|
});
|
|
86
87
|
const _meta: ContentLoaderEntry["_meta"] = {};
|
|
87
|
-
if (result.$excerpt != null)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (result.$
|
|
88
|
+
if (result.$excerpt != null) {
|
|
89
|
+
_meta.excerpt = result.$excerpt;
|
|
90
|
+
}
|
|
91
|
+
if (result.$toc != null) {
|
|
92
|
+
_meta.toc = result.$toc;
|
|
93
|
+
}
|
|
94
|
+
if (result.$readingTime != null) {
|
|
95
|
+
_meta.readingTime = result.$readingTime;
|
|
96
|
+
}
|
|
97
|
+
if (result.$wordCount != null) {
|
|
98
|
+
_meta.wordCount = result.$wordCount;
|
|
99
|
+
}
|
|
91
100
|
return [
|
|
92
101
|
{
|
|
93
|
-
id: result.slug,
|
|
94
|
-
data: result.frontmatter,
|
|
95
|
-
body: source,
|
|
96
102
|
$children: result.$children,
|
|
97
103
|
_meta,
|
|
104
|
+
body: source,
|
|
105
|
+
data: result.frontmatter,
|
|
106
|
+
id: result.slug,
|
|
98
107
|
},
|
|
99
108
|
];
|
|
100
109
|
}
|
|
@@ -106,7 +115,7 @@ export class Markdown {
|
|
|
106
115
|
const { resolve } = await import("node:path");
|
|
107
116
|
const { src, basePath, ...processorConfig } = this.config;
|
|
108
117
|
const filePath = basePath ? resolve(basePath, src) : resolve(src);
|
|
109
|
-
const source = readFileSync(filePath, "
|
|
118
|
+
const source = readFileSync(filePath, "utf8");
|
|
110
119
|
return processMarkdown(source, filePath, processorConfig);
|
|
111
120
|
}
|
|
112
121
|
}
|
package/src/md.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Jxsuite/md — Markdown integration for Jx
|
|
3
3
|
*
|
|
4
4
|
* Provides two exports:
|
|
5
5
|
* - MarkdownFile — Parse a single markdown file (external class for $prototype)
|
|
@@ -21,7 +21,7 @@ import { readFileSync } from "node:fs";
|
|
|
21
21
|
import { basename, extname, resolve as resolvePath } from "node:path";
|
|
22
22
|
import { globSync } from "glob";
|
|
23
23
|
import { mdastNodeToJx } from "./transpile.ts";
|
|
24
|
-
import type { MarkdownFileResult,
|
|
24
|
+
import type { MarkdownFileResult, MdastNode, TocEntry } from "./types.ts";
|
|
25
25
|
import type { JxElement } from "@jxsuite/schema/types";
|
|
26
26
|
|
|
27
27
|
// ─── Tree utilities (inline to avoid Bun ESM resolution issues with unist-util-*) ──
|
|
@@ -42,10 +42,16 @@ function visit(
|
|
|
42
42
|
const visitor = type ? maybeVisitor : typeOrVisitor;
|
|
43
43
|
|
|
44
44
|
function walk(node: MdastNode) {
|
|
45
|
-
if (!node || typeof node !== "object")
|
|
46
|
-
|
|
45
|
+
if (!node || typeof node !== "object") {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (!type || node.type === type) {
|
|
49
|
+
(visitor as (node: MdastNode) => void)(node);
|
|
50
|
+
}
|
|
47
51
|
if (Array.isArray(node.children)) {
|
|
48
|
-
for (const child of node.children)
|
|
52
|
+
for (const child of node.children) {
|
|
53
|
+
walk(child);
|
|
54
|
+
}
|
|
49
55
|
}
|
|
50
56
|
}
|
|
51
57
|
walk(tree as MdastNode);
|
|
@@ -58,10 +64,18 @@ function visit(
|
|
|
58
64
|
* @returns {string}
|
|
59
65
|
*/
|
|
60
66
|
function mdastToString(node: MdastNode): string {
|
|
61
|
-
if (!node)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (
|
|
67
|
+
if (!node) {
|
|
68
|
+
return "";
|
|
69
|
+
}
|
|
70
|
+
if (typeof node === "string") {
|
|
71
|
+
return node;
|
|
72
|
+
}
|
|
73
|
+
if (node.value) {
|
|
74
|
+
return node.value;
|
|
75
|
+
}
|
|
76
|
+
if (Array.isArray(node.children)) {
|
|
77
|
+
return node.children.map(mdastToString).join("");
|
|
78
|
+
}
|
|
65
79
|
return "";
|
|
66
80
|
}
|
|
67
81
|
|
|
@@ -91,11 +105,11 @@ function extractToc(tree: MdastNode) {
|
|
|
91
105
|
const text = mdastToString(node);
|
|
92
106
|
const id = text
|
|
93
107
|
.toLowerCase()
|
|
94
|
-
.
|
|
95
|
-
.
|
|
96
|
-
.
|
|
97
|
-
.
|
|
98
|
-
entries.push({ depth: node.depth as number,
|
|
108
|
+
.replaceAll(/[^\w\s-]/g, "")
|
|
109
|
+
.replaceAll(/\s+/g, "-")
|
|
110
|
+
.replaceAll(/-+/g, "-")
|
|
111
|
+
.replaceAll(/^-|-$/g, "");
|
|
112
|
+
entries.push({ depth: node.depth as number, id, text });
|
|
99
113
|
});
|
|
100
114
|
return entries;
|
|
101
115
|
}
|
|
@@ -109,9 +123,13 @@ function extractToc(tree: MdastNode) {
|
|
|
109
123
|
function extractExcerpt(tree: MdastNode) {
|
|
110
124
|
let firstParagraph: MdastNode | null = null;
|
|
111
125
|
visit(tree, "paragraph", (node: MdastNode) => {
|
|
112
|
-
if (!firstParagraph)
|
|
126
|
+
if (!firstParagraph) {
|
|
127
|
+
firstParagraph = node;
|
|
128
|
+
}
|
|
113
129
|
});
|
|
114
|
-
if (!firstParagraph)
|
|
130
|
+
if (!firstParagraph) {
|
|
131
|
+
return "";
|
|
132
|
+
}
|
|
115
133
|
return mdastToString(firstParagraph);
|
|
116
134
|
}
|
|
117
135
|
|
|
@@ -147,7 +165,6 @@ export function processMarkdown(
|
|
|
147
165
|
|
|
148
166
|
const tree = processor.parse(source);
|
|
149
167
|
const vfile = { data: {} };
|
|
150
|
-
// @ts-ignore - vfile shape satisfies unified's internal contract
|
|
151
168
|
processor.runSync(tree, vfile);
|
|
152
169
|
|
|
153
170
|
const vfileData = vfile.data as Record<string, unknown>;
|
|
@@ -161,19 +178,20 @@ export function processMarkdown(
|
|
|
161
178
|
const bodyNodes = tree.children.filter(
|
|
162
179
|
(n: MdastNode) => n.type !== "yaml" && n.type !== "toml",
|
|
163
180
|
) as MdastNode[];
|
|
164
|
-
const $children = bodyNodes
|
|
165
|
-
|
|
166
|
-
|
|
181
|
+
const $children = bodyNodes.map((n: MdastNode) => mdastNodeToJx(n)).filter(Boolean) as (
|
|
182
|
+
| JxElement
|
|
183
|
+
| string
|
|
184
|
+
)[];
|
|
167
185
|
|
|
168
186
|
return {
|
|
169
|
-
slug,
|
|
170
|
-
path: filePath,
|
|
171
|
-
frontmatter,
|
|
172
187
|
$children,
|
|
173
188
|
$excerpt: excerpt,
|
|
174
|
-
$toc: toc,
|
|
175
189
|
$readingTime: readingTime(plainText),
|
|
190
|
+
$toc: toc,
|
|
176
191
|
$wordCount: plainText.split(/\s+/).filter(Boolean).length,
|
|
192
|
+
frontmatter,
|
|
193
|
+
path: filePath,
|
|
194
|
+
slug,
|
|
177
195
|
};
|
|
178
196
|
}
|
|
179
197
|
|
|
@@ -185,12 +203,11 @@ export function processMarkdown(
|
|
|
185
203
|
* @returns {unknown}
|
|
186
204
|
*/
|
|
187
205
|
function getNestedValue(obj: Record<string, unknown> | MarkdownFileResult, path: string) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
);
|
|
206
|
+
let current: unknown = obj;
|
|
207
|
+
for (const k of path.split(".")) {
|
|
208
|
+
current = (current as Record<string, unknown> | undefined)?.[k];
|
|
209
|
+
}
|
|
210
|
+
return current;
|
|
194
211
|
}
|
|
195
212
|
|
|
196
213
|
// ─── Markdown format class (re-exported from browser-safe module) ────────────
|
|
@@ -266,22 +283,28 @@ export class MarkdownCollection {
|
|
|
266
283
|
const files = globSync(pattern, { absolute: true });
|
|
267
284
|
|
|
268
285
|
const results = files.map((filePath: string) => {
|
|
269
|
-
const source = readFileSync(filePath, "
|
|
286
|
+
const source = readFileSync(filePath, "utf8");
|
|
270
287
|
return processMarkdown(source, filePath, processorConfig);
|
|
271
288
|
});
|
|
272
289
|
|
|
273
290
|
// Filter
|
|
274
291
|
let filtered = results;
|
|
275
292
|
if (typeof filter === "function") {
|
|
276
|
-
filtered = results.filter(
|
|
293
|
+
filtered = results.filter((r: MarkdownFileResult) =>
|
|
294
|
+
(filter as (r: MarkdownFileResult) => boolean)(r),
|
|
295
|
+
);
|
|
277
296
|
}
|
|
278
297
|
|
|
279
298
|
// Sort
|
|
280
299
|
filtered.sort((a: MarkdownFileResult, b: MarkdownFileResult) => {
|
|
281
300
|
const aVal = getNestedValue(a, sortBy) ?? ("" as string | number);
|
|
282
301
|
const bVal = getNestedValue(b, sortBy) ?? ("" as string | number);
|
|
283
|
-
if (aVal < bVal)
|
|
284
|
-
|
|
302
|
+
if (aVal < bVal) {
|
|
303
|
+
return sortOrder === "asc" ? -1 : 1;
|
|
304
|
+
}
|
|
305
|
+
if (aVal > bVal) {
|
|
306
|
+
return sortOrder === "asc" ? 1 : -1;
|
|
307
|
+
}
|
|
285
308
|
return 0;
|
|
286
309
|
});
|
|
287
310
|
|