// 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 init S } // 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]) initialize() any { return e.init } 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) } // FuncInit can be used to define an Editor providing only the edit and releaseState functions, and the initial // state. func FuncInit[S any](edit func(r rune, state S) ([]rune, S), releaseState func(S) []rune, init S) 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, init: init} } // 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 { var s S return FuncInit(edit, releaseState, s) } // 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...) } // Indent applies indentation to multi-line text. Whitespaces are preserved. func Indent(first, rest string) Editor { return indent([]rune(first), []rune(rest)) } // 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 singleLine() } // 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 { seq := sequence(e...) return &Writer{ out: out, editor: seq, state: seq.(interface{ initialize() any }).initialize(), } } 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 initializer, ok := w.editor.(interface{ initialize() any }); ok { w.state = initializer.initialize() } 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. Is there were no errors, // and 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. The Flush method of underlying writers is not called. func (w *Writer) Flush() error { return w.flush() }