diff --git a/.run/Run Examples.run.xml b/.run/Run Examples.run.xml
new file mode 100644
index 00000000..8a988a7d
--- /dev/null
+++ b/.run/Run Examples.run.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.run/Serve Docs.run.xml b/.run/Serve Docs.run.xml
new file mode 100644
index 00000000..ae74dbff
--- /dev/null
+++ b/.run/Serve Docs.run.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.run/jest.config.js.run.xml b/.run/jest.config.js.run.xml
new file mode 100644
index 00000000..e5707ddf
--- /dev/null
+++ b/.run/jest.config.js.run.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/parse/__tests__/issues/issue340.spec.ts b/packages/parse/__tests__/issues/issue340.spec.ts
new file mode 100644
index 00000000..cf1cb092
--- /dev/null
+++ b/packages/parse/__tests__/issues/issue340.spec.ts
@@ -0,0 +1,22 @@
+import { EOL } from 'os';
+import { parseString, RowMap, RowArray } from '../../src';
+
+describe('Issue #340 - https://github.com/C2FO/fast-csv/issues/340', () => {
+ const CSV_CONTENT = ['Col1', `"A '"Good'" row''"`, 'Row 2'].join(EOL);
+ const expectedRows = [{ Col1: `A "Good" row'` }, { Col1: 'Row 2' }];
+
+ it('handle a trailing escape', (done) => {
+ const invalid: RowArray[] = [];
+ const rows: RowMap[] = [];
+ parseString(CSV_CONTENT, { headers: true, escape: "'" })
+ .on('data-invalid', (row: RowArray) => invalid.push(row))
+ .on('data', (r: RowMap) => rows.push(r))
+ .on('error', done)
+ .on('end', (count: number) => {
+ expect(rows).toEqual(expectedRows);
+ expect(invalid).toHaveLength(0);
+ expect(count).toBe(expectedRows.length + invalid.length);
+ done();
+ });
+ });
+});
diff --git a/packages/parse/__tests__/parser/column/QuotedColumnParser.spec.ts b/packages/parse/__tests__/parser/column/QuotedColumnParser.spec.ts
index 6a152d4a..9a9c730d 100644
--- a/packages/parse/__tests__/parser/column/QuotedColumnParser.spec.ts
+++ b/packages/parse/__tests__/parser/column/QuotedColumnParser.spec.ts
@@ -481,6 +481,13 @@ describe('QuotedColumnParser', () => {
expect(scanner.lineFromCursor).toBe(',"world"');
});
+ it('should parse an escape followed by another escape', () => {
+ const line = '"hello$$","world"';
+ const { scanner, col } = parse(line, true, { escape: '$' });
+ expect(col).toBe('hello$');
+ expect(scanner.lineFromCursor).toBe(',"world"');
+ });
+
it('should parse a quoted col up to a LF', () => {
const line = '"hello"\n"world"';
const { scanner, col } = parse(line, true, { escape: '$' });
diff --git a/packages/parse/src/parser/column/QuotedColumnParser.ts b/packages/parse/src/parser/column/QuotedColumnParser.ts
index b265c417..8554d418 100644
--- a/packages/parse/src/parser/column/QuotedColumnParser.ts
+++ b/packages/parse/src/parser/column/QuotedColumnParser.ts
@@ -35,7 +35,7 @@ export class QuotedColumnParser {
throw new Error(
`Parse Error: missing closing: '${
this.parserOptions.quote
- }' in line: at '${scanner.lineFromCursor.replace(/[r\n]/g, "\\n'")}'`,
+ }' in line: at '${scanner.lineFromCursor.replace(/[\r\n]/g, "\\n'")}'`,
);
}
return null;
@@ -62,7 +62,11 @@ export class QuotedColumnParser {
const tokenFollowingEscape = scanner.nextCharacterToken;
// if the character following the escape is a quote character then just add
// the quote and advance to that character
- if (tokenFollowingEscape !== null && isTokenQuote(tokenFollowingEscape, parserOptions)) {
+ if (
+ tokenFollowingEscape !== null &&
+ (isTokenQuote(tokenFollowingEscape, parserOptions) ||
+ isTokenEscapeCharacter(tokenFollowingEscape, parserOptions))
+ ) {
characters.push(tokenFollowingEscape.token);
nextToken = tokenFollowingEscape;
} else if (isQuote) {