// 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 } if f, ok := w.out.(interface{ Flush() error }); ok { if err := f.Flush(); err != nil { w.err = err return w.err } } else if f, ok := w.out.(interface{ Flush() }); ok { f.Flush() } 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. // // If the underlying io.Writer also implements Flusher (Flush() error or Flush()), then it will be implicitly // called. func (w *Writer) Flush() error { return w.flush() }