1
0
textedit/lib.go

167 lines
5.3 KiB
Go

// Package textedit provides a non-regexp, streaming editor to apply basic text manipulation.
package textedit
import (
"io"
"unicode"
)
// Editor instances can be used to edit a text stream. It is expected from the implementations to be reusable
// with fresh state after the Flush was called on the enclosing writer.
type Editor interface {
// Edit takes an input rune and the current state of the editor, and returns zero or more resulting runes
// and the updated state.
Edit(r rune, state any) ([]rune, any)
// On flushing the editing writer, the writer will call ReleaseState on each provided editor in the
// configured sequence with their latest associated state, and if there is any pending text to be written
// out, the editors should return it at that point.
ReleaseState(state any) []rune
}
type editorFunc[S any] struct {
edit func(rune, S) ([]rune, S)
releaseState func(S) []rune
}
// Writer implements the io.Writer interface, passing the input to every editor in the configured sequence, and
// writing out the edited text to the underlying io.Writer.
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)
return e.releaseState(s)
}
// Func can be used to define an Editor providing only the edit and releaseState functions.
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}
}
// Replace is a built-in editor for simple replace operations. The arguments are mapped such that the ones at
// odd positions will be the sequences to match in the input, and the immediately following ones will be the
// sequences replacing the matched sequences. Empty replacement sequences can be used to deleted the matched
// ones. In case of odd number of arguments, an empty arg is automatically appended.
func Replace(a ...string) Editor {
return replace(a...)
}
// Escape can be used for basic escaping, where the found characters from the chars argument are replaced with
// the same character prefixed by the escape char in the esc argument. The escape char will be escaped
// automatically.
func Escape(esc rune, chars ...rune) Editor {
return escape(esc, chars...)
}
// Indent applies indentation to multi-line text. Duplicate, leading, trailing and non-space whitespaces, other
// than the newline character, are collapsed into a single space.
func Indent(first, rest string) Editor {
return wrapIndent([]rune(first), []rune(rest), 0, 0)
}
// Wrap wrap wraps multiline text to define maximual text width. Duplicate, leading, trailing and non-space
// whitespaces are collapsed into a single space.
func Wrap(firstWidth, restWidth int) Editor {
return wrapIndent(nil, nil, firstWidth, restWidth)
}
// WrapIndent is like Wrap, but applies indentation.
func WrapIndent(firstIndent, restIndent string, firstWidth, restWidth int) Editor {
return wrapIndent([]rune(firstIndent), []rune(restIndent), firstWidth, restWidth)
}
// SingleLine is like Wrap, but with infinite text width.
func SingleLine() Editor {
return sequence(
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.
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 {
if ri == unicode.ReplacementChar {
continue
}
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
}
// Write calls the configured editors with the input and forwards their output to the underlying io.Writer. It
// always returns len(p) as the number bytes written. It only returns an error when the underlying writer
// returns and error.
func (w *Writer) Write(p []byte) (int, error) {
return len(p), w.write([]rune(string(p)))
}
// WriteRune calls the configured editors with the input and forwards their output to the underlying io.Writer.
// It always returns byte length of the input rune as the number bytes written. It only returns an error when
// the underlying writer returns and error.
func (w *Writer) WriteRune(r rune) (int, error) {
return len([]byte(string(r))), w.write([]rune{r})
}
// Flush makes the underlying editors to release their associated state, and writes out the resulting text to
// the underlying io.Writer, but first passes it to the subsequent editors for editing. When the used editor
// instances comply with the expectations of the Editor interface, the writer will have a fresh state and can be
// reused for further editing.
func (w *Writer) Flush() error {
return w.flush()
}