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