Skip to content

Commit 2dc58c4

Browse files
committed
feat: support full Gherkin table cell escaping (\\, \|, \n)
Implement the complete escape logic per the Cucumber spec: - \\ → literal backslash - \| → literal pipe (not a cell delimiter) - \n → newline character Fix \\| being incorrectly treated as escaped pipe instead of escaped backslash followed by a cell delimiter.
1 parent 7cde50f commit 2dc58c4

3 files changed

Lines changed: 163 additions & 20 deletions

File tree

TickSpec.Tests/EscapedPipe.feature

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Feature: Escaped pipe in table cells
1+
Feature: Escaped characters in table cells
22

33
Scenario Outline: Values with escaped pipes are parsed correctly
44
Given a value <value>
@@ -12,6 +12,22 @@ Feature: Escaped pipe in table cells
1212
| trailing\| | trailing\| |
1313
| a\|b\|c | a\|b\|c |
1414

15+
Scenario Outline: Values with escaped backslashes are parsed correctly
16+
Given a value <value>
17+
Then the value is <expected>
18+
19+
Examples:
20+
| value | expected |
21+
| hello\\world | hello\\world |
22+
23+
Scenario Outline: Values with escaped newlines are parsed correctly
24+
Given a value <value>
25+
Then the value is <expected>
26+
27+
Examples:
28+
| value | expected |
29+
| line1\nline2 | line1\nline2 |
30+
1531
Scenario: Step table with escaped pipes
1632
Given a table with escaped pipes
1733
| col1 | col2 |
@@ -20,3 +36,32 @@ Feature: Escaped pipe in table cells
2036
Then the table cell 0,0 is hello|world
2137
And the table cell 0,1 is normal
2238
And the table cell 1,0 is a|b|c
39+
40+
Scenario: Step table with escaped backslashes
41+
Given a table with escaped backslashes
42+
| col1 | col2 |
43+
| hello\\world | normal |
44+
Then the table cell 0,0 is hello\world
45+
And the table cell 0,1 is normal
46+
47+
Scenario: Escaped backslash before pipe acts as cell delimiter
48+
Given a table with escaped backslash before pipe
49+
| col1 | col2 | col3 |
50+
| before\\| after | end |
51+
Then the table cell 0,0 is before\
52+
And the table cell 0,1 is after
53+
And the table cell 0,2 is end
54+
55+
Scenario: Triple backslash-pipe is escaped backslash then escaped pipe
56+
Given a table with triple backslash-pipe
57+
| col1 | col2 |
58+
| a\\\|b | normal |
59+
Then the table cell 0,0 is a\|b
60+
And the table cell 0,1 is normal
61+
62+
Scenario: Escaped backslash before n is literal backslash-n not newline
63+
Given a table with escaped backslash before n
64+
| col1 | col2 |
65+
| test\\n | normal |
66+
Then the table cell 0,0 is test\n
67+
And the table cell 0,1 is normal

TickSpec.Tests/FeatureParserTest.fs

Lines changed: 103 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -607,13 +607,15 @@ let RuleKeyword_ParseBlocks () =
607607
Assert.AreEqual(1, secondRule.Scenarios.Length) // One scenario in second rule
608608

609609
[<Test>]
610-
let EscapedPipe_ParseLines () =
610+
let EscapedCharacters_ParseLines () =
611611
"TickSpec.Tests.EscapedPipe.feature"
612612
|> loadFeatureFile
613613
|> verifyLineParsing <|
614614
[
615615
FileStart
616-
FeatureName "Escaped pipe in table cells"
616+
FeatureName "Escaped characters in table cells"
617+
618+
// Scenario Outline: escaped pipes
617619
Scenario "Scenario Outline: Values with escaped pipes are parsed correctly"
618620
Step (GivenStep "a value <value>")
619621
Step (ThenStep "the value is <expected>")
@@ -624,6 +626,24 @@ let EscapedPipe_ParseLines () =
624626
Item (Examples, TableRow [ "|leading"; "|leading" ])
625627
Item (Examples, TableRow [ "trailing|"; "trailing|" ])
626628
Item (Examples, TableRow [ "a|b|c"; "a|b|c" ])
629+
630+
// Scenario Outline: escaped backslashes
631+
Scenario "Scenario Outline: Values with escaped backslashes are parsed correctly"
632+
Step (GivenStep "a value <value>")
633+
Step (ThenStep "the value is <expected>")
634+
Examples
635+
Item (Examples, TableRow [ "value"; "expected" ])
636+
Item (Examples, TableRow [ "hello\\world"; "hello\\world" ])
637+
638+
// Scenario Outline: escaped newlines
639+
Scenario "Scenario Outline: Values with escaped newlines are parsed correctly"
640+
Step (GivenStep "a value <value>")
641+
Step (ThenStep "the value is <expected>")
642+
Examples
643+
Item (Examples, TableRow [ "value"; "expected" ])
644+
Item (Examples, TableRow [ "line1\nline2"; "line1\nline2" ])
645+
646+
// Scenario: step table with escaped pipes
627647
Scenario "Scenario: Step table with escaped pipes"
628648
Step (GivenStep "a table with escaped pipes")
629649
Item (Step (GivenStep "a table with escaped pipes"), TableRow [ "col1"; "col2" ])
@@ -632,20 +652,51 @@ let EscapedPipe_ParseLines () =
632652
Step (ThenStep "the table cell 0,0 is hello|world")
633653
Step (ThenStep "the table cell 0,1 is normal")
634654
Step (ThenStep "the table cell 1,0 is a|b|c")
655+
656+
// Scenario: step table with escaped backslashes
657+
Scenario "Scenario: Step table with escaped backslashes"
658+
Step (GivenStep "a table with escaped backslashes")
659+
Item (Step (GivenStep "a table with escaped backslashes"), TableRow [ "col1"; "col2" ])
660+
Item (Step (GivenStep "a table with escaped backslashes"), TableRow [ "hello\\world"; "normal" ])
661+
Step (ThenStep "the table cell 0,0 is hello\\world")
662+
Step (ThenStep "the table cell 0,1 is normal")
663+
664+
// Scenario: escaped backslash before pipe acts as delimiter
665+
Scenario "Scenario: Escaped backslash before pipe acts as cell delimiter"
666+
Step (GivenStep "a table with escaped backslash before pipe")
667+
Item (Step (GivenStep "a table with escaped backslash before pipe"), TableRow [ "col1"; "col2"; "col3" ])
668+
Item (Step (GivenStep "a table with escaped backslash before pipe"), TableRow [ "before\\"; "after"; "end" ])
669+
Step (ThenStep "the table cell 0,0 is before\\")
670+
Step (ThenStep "the table cell 0,1 is after")
671+
Step (ThenStep "the table cell 0,2 is end")
672+
673+
// Scenario: triple backslash-pipe (\\\| -> \|)
674+
Scenario "Scenario: Triple backslash-pipe is escaped backslash then escaped pipe"
675+
Step (GivenStep "a table with triple backslash-pipe")
676+
Item (Step (GivenStep "a table with triple backslash-pipe"), TableRow [ "col1"; "col2" ])
677+
Item (Step (GivenStep "a table with triple backslash-pipe"), TableRow [ "a\\|b"; "normal" ])
678+
Step (ThenStep "the table cell 0,0 is a\\|b")
679+
Step (ThenStep "the table cell 0,1 is normal")
680+
681+
// Scenario: escaped backslash before n (\\n -> \n literal, not newline)
682+
Scenario "Scenario: Escaped backslash before n is literal backslash-n not newline"
683+
Step (GivenStep "a table with escaped backslash before n")
684+
Item (Step (GivenStep "a table with escaped backslash before n"), TableRow [ "col1"; "col2" ])
685+
Item (Step (GivenStep "a table with escaped backslash before n"), TableRow [ "test\\n"; "normal" ])
686+
Step (ThenStep "the table cell 0,0 is test\\n")
687+
Step (ThenStep "the table cell 0,1 is normal")
635688
]
636689

637690
[<Test>]
638-
let EscapedPipe_ParseFeature () =
691+
let EscapedCharacters_ParseFeature () =
639692
let featureSource =
640693
"TickSpec.Tests.EscapedPipe.feature"
641694
|> loadFeatureFile
642695
|> FeatureParser.parseFeature
643696

644-
Assert.AreEqual("Escaped pipe in table cells", featureSource.Name)
645-
Assert.AreEqual(6, featureSource.Scenarios.Length)
697+
Assert.AreEqual("Escaped characters in table cells", featureSource.Name)
646698

647-
// Verify the outline scenarios with escaped pipes produce correct parameter values
648-
// Row order: 0=no pipe, 1=before|after, 2=|leading, 3=trailing|, 4=a|b|c
699+
// Outline 1: escaped pipes (5 data rows -> scenarios 0-4)
649700
let scenario1 = featureSource.Scenarios.[1]
650701
let params1 = scenario1.Parameters |> dict
651702
Assert.AreEqual("before|after", params1.["value"])
@@ -656,12 +707,48 @@ let EscapedPipe_ParseFeature () =
656707
Assert.AreEqual("a|b|c", params4.["value"])
657708
Assert.AreEqual("a|b|c", params4.["expected"])
658709

659-
// Verify step table with escaped pipes
660-
let tableScenario = featureSource.Scenarios.[5]
661-
let tableStep = tableScenario.Steps.[0] |> fst
662-
Assert.AreEqual(GivenStep "a table with escaped pipes", tableStep)
663-
let table = (tableScenario.Steps.[0] |> snd).Table.Value
664-
Assert.AreEqual([| "col1"; "col2" |], table.Header)
665-
Assert.AreEqual("hello|world", table.Rows.[0].[0])
666-
Assert.AreEqual("normal", table.Rows.[0].[1])
667-
Assert.AreEqual("a|b|c", table.Rows.[1].[0])
710+
// Outline 2: escaped backslashes (1 data row -> scenario 5)
711+
let bsScenario = featureSource.Scenarios.[5]
712+
let bsParams = bsScenario.Parameters |> dict
713+
Assert.AreEqual("hello\\world", bsParams.["value"])
714+
Assert.AreEqual("hello\\world", bsParams.["expected"])
715+
716+
// Outline 3: escaped newlines (1 data row -> scenario 6)
717+
let nlScenario = featureSource.Scenarios.[6]
718+
let nlParams = nlScenario.Parameters |> dict
719+
Assert.AreEqual("line1\nline2", nlParams.["value"])
720+
Assert.AreEqual("line1\nline2", nlParams.["expected"])
721+
722+
// Scenario: step table with escaped pipes (scenario 7)
723+
let pipeTableScenario = featureSource.Scenarios.[7]
724+
let pipeTable = (pipeTableScenario.Steps.[0] |> snd).Table.Value
725+
Assert.AreEqual([| "col1"; "col2" |], pipeTable.Header)
726+
Assert.AreEqual("hello|world", pipeTable.Rows.[0].[0])
727+
Assert.AreEqual("normal", pipeTable.Rows.[0].[1])
728+
Assert.AreEqual("a|b|c", pipeTable.Rows.[1].[0])
729+
730+
// Scenario: step table with escaped backslashes (scenario 8)
731+
let bsTableScenario = featureSource.Scenarios.[8]
732+
let bsTable = (bsTableScenario.Steps.[0] |> snd).Table.Value
733+
Assert.AreEqual("hello\\world", bsTable.Rows.[0].[0])
734+
Assert.AreEqual("normal", bsTable.Rows.[0].[1])
735+
736+
// Scenario: escaped backslash before pipe = cell delimiter (scenario 9)
737+
let delimScenario = featureSource.Scenarios.[9]
738+
let delimTable = (delimScenario.Steps.[0] |> snd).Table.Value
739+
Assert.AreEqual([| "col1"; "col2"; "col3" |], delimTable.Header)
740+
Assert.AreEqual("before\\", delimTable.Rows.[0].[0])
741+
Assert.AreEqual("after", delimTable.Rows.[0].[1])
742+
Assert.AreEqual("end", delimTable.Rows.[0].[2])
743+
744+
// Scenario: triple backslash-pipe \\\| -> \| (scenario 10)
745+
let tripleScenario = featureSource.Scenarios.[10]
746+
let tripleTable = (tripleScenario.Steps.[0] |> snd).Table.Value
747+
Assert.AreEqual("a\\|b", tripleTable.Rows.[0].[0])
748+
Assert.AreEqual("normal", tripleTable.Rows.[0].[1])
749+
750+
// Scenario: \\n -> literal \n, not newline (scenario 11)
751+
let bsNScenario = featureSource.Scenarios.[11]
752+
let bsNTable = (bsNScenario.Steps.[0] |> snd).Table.Value
753+
Assert.AreEqual("test\\n", bsNTable.Rows.[0].[0])
754+
Assert.IsFalse(bsNTable.Rows.[0].[0].Contains("\n"), "Should not contain actual newline")

TickSpec/LineParser.fs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,21 @@ let (|ButLine|_|) s =
8484
|> Option.map (function Trim t -> ButLine t)
8585
let (|TableRowLine|_|) (s:string) =
8686
if s.Trim().StartsWith("|") then
87-
let placeholder = "\u0000"
88-
let escaped = s.Trim().Replace("\\|", placeholder)
87+
let escapedBackslash = "\u0000"
88+
let escapedPipe = "\u0001"
89+
let escapedNewline = "\u0002"
90+
let escaped =
91+
s.Trim()
92+
.Replace("\\\\", escapedBackslash)
93+
.Replace("\\|", escapedPipe)
94+
.Replace("\\n", escapedNewline)
8995
let columnsStrings = escaped.Split([|'|'|], System.StringSplitOptions.RemoveEmptyEntries)
90-
let columns = [ for (Trim s) in columnsStrings -> s.Replace(placeholder, "|") ]
96+
let columns =
97+
[ for (Trim s) in columnsStrings ->
98+
s
99+
.Replace(escapedBackslash, "\\")
100+
.Replace(escapedPipe, "|")
101+
.Replace(escapedNewline, "\n") ]
91102
TableRowLine columns |> Some
92103
else None
93104
let (|Bullet|_|) (s:string) =

0 commit comments

Comments
 (0)