init repo
This commit is contained in:
commit
94a26e5192
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
.cover
|
||||
25
Makefile
Normal file
25
Makefile
Normal file
@ -0,0 +1,25 @@
|
||||
sources = $(shell find . -name "*.go")
|
||||
|
||||
default: build
|
||||
|
||||
build: $(sources)
|
||||
go build
|
||||
|
||||
fmt: $(sources)
|
||||
go fmt
|
||||
|
||||
check: $(sources)
|
||||
go test -count 1
|
||||
|
||||
.cover: $(sources)
|
||||
go test -count 1 -coverprofile .cover
|
||||
|
||||
cover: .cover
|
||||
go tool cover -func .cover
|
||||
|
||||
showcover: .cover
|
||||
go tool cover -html .cover
|
||||
|
||||
clean:
|
||||
go clean
|
||||
rm .cover
|
||||
25
escape_test.go
Normal file
25
escape_test.go
Normal file
@ -0,0 +1,25 @@
|
||||
package textedit_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"code.squareroundforest.org/arpio/textedit"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEscape(t *testing.T) {
|
||||
t.Run("basic", func(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
w := textedit.New(&b, textedit.Escape('\\', '\n', '\t', '"'))
|
||||
if _, err := w.Write([]byte("foo\nbar\t\"baz\"")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if b.String() != "foo\\\nbar\\\t\\\"baz\\\"" {
|
||||
t.Fatal(b.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
3
go.mod
Normal file
3
go.mod
Normal file
@ -0,0 +1,3 @@
|
||||
module code.squareroundforest.org/arpio/textedit
|
||||
|
||||
go 1.25.3
|
||||
168
indent.go
Normal file
168
indent.go
Normal file
@ -0,0 +1,168 @@
|
||||
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
|
||||
}
|
||||
|
||||
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, state.currentWord...)
|
||||
state.currentLineLength = wl
|
||||
state.multipleLines = true
|
||||
state.currentWord = nil
|
||||
return ret, state
|
||||
}
|
||||
|
||||
if ws && cl > 0 && cl+wl+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) func(wrapIndentState) []rune {
|
||||
return func(state wrapIndentState) []rune {
|
||||
if len(state.currentWord) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var ret []rune
|
||||
indent := first
|
||||
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),
|
||||
)
|
||||
}
|
||||
404
indent_test.go
Normal file
404
indent_test.go
Normal file
@ -0,0 +1,404 @@
|
||||
package textedit_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"code.squareroundforest.org/arpio/textedit"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIndent(t *testing.T) {
|
||||
t.Run("no indent no wrap", func(t *testing.T) {
|
||||
t.Run("empty", func(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
w := textedit.New(&b, textedit.Indent("", ""))
|
||||
if _, err := w.Write(nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if b.String() != "" {
|
||||
t.Fatal(b.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("new line", func(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
w := textedit.New(&b, textedit.Indent("", ""))
|
||||
if _, err := w.Write([]byte("\n")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if b.String() != "\n" {
|
||||
t.Fatal(b.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("basic", func(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
w := textedit.New(&b, textedit.Indent("", ""))
|
||||
if _, err := w.Write([]byte("foo bar\n baz\nqux quux")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if b.String() != "foo bar\nbaz\nqux quux" {
|
||||
t.Fatal(b.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("closing new line", func(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
w := textedit.New(&b, textedit.Indent("", ""))
|
||||
if _, err := w.Write([]byte("foo bar\n baz\nqux quux\n")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if b.String() != "foo bar\nbaz\nqux quux\n" {
|
||||
t.Fatal(b.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("word mid line", func(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
w := textedit.New(&b, textedit.Indent("", ""))
|
||||
if _, err := w.Write([]byte("foo bar bar foo\n baz\nqux quux\n")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if b.String() != "foo bar bar foo\nbaz\nqux quux\n" {
|
||||
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"))
|
||||
if _, err := w.Write([]byte("foo bar bar foo\n baz\nqux quux\nfoo")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
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("first and rest same", func(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
w := textedit.New(&b, textedit.Indent("xx", "xx"))
|
||||
if _, err := w.Write([]byte("foo bar\n baz\nqux quux\n")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if b.String() != "xxfoo bar\nxxbaz\nxxqux quux\n" {
|
||||
t.Fatal(b.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("first out", func(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
w := textedit.New(&b, textedit.Indent("", "xx"))
|
||||
if _, err := w.Write([]byte("foo bar\n baz\nqux quux\n")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if b.String() != "foo bar\nxxbaz\nxxqux quux\n" {
|
||||
t.Fatal(b.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("first in", func(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
w := textedit.New(&b, textedit.Indent("xx", ""))
|
||||
if _, err := w.Write([]byte("foo bar\n baz\nqux quux\n")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if b.String() != "xxfoo bar\nbaz\nqux quux\n" {
|
||||
t.Fatal(b.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("first out offset", func(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
w := textedit.New(&b, textedit.Indent("xx", "xxxx"))
|
||||
if _, err := w.Write([]byte("foo bar\n baz\nqux quux\n")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if b.String() != "xxfoo bar\nxxxxbaz\nxxxxqux quux\n" {
|
||||
t.Fatal(b.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("first in offset", func(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
w := textedit.New(&b, textedit.Indent("xxxx", "xx"))
|
||||
if _, err := w.Write([]byte("foo bar\n baz\nqux quux\n")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if b.String() != "xxxxfoo bar\nxxbaz\nxxqux quux\n" {
|
||||
t.Fatal(b.String())
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
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))
|
||||
if _, err := w.Write([]byte("foo bar\n baz\nqux quux\n")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
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))
|
||||
if _, err := w.Write([]byte("foo bar\n baz\nqux quux\n")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
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))
|
||||
if _, err := w.Write([]byte("foo bar\n baz\nqux quux\n")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
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))
|
||||
if _, err := w.Write([]byte("foo bar\n baz\nqux quux\n")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
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))
|
||||
if _, err := w.Write([]byte(text)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
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))
|
||||
if _, err := w.Write([]byte(text)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
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))
|
||||
if _, err := w.Write([]byte(text)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
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))
|
||||
if _, err := w.Write([]byte(text)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
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))
|
||||
if _, err := w.Write([]byte("foo bar\n baz\nqux quux\n")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if b.String() != "\tfoo\nbar baz qux\nquux" {
|
||||
t.Fatal(b.String())
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
122
lib.go
Normal file
122
lib.go
Normal file
@ -0,0 +1,122 @@
|
||||
package textedit
|
||||
|
||||
import "io"
|
||||
|
||||
type Editor interface {
|
||||
Edit(r rune, state any) ([]rune, any)
|
||||
ReleaseState(state any) []rune
|
||||
}
|
||||
|
||||
type editorFunc[S any] struct {
|
||||
edit func(rune, S) ([]rune, S)
|
||||
releaseState func(S) []rune
|
||||
}
|
||||
|
||||
type Writer struct {
|
||||
out io.Writer
|
||||
editor Editor
|
||||
state any
|
||||
err error
|
||||
}
|
||||
|
||||
func (e editorFunc[S]) Edit(r rune, state any) ([]rune, any) {
|
||||
s, _ := state.(S)
|
||||
return e.edit(r, s)
|
||||
}
|
||||
|
||||
func (e editorFunc[S]) ReleaseState(state any) []rune {
|
||||
s, _ := state.(S)
|
||||
r := e.releaseState(s)
|
||||
return r
|
||||
}
|
||||
|
||||
func Func[S any](edit func(r rune, state S) ([]rune, S), releaseState func(S) []rune) Editor {
|
||||
if edit == nil {
|
||||
edit = func(r rune, s S) ([]rune, S) { return []rune{r}, s }
|
||||
}
|
||||
|
||||
if releaseState == nil {
|
||||
releaseState = func(S) []rune { return nil }
|
||||
}
|
||||
|
||||
return editorFunc[S]{edit: edit, releaseState: releaseState}
|
||||
}
|
||||
|
||||
func Replace(a ...string) Editor {
|
||||
return replace(a...)
|
||||
}
|
||||
|
||||
func Escape(esc rune, chars ...rune) Editor {
|
||||
return escape(esc, chars...)
|
||||
}
|
||||
|
||||
func Indent(first, rest string) Editor {
|
||||
return wrapIndent([]rune(first), []rune(rest), 0, 0)
|
||||
}
|
||||
|
||||
func Wrap(firstWidth, restWidth int) Editor {
|
||||
return wrapIndent(nil, nil, firstWidth, restWidth)
|
||||
}
|
||||
|
||||
func WrapIndent(firstIndent, restIndent string, firstWidth, restWidth int) Editor {
|
||||
return wrapIndent([]rune(firstIndent), []rune(restIndent), firstWidth, restWidth)
|
||||
}
|
||||
|
||||
func SingleLine() Editor {
|
||||
return sequence(
|
||||
replace("\n", " "),
|
||||
wrapIndent(nil, nil, 0, 0),
|
||||
)
|
||||
}
|
||||
|
||||
func New(out io.Writer, e ...Editor) *Writer {
|
||||
return &Writer{
|
||||
out: out,
|
||||
editor: sequence(e...),
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Writer) write(r []rune) error {
|
||||
if w.err != nil {
|
||||
return w.err
|
||||
}
|
||||
|
||||
for _, ri := range r {
|
||||
rr, s := w.editor.Edit(ri, w.state)
|
||||
if _, err := w.out.Write([]byte(string(rr))); err != nil {
|
||||
w.err = err
|
||||
return w.err
|
||||
}
|
||||
|
||||
w.state = s
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *Writer) flush() error {
|
||||
if w.err != nil {
|
||||
return w.err
|
||||
}
|
||||
|
||||
r := w.editor.ReleaseState(w.state)
|
||||
w.state = nil
|
||||
if _, err := w.out.Write([]byte(string(r))); err != nil {
|
||||
w.err = err
|
||||
return w.err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *Writer) Write(p []byte) (int, error) {
|
||||
return len(p), w.write([]rune(string(p)))
|
||||
}
|
||||
|
||||
func (w *Writer) WriteRune(r rune) (int, error) {
|
||||
return len([]byte(string(r))), w.write([]rune{r})
|
||||
}
|
||||
|
||||
func (w *Writer) Flush() error {
|
||||
return w.flush()
|
||||
}
|
||||
114
lib_test.go
Normal file
114
lib_test.go
Normal file
@ -0,0 +1,114 @@
|
||||
package textedit_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"code.squareroundforest.org/arpio/textedit"
|
||||
"errors"
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var errTest = errors.New("test")
|
||||
|
||||
type failingWriter struct {
|
||||
out io.Writer
|
||||
fail bool
|
||||
}
|
||||
|
||||
func (w *failingWriter) Write(p []byte) (int, error) {
|
||||
if w.fail {
|
||||
return 0, errTest
|
||||
}
|
||||
|
||||
return w.out.Write(p)
|
||||
}
|
||||
|
||||
func TestNoop(t *testing.T) {
|
||||
t.Run("editor", func(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
w := textedit.New(&b, textedit.Func(nil, func(int) []rune { return nil }))
|
||||
if _, err := w.Write([]byte("foo bar baz")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if b.String() != "foo bar baz" {
|
||||
t.Fatal(b.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("release", func(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
w := textedit.New(&b, textedit.Func(func(r rune, s int) ([]rune, int) { return []rune{r}, 9 }, nil))
|
||||
if _, err := w.Write([]byte("foo bar baz")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if b.String() != "foo bar baz" {
|
||||
t.Fatal(b.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestWriteRune(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
w := textedit.New(&b, textedit.Replace("o", "e"))
|
||||
for _, r := range []rune("foo bar baz") {
|
||||
if _, err := w.WriteRune(r); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if b.String() != "fee bar baz" {
|
||||
t.Fatal(b.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestFailingWriter(t *testing.T) {
|
||||
t.Run("after write", func(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
fw := failingWriter{out: &b}
|
||||
w := textedit.New(&fw, textedit.Replace("o", "e"))
|
||||
if _, err := w.Write([]byte("foo ")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fw.fail = true
|
||||
if _, err := w.Write([]byte("bar")); !errors.Is(err, errTest) {
|
||||
t.Fatal("failed to fail with the right error", err)
|
||||
}
|
||||
|
||||
if _, err := w.Write([]byte("bar")); !errors.Is(err, errTest) {
|
||||
t.Fatal("failed to fail with the right error", err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); !errors.Is(err, errTest) {
|
||||
t.Fatal("failed to fail with the right error", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("after flush", func(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
fw := failingWriter{out: &b}
|
||||
w := textedit.New(&fw, textedit.Replace("o", "e"))
|
||||
if _, err := w.Write([]byte("foo bar")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fw.fail = true
|
||||
if err := w.Flush(); !errors.Is(err, errTest) {
|
||||
t.Fatal("failed to fail with the right error", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
56
replace.go
Normal file
56
replace.go
Normal file
@ -0,0 +1,56 @@
|
||||
package textedit
|
||||
|
||||
func replaceEdit(match, replacement []rune) func(rune, []rune) ([]rune, []rune) {
|
||||
return func(r rune, state []rune) ([]rune, []rune) {
|
||||
state = append(state, r)
|
||||
if len(state) > len(match) {
|
||||
return state, nil
|
||||
}
|
||||
|
||||
if r != match[len(state)-1] {
|
||||
return state, nil
|
||||
}
|
||||
|
||||
if len(state) == len(match) {
|
||||
return replacement, nil
|
||||
}
|
||||
|
||||
return nil, state
|
||||
}
|
||||
}
|
||||
|
||||
func replaceRelease(state []rune) []rune {
|
||||
return state
|
||||
}
|
||||
|
||||
func replaceOne(match, replacement string) Editor {
|
||||
return Func(
|
||||
replaceEdit([]rune(match), []rune(replacement)),
|
||||
replaceRelease,
|
||||
)
|
||||
}
|
||||
|
||||
func replace(a ...string) Editor {
|
||||
if len(a)%2 != 0 {
|
||||
a = append(a, "")
|
||||
}
|
||||
|
||||
var e []Editor
|
||||
for i := 0; i < len(a); i += 2 {
|
||||
e = append(
|
||||
e,
|
||||
replaceOne(a[i], a[i+1]),
|
||||
)
|
||||
}
|
||||
|
||||
return sequence(e...)
|
||||
}
|
||||
|
||||
func escape(esc rune, chars ...rune) Editor {
|
||||
r := []string{string(esc), string([]rune{esc, esc})}
|
||||
for _, c := range chars {
|
||||
r = append(r, string(c), string([]rune{esc, c}))
|
||||
}
|
||||
|
||||
return replace(r...)
|
||||
}
|
||||
329
replace_test.go
Normal file
329
replace_test.go
Normal file
@ -0,0 +1,329 @@
|
||||
package textedit_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"code.squareroundforest.org/arpio/textedit"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReplace(t *testing.T) {
|
||||
t.Run("empty and no match", func(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
w := textedit.New(&b, textedit.Replace())
|
||||
if _, err := w.Write(nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if b.String() != "" {
|
||||
t.Fatal(b.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty and empty match", func(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
w := textedit.New(&b, textedit.Replace("r"))
|
||||
if _, err := w.Write(nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if b.String() != "" {
|
||||
t.Fatal(b.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty one single char match", func(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
w := textedit.New(&b, textedit.Replace("r", "z"))
|
||||
if _, err := w.Write(nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if b.String() != "" {
|
||||
t.Fatal(b.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty multiple single char matches", func(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
w := textedit.New(&b, textedit.Replace("o", "e", "r", "z"))
|
||||
if _, err := w.Write(nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if b.String() != "" {
|
||||
t.Fatal(b.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty one multi-char match", func(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
w := textedit.New(&b, textedit.Replace("oo", "ee"))
|
||||
if _, err := w.Write(nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if b.String() != "" {
|
||||
t.Fatal(b.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty multiple multi-char matches", func(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
w := textedit.New(&b, textedit.Replace("oo", "ee", "ar", "az"))
|
||||
if _, err := w.Write(nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if b.String() != "" {
|
||||
t.Fatal(b.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no match empty match", func(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
w := textedit.New(&b, textedit.Replace("r"))
|
||||
if _, err := w.Write([]byte("foo")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if b.String() != "foo" {
|
||||
t.Fatal(b.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no match one single char match", func(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
w := textedit.New(&b, textedit.Replace("r", "z"))
|
||||
if _, err := w.Write([]byte("foo")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if b.String() != "foo" {
|
||||
t.Fatal(b.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no match multiple single char matches", func(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
w := textedit.New(&b, textedit.Replace("b", "f", "r", "z"))
|
||||
if _, err := w.Write([]byte("foo")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if b.String() != "foo" {
|
||||
t.Fatal(b.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no match one multi-char match", func(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
w := textedit.New(&b, textedit.Replace("bar", "baz"))
|
||||
if _, err := w.Write([]byte("foo")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if b.String() != "foo" {
|
||||
t.Fatal(b.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no match multiple multi-char matches", func(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
w := textedit.New(&b, textedit.Replace("bar", "baz", "qux", "quuz"))
|
||||
if _, err := w.Write([]byte("foo")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if b.String() != "foo" {
|
||||
t.Fatal(b.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("match empty match", func(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
w := textedit.New(&b, textedit.Replace("a"))
|
||||
if _, err := w.Write([]byte("bar")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if b.String() != "br" {
|
||||
t.Fatal(b.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("match one single char match", func(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
w := textedit.New(&b, textedit.Replace("r", "z"))
|
||||
if _, err := w.Write([]byte("bar")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if b.String() != "baz" {
|
||||
t.Fatal(b.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("match multiple single char matches", func(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
w := textedit.New(&b, textedit.Replace("o", "e", "r", "z"))
|
||||
if _, err := w.Write([]byte("foo bar")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if b.String() != "fee baz" {
|
||||
t.Fatal(b.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("match one multi-char match", func(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
w := textedit.New(&b, textedit.Replace("oo", "ee"))
|
||||
if _, err := w.Write([]byte("foo bar")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if b.String() != "fee bar" {
|
||||
t.Fatal(b.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("match multiple multi-char matches", func(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
w := textedit.New(&b, textedit.Replace("oo", "ee", "ar", "az"))
|
||||
if _, err := w.Write([]byte("foo bar")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if b.String() != "fee baz" {
|
||||
t.Fatal(b.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("inverse order", func(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
w := textedit.New(&b, textedit.Replace("ar", "az", "oo", "ee"))
|
||||
if _, err := w.Write([]byte("foo bar")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if b.String() != "fee baz" {
|
||||
t.Fatal(b.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("mismatch after partial match", func(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
w := textedit.New(&b, textedit.Replace("foo", "bar"))
|
||||
if _, err := w.Write([]byte("fox baz")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if b.String() != "fox baz" {
|
||||
t.Fatal(b.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty match", func(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
w := textedit.New(&b, textedit.Replace("", "bar"))
|
||||
if _, err := w.Write([]byte("foo baz")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if b.String() != "foo baz" {
|
||||
t.Fatal(b.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("delete", func(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
w := textedit.New(&b, textedit.Replace("bar", ""))
|
||||
if _, err := w.Write([]byte("foo bar")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if b.String() != "foo " {
|
||||
t.Fatal(b.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
64
sequence.go
Normal file
64
sequence.go
Normal file
@ -0,0 +1,64 @@
|
||||
package textedit
|
||||
|
||||
func sequenceEdit(e []Editor) func(rune, []any) ([]rune, []any) {
|
||||
return func(r rune, s []any) ([]rune, []any) {
|
||||
var sr []any
|
||||
rr := []rune{r}
|
||||
for i, ei := range e {
|
||||
var si any
|
||||
if len(s) > i {
|
||||
si = s[i]
|
||||
}
|
||||
|
||||
rin := rr
|
||||
rr = nil
|
||||
for _, ri := range rin {
|
||||
var rout []rune
|
||||
rout, si = ei.Edit(ri, si)
|
||||
rr = append(rr, rout...)
|
||||
}
|
||||
|
||||
sr = append(sr, si)
|
||||
}
|
||||
|
||||
return rr, sr
|
||||
}
|
||||
}
|
||||
|
||||
func sequenceRelease(e []Editor) func([]any) []rune {
|
||||
return func(s []any) []rune {
|
||||
var rr []rune
|
||||
for i, ei := range e {
|
||||
var r []rune
|
||||
if i < len(s) {
|
||||
r = ei.ReleaseState(s[i])
|
||||
}
|
||||
|
||||
for _, ri := range r {
|
||||
var (
|
||||
rri []rune
|
||||
ssi []any
|
||||
)
|
||||
|
||||
if i < len(s) {
|
||||
ssi = s[i:]
|
||||
}
|
||||
|
||||
rri, ssi = sequenceEdit(e[i+1:])(ri, ssi)
|
||||
rr = append(rr, rri...)
|
||||
if i < len(s) {
|
||||
s = append(s[:i], ssi...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rr
|
||||
}
|
||||
}
|
||||
|
||||
func sequence(e ...Editor) Editor {
|
||||
return Func(
|
||||
sequenceEdit(e),
|
||||
sequenceRelease(e),
|
||||
)
|
||||
}
|
||||
25
singleline_test.go
Normal file
25
singleline_test.go
Normal file
@ -0,0 +1,25 @@
|
||||
package textedit_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"code.squareroundforest.org/arpio/textedit"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSingleLine(t *testing.T) {
|
||||
t.Run("basic", func(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
w := textedit.New(&b, textedit.SingleLine())
|
||||
if _, err := w.Write([]byte("\nfoo bar\n\nbaz \nqux quux\n")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if b.String() != "foo bar baz qux quux" {
|
||||
t.Fatal(b.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user