Skip to content

Commit

Permalink
cue/literal: fix multiline quotes
Browse files Browse the repository at this point in the history
We insert as many `#` characters as necessary.
Fixes #569.

Change-Id: I59888a9b36cf307e9260002d306c845ffd5ccb5a
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/7521
Reviewed-by: CUE cueckoo <cueckoo@gmail.com>
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
  • Loading branch information
rogpeppe authored and mpvl committed Oct 29, 2020
1 parent 409dacf commit 54b13db
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 16 deletions.
92 changes: 77 additions & 15 deletions cue/literal/quote.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@ import (

// Form defines how to quote a string or bytes literal.
type Form struct {
hashCount int
quote byte
multiline bool
auto bool
exact bool
asciiOnly bool
graphicOnly bool
indent string
tripleQuote string
}

// TODO:
Expand Down Expand Up @@ -99,8 +101,15 @@ var (
// Bytes defines the format of bytes literal.
Bytes Form = bytesForm

stringForm = Form{quote: '"'}
bytesForm = Form{quote: '\'', exact: true}
stringForm = Form{
quote: '"',
tripleQuote: `"""`,
}
bytesForm = Form{
quote: '\'',
tripleQuote: `'''`,
exact: true,
}
)

// Quote returns CUE string literal representing s. The returned string uses CUE
Expand All @@ -122,6 +131,9 @@ func (f Form) Append(buf []byte, s string) []byte {
if f.auto && strings.ContainsRune(s, '\n') {
f.multiline = true
}
if f.multiline {
f.hashCount = f.requiredHashCount(s)
}

// Often called with big strings, so preallocate. If there's quoting,
// this is conservative but still helps a lot.
Expand All @@ -130,9 +142,11 @@ func (f Form) Append(buf []byte, s string) []byte {
copy(nBuf, buf)
buf = nBuf
}
buf = append(buf, f.quote)
for i := 0; i < f.hashCount; i++ {
buf = append(buf, '#')
}
if f.multiline {
buf = append(buf, f.quote, f.quote, '\n')
buf = append(buf, f.quote, f.quote, f.quote, '\n')
if s == "" {
buf = append(buf, f.indent...)
buf = append(buf, f.quote, f.quote, f.quote)
Expand All @@ -141,6 +155,8 @@ func (f Form) Append(buf []byte, s string) []byte {
if len(s) > 0 && s[0] != '\n' {
buf = append(buf, f.indent...)
}
} else {
buf = append(buf, f.quote)
}

buf = f.appendEscaped(buf, s)
Expand All @@ -152,6 +168,9 @@ func (f Form) Append(buf []byte, s string) []byte {
} else {
buf = append(buf, f.quote)
}
for i := 0; i < f.hashCount; i++ {
buf = append(buf, '#')
}

return buf
}
Expand Down Expand Up @@ -206,7 +225,7 @@ func (f Form) appendEscaped(buf []byte, s string) []byte {
func (f *Form) appendEscapedRune(buf []byte, r rune) []byte {
var runeTmp [utf8.UTFMax]byte
if (!f.multiline && r == rune(f.quote)) || r == '\\' { // always backslashed
buf = append(buf, '\\')
buf = f.appendEscape(buf)
buf = append(buf, byte(r))
return buf
}
Expand All @@ -220,37 +239,38 @@ func (f *Form) appendEscapedRune(buf []byte, r rune) []byte {
buf = append(buf, runeTmp[:n]...)
return buf
}
buf = f.appendEscape(buf)
switch r {
case '\a':
buf = append(buf, `\a`...)
buf = append(buf, 'a')
case '\b':
buf = append(buf, `\b`...)
buf = append(buf, 'b')
case '\f':
buf = append(buf, `\f`...)
buf = append(buf, 'f')
case '\n':
buf = append(buf, `\n`...)
buf = append(buf, 'n')
case '\r':
buf = append(buf, `\r`...)
buf = append(buf, 'r')
case '\t':
buf = append(buf, `\t`...)
buf = append(buf, 't')
case '\v':
buf = append(buf, `\v`...)
buf = append(buf, 'v')
default:
switch {
case r < ' ' && f.exact:
buf = append(buf, `\x`...)
buf = append(buf, 'x')
buf = append(buf, lowerhex[byte(r)>>4])
buf = append(buf, lowerhex[byte(r)&0xF])
case r > utf8.MaxRune:
r = 0xFFFD
fallthrough
case r < 0x10000:
buf = append(buf, `\u`...)
buf = append(buf, 'u')
for s := 12; s >= 0; s -= 4 {
buf = append(buf, lowerhex[r>>uint(s)&0xF])
}
default:
buf = append(buf, `\U`...)
buf = append(buf, 'U')
for s := 28; s >= 0; s -= 4 {
buf = append(buf, lowerhex[r>>uint(s)&0xF])
}
Expand All @@ -259,6 +279,48 @@ func (f *Form) appendEscapedRune(buf []byte, r rune) []byte {
return buf
}

func (f *Form) appendEscape(buf []byte) []byte {
buf = append(buf, '\\')
for i := 0; i < f.hashCount; i++ {
buf = append(buf, '#')
}
return buf
}

// requiredHashCount returns the number of # characters
// that are required to quote the multiline string s.
func (f *Form) requiredHashCount(s string) int {
hashCount := 0
i := 0
// Find all occurrences of the triple-quote and count
// the maximum number of succeeding # characters.
for {
j := strings.Index(s[i:], f.tripleQuote)
if j == -1 {
break
}
i += j + 3
// Absorb all extra quotes, so we
// get to the end of the sequence.
for ; i < len(s); i++ {
if s[i] != f.quote {
break
}
}
e := i - 1
// Count succeeding # characters.
for ; i < len(s); i++ {
if s[i] != '#' {
break
}
}
if nhash := i - e; nhash > hashCount {
hashCount = nhash
}
}
return hashCount
}

// isInGraphicList reports whether the rune is in the isGraphic list. This separation
// from IsGraphic allows quoteWith to avoid two calls to IsPrint.
// Should be called only if IsPrint fails.
Expand Down
24 changes: 23 additions & 1 deletion cue/literal/quote_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package literal

import (
"fmt"
"strings"
"testing"

Expand All @@ -34,6 +35,7 @@ func TestQuote(t *testing.T) {
{form: String.WithASCIIOnly(),
in: "abc\xffdef", out: `"abc\ufffddef"`, lossy: true},
{form: String, in: "\a\b\f\r\n\t\v", out: `"\a\b\f\r\n\t\v"`},
{form: String, in: "\"", out: `"\""`},
{form: String, in: "\\", out: `"\\"`},
{form: String, in: "\u263a", out: `"☺"`},
{form: String, in: "\U0010ffff", out: `"\U0010ffff"`},
Expand Down Expand Up @@ -80,9 +82,29 @@ func TestQuote(t *testing.T) {
foo
"bar"
"""`},
{form: String.WithTabIndent(3), in: "foo\n\"\"\"bar\"", out: `#"""
foo
"""bar"
"""#`},
{form: String.WithTabIndent(3), in: "foo\n\"\"\"\"\"###bar\"", out: `####"""
foo
"""""###bar"
"""####`},
{form: String.WithTabIndent(3), in: "foo\n\"\"\"\r\f\\", out: `#"""
foo
"""\#r\#f\#\
"""#`},
{form: Bytes.WithTabIndent(3), in: "foo'''\nhello", out: `#'''
foo'''
hello
'''#`},
{form: Bytes.WithTabIndent(3), in: "foo\n'''\r\f\\", out: `#'''
foo
'''\#r\#f\#\
'''#`},
}
for _, tc := range testCases {
t.Run(tc.in, func(t *testing.T) {
t.Run(fmt.Sprintf("%q", tc.in), func(t *testing.T) {
got := tc.form.Quote(tc.in)
if got != tc.out {
t.Errorf("Quote: %s", cmp.Diff(tc.out, got))
Expand Down

0 comments on commit 54b13db

Please sign in to comment.