Skip to content

Commit

Permalink
Make quote symbol preserved in multiline quoted strings.
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 623954472
  • Loading branch information
txtpbfmt-copybara-robot committed Apr 15, 2024
1 parent ef3ab17 commit 7cc9baa
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 38 deletions.
8 changes: 4 additions & 4 deletions parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -1159,7 +1159,7 @@ func wrapLinesAtColumn(nd *ast.Node, depth int, c Config) error {
lengthBuffer := 4 // Even at depth 0 we have a 2-space indent and a pair of quotes
maxLength := c.WrapStringsAtColumn - lengthBuffer - (depth * len(indentSpaces))

str, err := unquote.Raw(nd)
str, quote, err := unquote.Raw(nd)
if err != nil {
return fmt.Errorf("skipping string wrapping on node %q (error unquoting string): %v", nd.Name, err)
}
Expand All @@ -1183,7 +1183,7 @@ func wrapLinesAtColumn(nd *ast.Node, depth int, c Config) error {
if i < len(lines)-1 {
line = line + " "
}
v.Value = fmt.Sprintf(`"%s"`, line)
v.Value = fmt.Sprintf(`%c%s%c`, quote, line, quote)
newValues = append(newValues, v)
}

Expand Down Expand Up @@ -1223,7 +1223,7 @@ func needsWrappingAfterNewlines(nd *ast.Node, c Config) bool {
// then wrap the string so each line ends with a newline.
// Wraps only the current Node (does not recurse into Children).
func wrapLinesAfterNewlines(nd *ast.Node, c Config) error {
str, err := unquote.Raw(nd)
str, quote, err := unquote.Raw(nd)
if err != nil {
return fmt.Errorf("skipping string wrapping on node %q (error unquoting string): %v", nd.Name, err)
}
Expand All @@ -1245,7 +1245,7 @@ func wrapLinesAfterNewlines(nd *ast.Node, c Config) error {
} else {
v = &ast.Value{}
}
v.Value = fmt.Sprintf(`"%s"`, line)
v.Value = fmt.Sprintf(`%c%s%c`, quote, line, quote)
newValues = append(newValues, v)
}

Expand Down
20 changes: 20 additions & 0 deletions parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1429,6 +1429,26 @@ field: "b"
value: { num: 1 }
}
}
`}, {
name: "plx dashboard mixed quotes",
in: `# txtpbfmt: wrap_strings_after_newlines
# txtpbfmt: smartquotes
types_text_content: {
text: "Some text\nwith a <a href=\"https://www.google.com\">hyperlink</a>\nincluded"
}
chart_spec: "{\"columnDefinitions\":[]}"
inline_script: "SELECT \'Hello\' AS hello"
`,
out: `# txtpbfmt: wrap_strings_after_newlines
# txtpbfmt: smartquotes
types_text_content: {
text:
'Some text\n'
'with a <a href="https://www.google.com">hyperlink</a>\n'
'included'
}
chart_spec: '{"columnDefinitions":[]}'
inline_script: "SELECT 'Hello' AS hello"
`}}
for _, input := range inputs {
out, err := Format([]byte(input.in))
Expand Down
36 changes: 21 additions & 15 deletions unquote/unquote.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,33 @@ import (
"github.com/protocolbuffers/txtpbfmt/ast"
)

// Unquote returns the value of the string node.
// Unquote returns the value of the string node and the rune used to quote it.
// Calling Unquote on non-string node doesn't panic, but is otherwise undefined.
func Unquote(n *ast.Node) (string, error) {
func Unquote(n *ast.Node) (string, rune, error) {
return unquoteValues(n.Values, unquote)
}

// Raw returns the raw value of the string node, with string escapes left in place.
// Raw returns the raw value of the string node and the rune used to quote it, with string escapes
// left in place.
// Calling UnquoteRaw on non-string node doesn't panic, but is otherwise undefined.
func Raw(n *ast.Node) (string, error) {
func Raw(n *ast.Node) (string, rune, error) {
return unquoteValues(n.Values, unquoteRaw)
}

func unquoteValues(values []*ast.Value, unquoter func(string) (string, error)) (string, error) {
func unquoteValues(values []*ast.Value, unquoter func(string) (string, rune, error)) (string, rune, error) {
var ret strings.Builder
firstQuote := rune(0)
for _, v := range values {
uq, err := unquoter(v.Value)
uq, quote, err := unquoter(v.Value)
if firstQuote == rune(0) {
firstQuote = quote
}
if err != nil {
return "", err
return "", rune(0), err
}
ret.WriteString(uq)
}
return ret.String(), nil
return ret.String(), firstQuote, nil
}

// Returns the quote rune used in the given string (' or "). Returns an error if the string doesn't
Expand All @@ -51,20 +56,21 @@ func quoteRune(s string) (rune, error) {
return rune(quote), nil
}

func unquote(s string) (string, error) {
func unquote(s string) (string, rune, error) {
quote, err := quoteRune(s)
if err != nil {
return "", err
return "", rune(0), err
}
return unquoteC(s[1:len(s)-1], quote)
unquoted, err := unquoteC(s[1:len(s)-1], quote)
return unquoted, quote, err
}

func unquoteRaw(s string) (string, error) {
_, err := quoteRune(s) // Trigger validation, which guarantees this is a quote-wrapped string.
func unquoteRaw(s string) (string, rune, error) {
quote, err := quoteRune(s) // Trigger validation, which guarantees this is a quote-wrapped string.
if err != nil {
return "", err
return "", rune(0), err
}
return s[1 : len(s)-1], nil
return s[1 : len(s)-1], quote, nil
}

var (
Expand Down
49 changes: 30 additions & 19 deletions unquote/unquote_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,35 +10,40 @@ import (

func TestUnquote(t *testing.T) {
inputs := []struct {
in string
want string
wantRaw string
in string
want string
wantRaw string
wantRune rune
}{
{
in: `"value"`,
want: `value`,
wantRaw: `value`,
in: `"value"`,
want: `value`,
wantRaw: `value`,
wantRune: rune('"'),
},
{
in: `'value'`,
want: `value`,
wantRaw: `value`,
in: `'value'`,
want: `value`,
wantRaw: `value`,
wantRune: rune('\''),
},
{
in: `"foo\'\a\b\f\n\r\t\vbar"`,
want: "foo'\a\b\f\n\r\t\vbar", // Double-quoted; string contains real control characters.
wantRaw: `foo\'\a\b\f\n\r\t\vbar`,
in: `"foo\'\a\b\f\n\r\t\vbar"`,
want: "foo'\a\b\f\n\r\t\vbar", // Double-quoted; string contains real control characters.
wantRaw: `foo\'\a\b\f\n\r\t\vbar`,
wantRune: rune('"'),
},
{
in: `'foo\"bar'`,
want: `foo"bar`,
wantRaw: `foo\"bar`,
in: `'foo\"bar'`,
want: `foo"bar`,
wantRaw: `foo\"bar`,
wantRune: rune('\''),
},
}
for _, input := range inputs {
node := &ast.Node{Name: "name", Values: []*ast.Value{{Value: input.in}}}

got, err := Unquote(node)
got, gotRune, err := Unquote(node)
if err != nil {
t.Errorf("Unquote(%v) returned err %v", input.in, err)
continue
Expand All @@ -48,15 +53,21 @@ func TestUnquote(t *testing.T) {
t.Logf("got: %q", got)
t.Errorf("Unquote(%v) returned diff (-want, +got):\n%s", input.in, diff)
}
if gotRune != input.wantRune {
t.Errorf("Unquote(%v) returned rune %q, want %q", input.in, gotRune, input.wantRune)
}

got, err = Raw(node)
got, gotRune, err = Raw(node)
if err != nil {
t.Errorf("unquote.Raw(%v) returned err %v", input.in, err)
continue
}
if diff := diff.Diff(input.wantRaw, got); diff != "" {
t.Errorf("unquote.Raw(%v) returned diff (-wantRaw, +got):\n%s", input.in, diff)
}
if gotRune != input.wantRune {
t.Errorf("unquote.Raw(%v) returned rune %q, want %q", input.in, gotRune, input.wantRune)
}
}
}

Expand All @@ -82,12 +93,12 @@ func TestErrorHandling(t *testing.T) {
for _, input := range inputs {
node := &ast.Node{Name: "name", Values: []*ast.Value{{Value: input.in}}}

_, err := Unquote(node)
_, _, err := Unquote(node)
if err == nil || !strings.Contains(err.Error(), input.wantErr) {
t.Errorf("Unquote(%s) got %v, want err to contain %q", input.in, err, input.wantErr)
}

_, err = Raw(node)
_, _, err = Raw(node)
if err == nil || !strings.Contains(err.Error(), input.wantErr) {
t.Errorf("Raw(%s) got %v, want err to contain %q", input.in, err, input.wantErr)
}
Expand Down

0 comments on commit 7cc9baa

Please sign in to comment.