toon-parser 2.0.0 → 2.1.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.
Files changed (52) hide show
  1. package/README.md +36 -6
  2. package/dist/cjs.test.cjs +46 -0
  3. package/dist/complex.test.cjs +35 -0
  4. package/dist/coverage.test.cjs +49 -0
  5. package/dist/csv.cjs +150 -0
  6. package/dist/csv.d.ts +20 -0
  7. package/dist/csv.d.ts.map +1 -0
  8. package/dist/csv.edge.test.cjs +20 -0
  9. package/dist/csv.js +142 -0
  10. package/dist/csv.js.map +1 -0
  11. package/dist/csv.test.cjs +27 -0
  12. package/dist/edgecases.test.cjs +23 -0
  13. package/dist/extra.test.cjs +19 -0
  14. package/dist/html.cjs +176 -0
  15. package/dist/html.d.ts +17 -0
  16. package/dist/html.d.ts.map +1 -0
  17. package/dist/html.edge.test.cjs +16 -0
  18. package/dist/html.js +181 -0
  19. package/dist/html.js.map +1 -0
  20. package/dist/html.test.cjs +28 -0
  21. package/dist/index.cjs +9 -1
  22. package/dist/index.d.ts +4 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +4 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/index.test.cjs +59 -0
  27. package/dist/limits.test.cjs +21 -0
  28. package/dist/log.cjs +57 -0
  29. package/dist/log.clf.redos.test.cjs +15 -0
  30. package/dist/log.d.ts +9 -0
  31. package/dist/log.d.ts.map +1 -0
  32. package/dist/log.js +36 -0
  33. package/dist/log.js.map +1 -0
  34. package/dist/log.test.cjs +20 -0
  35. package/dist/property.test.cjs +60 -0
  36. package/dist/smoke.test.cjs +132 -0
  37. package/dist/snapshots.test.cjs +21 -0
  38. package/dist/tabular.test.cjs +21 -0
  39. package/dist/url.cjs +72 -0
  40. package/dist/url.d.ts +3 -0
  41. package/dist/url.d.ts.map +1 -0
  42. package/dist/url.js +57 -0
  43. package/dist/url.js.map +1 -0
  44. package/dist/url.test.cjs +25 -0
  45. package/dist/xml.cjs +22 -0
  46. package/dist/xml.d.ts +1 -0
  47. package/dist/xml.d.ts.map +1 -1
  48. package/dist/xml.edge.test.cjs +16 -0
  49. package/dist/xml.js +22 -1
  50. package/dist/xml.js.map +1 -1
  51. package/dist/xml.test.cjs +86 -0
  52. package/package.json +14 -6
package/README.md CHANGED
@@ -1,7 +1,9 @@
1
1
  # toon-parser
2
2
 
3
3
  [![CI](https://github.com/BranLang/toon-parser/actions/workflows/ci.yml/badge.svg)](https://github.com/BranLang/toon-parser/actions/workflows/ci.yml)
4
+ [![Coverage](https://img.shields.io/badge/coverage-87%25-green)](#coverage)
4
5
  [![npm version](https://img.shields.io/npm/v/toon-parser.svg)](https://www.npmjs.com/package/toon-parser)
6
+ [![npm provenance](https://img.shields.io/badge/npm-provenance-blue)](https://docs.npmjs.com/generating-provenance-statements)
5
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
8
 
7
9
  Safe JSON ⇆ TOON encoder/decoder with strict validation and prototype-pollution guards.
@@ -14,6 +16,9 @@ npm install toon-parser
14
16
 
15
17
  Note: this package supports both ESM and CommonJS consumers (CJS builds are available as `dist/index.cjs`). The package requires Node >= 18 per `engines` in `package.json`.
16
18
 
19
+ ## New in 2.1.0
20
+ - **HTML/CSV/Log/URL Support**: Dedicated parsers for common formats to leverage Toon's structure.
21
+
17
22
  ## New in 2.0.0
18
23
  - **XML Support**: Convert XML strings directly to TOON with `xmlToToon`.
19
24
  - **Date Support**: Automatically converts `Date` objects to ISO strings.
@@ -65,12 +70,37 @@ const toon = xmlToToon('<user id="1">Alice</user>');
65
70
  // "@_id": 1
66
71
  ```
67
72
 
68
- ```
69
- 67:
70
- 68: > [!WARNING]
71
- 69: > **Security Note:** While `fast-xml-parser` v5 is generally secure by default, overriding `xmlOptions` can alter security properties (e.g., enabling entity expansion). Only enable such features if you trust the source XML.
72
- 70:
73
- 71: Options:
73
+ ### `htmlToToon(html, options?) => string`
74
+
75
+ Parses HTML string to Toon. Uses `node-html-parser`.
76
+
77
+ ### `csvToToon(csv, options?) => string`
78
+
79
+ Parses CSV string. Options:
80
+ - `delimiter` (default `,`)
81
+ - `hasHeader` (default `true`)
82
+
83
+ ### `urlToToon(urlOrQs, options?) => string`
84
+ Parses URL query strings to Toon object. Expands dotted/bracket notation (e.g. `user[name]`).
85
+
86
+ ### `logToToon(log, options?) => string`
87
+ Parses logs. Options:
88
+ - `format`: `'auto'` | `'clf'` | `'json'`
89
+
90
+ ### `csvToJson(csv, options?) => unknown[]`
91
+ Lightweight CSV to JSON helper. Throws when row widths mismatch headers or when the delimiter is not a single character.
92
+
93
+ ### `htmlToJson(html) => { children: ... }`
94
+ Parses HTML into a simplified JSON tree. Performs a minimal tag-balance check and trims whitespace-only nodes. Not intended for arbitrary HTML with scripts/styles.
95
+
96
+ ### `xmlToJson(xml, options?) => unknown`
97
+ Validates XML before parsing; returns `{}` for empty input and throws on malformed XML.
98
+
99
+
100
+ > [!WARNING]
101
+ > **Security Note:** While `fast-xml-parser` v5 is generally secure by default, overriding `xmlOptions` can alter security properties (e.g., enabling entity expansion). Only enable such features if you trust the source XML.
102
+
103
+ Options:
74
104
  - `indent` (number, default `2`): spaces per indentation level.
75
105
  - `delimiter` (`,` | `|` | `\t`, default `,`): delimiter for inline arrays and tabular rows.
76
106
  - `sortKeys` (boolean, default `false`): sort object keys alphabetically instead of preserving encounter order.
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (let key of __getOwnPropNames(from))
11
+ if (!__hasOwnProp.call(to, key) && key !== except)
12
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
13
+ }
14
+ return to;
15
+ };
16
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
17
+ // If the importer is in node compatibility mode or this is not an ESM
18
+ // file that has been converted to a CommonJS file using a Babel-
19
+ // compatible transform (i.e. "__esModule" has not been set), then set
20
+ // "default" to the CommonJS "module.exports" for node compatibility.
21
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
22
+ mod
23
+ ));
24
+ var import_vitest = require("vitest");
25
+ var esmModule = __toESM(require("./index"), 1);
26
+ var import_fs = __toESM(require("fs"), 1);
27
+ var import_path = __toESM(require("path"), 1);
28
+ const distPath = import_path.default.join(__dirname, "..", "dist", "index.cjs");
29
+ (0, import_vitest.describe)("CJS built package", () => {
30
+ (0, import_vitest.it)("exposes same API as ESM source (if built)", () => {
31
+ if (!import_fs.default.existsSync(distPath)) {
32
+ (0, import_vitest.expect)(true).toBeTruthy();
33
+ return;
34
+ }
35
+ const cjs = require(distPath);
36
+ (0, import_vitest.expect)(typeof cjs.jsonToToon).toBe("function");
37
+ (0, import_vitest.expect)(typeof cjs.toonToJson).toBe("function");
38
+ const sample = { a: 1, b: "x" };
39
+ const e = esmModule.jsonToToon(sample);
40
+ const d = cjs.jsonToToon(sample);
41
+ (0, import_vitest.expect)(e).toBeDefined();
42
+ (0, import_vitest.expect)(d).toBeDefined();
43
+ (0, import_vitest.expect)(esmModule.toonToJson(e)).toEqual(sample);
44
+ (0, import_vitest.expect)(cjs.toonToJson(d)).toEqual(sample);
45
+ });
46
+ });
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ var import_vitest = require("vitest");
3
+ var import_index = require("./index");
4
+ (0, import_vitest.describe)("Complex Scenarios", () => {
5
+ (0, import_vitest.describe)("CSV Complex Escaping", () => {
6
+ (0, import_vitest.it)("handles newlines and escaped quotes inside quoted fields", () => {
7
+ const csv = `"note","data"
8
+ "line1
9
+ line2","has ""quotes"" inside"`;
10
+ const toon = (0, import_index.csvToToon)(csv);
11
+ (0, import_vitest.expect)(toon).toContain('"line1\\nline2"');
12
+ (0, import_vitest.expect)(toon).toContain('"has \\"quotes\\" inside"');
13
+ });
14
+ });
15
+ (0, import_vitest.describe)("HTML Deep Nesting & Attributes", () => {
16
+ (0, import_vitest.it)("preserves attributes in deeply nested lists", () => {
17
+ const html = `<ul>
18
+ <li class="item" data-id="1">Item 1</li>
19
+ <li class="item" data-id="2">Item 2</li>
20
+ </ul>`;
21
+ const toon = (0, import_index.htmlToToon)(html);
22
+ (0, import_vitest.expect)(toon).toContain("ul:");
23
+ (0, import_vitest.expect)(toon).toContain("li:");
24
+ (0, import_vitest.expect)(toon).toContain('"@_class": item');
25
+ (0, import_vitest.expect)(toon).toContain('"@_data-id": "1"');
26
+ });
27
+ });
28
+ (0, import_vitest.describe)("URL Array Syntax", () => {
29
+ (0, import_vitest.it)("expands bracket notation deeply", () => {
30
+ const url = "sort[order]=asc&sort[fields][]=name&sort[fields][]=date";
31
+ const toon = (0, import_index.urlToToon)(url);
32
+ (0, import_vitest.expect)(toon).toContain("order: asc");
33
+ });
34
+ });
35
+ });
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ var import_vitest = require("vitest");
3
+ var import_index = require("./index");
4
+ (0, import_vitest.describe)("Coverage & Edge Case Gaps", () => {
5
+ (0, import_vitest.describe)("decodeKey Invalid Inputs", () => {
6
+ (0, import_vitest.it)("throws on unquoted key starting with invalid character", () => {
7
+ (0, import_vitest.expect)(() => (0, import_index.toonToJson)('123key: "val"')).toThrow(/Invalid key token/);
8
+ });
9
+ (0, import_vitest.it)("throws on unquoted key with special characters", () => {
10
+ (0, import_vitest.expect)(() => (0, import_index.toonToJson)('key+val: "val"')).toThrow(/Invalid key token/);
11
+ });
12
+ (0, import_vitest.it)("throws on unquoted key containing delimiter (fails regex anyway)", () => {
13
+ (0, import_vitest.expect)(() => (0, import_index.toonToJson)('key,val: "val"')).toThrow(/Invalid key token/);
14
+ });
15
+ });
16
+ (0, import_vitest.describe)("Tabular Fallback", () => {
17
+ (0, import_vitest.it)("falls back to list format if object values are non-primitive", () => {
18
+ const data = [
19
+ { id: 1, info: { meta: "not-primitive" } },
20
+ { id: 2, info: { meta: "x" } }
21
+ ];
22
+ const encoded = (0, import_index.jsonToToon)(data);
23
+ (0, import_vitest.expect)(encoded).not.toContain("]{id,info}");
24
+ (0, import_vitest.expect)(encoded).toContain("-");
25
+ (0, import_vitest.expect)((0, import_index.toonToJson)(encoded)).toEqual(data);
26
+ });
27
+ (0, import_vitest.it)("falls back to list format if objects have different keys", () => {
28
+ const data = [
29
+ { id: 1, a: 1 },
30
+ { id: 2, b: 2 }
31
+ ];
32
+ const encoded = (0, import_index.jsonToToon)(data);
33
+ (0, import_vitest.expect)(encoded).not.toContain("]{id,a}");
34
+ (0, import_vitest.expect)((0, import_index.toonToJson)(encoded)).toEqual(data);
35
+ });
36
+ });
37
+ (0, import_vitest.describe)("Circular References", () => {
38
+ (0, import_vitest.it)("throws error on circular reference", () => {
39
+ const a = { name: "a" };
40
+ a.self = a;
41
+ (0, import_vitest.expect)(() => (0, import_index.jsonToToon)(a)).toThrow(/Maximum depth \d+ exceeded/);
42
+ });
43
+ });
44
+ (0, import_vitest.describe)("Tabular Header Pollution", () => {
45
+ (0, import_vitest.it)("throws if tabular header contains disallowed keys when row is parsed", () => {
46
+ (0, import_vitest.expect)(() => (0, import_index.toonToJson)("[1]{__proto__}:\n 1")).toThrow(/Disallowed key "__proto__"/);
47
+ });
48
+ });
49
+ });
package/dist/csv.cjs ADDED
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var csv_exports = {};
20
+ __export(csv_exports, {
21
+ csvToJson: () => csvToJson,
22
+ csvToToon: () => csvToToon
23
+ });
24
+ module.exports = __toCommonJS(csv_exports);
25
+ var import_index = require("./index.cjs");
26
+ function csvToToon(csv, options = {}) {
27
+ const delimiter = options.delimiter ?? ",";
28
+ if (delimiter.length !== 1) {
29
+ throw new Error("Delimiter must be a single character");
30
+ }
31
+ const hasHeader = options.hasHeader ?? true;
32
+ const rows = parseCsv(csv, delimiter);
33
+ if (rows.length === 0) {
34
+ return (0, import_index.jsonToToon)([], options);
35
+ }
36
+ if (hasHeader) {
37
+ const header = rows[0];
38
+ if (!header) return (0, import_index.jsonToToon)([], options);
39
+ const width = header.length;
40
+ const dataRows = rows.slice(1);
41
+ for (const row of dataRows) {
42
+ if (row.length !== width) {
43
+ throw new Error("Malformed CSV: row length mismatch");
44
+ }
45
+ }
46
+ const data = dataRows.map((row) => {
47
+ const obj = {};
48
+ header.forEach((key, index) => {
49
+ obj[key] = inferType(row[index] ?? "");
50
+ });
51
+ return obj;
52
+ });
53
+ return (0, import_index.jsonToToon)(data, options);
54
+ } else {
55
+ const data = rows.map((row) => row.map((cell) => inferType(cell)));
56
+ return (0, import_index.jsonToToon)(data, options);
57
+ }
58
+ }
59
+ function csvToJson(csv, options = {}) {
60
+ const delimiter = options.delimiter ?? ",";
61
+ if (delimiter.length !== 1) {
62
+ throw new Error("Delimiter must be a single character");
63
+ }
64
+ const hasHeader = options.hasHeader ?? true;
65
+ const rows = parseCsv(csv, delimiter);
66
+ if (rows.length === 0) return [];
67
+ if (hasHeader) {
68
+ const header = rows[0];
69
+ if (!header || header.length === 0) return [];
70
+ const width = header.length;
71
+ const body = rows.slice(1);
72
+ for (const row of body) {
73
+ if (row.length !== width) {
74
+ throw new Error("Malformed CSV: row length mismatch");
75
+ }
76
+ }
77
+ return body.map((row) => {
78
+ const obj = {};
79
+ header.forEach((key, idx) => {
80
+ obj[key] = inferType(row[idx] ?? "");
81
+ });
82
+ return obj;
83
+ });
84
+ }
85
+ return rows.map((row) => row.map((cell) => inferType(cell)));
86
+ }
87
+ function parseCsv(text, delimiter) {
88
+ const rows = [];
89
+ let currentRow = [];
90
+ let currentCell = "";
91
+ let inQuote = false;
92
+ for (let i = 0; i < text.length; i++) {
93
+ const char = text[i];
94
+ const nextChar = text[i + 1];
95
+ if (inQuote) {
96
+ if (char === '"') {
97
+ if (nextChar === '"') {
98
+ currentCell += '"';
99
+ i++;
100
+ } else {
101
+ inQuote = false;
102
+ }
103
+ } else {
104
+ currentCell += char;
105
+ }
106
+ } else {
107
+ if (char === '"') {
108
+ inQuote = true;
109
+ } else if (char === delimiter) {
110
+ currentRow.push(currentCell);
111
+ currentCell = "";
112
+ } else if (char === "\r" || char === "\n") {
113
+ if (char === "\r" && nextChar === "\n") i++;
114
+ currentRow.push(currentCell);
115
+ rows.push(currentRow);
116
+ currentRow = [];
117
+ currentCell = "";
118
+ } else {
119
+ currentCell += char;
120
+ }
121
+ }
122
+ }
123
+ if (currentCell || currentRow.length > 0) {
124
+ currentRow.push(currentCell);
125
+ rows.push(currentRow);
126
+ }
127
+ if (rows.length > 0) {
128
+ const last = rows[rows.length - 1];
129
+ if (last && last.length === 1 && last[0] === "") {
130
+ rows.pop();
131
+ }
132
+ }
133
+ return rows;
134
+ }
135
+ function inferType(val) {
136
+ if (val === "true") return true;
137
+ if (val === "false") return false;
138
+ if (val === "null") return "null";
139
+ if (val.trim() === "") return "";
140
+ const num = Number(val);
141
+ if (!isNaN(num) && !val.includes(",")) {
142
+ return num;
143
+ }
144
+ return val;
145
+ }
146
+ // Annotate the CommonJS export names for ESM import in node:
147
+ 0 && (module.exports = {
148
+ csvToJson,
149
+ csvToToon
150
+ });
package/dist/csv.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ import { JsonToToonOptions } from './index.js';
2
+ export interface CsvToToonOptions extends Omit<JsonToToonOptions, 'delimiter'> {
3
+ /**
4
+ * Character used to separate fields. Defaults to comma.
5
+ */
6
+ delimiter?: string;
7
+ /**
8
+ * If true, the first row is treated as the header row.
9
+ * If false, the output will be a list of arrays instead of a tabular array.
10
+ * Defaults to true.
11
+ */
12
+ hasHeader?: boolean;
13
+ }
14
+ export declare function csvToToon(csv: string, options?: CsvToToonOptions): string;
15
+ export interface CsvToJsonOptions {
16
+ delimiter?: string;
17
+ hasHeader?: boolean;
18
+ }
19
+ export declare function csvToJson(csv: string, options?: CsvToJsonOptions): unknown[];
20
+ //# sourceMappingURL=csv.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"csv.d.ts","sourceRoot":"","sources":["../src/csv.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAE3D,MAAM,WAAW,gBAAiB,SAAQ,IAAI,CAAC,iBAAiB,EAAE,WAAW,CAAC;IAC5E;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,MAAM,CAqC7E;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,OAAO,EAAE,CAiChF"}
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ var import_csv = require("../src/csv");
3
+ var import_vitest = require("vitest");
4
+ (0, import_vitest.test)("malformed CSV throws error", () => {
5
+ const malformed = "a,b\n1,2,3";
6
+ (0, import_vitest.expect)(() => (0, import_csv.csvToJson)(malformed)).toThrow();
7
+ });
8
+ (0, import_vitest.test)("empty CSV returns empty array", () => {
9
+ const empty = "";
10
+ const result = (0, import_csv.csvToJson)(empty);
11
+ (0, import_vitest.expect)(result).toEqual([]);
12
+ });
13
+ (0, import_vitest.test)("multi-character delimiter is rejected", () => {
14
+ const data = "a|b\n1|2";
15
+ (0, import_vitest.expect)(() => (0, import_csv.csvToJson)(data, { delimiter: "||" })).toThrow();
16
+ });
17
+ (0, import_vitest.test)("row length mismatch throws in csvToToon", () => {
18
+ const malformed = "a,b\n1,2,3";
19
+ (0, import_vitest.expect)(() => (0, import_csv.csvToToon)(malformed)).toThrow();
20
+ });
package/dist/csv.js ADDED
@@ -0,0 +1,142 @@
1
+ import { jsonToToon } from './index.js';
2
+ export function csvToToon(csv, options = {}) {
3
+ const delimiter = options.delimiter ?? ',';
4
+ if (delimiter.length !== 1) {
5
+ throw new Error('Delimiter must be a single character');
6
+ }
7
+ const hasHeader = options.hasHeader ?? true;
8
+ const rows = parseCsv(csv, delimiter);
9
+ if (rows.length === 0) {
10
+ return jsonToToon([], options);
11
+ }
12
+ if (hasHeader) {
13
+ const header = rows[0];
14
+ if (!header)
15
+ return jsonToToon([], options);
16
+ const width = header.length;
17
+ const dataRows = rows.slice(1);
18
+ for (const row of dataRows) {
19
+ if (row.length !== width) {
20
+ throw new Error('Malformed CSV: row length mismatch');
21
+ }
22
+ }
23
+ const data = dataRows.map(row => {
24
+ const obj = {};
25
+ header.forEach((key, index) => {
26
+ obj[key] = inferType(row[index] ?? '');
27
+ });
28
+ return obj;
29
+ });
30
+ return jsonToToon(data, options);
31
+ }
32
+ else {
33
+ // No header: array of arrays
34
+ const data = rows.map(row => row.map(cell => inferType(cell)));
35
+ return jsonToToon(data, options);
36
+ }
37
+ }
38
+ export function csvToJson(csv, options = {}) {
39
+ const delimiter = options.delimiter ?? ',';
40
+ if (delimiter.length !== 1) {
41
+ throw new Error('Delimiter must be a single character');
42
+ }
43
+ const hasHeader = options.hasHeader ?? true;
44
+ const rows = parseCsv(csv, delimiter);
45
+ if (rows.length === 0)
46
+ return [];
47
+ if (hasHeader) {
48
+ const header = rows[0];
49
+ if (!header || header.length === 0)
50
+ return [];
51
+ const width = header.length;
52
+ const body = rows.slice(1);
53
+ for (const row of body) {
54
+ if (row.length !== width) {
55
+ throw new Error('Malformed CSV: row length mismatch');
56
+ }
57
+ }
58
+ return body.map(row => {
59
+ const obj = {};
60
+ header.forEach((key, idx) => {
61
+ obj[key] = inferType(row[idx] ?? '');
62
+ });
63
+ return obj;
64
+ });
65
+ }
66
+ // no header: return array of arrays with primitive inference
67
+ return rows.map(row => row.map(cell => inferType(cell)));
68
+ }
69
+ function parseCsv(text, delimiter) {
70
+ const rows = [];
71
+ let currentRow = [];
72
+ let currentCell = '';
73
+ let inQuote = false;
74
+ for (let i = 0; i < text.length; i++) {
75
+ const char = text[i];
76
+ const nextChar = text[i + 1];
77
+ if (inQuote) {
78
+ if (char === '"') {
79
+ if (nextChar === '"') {
80
+ currentCell += '"';
81
+ i++; // skip escaped quote
82
+ }
83
+ else {
84
+ inQuote = false;
85
+ }
86
+ }
87
+ else {
88
+ currentCell += char;
89
+ }
90
+ }
91
+ else {
92
+ if (char === '"') {
93
+ inQuote = true;
94
+ }
95
+ else if (char === delimiter) {
96
+ currentRow.push(currentCell);
97
+ currentCell = '';
98
+ }
99
+ else if (char === '\r' || char === '\n') {
100
+ if (char === '\r' && nextChar === '\n')
101
+ i++;
102
+ currentRow.push(currentCell);
103
+ rows.push(currentRow);
104
+ currentRow = [];
105
+ currentCell = '';
106
+ }
107
+ else {
108
+ currentCell += char;
109
+ }
110
+ }
111
+ }
112
+ // Flush last cell/row
113
+ if (currentCell || currentRow.length > 0) {
114
+ currentRow.push(currentCell);
115
+ rows.push(currentRow);
116
+ }
117
+ // Clean up empty trailing row if common in file save
118
+ if (rows.length > 0) {
119
+ const last = rows[rows.length - 1];
120
+ if (last && last.length === 1 && last[0] === '') {
121
+ rows.pop();
122
+ }
123
+ }
124
+ return rows;
125
+ }
126
+ function inferType(val) {
127
+ if (val === 'true')
128
+ return true;
129
+ if (val === 'false')
130
+ return false;
131
+ if (val === 'null')
132
+ return 'null'; // keep string null? or null value? usually string in CSV
133
+ // Attempt number conversion
134
+ if (val.trim() === '')
135
+ return '';
136
+ const num = Number(val);
137
+ if (!isNaN(num) && !val.includes(',')) { // simple check
138
+ return num;
139
+ }
140
+ return val;
141
+ }
142
+ //# sourceMappingURL=csv.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"csv.js","sourceRoot":"","sources":["../src/csv.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ var import_vitest = require("vitest");
3
+ var import_csv = require("./csv");
4
+ (0, import_vitest.describe)("csvToToon", () => {
5
+ (0, import_vitest.it)("converts simple CSV with header to table", () => {
6
+ const csv = `id,name,role
7
+ 1,Alice,Admin
8
+ 2,Bob,User`;
9
+ const toon = (0, import_csv.csvToToon)(csv);
10
+ (0, import_vitest.expect)(toon).toContain("[2]{id,name,role}:");
11
+ (0, import_vitest.expect)(toon).toContain("1,Alice,Admin");
12
+ (0, import_vitest.expect)(toon).toContain("2,Bob,User");
13
+ });
14
+ (0, import_vitest.it)("handles quoted values", () => {
15
+ const csv = `id,desc
16
+ 1,"Hello, World"`;
17
+ const toon = (0, import_csv.csvToToon)(csv);
18
+ (0, import_vitest.expect)(toon).toContain('"Hello, World"');
19
+ });
20
+ (0, import_vitest.it)("handles headerless csv", () => {
21
+ const csv = `1,2
22
+ 3,4`;
23
+ const toon = (0, import_csv.csvToToon)(csv, { hasHeader: false });
24
+ (0, import_vitest.expect)(toon).toContain("- [2]: 1,2");
25
+ (0, import_vitest.expect)(toon).toContain("- [2]: 3,4");
26
+ });
27
+ });
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ var import_vitest = require("vitest");
3
+ var import_index = require("./index");
4
+ (0, import_vitest.describe)("Edge case tests", () => {
5
+ (0, import_vitest.it)("round-trips arrays with empty objects", () => {
6
+ const data = { arr: [{}, { id: 1 }] };
7
+ const t = (0, import_index.jsonToToon)(data);
8
+ const back = (0, import_index.toonToJson)(t);
9
+ (0, import_vitest.expect)(back).toEqual(data);
10
+ });
11
+ (0, import_vitest.it)("round-trips keys with punctuation (quoted)", () => {
12
+ const data = { "a b": 1, "!ok": 2 };
13
+ const t = (0, import_index.jsonToToon)(data);
14
+ const back = (0, import_index.toonToJson)(t);
15
+ (0, import_vitest.expect)(back).toEqual(data);
16
+ });
17
+ (0, import_vitest.it)("handles strings with delimiter and colon characters by quoting", () => {
18
+ const data = { val: "1,2:3|x" };
19
+ const t = (0, import_index.jsonToToon)(data, { delimiter: "," });
20
+ const back = (0, import_index.toonToJson)(t);
21
+ (0, import_vitest.expect)(back).toEqual(data);
22
+ });
23
+ });
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ var import_vitest = require("vitest");
3
+ var import_index = require("./index");
4
+ (0, import_vitest.describe)("Toon parser strict vs non-strict and diagnostics", () => {
5
+ (0, import_vitest.it)("allows inline length mismatch when strict false", () => {
6
+ const result = (0, import_index.toonToJson)("vals[2]: 1", { strict: false });
7
+ (0, import_vitest.expect)(result).toEqual({ vals: [1] });
8
+ });
9
+ (0, import_vitest.it)("includes line numbers in errors", () => {
10
+ try {
11
+ (0, import_index.toonToJson)("vals[2]: 1", { strict: true });
12
+ throw new Error("Should have thrown");
13
+ } catch (err) {
14
+ (0, import_vitest.expect)(err).toBeInstanceOf(Error);
15
+ (0, import_vitest.expect)(String(err)).toMatch(/Line 1/);
16
+ (0, import_vitest.expect)(String(err)).toMatch(/length mismatch/);
17
+ }
18
+ });
19
+ });