web-csv-toolbox 0.2.0 → 0.3.1
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 +84 -14
- package/lib/index.js +0 -15
- package/lib/index.umd.js +1 -0
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
[](https://github.com/kamiazya/web-csv-toolbox/actions/workflows/node.js.yaml)
|
|
4
3
|
[](https://badge.fury.io/js/web-csv-toolbox)
|
|
5
4
|
[](https://opensource.org/licenses/MIT)
|
|
6
5
|
[](http://makeapullrequest.com)
|
|
@@ -38,9 +37,8 @@ A CSV Toolbox utilizing Web Standard APIs.
|
|
|
38
37
|
- Using only Web Standards APIs.
|
|
39
38
|
- 💪 **Property-based testing.**
|
|
40
39
|
- Using [fast-check](https://fast-check.dev/) and [vitest](https://vitest.dev).
|
|
41
|
-
- ✅ **
|
|
40
|
+
- ✅ **Cross-platform.**
|
|
42
41
|
- Works on browsers, Node.js, and Deno.
|
|
43
|
-
- Only web standard APIs are used, so it should work with these runtimes.
|
|
44
42
|
|
|
45
43
|
## Key Features 📗
|
|
46
44
|
|
|
@@ -58,6 +56,10 @@ A CSV Toolbox utilizing Web Standard APIs.
|
|
|
58
56
|
|
|
59
57
|
## Installation 📥
|
|
60
58
|
|
|
59
|
+
### With Package manager 📦
|
|
60
|
+
|
|
61
|
+
This package can then be installed using a package manager.
|
|
62
|
+
|
|
61
63
|
```sh
|
|
62
64
|
# Install with npm
|
|
63
65
|
$ npm install web-csv-toolbox
|
|
@@ -67,6 +69,50 @@ $ yarn add web-csv-toolbox
|
|
|
67
69
|
$ pnpm add web-csv-toolbox
|
|
68
70
|
```
|
|
69
71
|
|
|
72
|
+
### From CDN (unpkg.com) 🌐
|
|
73
|
+
|
|
74
|
+
#### UMD Style 🔄
|
|
75
|
+
|
|
76
|
+
```html
|
|
77
|
+
<script src="https://unpkg.com/web-csv-toolbox"></script>
|
|
78
|
+
<script>
|
|
79
|
+
const csv = `name,age
|
|
80
|
+
Alice,42
|
|
81
|
+
Bob,69`;
|
|
82
|
+
|
|
83
|
+
(async function () {
|
|
84
|
+
for await (const record of CSV.parse(csv)) {
|
|
85
|
+
console.log(record);
|
|
86
|
+
}
|
|
87
|
+
})();
|
|
88
|
+
</script>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
#### ESModule Style 📦
|
|
93
|
+
|
|
94
|
+
```html
|
|
95
|
+
<script type="module">
|
|
96
|
+
import { parse } from 'https://unpkg.com/web-csv-toolbox/lib/index.js';
|
|
97
|
+
|
|
98
|
+
const csv = `name,age
|
|
99
|
+
Alice,42
|
|
100
|
+
Bob,69`;
|
|
101
|
+
|
|
102
|
+
for await (const record of parse(csv)) {
|
|
103
|
+
console.log(record);
|
|
104
|
+
}
|
|
105
|
+
</script>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
#### Deno 🦕
|
|
109
|
+
|
|
110
|
+
You can install and use the package by specifying the following:
|
|
111
|
+
|
|
112
|
+
```js
|
|
113
|
+
import { parse } from "npm:web-csv-toolbox";
|
|
114
|
+
```
|
|
115
|
+
|
|
70
116
|
## Usage 📘
|
|
71
117
|
|
|
72
118
|
### Parsing CSV files from strings
|
|
@@ -158,6 +204,30 @@ for await (const record of parse(csv, { headers: ['name', 'age'] })) {
|
|
|
158
204
|
// { name: 'Bob', age: '69' }
|
|
159
205
|
```
|
|
160
206
|
|
|
207
|
+
## Supported Runtimes 💻
|
|
208
|
+
|
|
209
|
+
### Works on Node.js [](https://github.com/kamiazya/web-csv-toolbox/actions/workflows/node.js.yaml)
|
|
210
|
+
|
|
211
|
+
| Versions | Status |
|
|
212
|
+
| -------- | ------ |
|
|
213
|
+
| 20.x | ✅ |
|
|
214
|
+
| 18.x | ✅ |
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
### Works on Browser [](https://github.com/kamiazya/web-csv-toolbox/actions/workflows/browsers.yaml)
|
|
218
|
+
|
|
219
|
+
| OS | Chrome | FireFox | Default |
|
|
220
|
+
| ------- | ------ | ------- | ------------- |
|
|
221
|
+
| Windows | ✅ | ✅ | ✅ (Edge) |
|
|
222
|
+
| macos | ✅ | ✅ | ⬜ (Safari *) |
|
|
223
|
+
| Linux | ✅ | ✅ | - |
|
|
224
|
+
|
|
225
|
+
> **\* To Be Tested**: [I couldn't launch Safari in headless mode](https://github.com/vitest-dev/vitest/blob/main/packages/browser/src/node/providers/webdriver.ts#L39-L41) on GitHub Actions, so I couldn't verify it, but it probably works.
|
|
226
|
+
|
|
227
|
+
### Others
|
|
228
|
+
|
|
229
|
+
- Verify that JavaScript is executable on the Deno. [](https://github.com/kamiazya/web-csv-toolbox/actions/workflows/deno.yaml)
|
|
230
|
+
|
|
161
231
|
## APIs 🧑💻
|
|
162
232
|
|
|
163
233
|
### High-level APIs 🚀
|
|
@@ -204,20 +274,20 @@ ideal for developers looking for in-depth control and flexibility.
|
|
|
204
274
|
|
|
205
275
|
### Common Options ⚙️
|
|
206
276
|
|
|
207
|
-
| Option
|
|
208
|
-
|
|
|
209
|
-
| `delimiter`
|
|
210
|
-
| `quotation`
|
|
211
|
-
| `headers`
|
|
277
|
+
| Option | Description | Default | Notes |
|
|
278
|
+
| ----------- | ------------------------------------- | --------- | ------------------------------------------------- |
|
|
279
|
+
| `delimiter` | Character to separate fields | `,` | |
|
|
280
|
+
| `quotation` | Character used for quoting fields | `"` | |
|
|
281
|
+
| `headers` | Custom headers for the parsed records | First row | If not provided, the first row is used as headers |
|
|
212
282
|
|
|
213
283
|
### Advanced Options (Binary-Specific) 🧬
|
|
214
284
|
|
|
215
|
-
| Option | Description | Default
|
|
216
|
-
| --------------- | ------------------------------------------------- |
|
|
217
|
-
| `charset` | Character encoding for binary CSV inputs | `utf-8`
|
|
218
|
-
| `decompression` | Decompression algorithm for compressed CSV inputs |
|
|
219
|
-
| `ignoreBOM` | Whether to ignore Byte Order Mark (BOM) | `false`
|
|
220
|
-
| `fatal` | Throw an error on invalid characters | `false`
|
|
285
|
+
| Option | Description | Default | Notes |
|
|
286
|
+
| --------------- | ------------------------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
287
|
+
| `charset` | Character encoding for binary CSV inputs | `utf-8` | See [Encoding API Compatibility](https://developer.mozilla.org/en-US/docs/Web/API/Encoding_API/Encodings) for the encoding formats that can be specified. |
|
|
288
|
+
| `decompression` | Decompression algorithm for compressed CSV inputs | | See [DecompressionStream Compatibility](https://developer.mozilla.org/en-US/docs/Web/API/DecompressionStream#browser_compatibilit). |
|
|
289
|
+
| `ignoreBOM` | Whether to ignore Byte Order Mark (BOM) | `false` | See [TextDecoderOptions.ignoreBOM](https://developer.mozilla.org/en-US/docs/Web/API/TextDecoderStream/ignoreBOM) for more information about the BOM. |
|
|
290
|
+
| `fatal` | Throw an error on invalid characters | `false` | See [TextDecoderOptions.fatal](https://developer.mozilla.org/en-US/docs/Web/API/TextDecoderStream/fatal) for more information. |
|
|
221
291
|
|
|
222
292
|
## How to Contribute 💪
|
|
223
293
|
|
package/lib/index.js
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
const FieldDelimiter = Symbol.for("web-streams-csv.FieldDelimiter");
|
|
2
2
|
const RecordDelimiter = Symbol.for("web-streams-csv.RecordDelimiter");
|
|
3
3
|
const Field = Symbol.for("web-streams-csv.Field");
|
|
4
|
-
|
|
5
4
|
const CR = "\r";
|
|
6
5
|
const CRLF = "\r\n";
|
|
7
6
|
const LF = "\n";
|
|
8
7
|
const COMMA = ",";
|
|
9
8
|
const DOUBLE_QUATE = '"';
|
|
10
|
-
|
|
11
9
|
function assertCommonOptions(options) {
|
|
12
10
|
if (typeof options.quotation === "string" && options.quotation.length === 0) {
|
|
13
11
|
throw new Error("quotation must not be empty");
|
|
@@ -30,11 +28,9 @@ function assertCommonOptions(options) {
|
|
|
30
28
|
);
|
|
31
29
|
}
|
|
32
30
|
}
|
|
33
|
-
|
|
34
31
|
function escapeRegExp(v) {
|
|
35
32
|
return v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
36
33
|
}
|
|
37
|
-
|
|
38
34
|
class LexerTransformer extends TransformStream {
|
|
39
35
|
#demiliter;
|
|
40
36
|
#demiliterLength;
|
|
@@ -166,7 +162,6 @@ class LexerTransformer extends TransformStream {
|
|
|
166
162
|
return null;
|
|
167
163
|
}
|
|
168
164
|
}
|
|
169
|
-
|
|
170
165
|
class RecordAssemblerTransformar extends TransformStream {
|
|
171
166
|
#fieldIndex = 0;
|
|
172
167
|
#row = [];
|
|
@@ -229,7 +224,6 @@ class RecordAssemblerTransformar extends TransformStream {
|
|
|
229
224
|
}
|
|
230
225
|
}
|
|
231
226
|
}
|
|
232
|
-
|
|
233
227
|
class SingleValueReadableStream extends ReadableStream {
|
|
234
228
|
constructor(value) {
|
|
235
229
|
super({
|
|
@@ -240,7 +234,6 @@ class SingleValueReadableStream extends ReadableStream {
|
|
|
240
234
|
});
|
|
241
235
|
}
|
|
242
236
|
}
|
|
243
|
-
|
|
244
237
|
async function toArray(...args) {
|
|
245
238
|
const rows = [];
|
|
246
239
|
for await (const row of this(...args)) {
|
|
@@ -248,7 +241,6 @@ async function toArray(...args) {
|
|
|
248
241
|
}
|
|
249
242
|
return rows;
|
|
250
243
|
}
|
|
251
|
-
|
|
252
244
|
async function* parseStringStream(stream, options) {
|
|
253
245
|
let controller;
|
|
254
246
|
const readable = new ReadableStream({
|
|
@@ -277,14 +269,12 @@ async function* parseStringStream(stream, options) {
|
|
|
277
269
|
((parseStringStream) => {
|
|
278
270
|
parseStringStream.toArray = toArray;
|
|
279
271
|
})(parseStringStream || (parseStringStream = {}));
|
|
280
|
-
|
|
281
272
|
async function* parseString(csv, options) {
|
|
282
273
|
yield* parseStringStream(new SingleValueReadableStream(csv), options);
|
|
283
274
|
}
|
|
284
275
|
((parseString) => {
|
|
285
276
|
parseString.toArray = toArray;
|
|
286
277
|
})(parseString || (parseString = {}));
|
|
287
|
-
|
|
288
278
|
async function* parseBinaryStream(stream, options) {
|
|
289
279
|
const { charset, fatal, ignoreBOM, decomposition } = options ?? {};
|
|
290
280
|
yield* parseStringStream(
|
|
@@ -298,7 +288,6 @@ async function* parseBinaryStream(stream, options) {
|
|
|
298
288
|
((parseBinaryStream) => {
|
|
299
289
|
parseBinaryStream.toArray = toArray;
|
|
300
290
|
})(parseBinaryStream || (parseBinaryStream = {}));
|
|
301
|
-
|
|
302
291
|
function parseMime(contentType) {
|
|
303
292
|
const [type, ...parameters] = contentType.split(";");
|
|
304
293
|
const result = {
|
|
@@ -311,7 +300,6 @@ function parseMime(contentType) {
|
|
|
311
300
|
}
|
|
312
301
|
return result;
|
|
313
302
|
}
|
|
314
|
-
|
|
315
303
|
function parseResponse(response, options) {
|
|
316
304
|
const { headers } = response;
|
|
317
305
|
const contentType = headers.get("content-type") ?? "text/csv";
|
|
@@ -333,7 +321,6 @@ function parseResponse(response, options) {
|
|
|
333
321
|
((parseResponse) => {
|
|
334
322
|
parseResponse.toArray = toArray;
|
|
335
323
|
})(parseResponse || (parseResponse = {}));
|
|
336
|
-
|
|
337
324
|
async function* parseStream(stream, options) {
|
|
338
325
|
const [branch1, branch2] = stream.tee();
|
|
339
326
|
const reader1 = branch1.getReader();
|
|
@@ -348,7 +335,6 @@ async function* parseStream(stream, options) {
|
|
|
348
335
|
((parseStream) => {
|
|
349
336
|
parseStream.toArray = toArray;
|
|
350
337
|
})(parseStream || (parseStream = {}));
|
|
351
|
-
|
|
352
338
|
async function* parse(csv, options) {
|
|
353
339
|
if (typeof csv === "string") {
|
|
354
340
|
yield* parseString(csv, options);
|
|
@@ -361,7 +347,6 @@ async function* parse(csv, options) {
|
|
|
361
347
|
((parse) => {
|
|
362
348
|
parse.toArray = toArray;
|
|
363
349
|
})(parse || (parse = {}));
|
|
364
|
-
|
|
365
350
|
export {
|
|
366
351
|
Field,
|
|
367
352
|
FieldDelimiter,
|
package/lib/index.umd.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).CSV={})}(this,(function(e){"use strict";const t=Symbol.for("web-streams-csv.FieldDelimiter"),i=Symbol.for("web-streams-csv.RecordDelimiter"),r=Symbol.for("web-streams-csv.Field"),n="\n";function s(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}class o extends TransformStream{#e;#t;#i;#r;#n;#s="";get demiliter(){return this.#e}get quotation(){return this.#i}constructor({demiliter:e=",",quotation:t='"'}={}){!function(e){if("string"==typeof e.quotation&&0===e.quotation.length)throw new Error("quotation must not be empty");if("string"==typeof e.demiliter&&0===e.demiliter.length)throw new Error("demiliter must not be empty");if(e.quotation.includes(n)||e.quotation.includes("\r"))throw new Error("quotation must not include CR or LF");if(e.demiliter.includes(n)||e.demiliter.includes("\r"))throw new Error("demiliter must not include CR or LF");if(e.demiliter.includes(e.quotation)||e.quotation.includes(e.demiliter))throw new Error("demiliter and quotation must not include each other as a substring")}({demiliter:e,quotation:t}),super({transform:(e,t)=>{if(0!==e.length){this.#s+=e;for(const e of this.#o({flush:!1}))t.enqueue(e)}},flush:e=>{for(const t of this.#o({flush:!0}))e.enqueue(t)}}),this.#e=e,this.#t=e.length,this.#i=t,this.#r=t.length;const i=s(e),r=s(t);this.#n=new RegExp(`^(?:(?!${r})(?!${i})(?![\\r\\n]))([\\S\\s\\uFEFF\\xA0]+?)(?=${r}|${i}|\\r|\\n|$)`)}*#o({flush:e}){let n=null;for(let s;s=this.#a({flush:e});)switch(s.type){case r:n?n.value+=s.value:n=s;break;case t:case i:n&&(yield n,n=null),yield s}n&&(yield n)}#a({flush:e=!1}={}){if(0===this.#s.length)return null;if(this.#s.startsWith("\r\n"))return this.#s=this.#s.slice(2),{type:i,value:"\r\n"};if(this.#s.startsWith(n))return this.#s=this.#s.slice(1),{type:i,value:n};if(this.#s.startsWith(this.#e))return this.#s=this.#s.slice(this.#t),{type:t,value:this.#e};if(this.#s.startsWith(this.#i))return!1===e&&this.#s.endsWith(this.#i)?null:this.extractQuotedString();const s=this.#n.exec(this.#s);return s?!1===e&&s[0].length===this.#s.length?null:(this.#s=this.#s.slice(s[0].length),{type:r,value:s[0]}):null}extractQuotedString(){let e=this.#r,t="";for(;e<this.#s.length;)if(this.#s.slice(e,e+this.#r)!==this.quotation||this.#s.slice(e+this.#r,e+2*this.#r)!==this.quotation){if(this.#s.slice(e,e+this.#r)===this.quotation)return this.#s=this.#s.slice(e+this.#r),{type:r,value:t};t+=this.#s[e],e++}else t+=this.quotation,e+=2*this.#r;return null}}class a extends TransformStream{#u=0;#h=[];#l;#f=!1;constructor(e={}){super({transform:(e,n)=>{switch(e.type){case r:this.#f=!0,this.#h[this.#u]=e.value;break;case t:this.#u++;break;case i:if(void 0===this.#l)this.#d(this.#h);else if(this.#f){const e=Object.fromEntries(this.#l.filter((e=>e)).map(((e,t)=>[e,this.#h.at(t)])));n.enqueue(e)}this.#u=0,this.#h=new Array(this.#l?.length),this.#f=!1}},flush:e=>{if(0!==this.#u&&void 0!==this.#l&&this.#f){const t=Object.fromEntries(this.#l.filter((e=>e)).map(((e,t)=>[e,this.#h.at(t)])));e.enqueue(t)}}}),void 0!==e.header&&Array.isArray(e.header)&&this.#d(e.header)}#d(e){if(this.#l=e,0===this.#l.length)throw new Error("The header must not be empty.");if(new Set(this.#l).size!==this.#l.length)throw new Error("The header must not contain duplicate fields.")}}class u extends ReadableStream{constructor(e){super({start(t){t.enqueue(e),t.close()}})}}async function h(...e){const t=[];for await(const i of this(...e))t.push(i);return t}async function*l(e,t){let i;const r=new ReadableStream({start:e=>i=e});await e.pipeThrough(new o(t)).pipeThrough(new a(t)).pipeTo(new WritableStream({write:e=>i.enqueue(e),close:()=>i.close()}));const n=r.getReader();try{for(;;){const{value:e,done:t}=await n.read();if(t)break;yield e}}finally{n.releaseLock()}}async function*f(e,t){yield*l(new u(e),t)}async function*d(e,t){const{charset:i,fatal:r,ignoreBOM:n,decomposition:s}=t??{};yield*l([...s?[new DecompressionStream(s)]:[],new TextDecoderStream(i,{fatal:r,ignoreBOM:n})].reduce(((e,t)=>e.pipeThrough(t)),e),t)}function c(e,t){const{headers:i}=e,r=i.get("content-type")??"text/csv",n=function(e){const[t,...i]=e.split(";"),r={type:t.trim(),parameters:{}};for(const e of i){const[t,i]=e.split("=");r.parameters[t.trim()]=i.trim()}return r}(r);if("text/csv"!==n.type)throw new Error(`Invalid mime type: ${r}`);const s=i.get("content-encoding")??void 0,o=n.parameters.charset??"utf-8";if(null===e.body)throw new Error("Response body is null");return d(e.body,{decomposition:s,charset:o,...t})}async function*m(e,t){const[i,r]=e.tee(),n=i.getReader(),{value:s}=await n.read();n.releaseLock(),"string"==typeof s?yield*l(r,t):s instanceof Uint8Array&&(yield*d(r,t))}async function*y(e,t){"string"==typeof e?yield*f(e,t):e instanceof ReadableStream?yield*m(e,t):e instanceof Response&&(yield*c(e,t))}!function(e){e.toArray=h}(l||(l={})),function(e){e.toArray=h}(f||(f={})),function(e){e.toArray=h}(d||(d={})),function(e){e.toArray=h}(c||(c={})),function(e){e.toArray=h}(m||(m={})),function(e){e.toArray=h}(y||(y={})),e.Field=r,e.FieldDelimiter=t,e.LexerTransformer=o,e.RecordAssemblerTransformar=a,e.RecordDelimiter=i,e.parse=y,e.parseBinaryStream=d,e.parseResponse=c,e.parseStream=m,e.parseString=f,e.parseStringStream=l}));
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "web-csv-toolbox",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "A CSV Toolbox utilizing Web Standard APIs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"module": "lib/index.js",
|
|
8
8
|
"types": "lib/index.d.ts",
|
|
9
|
+
"unpkg": "lib/index.umd.js",
|
|
9
10
|
"engines": {
|
|
10
11
|
"node": ">=18.0.0"
|
|
11
12
|
},
|
|
@@ -23,6 +24,7 @@
|
|
|
23
24
|
"scripts": {
|
|
24
25
|
"doc": "typedoc",
|
|
25
26
|
"test": "vitest",
|
|
27
|
+
"test:browser": "vitest --browser",
|
|
26
28
|
"format": "biome format . --write",
|
|
27
29
|
"lint": "biome lint .",
|
|
28
30
|
"check": "biome check src --apply",
|
|
@@ -51,9 +53,10 @@
|
|
|
51
53
|
"@changesets/changelog-github": "^0.5.0",
|
|
52
54
|
"@changesets/cli": "^2.27.1",
|
|
53
55
|
"@fast-check/vitest": "^0.0.9",
|
|
56
|
+
"@rollup/plugin-terser": "^0.4.4",
|
|
57
|
+
"@vitest/browser": "^1.1.0",
|
|
54
58
|
"changesets-github-release": "^0.1.0",
|
|
55
59
|
"husky": "^8.0.0",
|
|
56
|
-
"jsdom": "^23.0.1",
|
|
57
60
|
"lint-staged": "^15.2.0",
|
|
58
61
|
"rollup": "^4.9.1",
|
|
59
62
|
"rollup-plugin-delete": "^2.0.0",
|
|
@@ -62,6 +65,7 @@
|
|
|
62
65
|
"typedoc": "^0.25.4",
|
|
63
66
|
"typedoc-plugin-mdn-links": "^3.1.9",
|
|
64
67
|
"typescript": "^5.3.2",
|
|
65
|
-
"vitest": "^1.1.0"
|
|
68
|
+
"vitest": "^1.1.0",
|
|
69
|
+
"webdriverio": "^8.27.0"
|
|
66
70
|
}
|
|
67
71
|
}
|