convert-csv-to-json 4.14.0 → 4.16.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/.github/workflows/ci-cd.yml +1 -1
- package/docs/.nojekyll +0 -0
- package/docs/README.md +44 -0
- package/docs/demo.bundle.js +1708 -0
- package/docs/index.html +536 -0
- package/eslint.config.js +1 -1
- package/package.json +5 -1
- package/docs/api/BrowserApi.html +0 -2662
- package/docs/api/BrowserApiError.html +0 -669
- package/docs/api/ConfigurationError.html +0 -745
- package/docs/api/CsvFormatError.html +0 -677
- package/docs/api/CsvParsingError.html +0 -511
- package/docs/api/CsvToJson.html +0 -3367
- package/docs/api/CsvToJsonAsync.html +0 -2880
- package/docs/api/FileOperationError.html +0 -382
- package/docs/api/FileUtils.html +0 -1150
- package/docs/api/InputValidationError.html +0 -407
- package/docs/api/JsonUtil.html +0 -452
- package/docs/api/JsonValidationError.html +0 -357
- package/docs/api/StringUtils.html +0 -833
- package/docs/api/global.html +0 -3498
- package/docs/api/index.html +0 -414
- package/docs/api/index.js.html +0 -447
- package/docs/api/scripts/app.min.js +0 -1
- package/docs/api/scripts/linenumber.js +0 -26
- package/docs/api/scripts/search.js +0 -39
- package/docs/api/src_browserApi.js.html +0 -362
- package/docs/api/src_csvToJson.js.html +0 -696
- package/docs/api/src_csvToJsonAsync.js.html +0 -335
- package/docs/api/src_util_errors.js.html +0 -472
- package/docs/api/src_util_fileUtils.js.html +0 -238
- package/docs/api/src_util_jsonUtils.js.html +0 -166
- package/docs/api/src_util_stringUtils.js.html +0 -306
- package/docs/api/styles/app.min.css +0 -1
- package/docs/api/styles/iframe.css +0 -13
- package/docs/api/styles/prettify-jsdoc.css +0 -111
- package/docs/api/styles/prettify-tomorrow.css +0 -132
- package/docs/api/styles/reset.css +0 -44
|
@@ -0,0 +1,1708 @@
|
|
|
1
|
+
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.demo = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
|
|
2
|
+
// Demo application for CSV to JSON converter
|
|
3
|
+
// This will be bundled with the library using browserify
|
|
4
|
+
|
|
5
|
+
const csvToJson = require('../src/browserApi');
|
|
6
|
+
|
|
7
|
+
(function() {
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
// Initialize when DOM is ready
|
|
11
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
12
|
+
setupEventListeners();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
function setupEventListeners() {
|
|
16
|
+
// Tab switching
|
|
17
|
+
document.querySelectorAll('.tab').forEach(tab => {
|
|
18
|
+
tab.addEventListener('click', function() {
|
|
19
|
+
showTab(this.getAttribute('data-tab'));
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Output tab switching
|
|
24
|
+
document.querySelectorAll('.output-tab').forEach(tab => {
|
|
25
|
+
tab.addEventListener('click', function() {
|
|
26
|
+
showOutputTab(this.getAttribute('data-output'));
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Convert button
|
|
31
|
+
document.getElementById('convert-btn').addEventListener('click', convert);
|
|
32
|
+
|
|
33
|
+
// Clear button
|
|
34
|
+
document.getElementById('clear-btn').addEventListener('click', clearAll);
|
|
35
|
+
|
|
36
|
+
// Options change listeners
|
|
37
|
+
document.getElementById('format-values').addEventListener('change', updateOptions);
|
|
38
|
+
document.getElementById('quoted-fields').addEventListener('change', updateOptions);
|
|
39
|
+
document.getElementById('delimiter').addEventListener('input', updateOptions);
|
|
40
|
+
document.getElementById('header-index').addEventListener('input', updateOptions);
|
|
41
|
+
|
|
42
|
+
// File input change
|
|
43
|
+
document.getElementById('csv-file').addEventListener('change', handleFileSelect);
|
|
44
|
+
|
|
45
|
+
// Sample data buttons
|
|
46
|
+
document.querySelectorAll('.sample-btn').forEach(btn => {
|
|
47
|
+
btn.addEventListener('click', function() {
|
|
48
|
+
loadSample(this.getAttribute('data-sample'));
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function showTab(tabName) {
|
|
54
|
+
// Hide all tabs
|
|
55
|
+
document.querySelectorAll('.tab-content').forEach(content => {
|
|
56
|
+
content.classList.remove('active');
|
|
57
|
+
});
|
|
58
|
+
document.querySelectorAll('.tab').forEach(tab => {
|
|
59
|
+
tab.classList.remove('active');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Show selected tab
|
|
63
|
+
document.getElementById(tabName + '-tab').classList.add('active');
|
|
64
|
+
document.querySelector(`[data-tab="${tabName}"]`).classList.add('active');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function showOutputTab(tabName) {
|
|
68
|
+
// Hide all output tabs
|
|
69
|
+
document.querySelectorAll('.output-content').forEach(content => {
|
|
70
|
+
content.classList.remove('active');
|
|
71
|
+
});
|
|
72
|
+
document.querySelectorAll('.output-tab').forEach(tab => {
|
|
73
|
+
tab.classList.remove('active');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Show selected output tab
|
|
77
|
+
document.getElementById(tabName + '-content').classList.add('active');
|
|
78
|
+
document.querySelector(`[data-output="${tabName}"]`).classList.add('active');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function updateOptions() {
|
|
82
|
+
const formatValues = document.getElementById('format-values').checked;
|
|
83
|
+
const quotedFields = document.getElementById('quoted-fields').checked;
|
|
84
|
+
const delimiter = document.getElementById('delimiter').value;
|
|
85
|
+
const headerIndex = parseInt(document.getElementById('header-index').value) || 0;
|
|
86
|
+
|
|
87
|
+
csvToJson.formatValueByType(formatValues);
|
|
88
|
+
csvToJson.supportQuotedField(quotedFields);
|
|
89
|
+
csvToJson.fieldDelimiter(delimiter);
|
|
90
|
+
csvToJson.indexHeader(headerIndex);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function convert() {
|
|
94
|
+
const output = document.getElementById('output');
|
|
95
|
+
const convertBtn = document.getElementById('convert-btn');
|
|
96
|
+
|
|
97
|
+
// Clear previous output
|
|
98
|
+
clearOutput();
|
|
99
|
+
|
|
100
|
+
// Disable button during conversion
|
|
101
|
+
convertBtn.disabled = true;
|
|
102
|
+
convertBtn.textContent = 'Converting...';
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
let result;
|
|
106
|
+
|
|
107
|
+
// Check which tab is active
|
|
108
|
+
const activeTab = document.querySelector('.tab-content.active').id;
|
|
109
|
+
if (activeTab === 'text-tab') {
|
|
110
|
+
// Text input
|
|
111
|
+
const csvText = document.getElementById('csv-input').value;
|
|
112
|
+
if (!csvText.trim()) {
|
|
113
|
+
throw new Error('Please enter CSV text');
|
|
114
|
+
}
|
|
115
|
+
result = csvToJson.csvStringToJson(csvText);
|
|
116
|
+
} else {
|
|
117
|
+
// File input
|
|
118
|
+
const fileInput = document.getElementById('csv-file');
|
|
119
|
+
if (!fileInput.files[0]) {
|
|
120
|
+
throw new Error('Please select a CSV file');
|
|
121
|
+
}
|
|
122
|
+
result = await csvToJson.parseFile(fileInput.files[0]);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Display result
|
|
126
|
+
displayResult(result);
|
|
127
|
+
|
|
128
|
+
} catch (error) {
|
|
129
|
+
displayError(error);
|
|
130
|
+
} finally {
|
|
131
|
+
// Re-enable button
|
|
132
|
+
convertBtn.disabled = false;
|
|
133
|
+
convertBtn.textContent = 'Convert to JSON';
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function displayResult(result) {
|
|
138
|
+
const output = document.getElementById('output');
|
|
139
|
+
const jsonOutput = document.getElementById('json-output');
|
|
140
|
+
const tableOutput = document.getElementById('table-output');
|
|
141
|
+
const statsOutput = document.getElementById('stats-output');
|
|
142
|
+
|
|
143
|
+
// Show results container
|
|
144
|
+
output.classList.remove('hidden');
|
|
145
|
+
|
|
146
|
+
// JSON output
|
|
147
|
+
jsonOutput.textContent = JSON.stringify(result, null, 2);
|
|
148
|
+
|
|
149
|
+
// Table output
|
|
150
|
+
displayTable(result);
|
|
151
|
+
|
|
152
|
+
// Stats
|
|
153
|
+
displayStats(result);
|
|
154
|
+
|
|
155
|
+
// Switch to table view by default
|
|
156
|
+
showOutputTab('table');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function displayTable(data) {
|
|
160
|
+
const tableOutput = document.getElementById('table-output');
|
|
161
|
+
if (!data || data.length === 0) {
|
|
162
|
+
tableOutput.innerHTML = '<p>No data to display</p>';
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
let html = '<table><thead><tr>';
|
|
167
|
+
// Headers
|
|
168
|
+
Object.keys(data[0]).forEach(key => {
|
|
169
|
+
html += `<th>${escapeHtml(key)}</th>`;
|
|
170
|
+
});
|
|
171
|
+
html += '</tr></thead><tbody>';
|
|
172
|
+
|
|
173
|
+
// Rows
|
|
174
|
+
data.forEach(row => {
|
|
175
|
+
html += '<tr>';
|
|
176
|
+
Object.values(row).forEach(value => {
|
|
177
|
+
html += `<td>${escapeHtml(String(value))}</td>`;
|
|
178
|
+
});
|
|
179
|
+
html += '</tr>';
|
|
180
|
+
});
|
|
181
|
+
html += '</tbody></table>';
|
|
182
|
+
|
|
183
|
+
tableOutput.innerHTML = html;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function displayStats(data) {
|
|
187
|
+
const statsOutput = document.getElementById('stats-output');
|
|
188
|
+
if (!data || data.length === 0) {
|
|
189
|
+
statsOutput.innerHTML = '<p>No data</p>';
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const numRows = data.length;
|
|
194
|
+
const numCols = Object.keys(data[0]).length;
|
|
195
|
+
const size = JSON.stringify(data).length;
|
|
196
|
+
|
|
197
|
+
statsOutput.innerHTML = `
|
|
198
|
+
<div><strong>Rows:</strong> ${numRows}</div>
|
|
199
|
+
<div><strong>Columns:</strong> ${numCols}</div>
|
|
200
|
+
<div><strong>JSON Size:</strong> ${size} characters</div>
|
|
201
|
+
`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function displayError(error) {
|
|
205
|
+
const output = document.getElementById('output');
|
|
206
|
+
const errorOutput = document.getElementById('error-output');
|
|
207
|
+
|
|
208
|
+
output.classList.remove('hidden');
|
|
209
|
+
errorOutput.textContent = error.message;
|
|
210
|
+
errorOutput.classList.remove('hidden');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function clearOutput() {
|
|
214
|
+
const output = document.getElementById('output');
|
|
215
|
+
const errorOutput = document.getElementById('error-output');
|
|
216
|
+
|
|
217
|
+
output.classList.add('hidden');
|
|
218
|
+
errorOutput.classList.add('hidden');
|
|
219
|
+
errorOutput.textContent = '';
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function clearAll() {
|
|
223
|
+
document.getElementById('csv-input').value = '';
|
|
224
|
+
document.getElementById('csv-file').value = '';
|
|
225
|
+
clearOutput();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function handleFileSelect(event) {
|
|
229
|
+
const file = event.target.files[0];
|
|
230
|
+
if (file) {
|
|
231
|
+
// Show file info
|
|
232
|
+
const fileInfo = document.getElementById('file-info');
|
|
233
|
+
fileInfo.textContent = `Selected: ${file.name} (${formatFileSize(file.size)})`;
|
|
234
|
+
fileInfo.classList.remove('hidden');
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function loadSample(sampleName) {
|
|
239
|
+
const samples = {
|
|
240
|
+
basic: `name,age,city
|
|
241
|
+
John,25,New York
|
|
242
|
+
Jane,30,London
|
|
243
|
+
Bob,35,Paris`,
|
|
244
|
+
|
|
245
|
+
quoted: `"name","age","description"
|
|
246
|
+
"John Doe","25","Software Engineer"
|
|
247
|
+
"Jane Smith","30","Product Manager"`,
|
|
248
|
+
|
|
249
|
+
numbers: `product,price,quantity
|
|
250
|
+
Widget A,19.99,100
|
|
251
|
+
Widget B,29.99,50
|
|
252
|
+
Widget C,9.99,200`,
|
|
253
|
+
|
|
254
|
+
dates: `event,date,attendees
|
|
255
|
+
Conference,2024-01-15,150
|
|
256
|
+
Workshop,2024-02-20,75
|
|
257
|
+
Seminar,2024-03-10,200`
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
document.getElementById('csv-input').value = samples[sampleName] || '';
|
|
261
|
+
showTab('text');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function formatFileSize(bytes) {
|
|
265
|
+
if (bytes === 0) return '0 Bytes';
|
|
266
|
+
const k = 1024;
|
|
267
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
268
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
269
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function escapeHtml(text) {
|
|
273
|
+
const div = document.createElement('div');
|
|
274
|
+
div.textContent = text;
|
|
275
|
+
return div.innerHTML;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function downloadJSON() {
|
|
279
|
+
const jsonOutput = document.getElementById('json-output');
|
|
280
|
+
const data = jsonOutput.textContent;
|
|
281
|
+
const blob = new Blob([data], { type: 'application/json' });
|
|
282
|
+
const url = URL.createObjectURL(blob);
|
|
283
|
+
|
|
284
|
+
const a = document.createElement('a');
|
|
285
|
+
a.href = url;
|
|
286
|
+
a.download = 'converted-data.json';
|
|
287
|
+
document.body.appendChild(a);
|
|
288
|
+
a.click();
|
|
289
|
+
document.body.removeChild(a);
|
|
290
|
+
URL.revokeObjectURL(url);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Make functions globally available for inline event handlers
|
|
294
|
+
window.showTab = showTab;
|
|
295
|
+
window.convert = convert;
|
|
296
|
+
window.clearAll = clearAll;
|
|
297
|
+
window.downloadJSON = downloadJSON;
|
|
298
|
+
})();
|
|
299
|
+
},{"../src/browserApi":3}],2:[function(require,module,exports){
|
|
300
|
+
|
|
301
|
+
},{}],3:[function(require,module,exports){
|
|
302
|
+
/* globals CsvFormatError */
|
|
303
|
+
|
|
304
|
+
"use strict";
|
|
305
|
+
|
|
306
|
+
const csvToJson = require('./csvToJson');
|
|
307
|
+
const { InputValidationError, BrowserApiError } = require('./util/errors');
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Browser-friendly CSV to JSON API
|
|
311
|
+
* Provides methods for parsing CSV strings and File/Blob objects in browser environments
|
|
312
|
+
* Proxies configuration to sync csvToJson instance
|
|
313
|
+
* @category 4-Browser
|
|
314
|
+
*/
|
|
315
|
+
class BrowserApi {
|
|
316
|
+
/**
|
|
317
|
+
* Constructor initializes proxy to sync csvToJson instance
|
|
318
|
+
*/
|
|
319
|
+
constructor() {
|
|
320
|
+
// reuse the existing csvToJson instance for parsing and configuration
|
|
321
|
+
this.csvToJson = csvToJson;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Enable or disable automatic type formatting for values
|
|
326
|
+
* @param {boolean} active - Whether to format values by type (default: true)
|
|
327
|
+
* @returns {this} For method chaining
|
|
328
|
+
*/
|
|
329
|
+
formatValueByType(active = true) {
|
|
330
|
+
this.csvToJson.formatValueByType(active);
|
|
331
|
+
return this;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Enable or disable support for RFC 4180 quoted fields
|
|
336
|
+
* @param {boolean} active - Whether to support quoted fields (default: false)
|
|
337
|
+
* @returns {this} For method chaining
|
|
338
|
+
*/
|
|
339
|
+
supportQuotedField(active = false) {
|
|
340
|
+
this.csvToJson.supportQuotedField(active);
|
|
341
|
+
return this;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Set the field delimiter character
|
|
346
|
+
* @param {string} delimiter - Character(s) to use as field separator
|
|
347
|
+
* @returns {this} For method chaining
|
|
348
|
+
*/
|
|
349
|
+
fieldDelimiter(delimiter) {
|
|
350
|
+
this.csvToJson.fieldDelimiter(delimiter);
|
|
351
|
+
return this;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Configure whitespace handling in header field names
|
|
356
|
+
* @param {boolean} active - If true, removes all whitespace; if false, only trims edges (default: false)
|
|
357
|
+
* @returns {this} For method chaining
|
|
358
|
+
*/
|
|
359
|
+
trimHeaderFieldWhiteSpace(active = false) {
|
|
360
|
+
this.csvToJson.trimHeaderFieldWhiteSpace(active);
|
|
361
|
+
return this;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Set the row index where CSV headers are located
|
|
366
|
+
* @param {number} index - Zero-based row index containing headers
|
|
367
|
+
* @returns {this} For method chaining
|
|
368
|
+
*/
|
|
369
|
+
indexHeader(index) {
|
|
370
|
+
this.csvToJson.indexHeader(index);
|
|
371
|
+
return this;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Configure sub-array parsing for special field values
|
|
376
|
+
* @param {string} delimiter - Bracket character (default: '*')
|
|
377
|
+
* @param {string} separator - Item separator within brackets (default: ',')
|
|
378
|
+
* @returns {this} For method chaining
|
|
379
|
+
*/
|
|
380
|
+
parseSubArray(delimiter = '*', separator = ',') {
|
|
381
|
+
this.csvToJson.parseSubArray(delimiter, separator);
|
|
382
|
+
return this;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Set a mapper function to transform each row after conversion
|
|
387
|
+
* @param {function(object, number): (object|null)} mapperFn - Function receiving (row, index) that returns transformed row or null to filter
|
|
388
|
+
* @returns {this} For method chaining
|
|
389
|
+
*/
|
|
390
|
+
mapRows(mapperFn) {
|
|
391
|
+
this.csvToJson.mapRows(mapperFn);
|
|
392
|
+
return this;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Parse a CSV string and return as JSON array of objects
|
|
397
|
+
* @param {string} csvString - CSV content as string
|
|
398
|
+
* @returns {Array<object>} Array of objects representing CSV rows
|
|
399
|
+
* @throws {InputValidationError} If csvString is invalid
|
|
400
|
+
* @throws {CsvFormatError} If CSV is malformed
|
|
401
|
+
* @example
|
|
402
|
+
* const csvToJson = require('convert-csv-to-json');
|
|
403
|
+
* const rows = csvToJson.browser.csvStringToJson('name,age\nAlice,30');
|
|
404
|
+
* console.log(rows); // [{ name: 'Alice', age: '30' }]
|
|
405
|
+
*/
|
|
406
|
+
csvStringToJson(csvString) {
|
|
407
|
+
if (csvString === undefined || csvString === null) {
|
|
408
|
+
throw new InputValidationError(
|
|
409
|
+
'csvString',
|
|
410
|
+
'string',
|
|
411
|
+
`${typeof csvString}`,
|
|
412
|
+
'Provide valid CSV content as a string to parse.'
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
return this.csvToJson.csvToJson(csvString);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Parse a CSV string and return as stringified JSON
|
|
420
|
+
* @param {string} csvString - CSV content as string
|
|
421
|
+
* @returns {string} JSON stringified array of objects
|
|
422
|
+
* @throws {InputValidationError} If csvString is invalid
|
|
423
|
+
* @throws {CsvFormatError} If CSV is malformed
|
|
424
|
+
* @example
|
|
425
|
+
* const csvToJson = require('convert-csv-to-json');
|
|
426
|
+
* const jsonString = csvToJson.browser.csvStringToJsonStringified('name,age\nAlice,30');
|
|
427
|
+
* console.log(jsonString);
|
|
428
|
+
*/
|
|
429
|
+
csvStringToJsonStringified(csvString) {
|
|
430
|
+
if (csvString === undefined || csvString === null) {
|
|
431
|
+
throw new InputValidationError(
|
|
432
|
+
'csvString',
|
|
433
|
+
'string',
|
|
434
|
+
`${typeof csvString}`,
|
|
435
|
+
'Provide valid CSV content as a string to parse.'
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
return this.csvToJson.csvStringToJsonStringified(csvString);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Parse a CSV string asynchronously (returns resolved Promise)
|
|
443
|
+
* @param {string} csvString - CSV content as string
|
|
444
|
+
* @returns {Promise<Array<object>>} Promise resolving to array of objects
|
|
445
|
+
* @throws {InputValidationError} If csvString is invalid
|
|
446
|
+
* @throws {CsvFormatError} If CSV is malformed
|
|
447
|
+
* @example
|
|
448
|
+
* const csvToJson = require('convert-csv-to-json');
|
|
449
|
+
* const rows = await csvToJson.browser.csvStringToJsonAsync('name,age\nAlice,30');
|
|
450
|
+
* console.log(rows);
|
|
451
|
+
*/
|
|
452
|
+
csvStringToJsonAsync(csvString) {
|
|
453
|
+
return Promise.resolve(this.csvStringToJson(csvString));
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Parse a CSV string asynchronously and return as stringified JSON
|
|
458
|
+
* @param {string} csvString - CSV content as string
|
|
459
|
+
* @returns {Promise<string>} Promise resolving to JSON stringified array
|
|
460
|
+
* @throws {InputValidationError} If csvString is invalid
|
|
461
|
+
* @throws {CsvFormatError} If CSV is malformed
|
|
462
|
+
* @example
|
|
463
|
+
* const csvToJson = require('convert-csv-to-json');
|
|
464
|
+
* const json = await csvToJson.browser.csvStringToJsonStringifiedAsync('name,age\nAlice,30');
|
|
465
|
+
* console.log(json);
|
|
466
|
+
*/
|
|
467
|
+
csvStringToJsonStringifiedAsync(csvString) {
|
|
468
|
+
return Promise.resolve(this.csvStringToJsonStringified(csvString));
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Parse a browser File or Blob object to JSON array.
|
|
473
|
+
* @param {File|Blob} file - File or Blob to read as text
|
|
474
|
+
* @param {object} [options] - options: { encoding?: string }
|
|
475
|
+
* @returns {Promise<object[]>} Promise resolving to parsed JSON rows
|
|
476
|
+
* @example
|
|
477
|
+
* const csvToJson = require('convert-csv-to-json');
|
|
478
|
+
* const fileInput = document.querySelector('#csvfile').files[0];
|
|
479
|
+
* const rows = await csvToJson.browser.parseFile(fileInput);
|
|
480
|
+
* console.log(rows);
|
|
481
|
+
*/
|
|
482
|
+
parseFile(file, options = {}) {
|
|
483
|
+
if (!file) {
|
|
484
|
+
return Promise.reject(new InputValidationError(
|
|
485
|
+
'file',
|
|
486
|
+
'File or Blob object',
|
|
487
|
+
`${typeof file}`,
|
|
488
|
+
'Provide a valid File or Blob object to parse.'
|
|
489
|
+
));
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return new Promise((resolve, reject) => {
|
|
493
|
+
if (typeof FileReader === 'undefined') {
|
|
494
|
+
reject(BrowserApiError.fileReaderNotAvailable());
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const reader = new FileReader();
|
|
499
|
+
reader.onerror = () => reject(BrowserApiError.parseFileError(
|
|
500
|
+
reader.error || new Error('Unknown file reading error')
|
|
501
|
+
));
|
|
502
|
+
reader.onload = () => {
|
|
503
|
+
try {
|
|
504
|
+
const text = reader.result;
|
|
505
|
+
const result = this.csvToJson.csvToJson(String(text));
|
|
506
|
+
resolve(result);
|
|
507
|
+
} catch (err) {
|
|
508
|
+
reject(BrowserApiError.parseFileError(err));
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
// If encoding is provided, pass it to readAsText
|
|
513
|
+
if (options.encoding) {
|
|
514
|
+
reader.readAsText(file, options.encoding);
|
|
515
|
+
} else {
|
|
516
|
+
reader.readAsText(file);
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
module.exports = new BrowserApi();
|
|
523
|
+
|
|
524
|
+
},{"./csvToJson":4,"./util/errors":5}],4:[function(require,module,exports){
|
|
525
|
+
/* globals FileOperationError */
|
|
526
|
+
"use strict";
|
|
527
|
+
|
|
528
|
+
const fileUtils = require('./util/fileUtils');
|
|
529
|
+
const stringUtils = require('./util/stringUtils');
|
|
530
|
+
const jsonUtils = require('./util/jsonUtils');
|
|
531
|
+
const {
|
|
532
|
+
ConfigurationError,
|
|
533
|
+
CsvFormatError,
|
|
534
|
+
JsonValidationError
|
|
535
|
+
} = require('./util/errors');
|
|
536
|
+
|
|
537
|
+
const DEFAULT_FIELD_DELIMITER = ",";
|
|
538
|
+
const QUOTE_CHAR = '"';
|
|
539
|
+
const CRLF = '\r\n';
|
|
540
|
+
const LF = '\n';
|
|
541
|
+
const CR = '\r';
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Main CSV to JSON converter class
|
|
545
|
+
* Provides chainable API for configuring and converting CSV data
|
|
546
|
+
* @category 2-Sync
|
|
547
|
+
*/
|
|
548
|
+
class CsvToJson {
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Enable or disable automatic type formatting for values
|
|
552
|
+
* When enabled, numeric strings are converted to numbers, 'true'/'false' to booleans
|
|
553
|
+
* @param {boolean} active - Whether to format values by type
|
|
554
|
+
* @returns {this} For method chaining
|
|
555
|
+
*/
|
|
556
|
+
formatValueByType(active) {
|
|
557
|
+
this.printValueFormatByType = active;
|
|
558
|
+
return this;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Enable or disable support for RFC 4180 quoted fields
|
|
563
|
+
* When enabled, fields wrapped in double quotes can contain delimiters and newlines
|
|
564
|
+
* @param {boolean} active - Whether to support quoted fields
|
|
565
|
+
* @returns {this} For method chaining
|
|
566
|
+
*/
|
|
567
|
+
supportQuotedField(active) {
|
|
568
|
+
this.isSupportQuotedField = active;
|
|
569
|
+
return this;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Set the field delimiter character
|
|
574
|
+
* @param {string} delimiter - Character(s) to use as field separator (default: ',')
|
|
575
|
+
* @returns {this} For method chaining
|
|
576
|
+
*/
|
|
577
|
+
fieldDelimiter(delimiter) {
|
|
578
|
+
this.delimiter = delimiter;
|
|
579
|
+
return this;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Configure whitespace handling in header field names
|
|
584
|
+
* @param {boolean} active - If true, removes all whitespace from header names; if false, only trims edges
|
|
585
|
+
* @returns {this} For method chaining
|
|
586
|
+
*/
|
|
587
|
+
trimHeaderFieldWhiteSpace(active) {
|
|
588
|
+
this.isTrimHeaderFieldWhiteSpace = active;
|
|
589
|
+
return this;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Set the row index where CSV headers are located
|
|
594
|
+
* @param {number} indexHeaderValue - Zero-based row index containing headers
|
|
595
|
+
* @returns {this} For method chaining
|
|
596
|
+
* @throws {ConfigurationError} If not a valid number
|
|
597
|
+
*/
|
|
598
|
+
indexHeader(indexHeaderValue) {
|
|
599
|
+
if (isNaN(indexHeaderValue)) {
|
|
600
|
+
throw ConfigurationError.invalidHeaderIndex(indexHeaderValue);
|
|
601
|
+
}
|
|
602
|
+
this.indexHeaderValue = indexHeaderValue;
|
|
603
|
+
return this;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Configure sub-array parsing for special field values
|
|
609
|
+
* Fields bracketed by delimiter and containing separator are parsed into arrays
|
|
610
|
+
* @param {string} delimiter - Bracket character (default: '*')
|
|
611
|
+
* @param {string} separator - Item separator within brackets (default: ',')
|
|
612
|
+
* @returns {this} For method chaining
|
|
613
|
+
* @example
|
|
614
|
+
* // Input: "*val1,val2,val3*"
|
|
615
|
+
* // Output: ["val1", "val2", "val3"]
|
|
616
|
+
* .parseSubArray('*', ',')
|
|
617
|
+
*/
|
|
618
|
+
parseSubArray(delimiter = '*',separator = ',') {
|
|
619
|
+
this.parseSubArrayDelimiter = delimiter;
|
|
620
|
+
this.parseSubArraySeparator = separator;
|
|
621
|
+
return this;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Set file encoding for reading CSV files
|
|
626
|
+
* @param {string} encoding - Node.js supported encoding (e.g., 'utf8', 'latin1', 'ascii')
|
|
627
|
+
* @returns {this} For method chaining
|
|
628
|
+
*/
|
|
629
|
+
encoding(encoding){
|
|
630
|
+
this.encoding = encoding;
|
|
631
|
+
return this;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Sets a mapper function to transform each row after conversion
|
|
636
|
+
* @param {function(object, number): (object|null)} mapperFn - Function that receives (row, index) and returns transformed row or null to filter out
|
|
637
|
+
* @returns {this} For method chaining
|
|
638
|
+
*/
|
|
639
|
+
mapRows(mapperFn) {
|
|
640
|
+
if (typeof mapperFn !== 'function') {
|
|
641
|
+
throw new TypeError('mapperFn must be a function');
|
|
642
|
+
}
|
|
643
|
+
this.rowMapper = mapperFn;
|
|
644
|
+
return this;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Read a CSV file and write the parsed JSON to an output file
|
|
649
|
+
* @param {string} fileInputName - Path to input CSV file
|
|
650
|
+
* @param {string} fileOutputName - Path to output JSON file
|
|
651
|
+
* @throws {FileOperationError} If file read or write fails
|
|
652
|
+
* @throws {CsvFormatError} If CSV is malformed
|
|
653
|
+
*/
|
|
654
|
+
generateJsonFileFromCsv(fileInputName, fileOutputName) {
|
|
655
|
+
let jsonStringified = this.getJsonFromCsvStringified(fileInputName);
|
|
656
|
+
fileUtils.writeFile(jsonStringified, fileOutputName);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Read a CSV file and return parsed data as stringified JSON
|
|
661
|
+
* @param {string} fileInputName - Path to input CSV file
|
|
662
|
+
* @returns {string} JSON stringified array of objects
|
|
663
|
+
* @throws {FileOperationError} If file read fails
|
|
664
|
+
* @throws {CsvFormatError} If CSV is malformed
|
|
665
|
+
* @throws {JsonValidationError} If JSON generation fails
|
|
666
|
+
* @example
|
|
667
|
+
* const csvToJson = require('convert-csv-to-json');
|
|
668
|
+
* const jsonString = csvToJson.getJsonFromCsvStringified('resource/input.csv');
|
|
669
|
+
* console.log(jsonString);
|
|
670
|
+
*/
|
|
671
|
+
getJsonFromCsvStringified(fileInputName) {
|
|
672
|
+
let json = this.getJsonFromCsv(fileInputName);
|
|
673
|
+
let jsonStringified = JSON.stringify(json, undefined, 1);
|
|
674
|
+
jsonUtils.validateJson(jsonStringified);
|
|
675
|
+
return jsonStringified;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Read a CSV file and return parsed data as JSON array of objects
|
|
680
|
+
* @param {string} fileInputName - Path to input CSV file
|
|
681
|
+
* @returns {Array<object>} Array of objects representing CSV rows
|
|
682
|
+
* @throws {FileOperationError} If file read fails
|
|
683
|
+
* @throws {CsvFormatError} If CSV is malformed
|
|
684
|
+
* @example
|
|
685
|
+
* const csvToJson = require('convert-csv-to-json');
|
|
686
|
+
* const rows = csvToJson.getJsonFromCsv('resource/input.csv');
|
|
687
|
+
* console.log(rows);
|
|
688
|
+
*/
|
|
689
|
+
getJsonFromCsv(fileInputName) {
|
|
690
|
+
let parsedCsv = fileUtils.readFile(fileInputName, this.encoding);
|
|
691
|
+
return this.csvToJson(parsedCsv);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Parse CSV string content and return as JSON array of objects
|
|
696
|
+
* @param {string} csvString - CSV content as string
|
|
697
|
+
* @returns {Array<object>} Array of objects representing CSV rows
|
|
698
|
+
* @throws {CsvFormatError} If CSV is malformed
|
|
699
|
+
* @example
|
|
700
|
+
* const csvToJson = require('convert-csv-to-json');
|
|
701
|
+
* const rows = csvToJson.csvStringToJson('name,age\nAlice,30');
|
|
702
|
+
* console.log(rows); // [{ name: 'Alice', age: '30' }]
|
|
703
|
+
*/
|
|
704
|
+
csvStringToJson(csvString) {
|
|
705
|
+
return this.csvToJson(csvString);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Parse CSV string content and return as stringified JSON
|
|
710
|
+
* @param {string} csvString - CSV content as string
|
|
711
|
+
* @returns {string} JSON stringified array of objects
|
|
712
|
+
* @throws {CsvFormatError} If CSV is malformed
|
|
713
|
+
* @throws {JsonValidationError} If JSON generation fails
|
|
714
|
+
* @example
|
|
715
|
+
* const csvToJson = require('convert-csv-to-json');
|
|
716
|
+
* const jsonString = csvToJson.csvStringToJsonStringified('name,age\nAlice,30');
|
|
717
|
+
* console.log(jsonString);
|
|
718
|
+
*/
|
|
719
|
+
csvStringToJsonStringified(csvString) {
|
|
720
|
+
let json = this.csvStringToJson(csvString);
|
|
721
|
+
let jsonStringified = JSON.stringify(json, undefined, 1);
|
|
722
|
+
jsonUtils.validateJson(jsonStringified);
|
|
723
|
+
return jsonStringified;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Core CSV parsing logic - converts CSV string to JSON array
|
|
728
|
+
* Handles quoted fields per RFC 4180 when configured
|
|
729
|
+
* Applies row mapping and filtering when configured
|
|
730
|
+
* @param {string} parsedCsv - Raw CSV content as string
|
|
731
|
+
* @returns {Array<object>} Array of objects with CSV data
|
|
732
|
+
* @private
|
|
733
|
+
*/
|
|
734
|
+
csvToJson(parsedCsv) {
|
|
735
|
+
this.validateInputConfig();
|
|
736
|
+
|
|
737
|
+
// Parse CSV into individual records, respecting quoted fields that may contain newlines
|
|
738
|
+
let records = this.parseRecords(parsedCsv);
|
|
739
|
+
|
|
740
|
+
let fieldDelimiter = this.getFieldDelimiter();
|
|
741
|
+
let index = this.getIndexHeader();
|
|
742
|
+
let headers;
|
|
743
|
+
|
|
744
|
+
// Find the header row
|
|
745
|
+
while (index < records.length) {
|
|
746
|
+
if (this.isSupportQuotedField) {
|
|
747
|
+
headers = this.split(records[index]);
|
|
748
|
+
} else {
|
|
749
|
+
headers = records[index].split(fieldDelimiter);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
if (stringUtils.hasContent(headers)) {
|
|
753
|
+
break;
|
|
754
|
+
}
|
|
755
|
+
index++;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
if (!headers) {
|
|
759
|
+
throw CsvFormatError.missingHeader();
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
let jsonResult = [];
|
|
763
|
+
for (let i = (index + 1); i < records.length; i++) {
|
|
764
|
+
let currentLine;
|
|
765
|
+
if (this.isSupportQuotedField) {
|
|
766
|
+
currentLine = this.split(records[i]);
|
|
767
|
+
} else {
|
|
768
|
+
currentLine = records[i].split(fieldDelimiter);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
if (stringUtils.hasContent(currentLine)) {
|
|
772
|
+
let row = this.buildJsonResult(headers, currentLine);
|
|
773
|
+
|
|
774
|
+
// Apply row mapper if defined
|
|
775
|
+
if (this.rowMapper) {
|
|
776
|
+
row = this.rowMapper(row, i - (index + 1)); // Pass row and 0-based row index
|
|
777
|
+
// If mapper returns null/undefined, skip this row (allows filtering)
|
|
778
|
+
if (row != null) {
|
|
779
|
+
jsonResult.push(row);
|
|
780
|
+
}
|
|
781
|
+
} else {
|
|
782
|
+
jsonResult.push(row);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
return jsonResult;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Parse CSV content into individual records, respecting quoted fields that may span multiple lines.
|
|
791
|
+
* RFC 4180 compliant parsing - handles quoted fields that may contain newlines.
|
|
792
|
+
* @param {string} csvContent - The raw CSV content
|
|
793
|
+
* @returns {string[]} Array of record strings
|
|
794
|
+
*/
|
|
795
|
+
parseRecords(csvContent) {
|
|
796
|
+
let records = [];
|
|
797
|
+
let currentRecord = '';
|
|
798
|
+
let insideQuotes = false;
|
|
799
|
+
let i = 0;
|
|
800
|
+
|
|
801
|
+
while (i < csvContent.length) {
|
|
802
|
+
let char = csvContent[i];
|
|
803
|
+
|
|
804
|
+
// Handle quote characters
|
|
805
|
+
if (char === QUOTE_CHAR) {
|
|
806
|
+
if (insideQuotes && i + 1 < csvContent.length && csvContent[i + 1] === QUOTE_CHAR) {
|
|
807
|
+
// Escaped quote: two consecutive quotes = single quote representation
|
|
808
|
+
currentRecord += QUOTE_CHAR + QUOTE_CHAR;
|
|
809
|
+
i += 2;
|
|
810
|
+
} else {
|
|
811
|
+
// Toggle quote state
|
|
812
|
+
insideQuotes = !insideQuotes;
|
|
813
|
+
currentRecord += char;
|
|
814
|
+
i++;
|
|
815
|
+
}
|
|
816
|
+
continue;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// Handle line endings (only outside quoted fields)
|
|
820
|
+
if (!insideQuotes) {
|
|
821
|
+
let lineEndingLength = this.getLineEndingLength(csvContent, i);
|
|
822
|
+
if (lineEndingLength > 0) {
|
|
823
|
+
records.push(currentRecord);
|
|
824
|
+
currentRecord = '';
|
|
825
|
+
i += lineEndingLength;
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// Regular character
|
|
831
|
+
currentRecord += char;
|
|
832
|
+
i++;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Add the last record if not empty
|
|
836
|
+
if (currentRecord.length > 0) {
|
|
837
|
+
records.push(currentRecord);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// Validate matching quotes
|
|
841
|
+
if (insideQuotes) {
|
|
842
|
+
throw CsvFormatError.mismatchedQuotes('CSV');
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
return records;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Get the length of line ending at current position (CRLF=2, LF=1, CR=1, or 0)
|
|
850
|
+
* @param {string} content - CSV content
|
|
851
|
+
* @param {number} index - Current index to check
|
|
852
|
+
* @returns {number} Length of line ending (0 if none)
|
|
853
|
+
*/
|
|
854
|
+
getLineEndingLength(content, index) {
|
|
855
|
+
if (content.slice(index, index + 2) === CRLF) {
|
|
856
|
+
return 2;
|
|
857
|
+
}
|
|
858
|
+
if (content[index] === LF) {
|
|
859
|
+
return 1;
|
|
860
|
+
}
|
|
861
|
+
if (content[index] === CR && content[index + 1] !== LF) {
|
|
862
|
+
return 1;
|
|
863
|
+
}
|
|
864
|
+
return 0;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Get the configured field delimiter, or default if not set
|
|
869
|
+
* @returns {string} Field delimiter character
|
|
870
|
+
* @private
|
|
871
|
+
*/
|
|
872
|
+
getFieldDelimiter() {
|
|
873
|
+
if (this.delimiter) {
|
|
874
|
+
return this.delimiter;
|
|
875
|
+
}
|
|
876
|
+
return DEFAULT_FIELD_DELIMITER;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Get the configured header row index, or default (0) if not set
|
|
881
|
+
* @returns {number} Header row index
|
|
882
|
+
* @private
|
|
883
|
+
*/
|
|
884
|
+
getIndexHeader(){
|
|
885
|
+
if(this.indexHeaderValue !== null && !isNaN(this.indexHeaderValue)){
|
|
886
|
+
return this.indexHeaderValue;
|
|
887
|
+
}
|
|
888
|
+
return 0;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* Build a JSON object from headers and field values
|
|
893
|
+
* Applies type formatting and sub-array parsing as configured
|
|
894
|
+
* @param {string[]} headers - Array of header field names
|
|
895
|
+
* @param {string[]} currentLine - Array of field values
|
|
896
|
+
* @returns {object} JSON object with header names as keys
|
|
897
|
+
* @private
|
|
898
|
+
*/
|
|
899
|
+
buildJsonResult(headers, currentLine) {
|
|
900
|
+
let jsonObject = {};
|
|
901
|
+
for (let j = 0; j < headers.length; j++) {
|
|
902
|
+
let propertyName = stringUtils.trimPropertyName(this.isTrimHeaderFieldWhiteSpace, headers[j]);
|
|
903
|
+
let value = currentLine[j];
|
|
904
|
+
|
|
905
|
+
if(this.isParseSubArray(value)){
|
|
906
|
+
value = this.buildJsonSubArray(value);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
if (this.printValueFormatByType && !Array.isArray(value)) {
|
|
910
|
+
value = stringUtils.getValueFormatByType(currentLine[j]);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
jsonObject[propertyName] = value;
|
|
914
|
+
}
|
|
915
|
+
return jsonObject;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* Parse a field value into a sub-array using configured delimiter and separator
|
|
920
|
+
* @param {string} value - Field value to parse
|
|
921
|
+
* @returns {Array<string|number|boolean>} Array of parsed values
|
|
922
|
+
* @private
|
|
923
|
+
*/
|
|
924
|
+
buildJsonSubArray(value) {
|
|
925
|
+
let extractedValues = value.substring(
|
|
926
|
+
value.indexOf(this.parseSubArrayDelimiter) + 1,
|
|
927
|
+
value.lastIndexOf(this.parseSubArrayDelimiter)
|
|
928
|
+
);
|
|
929
|
+
extractedValues.trim();
|
|
930
|
+
value = extractedValues.split(this.parseSubArraySeparator);
|
|
931
|
+
if(this.printValueFormatByType){
|
|
932
|
+
for(let i=0; i < value.length; i++){
|
|
933
|
+
value[i] = stringUtils.getValueFormatByType(value[i]);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
return value;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* Check if a field value should be parsed as a sub-array
|
|
941
|
+
* @param {string} value - Field value to check
|
|
942
|
+
* @returns {boolean} True if value is bracketed with sub-array delimiter
|
|
943
|
+
* @private
|
|
944
|
+
*/
|
|
945
|
+
isParseSubArray(value){
|
|
946
|
+
if(this.parseSubArrayDelimiter){
|
|
947
|
+
if (value && (value.indexOf(this.parseSubArrayDelimiter) === 0 && value.lastIndexOf(this.parseSubArrayDelimiter) === (value.length - 1))) {
|
|
948
|
+
return true;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
return false;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* Validate configuration for conflicts and incompatibilities
|
|
956
|
+
* @throws {ConfigurationError} If incompatible options are set
|
|
957
|
+
* @private
|
|
958
|
+
*/
|
|
959
|
+
validateInputConfig(){
|
|
960
|
+
if(this.isSupportQuotedField) {
|
|
961
|
+
if(this.getFieldDelimiter() === '"'){
|
|
962
|
+
throw ConfigurationError.quotedFieldConflict('fieldDelimiter', '"');
|
|
963
|
+
}
|
|
964
|
+
if(this.parseSubArraySeparator === '"'){
|
|
965
|
+
throw ConfigurationError.quotedFieldConflict('parseSubArraySeparator', '"');
|
|
966
|
+
}
|
|
967
|
+
if(this.parseSubArrayDelimiter === '"'){
|
|
968
|
+
throw ConfigurationError.quotedFieldConflict('parseSubArrayDelimiter', '"');
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* Check if a line contains quote characters
|
|
975
|
+
* @param {string} line - Line to check
|
|
976
|
+
* @returns {boolean} True if line contains quotes
|
|
977
|
+
* @private
|
|
978
|
+
*/
|
|
979
|
+
hasQuotes(line) {
|
|
980
|
+
return line.includes('"');
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
/**
|
|
984
|
+
* Split a CSV record line into fields, respecting quoted fields per RFC 4180.
|
|
985
|
+
* Handles:
|
|
986
|
+
* - Quoted fields with embedded delimiters and newlines
|
|
987
|
+
* - Escaped quotes (double quotes within quoted fields)
|
|
988
|
+
* - Empty quoted fields
|
|
989
|
+
* @param {string} line - A single CSV record line
|
|
990
|
+
* @returns {string[]} Array of field values
|
|
991
|
+
*/
|
|
992
|
+
split(line) {
|
|
993
|
+
if (line.length === 0) {
|
|
994
|
+
return [];
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
let fields = [];
|
|
998
|
+
let currentField = '';
|
|
999
|
+
let insideQuotes = false;
|
|
1000
|
+
let delimiter = this.getFieldDelimiter();
|
|
1001
|
+
|
|
1002
|
+
for (let i = 0; i < line.length; i++) {
|
|
1003
|
+
let char = line[i];
|
|
1004
|
+
|
|
1005
|
+
// Handle quote character
|
|
1006
|
+
if (char === QUOTE_CHAR) {
|
|
1007
|
+
if (this.isEscapedQuote(line, i, insideQuotes)) {
|
|
1008
|
+
// Two consecutive quotes inside quoted field = escaped quote
|
|
1009
|
+
currentField += QUOTE_CHAR;
|
|
1010
|
+
i++; // Skip next quote
|
|
1011
|
+
} else if (this.isEmptyQuotedField(line, i, insideQuotes, currentField, delimiter)) {
|
|
1012
|
+
// Empty quoted field: "" at field start before delimiter/end
|
|
1013
|
+
i++; // Skip closing quote
|
|
1014
|
+
} else {
|
|
1015
|
+
// Regular quote: toggle quoted state
|
|
1016
|
+
insideQuotes = !insideQuotes;
|
|
1017
|
+
}
|
|
1018
|
+
} else if (char === delimiter && !insideQuotes) {
|
|
1019
|
+
// Delimiter outside quotes marks field boundary
|
|
1020
|
+
fields.push(currentField);
|
|
1021
|
+
currentField = '';
|
|
1022
|
+
} else {
|
|
1023
|
+
// Regular character (including embedded newlines in quoted fields)
|
|
1024
|
+
currentField += char;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// Add final field
|
|
1029
|
+
fields.push(currentField);
|
|
1030
|
+
|
|
1031
|
+
// Validate matching quotes
|
|
1032
|
+
if (insideQuotes) {
|
|
1033
|
+
throw CsvFormatError.mismatchedQuotes('row');
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
return fields;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/**
|
|
1040
|
+
* Check if character at index is an escaped quote (double quote)
|
|
1041
|
+
* Escaped quotes appear as "" within quoted fields per RFC 4180
|
|
1042
|
+
* @param {string} line - Line being parsed
|
|
1043
|
+
* @param {number} index - Character index to check
|
|
1044
|
+
* @param {boolean} insideQuoted - Whether currently inside a quoted field
|
|
1045
|
+
* @returns {boolean} True if character is an escaped quote
|
|
1046
|
+
* @private
|
|
1047
|
+
*/
|
|
1048
|
+
isEscapedQuote(line, index, insideQuoted) {
|
|
1049
|
+
return insideQuoted &&
|
|
1050
|
+
index + 1 < line.length &&
|
|
1051
|
+
line[index + 1] === QUOTE_CHAR;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
/**
|
|
1055
|
+
* Check if this is an empty quoted field: "" before delimiter or end of line
|
|
1056
|
+
* @param {string} line - Line being parsed
|
|
1057
|
+
* @param {number} index - Character index to check
|
|
1058
|
+
* @param {boolean} insideQuoted - Whether currently inside a quoted field
|
|
1059
|
+
* @param {string} currentField - Current field accumulation
|
|
1060
|
+
* @param {string} delimiter - Field delimiter character
|
|
1061
|
+
* @returns {boolean} True if this represents an empty quoted field
|
|
1062
|
+
* @private
|
|
1063
|
+
*/
|
|
1064
|
+
isEmptyQuotedField(line, index, insideQuoted, currentField, delimiter) {
|
|
1065
|
+
if (insideQuoted || currentField !== '' || index + 1 >= line.length) {
|
|
1066
|
+
return false;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
let nextChar = line[index + 1];
|
|
1070
|
+
if (nextChar !== QUOTE_CHAR) {
|
|
1071
|
+
return false; // Not a quote pair
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
let afterQuotes = index + 2;
|
|
1075
|
+
return afterQuotes === line.length || line[afterQuotes] === delimiter;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
module.exports = new CsvToJson();
|
|
1080
|
+
|
|
1081
|
+
},{"./util/errors":5,"./util/fileUtils":6,"./util/jsonUtils":7,"./util/stringUtils":8}],5:[function(require,module,exports){
|
|
1082
|
+
'use strict';
|
|
1083
|
+
|
|
1084
|
+
/**
|
|
1085
|
+
* Custom error classes following clean code principles
|
|
1086
|
+
* Provides clear, actionable error messages with context
|
|
1087
|
+
* @category Error Classes
|
|
1088
|
+
*/
|
|
1089
|
+
|
|
1090
|
+
/**
|
|
1091
|
+
* Base class for all CSV parsing errors
|
|
1092
|
+
* Provides consistent error formatting and context
|
|
1093
|
+
* @category Error Classes
|
|
1094
|
+
*/
|
|
1095
|
+
class CsvParsingError extends Error {
|
|
1096
|
+
/**
|
|
1097
|
+
* Create a CSV parsing error
|
|
1098
|
+
* @param {string} message - Error message
|
|
1099
|
+
* @param {string} code - Error code for identification
|
|
1100
|
+
* @param {object} context - Additional context information (default: {})
|
|
1101
|
+
*/
|
|
1102
|
+
constructor(message, code, context = {}) {
|
|
1103
|
+
super(message);
|
|
1104
|
+
this.name = 'CsvParsingError';
|
|
1105
|
+
this.code = code;
|
|
1106
|
+
this.context = context;
|
|
1107
|
+
Error.captureStackTrace(this, this.constructor);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
/**
|
|
1111
|
+
* Convert error to formatted string with context information
|
|
1112
|
+
* @returns {string} Formatted error message including context
|
|
1113
|
+
*/
|
|
1114
|
+
toString() {
|
|
1115
|
+
let output = `${this.name}: ${this.message}`;
|
|
1116
|
+
|
|
1117
|
+
if (this.context && Object.keys(this.context).length > 0) {
|
|
1118
|
+
output += '\n\nContext:';
|
|
1119
|
+
Object.entries(this.context).forEach(([key, value]) => {
|
|
1120
|
+
output += `\n ${key}: ${this.formatValue(value)}`;
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
return output;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
/**
|
|
1128
|
+
* Format a context value for display in error message
|
|
1129
|
+
* @param {unknown} value - Value to format
|
|
1130
|
+
* @returns {string} Formatted value string
|
|
1131
|
+
* @private
|
|
1132
|
+
*/
|
|
1133
|
+
formatValue(value) {
|
|
1134
|
+
if (value === null) return 'null';
|
|
1135
|
+
if (value === undefined) return 'undefined';
|
|
1136
|
+
if (typeof value === 'string') return `"${value}"`;
|
|
1137
|
+
if (typeof value === 'object') return JSON.stringify(value);
|
|
1138
|
+
return String(value);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
/**
|
|
1143
|
+
* Input validation errors
|
|
1144
|
+
* Thrown when function parameters don't meet expected type or value requirements
|
|
1145
|
+
* @category Error Classes
|
|
1146
|
+
*/
|
|
1147
|
+
class InputValidationError extends CsvParsingError {
|
|
1148
|
+
/**
|
|
1149
|
+
* Create an input validation error
|
|
1150
|
+
* @param {string} paramName - Name of the invalid parameter
|
|
1151
|
+
* @param {string} expectedType - Expected type description
|
|
1152
|
+
* @param {string} receivedType - Actual type received
|
|
1153
|
+
* @param {string} details - Additional error details (optional)
|
|
1154
|
+
*/
|
|
1155
|
+
constructor(paramName, expectedType, receivedType, details = '') {
|
|
1156
|
+
const message =
|
|
1157
|
+
`Invalid input: Parameter '${paramName}' is required.\n` +
|
|
1158
|
+
`Expected: ${expectedType}\n` +
|
|
1159
|
+
`Received: ${receivedType}${details ? '\n' + details : ''}`;
|
|
1160
|
+
|
|
1161
|
+
super(message, 'INPUT_VALIDATION_ERROR', {
|
|
1162
|
+
parameter: paramName,
|
|
1163
|
+
expectedType,
|
|
1164
|
+
receivedType
|
|
1165
|
+
});
|
|
1166
|
+
this.name = 'InputValidationError';
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
/**
|
|
1171
|
+
* Configuration-related errors
|
|
1172
|
+
* Thrown when configuration options conflict or are invalid
|
|
1173
|
+
* @category Error Classes
|
|
1174
|
+
*/
|
|
1175
|
+
class ConfigurationError extends CsvParsingError {
|
|
1176
|
+
/**
|
|
1177
|
+
* Create a configuration error
|
|
1178
|
+
* @param {string} message - Error message
|
|
1179
|
+
* @param {object} conflictingOptions - Configuration options in conflict (optional)
|
|
1180
|
+
*/
|
|
1181
|
+
constructor(message, conflictingOptions = {}) {
|
|
1182
|
+
super(message, 'CONFIGURATION_ERROR', conflictingOptions);
|
|
1183
|
+
this.name = 'ConfigurationError';
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
/**
|
|
1187
|
+
* Create error for quoted field configuration conflict
|
|
1188
|
+
* Occurs when quote character is used as delimiter while quoted fields are enabled
|
|
1189
|
+
* @param {string} optionName - Name of the conflicting option
|
|
1190
|
+
* @param {string} value - Value that causes the conflict
|
|
1191
|
+
* @returns {ConfigurationError} Configured error instance
|
|
1192
|
+
* @static
|
|
1193
|
+
*/
|
|
1194
|
+
static quotedFieldConflict(optionName, value) {
|
|
1195
|
+
return new ConfigurationError(
|
|
1196
|
+
`Configuration conflict: supportQuotedField() is enabled, but ${optionName} is set to '${value}'.\n` +
|
|
1197
|
+
`The quote character (") cannot be used as a field delimiter, separator, or sub-array delimiter when quoted field support is active.\n\n` +
|
|
1198
|
+
`Solutions:\n` +
|
|
1199
|
+
` 1. Use a different character for ${optionName} (e.g., '|', '\\t', ';')\n` +
|
|
1200
|
+
` 2. Disable supportQuotedField() if your CSV doesn't contain quoted fields\n` +
|
|
1201
|
+
` 3. Refer to RFC 4180 for proper CSV formatting: https://tools.ietf.org/html/rfc4180`,
|
|
1202
|
+
{ optionName, value, conflictingOption: 'supportQuotedField' }
|
|
1203
|
+
);
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
/**
|
|
1207
|
+
* Create error for invalid header index
|
|
1208
|
+
* Occurs when indexHeader() receives non-numeric value
|
|
1209
|
+
* @param {unknown} value - Invalid header index value
|
|
1210
|
+
* @returns {ConfigurationError} Configured error instance
|
|
1211
|
+
* @static
|
|
1212
|
+
*/
|
|
1213
|
+
static invalidHeaderIndex(value) {
|
|
1214
|
+
return new ConfigurationError(
|
|
1215
|
+
`Invalid configuration: indexHeader() expects a numeric value.\n` +
|
|
1216
|
+
`Received: ${typeof value} (${value})\n\n` +
|
|
1217
|
+
`Solutions:\n` +
|
|
1218
|
+
` 1. Ensure indexHeader() receives a number: indexHeader(0), indexHeader(1), etc.\n` +
|
|
1219
|
+
` 2. Headers are typically found on row 0 (first line)\n` +
|
|
1220
|
+
` 3. Use indexHeader(2) if headers are on the 3rd line`,
|
|
1221
|
+
{ parameterName: 'indexHeader', value, type: typeof value }
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
/**
|
|
1227
|
+
* CSV parsing errors with detailed context
|
|
1228
|
+
* Thrown when CSV format is invalid or malformed
|
|
1229
|
+
* @category Error Classes
|
|
1230
|
+
*/
|
|
1231
|
+
class CsvFormatError extends CsvParsingError {
|
|
1232
|
+
/**
|
|
1233
|
+
* Create a CSV format error
|
|
1234
|
+
* @param {string} message - Error message
|
|
1235
|
+
* @param {object} context - Additional context information (optional)
|
|
1236
|
+
*/
|
|
1237
|
+
constructor(message, context = {}) {
|
|
1238
|
+
super(message, 'CSV_FORMAT_ERROR', context);
|
|
1239
|
+
this.name = 'CsvFormatError';
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
/**
|
|
1243
|
+
* Create error for missing CSV header row
|
|
1244
|
+
* Occurs when no valid header row is found in CSV
|
|
1245
|
+
* @returns {CsvFormatError} Configured error instance
|
|
1246
|
+
* @static
|
|
1247
|
+
*/
|
|
1248
|
+
static missingHeader() {
|
|
1249
|
+
return new CsvFormatError(
|
|
1250
|
+
`CSV parsing error: No header row found.\n` +
|
|
1251
|
+
`The CSV file appears to be empty or has no valid header line.\n\n` +
|
|
1252
|
+
`Solutions:\n` +
|
|
1253
|
+
` 1. Ensure your CSV file contains at least one row (header row)\n` +
|
|
1254
|
+
` 2. Verify the file is not empty or contains only whitespace\n` +
|
|
1255
|
+
` 3. Check if you need to use indexHeader(n) to specify a non-standard header row\n` +
|
|
1256
|
+
` 4. Refer to RFC 4180 for proper CSV format: https://tools.ietf.org/html/rfc4180`
|
|
1257
|
+
);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
/**
|
|
1261
|
+
* Create error for mismatched quotes in CSV
|
|
1262
|
+
* Occurs when quoted fields are not properly closed
|
|
1263
|
+
* @param {string} location - Where the error occurred (default: 'CSV')
|
|
1264
|
+
* @returns {CsvFormatError} Configured error instance
|
|
1265
|
+
* @static
|
|
1266
|
+
*/
|
|
1267
|
+
static mismatchedQuotes(location = 'CSV') {
|
|
1268
|
+
return new CsvFormatError(
|
|
1269
|
+
`CSV parsing error: Mismatched quotes detected in ${location}.\n` +
|
|
1270
|
+
`A quoted field was not properly closed with a matching quote character.\n\n` +
|
|
1271
|
+
`RFC 4180 rules for quoted fields:\n` +
|
|
1272
|
+
` • Fields containing delimiters or quotes MUST be enclosed in double quotes\n` +
|
|
1273
|
+
` • To include a quote within a quoted field, use two consecutive quotes: ""\n` +
|
|
1274
|
+
` • Example: "Smith, John" (name contains comma)\n` +
|
|
1275
|
+
` • Example: "He said ""Hello""" (text contains quotes)\n\n` +
|
|
1276
|
+
`Solutions:\n` +
|
|
1277
|
+
` 1. Review your CSV for properly paired quote characters\n` +
|
|
1278
|
+
` 2. Use double quotes ("") to escape quotes within quoted fields\n` +
|
|
1279
|
+
` 3. Ensure all commas within field values are inside quotes\n` +
|
|
1280
|
+
` 4. Enable supportQuotedField(true) if you're using quoted fields`,
|
|
1281
|
+
{ location }
|
|
1282
|
+
);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
/**
|
|
1287
|
+
* File operation errors
|
|
1288
|
+
* Thrown when file read or write operations fail
|
|
1289
|
+
* @category Error Classes
|
|
1290
|
+
*/
|
|
1291
|
+
class FileOperationError extends CsvParsingError {
|
|
1292
|
+
/**
|
|
1293
|
+
* Create a file operation error
|
|
1294
|
+
* @param {string} operation - Type of operation that failed (e.g., 'read', 'write')
|
|
1295
|
+
* @param {string} filePath - Path to the file where operation failed
|
|
1296
|
+
* @param {Error} originalError - The underlying error object from Node.js
|
|
1297
|
+
*/
|
|
1298
|
+
constructor(operation, filePath, originalError) {
|
|
1299
|
+
const message =
|
|
1300
|
+
`File operation error: Failed to ${operation} file.\n` +
|
|
1301
|
+
`File path: ${filePath}\n` +
|
|
1302
|
+
`Reason: ${originalError.message}\n\n` +
|
|
1303
|
+
`Solutions:\n` +
|
|
1304
|
+
` 1. Verify the file path is correct: ${filePath}\n` +
|
|
1305
|
+
` 2. Check file permissions (read access for input, write access for output)\n` +
|
|
1306
|
+
` 3. Ensure the directory exists and is writable for output files\n` +
|
|
1307
|
+
` 4. Verify the file is not in use by another process`;
|
|
1308
|
+
|
|
1309
|
+
super(message, 'FILE_OPERATION_ERROR', {
|
|
1310
|
+
operation,
|
|
1311
|
+
filePath,
|
|
1312
|
+
originalError: originalError.message
|
|
1313
|
+
});
|
|
1314
|
+
this.name = 'FileOperationError';
|
|
1315
|
+
this.originalError = originalError;
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
/**
|
|
1320
|
+
* JSON validation errors
|
|
1321
|
+
* Thrown when parsed CSV data cannot be converted to valid JSON
|
|
1322
|
+
* @category Error Classes
|
|
1323
|
+
*/
|
|
1324
|
+
class JsonValidationError extends CsvParsingError {
|
|
1325
|
+
/**
|
|
1326
|
+
* Create a JSON validation error
|
|
1327
|
+
* @param {string} csvData - The CSV data that failed validation
|
|
1328
|
+
* @param {Error} originalError - The underlying JSON parsing error
|
|
1329
|
+
*/
|
|
1330
|
+
constructor(csvData, originalError) {
|
|
1331
|
+
const message =
|
|
1332
|
+
`JSON validation error: The parsed CSV data generated invalid JSON.\n` +
|
|
1333
|
+
`This typically indicates malformed field names or values in the CSV.\n` +
|
|
1334
|
+
`Original error: ${originalError.message}\n\n` +
|
|
1335
|
+
`Solutions:\n` +
|
|
1336
|
+
` 1. Check that field names are valid JavaScript identifiers (or will be converted safely)\n` +
|
|
1337
|
+
` 2. Review the CSV data for special characters that aren't properly escaped\n` +
|
|
1338
|
+
` 3. Enable supportQuotedField(true) for fields containing special characters\n` +
|
|
1339
|
+
` 4. Verify that formatValueByType() isn't converting values incorrectly`;
|
|
1340
|
+
|
|
1341
|
+
super(message, 'JSON_VALIDATION_ERROR', {
|
|
1342
|
+
originalError: originalError.message,
|
|
1343
|
+
csvPreview: csvData ? csvData.substring(0, 200) : 'N/A'
|
|
1344
|
+
});
|
|
1345
|
+
this.name = 'JsonValidationError';
|
|
1346
|
+
this.originalError = originalError;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
/**
|
|
1351
|
+
* Browser-specific errors
|
|
1352
|
+
* Thrown when browser API operations fail
|
|
1353
|
+
* @category Error Classes
|
|
1354
|
+
*/
|
|
1355
|
+
class BrowserApiError extends CsvParsingError {
|
|
1356
|
+
/**
|
|
1357
|
+
* Create a browser API error
|
|
1358
|
+
* @param {string} message - Error message
|
|
1359
|
+
* @param {object} context - Additional context information (optional)
|
|
1360
|
+
*/
|
|
1361
|
+
constructor(message, context = {}) {
|
|
1362
|
+
super(message, 'BROWSER_API_ERROR', context);
|
|
1363
|
+
this.name = 'BrowserApiError';
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
/**
|
|
1367
|
+
* Create error for unavailable FileReader API
|
|
1368
|
+
* Occurs when browser doesn't support FileReader
|
|
1369
|
+
* @returns {BrowserApiError} Configured error instance
|
|
1370
|
+
* @static
|
|
1371
|
+
*/
|
|
1372
|
+
static fileReaderNotAvailable() {
|
|
1373
|
+
return new BrowserApiError(
|
|
1374
|
+
`Browser compatibility error: FileReader API is not available.\n` +
|
|
1375
|
+
`Your browser does not support the FileReader API required for file parsing.\n\n` +
|
|
1376
|
+
`Solutions:\n` +
|
|
1377
|
+
` 1. Use a modern browser that supports FileReader (Chrome 13+, Firefox 10+, Safari 6+)\n` +
|
|
1378
|
+
` 2. Consider using csvStringToJson() or csvStringToJsonAsync() for string-based parsing\n` +
|
|
1379
|
+
` 3. Implement a polyfill or alternative file reading method`
|
|
1380
|
+
);
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
/**
|
|
1384
|
+
* Create error for file parsing failure in browser
|
|
1385
|
+
* Occurs when file read or CSV parse fails
|
|
1386
|
+
* @param {Error} originalError - The underlying error that occurred
|
|
1387
|
+
* @returns {BrowserApiError} Configured error instance
|
|
1388
|
+
* @static
|
|
1389
|
+
*/
|
|
1390
|
+
static parseFileError(originalError) {
|
|
1391
|
+
return new BrowserApiError(
|
|
1392
|
+
`Browser file parsing error: Failed to read and parse the file.\n` +
|
|
1393
|
+
`Error details: ${originalError.message}\n\n` +
|
|
1394
|
+
`Solutions:\n` +
|
|
1395
|
+
` 1. Verify the file is a valid CSV file\n` +
|
|
1396
|
+
` 2. Check the file encoding (UTF-8 is recommended)\n` +
|
|
1397
|
+
` 3. Try a smaller file to isolate the issue\n` +
|
|
1398
|
+
` 4. Check browser console for additional error details`,
|
|
1399
|
+
{ originalError: originalError.message }
|
|
1400
|
+
);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
module.exports = {
|
|
1405
|
+
CsvParsingError,
|
|
1406
|
+
InputValidationError,
|
|
1407
|
+
ConfigurationError,
|
|
1408
|
+
CsvFormatError,
|
|
1409
|
+
FileOperationError,
|
|
1410
|
+
JsonValidationError,
|
|
1411
|
+
BrowserApiError
|
|
1412
|
+
};
|
|
1413
|
+
|
|
1414
|
+
},{}],6:[function(require,module,exports){
|
|
1415
|
+
'use strict';
|
|
1416
|
+
|
|
1417
|
+
const fs = require('fs');
|
|
1418
|
+
const { FileOperationError } = require('./errors');
|
|
1419
|
+
|
|
1420
|
+
/**
|
|
1421
|
+
* File I/O utilities for reading and writing CSV/JSON files
|
|
1422
|
+
* Provides both synchronous and asynchronous file operations
|
|
1423
|
+
* @category Utilities
|
|
1424
|
+
*/
|
|
1425
|
+
class FileUtils {
|
|
1426
|
+
|
|
1427
|
+
/**
|
|
1428
|
+
* Read a file synchronously with specified encoding
|
|
1429
|
+
* @param {string} fileInputName - Path to file to read
|
|
1430
|
+
* @param {string} encoding - File encoding (e.g., 'utf8', 'latin1')
|
|
1431
|
+
* @returns {string} File contents as string
|
|
1432
|
+
* @throws {FileOperationError} If file read fails
|
|
1433
|
+
*/
|
|
1434
|
+
readFile(fileInputName, encoding) {
|
|
1435
|
+
try {
|
|
1436
|
+
return fs.readFileSync(fileInputName, encoding).toString();
|
|
1437
|
+
} catch (error) {
|
|
1438
|
+
throw new FileOperationError('read', fileInputName, error);
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
/**
|
|
1443
|
+
* Read a file asynchronously with specified encoding
|
|
1444
|
+
* Uses fs.promises when available, falls back to callback-based API
|
|
1445
|
+
* @param {string} fileInputName - Path to file to read
|
|
1446
|
+
* @param {string} encoding - File encoding (default: 'utf8')
|
|
1447
|
+
* @returns {Promise<string>} Promise resolving to file contents
|
|
1448
|
+
* @throws {FileOperationError} If file read fails
|
|
1449
|
+
*/
|
|
1450
|
+
readFileAsync(fileInputName, encoding = 'utf8') {
|
|
1451
|
+
// Use fs.promises when available for a Promise-based API
|
|
1452
|
+
if (fs.promises && typeof fs.promises.readFile === 'function') {
|
|
1453
|
+
return fs.promises.readFile(fileInputName, encoding)
|
|
1454
|
+
.then(buf => buf.toString())
|
|
1455
|
+
.catch(error => {
|
|
1456
|
+
throw new FileOperationError('read', fileInputName, error);
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
return new Promise((resolve, reject) => {
|
|
1460
|
+
fs.readFile(fileInputName, encoding, (err, data) => {
|
|
1461
|
+
if (err) {
|
|
1462
|
+
reject(new FileOperationError('read', fileInputName, err));
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
resolve(data.toString());
|
|
1466
|
+
});
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
/**
|
|
1471
|
+
* Write content to a file synchronously
|
|
1472
|
+
* Logs confirmation message to console on success
|
|
1473
|
+
* @param {string} json - Content to write to file
|
|
1474
|
+
* @param {string} fileOutputName - Path to output file
|
|
1475
|
+
* @throws {FileOperationError} If file write fails
|
|
1476
|
+
*/
|
|
1477
|
+
writeFile(json, fileOutputName) {
|
|
1478
|
+
fs.writeFile(fileOutputName, json, function (err) {
|
|
1479
|
+
if (err) {
|
|
1480
|
+
throw new FileOperationError('write', fileOutputName, err);
|
|
1481
|
+
} else {
|
|
1482
|
+
console.log('File saved: ' + fileOutputName);
|
|
1483
|
+
}
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
/**
|
|
1488
|
+
* Write content to a file asynchronously
|
|
1489
|
+
* Uses fs.promises when available, falls back to callback-based API
|
|
1490
|
+
* @param {string} json - Content to write to file
|
|
1491
|
+
* @param {string} fileOutputName - Path to output file
|
|
1492
|
+
* @returns {Promise<void>} Promise that resolves when write completes
|
|
1493
|
+
* @throws {FileOperationError} If file write fails
|
|
1494
|
+
*/
|
|
1495
|
+
writeFileAsync(json, fileOutputName) {
|
|
1496
|
+
if (fs.promises && typeof fs.promises.writeFile === 'function') {
|
|
1497
|
+
return fs.promises.writeFile(fileOutputName, json)
|
|
1498
|
+
.catch(error => {
|
|
1499
|
+
throw new FileOperationError('write', fileOutputName, error);
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1502
|
+
return new Promise((resolve, reject) => {
|
|
1503
|
+
fs.writeFile(fileOutputName, json, (err) => {
|
|
1504
|
+
if (err) return reject(new FileOperationError('write', fileOutputName, err));
|
|
1505
|
+
resolve();
|
|
1506
|
+
});
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
}
|
|
1511
|
+
module.exports = new FileUtils();
|
|
1512
|
+
|
|
1513
|
+
},{"./errors":5,"fs":2}],7:[function(require,module,exports){
|
|
1514
|
+
'use strict';
|
|
1515
|
+
|
|
1516
|
+
const { JsonValidationError } = require('./errors');
|
|
1517
|
+
|
|
1518
|
+
/**
|
|
1519
|
+
* JSON validation utilities
|
|
1520
|
+
* @category Utilities
|
|
1521
|
+
*/
|
|
1522
|
+
class JsonUtil {
|
|
1523
|
+
|
|
1524
|
+
/**
|
|
1525
|
+
* Validate that a string is valid JSON
|
|
1526
|
+
* @param {string} json - JSON string to validate
|
|
1527
|
+
* @throws {JsonValidationError} If JSON is invalid
|
|
1528
|
+
*/
|
|
1529
|
+
validateJson(json) {
|
|
1530
|
+
try {
|
|
1531
|
+
JSON.parse(json);
|
|
1532
|
+
} catch (err) {
|
|
1533
|
+
throw new JsonValidationError(json, err);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
module.exports = new JsonUtil();
|
|
1540
|
+
},{"./errors":5}],8:[function(require,module,exports){
|
|
1541
|
+
'use strict';
|
|
1542
|
+
|
|
1543
|
+
/**
|
|
1544
|
+
* String processing utilities for CSV parsing
|
|
1545
|
+
* @category Utilities
|
|
1546
|
+
*/
|
|
1547
|
+
class StringUtils {
|
|
1548
|
+
// Regular expressions as constants for better maintainability
|
|
1549
|
+
static PATTERNS = {
|
|
1550
|
+
INTEGER: /^-?\d+$/,
|
|
1551
|
+
FLOAT: /^-?\d*\.\d+$/,
|
|
1552
|
+
WHITESPACE: /\s/g
|
|
1553
|
+
};
|
|
1554
|
+
|
|
1555
|
+
static BOOLEAN_VALUES = {
|
|
1556
|
+
TRUE: 'true',
|
|
1557
|
+
FALSE: 'false'
|
|
1558
|
+
};
|
|
1559
|
+
|
|
1560
|
+
/**
|
|
1561
|
+
* Removes whitespace from property names based on configuration
|
|
1562
|
+
* @param {boolean} shouldTrimAll - If true, removes all whitespace, otherwise only trims edges
|
|
1563
|
+
* @param {string} propertyName - The property name to process
|
|
1564
|
+
* @returns {string} The processed property name
|
|
1565
|
+
*/
|
|
1566
|
+
trimPropertyName(shouldTrimAll, propertyName) {
|
|
1567
|
+
if (!propertyName) {
|
|
1568
|
+
return '';
|
|
1569
|
+
}
|
|
1570
|
+
return shouldTrimAll ?
|
|
1571
|
+
propertyName.replace(StringUtils.PATTERNS.WHITESPACE, '') :
|
|
1572
|
+
propertyName.trim();
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
/**
|
|
1576
|
+
* Converts a string value to its appropriate type while preserving data integrity
|
|
1577
|
+
* @param {string} value - The input value to convert
|
|
1578
|
+
* @returns {string|number|boolean} The converted value
|
|
1579
|
+
*/
|
|
1580
|
+
getValueFormatByType(value) {
|
|
1581
|
+
if (this.isEmpty(value)) {
|
|
1582
|
+
return String();
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
if (this.isBoolean(value)) {
|
|
1586
|
+
return this.convertToBoolean(value);
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
if (this.isInteger(value)) {
|
|
1590
|
+
return this.convertInteger(value);
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
if (this.isFloat(value)) {
|
|
1594
|
+
return this.convertFloat(value);
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
return String(value);
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
/**
|
|
1601
|
+
* Checks if a value array contains any non-empty values
|
|
1602
|
+
* @param {Array} values - Array to check for content
|
|
1603
|
+
* @returns {boolean} True if array has any non-empty values
|
|
1604
|
+
*/
|
|
1605
|
+
hasContent(values = []) {
|
|
1606
|
+
return Array.isArray(values) &&
|
|
1607
|
+
values.some(value => Boolean(value));
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
// Private helper methods for type checking and conversion
|
|
1611
|
+
/**
|
|
1612
|
+
* Check if a value is empty (undefined or empty string)
|
|
1613
|
+
* @param {unknown} value - Value to check
|
|
1614
|
+
* @returns {boolean} True if value is undefined or empty string
|
|
1615
|
+
* @private
|
|
1616
|
+
*/
|
|
1617
|
+
isEmpty(value) {
|
|
1618
|
+
return value === undefined || value === '';
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
/**
|
|
1622
|
+
* Check if a value is a boolean string ('true' or 'false', case-insensitive)
|
|
1623
|
+
* @param {string} value - Value to check
|
|
1624
|
+
* @returns {boolean} True if value is 'true' or 'false'
|
|
1625
|
+
* @private
|
|
1626
|
+
*/
|
|
1627
|
+
isBoolean(value) {
|
|
1628
|
+
const normalizedValue = value.toLowerCase();
|
|
1629
|
+
return normalizedValue === StringUtils.BOOLEAN_VALUES.TRUE ||
|
|
1630
|
+
normalizedValue === StringUtils.BOOLEAN_VALUES.FALSE;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
/**
|
|
1634
|
+
* Check if a value is an integer string (with optional leading minus sign)
|
|
1635
|
+
* @param {string} value - Value to check
|
|
1636
|
+
* @returns {boolean} True if value matches integer pattern
|
|
1637
|
+
* @private
|
|
1638
|
+
*/
|
|
1639
|
+
isInteger(value) {
|
|
1640
|
+
return StringUtils.PATTERNS.INTEGER.test(value);
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
/**
|
|
1644
|
+
* Check if a value is a float string (decimal number with optional leading minus sign)
|
|
1645
|
+
* @param {string} value - Value to check
|
|
1646
|
+
* @returns {boolean} True if value matches float pattern
|
|
1647
|
+
* @private
|
|
1648
|
+
*/
|
|
1649
|
+
isFloat(value) {
|
|
1650
|
+
return StringUtils.PATTERNS.FLOAT.test(value);
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
/**
|
|
1654
|
+
* Check if a numeric string has a leading zero (e.g., '01' or '-01')
|
|
1655
|
+
* Leading zeros indicate the value should be kept as a string to preserve formatting
|
|
1656
|
+
* @param {string} value - Numeric string value to check
|
|
1657
|
+
* @returns {boolean} True if value has a leading zero
|
|
1658
|
+
* @private
|
|
1659
|
+
*/
|
|
1660
|
+
hasLeadingZero(value) {
|
|
1661
|
+
const isPositiveWithLeadingZero = value.length > 1 && value[0] === '0';
|
|
1662
|
+
const isNegativeWithLeadingZero = value.length > 2 && value[0] === '-' && value[1] === '0';
|
|
1663
|
+
return isPositiveWithLeadingZero || isNegativeWithLeadingZero;
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
/**
|
|
1667
|
+
* Convert a boolean string to native boolean value
|
|
1668
|
+
* Safely converts 'true' to true and 'false' to false
|
|
1669
|
+
* @param {string} value - Boolean string ('true' or 'false')
|
|
1670
|
+
* @returns {boolean} Native boolean value
|
|
1671
|
+
* @private
|
|
1672
|
+
*/
|
|
1673
|
+
convertToBoolean(value) {
|
|
1674
|
+
return JSON.parse(value.toLowerCase());
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
/**
|
|
1678
|
+
* Convert an integer string to number or keep as string if it has leading zeros
|
|
1679
|
+
* Preserves leading zeros in strings (e.g., '007' stays as string)
|
|
1680
|
+
* @param {string} value - Integer string to convert
|
|
1681
|
+
* @returns {number|string} Number if safe, otherwise string value
|
|
1682
|
+
* @private
|
|
1683
|
+
*/
|
|
1684
|
+
convertInteger(value) {
|
|
1685
|
+
if (this.hasLeadingZero(value)) {
|
|
1686
|
+
return String(value);
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
const num = Number(value);
|
|
1690
|
+
return Number.isSafeInteger(num) ? num : String(value);
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
/**
|
|
1694
|
+
* Convert a float string to number or keep as string if conversion is unsafe
|
|
1695
|
+
* @param {string} value - Float string to convert
|
|
1696
|
+
* @returns {number|string} Number if finite and valid, otherwise string value
|
|
1697
|
+
* @private
|
|
1698
|
+
*/
|
|
1699
|
+
convertFloat(value) {
|
|
1700
|
+
const num = Number(value);
|
|
1701
|
+
return Number.isFinite(num) ? num : String(value);
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
module.exports = new StringUtils();
|
|
1706
|
+
|
|
1707
|
+
},{}]},{},[1])(1)
|
|
1708
|
+
});
|