Skip to content

Commit f470a77

Browse files
committed
Fix ignoring of separator within quotes
1 parent 5f78a71 commit f470a77

File tree

3 files changed

+127
-28
lines changed

3 files changed

+127
-28
lines changed

src/index.test.ts

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -130,36 +130,71 @@ it('accepts custom indentation', () => {
130130
);
131131
});
132132

133-
it('accepts CSV with quotes', () => {
133+
describe('accepts CSV with quotes', () => {
134134
const csv = `color,maxSpeed,age
135+
"red, yellow",'120',2`;
136+
137+
const csv2 = `color,maxSpeed,age
135138
"red",'120',2`;
136-
const xml = csvToXml(csv, { quotes: 'double' });
137-
expect(xml).toBe(
138-
`<row>
139-
<color>red</color>
139+
it('double', () => {
140+
const xml = csvToXml(csv, { quote: 'double' });
141+
expect(xml).toBe(
142+
`<row>
143+
<color>red, yellow</color>
140144
<maxSpeed>'120'</maxSpeed>
141145
<age>2</age>
142146
</row>
143147
`
144-
);
148+
);
149+
});
145150

146-
const xml2 = csvToXml(csv, { quotes: 'single' });
147-
expect(xml2).toBe(
148-
`<row>
151+
it('single', () => {
152+
const xml2 = csvToXml(csv2, { quote: 'single' });
153+
expect(xml2).toBe(
154+
`<row>
149155
<color>"red"</color>
150156
<maxSpeed>120</maxSpeed>
151157
<age>2</age>
152158
</row>
153159
`
154-
);
160+
);
161+
});
155162

156-
const xml3 = csvToXml(csv, { quotes: 'none' });
157-
expect(xml3).toBe(
158-
`<row>
163+
it('none', () => {
164+
const xml3 = csvToXml(csv2, { quote: 'none' });
165+
expect(xml3).toBe(
166+
`<row>
159167
<color>"red"</color>
160168
<maxSpeed>'120'</maxSpeed>
161169
<age>2</age>
162170
</row>
163171
`
164-
);
172+
);
173+
});
174+
175+
it('all', () => {
176+
const xml4 = csvToXml(csv2, { quote: 'all' });
177+
expect(xml4).toBe(
178+
`<row>
179+
<color>red</color>
180+
<maxSpeed>120</maxSpeed>
181+
<age>2</age>
182+
</row>
183+
`
184+
);
185+
});
186+
187+
it('custom regex', () => {
188+
const csv5 = `color,maxSpeed,age
189+
(red, yellow),'120',2`;
190+
const xml5 = csvToXml(csv5, { quote: /[\(|\)]/ });
191+
expect(xml5).toBe(
192+
`<row>
193+
<color>red, yellow</color>
194+
<maxSpeed>'120'</maxSpeed>
195+
<age>2</age>
196+
</row>
197+
`
198+
);
199+
});
165200
});

src/index.ts

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import detectEOL from 'detect-eol';
2+
import { tryRegexToString, splitIgnoringChar } from './utils';
23

34
type CSVToXMLOptions = {
45
eol?: string;
@@ -7,7 +8,7 @@ type CSVToXMLOptions = {
78
headerList?: string[];
89
header?: boolean;
910
indentation?: number | string;
10-
quotes?: 'single' | 'double' | 'none';
11+
quote?: 'single' | 'double' | 'all' | 'none' | RegExp;
1112
};
1213

1314
const defaultOptions: CSVToXMLOptions = {
@@ -17,7 +18,7 @@ const defaultOptions: CSVToXMLOptions = {
1718
header: true,
1819
headerList: [],
1920
indentation: 4,
20-
quotes: 'none',
21+
quote: 'none',
2122
};
2223

2324
export default function csvToXml(
@@ -29,12 +30,35 @@ export default function csvToXml(
2930
const csvData = csvString.split(eol!).map((row) => row.trim());
3031
const separator = usedOptions.separator!;
3132

32-
const firstRow = csvData[0].split(separator);
33+
let quote;
34+
if (usedOptions.quote instanceof RegExp) {
35+
quote = usedOptions.quote;
36+
} else if (usedOptions.quote) {
37+
quote = usedOptions.quote;
38+
quote = { single: "'", double: '"', all: /(?:'|")/, none: undefined }[
39+
usedOptions.quote
40+
];
41+
}
42+
43+
const split = (str: string) => {
44+
return splitIgnoringChar({ str, separator, charToIgnore: quote }).map(
45+
(ch) =>
46+
ch.replace(
47+
new RegExp(
48+
String.raw`${tryRegexToString(quote) ?? ''}(.*?)${
49+
tryRegexToString(quote) ?? ''
50+
}`
51+
),
52+
'$1'
53+
)
54+
);
55+
};
56+
57+
const firstRow = split(csvData[0]);
58+
3359
const colCount = firstRow.length;
3460

35-
const foundHeaders = usedOptions.header
36-
? csvData[0].split(separator)
37-
: [...Array(colCount)];
61+
const foundHeaders = usedOptions.header ? firstRow : [...Array(colCount)];
3862
const specifiedHeaders: (string | undefined)[] = [...usedOptions.headerList!];
3963

4064
const usedHeaders: string[] = [];
@@ -69,7 +93,7 @@ export default function csvToXml(
6993
: usedOptions.indentation;
7094

7195
for (let i = rowStartLine; i < csvData.length; i++) {
72-
const details = csvData[i].split(separator);
96+
const details = split(csvData[i]);
7397

7498
if (details.length < colCount) {
7599
warn('rows found without enough columns.');
@@ -79,13 +103,6 @@ export default function csvToXml(
79103
xml += `<${usedOptions.rowName}>\n`;
80104
for (let j = 0; j < colCount; j++) {
81105
let colValue = details[j];
82-
if (!usedOptions.quotes || usedOptions.quotes !== 'none') {
83-
const quoteRemovingRegex = {
84-
double: /"(.*?)"/,
85-
single: /'(.*?)'/,
86-
}[usedOptions.quotes!];
87-
colValue = colValue.replace(quoteRemovingRegex, '$1');
88-
}
89106
xml += `${spaces}<${usedHeaders[j]}>${colValue}</${usedHeaders[j]}>\n`;
90107
}
91108
xml += `</${usedOptions.rowName}>\n`;

src/utils.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
export function splitIgnoringChar(params: {
2+
str: string;
3+
separator: string;
4+
charToIgnore: string | RegExp;
5+
}) {
6+
const { str, separator, charToIgnore } = params;
7+
8+
let result: string[] = [];
9+
let awaitingEndQuote = false;
10+
let accElement = '';
11+
12+
for (let i = 0; i < str.length; i++) {
13+
const ch = str[i];
14+
15+
let isCharToIgnore = false;
16+
if (charToIgnore instanceof RegExp) {
17+
isCharToIgnore = charToIgnore.test(ch);
18+
} else if (typeof charToIgnore === 'string') {
19+
isCharToIgnore = charToIgnore === ch;
20+
}
21+
22+
if (isCharToIgnore) {
23+
awaitingEndQuote = !awaitingEndQuote;
24+
}
25+
if (ch === separator) {
26+
if (!awaitingEndQuote) {
27+
result.push(accElement);
28+
accElement = '';
29+
continue;
30+
}
31+
}
32+
accElement += ch;
33+
}
34+
result.push(accElement);
35+
36+
return result;
37+
}
38+
39+
export function tryRegexToString(
40+
regexOrString?: string | RegExp
41+
): string | undefined {
42+
if (typeof regexOrString === 'string') {
43+
return regexOrString;
44+
} else if (regexOrString instanceof RegExp) {
45+
return regexOrString?.toString()?.slice(1, -1);
46+
} else return undefined;
47+
}

0 commit comments

Comments
 (0)