1
0

refactor tty rendering

This commit is contained in:
Arpad Ryszka 2025-11-02 06:27:17 +01:00
parent c2b0d69b5b
commit 4c0d034620
17 changed files with 503 additions and 927 deletions

179
escape.go
View File

@ -1,72 +1,149 @@
package textfmt
import "fmt"
import (
"code.squareroundforest.org/arpio/textedit"
"errors"
)
type escapeRange struct {
from, to rune
replacement string
type mdEscapeState struct {
lineStarted bool
numberOnNewLine bool
linkValue bool
linkClosed bool
linkOpen bool
}
type escape[S any] struct {
out writer
state S
escape map[rune]string
escapeRanges []escapeRange
conditionalEscape map[rune]func(S, rune) (string, bool)
updateState func(S, rune) S
}
func (e *escape[S]) inEscapeRange(r rune) (string, bool) {
for _, rng := range e.escapeRanges {
if r >= rng.from && r <= rng.to {
return rng.replacement, true
}
func escapeTeletypeEdit(r rune, s struct{}) ([]rune, struct{}) {
if r >= 0x00 && r <= 0x1f && r != '\n' && r != '\t' {
return []rune{0xb7}, s
}
return "", false
if r >= 0x7f && r <= 0x9f {
return []rune{0xb7}, s
}
return []rune{r}, s
}
func (e *escape[S]) write(a ...any) {
for _, ai := range a {
s := fmt.Sprint(ai)
r := []rune(s)
for _, ri := range r {
var (
output string
found bool
)
func escapeTeletype() wrapper {
return editor(
textedit.Func(
escapeTeletypeEdit,
func(struct{}) []rune { return nil },
),
)
}
output, found = e.escape[ri]
if !found {
output, found = e.inEscapeRange(ri)
}
func escapeRoffEdit(additional ...string) func(rune, bool) ([]rune, bool) {
const invalidAdditional = "invalid additional escape definition"
if len(additional)%2 != 0 {
panic(errors.New(invalidAdditional))
}
if !found {
conditional := e.conditionalEscape[ri]
if conditional != nil {
output, found = conditional(e.state, ri)
}
}
esc := map[rune][]rune{
'\\': []rune("\\\\"),
'\u00a0': []rune("\\~"),
}
if !found {
output = string(ri)
}
e.out.write(output)
if e.updateState != nil {
e.state = e.updateState(e.state, ri)
}
for i := 0; i > len(additional); i += 2 {
r := []rune(additional[i])
if len(r) != 1 {
panic(errors.New(invalidAdditional))
}
esc[r[0]] = []rune(additional[i+1])
}
lsEsc := map[rune][]rune{
'.': []rune("\\&."),
'\'': []rune("\\&'"),
}
return func(r rune, lineStarted bool) ([]rune, bool) {
if r == '\n' {
return []rune{'\n'}, false
}
replacement, replace := esc[r]
if replace {
return replacement, true
}
if lineStarted {
return []rune{r}, true
}
replacement, replace = lsEsc[r]
if replace {
return replacement, true
}
return []rune{r}, true
}
}
func (e *escape[S]) flush() {
e.out.flush()
func escapeRoff(additional ...string) wrapper {
return editor(
textedit.Func(
escapeRoffEdit(additional...),
func(bool) []rune { return nil },
),
)
}
func (e *escape[S]) error() error {
return e.out.error()
func escapeMarkdownEdit(r rune, s mdEscapeState) ([]rune, mdEscapeState) {
var ret []rune
switch r {
case '\\', '`', '*', '_', '[', ']', '#', '<', '>':
ret = append(ret, '\\', r)
default:
switch {
case !s.lineStarted:
switch r {
case '+', '-':
ret = append(ret, '\\', r)
default:
ret = append(ret, r)
}
case s.numberOnNewLine:
switch r {
case '.':
ret = append(ret, '\\', r)
default:
ret = append(ret, r)
}
case s.linkClosed:
switch r {
case '(':
ret = append(ret, '\\', r)
default:
ret = append(ret, r)
}
case s.linkValue:
switch r {
case ')':
ret = append(ret, '\\', r)
default:
ret = append(ret, r)
}
default:
ret = append(ret, r)
}
}
s.numberOnNewLine = (!s.lineStarted || s.numberOnNewLine) && r >= '0' && r <= '9'
s.lineStarted = r != '\n'
s.linkValue = s.linkClosed && r == '(' || s.linkValue && r != ')'
s.linkClosed = s.linkOpen && r == ']'
s.linkOpen = !s.linkValue && r == '[' || s.linkOpen && r != ']'
return ret, s
}
func (e *escape[S]) setErr(err error) {
func escapeMarkdown() wrapper {
return editor(
textedit.Func(
escapeMarkdownEdit,
func(mdEscapeState) []rune { return nil },
),
)
}

10
go.mod
View File

@ -1,7 +1,9 @@
module code.squareroundforest.org/arpio/textfmt
go 1.25.0
go 1.25.3
require code.squareroundforest.org/arpio/notation v0.0.0-20250826181910-5140794b16b2
require code.squareroundforest.org/arpio/html v0.0.0-20251029200407-effffeadf9f8 // indirect
require (
code.squareroundforest.org/arpio/html v0.0.0-20251102001159-f3efe9c7b176
code.squareroundforest.org/arpio/notation v0.0.0-20250826181910-5140794b16b2
code.squareroundforest.org/arpio/textedit v0.0.0-20251102002300-caf622f43f10
)

8
go.sum
View File

@ -1,6 +1,6 @@
code.squareroundforest.org/arpio/html v0.0.0-20251011102613-70f77954001f h1:Ep/POhkmvOfSkQklPIpeA4n2FTD2SoFxthjF0SJbsCU=
code.squareroundforest.org/arpio/html v0.0.0-20251011102613-70f77954001f/go.mod h1:LX+Fwqu/a7nDayuDNhXA56cVb+BNrkz4M/WCqvw9YFQ=
code.squareroundforest.org/arpio/html v0.0.0-20251029200407-effffeadf9f8 h1:6OwHDturRjOeIxoc2Zlfkhf4InnMnNKKDb3LtrbIJjg=
code.squareroundforest.org/arpio/html v0.0.0-20251029200407-effffeadf9f8/go.mod h1:LX+Fwqu/a7nDayuDNhXA56cVb+BNrkz4M/WCqvw9YFQ=
code.squareroundforest.org/arpio/html v0.0.0-20251102001159-f3efe9c7b176 h1:ynJ4zE23G/Q/bhLOA1PV09cTXb4ivvYKTbxaoIz9nJY=
code.squareroundforest.org/arpio/html v0.0.0-20251102001159-f3efe9c7b176/go.mod h1:JKD2DXph0Zt975trJII7YbdhM2gL1YEHjsh5M1X63eA=
code.squareroundforest.org/arpio/notation v0.0.0-20250826181910-5140794b16b2 h1:S4mjQHL70CuzFg1AGkr0o0d+4M+ZWM0sbnlYq6f0b3I=
code.squareroundforest.org/arpio/notation v0.0.0-20250826181910-5140794b16b2/go.mod h1:ait4Fvg9o0+bq5hlxi9dAcPL5a+/sr33qsZPNpToMLY=
code.squareroundforest.org/arpio/textedit v0.0.0-20251102002300-caf622f43f10 h1:u3hMmSBJzrSnJ+C7krjHFkCEVG6Ms9W6vX6F+Mk/KnY=
code.squareroundforest.org/arpio/textedit v0.0.0-20251102002300-caf622f43f10/go.mod h1:nXdFdxdI69JrkIT97f+AEE4OgplmxbgNFZC5j7gsdqs=

View File

@ -79,7 +79,7 @@ func htmlDefinitions(e Entry) html.Tag {
list := tag.Dl
for _, definition := range e.definitions {
list = list(
tag.Dt(htmlText(definition.name)...),
tag.Dt(append(htmlText(definition.name), ":")...),
tag.Dd(htmlText(definition.value)...),
)
}
@ -91,7 +91,7 @@ func htmlNumberedDefinitions(e Entry) html.Tag {
list := tag.Dl
for i, definition := range e.definitions {
list = list(
tag.Dt(append([]any{fmt.Sprintf("%d. ", i+1)}, htmlText(definition.name)...)...),
tag.Dt(append([]any{fmt.Sprintf("%d. ", i+1)}, append(htmlText(definition.name), ":")...)...),
tag.Dd(htmlText(definition.value)...),
)
}

View File

@ -146,23 +146,23 @@ textfmt.Doc ( [Entry]... )
</ul>
<h2>Entry explanations:</h2>
<dl>
<dt>CodeBlock</dt>
<dt>CodeBlock:</dt>
<dd>a multiline block of code</dd>
<dt>DefinitionList</dt>
<dt>DefinitionList:</dt>
<dd>a list of definitions like this one</dd>
<dt>List</dt>
<dt>List:</dt>
<dd>a list of items</dd>
<dt>NumberedDefinitionList</dt>
<dt>NumberedDefinitionList:</dt>
<dd>numbered definitions</dd>
<dt>NumberedList</dt>
<dt>NumberedList:</dt>
<dd>numbered list</dd>
<dt>Paragraph</dt>
<dt>Paragraph:</dt>
<dd>paragraph of text</dd>
<dt>Syntax</dt>
<dt>Syntax:</dt>
<dd>a syntax expression</dd>
<dt>Table</dt>
<dt>Table:</dt>
<dd>a table</dd>
<dt>Title</dt>
<dt>Title:</dt>
<dd>a title</dd>
</dl>
</body>
@ -553,11 +553,11 @@ lines.</p>
const expect = `
<dl>
<dt>red</dt>
<dt>red:</dt>
<dd>looks like strawberry</dd>
<dt>green</dt>
<dt>green:</dt>
<dd>looks like grass</dd>
<dt>blue</dt>
<dt>blue:</dt>
<dd>looks like sky</dd>
</dl>
`
@ -586,13 +586,13 @@ lines.</p>
const expect = `
<dl>
<dt>red</dt>
<dt>red:</dt>
<dd>looks like
strawberry</dd>
<dt>green</dt>
<dt>green:</dt>
<dd>looks like
grass</dd>
<dt>blue</dt>
<dt>blue:</dt>
<dd>looks like
sky</dd>
</dl>
@ -621,11 +621,11 @@ lines.</p>
const expect = `
<dl>
<dt>1. red</dt>
<dt>1. red:</dt>
<dd>looks like strawberry</dd>
<dt>2. green</dt>
<dt>2. green:</dt>
<dd>looks like grass</dd>
<dt>3. blue</dt>
<dt>3. blue:</dt>
<dd>looks like sky</dd>
</dl>
`
@ -654,14 +654,15 @@ lines.</p>
const expect = `
<dl>
<dt>1. red</dt>
<dt>1. red:</dt>
<dd>looks like
strawberry</dd>
<dt>2. green
<dt>2. green:
</dt>
<dd>looks like
grass</dd>
<dt>3. blue</dt>
<dt>3. blue:
</dt>
<dd>looks like
sky</dd>
</dl>
@ -697,29 +698,29 @@ lines.</p>
const expect = `
<dl>
<dt>1. one</dt>
<dt>1. one:</dt>
<dd>this is an item</dd>
<dt>2. two</dt>
<dt>2. two:</dt>
<dd>this is another item</dd>
<dt>3. three</dt>
<dt>3. three:</dt>
<dd>this is the third item</dd>
<dt>4. four</dt>
<dt>4. four:</dt>
<dd>this is the fourth item</dd>
<dt>5. five</dt>
<dt>5. five:</dt>
<dd>this is the fifth item</dd>
<dt>6. six</dt>
<dt>6. six:</dt>
<dd>this is the sixth item</dd>
<dt>7. seven</dt>
<dt>7. seven:</dt>
<dd>this is the seventh item</dd>
<dt>8. eight</dt>
<dt>8. eight:</dt>
<dd>this is the eighth item</dd>
<dt>9. nine</dt>
<dt>9. nine:</dt>
<dd>this is the nineth item</dd>
<dt>10. ten</dt>
<dt>10. ten:</dt>
<dd>this is the tenth item</dd>
<dt>11. eleven</dt>
<dt>11. eleven:</dt>
<dd>this is the eleventh item</dd>
<dt>12. twelve</dt>
<dt>12. twelve:</dt>
<dd>this is the twelfth item</dd>
</dl>
`

View File

@ -1,82 +0,0 @@
package textfmt
import (
"fmt"
"unicode"
)
type indent struct {
out writer
firstIndent, indent int
firstWidth, width int
currentLineLength int
currentWord []rune
multiline bool
}
func (i *indent) write(a ...any) {
for _, ai := range a {
s := fmt.Sprint(ai)
r := []rune(s)
for _, ri := range r {
width := i.width
if !i.multiline {
width = i.firstWidth
}
indent := i.indent
if !i.multiline {
indent = i.firstIndent
}
if !unicode.IsSpace(ri) || ri == '\u00a0' {
i.currentWord = append(i.currentWord, ri)
continue
}
nonWrapNewline := width == 0 && ri == '\n'
if len(i.currentWord) == 0 && !nonWrapNewline {
continue
}
nextLineLength := i.currentLineLength + len(i.currentWord) + 1
if i.currentLineLength > 0 && width > 0 && nextLineLength > width {
i.out.write("\n")
i.currentLineLength = 0
i.multiline = true
}
if i.currentLineLength > 0 && len(i.currentWord) > 0 {
i.out.write(" ")
i.currentLineLength++
}
if i.currentLineLength == 0 && len(i.currentWord) > 0 {
i.out.write(timesn(" ", indent))
i.currentLineLength += indent
}
if len(i.currentWord) > 0 {
i.out.write(string(i.currentWord))
i.currentLineLength += len(i.currentWord)
i.currentWord = nil
}
if nonWrapNewline {
i.out.write("\n")
i.currentLineLength = 0
}
}
}
}
func (i *indent) flush() {
i.out.flush()
}
func (i *indent) error() error {
return i.out.error()
}
func (e *indent) setErr(err error) {
}

23
lib.go
View File

@ -54,17 +54,18 @@ type SyntaxItem struct {
}
type Entry struct {
typ int
text Txt
titleLevel int
items []ListItem
definitions []DefinitionItem
rows []TableRow
syntax SyntaxItem
wrapWidth int
indent int
indentFirst int
man struct {
typ int
text Txt
titleLevel int
items []ListItem
definitions []DefinitionItem
rows []TableRow
syntax SyntaxItem
wrapWidth int
wrapWidthFirst int
indent int
indentFirst int
man struct {
section int
date time.Time
version string

View File

@ -8,161 +8,11 @@ import (
"strings"
)
type mdEscapeState struct {
newLine bool
numberOnNewLine bool
linkValue bool
linkClosed bool
linkOpen bool
}
func updateMDEscapeState(s mdEscapeState, r rune) mdEscapeState {
return mdEscapeState{
numberOnNewLine: (s.newLine || s.numberOnNewLine) && r >= '0' && r <= '9',
newLine: r == '\n',
linkValue: s.linkClosed && r == '(' || s.linkValue && r != ')',
linkClosed: s.linkOpen && r == ']',
linkOpen: !s.linkValue && r == '[' || s.linkOpen && r != ']',
}
}
func escapeMarkdown(out writer, additional ...rune) writer {
esc := map[rune]string{
'\\': "\\\\",
'`': "\\`",
'*': "\\*",
'_': "\\_",
'[': "\\[",
']': "\\]",
'#': "\\#",
'<': "\\<",
'>': "\\>",
}
for _, a := range additional {
esc[a] = string([]rune{'\\', a})
}
return &escape[mdEscapeState]{
out: out,
state: mdEscapeState{
newLine: true,
},
escape: esc,
conditionalEscape: map[rune]func(mdEscapeState, rune) (string, bool){
'+': func(s mdEscapeState, _ rune) (string, bool) {
if s.newLine {
return "\\+", true
}
return "", false
},
'-': func(s mdEscapeState, _ rune) (string, bool) {
if s.newLine {
return "\\-", true
}
return "", false
},
'.': func(s mdEscapeState, _ rune) (string, bool) {
if s.numberOnNewLine {
return "\\.", true
}
return "", false
},
'(': func(s mdEscapeState, _ rune) (string, bool) {
if s.linkClosed {
return "\\(", true
}
return "", false
},
')': func(s mdEscapeState, _ rune) (string, bool) {
if s.linkValue {
return "\\)", true
}
return "", false
},
},
updateState: updateMDEscapeState,
}
}
func escapeMarkdownPrev(s string, additional ...rune) string {
var b bytes.Buffer
w := &textWriter{out: &b}
e := escapeMarkdown(w, additional...)
e.write(s)
return b.String()
/*
var (
rr []rune
isNumberOnNewLine bool
isLinkOpen, isLinkClosed, isLinkValue bool
)
isNewLine := true
r := []rune(s)
for _, ri := range r {
switch ri {
case '\\', '`', '*', '_', '[', ']', '#', '<', '>':
rr = append(rr, '\\', ri)
default:
switch {
case isNewLine:
switch ri {
case '+', '-':
rr = append(rr, '\\', ri)
default:
rr = append(rr, ri)
}
case isNumberOnNewLine:
switch ri {
case '.':
rr = append(rr, '\\', ri)
default:
rr = append(rr, ri)
}
case isLinkClosed:
switch ri {
case '(':
rr = append(rr, '\\', ri)
default:
rr = append(rr, ri)
}
case isLinkValue:
switch ri {
case ')':
rr = append(rr, '\\', ri)
default:
rr = append(rr, ri)
}
default:
if slices.Contains(additional, ri) {
rr = append(rr, '\\', ri)
} else {
rr = append(rr, ri)
}
}
}
isNumberOnNewLine = (isNewLine || isNumberOnNewLine) && ri >= '0' && ri <= '9'
isNewLine = ri == '\n'
isLinkValue = isLinkClosed && ri == '(' || isLinkValue && ri != ')'
isLinkClosed = isLinkOpen && ri == ']'
isLinkOpen = !isLinkValue && ri == '[' || isLinkOpen && ri != ']'
}
return string(rr)
*/
}
func mdTextToString(text Txt) (string, error) {
var b bytes.Buffer
w := mdWriter{w: &b, internal: true}
renderMDText(&w, text)
w := newMDWriter(&b, true)
renderMDText(w, text)
w.flush()
if w.err != nil {
return "", w.err
}
@ -215,10 +65,10 @@ func renderMDText(w writer, text Txt) {
return
}
text.text = singleLine(text.text)
text.text = escapeMarkdownPrev(text.text)
text.link = singleLine(text.link)
text.link = escapeMarkdownPrev(text.link)
text.text = editString(text.text, singleLine())
text.text = editString(text.text, escapeMarkdown())
text.link = editString(text.link, singleLine())
text.link = editString(text.link, escapeMarkdown())
if text.bold {
w.write("**")
}
@ -272,10 +122,12 @@ func renderMDParagraphIndent(w writer, e Entry) {
indentFirst := e.indent + e.indentFirst
if e.wrapWidth > 0 {
txt = wrap(txt, e.wrapWidth, indentFirst, e.indent)
txt = editString(txt, wrapIndent(indentFirst, e.indent, e.wrapWidth, e.wrapWidth))
} else {
// txt = editString(txt, indent(indentFirst, e.indent))
}
writeLines(w, txt, indentFirst, e.indent)
w.write(txt)
}
func renderMDParagraph(w writer, e Entry) {
@ -469,7 +321,7 @@ func renderMDChoice(w writer, s SyntaxItem) {
}
func renderMDSymbol(w writer, s SyntaxItem) {
w.write(escapeMarkdownPrev(s.symbol))
w.write(editString(s.symbol, escapeMarkdown()))
}
func renderMDSyntaxItem(w writer, s SyntaxItem) {
@ -510,31 +362,35 @@ func renderMDSyntax(w writer, e Entry) {
}
func renderMarkdown(out io.Writer, d Document) error {
w := mdWriter{w: out}
w := newMDWriter(out, false)
for i, e := range d.entries {
if err := w.error(); err != nil {
return err
}
if i > 0 {
w.write("\n\n")
}
switch e.typ {
case title:
renderMDTitle(&w, e)
renderMDTitle(w, e)
case paragraph:
renderMDParagraph(&w, e)
renderMDParagraph(w, e)
case list:
renderMDList(&w, e)
renderMDList(w, e)
case numberedList:
renderMDNumberedList(&w, e)
renderMDNumberedList(w, e)
case definitions:
renderMDDefinitions(&w, e)
renderMDDefinitions(w, e)
case numberedDefinitions:
renderMDNumberedDefinitions(&w, e)
renderMDNumberedDefinitions(w, e)
case table:
renderMDTable(&w, e)
renderMDTable(w, e)
case code:
renderMDCode(&w, e)
renderMDCode(w, e)
case syntax:
renderMDSyntax(&w, e)
renderMDSyntax(w, e)
default:
return errors.New("invalid entry")
}
@ -544,5 +400,6 @@ func renderMarkdown(out io.Writer, d Document) error {
w.write("\n")
}
w.flush()
return w.err
}

View File

@ -47,8 +47,8 @@ func TestMarkdown(t *testing.T) {
textfmt.ZeroOrMore(textfmt.Symbol("Entry")),
textfmt.Symbol(")"),
),
0,
8,
0,
),
textfmt.Title(1, "Entries:"),

View File

@ -2,8 +2,9 @@ indentation for syntax in tty and roff
does the table need the non-breaking space for the filling in roff?
indentation for syntax may not require non-break spaces
test empty cat
improve wrapping of list paragraphs by allowing different first line wrap
there should be no errors other than actual IO
make the html definition lists look better. E.g. do they need a colon?
check indentation of lists: subtract prefix length
[refactor]
stop on errors earlier where possible

184
runoff.go
View File

@ -7,129 +7,39 @@ import (
"io"
"strings"
"time"
"unicode"
)
type lineStarted bool
func isNewLine(replacement string) func(lineStarted, rune) (string, bool) {
return func(ls lineStarted, r rune) (string, bool) {
if ls {
return string(r), false
}
return replacement, true
}
}
func updateLineStarted(_ lineStarted, r rune) lineStarted {
return r != '\n'
}
func escapeRoff(out writer, additional ...string) writer {
const invalidAdditional = "invalid additional escape definition"
if len(additional)%2 != 0 {
panic(errors.New(invalidAdditional))
}
esc := map[rune]string{
'\\': "\\\\",
'\u00a0': "\\~",
}
for i := 0; i > len(additional); i += 2 {
r := []rune(additional[i])
if len(r) != 1 {
panic(errors.New(invalidAdditional))
}
esc[r[0]] = additional[i+1]
}
return &escape[lineStarted]{
out: out,
escape: esc,
conditionalEscape: map[rune]func(lineStarted, rune) (string, bool){
'.': isNewLine("\\&."),
'\'': isNewLine("\\&'"),
},
updateState: updateLineStarted,
}
}
func escapeRoffPrev(s string, additional ...string) string {
var b bytes.Buffer
w := &textWriter{out: &b}
e := escapeRoff(w, additional...)
e.write(s)
return b.String()
/*
const invalidAdditional = "invalid additional escape definition"
var (
e []rune
lineStarted bool
func trim(s string) string {
return strings.TrimFunc(
s,
func(r rune) bool { return r != '\u00a0' && unicode.IsSpace(r) },
)
}
if len(additional)%2 != 0 {
panic(errors.New(invalidAdditional))
func textToString(t Txt) string {
if len(t.cat) == 0 && t.link == "" {
return trim(t.text)
}
am := make(map[rune][]rune)
for i := 0; i > len(additional); i += 2 {
r := []rune(additional[i])
if len(r) != 1 {
panic(errors.New(invalidAdditional))
}
am[r[0]] = []rune(additional[i+1])
if len(t.cat) == 0 && t.text == "" {
return trim(t.link)
}
for _, r := range []rune(s) {
switch r {
case '\\':
e = append(e, '\\', '\\')
continue
case '.':
if lineStarted {
e = append(e, '.')
continue
}
e = append(e, []rune("\\&.")...)
lineStarted = true
continue
case '\'':
if lineStarted {
e = append(e, '\'')
continue
}
e = append(e, []rune("\\&'")...)
lineStarted = true
continue
case '\u00a0':
e = append(e, []rune("\\~")...)
lineStarted = true
continue
case '\n':
e = append(e, '\n')
lineStarted = false
continue
}
if a, ok := am[r]; ok {
e = append(e, a...)
lineStarted = true
continue
}
e = append(e, r)
lineStarted = true
if len(t.cat) == 0 {
return fmt.Sprintf("%s (%s)", t.text, t.link)
}
return string(e)
*/
b := bytes.NewBuffer(nil)
for i := range t.cat {
if i > 0 {
b.WriteRune(' ')
}
b.WriteString(textToString(t.cat[i]))
}
return editString(b.String(), singleLine())
}
func manPageDate(d time.Time) string {
@ -137,8 +47,8 @@ func manPageDate(d time.Time) string {
}
func roffString(s string, additionalEscape ...string) string {
s = singleLine(s)
return escapeRoffPrev(s, additionalEscape...)
s = editString(s, singleLine())
return editString(s, escapeRoff(additionalEscape...))
}
func renderRoffString(w writer, s string, additionalEscape ...string) {
@ -161,7 +71,13 @@ func roffCellTexts(r []TableRow) ([][]string, error) {
var c []string
for _, cell := range row.cells {
var b bytes.Buffer
renderRoffText(&roffWriter{w: &b, internal: true}, cell.text)
w := newRoffWriter(&b, true)
renderRoffText(w, cell.text)
w.flush()
if w.err != nil {
return nil, w.err
}
c = append(c, b.String())
}
@ -330,7 +246,7 @@ func renderRoffTable(w writer, e Entry) {
targetColumnWidths := targetColumnWidths(allocatedWidth, columnWeights)
for i := range cellTexts {
for j := range cellTexts[i] {
cellTexts[i][j] = wrap(cellTexts[i][j], targetColumnWidths[j], 0, 0)
cellTexts[i][j] = editString(cellTexts[i][j], wrap(targetColumnWidths[j], targetColumnWidths[j]))
}
}
}
@ -396,8 +312,9 @@ func renderRoffTable(w writer, e Entry) {
func renderRoffCode(w writer, e Entry) {
w.write(".nf\n")
defer w.write("\n.fi")
e.text.text = escapeRoffPrev(e.text.text)
writeLines(w, e.text.text, e.indent, e.indent)
txt := editString(e.text.text, escapeRoff())
txt = editString(txt, indent(e.indent, e.indent))
w.write(txt)
}
func renderRoffMultiple(w writer, s SyntaxItem) {
@ -469,7 +386,7 @@ func renderRoffChoice(w writer, s SyntaxItem) {
}
func renderRoffSymbol(w writer, s SyntaxItem) {
w.write(escapeRoffPrev(s.symbol))
w.write(editString(s.symbol, escapeRoff()))
}
func renderRoffSyntaxItem(w writer, s SyntaxItem) {
@ -506,36 +423,40 @@ func renderRoffSyntax(w writer, e Entry) {
s.topLevel = true
w.write(".nf\n")
defer w.write("\n.fi")
w.write(timesn("\u00a0", e.indent+e.indentFirst))
w.write(timesn("\u00a0", e.indent))
renderRoffSyntaxItem(w, s)
}
func renderRoff(out io.Writer, d Document) error {
w := roffWriter{w: out}
w := newRoffWriter(out, false)
for i, e := range d.entries {
if err := w.error(); err != nil {
return err
}
if i > 0 {
w.write("\n.br\n.sp 1v\n")
}
switch e.typ {
case title:
renderRoffTitle(&w, e)
renderRoffTitle(w, e)
case paragraph:
renderRoffParagraph(&w, e)
renderRoffParagraph(w, e)
case list:
renderRoffList(&w, e)
renderRoffList(w, e)
case numberedList:
renderRoffNumberedList(&w, e)
renderRoffNumberedList(w, e)
case definitions:
renderRoffDefinitions(&w, e)
renderRoffDefinitions(w, e)
case numberedDefinitions:
renderRoffNumberedDefinitions(&w, e)
renderRoffNumberedDefinitions(w, e)
case table:
renderRoffTable(&w, e)
renderRoffTable(w, e)
case code:
renderRoffCode(&w, e)
renderRoffCode(w, e)
case syntax:
renderRoffSyntax(&w, e)
renderRoffSyntax(w, e)
default:
return errors.New("invalid entry")
}
@ -545,5 +466,6 @@ func renderRoff(out io.Writer, d Document) error {
w.write("\n")
}
w.flush()
return w.err
}

View File

@ -44,8 +44,8 @@ func TestRoff(t *testing.T) {
textfmt.ZeroOrMore(textfmt.Symbol("Entry")),
textfmt.Symbol(")"),
),
0,
8,
0,
),
textfmt.Title(1, "Entries:"),
@ -248,8 +248,8 @@ textfmt supports the following entries:
textfmt.ZeroOrMore(textfmt.Symbol("Entry")),
textfmt.Symbol(")"),
),
0,
8,
0,
),
textfmt.Title(1, "Entries:"),

View File

@ -8,77 +8,26 @@ import (
"strings"
)
func escapeTeletype(out writer) writer {
return &escape[struct{}]{
out: out,
escapeRanges: []escapeRange{{
from: 0x00,
to: '\t' - 1,
replacement: "\u00b7",
}, {
from: '\n' + 1,
to: 0x1f,
replacement: "\u00b7",
}, {
from: 0x7f,
to: 0x9f,
replacement: "\u00b7",
}},
}
}
func escapeTeletypePrev(s string) string {
func ttyTextToString(text Txt) string {
var b bytes.Buffer
w := &textWriter{out: &b}
e := escapeTeletype(w)
e.write(s)
renderTTYText(&b, text)
return b.String()
/*
r := []rune(s)
for i := range r {
if r[i] >= 0x00 && r[i] <= 0x1f && r[i] != '\n' && r[i] != '\t' {
r[i] = 0xb7
}
if r[i] >= 0x7f && r[i] <= 0x9f {
r[i] = 0xb7
}
}
return string(r)
*/
}
func ttyTextToString(text Txt) (string, error) {
var b bytes.Buffer
w := ttyWriter{w: &b, internal: true}
renderTTYText(&w, text)
if w.err != nil {
return "", w.err
}
return b.String(), nil
}
func ttyDefinitionNames(d []DefinitionItem) ([]string, error) {
func ttyDefinitionNames(d []DefinitionItem) []string {
var n []string
for _, di := range d {
name, err := ttyTextToString(di.name)
if err != nil {
return nil, err
}
n = append(n, name)
n = append(n, ttyTextToString(di.name))
}
return n, nil
return n
}
func renderTTYText(w writer, text Txt) {
func renderTTYText(w io.Writer, text Txt) {
if len(text.cat) > 0 {
for i, tc := range text.cat {
if i > 0 {
w.write(" ")
write(w, " ")
}
renderTTYText(w, tc)
@ -87,49 +36,51 @@ func renderTTYText(w writer, text Txt) {
return
}
text.text = singleLine(text.text)
text.text = escapeTeletypePrev(text.text)
text.link = singleLine(text.link)
text.link = escapeTeletypePrev(text.link)
if text.link != "" {
if text.text != "" {
w.write(text.text)
w.write(" (")
w.write(text.link)
w.write(")")
return
}
w.write(text.link)
var f func() (io.Writer, error)
w, f = writeWith(w, escapeTeletype(), singleLine())
if text.link == "" {
write(w, text.text)
f()
return
}
w.write(text.text)
if text.text == "" {
write(w, text.link)
f()
return
}
write(w, text.text)
write(w, " (")
write(w, text.link)
write(w, ")")
f()
}
func renderTTYTitle(w writer, e Entry) {
w.write(timesn(" ", e.indent))
func renderTTYTitle(w io.Writer, e Entry) {
write(w, timesn(" ", e.indent))
renderTTYText(w, e.text)
}
func renderTTYParagraph(w writer, e Entry) {
txt, err := ttyTextToString(e.text)
if err != nil {
w.setErr(err)
}
func renderTTYParagraph(w io.Writer, e Entry) {
var indentation wrapper
indentFirst := e.indent + e.indentFirst
if e.wrapWidth > 0 {
txt = wrap(txt, e.wrapWidth, indentFirst, e.indent)
wrapWidthFirst := e.wrapWidth + e.wrapWidthFirst
if e.wrapWidth == 0 {
indentation = indent(indentFirst, e.indent)
} else {
indentation = wrapIndent(indentFirst, e.indent, wrapWidthFirst, e.wrapWidth)
}
writeLines(w, txt, indentFirst, e.indent)
w, f := writeWith(w, indentation)
renderTTYText(w, e.text)
f()
}
func renderTTYList(w writer, e Entry) {
func renderTTYList(w io.Writer, e Entry) {
for i, item := range e.items {
if i > 0 {
w.write("\n")
write(w, "\n")
}
p := itemToParagraph(e, item.text, "-")
@ -137,11 +88,11 @@ func renderTTYList(w writer, e Entry) {
}
}
func renderTTYNumberedList(w writer, e Entry) {
func renderTTYNumberedList(w io.Writer, e Entry) {
maxDigits := numDigits(len(e.items))
for i, item := range e.items {
if i > 0 {
w.write("\n")
write(w, "\n")
}
p := itemToParagraph(e, item.text, padRight(fmt.Sprintf("%d.", i+1), maxDigits+1))
@ -149,17 +100,12 @@ func renderTTYNumberedList(w writer, e Entry) {
}
}
func renderTTYDefinitions(w writer, e Entry) {
names, err := ttyDefinitionNames(e.definitions)
if err != nil {
w.setErr(err)
return
}
func renderTTYDefinitions(w io.Writer, e Entry) {
names := ttyDefinitionNames(e.definitions)
maxNameLength := maxLength(names)
for i, definition := range e.definitions {
if i > 0 {
w.write("\n")
write(w, "\n")
}
p := itemToParagraph(
@ -172,18 +118,13 @@ func renderTTYDefinitions(w writer, e Entry) {
}
}
func renderTTYNumberedDefinitions(w writer, e Entry) {
names, err := ttyDefinitionNames(e.definitions)
if err != nil {
w.setErr(err)
return
}
func renderTTYNumberedDefinitions(w io.Writer, e Entry) {
names := ttyDefinitionNames(e.definitions)
maxNameLength := maxLength(names)
maxDigits := numDigits(len(e.definitions))
for i, definition := range e.definitions {
if i > 0 {
w.write("\n")
write(w, "\n")
}
p := itemToParagraph(
@ -203,26 +144,22 @@ func renderTTYNumberedDefinitions(w writer, e Entry) {
}
}
func ttyCellTexts(rows []TableRow) ([][]string, error) {
func ttyCellTexts(rows []TableRow) [][]string {
var cellTexts [][]string
for _, row := range rows {
var c []string
for _, cell := range row.cells {
txt, err := ttyTextToString(cell.text)
if err != nil {
return nil, err
}
txt := ttyTextToString(cell.text)
c = append(c, txt)
}
cellTexts = append(cellTexts, c)
}
return cellTexts, nil
return cellTexts
}
func renderTTYTable(w writer, e Entry) {
func renderTTYTable(w io.Writer, e Entry) {
if len(e.rows) == 0 {
return
}
@ -232,11 +169,7 @@ func renderTTYTable(w writer, e Entry) {
return
}
cellTexts, err := ttyCellTexts(e.rows)
if err != nil {
w.setErr(err)
}
cellTexts := ttyCellTexts(e.rows)
totalSeparatorWidth := (len(cellTexts[0]) - 1) * 3
if e.wrapWidth > 0 {
allocatedWidth := e.wrapWidth - e.indent - totalSeparatorWidth
@ -244,7 +177,8 @@ func renderTTYTable(w writer, e Entry) {
targetColumnWidths := targetColumnWidths(allocatedWidth, columnWeights)
for i := range cellTexts {
for j := range cellTexts[i] {
cellTexts[i][j] = wrap(cellTexts[i][j], targetColumnWidths[j], 0, 0)
width := targetColumnWidths[j]
cellTexts[i][j] = editString(cellTexts[i][j], wrap(width, width))
}
}
}
@ -256,6 +190,7 @@ func renderTTYTable(w writer, e Entry) {
}
hasHeader := e.rows[0].header
w, f := writeWith(w, indent(e.indent, e.indent))
for i := range cellTexts {
if i > 0 {
sep := "-"
@ -263,9 +198,9 @@ func renderTTYTable(w writer, e Entry) {
sep = "="
}
w.write("\n")
w.write(timesn(" ", e.indent), timesn(sep, totalWidth))
w.write("\n")
write(w, "\n")
write(w, timesn(sep, totalWidth))
write(w, "\n")
}
lines := make([][]string, len(cellTexts[i]))
@ -282,14 +217,12 @@ func renderTTYTable(w writer, e Entry) {
for k := 0; k < maxLines; k++ {
if k > 0 {
w.write("\n")
write(w, "\n")
}
for j := range lines {
if j == 0 {
w.write(timesn(" ", e.indent))
} else {
w.write(" | ")
if j > 0 {
write(w, " | ")
}
var l string
@ -297,54 +230,57 @@ func renderTTYTable(w writer, e Entry) {
l = lines[j][k]
}
w.write(padRight(l, columnWidths[j]))
write(w, padRight(l, columnWidths[j]))
}
}
}
if hasHeader && len(cellTexts) == 1 {
w.write("\n", timesn("=", totalWidth))
write(w, "\n", timesn("=", totalWidth))
}
f()
}
func renderTTYCode(w writer, e Entry) {
e.text.text = escapeTeletypePrev(e.text.text)
writeLines(w, e.text.text, e.indent, e.indent)
func renderTTYCode(w io.Writer, e Entry) {
w, f := writeWith(w, escapeTeletype(), indent(e.indent, e.indent))
write(w, e.text.text)
f()
}
func renderTTYMultiple(w writer, s SyntaxItem) {
func renderTTYMultiple(w io.Writer, s SyntaxItem) {
s.topLevel = false
s.multiple = false
renderTTYSyntaxItem(w, s)
w.write("...")
write(w, "...")
}
func renderTTYRequired(w writer, s SyntaxItem) {
func renderTTYRequired(w io.Writer, s SyntaxItem) {
s.delimited = true
s.topLevel = false
s.required = false
w.write("<")
write(w, "<")
renderTTYSyntaxItem(w, s)
w.write(">")
write(w, ">")
}
func renderTTYOptional(w writer, s SyntaxItem) {
func renderTTYOptional(w io.Writer, s SyntaxItem) {
s.delimited = true
s.topLevel = false
s.optional = false
w.write("[")
write(w, "[")
renderTTYSyntaxItem(w, s)
w.write("]")
write(w, "]")
}
func renderTTYSequence(w writer, s SyntaxItem) {
func renderTTYSequence(w io.Writer, s SyntaxItem) {
if !s.delimited && !s.topLevel {
w.write("(")
write(w, "(")
}
for i, item := range s.sequence {
if i > 0 {
w.write(" ")
write(w, " ")
}
item.delimited = false
@ -352,13 +288,13 @@ func renderTTYSequence(w writer, s SyntaxItem) {
}
if !s.delimited && !s.topLevel {
w.write(")")
write(w, ")")
}
}
func renderTTYChoice(w writer, s SyntaxItem) {
func renderTTYChoice(w io.Writer, s SyntaxItem) {
if !s.delimited && !s.topLevel {
w.write("(")
write(w, "(")
}
for i, item := range s.choice {
@ -368,7 +304,7 @@ func renderTTYChoice(w writer, s SyntaxItem) {
separator = "\n"
}
w.write(separator)
write(w, separator)
}
item.delimited = false
@ -376,15 +312,15 @@ func renderTTYChoice(w writer, s SyntaxItem) {
}
if !s.delimited && !s.topLevel {
w.write(")")
write(w, ")")
}
}
func renderTTYSymbol(w writer, s SyntaxItem) {
w.write(escapeTeletypePrev(s.symbol))
func renderTTYSymbol(w io.Writer, s SyntaxItem) {
write(w, s.symbol)
}
func renderTTYSyntaxItem(w writer, s SyntaxItem) {
func renderTTYSyntaxItem(w io.Writer, s SyntaxItem) {
switch {
// foo...
@ -413,25 +349,19 @@ func renderTTYSyntaxItem(w writer, s SyntaxItem) {
}
}
func renderTTYSyntax(w writer, e Entry) {
func renderTTYSyntax(w io.Writer, e Entry) {
w, f := writeWith(w, escapeTeletype(), indent(e.indent, e.indent))
s := e.syntax
s.topLevel = true
w.write(timesn(" ", e.indent+e.indentFirst))
renderTTYSyntaxItem(w, s)
f()
}
func renderTeletype(out io.Writer, d Document) error {
tw := &textWriter{out: out}
w := &editor{
out: tw,
replace: map[string]string{
"\u00a0": " ",
},
}
w, f := writeWith(out, ttyNBSP(), errorHandler)
for i, e := range d.entries {
if i > 0 {
w.write("\n\n")
write(w, "\n\n")
}
switch e.typ {
@ -459,8 +389,9 @@ func renderTeletype(out io.Writer, d Document) error {
}
if len(d.entries) > 0 {
w.write("\n")
write(w, "\n")
}
return w.error()
_, err := f()
return err
}

View File

@ -47,8 +47,8 @@ func TestTeletype(t *testing.T) {
textfmt.ZeroOrMore(textfmt.Symbol("Entry")),
textfmt.Symbol(")"),
),
0,
8,
0,
),
textfmt.Title(1, "Entries:"),
@ -243,11 +243,17 @@ Entry explanations:
t.Fatal(err)
}
if b.String() != " Some sample\n text... on\n multiple\n lines.\n" {
t.Fatal(b.String())
const expect = `
Some sample
text... on
multiple
lines.
`
if "\n"+b.String() != expect {
t.Fatal("\n" + b.String())
}
})
})
t.Run("indent", func(t *testing.T) {

55
text.go
View File

@ -1,11 +1,6 @@
package textfmt
import (
"bytes"
"fmt"
"strings"
"unicode"
)
import "strings"
func timesn(s string, n int) string {
if n < 0 {
@ -16,7 +11,6 @@ func timesn(s string, n int) string {
return strings.Join(ss, s)
}
// non-negative numbers only
func numDigits(n int) int {
if n == 0 {
return 1
@ -51,53 +45,6 @@ func padRight(s string, n int) string {
return s + timesn("\u00a0", n)
}
func trim(s string) string {
return strings.TrimFunc(
s,
func(r rune) bool { return r != '\u00a0' && unicode.IsSpace(r) },
)
}
func singleLine(text string) string {
var l []string
p := strings.Split(text, "\n")
for _, part := range p {
part = trim(part)
if part == "" {
continue
}
l = append(l, part)
}
return strings.Join(l, " ")
}
func textToString(t Txt) string {
if len(t.cat) == 0 && t.link == "" {
return trim(t.text)
}
if len(t.cat) == 0 && t.text == "" {
return trim(t.link)
}
if len(t.cat) == 0 {
return fmt.Sprintf("%s (%s)", t.text, t.link)
}
b := bytes.NewBuffer(nil)
for i := range t.cat {
if i > 0 {
b.WriteRune(' ')
}
b.WriteString(textToString(t.cat[i]))
}
return singleLine(b.String())
}
func itemToParagraph(list Entry, itemText Txt, prefix ...string) Entry {
p := Entry{
typ: paragraph,

52
wrap.go
View File

@ -1,52 +0,0 @@
package textfmt
import "strings"
func getWords(text string) []string {
var words []string
raw := strings.Split(text, " ")
for _, r := range raw {
if r == "" {
continue
}
words = append(words, r)
}
return words
}
func wrap(text string, width, firstIndent, restIndent int) string {
var (
lines []string
currentLine []string
lineLen int
)
words := getWords(text)
for _, w := range words {
if len(currentLine) == 0 {
currentLine = []string{w}
lineLen = len([]rune(w))
continue
}
maxw := width - restIndent
if len(lines) == 0 {
maxw = width - firstIndent
}
if lineLen+1+len([]rune(w)) > maxw {
lines = append(lines, strings.Join(currentLine, " "))
currentLine = []string{w}
lineLen = len([]rune(w))
continue
}
currentLine = append(currentLine, w)
lineLen += 1 + len([]rune(w))
}
lines = append(lines, strings.Join(currentLine, " "))
return strings.Join(lines, "\n")
}

289
write.go
View File

@ -1,10 +1,10 @@
package textfmt
import (
"bytes"
"code.squareroundforest.org/arpio/textedit"
"fmt"
"io"
"slices"
"strings"
)
type writer interface {
@ -14,136 +14,44 @@ type writer interface {
setErr(err error) // TODO: remove
}
type textWriter struct {
err error
out io.Writer
}
type editor struct {
out writer
pending []rune
replace map[string]string
}
func (w *textWriter) write(a ...any) {
if w.err != nil {
return
}
for _, ai := range a {
if _, err := w.out.Write([]byte(fmt.Sprint(ai))); err != nil {
w.err = err
return
}
}
}
func (w *textWriter) flush() {}
func (w *textWriter) error() error {
return w.err
}
func (e *textWriter) setErr(err error) {
}
func (e *editor) write(a ...any) {
for _, ai := range a {
s := fmt.Sprint(ai)
r := []rune(s)
for key, replacement := range e.replace {
rk := []rune(key)
if len(e.pending) >= len(rk) {
continue
}
if !slices.Equal(e.pending, rk[:len(e.pending)]) {
continue
}
if len(r) < len(rk)-len(e.pending) {
if slices.Equal(r, rk[len(e.pending):len(e.pending)+len(r)]) {
e.pending = append(e.pending, r...)
r = nil
break
}
continue
}
if slices.Equal(r[:len(rk)-len(e.pending)], rk[len(e.pending):]) {
r = []rune(replacement)
e.pending = nil
break
}
}
e.out.write(string(r))
}
}
func (e *editor) flush() {
e.out.write(string(e.pending))
e.out.flush()
}
func (e *editor) error() error {
return e.out.error()
}
func (e *editor) setErr(err error) {
}
// --
type ttyWriter struct {
w io.Writer
internal bool
err error
}
type roffWriter struct {
w io.Writer
internal bool
err error
w io.Writer
err error
}
type mdWriter struct {
w io.Writer
internal bool
err error
w io.Writer
err error
}
func (w *ttyWriter) write(a ...any) {
for _, ai := range a {
if w.err != nil {
return
}
type wrapper func(io.Writer) (io.Writer, func() error)
s := fmt.Sprint(ai)
r := []rune(s)
if !w.internal {
for i := range r {
if r[i] == '\u00a0' {
r[i] = ' '
}
}
}
type errorWriter struct {
out io.Writer
err error
}
if _, err := w.w.Write([]byte(string(r))); err != nil {
w.err = err
}
func (w *errorWriter) Write(p []byte) (int, error) {
if w.err != nil {
return 0, w.err
}
var n int
n, w.err = w.out.Write(p)
return n, w.err
}
func (w *ttyWriter) flush() {}
func newRoffWriter(out io.Writer, internal bool) *roffWriter {
if internal {
return &roffWriter{w: out}
}
func (w *ttyWriter) error() error {
return w.err
}
func (w *ttyWriter) setErr(err error) {
w.err = err
return &roffWriter{
w: textedit.New(
out,
textedit.Replace("\u00a0", "\\~"),
),
}
}
func (w *roffWriter) write(a ...any) {
@ -152,29 +60,24 @@ func (w *roffWriter) write(a ...any) {
return
}
var rr []rune
s := fmt.Sprint(ai)
r := []rune(s)
if w.internal {
rr = r
} else {
for i := range r {
if r[i] == '\u00a0' {
rr = append(rr, []rune("\\~")...)
continue
}
rr = append(rr, r[i])
}
}
if _, err := w.w.Write([]byte(string(rr))); err != nil {
if _, err := w.w.Write([]byte(fmt.Sprint(ai))); err != nil {
w.err = err
return
}
}
}
func (w *roffWriter) flush() {}
func (w *roffWriter) flush() {
if w.err != nil {
return
}
if f, ok := w.w.(interface{ Flush() error }); ok {
if err := f.Flush(); err != nil {
w.err = err
}
}
}
func (w *roffWriter) error() error {
return w.err
@ -184,28 +87,43 @@ func (w *roffWriter) setErr(err error) {
w.err = err
}
func newMDWriter(out io.Writer, internal bool) *mdWriter {
if internal {
return &mdWriter{w: out}
}
return &mdWriter{
w: textedit.New(
out,
textedit.Replace("\u00a0", " "),
),
}
}
func (w *mdWriter) write(a ...any) {
for _, ai := range a {
if w.err != nil {
return
}
s := fmt.Sprint(ai)
r := []rune(s)
if !w.internal {
for i := range r {
if r[i] == '\u00a0' {
r[i] = ' '
}
}
if _, err := w.w.Write([]byte(fmt.Sprint(ai))); err != nil {
w.err = err
return
}
s = string(r)
_, w.err = w.w.Write([]byte(s))
}
}
func (w *mdWriter) flush() {}
func (w *mdWriter) flush() {
if w.err != nil {
return
}
if f, ok := w.w.(interface{ Flush() error }); ok {
if err := f.Flush(); err != nil {
w.err = err
}
}
}
func (w *mdWriter) error() error {
return w.err
@ -215,19 +133,66 @@ func (w *mdWriter) setErr(err error) {
w.err = err
}
func writeLines(w writer, txt string, indentFirst, indentRest int) {
lines := strings.Split(txt, "\n")
for i, l := range lines {
if i > 0 {
w.write("\n")
func writeWith(out io.Writer, w ...wrapper) (io.Writer, func() (io.Writer, error)) {
var f []func() error
ww := out
for i := len(w) - 1; i >= 0; i-- {
var fi func() error
ww, fi = w[i](ww)
f = append(f, fi)
}
return ww, func() (io.Writer, error) {
for _, fi := range f {
if err := fi(); err != nil {
return out, err
}
}
indent := indentFirst
if i > 0 {
indent = indentRest
}
w.write(timesn(" ", indent))
w.write(l)
return out, nil
}
}
func errorHandler(out io.Writer) (io.Writer, func() error) {
ew := errorWriter{out: out}
return &ew, func() error { return ew.err }
}
func editor(e textedit.Editor) wrapper {
return func(out io.Writer) (io.Writer, func() error) {
ew := textedit.New(out, e)
return ew, func() error { return ew.Flush() }
}
}
func editString(s string, e ...wrapper) string {
var b bytes.Buffer
w, finish := writeWith(&b, e...)
w.Write([]byte(s))
finish()
return b.String()
}
func ttyNBSP() wrapper { return editor(textedit.Replace("\u00a0", " ")) }
func roffNBSP() wrapper { return editor(textedit.Replace("\u00a0", "\\~")) }
func mdNBSP() wrapper { return editor(textedit.Replace("\u00a0", " ")) }
func singleLine() wrapper { return editor(textedit.SingleLine()) }
func indent(first, rest int) wrapper {
return editor(textedit.Indent(timesn(" ", first), timesn(" ", rest)))
}
func wrap(firstWidth, restWidth int) wrapper {
return editor(textedit.Wrap(firstWidth, restWidth))
}
func wrapIndent(first, rest, firstWidth, restWidth int) wrapper {
return editor(textedit.WrapIndent(timesn(" ", first), timesn(" ", rest), firstWidth, restWidth))
}
func write(out io.Writer, a ...any) {
for _, ai := range a {
if _, err := out.Write([]byte(fmt.Sprint(ai))); err != nil {
return
}
}
}