Skip to content

Commit 3998ab8

Browse files
committed
fix(inspector): make the CSV doubled-quote escape track the selected quote character
1 parent 0d8c150 commit 3998ab8

4 files changed

Lines changed: 39 additions & 3 deletions

File tree

Plugins/TableProPluginKit/CSVDialect.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public struct CSVDialect: Equatable, Sendable {
1717

1818
public var delimiter: UInt8
1919
public var quoteChar: UInt8
20-
public var escapeChar: UInt8 = 0x22
20+
public var escapeChar: UInt8
2121
public var encoding: String.Encoding
2222
public var lineEnding: LineEnding
2323
public var hasBom: Bool
@@ -31,6 +31,7 @@ public struct CSVDialect: Equatable, Sendable {
3131
) {
3232
self.delimiter = delimiter
3333
self.quoteChar = quoteChar
34+
self.escapeChar = quoteChar
3435
self.encoding = encoding
3536
self.lineEnding = lineEnding
3637
self.hasBom = hasBom

TablePro/Views/Inspector/CSVPropertyOptions.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ enum CSVPropertyOptions {
2626
(String(localized: "Backslash \\"), 0x5C),
2727
]
2828

29+
static let backslashEscapeIndex = 1
30+
2931
static let encodings: [(label: String, encoding: String.Encoding)] = [
3032
("UTF-8", .utf8),
3133
("UTF-16 LE", .utf16LittleEndian),
@@ -49,7 +51,7 @@ enum CSVPropertyOptions {
4951
}
5052

5153
static func escapeIndex(for byte: UInt8) -> Int {
52-
escapes.firstIndex { $0.byte == byte } ?? 0
54+
byte == escapes[backslashEscapeIndex].byte ? backslashEscapeIndex : 0
5355
}
5456

5557
static func encodingIndex(for encoding: String.Encoding) -> Int {
@@ -75,7 +77,9 @@ enum CSVPropertyOptions {
7577
lineEnding: lineEndings.indices.contains(lineEndingIndex) ? lineEndings[lineEndingIndex].value : base.lineEnding,
7678
hasBom: base.hasBom
7779
)
78-
dialect.escapeChar = escapes.indices.contains(escapeIndex) ? escapes[escapeIndex].byte : base.escapeChar
80+
dialect.escapeChar = escapeIndex == backslashEscapeIndex
81+
? escapes[backslashEscapeIndex].byte
82+
: dialect.quoteChar
7983
return dialect
8084
}
8185
}

TableProTests/Plugins/CSVInspectorTests.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ struct CSVDialectDetectionTests {
8484
let detected = CSVDialect.detect(from: "a,b\n1,2\n".data(using: .utf8)!)
8585
#expect(detected.escapeChar == 0x22)
8686
}
87+
88+
@Test("Escape character follows a non-default quote character")
89+
func escapeFollowsQuote() {
90+
#expect(CSVDialect(delimiter: 0x2C, quoteChar: 0x27).escapeChar == 0x27)
91+
}
8792
}
8893

8994
@Suite("CSVStreamingParser")
@@ -159,6 +164,18 @@ struct CSVStreamingParserTests {
159164
#expect(fields == [#"a\b"#])
160165
}
161166

167+
@Test("field(at:column:) honors the backslash escape inside a quoted field")
168+
func fieldBackslashEscape() {
169+
var dialect = CSVDialect.csv
170+
dialect.escapeChar = 0x5C
171+
let (data, ranges, parser) = parse(#""a\"b",c"# + "\n", dialect: dialect)
172+
let first = data.withUnsafeBytes { raw -> String in
173+
guard let base = raw.bindMemory(to: UInt8.self).baseAddress else { return "" }
174+
return parser.field(UnsafeBufferPointer(start: base, count: raw.count), range: ranges[0], column: 0)
175+
}
176+
#expect(first == #"a"b"#)
177+
}
178+
162179
@Test("Empty fields preserved")
163180
func emptyFields() {
164181
let (data, ranges, parser) = parse(",,,\n")

TableProTests/Views/CSVPropertyOptionsTests.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,18 @@ struct CSVPropertyOptionsTests {
4040
#expect(dialect.lineEnding == .cr)
4141
#expect(dialect.hasBom)
4242
}
43+
44+
@Test("The doubled-quote escape follows the selected quote character")
45+
func doubledQuoteEscapeTracksQuote() {
46+
let dialect = CSVPropertyOptions.dialect(
47+
base: CSVDialect(delimiter: 0x2C),
48+
delimiterIndex: 0,
49+
quoteIndex: CSVPropertyOptions.quoteIndex(for: 0x27),
50+
escapeIndex: 0,
51+
encodingIndex: 0,
52+
lineEndingIndex: 0
53+
)
54+
#expect(dialect.quoteChar == 0x27)
55+
#expect(dialect.escapeChar == 0x27)
56+
}
4357
}

0 commit comments

Comments
 (0)