@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # `@jxsuite/parser`
2
2
 
3
- > Content & Markdown extension for Jx — file-based content collections with Markdown and CSV formats.
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 — add the package to `project.json`:
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 — no `$src` or imports wiring. There are no implicit defaults: a project without the extension supports only `.json` documents.
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`** — 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`** — 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
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 — `:::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.
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
  };