@jxsuite/parser 1.5.6 → 1.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/README.md +8 -8
- package/dist/csv.js +100 -1
- package/dist/markdown.js +2447 -5993
- package/dist/serialize.js +8233 -10597
- package/package.json +6 -6
- package/src/Csv.class.json +31 -0
- package/src/csv.ts +176 -1
- package/src/serialize.ts +3 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `@jxsuite/parser`
|
|
2
2
|
|
|
3
|
-
> Content & Markdown extension for Jx
|
|
3
|
+
> Content & Markdown extension for Jx: file-based content collections with Markdown and CSV formats.
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
@@ -23,7 +23,7 @@ Built on the [`unified`](https://unifiedjs.com/) / remark ecosystem.
|
|
|
23
23
|
bun add @jxsuite/parser
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
Registration is manifest-based
|
|
26
|
+
Registration is manifest-based. Add the package to `project.json`:
|
|
27
27
|
|
|
28
28
|
```json
|
|
29
29
|
{
|
|
@@ -31,7 +31,7 @@ Registration is manifest-based — add the package to `project.json`:
|
|
|
31
31
|
}
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
That one entry registers the `.md`/`.csv` formats, the `content` project section, and the classes: `$prototype: "ContentEntry"` (etc.) resolves by name through the project's extension registry
|
|
34
|
+
That one entry registers the `.md`/`.csv` formats, the `content` project section, and the classes: `$prototype: "ContentEntry"` (etc.) resolves by name through the project's extension registry, with no `$src` or imports wiring. There are no implicit defaults: a project without the extension supports only `.json` documents.
|
|
35
35
|
|
|
36
36
|
## Content collections
|
|
37
37
|
|
|
@@ -79,10 +79,10 @@ Query them from any page or component:
|
|
|
79
79
|
|
|
80
80
|
One class carries every format capability:
|
|
81
81
|
|
|
82
|
-
- **static `parse
|
|
83
|
-
- **static `serialize
|
|
84
|
-
- **static `discover` / `load`**
|
|
85
|
-
- **instance `resolve
|
|
82
|
+
- **static `parse`**: Jx Markdown source → Jx document (browser-safe; Studio opens `.md` files with it)
|
|
83
|
+
- **static `serialize`**: Jx document → markdown, `roundtrip` (lossless, default) or `export` (lossy clean markdown)
|
|
84
|
+
- **static `discover` / `load`** give compile-time content access: frontmatter → `data`, source preserved as `body`, parsed `$children`, and `_meta` (excerpt, toc, reading time, word count)
|
|
85
|
+
- **instance `resolve`**: runtime on-demand access for a `$prototype: "Markdown"` state entry
|
|
86
86
|
|
|
87
87
|
```json
|
|
88
88
|
{
|
|
@@ -107,7 +107,7 @@ One class carries every format capability:
|
|
|
107
107
|
|
|
108
108
|
### Directives
|
|
109
109
|
|
|
110
|
-
`:::name{attrs}` container syntax converts directly to Jx element nodes
|
|
110
|
+
`:::name{attrs}` container syntax converts directly to Jx element nodes. `:::my-card{title="Hello"}` becomes a `my-card` element with its attributes routed to the right Jx locations (props, `$`-annotations, styles). Capitalized directives (e.g. `:::Array`) become tagName-less `$prototype` nodes. A content type's `$elements` list declares which components its entries may use.
|
|
111
111
|
|
|
112
112
|
## `Csv`
|
|
113
113
|
|
package/dist/csv.js
CHANGED
|
@@ -116,6 +116,101 @@ function parseCSV(csv) {
|
|
|
116
116
|
}
|
|
117
117
|
return rows;
|
|
118
118
|
}
|
|
119
|
+
function scanRows(csv) {
|
|
120
|
+
const rows = [];
|
|
121
|
+
let start = 0;
|
|
122
|
+
let inQuotes = false;
|
|
123
|
+
for (let i = 0;i < csv.length; i++) {
|
|
124
|
+
const ch = csv[i];
|
|
125
|
+
if (ch === '"') {
|
|
126
|
+
if (inQuotes && csv[i + 1] === '"') {
|
|
127
|
+
i += 1;
|
|
128
|
+
} else {
|
|
129
|
+
inQuotes = !inQuotes;
|
|
130
|
+
}
|
|
131
|
+
} else if ((ch === `
|
|
132
|
+
` || ch === "\r" && csv[i + 1] === `
|
|
133
|
+
`) && !inQuotes) {
|
|
134
|
+
rows.push({ end: i, start });
|
|
135
|
+
if (ch === "\r") {
|
|
136
|
+
i += 1;
|
|
137
|
+
}
|
|
138
|
+
start = i + 1;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (csv.slice(start).trim()) {
|
|
142
|
+
rows.push({ end: csv.length, start });
|
|
143
|
+
}
|
|
144
|
+
return rows;
|
|
145
|
+
}
|
|
146
|
+
function scanRowFields(csv, row, span) {
|
|
147
|
+
const line = csv.slice(span.start, span.end);
|
|
148
|
+
const fields = [];
|
|
149
|
+
let fieldStart = 0;
|
|
150
|
+
let value = "";
|
|
151
|
+
let inQuotes = false;
|
|
152
|
+
for (let i = 0;i < line.length; i++) {
|
|
153
|
+
const ch = line[i];
|
|
154
|
+
if (ch === '"') {
|
|
155
|
+
if (inQuotes && line[i + 1] === '"') {
|
|
156
|
+
value += '"';
|
|
157
|
+
i += 1;
|
|
158
|
+
} else {
|
|
159
|
+
inQuotes = !inQuotes;
|
|
160
|
+
}
|
|
161
|
+
} else if (ch === "," && !inQuotes) {
|
|
162
|
+
fields.push({
|
|
163
|
+
end: span.start + i,
|
|
164
|
+
row,
|
|
165
|
+
start: span.start + fieldStart,
|
|
166
|
+
value: value.trim()
|
|
167
|
+
});
|
|
168
|
+
value = "";
|
|
169
|
+
fieldStart = i + 1;
|
|
170
|
+
} else {
|
|
171
|
+
value += ch;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
fields.push({ end: span.end, row, start: span.start + fieldStart, value: value.trim() });
|
|
175
|
+
return fields;
|
|
176
|
+
}
|
|
177
|
+
function scanFields(csv) {
|
|
178
|
+
return scanRows(csv).flatMap((span, row) => scanRowFields(csv, row, span));
|
|
179
|
+
}
|
|
180
|
+
function encodeField(raw, value) {
|
|
181
|
+
const lead = raw.slice(0, raw.length - raw.trimStart().length);
|
|
182
|
+
const trail = raw.slice(raw.trimEnd().length);
|
|
183
|
+
const core = raw.trim();
|
|
184
|
+
const wasQuoted = core.length >= 2 && core.startsWith('"') && core.endsWith('"');
|
|
185
|
+
const mustQuote = /["\r\n,]/.test(value) || value.trim() !== value;
|
|
186
|
+
return lead + (wasQuoted || mustQuote ? `"${value.replaceAll('"', '""')}"` : value) + trail;
|
|
187
|
+
}
|
|
188
|
+
function rewriteCSV(source, edits) {
|
|
189
|
+
const replacements = new Map;
|
|
190
|
+
for (const edit of edits) {
|
|
191
|
+
if (edit.from !== edit.to) {
|
|
192
|
+
replacements.set(edit.from, edit.to);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (replacements.size === 0) {
|
|
196
|
+
return source;
|
|
197
|
+
}
|
|
198
|
+
let out = "";
|
|
199
|
+
let cursor = 0;
|
|
200
|
+
for (const field of scanFields(source)) {
|
|
201
|
+
if (field.row === 0) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const to = replacements.get(field.value);
|
|
205
|
+
if (to === undefined) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
out += source.slice(cursor, field.start);
|
|
209
|
+
out += encodeField(source.slice(field.start, field.end), to);
|
|
210
|
+
cursor = field.end;
|
|
211
|
+
}
|
|
212
|
+
return out + source.slice(cursor);
|
|
213
|
+
}
|
|
119
214
|
function coerceCSVRows(rows, schema, idField) {
|
|
120
215
|
return rows.map((row, i) => {
|
|
121
216
|
const data = { ...row };
|
|
@@ -154,6 +249,9 @@ class Csv {
|
|
|
154
249
|
static parse(source, options = {}) {
|
|
155
250
|
return coerceCSVRows(parseCSV(source), options.schema, options.idField);
|
|
156
251
|
}
|
|
252
|
+
static rewrite(source, edits = []) {
|
|
253
|
+
return rewriteCSV(source, edits);
|
|
254
|
+
}
|
|
157
255
|
static async discover(source, options = {}) {
|
|
158
256
|
const { existsSync, readdirSync } = await import("node:fs");
|
|
159
257
|
const { resolve, extname } = await import("node:path");
|
|
@@ -191,5 +289,6 @@ class Csv {
|
|
|
191
289
|
export {
|
|
192
290
|
Csv,
|
|
193
291
|
coerceCSVRows,
|
|
194
|
-
parseCSV
|
|
292
|
+
parseCSV,
|
|
293
|
+
rewriteCSV
|
|
195
294
|
};
|