1 | | // Generated by CoffeeScript 1.3.3 |
2 | | /* |
3 | | Module CSV - Copyright David Worms <open@adaltas.com> (BSD Licensed) |
4 | | |
5 | | |---------------| |---------------|---------------| |---------------| |
6 | | | | | | | | | |
7 | | | | | CSV | | | |
8 | | | | | | | | | |
9 | | | Stream | | Writer | Reader | | Stream | |
10 | | | Reader | .pipe( | API | API | ).pipe( | Writer | ) |
11 | | | | | | | | | |
12 | | | | | | | | | |
13 | | |---------------| |---------------|---------------| |---------------| |
14 | | |
15 | | fs.createReadStream('in'.pipe( csv() ).pipe( fs.createWriteStream('out') ) |
16 | | */ |
17 | | |
18 | 1 | var from, options, state, stream, to; |
19 | | |
20 | 1 | stream = require('stream'); |
21 | | |
22 | 1 | state = require('./state'); |
23 | | |
24 | 1 | options = require('./options'); |
25 | | |
26 | 1 | from = require('./from'); |
27 | | |
28 | 1 | to = require('./to'); |
29 | | |
30 | 1 | module.exports = function() { |
31 | 11 | var CSV, csv, error, parse, transform, write; |
32 | 11 | CSV = function() { |
33 | 11 | this.readable = true; |
34 | 11 | this.writable = true; |
35 | 11 | this.state = state(); |
36 | 11 | this.options = options(); |
37 | 11 | this.from = from(this); |
38 | 11 | this.to = to(this); |
39 | 11 | return this; |
40 | | }; |
41 | 11 | CSV.prototype.__proto__ = stream.prototype; |
42 | | /* |
43 | | |
44 | | `write(data, [preserve])`: Write data |
45 | | ------------------------------------- |
46 | | |
47 | | Implementation of the StreamWriter API with a larger signature. Data |
48 | | may be a string, a buffer, an array or an object. |
49 | | |
50 | | If data is a string or a buffer, it could span multiple lines. If data |
51 | | is an object or an array, it must represent a single line. |
52 | | Preserve is for line which are not considered as CSV data. |
53 | | */ |
54 | | |
55 | 11 | CSV.prototype.write = function(data, preserve) { |
56 | 12 | if (!this.writable) { |
57 | 0 | return; |
58 | | } |
59 | 12 | if (typeof data === 'string' && !preserve) { |
60 | 12 | return parse(data); |
61 | 0 | } else if (Array.isArray(data) && !this.state.transforming) { |
62 | 0 | this.state.line = data; |
63 | 0 | return transform(); |
64 | | } |
65 | 0 | if (this.state.count === 0 && this.options.to.header === true) { |
66 | 0 | write(this.options.to.columns || this.options.from.columns); |
67 | | } |
68 | 0 | write(data, preserve); |
69 | 0 | if (!this.state.transforming && !preserve) { |
70 | 0 | return this.state.count++; |
71 | | } |
72 | | }; |
73 | | /* |
74 | | |
75 | | `end()`: Terminate the parsing |
76 | | ------------------------------- |
77 | | |
78 | | Call this method when no more csv data is to be parsed. It |
79 | | implement the StreamWriter API by setting the `writable` |
80 | | property to "false" and emitting the `end` event. |
81 | | */ |
82 | | |
83 | 11 | CSV.prototype.end = function() { |
84 | 11 | if (!this.writable) { |
85 | 0 | return; |
86 | | } |
87 | 11 | if (this.state.quoted) { |
88 | 0 | return error(new Error('Quoted field not terminated')); |
89 | | } |
90 | 11 | if (this.state.field || this.state.lastC === this.options.from.delimiter || this.state.lastC === this.options.from.quote) { |
91 | 1 | if (this.options.from.trim || this.options.from.rtrim) { |
92 | 0 | this.state.field = this.state.field.trimRight(); |
93 | | } |
94 | 1 | this.state.line.push(this.state.field); |
95 | 1 | this.state.field = ''; |
96 | | } |
97 | 11 | if (this.state.line.length > 0) { |
98 | 1 | transform(); |
99 | | } |
100 | 11 | if (this.writeStream) { |
101 | 11 | if (this.state.bufferPosition !== 0) { |
102 | 11 | this.writeStream.write(this.state.buffer.slice(0, this.state.bufferPosition)); |
103 | | } |
104 | 11 | if (this.options.to.end) { |
105 | 11 | return this.writeStream.end(); |
106 | | } else { |
107 | 0 | this.emit('end', this.state.count); |
108 | 0 | return this.readable = false; |
109 | | } |
110 | | } else { |
111 | 0 | this.emit('end', this.state.count); |
112 | 0 | return this.readable = false; |
113 | | } |
114 | | }; |
115 | | /* |
116 | | |
117 | | `transform(callback)`: Register the transformer callback |
118 | | -------------------------------------------------------- |
119 | | |
120 | | User provided function call on each line to filter, enrich or modify |
121 | | the dataset. The callback is called asynchronously. |
122 | | */ |
123 | | |
124 | 11 | CSV.prototype.transform = function(callback) { |
125 | 8 | this.transformer = callback; |
126 | 8 | return this; |
127 | | }; |
128 | 11 | csv = new CSV(); |
129 | | /* |
130 | | Parse a string which may hold multiple lines. |
131 | | Private state object is enriched on each character until |
132 | | transform is called on a new line |
133 | | */ |
134 | | |
135 | 11 | parse = function(chars) { |
136 | 12 | var c, escapeIsQuoted, i, isEscaped, isQuoted, isReallyEscaped, l, nextChar; |
137 | 12 | chars = '' + chars; |
138 | 12 | l = chars.length; |
139 | 12 | i = 0; |
140 | 12 | while (i < l) { |
141 | 3270 | c = chars.charAt(i); |
142 | 3270 | switch (c) { |
143 | | case csv.options.from.escape: |
144 | | case csv.options.from.quote: |
145 | 14 | if (csv.state.commented) { |
146 | 0 | break; |
147 | | } |
148 | 14 | isReallyEscaped = false; |
149 | 14 | if (c === csv.options.from.escape) { |
150 | 10 | nextChar = chars.charAt(i + 1); |
151 | 10 | escapeIsQuoted = csv.options.from.escape === csv.options.from.quote; |
152 | 10 | isEscaped = nextChar === csv.options.from.escape; |
153 | 10 | isQuoted = nextChar === csv.options.from.quote; |
154 | 10 | if (!(escapeIsQuoted && !csv.state.field && !csv.state.quoted) && (isEscaped || isQuoted)) { |
155 | 6 | i++; |
156 | 6 | isReallyEscaped = true; |
157 | 6 | c = chars.charAt(i); |
158 | 6 | csv.state.field += c; |
159 | | } |
160 | | } |
161 | 14 | if (!isReallyEscaped && c === csv.options.from.quote) { |
162 | 8 | if (csv.state.field && !csv.state.quoted) { |
163 | 0 | csv.state.field += c; |
164 | 0 | break; |
165 | | } |
166 | 8 | if (csv.state.quoted) { |
167 | 4 | nextChar = chars.charAt(i + 1); |
168 | 4 | if (nextChar && nextChar !== '\r' && nextChar !== '\n' && nextChar !== csv.options.from.delimiter) { |
169 | 0 | return error(new Error('Invalid closing quote; found "' + nextChar + '" instead of delimiter "' + csv.options.from.delimiter + '"')); |
170 | | } |
171 | 4 | csv.state.quoted = false; |
172 | 4 | } else if (csv.state.field === '') { |
173 | 4 | csv.state.quoted = true; |
174 | | } |
175 | | } |
176 | 14 | break; |
177 | | case csv.options.from.delimiter: |
178 | 341 | if (csv.state.commented) { |
179 | 0 | break; |
180 | | } |
181 | 341 | if (csv.state.quoted) { |
182 | 0 | csv.state.field += c; |
183 | | } else { |
184 | 341 | if (csv.options.from.trim || csv.options.from.rtrim) { |
185 | 0 | csv.state.field = csv.state.field.trimRight(); |
186 | | } |
187 | 341 | csv.state.line.push(csv.state.field); |
188 | 341 | csv.state.field = ''; |
189 | | } |
190 | 341 | break; |
191 | | case '\n': |
192 | | case '\r': |
193 | 68 | if (csv.state.quoted) { |
194 | 0 | csv.state.field += c; |
195 | 0 | break; |
196 | | } |
197 | 68 | if (!csv.options.from.quoted && csv.state.lastC === '\r') { |
198 | 0 | break; |
199 | | } |
200 | 68 | if (csv.options.to.lineBreaks === null) { |
201 | 11 | csv.options.to.lineBreaks = c + (c === '\r' && chars.charAt(i + 1) === '\n' ? '\n' : ''); |
202 | | } |
203 | 68 | if (csv.options.from.trim || csv.options.from.rtrim) { |
204 | 0 | csv.state.field = csv.state.field.trimRight(); |
205 | | } |
206 | 68 | csv.state.line.push(csv.state.field); |
207 | 68 | csv.state.field = ''; |
208 | 68 | transform(); |
209 | 68 | break; |
210 | | case ' ': |
211 | | case '\t': |
212 | 0 | if (csv.state.quoted || (!csv.options.from.trim && !csv.options.from.ltrim) || csv.state.field) { |
213 | 0 | csv.state.field += c; |
214 | 0 | break; |
215 | | } |
216 | 0 | break; |
217 | | default: |
218 | 2847 | if (csv.state.commented) { |
219 | 0 | break; |
220 | | } |
221 | 2847 | csv.state.field += c; |
222 | | } |
223 | 3270 | csv.state.lastC = c; |
224 | 3270 | i++; |
225 | | } |
226 | | }; |
227 | | /* |
228 | | Called by the `parse` function on each line. It is responsible for |
229 | | transforming the data and finally calling `write`. |
230 | | */ |
231 | | |
232 | 11 | transform = function() { |
233 | 69 | var column, columns, i, isObject, line, _i, _ref; |
234 | 69 | line = null; |
235 | 69 | columns = csv.options.from.columns; |
236 | 69 | if (columns) { |
237 | 8 | if (csv.state.count === 0 && columns === true) { |
238 | 2 | csv.options.from.columns = columns = csv.state.line; |
239 | 2 | csv.state.line = []; |
240 | 2 | csv.state.lastC = ''; |
241 | 2 | return; |
242 | | } |
243 | 6 | line = {}; |
244 | 6 | for (i = _i = 0, _ref = columns.length; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) { |
245 | 37 | column = columns[i]; |
246 | 37 | line[column] = csv.state.line[i] || null; |
247 | | } |
248 | 6 | csv.state.line = line; |
249 | 6 | line = null; |
250 | | } |
251 | 67 | if (csv.transformer) { |
252 | 61 | csv.state.transforming = true; |
253 | 61 | try { |
254 | 61 | line = csv.transformer(csv.state.line, csv.state.count); |
255 | | } catch (e) { |
256 | 0 | return error(e); |
257 | | } |
258 | 61 | isObject = typeof line === 'object' && !Array.isArray(line); |
259 | 61 | if (csv.options.to.newColumns && !csv.options.to.columns && isObject) { |
260 | 2 | Object.keys(line).filter(function(column) { |
261 | 14 | return columns.indexOf(column) === -1; |
262 | | }).forEach(function(column) { |
263 | 1 | return columns.push(column); |
264 | | }); |
265 | | } |
266 | 61 | csv.state.transforming = false; |
267 | | } else { |
268 | 6 | line = csv.state.line; |
269 | | } |
270 | 67 | if (csv.state.count === 0 && csv.options.to.header === true) { |
271 | 1 | write(csv.options.to.columns || columns); |
272 | | } |
273 | 67 | write(line); |
274 | 67 | csv.state.count++; |
275 | 67 | csv.state.line = []; |
276 | 67 | return csv.state.lastC = ''; |
277 | | }; |
278 | | /* |
279 | | Write a line to the written stream. |
280 | | Line may be an object, an array or a string |
281 | | Preserve is for line which are not considered as CSV data |
282 | | */ |
283 | | |
284 | 11 | write = function(line, preserve) { |
285 | 68 | var column, columns, containsLinebreak, containsQuote, containsdelimiter, field, i, newLine, regexp, _i, _j, _line, _ref, _ref1; |
286 | 68 | if (typeof line === 'undefined' || line === null) { |
287 | 0 | return; |
288 | | } |
289 | 68 | if (!preserve) { |
290 | 68 | try { |
291 | 68 | csv.emit('data', line, csv.state.count); |
292 | | } catch (e) { |
293 | 0 | return error(e); |
294 | | } |
295 | | } |
296 | 68 | if (typeof line === 'object') { |
297 | 68 | if (!Array.isArray(line)) { |
298 | 8 | columns = csv.options.to.columns || csv.options.from.columns; |
299 | 8 | _line = []; |
300 | 8 | if (columns) { |
301 | 8 | for (i = _i = 0, _ref = columns.length; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) { |
302 | 42 | column = columns[i]; |
303 | 42 | _line[i] = typeof line[column] === 'undefined' || line[column] === null ? '' : line[column]; |
304 | | } |
305 | | } else { |
306 | 0 | for (column in line) { |
307 | 0 | _line.push(line[column]); |
308 | | } |
309 | | } |
310 | 8 | line = _line; |
311 | 8 | _line = null; |
312 | 60 | } else if (csv.options.to.columns) { |
313 | 2 | line.splice(csv.options.to.columns.length); |
314 | | } |
315 | 68 | if (Array.isArray(line)) { |
316 | 68 | newLine = csv.state.countWriten ? csv.options.to.lineBreaks || "\n" : ''; |
317 | 68 | for (i = _j = 0, _ref1 = line.length; 0 <= _ref1 ? _j < _ref1 : _j > _ref1; i = 0 <= _ref1 ? ++_j : --_j) { |
318 | 391 | field = line[i]; |
319 | 391 | if (typeof field === 'string') { |
320 | | |
321 | 0 | } else if (typeof field === 'number') { |
322 | 0 | field = '' + field; |
323 | 0 | } else if (typeof field === 'boolean') { |
324 | 0 | field = field ? '1' : ''; |
325 | 0 | } else if (field instanceof Date) { |
326 | 0 | field = '' + field.getTime(); |
327 | | } |
328 | 391 | if (field) { |
329 | 381 | containsdelimiter = field.indexOf(csv.options.to.delimiter || csv.options.from.delimiter) >= 0; |
330 | 381 | containsQuote = field.indexOf(csv.options.to.quote || csv.options.from.quote) >= 0; |
331 | 381 | containsLinebreak = field.indexOf("\r") >= 0 || field.indexOf("\n") >= 0; |
332 | 381 | if (containsQuote) { |
333 | 4 | regexp = new RegExp(csv.options.to.quote || csv.options.from.quote, 'g'); |
334 | 4 | field = field.replace(regexp, (csv.options.to.escape || csv.options.from.escape) + (csv.options.to.quote || csv.options.from.quote)); |
335 | | } |
336 | 381 | if (containsQuote || containsdelimiter || containsLinebreak || csv.options.to.quoted) { |
337 | 4 | field = (csv.options.to.quote || csv.options.from.quote) + field + (csv.options.to.quote || csv.options.from.quote); |
338 | | } |
339 | 381 | newLine += field; |
340 | | } |
341 | 391 | if (i !== line.length - 1) { |
342 | 323 | newLine += csv.options.to.delimiter || csv.options.from.delimiter; |
343 | | } |
344 | | } |
345 | 68 | line = newLine; |
346 | | } |
347 | 0 | } else if (typeof line === 'number') { |
348 | 0 | line = '' + line; |
349 | | } |
350 | 68 | if (csv.state.buffer) { |
351 | 68 | if (csv.state.bufferPosition + Buffer.byteLength(line, csv.options.to.encoding) > csv.options.from.bufferSize) { |
352 | 1 | csv.writeStream.write(csv.state.buffer.slice(0, csv.state.bufferPosition)); |
353 | 1 | csv.state.buffer = new Buffer(csv.options.from.bufferSize); |
354 | 1 | csv.state.bufferPosition = 0; |
355 | | } |
356 | 68 | csv.state.bufferPosition += csv.state.buffer.write(line, csv.state.bufferPosition, csv.options.to.encoding); |
357 | | } |
358 | 68 | if (!preserve) { |
359 | 68 | csv.state.countWriten++; |
360 | | } |
361 | 68 | return true; |
362 | | }; |
363 | 11 | error = function(e) { |
364 | 0 | csv.readable = false; |
365 | 0 | csv.writable = false; |
366 | 0 | csv.emit('error', e); |
367 | 0 | if (csv.readStream) { |
368 | 0 | csv.readStream.destroy(); |
369 | | } |
370 | 0 | return e; |
371 | | }; |
372 | 11 | return csv; |
373 | | }; |