simplify indent-only operation
This commit is contained in:
parent
259163c737
commit
f8e59c1465
183
indent.go
183
indent.go
@ -1,181 +1,34 @@
|
|||||||
package textedit
|
package textedit
|
||||||
|
|
||||||
import "unicode"
|
type indentState struct {
|
||||||
|
started, lineStarted bool
|
||||||
const nonbreakSpace = '\u00a0'
|
|
||||||
|
|
||||||
type wrapIndentState struct {
|
|
||||||
currentWord []rune
|
|
||||||
currentLineLength int
|
|
||||||
multipleLines bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func indentLength(i []rune) int {
|
func indentEdit(first, rest []rune) func(rune, indentState) ([]rune, indentState) {
|
||||||
var l int
|
return func(r rune, s indentState) ([]rune, indentState) {
|
||||||
for _, ii := range i {
|
if r == '\n' {
|
||||||
if ii == '\t' {
|
s.started = true
|
||||||
l += 8
|
s.lineStarted = false
|
||||||
continue
|
return []rune{'\n'}, s
|
||||||
}
|
}
|
||||||
|
|
||||||
l++
|
|
||||||
}
|
|
||||||
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
|
|
||||||
func wrapIndentEdit(first, rest []rune, firstWidth, restWidth int) func(rune, wrapIndentState) ([]rune, wrapIndentState) {
|
|
||||||
firstIndentLength := indentLength(first)
|
|
||||||
restIndentLength := indentLength(rest)
|
|
||||||
return func(r rune, state wrapIndentState) ([]rune, wrapIndentState) {
|
|
||||||
var ret []rune
|
var ret []rune
|
||||||
indent := first
|
if !s.started {
|
||||||
il := firstIndentLength
|
ret = append(ret, first...)
|
||||||
width := firstWidth
|
} else if !s.lineStarted {
|
||||||
if state.multipleLines {
|
|
||||||
indent = rest
|
|
||||||
il = restIndentLength
|
|
||||||
width = restWidth
|
|
||||||
}
|
|
||||||
|
|
||||||
nw := width <= 0
|
|
||||||
cl := state.currentLineLength
|
|
||||||
wl := len(state.currentWord)
|
|
||||||
nl := r == '\n'
|
|
||||||
ws := unicode.IsSpace(r) && r != nonbreakSpace
|
|
||||||
if nw && nl && wl > 0 {
|
|
||||||
if cl == 0 {
|
|
||||||
ret = append(ret, indent...)
|
|
||||||
}
|
|
||||||
|
|
||||||
if cl > 0 {
|
|
||||||
ret = append(ret, ' ')
|
|
||||||
}
|
|
||||||
|
|
||||||
ret = append(ret, state.currentWord...)
|
|
||||||
ret = append(ret, '\n')
|
|
||||||
state.currentWord = nil
|
|
||||||
state.currentLineLength = 0
|
|
||||||
state.multipleLines = true
|
|
||||||
return ret, state
|
|
||||||
}
|
|
||||||
|
|
||||||
if nw && nl {
|
|
||||||
ret = append(ret, '\n')
|
|
||||||
state.currentLineLength = 0
|
|
||||||
state.multipleLines = true
|
|
||||||
return ret, state
|
|
||||||
}
|
|
||||||
|
|
||||||
if nw && ws && wl > 0 {
|
|
||||||
if cl == 0 {
|
|
||||||
ret = append(ret, indent...)
|
|
||||||
}
|
|
||||||
|
|
||||||
if cl > 0 {
|
|
||||||
ret = append(ret, ' ')
|
|
||||||
state.currentLineLength++
|
|
||||||
}
|
|
||||||
|
|
||||||
ret = append(ret, state.currentWord...)
|
|
||||||
state.currentLineLength += wl
|
|
||||||
state.currentWord = nil
|
|
||||||
return ret, state
|
|
||||||
}
|
|
||||||
|
|
||||||
if nw && ws {
|
|
||||||
return nil, state
|
|
||||||
}
|
|
||||||
|
|
||||||
if nw {
|
|
||||||
state.currentWord = append(state.currentWord, r)
|
|
||||||
return nil, state
|
|
||||||
}
|
|
||||||
|
|
||||||
if ws && cl > 0 && cl+wl+il+1 > width && wl > 0 {
|
|
||||||
ret = append(ret, '\n')
|
|
||||||
ret = append(ret, rest...)
|
ret = append(ret, rest...)
|
||||||
ret = append(ret, state.currentWord...)
|
|
||||||
state.currentLineLength = wl
|
|
||||||
state.multipleLines = true
|
|
||||||
state.currentWord = nil
|
|
||||||
return ret, state
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ws && cl > 0 && cl+il+1 > width {
|
ret = append(ret, r)
|
||||||
ret = append(ret, '\n')
|
s.started = true
|
||||||
state.currentLineLength = 0
|
s.lineStarted = true
|
||||||
state.multipleLines = true
|
return ret, s
|
||||||
return ret, state
|
|
||||||
}
|
|
||||||
|
|
||||||
if ws && cl > 0 && wl > 0 {
|
|
||||||
ret = append(ret, ' ')
|
|
||||||
ret = append(ret, state.currentWord...)
|
|
||||||
state.currentLineLength++
|
|
||||||
state.currentLineLength += wl
|
|
||||||
state.currentWord = nil
|
|
||||||
return ret, state
|
|
||||||
}
|
|
||||||
|
|
||||||
if ws && wl > 0 {
|
|
||||||
ret = append(ret, indent...)
|
|
||||||
ret = append(ret, state.currentWord...)
|
|
||||||
state.currentLineLength += wl
|
|
||||||
state.currentWord = nil
|
|
||||||
return ret, state
|
|
||||||
}
|
|
||||||
|
|
||||||
if ws {
|
|
||||||
return nil, state
|
|
||||||
}
|
|
||||||
|
|
||||||
state.currentWord = append(state.currentWord, r)
|
|
||||||
return nil, state
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func wrapIndentRelease(first, rest []rune, firstWidth, restWidth int) func(wrapIndentState) []rune {
|
func indent(first, rest []rune) Editor {
|
||||||
return func(state wrapIndentState) []rune {
|
|
||||||
if len(state.currentWord) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var ret []rune
|
|
||||||
indent := first
|
|
||||||
width := firstWidth
|
|
||||||
if state.multipleLines {
|
|
||||||
indent = rest
|
|
||||||
width = restWidth
|
|
||||||
}
|
|
||||||
|
|
||||||
if width > 0 && state.currentLineLength > 0 &&
|
|
||||||
state.currentLineLength+len(state.currentWord)+1+len(indent) > width {
|
|
||||||
ret = append(ret, '\n')
|
|
||||||
state.currentLineLength = 0
|
|
||||||
state.multipleLines = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if state.multipleLines {
|
|
||||||
indent = rest
|
|
||||||
}
|
|
||||||
|
|
||||||
if state.currentLineLength == 0 {
|
|
||||||
ret = append(ret, indent...)
|
|
||||||
}
|
|
||||||
|
|
||||||
if state.currentLineLength > 0 {
|
|
||||||
ret = append(ret, ' ')
|
|
||||||
}
|
|
||||||
|
|
||||||
ret = append(ret, state.currentWord...)
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func wrapIndent(first, rest []rune, firstWidth, restWidth int) Editor {
|
|
||||||
return Func(
|
return Func(
|
||||||
wrapIndentEdit(first, rest, firstWidth, restWidth),
|
indentEdit(first, rest),
|
||||||
wrapIndentRelease(first, rest, firstWidth, restWidth),
|
func(indentState) []rune { return nil },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
198
indent_test.go
198
indent_test.go
@ -57,16 +57,6 @@ func TestIndent(t *testing.T) {
|
|||||||
t.Fatal(b.String())
|
t.Fatal(b.String())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("indent last word", func(t *testing.T) {
|
|
||||||
var b bytes.Buffer
|
|
||||||
w := textedit.New(&b, textedit.Indent("xxxx", "xxxx"))
|
|
||||||
w.Write([]byte("foo bar bar foo\n baz\nqux quux\nfoo"))
|
|
||||||
w.Flush()
|
|
||||||
if b.String() != "xxxxfoo bar bar foo\nxxxxbaz\nxxxxqux quux\nxxxxfoo" {
|
|
||||||
t.Fatal(b.String())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("indent no wrap", func(t *testing.T) {
|
t.Run("indent no wrap", func(t *testing.T) {
|
||||||
@ -80,6 +70,16 @@ func TestIndent(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("indent last word", func(t *testing.T) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
w := textedit.New(&b, textedit.Indent("xxxx", "xxxx"))
|
||||||
|
w.Write([]byte("foo bar bar foo\n baz\nqux quux\nfoo"))
|
||||||
|
w.Flush()
|
||||||
|
if b.String() != "xxxxfoo bar bar foo\nxxxx baz\nxxxxqux quux\nxxxxfoo" {
|
||||||
|
t.Fatal(b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("first out", func(t *testing.T) {
|
t.Run("first out", func(t *testing.T) {
|
||||||
var b bytes.Buffer
|
var b bytes.Buffer
|
||||||
w := textedit.New(&b, textedit.Indent("", "xx"))
|
w := textedit.New(&b, textedit.Indent("", "xx"))
|
||||||
@ -120,182 +120,4 @@ func TestIndent(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("wrap", func(t *testing.T) {
|
|
||||||
t.Run("same first and rest", func(t *testing.T) {
|
|
||||||
var b bytes.Buffer
|
|
||||||
w := textedit.New(&b, textedit.Wrap(9, 9))
|
|
||||||
w.Write([]byte("foo bar\n baz\nqux quux\n"))
|
|
||||||
w.Flush()
|
|
||||||
if b.String() != "foo bar\nbaz qux\nquux" {
|
|
||||||
t.Fatal(b.String())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("shorter first", func(t *testing.T) {
|
|
||||||
var b bytes.Buffer
|
|
||||||
w := textedit.New(&b, textedit.Wrap(5, 9))
|
|
||||||
w.Write([]byte("foo bar\n baz\nqux quux\n"))
|
|
||||||
w.Flush()
|
|
||||||
if b.String() != "foo\nbar baz\nqux quux" {
|
|
||||||
t.Fatal(b.String())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("longer first", func(t *testing.T) {
|
|
||||||
var b bytes.Buffer
|
|
||||||
w := textedit.New(&b, textedit.Wrap(9, 5))
|
|
||||||
w.Write([]byte("foo bar\n baz\nqux quux\n"))
|
|
||||||
w.Flush()
|
|
||||||
if b.String() != "foo bar\nbaz\nqux\nquux" {
|
|
||||||
t.Fatal(b.String())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("word longer than width", func(t *testing.T) {
|
|
||||||
var b bytes.Buffer
|
|
||||||
w := textedit.New(&b, textedit.Wrap(2, 2))
|
|
||||||
w.Write([]byte("foo bar\n baz\nqux quux\n"))
|
|
||||||
w.Flush()
|
|
||||||
if b.String() != "foo\nbar\nbaz\nqux\nquux" {
|
|
||||||
t.Fatal(b.String())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("indent and wrap", func(t *testing.T) {
|
|
||||||
const text = `
|
|
||||||
Walking through the mixed forests of Brandenburg in early autumn, one notices the dominant
|
|
||||||
presence of Scots pine (Pinus sylvestris) interspersed with sessile oak (Quercus petraea) and
|
|
||||||
silver birch (Betula pendula), their canopies creating a mosaic of light and shadow on the
|
|
||||||
forest floor. The sandy, acidic soils typical of this region support a ground layer rich in
|
|
||||||
ericaceous plants, particularly bilberry (Vaccinium myrtillus) with its dark-green oval leaves
|
|
||||||
now tinged with burgundy, and the occasional patch of heather (Calluna vulgaris) persisting in
|
|
||||||
sunnier clearings. Closer inspection of the understory reveals common wood sorrel (Oxalis
|
|
||||||
acetosella) thriving in moister pockets, while various moss species—including the feathery
|
|
||||||
fronds of Hypnum cupressiforme—carpet fallen logs in varying stages of decay. The drier sections
|
|
||||||
host wavy hair-grass (Deschampsia flexuosa) in delicate tufts, and where old pines have been
|
|
||||||
felled, pioneering stands of downy birch and rowan (Sorbus aucuparia) compete for space, their
|
|
||||||
growth marking the forest's continuous cycle of regeneration in this characteristically glacial
|
|
||||||
landscape of the North European Plain.
|
|
||||||
`
|
|
||||||
|
|
||||||
t.Run("uniform", func(t *testing.T) {
|
|
||||||
var b bytes.Buffer
|
|
||||||
w := textedit.New(&b, textedit.WrapIndent(" ", " ", 120, 120))
|
|
||||||
w.Write([]byte(text))
|
|
||||||
w.Flush()
|
|
||||||
const expect = `
|
|
||||||
Walking through the mixed forests of Brandenburg in early autumn, one notices the dominant presence of Scots pine
|
|
||||||
(Pinus sylvestris) interspersed with sessile oak (Quercus petraea) and silver birch (Betula pendula), their canopies
|
|
||||||
creating a mosaic of light and shadow on the forest floor. The sandy, acidic soils typical of this region support a
|
|
||||||
ground layer rich in ericaceous plants, particularly bilberry (Vaccinium myrtillus) with its dark-green oval leaves
|
|
||||||
now tinged with burgundy, and the occasional patch of heather (Calluna vulgaris) persisting in sunnier clearings.
|
|
||||||
Closer inspection of the understory reveals common wood sorrel (Oxalis acetosella) thriving in moister pockets,
|
|
||||||
while various moss species—including the feathery fronds of Hypnum cupressiforme—carpet fallen logs in varying
|
|
||||||
stages of decay. The drier sections host wavy hair-grass (Deschampsia flexuosa) in delicate tufts, and where old
|
|
||||||
pines have been felled, pioneering stands of downy birch and rowan (Sorbus aucuparia) compete for space, their
|
|
||||||
growth marking the forest's continuous cycle of regeneration in this characteristically glacial landscape of the
|
|
||||||
North European Plain.`
|
|
||||||
|
|
||||||
if "\n"+b.String() != expect {
|
|
||||||
t.Fatal("\n" + b.String())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("classic", func(t *testing.T) {
|
|
||||||
var b bytes.Buffer
|
|
||||||
w := textedit.New(&b, textedit.WrapIndent(" ", "", 120, 120))
|
|
||||||
w.Write([]byte(text))
|
|
||||||
w.Flush()
|
|
||||||
const expect = `
|
|
||||||
Walking through the mixed forests of Brandenburg in early autumn, one notices the dominant presence of Scots pine
|
|
||||||
(Pinus sylvestris) interspersed with sessile oak (Quercus petraea) and silver birch (Betula pendula), their canopies
|
|
||||||
creating a mosaic of light and shadow on the forest floor. The sandy, acidic soils typical of this region support a
|
|
||||||
ground layer rich in ericaceous plants, particularly bilberry (Vaccinium myrtillus) with its dark-green oval leaves now
|
|
||||||
tinged with burgundy, and the occasional patch of heather (Calluna vulgaris) persisting in sunnier clearings. Closer
|
|
||||||
inspection of the understory reveals common wood sorrel (Oxalis acetosella) thriving in moister pockets, while various
|
|
||||||
moss species—including the feathery fronds of Hypnum cupressiforme—carpet fallen logs in varying stages of decay. The
|
|
||||||
drier sections host wavy hair-grass (Deschampsia flexuosa) in delicate tufts, and where old pines have been felled,
|
|
||||||
pioneering stands of downy birch and rowan (Sorbus aucuparia) compete for space, their growth marking the forest's
|
|
||||||
continuous cycle of regeneration in this characteristically glacial landscape of the North European Plain.`
|
|
||||||
|
|
||||||
if "\n"+b.String() != expect {
|
|
||||||
t.Fatal("\n" + b.String())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("indent out", func(t *testing.T) {
|
|
||||||
var b bytes.Buffer
|
|
||||||
w := textedit.New(&b, textedit.WrapIndent("", " ", 120, 120))
|
|
||||||
w.Write([]byte(text))
|
|
||||||
w.Flush()
|
|
||||||
const expect = `
|
|
||||||
Walking through the mixed forests of Brandenburg in early autumn, one notices the dominant presence of Scots pine (Pinus
|
|
||||||
sylvestris) interspersed with sessile oak (Quercus petraea) and silver birch (Betula pendula), their canopies
|
|
||||||
creating a mosaic of light and shadow on the forest floor. The sandy, acidic soils typical of this region support a
|
|
||||||
ground layer rich in ericaceous plants, particularly bilberry (Vaccinium myrtillus) with its dark-green oval leaves
|
|
||||||
now tinged with burgundy, and the occasional patch of heather (Calluna vulgaris) persisting in sunnier clearings.
|
|
||||||
Closer inspection of the understory reveals common wood sorrel (Oxalis acetosella) thriving in moister pockets,
|
|
||||||
while various moss species—including the feathery fronds of Hypnum cupressiforme—carpet fallen logs in varying
|
|
||||||
stages of decay. The drier sections host wavy hair-grass (Deschampsia flexuosa) in delicate tufts, and where old
|
|
||||||
pines have been felled, pioneering stands of downy birch and rowan (Sorbus aucuparia) compete for space, their
|
|
||||||
growth marking the forest's continuous cycle of regeneration in this characteristically glacial landscape of the
|
|
||||||
North European Plain.`
|
|
||||||
|
|
||||||
if "\n"+b.String() != expect {
|
|
||||||
t.Fatal("\n" + b.String())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("indent out with same width", func(t *testing.T) {
|
|
||||||
var b bytes.Buffer
|
|
||||||
w := textedit.New(&b, textedit.WrapIndent("", " ", 116, 120))
|
|
||||||
w.Write([]byte(text))
|
|
||||||
w.Flush()
|
|
||||||
const expect = `
|
|
||||||
Walking through the mixed forests of Brandenburg in early autumn, one notices the dominant presence of Scots pine
|
|
||||||
(Pinus sylvestris) interspersed with sessile oak (Quercus petraea) and silver birch (Betula pendula), their canopies
|
|
||||||
creating a mosaic of light and shadow on the forest floor. The sandy, acidic soils typical of this region support a
|
|
||||||
ground layer rich in ericaceous plants, particularly bilberry (Vaccinium myrtillus) with its dark-green oval leaves
|
|
||||||
now tinged with burgundy, and the occasional patch of heather (Calluna vulgaris) persisting in sunnier clearings.
|
|
||||||
Closer inspection of the understory reveals common wood sorrel (Oxalis acetosella) thriving in moister pockets,
|
|
||||||
while various moss species—including the feathery fronds of Hypnum cupressiforme—carpet fallen logs in varying
|
|
||||||
stages of decay. The drier sections host wavy hair-grass (Deschampsia flexuosa) in delicate tufts, and where old
|
|
||||||
pines have been felled, pioneering stands of downy birch and rowan (Sorbus aucuparia) compete for space, their
|
|
||||||
growth marking the forest's continuous cycle of regeneration in this characteristically glacial landscape of the
|
|
||||||
North European Plain.`
|
|
||||||
|
|
||||||
if "\n"+b.String() != expect {
|
|
||||||
t.Fatal("\n" + b.String())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("tab is 8", func(t *testing.T) {
|
|
||||||
var b bytes.Buffer
|
|
||||||
w := textedit.New(&b, textedit.WrapIndent("\t", "", 12, 12))
|
|
||||||
w.Write([]byte("foo bar\n baz\nqux quux\n"))
|
|
||||||
w.Flush()
|
|
||||||
if b.String() != "\tfoo\nbar baz qux\nquux" {
|
|
||||||
t.Fatal(b.String())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("wrap on release", func(t *testing.T) {
|
|
||||||
var b bytes.Buffer
|
|
||||||
w := textedit.New(&b, textedit.WrapIndent(" ", " ", 15, 15))
|
|
||||||
w.Write([]byte("Some sample text...\n on multiple lines."))
|
|
||||||
w.Flush()
|
|
||||||
|
|
||||||
const expect = `
|
|
||||||
Some sample
|
|
||||||
text... on
|
|
||||||
multiple
|
|
||||||
lines.`
|
|
||||||
|
|
||||||
if "\n"+b.String() != expect {
|
|
||||||
t.Fatal("\n" + b.String())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
10
lib.go
10
lib.go
@ -72,10 +72,9 @@ func Escape(esc rune, chars ...rune) Editor {
|
|||||||
return escape(esc, chars...)
|
return escape(esc, chars...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Indent applies indentation to multi-line text. Duplicate, leading, trailing and non-space whitespaces, other
|
// Indent applies indentation to multi-line text. Whitespaces are preserved.
|
||||||
// than the newline character, are collapsed into a single space.
|
|
||||||
func Indent(first, rest string) Editor {
|
func Indent(first, rest string) Editor {
|
||||||
return wrapIndent([]rune(first), []rune(rest), 0, 0)
|
return indent([]rune(first), []rune(rest))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wrap wrap wraps multiline text to define maximual text width. Duplicate, leading, trailing and non-space
|
// Wrap wrap wraps multiline text to define maximual text width. Duplicate, leading, trailing and non-space
|
||||||
@ -91,10 +90,7 @@ func WrapIndent(firstIndent, restIndent string, firstWidth, restWidth int) Edito
|
|||||||
|
|
||||||
// SingleLine is like Wrap, but with infinite text width.
|
// SingleLine is like Wrap, but with infinite text width.
|
||||||
func SingleLine() Editor {
|
func SingleLine() Editor {
|
||||||
return sequence(
|
return singleLine()
|
||||||
replace("\n", " "),
|
|
||||||
wrapIndent(nil, nil, 0, 0),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// New initializes an editing writer. The editor instances will be called in the order they are passed in to
|
// New initializes an editing writer. The editor instances will be called in the order they are passed in to
|
||||||
|
|||||||
36
singleline.go
Normal file
36
singleline.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package textedit
|
||||||
|
|
||||||
|
import "unicode"
|
||||||
|
|
||||||
|
type singleLineState struct {
|
||||||
|
started bool
|
||||||
|
currentWord []rune
|
||||||
|
}
|
||||||
|
|
||||||
|
func singleLineEdit(r rune, s singleLineState) ([]rune, singleLineState) {
|
||||||
|
if !unicode.IsSpace(r) || r == nonbreakSpace {
|
||||||
|
s.currentWord = append(s.currentWord, r)
|
||||||
|
return nil, s
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(s.currentWord) == 0 {
|
||||||
|
return nil, s
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := s.currentWord
|
||||||
|
if s.started {
|
||||||
|
ret = append([]rune{' '}, ret...)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.currentWord = nil
|
||||||
|
s.started = true
|
||||||
|
return ret, s
|
||||||
|
}
|
||||||
|
|
||||||
|
func singleLineReleaseState(s singleLineState) []rune {
|
||||||
|
return s.currentWord
|
||||||
|
}
|
||||||
|
|
||||||
|
func singleLine() Editor {
|
||||||
|
return Func(singleLineEdit, singleLineReleaseState)
|
||||||
|
}
|
||||||
130
wrap.go
Normal file
130
wrap.go
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
package textedit
|
||||||
|
|
||||||
|
import "unicode"
|
||||||
|
|
||||||
|
const nonbreakSpace = '\u00a0'
|
||||||
|
|
||||||
|
type wrapIndentState struct {
|
||||||
|
currentWord []rune
|
||||||
|
currentLineLength int
|
||||||
|
multipleLines bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func indentLength(i []rune) int {
|
||||||
|
var l int
|
||||||
|
for _, ii := range i {
|
||||||
|
if ii == '\t' {
|
||||||
|
l += 8
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
l++
|
||||||
|
}
|
||||||
|
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func wrapIndentEdit(first, rest []rune, firstWidth, restWidth int) func(rune, wrapIndentState) ([]rune, wrapIndentState) {
|
||||||
|
firstIndentLength := indentLength(first)
|
||||||
|
restIndentLength := indentLength(rest)
|
||||||
|
return func(r rune, state wrapIndentState) ([]rune, wrapIndentState) {
|
||||||
|
var ret []rune
|
||||||
|
indent := first
|
||||||
|
il := firstIndentLength
|
||||||
|
width := firstWidth
|
||||||
|
if state.multipleLines {
|
||||||
|
indent = rest
|
||||||
|
il = restIndentLength
|
||||||
|
width = restWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
cl := state.currentLineLength
|
||||||
|
wl := len(state.currentWord)
|
||||||
|
ws := unicode.IsSpace(r) && r != nonbreakSpace
|
||||||
|
if ws && cl > 0 && cl+wl+il+1 > width && wl > 0 {
|
||||||
|
ret = append(ret, '\n')
|
||||||
|
ret = append(ret, rest...)
|
||||||
|
ret = append(ret, state.currentWord...)
|
||||||
|
state.currentLineLength = wl
|
||||||
|
state.multipleLines = true
|
||||||
|
state.currentWord = nil
|
||||||
|
return ret, state
|
||||||
|
}
|
||||||
|
|
||||||
|
if ws && cl > 0 && cl+il+1 > width {
|
||||||
|
ret = append(ret, '\n')
|
||||||
|
state.currentLineLength = 0
|
||||||
|
state.multipleLines = true
|
||||||
|
return ret, state
|
||||||
|
}
|
||||||
|
|
||||||
|
if ws && cl > 0 && wl > 0 {
|
||||||
|
ret = append(ret, ' ')
|
||||||
|
ret = append(ret, state.currentWord...)
|
||||||
|
state.currentLineLength++
|
||||||
|
state.currentLineLength += wl
|
||||||
|
state.currentWord = nil
|
||||||
|
return ret, state
|
||||||
|
}
|
||||||
|
|
||||||
|
if ws && wl > 0 {
|
||||||
|
ret = append(ret, indent...)
|
||||||
|
ret = append(ret, state.currentWord...)
|
||||||
|
state.currentLineLength += wl
|
||||||
|
state.currentWord = nil
|
||||||
|
return ret, state
|
||||||
|
}
|
||||||
|
|
||||||
|
if ws {
|
||||||
|
return nil, state
|
||||||
|
}
|
||||||
|
|
||||||
|
state.currentWord = append(state.currentWord, r)
|
||||||
|
return nil, state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func wrapIndentRelease(first, rest []rune, firstWidth, restWidth int) func(wrapIndentState) []rune {
|
||||||
|
return func(state wrapIndentState) []rune {
|
||||||
|
if len(state.currentWord) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var ret []rune
|
||||||
|
indent := first
|
||||||
|
width := firstWidth
|
||||||
|
if state.multipleLines {
|
||||||
|
indent = rest
|
||||||
|
width = restWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.currentLineLength > 0 &&
|
||||||
|
state.currentLineLength+len(state.currentWord)+1+len(indent) > width {
|
||||||
|
ret = append(ret, '\n')
|
||||||
|
state.currentLineLength = 0
|
||||||
|
state.multipleLines = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.multipleLines {
|
||||||
|
indent = rest
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.currentLineLength == 0 {
|
||||||
|
ret = append(ret, indent...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.currentLineLength > 0 {
|
||||||
|
ret = append(ret, ' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = append(ret, state.currentWord...)
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func wrapIndent(first, rest []rune, firstWidth, restWidth int) Editor {
|
||||||
|
return Func(
|
||||||
|
wrapIndentEdit(first, rest, firstWidth, restWidth),
|
||||||
|
wrapIndentRelease(first, rest, firstWidth, restWidth),
|
||||||
|
)
|
||||||
|
}
|
||||||
237
wrap_test.go
Normal file
237
wrap_test.go
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
package textedit_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"code.squareroundforest.org/arpio/textedit"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWrap(t *testing.T) {
|
||||||
|
t.Run("wrap", func(t *testing.T) {
|
||||||
|
t.Run("same first and rest", func(t *testing.T) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
w := textedit.New(&b, textedit.Wrap(9, 9))
|
||||||
|
w.Write([]byte("foo bar\n baz\nqux quux\n"))
|
||||||
|
w.Flush()
|
||||||
|
if b.String() != "foo bar\nbaz qux\nquux" {
|
||||||
|
t.Fatal(b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("shorter first", func(t *testing.T) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
w := textedit.New(&b, textedit.Wrap(5, 9))
|
||||||
|
w.Write([]byte("foo bar\n baz\nqux quux\n"))
|
||||||
|
w.Flush()
|
||||||
|
if b.String() != "foo\nbar baz\nqux quux" {
|
||||||
|
t.Fatal(b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("longer first", func(t *testing.T) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
w := textedit.New(&b, textedit.Wrap(9, 5))
|
||||||
|
w.Write([]byte("foo bar\n baz\nqux quux\n"))
|
||||||
|
w.Flush()
|
||||||
|
if b.String() != "foo bar\nbaz\nqux\nquux" {
|
||||||
|
t.Fatal(b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("word longer than width", func(t *testing.T) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
w := textedit.New(&b, textedit.Wrap(2, 2))
|
||||||
|
w.Write([]byte("foo bar\n baz\nqux quux\n"))
|
||||||
|
w.Flush()
|
||||||
|
if b.String() != "foo\nbar\nbaz\nqux\nquux" {
|
||||||
|
t.Fatal(b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("zero width", func(t *testing.T) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
w := textedit.New(&b, textedit.Wrap(0, 0))
|
||||||
|
w.Write([]byte("foo bar\n baz\nqux quux\n"))
|
||||||
|
w.Flush()
|
||||||
|
if b.String() != "foo\nbar\nbaz\nqux\nquux" {
|
||||||
|
t.Fatal(b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("negative width", func(t *testing.T) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
w := textedit.New(&b, textedit.Wrap(-2, -2))
|
||||||
|
w.Write([]byte("foo bar\n baz\nqux quux\n"))
|
||||||
|
w.Flush()
|
||||||
|
if b.String() != "foo\nbar\nbaz\nqux\nquux" {
|
||||||
|
t.Fatal(b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("indent and wrap", func(t *testing.T) {
|
||||||
|
const text = `
|
||||||
|
Walking through the mixed forests of Brandenburg in early autumn, one notices the dominant
|
||||||
|
presence of Scots pine (Pinus sylvestris) interspersed with sessile oak (Quercus petraea) and
|
||||||
|
silver birch (Betula pendula), their canopies creating a mosaic of light and shadow on the
|
||||||
|
forest floor. The sandy, acidic soils typical of this region support a ground layer rich in
|
||||||
|
ericaceous plants, particularly bilberry (Vaccinium myrtillus) with its dark-green oval leaves
|
||||||
|
now tinged with burgundy, and the occasional patch of heather (Calluna vulgaris) persisting in
|
||||||
|
sunnier clearings. Closer inspection of the understory reveals common wood sorrel (Oxalis
|
||||||
|
acetosella) thriving in moister pockets, while various moss species—including the feathery
|
||||||
|
fronds of Hypnum cupressiforme—carpet fallen logs in varying stages of decay. The drier sections
|
||||||
|
host wavy hair-grass (Deschampsia flexuosa) in delicate tufts, and where old pines have been
|
||||||
|
felled, pioneering stands of downy birch and rowan (Sorbus aucuparia) compete for space, their
|
||||||
|
growth marking the forest's continuous cycle of regeneration in this characteristically glacial
|
||||||
|
landscape of the North European Plain.
|
||||||
|
`
|
||||||
|
|
||||||
|
t.Run("uniform", func(t *testing.T) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
w := textedit.New(&b, textedit.WrapIndent(" ", " ", 120, 120))
|
||||||
|
w.Write([]byte(text))
|
||||||
|
w.Flush()
|
||||||
|
const expect = `
|
||||||
|
Walking through the mixed forests of Brandenburg in early autumn, one notices the dominant presence of Scots pine
|
||||||
|
(Pinus sylvestris) interspersed with sessile oak (Quercus petraea) and silver birch (Betula pendula), their canopies
|
||||||
|
creating a mosaic of light and shadow on the forest floor. The sandy, acidic soils typical of this region support a
|
||||||
|
ground layer rich in ericaceous plants, particularly bilberry (Vaccinium myrtillus) with its dark-green oval leaves
|
||||||
|
now tinged with burgundy, and the occasional patch of heather (Calluna vulgaris) persisting in sunnier clearings.
|
||||||
|
Closer inspection of the understory reveals common wood sorrel (Oxalis acetosella) thriving in moister pockets,
|
||||||
|
while various moss species—including the feathery fronds of Hypnum cupressiforme—carpet fallen logs in varying
|
||||||
|
stages of decay. The drier sections host wavy hair-grass (Deschampsia flexuosa) in delicate tufts, and where old
|
||||||
|
pines have been felled, pioneering stands of downy birch and rowan (Sorbus aucuparia) compete for space, their
|
||||||
|
growth marking the forest's continuous cycle of regeneration in this characteristically glacial landscape of the
|
||||||
|
North European Plain.`
|
||||||
|
|
||||||
|
if "\n"+b.String() != expect {
|
||||||
|
t.Fatal("\n" + b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("classic", func(t *testing.T) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
w := textedit.New(&b, textedit.WrapIndent(" ", "", 120, 120))
|
||||||
|
w.Write([]byte(text))
|
||||||
|
w.Flush()
|
||||||
|
const expect = `
|
||||||
|
Walking through the mixed forests of Brandenburg in early autumn, one notices the dominant presence of Scots pine
|
||||||
|
(Pinus sylvestris) interspersed with sessile oak (Quercus petraea) and silver birch (Betula pendula), their canopies
|
||||||
|
creating a mosaic of light and shadow on the forest floor. The sandy, acidic soils typical of this region support a
|
||||||
|
ground layer rich in ericaceous plants, particularly bilberry (Vaccinium myrtillus) with its dark-green oval leaves now
|
||||||
|
tinged with burgundy, and the occasional patch of heather (Calluna vulgaris) persisting in sunnier clearings. Closer
|
||||||
|
inspection of the understory reveals common wood sorrel (Oxalis acetosella) thriving in moister pockets, while various
|
||||||
|
moss species—including the feathery fronds of Hypnum cupressiforme—carpet fallen logs in varying stages of decay. The
|
||||||
|
drier sections host wavy hair-grass (Deschampsia flexuosa) in delicate tufts, and where old pines have been felled,
|
||||||
|
pioneering stands of downy birch and rowan (Sorbus aucuparia) compete for space, their growth marking the forest's
|
||||||
|
continuous cycle of regeneration in this characteristically glacial landscape of the North European Plain.`
|
||||||
|
|
||||||
|
if "\n"+b.String() != expect {
|
||||||
|
t.Fatal("\n" + b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("indent out", func(t *testing.T) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
w := textedit.New(&b, textedit.WrapIndent("", " ", 120, 120))
|
||||||
|
w.Write([]byte(text))
|
||||||
|
w.Flush()
|
||||||
|
const expect = `
|
||||||
|
Walking through the mixed forests of Brandenburg in early autumn, one notices the dominant presence of Scots pine (Pinus
|
||||||
|
sylvestris) interspersed with sessile oak (Quercus petraea) and silver birch (Betula pendula), their canopies
|
||||||
|
creating a mosaic of light and shadow on the forest floor. The sandy, acidic soils typical of this region support a
|
||||||
|
ground layer rich in ericaceous plants, particularly bilberry (Vaccinium myrtillus) with its dark-green oval leaves
|
||||||
|
now tinged with burgundy, and the occasional patch of heather (Calluna vulgaris) persisting in sunnier clearings.
|
||||||
|
Closer inspection of the understory reveals common wood sorrel (Oxalis acetosella) thriving in moister pockets,
|
||||||
|
while various moss species—including the feathery fronds of Hypnum cupressiforme—carpet fallen logs in varying
|
||||||
|
stages of decay. The drier sections host wavy hair-grass (Deschampsia flexuosa) in delicate tufts, and where old
|
||||||
|
pines have been felled, pioneering stands of downy birch and rowan (Sorbus aucuparia) compete for space, their
|
||||||
|
growth marking the forest's continuous cycle of regeneration in this characteristically glacial landscape of the
|
||||||
|
North European Plain.`
|
||||||
|
|
||||||
|
if "\n"+b.String() != expect {
|
||||||
|
t.Fatal("\n" + b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("indent out with same width", func(t *testing.T) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
w := textedit.New(&b, textedit.WrapIndent("", " ", 116, 120))
|
||||||
|
w.Write([]byte(text))
|
||||||
|
w.Flush()
|
||||||
|
const expect = `
|
||||||
|
Walking through the mixed forests of Brandenburg in early autumn, one notices the dominant presence of Scots pine
|
||||||
|
(Pinus sylvestris) interspersed with sessile oak (Quercus petraea) and silver birch (Betula pendula), their canopies
|
||||||
|
creating a mosaic of light and shadow on the forest floor. The sandy, acidic soils typical of this region support a
|
||||||
|
ground layer rich in ericaceous plants, particularly bilberry (Vaccinium myrtillus) with its dark-green oval leaves
|
||||||
|
now tinged with burgundy, and the occasional patch of heather (Calluna vulgaris) persisting in sunnier clearings.
|
||||||
|
Closer inspection of the understory reveals common wood sorrel (Oxalis acetosella) thriving in moister pockets,
|
||||||
|
while various moss species—including the feathery fronds of Hypnum cupressiforme—carpet fallen logs in varying
|
||||||
|
stages of decay. The drier sections host wavy hair-grass (Deschampsia flexuosa) in delicate tufts, and where old
|
||||||
|
pines have been felled, pioneering stands of downy birch and rowan (Sorbus aucuparia) compete for space, their
|
||||||
|
growth marking the forest's continuous cycle of regeneration in this characteristically glacial landscape of the
|
||||||
|
North European Plain.`
|
||||||
|
|
||||||
|
if "\n"+b.String() != expect {
|
||||||
|
t.Fatal("\n" + b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("tab is 8", func(t *testing.T) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
w := textedit.New(&b, textedit.WrapIndent("\t", "", 12, 12))
|
||||||
|
w.Write([]byte("foo bar\n baz\nqux quux\n"))
|
||||||
|
w.Flush()
|
||||||
|
if b.String() != "\tfoo\nbar baz qux\nquux" {
|
||||||
|
t.Fatal(b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("wrap on release", func(t *testing.T) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
w := textedit.New(&b, textedit.WrapIndent(" ", " ", 15, 15))
|
||||||
|
w.Write([]byte("Some sample text...\n on multiple lines."))
|
||||||
|
w.Flush()
|
||||||
|
|
||||||
|
const expect = `
|
||||||
|
Some sample
|
||||||
|
text... on
|
||||||
|
multiple
|
||||||
|
lines.`
|
||||||
|
|
||||||
|
if "\n"+b.String() != expect {
|
||||||
|
t.Fatal("\n" + b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("zero width", func(t *testing.T) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
w := textedit.New(&b, textedit.WrapIndent("xx", "xx", 0, 0))
|
||||||
|
w.Write([]byte("foo bar\n baz\nqux quux\n"))
|
||||||
|
w.Flush()
|
||||||
|
if b.String() != "xxfoo\nxxbar\nxxbaz\nxxqux\nxxquux" {
|
||||||
|
t.Fatal(b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("negative width", func(t *testing.T) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
w := textedit.New(&b, textedit.WrapIndent("xx", "xx", -2, -2))
|
||||||
|
w.Write([]byte("foo bar\n baz\nqux quux\n"))
|
||||||
|
w.Flush()
|
||||||
|
if b.String() != "xxfoo\nxxbar\nxxbaz\nxxqux\nxxquux" {
|
||||||
|
t.Fatal(b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("last word on the same line", func(t *testing.T) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
w := textedit.New(&b, textedit.WrapIndent("xx", "xx", 9, 9))
|
||||||
|
w.Write([]byte("foo bar"))
|
||||||
|
w.Flush()
|
||||||
|
if b.String() != "xxfoo bar" {
|
||||||
|
t.Fatal(b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user