@rip-lang/csv 1.0.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.
Files changed (3) hide show
  1. package/README.md +240 -0
  2. package/csv.rip +437 -0
  3. package/package.json +40 -0
package/README.md ADDED
@@ -0,0 +1,240 @@
1
+ <img src="https://raw.githubusercontent.com/shreeve/rip-lang/main/docs/rip.svg" style="width:50px" /> <br>
2
+
3
+ # Rip CSV - @rip-lang/csv
4
+
5
+ > **Fast, flexible CSV parser and writer — indexOf ratchet engine, auto-detection, zero dependencies**
6
+
7
+ A high-performance CSV library for Rip that uses the JavaScript engine's
8
+ SIMD-accelerated `indexOf` to skip over content in bulk. Auto-detects
9
+ delimiters, quoting, escaping, BOM, and line endings. Supports excel mode,
10
+ relax mode, headers, comments, streaming via row callback, and reusable
11
+ writer instances. ~300 lines of Rip, zero dependencies.
12
+
13
+ ## Quick Start
14
+
15
+ ```bash
16
+ bun add @rip-lang/csv
17
+ ```
18
+
19
+ ```coffee
20
+ import { CSV } from '@rip-lang/csv'
21
+
22
+ # Parse a string
23
+ rows = CSV.read "name,age\nAlice,30\nBob,25\n"
24
+ # [['name','age'], ['Alice','30'], ['Bob','25']]
25
+
26
+ # Parse with headers (returns objects)
27
+ users = CSV.read "name,age\nAlice,30\nBob,25\n", headers: true
28
+ # [{name: 'Alice', age: '30'}, {name: 'Bob', age: '25'}]
29
+
30
+ # Parse a file
31
+ data = CSV.load! 'data.csv'
32
+ data = CSV.load! 'data.csv', headers: true
33
+
34
+ # Write CSV
35
+ str = CSV.write [['a','b'], ['1','2']]
36
+ # "a,b\n1,2\n"
37
+
38
+ # Write to file
39
+ CSV.save! 'out.csv', rows
40
+ ```
41
+
42
+ ## How It Works
43
+
44
+ The parser uses an **indexOf ratchet** — a technique where the JavaScript
45
+ engine's native `indexOf` (backed by SIMD instructions in V8 and JSC) does
46
+ the heavy lifting. Instead of inspecting every character, the parser calls
47
+ `indexOf` to jump directly to the next delimiter, newline, or quote. Each
48
+ call can skip hundreds of bytes in a single native operation.
49
+
50
+ ```
51
+ Source string: "Alice,30,New York\nBob,25,Chicago\n..."
52
+ ↑ ↑ ↑ ↑
53
+ │ │ │ └── indexOf('\n') jumps here
54
+ │ │ └── indexOf(',') jumps here
55
+ │ └── indexOf(',') jumps here
56
+ └── start
57
+
58
+ Each indexOf call skips bulk content via SIMD — no per-byte scanning in JS.
59
+ ```
60
+
61
+ The parser has two code paths, selected at startup by probing the first ~8KB:
62
+
63
+ - **Fast path** — no quotes detected: pure indexOf for separators and newlines
64
+ - **Full path** — quotes present: indexOf ratchet with quote/escape handling
65
+
66
+ ## Reading
67
+
68
+ ### Basic Parsing
69
+
70
+ ```coffee
71
+ # Auto-detects delimiter, quoting, line endings
72
+ rows = CSV.read str
73
+
74
+ # Tab-separated, pipe-separated — auto-detected
75
+ rows = CSV.read "a\tb\tc\n1\t2\t3\n"
76
+ rows = CSV.read "a|b|c\n1|2|3\n"
77
+
78
+ # Explicit separator
79
+ rows = CSV.read str, sep: ';'
80
+ ```
81
+
82
+ ### Headers Mode
83
+
84
+ ```coffee
85
+ # First row becomes object keys
86
+ users = CSV.read str, headers: true
87
+ # [{name: 'Alice', age: '30'}, ...]
88
+
89
+ console.log users[0].name # "Alice"
90
+ ```
91
+
92
+ ### Row-by-Row Processing
93
+
94
+ ```coffee
95
+ # Process rows one at a time without building an array
96
+ count = CSV.read str, each: (row, index) ->
97
+ console.log "Row #{index}: #{row}"
98
+
99
+ # Early halt by returning false
100
+ CSV.read str, each: (row) ->
101
+ if row[0] is 'STOP'
102
+ return false
103
+ process(row)
104
+ ```
105
+
106
+ ### File I/O
107
+
108
+ ```coffee
109
+ # Read a file (async)
110
+ rows = CSV.load! 'data.csv'
111
+ rows = CSV.load! 'data.csv', headers: true, strip: true
112
+
113
+ # Row-by-row file processing
114
+ CSV.load! 'huge.csv', each: (row) -> db.insert!(row)
115
+ ```
116
+
117
+ ### Excel Mode
118
+
119
+ ```coffee
120
+ # Handles ="01" literals (preserves leading zeros)
121
+ rows = CSV.read '="01",hello\n', excel: true
122
+ # [['01', 'hello']]
123
+ ```
124
+
125
+ ### Relax Mode
126
+
127
+ ```coffee
128
+ # Recovers from stray/unmatched quotes instead of throwing
129
+ rows = CSV.read str, relax: true
130
+ ```
131
+
132
+ ## Writing
133
+
134
+ ### Basic Writing
135
+
136
+ ```coffee
137
+ str = CSV.write [['name','age'], ['Alice','30']]
138
+ # "name,age\nAlice,30\n"
139
+
140
+ # Write to file (async)
141
+ CSV.save! 'out.csv', rows
142
+ ```
143
+
144
+ ### Format a Single Row
145
+
146
+ ```coffee
147
+ line = CSV.formatRow ['Alice', 'New York, NY', '30']
148
+ # 'Alice,"New York, NY",30'
149
+ ```
150
+
151
+ ### Reusable Writer
152
+
153
+ ```coffee
154
+ w = CSV.writer(sep: '\t', excel: true)
155
+
156
+ for record in records
157
+ line = w.row(record)
158
+ stream.write "#{line}\n"
159
+
160
+ # Or format all at once
161
+ output = w.rows(records)
162
+ ```
163
+
164
+ ### Writer Modes
165
+
166
+ ```coffee
167
+ # Compact (default): quote only when necessary
168
+ CSV.write rows, mode: 'compact'
169
+
170
+ # Full: quote every field
171
+ CSV.write rows, mode: 'full'
172
+
173
+ # Excel: emit ="0123" for leading-zero numbers
174
+ CSV.write rows, excel: true
175
+
176
+ # Drop trailing empty columns
177
+ CSV.write rows, drop: true
178
+ ```
179
+
180
+ ## Options Reference
181
+
182
+ ### Reader Options
183
+
184
+ | Option | Type | Default | Description |
185
+ |--------|------|---------|-------------|
186
+ | `sep` | string | auto | Field delimiter (`,` `\t` `\|` `;` or any string) |
187
+ | `quote` | string | `"` | Quote/enclosure character |
188
+ | `escape` | string | same as `quote` | Escape character (`"` for doubled, `\` for backslash) |
189
+ | `headers` | boolean | `false` | First row as keys — return objects |
190
+ | `excel` | boolean | `false` | Handle `="01"` literals |
191
+ | `relax` | boolean | `false` | Recover from stray quotes |
192
+ | `strip` | boolean | `false` | Trim whitespace from fields |
193
+ | `comments` | string | `null` | Skip lines starting with this character |
194
+ | `skipBlanks` | boolean | `true` | Skip blank lines |
195
+ | `row` | string | auto | Line ending override (`\n`, `\r\n`, `\r`) |
196
+ | `each` | function | `null` | `(row, index) ->` callback per row |
197
+
198
+ ### Writer Options
199
+
200
+ | Option | Type | Default | Description |
201
+ |--------|------|---------|-------------|
202
+ | `sep` | string | `','` | Field delimiter |
203
+ | `quote` | string | `'"'` | Quote character |
204
+ | `escape` | string | same as `quote` | Escape character |
205
+ | `mode` | string | `'compact'` | `'compact'` or `'full'` |
206
+ | `excel` | boolean | `false` | Emit `="0123"` for leading zeros |
207
+ | `drop` | boolean | `false` | Drop trailing empty columns |
208
+ | `rowsep` | string | `'\n'` | Row separator |
209
+
210
+ > **Note:** The writer defaults to doubled-quote escaping (`""`). Pass
211
+ > `escape: '\\'` for backslash style.
212
+
213
+ ## Auto-Detection
214
+
215
+ When you call `CSV.read(str)` with no options, the probe function scans the
216
+ first ~8KB to automatically detect:
217
+
218
+ - **BOM** — strips UTF-8 BOM if present
219
+ - **`sep=` header** — Excel convention for declaring delimiter
220
+ - **Delimiter** — tries `,` `\t` `|` `;`, picks the most frequent
221
+ - **Quote character** — detects if `"` appears in the sample
222
+ - **Escape style** — `\"` (backslash) vs `""` (doubled quote)
223
+ - **Line endings** — `\r\n`, `\n`, or `\r`
224
+
225
+ User options override any probed value.
226
+
227
+ ## API Summary
228
+
229
+ ```coffee
230
+ CSV.read(str, opts) # parse string -> rows or objects
231
+ CSV.load!(path, opts) # parse file (async)
232
+ CSV.write(rows, opts) # format rows -> CSV string
233
+ CSV.save!(path, rows, opts) # write to file (async)
234
+ CSV.writer(opts) # create reusable Writer instance
235
+ CSV.formatRow(row, opts) # format single row -> string
236
+ ```
237
+
238
+ ## License
239
+
240
+ MIT
package/csv.rip ADDED
@@ -0,0 +1,437 @@
1
+ # ==============================================================================
2
+ # csv — Fast, flexible CSV parser and writer for Rip
3
+ #
4
+ # Author: Steve Shreeve (steve.shreeve@gmail.com)
5
+ # Date: February 6, 2026
6
+ #
7
+ # Engine: indexOf ratchet — SIMD-accelerated scanning via the JS engine's
8
+ # native indexOf, skipping bulk content in a single call. No regex
9
+ # in the hot loop. Auto-detects delimiter, quoting, escaping, BOM,
10
+ # and line endings. Supports excel mode, relax mode, headers, comments,
11
+ # streaming via row callback, and reusable writer instances.
12
+ # ==============================================================================
13
+
14
+ # ==[ Constants ]==
15
+
16
+ DELIMITERS =! [',', '\t', '|', ';']
17
+ CRLF =! '\r\n'
18
+ CR =! 13 # \r
19
+ LF =! 10 # \n
20
+ EQ =! 61 # =
21
+
22
+ # ==============================================================================
23
+ # Probe — auto-detect CSV dialect from the first few KB
24
+ # ==============================================================================
25
+
26
+ def probe(str, opts = {})
27
+
28
+ # strip BOM
29
+ if str.charCodeAt(0) is 0xFEFF
30
+ str = str.slice(1)
31
+
32
+ # detect sep= header (Excel convention)
33
+ if str[0..3] is "sep="
34
+ end = str.indexOf('\n')
35
+ end = str.indexOf('\r') if end is -1
36
+ stop = (end > 0 and str.charCodeAt(end - 1) is CR) ? end - 1 : end
37
+ sep = str.slice(4, stop)
38
+ str = str.slice(end + 1) if end >= 0
39
+
40
+ # sample first ~8KB for sniffing
41
+ sample = str.slice(0, 8192)
42
+
43
+ # detect line ending style
44
+ cr = sample.indexOf('\r')
45
+ lf = sample.indexOf('\n')
46
+ if cr >= 0 and lf is cr + 1
47
+ row = CRLF
48
+ else if lf >= 0
49
+ row = '\n'
50
+ else if cr >= 0
51
+ row = '\r'
52
+ else
53
+ row = '\n'
54
+
55
+ # detect delimiter from first line
56
+ lineEnd = sample.indexOf(row is CRLF ? '\r' : row)
57
+ lineEnd = sample.length if lineEnd is -1
58
+ firstLine = sample.slice(0, lineEnd)
59
+ unless opts.sep
60
+ best = null
61
+ bestCount = 0
62
+ for d in DELIMITERS
63
+ n = 0
64
+ i = -1
65
+ n++ while (i = firstLine.indexOf(d, i + 1)) isnt -1
66
+ if n > bestCount
67
+ best = d
68
+ bestCount = n
69
+ sep ?= best ? ','
70
+
71
+ # detect quoting
72
+ quote = opts.quote ? '"'
73
+ hasQuotes = sample.indexOf(quote) >= 0
74
+
75
+ # detect escape style: backslash vs doubled quote
76
+ escape = opts.escape
77
+ unless escape
78
+ if hasQuotes
79
+ escape = sample.indexOf("\\#{quote}") >= 0 ? '\\' : quote
80
+ else
81
+ escape = quote
82
+
83
+ # merge with user options (user wins)
84
+ {
85
+ str
86
+ sep: opts.sep ? sep
87
+ quote: quote
88
+ escape: escape
89
+ row: opts.row ? row
90
+ hasQuotes: hasQuotes
91
+ excel: opts.excel ? false
92
+ relax: opts.relax ? false
93
+ strip: opts.strip ? false
94
+ headers: opts.headers ? false
95
+ comments: opts.comments ? null
96
+ skipBlanks: opts.skipBlanks ? true
97
+ each: opts.each ?? null
98
+ }
99
+
100
+ # ==============================================================================
101
+ # Helpers — emit rows with headers/callback support
102
+ # ==============================================================================
103
+
104
+ def makeEmitter(cfg)
105
+ {headers, strip, each} = cfg
106
+ ctx = {keys: null, rows: (each ? null : []), count: 0}
107
+
108
+ emit = (row) ->
109
+ row = row.map((c) -> c.trim()) if strip
110
+
111
+ # first row becomes keys in headers mode
112
+ if headers and not ctx.keys
113
+ ctx.keys = row
114
+ return true
115
+
116
+ # zip with keys for object output
117
+ if ctx.keys
118
+ obj = {}
119
+ for key, i in ctx.keys
120
+ obj[key] = row[i] ? ''
121
+ if each
122
+ ctx.count++
123
+ return each(obj, ctx.count - 1) isnt false
124
+ ctx.rows.push obj
125
+ return true
126
+
127
+ # plain array output
128
+ if each
129
+ ctx.count++
130
+ return each(row, ctx.count - 1) isnt false
131
+ ctx.rows.push row
132
+ true
133
+
134
+ result = -> each ? ctx.count : ctx.rows
135
+
136
+ {emit, result}
137
+
138
+ # ==============================================================================
139
+ # Helper — advance past \r\n or single \r or \n
140
+ # ==============================================================================
141
+
142
+ def crlfLen(str, pos)
143
+ if str.charCodeAt(pos) is CR and str.charCodeAt(pos + 1) is LF then 2 else 1
144
+
145
+ # ==============================================================================
146
+ # Reader — Fast path (no quotes detected)
147
+ # ==============================================================================
148
+
149
+ def readFast(str, cfg)
150
+ {sep, row: rowDelim, comments, skipBlanks} = cfg
151
+ {emit, result} = makeEmitter(cfg)
152
+
153
+ sepLen = sep.length
154
+ rowLen = rowDelim.length
155
+ len = str.length
156
+ pos = 0
157
+
158
+ while pos < len
159
+ # find end of current line
160
+ rowEnd = str.indexOf(rowDelim, pos)
161
+ rowEnd = len if rowEnd is -1
162
+
163
+ # skip empty lines
164
+ if pos is rowEnd
165
+ pos = rowEnd + rowLen
166
+ continue if skipBlanks
167
+
168
+ # skip comment lines
169
+ if comments and str[pos] is comments
170
+ pos = rowEnd + rowLen
171
+ continue
172
+
173
+ # extract fields with indexOf ratchet for separator
174
+ row = []
175
+ p = pos
176
+ loop
177
+ s = str.indexOf(sep, p)
178
+ if s >= 0 and s < rowEnd
179
+ row.push str.slice(p, s)
180
+ p = s + sepLen
181
+ if p >= rowEnd
182
+ row.push '' # trailing separator -> empty final field
183
+ break
184
+ else
185
+ row.push str.slice(p, rowEnd)
186
+ break
187
+
188
+ pos = rowEnd + rowLen
189
+
190
+ # trim trailing \r for mixed line endings
191
+ last = row.length - 1
192
+ if last >= 0 and row[last].endsWith('\r')
193
+ row[last] = row[last].slice(0, -1)
194
+
195
+ break unless emit(row)
196
+
197
+ result()
198
+
199
+ # ==============================================================================
200
+ # Reader — Full path (quotes present)
201
+ # ==============================================================================
202
+
203
+ def readFull(str, cfg)
204
+ {sep, quote, escape, excel, relax} = cfg
205
+ {comments, skipBlanks} = cfg
206
+ {emit, result} = makeEmitter(cfg)
207
+
208
+ sepCode = sep.charCodeAt(0)
209
+ quoteCode = quote.charCodeAt(0)
210
+ sepLen = sep.length
211
+ escSame = escape is quote
212
+ escCode = escape.charCodeAt(0)
213
+ len = str.length
214
+ pos = 0
215
+
216
+ row = []
217
+ atLineStart = true
218
+
219
+ while pos < len
220
+ c = str.charCodeAt(pos)
221
+
222
+ # skip empty lines at line start
223
+ if atLineStart
224
+ if skipBlanks and (c is LF or c is CR)
225
+ pos += crlfLen(str, pos)
226
+ continue
227
+ if comments and str[pos] is comments
228
+ nl = str.indexOf('\n', pos)
229
+ if nl is -1
230
+ nl = str.indexOf('\r', pos)
231
+ pos = nl is -1 ? len : nl + 1
232
+ continue
233
+ atLineStart = false
234
+
235
+ # === quoted field ===
236
+ if c is quoteCode or (excel and c is EQ and str.charCodeAt(pos + 1) is quoteCode)
237
+ if excel and c is EQ
238
+ pos += 2 # skip ="
239
+ else
240
+ pos += 1 # skip opening quote
241
+
242
+ field = ''
243
+ loop
244
+ # indexOf to jump to next quote — bulk skip over content
245
+ q = str.indexOf(quote, pos)
246
+
247
+ unless q >= 0
248
+ # no closing quote found
249
+ if relax
250
+ field += str.slice(pos)
251
+ pos = len
252
+ break
253
+ throw new Error "CSV: unclosed quote at position #{pos}"
254
+
255
+ field += str.slice(pos, q)
256
+ pos = q + quote.length
257
+
258
+ # doubled-quote escape: "" -> "
259
+ if escSame
260
+ if pos < len and str.charCodeAt(pos) is quoteCode
261
+ field += quote
262
+ pos += quote.length
263
+ continue
264
+ else
265
+ # backslash escape: \" -> "
266
+ if q > 0 and str.charCodeAt(q - 1) is escCode
267
+ field = field.slice(0, -1) + quote
268
+ continue
269
+
270
+ # check what follows the closing quote
271
+ break if pos >= len # end of string
272
+
273
+ c2 = str.charCodeAt(pos)
274
+ break if c2 is sepCode or c2 is LF or c2 is CR # valid end-of-field
275
+
276
+ # unexpected character after closing quote
277
+ unless relax
278
+ throw new Error "CSV: unexpected character after quote at position #{pos}"
279
+
280
+ # relax mode: treat the quote as literal, keep scanning
281
+ field += quote
282
+ continue
283
+
284
+ # push field and consume trailing delimiter
285
+ row.push field
286
+
287
+ if pos < len
288
+ c2 = str.charCodeAt(pos)
289
+ if c2 is sepCode
290
+ pos += sepLen
291
+ else if c2 is LF or c2 is CR
292
+ pos += crlfLen(str, pos)
293
+ break unless emit(row)
294
+ row = []
295
+ atLineStart = true
296
+
297
+ # === newline (end of row) ===
298
+ else if c is LF or c is CR
299
+ pos += crlfLen(str, pos)
300
+ break unless emit(row)
301
+ row = []
302
+ atLineStart = true
303
+
304
+ # === separator (empty field) ===
305
+ else if c is sepCode
306
+ row.push ''
307
+ pos += sepLen
308
+
309
+ # === unquoted field ===
310
+ else
311
+ # indexOf ratchet: find nearest sep or newline
312
+ s = str.indexOf(sep, pos)
313
+ n = str.indexOf('\n', pos)
314
+ r = str.indexOf('\r', pos)
315
+
316
+ # nearest newline (\r or \n)
317
+ if r >= 0 and n >= 0
318
+ nl = Math.min(r, n)
319
+ else if r >= 0
320
+ nl = r
321
+ else
322
+ nl = n
323
+
324
+ # take the nearer boundary
325
+ if s >= 0 and (nl is -1 or s < nl)
326
+ row.push str.slice(pos, s)
327
+ pos = s + sepLen
328
+ else if nl >= 0
329
+ row.push str.slice(pos, nl)
330
+ pos = nl + crlfLen(str, nl)
331
+ break unless emit(row)
332
+ row = []
333
+ atLineStart = true
334
+ else
335
+ row.push str.slice(pos)
336
+ pos = len
337
+
338
+ # emit final row if pending
339
+ emit(row) if row.length > 0
340
+
341
+ result()
342
+
343
+ # ==============================================================================
344
+ # Writer — format data as CSV strings
345
+ # ==============================================================================
346
+
347
+ class Writer
348
+ constructor: (opts = {}) ->
349
+ @sep = opts.sep ? ','
350
+ @quote = opts.quote ? '"'
351
+ @escape = opts.escape ? @quote
352
+ @mode = opts.mode ? 'compact'
353
+ @excel = opts.excel ? false
354
+ @drop = opts.drop ? false
355
+ @rowsep = opts.rowsep ? '\n'
356
+
357
+ # pre-compute escaped quote
358
+ @esc = @escape + @quote
359
+ @leadZero = /^0\d+$/
360
+
361
+ # check if a cell value needs quoting
362
+ needsQuote: (cell) ->
363
+ cell.indexOf(@sep) >= 0 or
364
+ cell.indexOf('\n') >= 0 or
365
+ cell.indexOf('\r') >= 0 or
366
+ cell.indexOf(@quote) >= 0
367
+
368
+ # format a single row as a CSV line (no trailing row separator)
369
+ row: (data) ->
370
+ cells = (String(v ? '') for v in data)
371
+
372
+ # drop trailing empty columns
373
+ if @drop
374
+ cells.pop() while cells.length > 0 and cells[cells.length - 1] is ''
375
+
376
+ q = @quote
377
+ esc = @esc
378
+
379
+ formatted = switch @mode
380
+ when 'compact'
381
+ for cell in cells
382
+ if @excel and @leadZero.test(cell)
383
+ "=#{q}#{cell}#{q}"
384
+ else if @needsQuote(cell)
385
+ "#{q}#{cell.replaceAll(q, esc)}#{q}"
386
+ else
387
+ cell
388
+ when 'full'
389
+ for cell in cells
390
+ if @excel and @leadZero.test(cell)
391
+ "=#{q}#{cell}#{q}"
392
+ else
393
+ "#{q}#{cell.replaceAll(q, esc)}#{q}"
394
+ else
395
+ cells
396
+
397
+ formatted.join @sep
398
+
399
+ # format multiple rows as a complete CSV string
400
+ rows: (data) ->
401
+ return '' unless data?.length
402
+ ((@row(r) for r in data).join(@rowsep)) + @rowsep
403
+
404
+ # ==============================================================================
405
+ # Public API
406
+ # ==============================================================================
407
+
408
+ export CSV =
409
+ # parse a CSV string into rows (arrays or objects)
410
+ read: (str, opts = {}) ->
411
+ return [] unless str?.length
412
+ cfg = probe(str, opts)
413
+ if cfg.hasQuotes
414
+ readFull(cfg.str, cfg)
415
+ else
416
+ readFast(cfg.str, cfg)
417
+
418
+ # format row arrays into a CSV string
419
+ write: (rows, opts = {}) ->
420
+ new Writer(opts).rows(rows)
421
+
422
+ # read and parse a CSV file (async — uses Bun.file)
423
+ load: (path, opts = {}) ->
424
+ str = Bun.file(path).text!
425
+ CSV.read str, opts
426
+
427
+ # write rows to a CSV file (async — uses Bun.write)
428
+ save: (path, rows, opts = {}) ->
429
+ Bun.write! path, CSV.write(rows, opts)
430
+
431
+ # create a reusable Writer instance
432
+ writer: (opts = {}) ->
433
+ new Writer(opts)
434
+
435
+ # format a single row (convenience — creates a one-shot Writer)
436
+ formatRow: (row, opts = {}) ->
437
+ new Writer(opts).row(row)
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@rip-lang/csv",
3
+ "version": "1.0.1",
4
+ "description": "Fast, flexible CSV parser and writer for Rip — indexOf ratchet engine, auto-detection, zero dependencies",
5
+ "type": "module",
6
+ "main": "csv.rip",
7
+ "exports": {
8
+ ".": "./csv.rip"
9
+ },
10
+ "scripts": {
11
+ "test": "rip test/basic.rip"
12
+ },
13
+ "keywords": [
14
+ "csv",
15
+ "parser",
16
+ "writer",
17
+ "fast",
18
+ "indexOf",
19
+ "bun",
20
+ "rip"
21
+ ],
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/shreeve/rip-lang.git",
25
+ "directory": "packages/csv"
26
+ },
27
+ "homepage": "https://github.com/shreeve/rip-lang/tree/main/packages/csv#readme",
28
+ "bugs": {
29
+ "url": "https://github.com/shreeve/rip-lang/issues"
30
+ },
31
+ "author": "Steve Shreeve <steve.shreeve@gmail.com>",
32
+ "license": "MIT",
33
+ "dependencies": {
34
+ "rip-lang": "^2.9.0"
35
+ },
36
+ "files": [
37
+ "csv.rip",
38
+ "README.md"
39
+ ]
40
+ }