fix line mode wrapping and raw text printing

This commit is contained in:
Arpad Ryszka 2020-11-22 19:14:45 +01:00
parent d46324fe78
commit 67225d3ae5
5 changed files with 178 additions and 54 deletions

111
fprint.go
View File

@ -1,5 +1,7 @@
package notation package notation
import "strings"
func unwrappable(n node) bool { func unwrappable(n node) bool {
return n.len == n.wrapLen.max && return n.len == n.wrapLen.max &&
n.len == n.fullWrap.max n.len == n.fullWrap.max
@ -29,6 +31,36 @@ func nodeLen(t int, n node) node {
n.len += len(part) n.len += len(part)
w += len(part) w += len(part)
f += len(part) f += len(part)
case str:
// We assume here that an str is always contained by a node that has only a
// single str.
//
// If this changes in the future, then we need to provide tests for the
// additional cases. If this doesn't change anytime soon, then we can
// refactor this part.
//
n.len = len(part.val)
if part.raw == "" {
w = len(part.val)
f = len(part.val)
} else {
lines := strings.Split(part.raw, "\n")
part.rawLen.first = len(lines[0])
for _, line := range lines {
if len(line) > part.rawLen.max {
part.rawLen.max = len(line)
}
}
part.rawLen.last = len(lines[len(lines)-1])
n.parts[i] = part
n.wrapLen.first = part.rawLen.first
n.fullWrap.first = part.rawLen.first
n.wrapLen.max = part.rawLen.max
n.fullWrap.max = part.rawLen.max
w = part.rawLen.last
f = part.rawLen.last
}
case node: case node:
part = nodeLen(t, part) part = nodeLen(t, part)
n.parts[i] = part n.parts[i] = part
@ -100,7 +132,7 @@ func nodeLen(t int, n node) node {
return n return n
} }
func wrapNode(t, c0, c1 int, n node) node { func wrapNode(t, cf0, c0, c1 int, n node) node {
if n.len <= c0 { if n.len <= c0 {
return n return n
} }
@ -120,12 +152,32 @@ func wrapNode(t, c0, c1 int, n node) node {
for i := 0; i < len(n.parts); i++ { for i := 0; i < len(n.parts); i++ {
p := n.parts[i] p := n.parts[i]
switch part := p.(type) { switch part := p.(type) {
case string:
cc0 -= len(part)
cc1 -= len(part)
if !trackBack && cc1 < 0 {
cc0 = 0
cc1 = 0
i = lastWrapperIndex
trackBack = true
}
case str:
// We assume here that an str is always contained by a node that has only a
// single str. Therefore we don't need to trackback to here, because the
// decision on wrapping was already made for the node.
//
// If this changes in the future, then we need to provide tests for the
// additional cases. If this doesn't change anytime soon, then we can
// refactor this part.
//
part.useRaw = part.raw != ""
n.parts[i] = part
case node: case node:
part = wrapNode(t, cc0, cc1, part) part = wrapNode(t, cf0, cc0, cc1, part)
n.parts[i] = part n.parts[i] = part
if part.wrap { if part.wrap {
// This is an approximation: sometimes part.fullWrap.first is applied here, // This is an approximation: sometimes part.fullWrap.first should be applied
// but usually those are the same. // here, but usually those are the same.
cc0 -= part.wrapLen.first cc0 -= part.wrapLen.first
cc1 -= part.wrapLen.first cc1 -= part.wrapLen.first
} else { } else {
@ -149,13 +201,13 @@ func wrapNode(t, c0, c1 int, n node) node {
lastWrapperIndex = i lastWrapperIndex = i
switch part.mode { switch part.mode {
case line: case line:
c0, c1 = c0-t, c1-t cl := cf0 - t
var w int var w int
for j, ni := range part.items { for j, ni := range part.items {
if w > 0 && w+len(part.sep)+ni.len > c0 { if w > 0 && w+len(part.sep)+ni.len > cl {
w = 0 w = 0
part.lineWrappers = append( part.lineEnds = append(
part.lineWrappers, part.lineEnds,
j, j,
) )
} }
@ -167,12 +219,13 @@ func wrapNode(t, c0, c1 int, n node) node {
w += ni.len w += ni.len
} }
part.lineEnds = append(part.lineEnds, len(part.items))
n.parts[i] = part n.parts[i] = part
c0, c1 = c0+t, c1+t
default: default:
for j := range part.items { for j := range part.items {
part.items[j] = wrapNode( part.items[j] = wrapNode(
t, t,
cf0,
c0-t, c0-t,
c1-t, c1-t,
part.items[j], part.items[j],
@ -201,48 +254,50 @@ func fprint(w *writer, t int, n node) {
if !n.wrap { if !n.wrap {
for i, ni := range part.items { for i, ni := range part.items {
fprint(w, t, ni) if i > 0 {
if i < len(part.items)-1 {
w.write(part.sep) w.write(part.sep)
} }
fprint(w, t, ni)
} }
continue continue
} }
t++
switch part.mode { switch part.mode {
case line: case line:
var ( var (
wi int lines [][]node
lineStarted bool last int
) )
w.line(t) for _, i := range part.lineEnds {
for i, ni := range part.items { lines = append(lines, part.items[last:i])
if len(part.lineWrappers) > wi && last = i
i == part.lineWrappers[wi] { }
wi++
w.line(t)
lineStarted = false
}
if lineStarted { for _, line := range lines {
w.write(part.sep) w.blankLine()
} w.tabs(1)
for i, ni := range line {
if i > 0 {
w.write(part.sep)
}
fprint(w, 0, ni) fprint(w, 0, ni)
lineStarted = true }
} }
default: default:
t++
for _, ni := range part.items { for _, ni := range part.items {
w.line(t) w.line(t)
fprint(w, t, ni) fprint(w, t, ni)
w.write(part.suffix) w.write(part.suffix)
} }
t--
} }
t--
w.line(t) w.line(t)
default: default:
w.write(part) w.write(part)

View File

@ -110,13 +110,13 @@ func TestFprint(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
if n != len(expect) {
t.Fatalf("invalid write length; expected: %d, got: %d", len(expect), n)
}
if b.String() != expect { if b.String() != expect {
t.Fatalf("invalid output; expected: %s, got: %s", expect, b.String()) t.Fatalf("invalid output; expected: %s, got: %s", expect, b.String())
} }
if n != len(expect) {
t.Fatalf("invalid write length; expected: %d, got: %d", len(expect), n)
}
}) })
t.Run("Fprintt", func(t *testing.T) { t.Run("Fprintt", func(t *testing.T) {

View File

@ -34,6 +34,13 @@ type node struct {
parts []interface{} parts []interface{}
} }
type str struct {
val string
raw string
useRaw bool
rawLen wrapLen
}
type wrapMode int type wrapMode int
const ( const (
@ -42,10 +49,10 @@ const (
) )
type wrapper struct { type wrapper struct {
mode wrapMode mode wrapMode
sep, suffix string sep, suffix string
items []node items []node
lineWrappers []int lineEnds []int
} }
type writer struct { type writer struct {
@ -61,6 +68,14 @@ func (n node) String() string {
return b.String() return b.String()
} }
func (s str) String() string {
if s.useRaw {
return s.raw
}
return s.val
}
func (w *writer) write(o interface{}) { func (w *writer) write(o interface{}) {
if w.err != nil { if w.err != nil {
return return
@ -71,13 +86,21 @@ func (w *writer) write(o interface{}) {
w.err = err w.err = err
} }
func (w *writer) line(t int) { func (w *writer) blankLine() {
w.write("\n") w.write("\n")
for i := 0; i < t; i++ { }
func (w *writer) tabs(n int) {
for i := 0; i < n; i++ {
w.write("\t") w.write("\t")
} }
} }
func (w *writer) line(t int) {
w.blankLine()
w.tabs(t)
}
func nodeOf(parts ...interface{}) node { func nodeOf(parts ...interface{}) node {
return node{parts: parts} return node{parts: parts}
} }
@ -131,7 +154,7 @@ func fprintValues(w io.Writer, o opts, v []interface{}) (int, error) {
n := reflectValue(o, reflect.ValueOf(vi)) n := reflectValue(o, reflect.ValueOf(vi))
if o&wrap != 0 { if o&wrap != 0 {
n = nodeLen(tab, n) n = nodeLen(tab, n)
n = wrapNode(tab, cols0, cols1, n) n = wrapNode(tab, cols0, cols0, cols1, n)
} }
fprint(wr, 0, n) fprint(wr, 0, n)

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"reflect" "reflect"
"sort" "sort"
"strings"
) )
func withType(o opts) (opts, bool, bool) { func withType(o opts) (opts, bool, bool) {
@ -140,6 +141,7 @@ func reflectMap(o opts, r reflect.Value) node {
skeys []string skeys []string
) )
// TODO: simplify this when no sorting is required
items := wrapper{sep: ", ", suffix: ","} items := wrapper{sep: ", ", suffix: ","}
itemOpts := o | skipTypes itemOpts := o | skipTypes
keys := r.MapKeys() keys := r.MapKeys()
@ -201,7 +203,8 @@ func reflectList(o opts, r reflect.Value) node {
} }
func reflectString(o opts, r reflect.Value) node { func reflectString(o opts, r reflect.Value) node {
b := []byte(r.String()) sv := r.String()
b := []byte(sv)
e := make([]byte, 0, len(b)) e := make([]byte, 0, len(b))
for _, c := range b { for _, c := range b {
switch c { switch c {
@ -226,18 +229,23 @@ func reflectString(o opts, r reflect.Value) node {
} }
} }
s := fmt.Sprintf("\"%s\"", string(e)) s := str{val: fmt.Sprintf("\"%s\"", string(e))}
if !strings.Contains(sv, "`") && strings.Contains(sv, "\n") {
s.raw = fmt.Sprintf("`%s`", sv)
}
n := nodeOf(s)
_, t, a := withType(o) _, t, a := withType(o)
if !t { if !t {
return nodeOf(s) return n
} }
tn := reflectType(r.Type()) tn := reflectType(r.Type())
if !a && tn.parts[0] == "string" { if !a && tn.parts[0] == "string" {
return nodeOf(s) return n
} }
return nodeOf(tn, "(", wrapper{items: []node{nodeOf(s)}}, ")") return nodeOf(tn, "(", wrapper{items: []node{n}}, ")")
} }
func reflectStruct(o opts, r reflect.Value) node { func reflectStruct(o opts, r reflect.Value) node {

View File

@ -126,8 +126,8 @@ func defaultSet() tests {
{"nil custom list", struct{ l myList }{}, "{l: nil}"}, {"nil custom list", struct{ l myList }{}, "{l: nil}"},
{"long item", []string{"foobarbazqux"}, "[]{\"foobarbazqux\"}"}, {"long item", []string{"foobarbazqux"}, "[]{\"foobarbazqux\"}"},
{"long subitem", []struct{ foo string }{{foo: "foobarbazqux"}}, "[]{{foo: \"foobarbazqux\"}}"}, {"long subitem", []struct{ foo string }{{foo: "foobarbazqux"}}, "[]{{foo: \"foobarbazqux\"}}"},
{"string", "\\\"\b\f\n\r\t\vfoo", "\"\\\\\\\"\\b\\f\\n\\r\\t\\vfoo\""}, {"string", "\\\"\b\f\r\t\vfoo", "\"\\\\\\\"\\b\\f\\r\\t\\vfoo\""},
{"custom string", myString("\\\"\b\f\n\r\t\vfoo"), "\"\\\\\\\"\\b\\f\\n\\r\\t\\vfoo\""}, {"custom string", myString("\\\"\b\f\r\t\vfoo"), "\"\\\\\\\"\\b\\f\\r\\t\\vfoo\""},
{"structure", struct{ foo int }{42}, "{foo: 42}"}, {"structure", struct{ foo int }{42}, "{foo: 42}"},
{"custom structure", myStruct{42}, "{field: 42}"}, {"custom structure", myStruct{42}, "{field: 42}"},
{"unsafe pointer", unsafe.Pointer(&struct{}{}), "pointer"}, {"unsafe pointer", unsafe.Pointer(&struct{}{}), "pointer"},
@ -178,7 +178,7 @@ func (t tests) expectTypes() tests {
"nil custom list": "struct{l myList}{l: nil}", "nil custom list": "struct{l myList}{l: nil}",
"long item": "[]string{\"foobarbazqux\"}", "long item": "[]string{\"foobarbazqux\"}",
"long subitem": "[]struct{foo string}{{foo: \"foobarbazqux\"}}", "long subitem": "[]struct{foo string}{{foo: \"foobarbazqux\"}}",
"custom string": "myString(\"\\\\\\\"\\b\\f\\n\\r\\t\\vfoo\")", "custom string": "myString(\"\\\\\\\"\\b\\f\\r\\t\\vfoo\")",
"structure": "struct{foo int}{foo: 42}", "structure": "struct{foo int}{foo: 42}",
"custom structure": "myStruct{field: 42}", "custom structure": "myStruct{field: 42}",
"unsafe pointer type": "struct{p Pointer}{p: nil}", "unsafe pointer type": "struct{p Pointer}{p: nil}",
@ -214,7 +214,7 @@ func (t tests) expectVerboseTypes() tests {
"nil custom list": "struct{l myList}{l: myList(nil)}", "nil custom list": "struct{l myList}{l: myList(nil)}",
"long item": "[]string{string(\"foobarbazqux\")}", "long item": "[]string{string(\"foobarbazqux\")}",
"long subitem": "[]struct{foo string}{struct{foo string}{foo: string(\"foobarbazqux\")}}", "long subitem": "[]struct{foo string}{struct{foo string}{foo: string(\"foobarbazqux\")}}",
"string": "string(\"\\\\\\\"\\b\\f\\n\\r\\t\\vfoo\")", "string": "string(\"\\\\\\\"\\b\\f\\r\\t\\vfoo\")",
"structure": "struct{foo int}{foo: int(42)}", "structure": "struct{foo int}{foo: int(42)}",
"custom structure": "myStruct{field: interface{}(int(42))}", "custom structure": "myStruct{field: interface{}(int(42))}",
"unsafe pointer": "Pointer(pointer)", "unsafe pointer": "Pointer(pointer)",
@ -433,7 +433,7 @@ func (t tests) expectWrapAllWithTypes() tests {
foo: "foobarbazqux", foo: "foobarbazqux",
}, },
}`, }`,
"custom string": "myString(\n\t\"\\\\\\\"\\b\\f\\n\\r\\t\\vfoo\"\n)", "custom string": "myString(\n\t\"\\\\\\\"\\b\\f\\r\\t\\vfoo\"\n)",
"structure": `struct{ "structure": `struct{
foo int foo int
}{ }{
@ -474,7 +474,16 @@ func (t tests) expectOnlyLongWrappedWithTypes() tests {
}{ }{
i: nil, i: nil,
}`, }`,
"nil map": `struct{m map[int]int}{ "function with multiple return args": `func(
int,
int,
) (
int,
int,
)`,
"nil map": `struct{
m map[int]int
}{
m: nil, m: nil,
}`, }`,
"nil custom map": `struct{m myMap}{ "nil custom map": `struct{m myMap}{
@ -498,7 +507,7 @@ func (t tests) expectOnlyLongWrappedWithTypes() tests {
"long subitem": `[]struct{foo string}{ "long subitem": `[]struct{foo string}{
{foo: "foobarbazqux"}, {foo: "foobarbazqux"},
}`, }`,
"custom string": "myString(\n\t\"\\\\\\\"\\b\\f\\n\\r\\t\\vfoo\"\n)", "custom string": "myString(\n\t\"\\\\\\\"\\b\\f\\r\\t\\vfoo\"\n)",
"structure": `struct{foo int}{ "structure": `struct{foo int}{
foo: 42, foo: 42,
}`, }`,
@ -630,8 +639,8 @@ func (t tests) expectWrapAllWithVerboseTypes() tests {
), ),
}, },
}`, }`,
"string": "string(\n\t\"\\\\\\\"\\b\\f\\n\\r\\t\\vfoo\"\n)", "string": "string(\n\t\"\\\\\\\"\\b\\f\\r\\t\\vfoo\"\n)",
"custom string": "myString(\n\t\"\\\\\\\"\\b\\f\\n\\r\\t\\vfoo\"\n)", "custom string": "myString(\n\t\"\\\\\\\"\\b\\f\\r\\t\\vfoo\"\n)",
"structure": `struct{ "structure": `struct{
foo int foo int
}{ }{
@ -941,7 +950,7 @@ func TestBytes(t *testing.T) {
}) })
} }
func TestNonWrapperNodes(t *testing.T) { func TestLongNonWrapperNodes(t *testing.T) {
const expect = `map[struct{ const expect = `map[struct{
foo int foo int
bar int bar int
@ -963,3 +972,32 @@ func TestNonWrapperNodes(t *testing.T) {
t.Fatalf("expected: %s, got: %s", expect, s) t.Fatalf("expected: %s, got: %s", expect, s)
} }
} }
func TestSingleLongString(t *testing.T) {
t.Run("no line breaks", func(t *testing.T) {
const expect = `"foobarbazquxquuxquzquuz"`
defer withEnv(t, "TABWIDTH=2", "LINEWIDTH=9", "LINEWIDTH1=12")()
s := Sprintwt("foobarbazquxquuxquzquuz")
if s != expect {
t.Fatalf("expected: %s, got: %s", expect, s)
}
})
t.Run("line break and backquote", func(t *testing.T) {
const expect = "\"foobarbazqux`\\nquuxquzquuz\""
defer withEnv(t, "TABWIDTH=2", "LINEWIDTH=9", "LINEWIDTH1=12")()
s := Sprintwt("foobarbazqux`\nquuxquzquuz")
if s != expect {
t.Fatalf("expected: %s, got: %s", expect, s)
}
})
t.Run("line break and no backquote", func(t *testing.T) {
const expect = "`foobarbazqux\nquuxquzquuz`"
defer withEnv(t, "TABWIDTH=2", "LINEWIDTH=9", "LINEWIDTH1=12")()
s := Sprintwt("foobarbazqux\nquuxquzquuz")
if s != expect {
t.Fatalf("expected: %s, got: %s", expect, s)
}
})
}