@jxsuite/parser 1.6.0 → 1.8.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/dist/content-loader.js +9 -59
- package/dist/content.js +0 -50
- package/dist/csv.js +100 -51
- package/dist/markdown.js +45509 -49146
- package/dist/serialize.js +7368 -9752
- package/package.json +6 -6
- package/src/Content.class.json +1 -1
- package/src/Csv.class.json +31 -0
- package/src/csv.ts +176 -1
- package/src/transpile.ts +37 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/parser",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Jx markdown parser and external class integration",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
"upgrade": "bun update --latest && bun install"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@jxsuite/markup": "^0.4.
|
|
52
|
-
"@jxsuite/schema": "^2.
|
|
51
|
+
"@jxsuite/markup": "^0.4.10",
|
|
52
|
+
"@jxsuite/schema": "^2.2.0",
|
|
53
53
|
"@shikijs/langs": "^4.4.3",
|
|
54
54
|
"@shikijs/themes": "^4.4.3",
|
|
55
55
|
"glob": "^13.0.6",
|
|
@@ -63,11 +63,11 @@
|
|
|
63
63
|
"shiki": "^4.4.3",
|
|
64
64
|
"unified": "^11.0.5",
|
|
65
65
|
"unist-util-visit": "^5.1.0",
|
|
66
|
-
"yaml": "^2.9.
|
|
66
|
+
"yaml": "^2.9.1"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
|
-
"@happy-dom/global-registrator": "^20.
|
|
70
|
-
"@jxsuite/runtime": "^
|
|
69
|
+
"@happy-dom/global-registrator": "^20.14.5",
|
|
70
|
+
"@jxsuite/runtime": "^4.0.0",
|
|
71
71
|
"@types/mdast": "^4.0.4",
|
|
72
72
|
"ajv": "^8.20.0"
|
|
73
73
|
},
|
package/src/Content.class.json
CHANGED
package/src/Csv.class.json
CHANGED
|
@@ -115,6 +115,37 @@
|
|
|
115
115
|
"returnType": { "$ref": "#/$defs/returnTypes/ContentLoaderEntries" },
|
|
116
116
|
"description": "Parse CSV text into typed content entries (pure, browser-safe)"
|
|
117
117
|
},
|
|
118
|
+
"rewrite": {
|
|
119
|
+
"role": "rewrite",
|
|
120
|
+
"$prototype": "Function",
|
|
121
|
+
"access": "public",
|
|
122
|
+
"scope": "static",
|
|
123
|
+
"identifier": "rewrite",
|
|
124
|
+
"timing": ["compiler", "server", "client"],
|
|
125
|
+
"parameters": [
|
|
126
|
+
{
|
|
127
|
+
"identifier": "source",
|
|
128
|
+
"type": { "type": "string" },
|
|
129
|
+
"description": "Raw CSV text"
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"identifier": "edits",
|
|
133
|
+
"type": {
|
|
134
|
+
"type": "array",
|
|
135
|
+
"items": {
|
|
136
|
+
"type": "object",
|
|
137
|
+
"properties": {
|
|
138
|
+
"from": { "type": "string" },
|
|
139
|
+
"to": { "type": "string" }
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
"description": "Authored cell values to replace, matched whole against a data cell"
|
|
144
|
+
}
|
|
145
|
+
],
|
|
146
|
+
"returnType": { "type": "string" },
|
|
147
|
+
"description": "Replace authored cell values in CSV source text, preserving every other byte"
|
|
148
|
+
},
|
|
118
149
|
"discover": {
|
|
119
150
|
"role": "discover",
|
|
120
151
|
"$prototype": "Function",
|
package/src/csv.ts
CHANGED
|
@@ -2,10 +2,15 @@
|
|
|
2
2
|
* Csv — CSV content format for Jx
|
|
3
3
|
*
|
|
4
4
|
* RFC 4180-style CSV parsing with schema-driven type coercion, packaged as a Jx
|
|
5
|
-
* format-extension class. `parse`
|
|
5
|
+
* format-extension class. `parse` and `rewrite` are pure and browser-safe; `discover`, `load`, and
|
|
6
6
|
* instance `resolve` touch the filesystem (or fetch remote URLs) and dynamically
|
|
7
7
|
* import node modules so the module itself stays importable in the browser.
|
|
8
8
|
*
|
|
9
|
+
* There is no `serialize`, and there will not be one: a collection is a data source entries are
|
|
10
|
+
* loaded FROM, and re-emitting one would mean choosing a quoting style, a line ending and a column
|
|
11
|
+
* order the author already chose. `rewrite` is the narrower promise that covers the case that
|
|
12
|
+
* actually needed it — a rename repairing a reference a row names (specs/extensions.md §8).
|
|
13
|
+
*
|
|
9
14
|
* @module @jxsuite/parser/csv
|
|
10
15
|
* @license MIT
|
|
11
16
|
*/
|
|
@@ -90,6 +95,164 @@ export function parseCSV(csv: string): Record<string, string>[] {
|
|
|
90
95
|
return rows;
|
|
91
96
|
}
|
|
92
97
|
|
|
98
|
+
// ─── Cell-level rewriting ────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
/** One authored value to replace, wherever a data cell holds exactly it. */
|
|
101
|
+
export interface CsvEdit {
|
|
102
|
+
/** The cell value as `parse` produced it — unquoted and trimmed. */
|
|
103
|
+
from: string;
|
|
104
|
+
/** What to write in its place. */
|
|
105
|
+
to: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** One field of a CSV source: where its bytes are, and what {@link parseCSV} made of it. */
|
|
109
|
+
interface CsvField {
|
|
110
|
+
/** Row index in the source. Row 0 is the header. */
|
|
111
|
+
row: number;
|
|
112
|
+
/** Offsets of the field's raw text within the source, delimiting commas excluded. */
|
|
113
|
+
start: number;
|
|
114
|
+
end: number;
|
|
115
|
+
/** The value {@link parseCSV} produces for it — unquoted and trimmed. */
|
|
116
|
+
value: string;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Row spans, splitting on newlines that are not inside quotes.
|
|
121
|
+
*
|
|
122
|
+
* A deliberate mirror of {@link parseCSV}'s first pass, offsets instead of text: the two must agree
|
|
123
|
+
* on where a row ends or a rewrite would splice into the wrong cell. The accumulated text there is
|
|
124
|
+
* exactly `source.slice(start, end)` here, because the only characters that pass are contiguous.
|
|
125
|
+
*/
|
|
126
|
+
function scanRows(csv: string): { start: number; end: number }[] {
|
|
127
|
+
const rows: { start: number; end: number }[] = [];
|
|
128
|
+
let start = 0;
|
|
129
|
+
let inQuotes = false;
|
|
130
|
+
for (let i = 0; i < csv.length; i++) {
|
|
131
|
+
const ch = csv[i];
|
|
132
|
+
if (ch === '"') {
|
|
133
|
+
if (inQuotes && csv[i + 1] === '"') {
|
|
134
|
+
i += 1;
|
|
135
|
+
} else {
|
|
136
|
+
inQuotes = !inQuotes;
|
|
137
|
+
}
|
|
138
|
+
} else if ((ch === "\n" || (ch === "\r" && csv[i + 1] === "\n")) && !inQuotes) {
|
|
139
|
+
rows.push({ end: i, start });
|
|
140
|
+
if (ch === "\r") {
|
|
141
|
+
i += 1;
|
|
142
|
+
}
|
|
143
|
+
start = i + 1;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// The same test {@link parseCSV} applies to its final accumulated row, which is exactly this
|
|
147
|
+
// Slice: a trailing newline leaves whitespace behind, and that is not a row.
|
|
148
|
+
if (csv.slice(start).trim()) {
|
|
149
|
+
rows.push({ end: csv.length, start });
|
|
150
|
+
}
|
|
151
|
+
return rows;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Field spans within one row, mirroring {@link parseCSV}'s second pass. */
|
|
155
|
+
function scanRowFields(csv: string, row: number, span: { start: number; end: number }): CsvField[] {
|
|
156
|
+
const line = csv.slice(span.start, span.end);
|
|
157
|
+
const fields: CsvField[] = [];
|
|
158
|
+
let fieldStart = 0;
|
|
159
|
+
let value = "";
|
|
160
|
+
let inQuotes = false;
|
|
161
|
+
for (let i = 0; i < line.length; i++) {
|
|
162
|
+
const ch = line[i];
|
|
163
|
+
if (ch === '"') {
|
|
164
|
+
if (inQuotes && line[i + 1] === '"') {
|
|
165
|
+
value += '"';
|
|
166
|
+
i += 1;
|
|
167
|
+
} else {
|
|
168
|
+
inQuotes = !inQuotes;
|
|
169
|
+
}
|
|
170
|
+
} else if (ch === "," && !inQuotes) {
|
|
171
|
+
fields.push({
|
|
172
|
+
end: span.start + i,
|
|
173
|
+
row,
|
|
174
|
+
start: span.start + fieldStart,
|
|
175
|
+
value: value.trim(),
|
|
176
|
+
});
|
|
177
|
+
value = "";
|
|
178
|
+
fieldStart = i + 1;
|
|
179
|
+
} else {
|
|
180
|
+
value += ch;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
fields.push({ end: span.end, row, start: span.start + fieldStart, value: value.trim() });
|
|
184
|
+
return fields;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Every field of a CSV source in document order, with the offsets a splice needs. */
|
|
188
|
+
function scanFields(csv: string): CsvField[] {
|
|
189
|
+
return scanRows(csv).flatMap((span, row) => scanRowFields(csv, row, span));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Write `value` into the space a field's raw text occupied, keeping that field's own conventions.
|
|
194
|
+
*
|
|
195
|
+
* Padding survives because it is the author's, and quoting survives for the same reason — a column
|
|
196
|
+
* written `"a","b"` stays quoted even when the new value would not need it. A value that WOULD be
|
|
197
|
+
* misread unquoted (a comma, a quote, a newline, or padding of its own) is quoted regardless, which
|
|
198
|
+
* is the only case where the output style is not simply the input's.
|
|
199
|
+
*/
|
|
200
|
+
function encodeField(raw: string, value: string): string {
|
|
201
|
+
const lead = raw.slice(0, raw.length - raw.trimStart().length);
|
|
202
|
+
const trail = raw.slice(raw.trimEnd().length);
|
|
203
|
+
const core = raw.trim();
|
|
204
|
+
const wasQuoted = core.length >= 2 && core.startsWith('"') && core.endsWith('"');
|
|
205
|
+
const mustQuote = /["\r\n,]/.test(value) || value.trim() !== value;
|
|
206
|
+
return lead + (wasQuoted || mustQuote ? `"${value.replaceAll('"', '""')}"` : value) + trail;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Replace authored cell values in CSV source text, preserving every other byte.
|
|
211
|
+
*
|
|
212
|
+
* The `rewrite` capability (specs/extensions.md §8), and deliberately narrower than `serialize`. A
|
|
213
|
+
* CSV collection is a data source entries are loaded FROM, not a document Jx round-trips, so it has
|
|
214
|
+
* no serializer and never will — quoting style, line endings and column order are the author's and
|
|
215
|
+
* the loader has no opinion about any of them. But the rename refactor does not need a document
|
|
216
|
+
* back; it needs one cell's text changed. Without that, renaming a file a CSV row names updated
|
|
217
|
+
* every other reference and reported this one as a remainder the author had to repair by hand
|
|
218
|
+
* (issue 246).
|
|
219
|
+
*
|
|
220
|
+
* Matching is on the WHOLE cell value as {@link parseCSV} produced it, never on a substring:
|
|
221
|
+
* `hero.jpg` must not rewrite the middle of `my-hero.jpg`. The HEADER row is never touched — it
|
|
222
|
+
* names columns rather than files, `parse` does not expose it as a value, and an edit derived from
|
|
223
|
+
* `parse` output therefore cannot legitimately name it.
|
|
224
|
+
*
|
|
225
|
+
* @param source — the CSV file's original text
|
|
226
|
+
* @param edits — authored values to replace
|
|
227
|
+
* @returns The source with exactly those cells rewritten
|
|
228
|
+
*/
|
|
229
|
+
export function rewriteCSV(source: string, edits: readonly CsvEdit[]): string {
|
|
230
|
+
const replacements = new Map<string, string>();
|
|
231
|
+
for (const edit of edits) {
|
|
232
|
+
if (edit.from !== edit.to) {
|
|
233
|
+
replacements.set(edit.from, edit.to);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (replacements.size === 0) {
|
|
237
|
+
return source;
|
|
238
|
+
}
|
|
239
|
+
let out = "";
|
|
240
|
+
let cursor = 0;
|
|
241
|
+
for (const field of scanFields(source)) {
|
|
242
|
+
if (field.row === 0) {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
const to = replacements.get(field.value);
|
|
246
|
+
if (to === undefined) {
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
out += source.slice(cursor, field.start);
|
|
250
|
+
out += encodeField(source.slice(field.start, field.end), to);
|
|
251
|
+
cursor = field.end;
|
|
252
|
+
}
|
|
253
|
+
return out + source.slice(cursor);
|
|
254
|
+
}
|
|
255
|
+
|
|
93
256
|
// ─── Type coercion ───────────────────────────────────────────────────────────
|
|
94
257
|
|
|
95
258
|
export interface CsvOptions {
|
|
@@ -171,6 +334,18 @@ export class Csv {
|
|
|
171
334
|
return coerceCSVRows(parseCSV(source), options.schema, options.idField);
|
|
172
335
|
}
|
|
173
336
|
|
|
337
|
+
/**
|
|
338
|
+
* Replace authored cell values, preserving every other byte (pure, browser-safe).
|
|
339
|
+
*
|
|
340
|
+
* The `rewrite` capability. Csv declares it and deliberately declares no `serialize`: rows are
|
|
341
|
+
* loaded, not round-tripped, so there is no document to write back — but one cell's text can be
|
|
342
|
+
* changed without inventing an opinion about quoting, line endings or column order. See
|
|
343
|
+
* {@link rewriteCSV}.
|
|
344
|
+
*/
|
|
345
|
+
static rewrite(source: string, edits: readonly CsvEdit[] = []): string {
|
|
346
|
+
return rewriteCSV(source, edits);
|
|
347
|
+
}
|
|
348
|
+
|
|
174
349
|
/** List .csv entry files for a content-type source (file path or directory). */
|
|
175
350
|
static async discover(source: string, options: { baseDir?: string } = {}): Promise<string[]> {
|
|
176
351
|
const { existsSync, readdirSync } = await import("node:fs");
|
package/src/transpile.ts
CHANGED
|
@@ -136,7 +136,14 @@ export function collapseDotPaths(obj: Record<string, unknown>) {
|
|
|
136
136
|
return result;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
/**
|
|
139
|
+
/**
|
|
140
|
+
* CSS pseudo-CLASS names — keys that become `:` prefixed in style objects.
|
|
141
|
+
*
|
|
142
|
+
* `before` and `after` are pseudo-_elements_ and belong in the set below on the modern spelling,
|
|
143
|
+
* but they have been written with one colon since this file existed and CSS still accepts that
|
|
144
|
+
* form. Moving them would rewrite every `.md` component in the repo for no behaviour change, so
|
|
145
|
+
* they stay here; {@link CSS_PSEUDO_ELEMENTS} is for names that have never had a one-colon form.
|
|
146
|
+
*/
|
|
140
147
|
const CSS_PSEUDO_NAMES = new Set([
|
|
141
148
|
"hover",
|
|
142
149
|
"focus",
|
|
@@ -156,8 +163,24 @@ const CSS_PSEUDO_NAMES = new Set([
|
|
|
156
163
|
"selection",
|
|
157
164
|
"before",
|
|
158
165
|
"after",
|
|
166
|
+
// The overlay states. Without these an author could declare a popover in a `.md` component and
|
|
167
|
+
// Have no way to style it open — `style.popover-open.opacity` fell through unmapped, and
|
|
168
|
+
// `collectStyles` then read the bare key as a DESCENDANT TYPE SELECTOR, emitting
|
|
169
|
+
// `#panel popover-open { … }`: a rule that matches nothing, with nothing to say it does not.
|
|
170
|
+
"popover-open",
|
|
171
|
+
"open",
|
|
172
|
+
"modal",
|
|
159
173
|
]);
|
|
160
174
|
|
|
175
|
+
/**
|
|
176
|
+
* CSS pseudo-ELEMENT names — keys that become `::` prefixed.
|
|
177
|
+
*
|
|
178
|
+
* A separate set because the prefix is different, not because the concept is. `backdrop` is the
|
|
179
|
+
* only member today and it is the one a popover needs: there is no one-colon form of it, so the
|
|
180
|
+
* single-colon set above could not have carried it.
|
|
181
|
+
*/
|
|
182
|
+
const CSS_PSEUDO_ELEMENTS = new Set(["backdrop"]);
|
|
183
|
+
|
|
161
184
|
/**
|
|
162
185
|
* Apply CSS pseudo-class and media query key mapping to a style object's top-level keys.
|
|
163
186
|
*
|
|
@@ -171,7 +194,9 @@ const CSS_PSEUDO_NAMES = new Set([
|
|
|
171
194
|
export function applyStyleKeyMapping(styleObj: Record<string, unknown>) {
|
|
172
195
|
const result: Record<string, unknown> = {};
|
|
173
196
|
for (const [key, value] of Object.entries(styleObj)) {
|
|
174
|
-
if (
|
|
197
|
+
if (CSS_PSEUDO_ELEMENTS.has(key)) {
|
|
198
|
+
result[`::${key}`] = value;
|
|
199
|
+
} else if (CSS_PSEUDO_NAMES.has(key)) {
|
|
175
200
|
result[`:${key}`] = value;
|
|
176
201
|
} else if (key.startsWith("--")) {
|
|
177
202
|
result[`@${key}`] = value;
|
|
@@ -208,7 +233,11 @@ export function collapseStylePaths(styleObj: Record<string, unknown>) {
|
|
|
208
233
|
const normalized: Record<string, unknown> = {};
|
|
209
234
|
|
|
210
235
|
for (const [key, value] of Object.entries(styleObj)) {
|
|
211
|
-
|
|
236
|
+
/* `::` before `:`, or a pseudo-element would match the pseudo-class arm on its second colon
|
|
237
|
+
and be re-emitted with one colon. */
|
|
238
|
+
if (key.startsWith("::") && CSS_PSEUDO_ELEMENTS.has(key.slice(2))) {
|
|
239
|
+
normalized[key.slice(2)] = value;
|
|
240
|
+
} else if (key.startsWith(":") && CSS_PSEUDO_NAMES.has(key.slice(1))) {
|
|
212
241
|
normalized[key.slice(1)] = value;
|
|
213
242
|
} else if (key.startsWith("@--")) {
|
|
214
243
|
normalized[key.slice(1)] = value;
|
|
@@ -730,10 +759,11 @@ export function transpileJxMarkdown(source: string) {
|
|
|
730
759
|
* search deep links (specs/parser.md).
|
|
731
760
|
*
|
|
732
761
|
* **NFC first, and that is the whole point of the change** (UAX #15). "Café" typed on macOS is `e`
|
|
733
|
-
*
|
|
734
|
-
*
|
|
735
|
-
*
|
|
736
|
-
*
|
|
762
|
+
*
|
|
763
|
+
* - U+0301; the same word typed on Windows, or pasted from a web page, is U+00E9. They look
|
|
764
|
+
* identical, they compare unequal, and they produced two different anchors — so a link written
|
|
765
|
+
* against one heading silently stopped working when someone re-typed the heading. Normalizing
|
|
766
|
+
* puts both spellings in one form before anything else looks at them.
|
|
737
767
|
*
|
|
738
768
|
* **`toLowerCase`, never `toLocaleLowerCase`.** Locale-sensitive casing would make the same heading
|
|
739
769
|
* produce different anchors in different projects — Turkish maps `I` to `ı`, not `i` — and an
|