diff --git a/common/parser/strings.go b/common/parser/strings.go new file mode 100644 index 0000000..6ccc962 --- /dev/null +++ b/common/parser/strings.go @@ -0,0 +1,129 @@ +package parser + +type ParseFeatures struct { + ParseDoubleQuotes bool + ParseEscapedCharacters bool +} + +var FullFeatures = ParseFeatures{ + ParseDoubleQuotes: true, + ParseEscapedCharacters: true, +} + +type ParsedString struct { + Raw string + Value string + + Features ParseFeatures +} + +func ParseRawString( + raw string, + features ParseFeatures, +) ParsedString { + value := raw + + // Parse double quotes + if features.ParseDoubleQuotes { + value = ParseDoubleQuotes(value) + } + + // Parse escaped characters + if features.ParseEscapedCharacters { + value = ParseEscapedCharacters(value) + } + + return ParsedString{ + Raw: raw, + Value: value, + Features: features, + } +} + +func ParseDoubleQuotes( + raw string, +) string { + value := raw + currentIndex := 0 + + for { + start, found := findNextDoubleQuote(value, currentIndex) + + if found && start < (len(value)-1) { + currentIndex = max(0, start-1) + end, found := findNextDoubleQuote(value, start+1) + + if found { + insideContent := value[start+1 : end] + value = modifyString(value, start, end+1, insideContent) + + continue + } + } + + break + } + + return value +} + +func ParseEscapedCharacters( + raw string, +) string { + value := raw + currentIndex := 0 + + for { + position, found := findNextEscapedCharacter(value, currentIndex) + + if found { + currentIndex = max(0, position-1) + escapedCharacter := value[position+1] + value = modifyString(value, position, position+2, string(escapedCharacter)) + } else { + break + } + } + + return value +} + +func modifyString( + input string, + start int, + end int, + newValue string, +) string { + return input[:start] + newValue + input[end:] +} + +// Find the next non-escaped double quote in [raw] starting from [startIndex] +// When no double quote is found, return -1 +// Return as the second argument whether a double quote was found +func findNextDoubleQuote( + raw string, + startIndex int, +) (int, bool) { + for index := startIndex; index < len(raw); index++ { + if raw[index] == '"' { + if index == 0 || raw[index-1] != '\\' { + return index, true + } + } + } + + return -1, false +} + +func findNextEscapedCharacter( + raw string, + startIndex int, +) (int, bool) { + for index := startIndex; index < len(raw); index++ { + if raw[index] == '\\' && index < len(raw)-1 { + return index, true + } + } + + return -1, false +} diff --git a/common/parser/strings_test.go b/common/parser/strings_test.go new file mode 100644 index 0000000..bb3265b --- /dev/null +++ b/common/parser/strings_test.go @@ -0,0 +1,177 @@ +package parser + +import ( + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestStringsSingleWortQuotedFullFeatures( + t *testing.T, +) { + input := `hello "world"` + expected := ParsedString{ + Raw: input, + Value: "hello world", + Features: FullFeatures, + } + + actual := ParseRawString(input, FullFeatures) + + if !(cmp.Equal(expected, actual)) { + t.Errorf("Expected %v, got %v", expected, actual) + } +} + +func TestStringsFullyQuotedFullFeatures( + t *testing.T, +) { + input := `"hello world"` + expected := ParsedString{ + Raw: input, + Value: "hello world", + Features: FullFeatures, + } + + actual := ParseRawString(input, FullFeatures) + + if !(cmp.Equal(expected, actual)) { + t.Errorf("Expected %v, got %v", expected, actual) + } +} + +func TestStringsMultipleQuotesFullFeatures( + t *testing.T, +) { + input := `hello "world goodbye"` + expected := ParsedString{ + Raw: input, + Value: "hello world goodbye", + Features: FullFeatures, + } + + actual := ParseRawString(input, FullFeatures) + + if !(cmp.Equal(expected, actual)) { + t.Errorf("Expected %v, got %v", expected, actual) + } +} + +func TestStringsSimpleEscapedFullFeatures( + t *testing.T, +) { + input := `hello \"world` + expected := ParsedString{ + Raw: input, + Value: `hello "world`, + Features: FullFeatures, + } + + actual := ParseRawString(input, FullFeatures) + + if !(cmp.Equal(expected, actual)) { + t.Errorf("Expected %v, got %v", expected, actual) + } +} + +func TestStringsEscapedQuotesFullFeatures( + t *testing.T, +) { + input := `hello \"world\"` + expected := ParsedString{ + Raw: input, + Value: `hello "world"`, + Features: FullFeatures, + } + + actual := ParseRawString(input, FullFeatures) + + if !(cmp.Equal(expected, actual)) { + t.Errorf("Expected %v, got %v", expected, actual) + } +} + +func TestStringsQuotesAndEscapedFullFeatures( + t *testing.T, +) { + input := `hello "world how\" are you"` + expected := ParsedString{ + Raw: input, + Value: `hello world how" are you`, + Features: FullFeatures, + } + + actual := ParseRawString(input, FullFeatures) + + if !(cmp.Equal(expected, actual)) { + t.Errorf("Expected %v, got %v", expected, actual) + } +} + +func TestStringsIncompleteQuotesFullFeatures( + t *testing.T, +) { + input := `hello "world` + expected := ParsedString{ + Raw: input, + Value: `hello "world`, + Features: FullFeatures, + } + + actual := ParseRawString(input, FullFeatures) + + if !(cmp.Equal(expected, actual)) { + t.Errorf("Expected %v, got %v", expected, actual) + } +} + +func TestStringsIncompleteQuoteEscapedFullFeatures( + t *testing.T, +) { + input := `hello "world\"` + expected := ParsedString{ + Raw: input, + Value: `hello "world"`, + Features: FullFeatures, + } + + actual := ParseRawString(input, FullFeatures) + + if !(cmp.Equal(expected, actual)) { + t.Errorf("Expected %v, got %v", expected, actual) + } +} + +func TestStringsIncompleteQuotes2FullFeatures( + t *testing.T, +) { + input := `hello "world how" "are you` + expected := ParsedString{ + Raw: input, + Value: `hello world how "are you`, + Features: FullFeatures, + } + + actual := ParseRawString(input, FullFeatures) + + if !(cmp.Equal(expected, actual)) { + t.Errorf("Expected %v, got %v", expected, actual) + } +} + +func TestStringsIncompleteQuotes3FullFeatures( + t *testing.T, +) { + input := `hello "world how are you` + expected := ParsedString{ + Raw: input, + Value: `hello "world how are you`, + Features: FullFeatures, + } + + actual := ParseRawString(input, FullFeatures) + + if !(cmp.Equal(expected, actual)) { + t.Errorf("Expected %v, got %v", expected, actual) + } +} diff --git a/go.mod b/go.mod index 661cc9a..8c84359 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/iancoleman/strcase v0.3.0 // indirect github.com/k0kubun/pp v3.0.1+incompatible // indirect diff --git a/go.sum b/go.sum index 60c2e89..100a1f7 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiE github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=