Skip to content

Commit

Permalink
utter: add quoting strategy support
Browse files Browse the repository at this point in the history
WIP:
* needs tests
* API needs more thought
  • Loading branch information
kortschak committed Feb 5, 2022
1 parent 1166d33 commit 9ce0530
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 1 deletion.
2 changes: 2 additions & 0 deletions common.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ func unsafeReflectValue(v reflect.Value) (rv reflect.Value) {
// Some constants in the form of bytes to avoid string overhead. This mirrors
// the technique used in the fmt package.
var (
backQuoteBytes = []byte("`")
quoteBytes = []byte(`"`)
plusBytes = []byte("+")
iBytes = []byte("i")
trueBytes = []byte("true")
Expand Down
23 changes: 23 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ type ConfigState struct {
// a string slice or array. Zero specifies all entries on one line.
StringWidth int

// Quoting specifies the quoting strategy to use when printing strings.
Quoting Quoting

// BytesWidth specifies the number of byte columns to use when dumping a
// byte slice or array.
BytesWidth int
Expand Down Expand Up @@ -84,6 +87,26 @@ type ConfigState struct {
SortKeys bool
}

// Quoting describes string quoting strategies.
type Quoting uint

const (
// Quote strings with double quotes.
DoubleQuote Quoting = 0

// AvoidEscapes quotes strings using backquotes to avoid escape sequences
// if possible, otherwise double quotes are used.
AvoidEscapes Quoting = 1 << iota

// Backquote always quotes strings using backquotes if possible, otherwise
// double quotes are used.
Backquote

// Force is a modifier of AvoidEscapes and Backquote that adds additional
// double quote syntax to represent parts that cannot be backquoted.
Force
)

// Config is the active configuration of the top-level functions.
// The configuration can be changed by modifying the contents of utter.Config.
var Config = ConfigState{
Expand Down
105 changes: 104 additions & 1 deletion dump.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import (
"regexp"
"strconv"
"strings"
"unicode"
"unicode/utf8"
)

var (
Expand Down Expand Up @@ -463,7 +465,7 @@ func (d *dumpState) dump(v reflect.Value, wasPtr, static, canElideCompound bool,
d.dumpSlice(v, !interfaceContext)

case reflect.String:
d.w.Write([]byte(strconv.Quote(v.String())))
d.writeQuoted(v.String())

case reflect.Interface:
// The only time we should get here is for nil interfaces due to
Expand Down Expand Up @@ -585,6 +587,107 @@ func (d *dumpState) dump(v reflect.Value, wasPtr, static, canElideCompound bool,
}
}

// writeQuoted writes the string s quoted according to the quoting strategy.
func (d *dumpState) writeQuoted(s string) {
switch d.cs.Quoting {
default:
fallthrough
case DoubleQuote:
d.w.Write([]byte(strconv.Quote(s)))

case AvoidEscapes:
if !needsEscape(s) || !canBackquoteString(s) {
d.w.Write([]byte(strconv.Quote(s)))
return
}
d.backQuote(s)

case AvoidEscapes | Force:
if !needsEscape(s) {
d.w.Write([]byte(strconv.Quote(s)))
return
}

fallthrough
case Backquote, Backquote | Force:
if canBackquoteString(s) {
d.backQuote(s)
return
}

var last int
inBackquote := true
for i, r := range s {
if canBackquote(r) != inBackquote {
if last != 0 {
d.w.Write(plusBytes)
}
if inBackquote {
d.backQuote(s[last:i])
} else {
d.w.Write([]byte(strconv.Quote(s[last:i])))
}
last = i
inBackquote = !inBackquote
}
}
if last != len(s) {
if last != 0 {
d.w.Write(plusBytes)
}
if !inBackquote {
d.w.Write([]byte(strconv.Quote(s[last:])))
return
}
d.backQuote(s[last:])
}
}
}

// backQuote writes s backquoted.
func (d *dumpState) backQuote(s string) {
d.w.Write(backQuoteBytes)
d.w.Write([]byte(s))
d.w.Write(backQuoteBytes)
}

// needsEscape returns whether the string s needs any escape sequence to be
// double quote printed.
func needsEscape(s string) bool {
for _, r := range s {
if r == '"' || r == '\\' {
return true
}
if !strconv.IsPrint(r) && !strconv.IsGraphic(r) {
return true
}
}
return false
}

// canBackquoteString returns whether the string s can be represented
// unchanged as a backquoted string without non-space control characters.
func canBackquoteString(s string) bool {
for _, r := range s {
if !canBackquote(r) {
return false
}
}
return true
}

// canBackquote returns whether the rune r can be represented unchanged as a
// backquoted string without non-space control characters.
func canBackquote(r rune) bool {
if r == utf8.RuneError {
return false
}
if utf8.RuneLen(r) > 1 {
return r != '\ufeff'
}
return (unicode.IsSpace(r) || ' ' < r) && r != '`' && r != '\u007f'
}

// typeString returns the string representation of the reflect.Type with the local
// package selector removed.
func typeString(typ reflect.Type, local string) string {
Expand Down

0 comments on commit 9ce0530

Please sign in to comment.