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 package textfmt
import "fmt" import (
"code.squareroundforest.org/arpio/textedit"
"errors"
)
type escapeRange struct { type mdEscapeState struct {
from, to rune lineStarted bool
replacement string numberOnNewLine bool
linkValue bool
linkClosed bool
linkOpen bool
} }
type escape[S any] struct { func escapeTeletypeEdit(r rune, s struct{}) ([]rune, struct{}) {
out writer if r >= 0x00 && r <= 0x1f && r != '\n' && r != '\t' {
state S return []rune{0xb7}, 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
}
} }
return "", false if r >= 0x7f && r <= 0x9f {
return []rune{0xb7}, s
}
return []rune{r}, s
} }
func (e *escape[S]) write(a ...any) { func escapeTeletype() wrapper {
for _, ai := range a { return editor(
s := fmt.Sprint(ai) textedit.Func(
r := []rune(s) escapeTeletypeEdit,
for _, ri := range r { func(struct{}) []rune { return nil },
var ( ),
output string )
found bool }
)
output, found = e.escape[ri] func escapeRoffEdit(additional ...string) func(rune, bool) ([]rune, bool) {
if !found { const invalidAdditional = "invalid additional escape definition"
output, found = e.inEscapeRange(ri) if len(additional)%2 != 0 {
} panic(errors.New(invalidAdditional))
}
if !found { esc := map[rune][]rune{
conditional := e.conditionalEscape[ri] '\\': []rune("\\\\"),
if conditional != nil { '\u00a0': []rune("\\~"),
output, found = conditional(e.state, ri) }
}
}
if !found { for i := 0; i > len(additional); i += 2 {
output = string(ri) r := []rune(additional[i])
} if len(r) != 1 {
panic(errors.New(invalidAdditional))
e.out.write(output)
if e.updateState != nil {
e.state = e.updateState(e.state, ri)
}
} }
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() { func escapeRoff(additional ...string) wrapper {
e.out.flush() return editor(
textedit.Func(
escapeRoffEdit(additional...),
func(bool) []rune { return nil },
),
)
} }
func (e *escape[S]) error() error { func escapeMarkdownEdit(r rune, s mdEscapeState) ([]rune, mdEscapeState) {
return e.out.error() 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 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-20251102001159-f3efe9c7b176
require code.squareroundforest.org/arpio/html v0.0.0-20251029200407-effffeadf9f8 // indirect 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-20251102001159-f3efe9c7b176 h1:ynJ4zE23G/Q/bhLOA1PV09cTXb4ivvYKTbxaoIz9nJY=
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-20251102001159-f3efe9c7b176/go.mod h1:JKD2DXph0Zt975trJII7YbdhM2gL1YEHjsh5M1X63eA=
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/notation v0.0.0-20250826181910-5140794b16b2 h1:S4mjQHL70CuzFg1AGkr0o0d+4M+ZWM0sbnlYq6f0b3I= 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/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 list := tag.Dl
for _, definition := range e.definitions { for _, definition := range e.definitions {
list = list( list = list(
tag.Dt(htmlText(definition.name)...), tag.Dt(append(htmlText(definition.name), ":")...),
tag.Dd(htmlText(definition.value)...), tag.Dd(htmlText(definition.value)...),
) )
} }
@ -91,7 +91,7 @@ func htmlNumberedDefinitions(e Entry) html.Tag {
list := tag.Dl list := tag.Dl
for i, definition := range e.definitions { for i, definition := range e.definitions {
list = list( 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)...), tag.Dd(htmlText(definition.value)...),
) )
} }

View File

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

View File

@ -8,161 +8,11 @@ import (
"strings" "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) { func mdTextToString(text Txt) (string, error) {
var b bytes.Buffer var b bytes.Buffer
w := mdWriter{w: &b, internal: true} w := newMDWriter(&b, true)
renderMDText(&w, text) renderMDText(w, text)
w.flush()
if w.err != nil { if w.err != nil {
return "", w.err return "", w.err
} }
@ -215,10 +65,10 @@ func renderMDText(w writer, text Txt) {
return return
} }
text.text = singleLine(text.text) text.text = editString(text.text, singleLine())
text.text = escapeMarkdownPrev(text.text) text.text = editString(text.text, escapeMarkdown())
text.link = singleLine(text.link) text.link = editString(text.link, singleLine())
text.link = escapeMarkdownPrev(text.link) text.link = editString(text.link, escapeMarkdown())
if text.bold { if text.bold {
w.write("**") w.write("**")
} }
@ -272,10 +122,12 @@ func renderMDParagraphIndent(w writer, e Entry) {
indentFirst := e.indent + e.indentFirst indentFirst := e.indent + e.indentFirst
if e.wrapWidth > 0 { 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) { func renderMDParagraph(w writer, e Entry) {
@ -469,7 +321,7 @@ func renderMDChoice(w writer, s SyntaxItem) {
} }
func renderMDSymbol(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) { func renderMDSyntaxItem(w writer, s SyntaxItem) {
@ -510,31 +362,35 @@ func renderMDSyntax(w writer, e Entry) {
} }
func renderMarkdown(out io.Writer, d Document) error { func renderMarkdown(out io.Writer, d Document) error {
w := mdWriter{w: out} w := newMDWriter(out, false)
for i, e := range d.entries { for i, e := range d.entries {
if err := w.error(); err != nil {
return err
}
if i > 0 { if i > 0 {
w.write("\n\n") w.write("\n\n")
} }
switch e.typ { switch e.typ {
case title: case title:
renderMDTitle(&w, e) renderMDTitle(w, e)
case paragraph: case paragraph:
renderMDParagraph(&w, e) renderMDParagraph(w, e)
case list: case list:
renderMDList(&w, e) renderMDList(w, e)
case numberedList: case numberedList:
renderMDNumberedList(&w, e) renderMDNumberedList(w, e)
case definitions: case definitions:
renderMDDefinitions(&w, e) renderMDDefinitions(w, e)
case numberedDefinitions: case numberedDefinitions:
renderMDNumberedDefinitions(&w, e) renderMDNumberedDefinitions(w, e)
case table: case table:
renderMDTable(&w, e) renderMDTable(w, e)
case code: case code:
renderMDCode(&w, e) renderMDCode(w, e)
case syntax: case syntax:
renderMDSyntax(&w, e) renderMDSyntax(w, e)
default: default:
return errors.New("invalid entry") return errors.New("invalid entry")
} }
@ -544,5 +400,6 @@ func renderMarkdown(out io.Writer, d Document) error {
w.write("\n") w.write("\n")
} }
w.flush()
return w.err return w.err
} }

View File

@ -47,8 +47,8 @@ func TestMarkdown(t *testing.T) {
textfmt.ZeroOrMore(textfmt.Symbol("Entry")), textfmt.ZeroOrMore(textfmt.Symbol("Entry")),
textfmt.Symbol(")"), textfmt.Symbol(")"),
), ),
0,
8, 8,
0,
), ),
textfmt.Title(1, "Entries:"), 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? does the table need the non-breaking space for the filling in roff?
indentation for syntax may not require non-break spaces indentation for syntax may not require non-break spaces
test empty cat test empty cat
improve wrapping of list paragraphs by allowing different first line wrap
there should be no errors other than actual IO 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] [refactor]
stop on errors earlier where possible stop on errors earlier where possible

184
runoff.go
View File

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

View File

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

View File

@ -8,77 +8,26 @@ import (
"strings" "strings"
) )
func escapeTeletype(out writer) writer { func ttyTextToString(text Txt) string {
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 {
var b bytes.Buffer var b bytes.Buffer
w := &textWriter{out: &b} renderTTYText(&b, text)
e := escapeTeletype(w)
e.write(s)
return b.String() 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) { func ttyDefinitionNames(d []DefinitionItem) []string {
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) {
var n []string var n []string
for _, di := range d { for _, di := range d {
name, err := ttyTextToString(di.name) n = append(n, ttyTextToString(di.name))
if err != nil {
return nil, err
}
n = append(n, name)
} }
return n, nil return n
} }
func renderTTYText(w writer, text Txt) { func renderTTYText(w io.Writer, text Txt) {
if len(text.cat) > 0 { if len(text.cat) > 0 {
for i, tc := range text.cat { for i, tc := range text.cat {
if i > 0 { if i > 0 {
w.write(" ") write(w, " ")
} }
renderTTYText(w, tc) renderTTYText(w, tc)
@ -87,49 +36,51 @@ func renderTTYText(w writer, text Txt) {
return return
} }
text.text = singleLine(text.text) var f func() (io.Writer, error)
text.text = escapeTeletypePrev(text.text) w, f = writeWith(w, escapeTeletype(), singleLine())
text.link = singleLine(text.link) if text.link == "" {
text.link = escapeTeletypePrev(text.link) write(w, text.text)
if text.link != "" { f()
if text.text != "" {
w.write(text.text)
w.write(" (")
w.write(text.link)
w.write(")")
return
}
w.write(text.link)
return 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) { func renderTTYTitle(w io.Writer, e Entry) {
w.write(timesn(" ", e.indent)) write(w, timesn(" ", e.indent))
renderTTYText(w, e.text) renderTTYText(w, e.text)
} }
func renderTTYParagraph(w writer, e Entry) { func renderTTYParagraph(w io.Writer, e Entry) {
txt, err := ttyTextToString(e.text) var indentation wrapper
if err != nil {
w.setErr(err)
}
indentFirst := e.indent + e.indentFirst indentFirst := e.indent + e.indentFirst
if e.wrapWidth > 0 { wrapWidthFirst := e.wrapWidth + e.wrapWidthFirst
txt = wrap(txt, e.wrapWidth, indentFirst, e.indent) 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 { for i, item := range e.items {
if i > 0 { if i > 0 {
w.write("\n") write(w, "\n")
} }
p := itemToParagraph(e, item.text, "-") 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)) maxDigits := numDigits(len(e.items))
for i, item := range e.items { for i, item := range e.items {
if i > 0 { if i > 0 {
w.write("\n") write(w, "\n")
} }
p := itemToParagraph(e, item.text, padRight(fmt.Sprintf("%d.", i+1), maxDigits+1)) 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) { func renderTTYDefinitions(w io.Writer, e Entry) {
names, err := ttyDefinitionNames(e.definitions) names := ttyDefinitionNames(e.definitions)
if err != nil {
w.setErr(err)
return
}
maxNameLength := maxLength(names) maxNameLength := maxLength(names)
for i, definition := range e.definitions { for i, definition := range e.definitions {
if i > 0 { if i > 0 {
w.write("\n") write(w, "\n")
} }
p := itemToParagraph( p := itemToParagraph(
@ -172,18 +118,13 @@ func renderTTYDefinitions(w writer, e Entry) {
} }
} }
func renderTTYNumberedDefinitions(w writer, e Entry) { func renderTTYNumberedDefinitions(w io.Writer, e Entry) {
names, err := ttyDefinitionNames(e.definitions) names := ttyDefinitionNames(e.definitions)
if err != nil {
w.setErr(err)
return
}
maxNameLength := maxLength(names) maxNameLength := maxLength(names)
maxDigits := numDigits(len(e.definitions)) maxDigits := numDigits(len(e.definitions))
for i, definition := range e.definitions { for i, definition := range e.definitions {
if i > 0 { if i > 0 {
w.write("\n") write(w, "\n")
} }
p := itemToParagraph( 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 var cellTexts [][]string
for _, row := range rows { for _, row := range rows {
var c []string var c []string
for _, cell := range row.cells { for _, cell := range row.cells {
txt, err := ttyTextToString(cell.text) txt := ttyTextToString(cell.text)
if err != nil {
return nil, err
}
c = append(c, txt) c = append(c, txt)
} }
cellTexts = append(cellTexts, c) 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 { if len(e.rows) == 0 {
return return
} }
@ -232,11 +169,7 @@ func renderTTYTable(w writer, e Entry) {
return return
} }
cellTexts, err := ttyCellTexts(e.rows) cellTexts := ttyCellTexts(e.rows)
if err != nil {
w.setErr(err)
}
totalSeparatorWidth := (len(cellTexts[0]) - 1) * 3 totalSeparatorWidth := (len(cellTexts[0]) - 1) * 3
if e.wrapWidth > 0 { if e.wrapWidth > 0 {
allocatedWidth := e.wrapWidth - e.indent - totalSeparatorWidth allocatedWidth := e.wrapWidth - e.indent - totalSeparatorWidth
@ -244,7 +177,8 @@ func renderTTYTable(w writer, e Entry) {
targetColumnWidths := targetColumnWidths(allocatedWidth, columnWeights) targetColumnWidths := targetColumnWidths(allocatedWidth, columnWeights)
for i := range cellTexts { for i := range cellTexts {
for j := range cellTexts[i] { 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 hasHeader := e.rows[0].header
w, f := writeWith(w, indent(e.indent, e.indent))
for i := range cellTexts { for i := range cellTexts {
if i > 0 { if i > 0 {
sep := "-" sep := "-"
@ -263,9 +198,9 @@ func renderTTYTable(w writer, e Entry) {
sep = "=" sep = "="
} }
w.write("\n") write(w, "\n")
w.write(timesn(" ", e.indent), timesn(sep, totalWidth)) write(w, timesn(sep, totalWidth))
w.write("\n") write(w, "\n")
} }
lines := make([][]string, len(cellTexts[i])) lines := make([][]string, len(cellTexts[i]))
@ -282,14 +217,12 @@ func renderTTYTable(w writer, e Entry) {
for k := 0; k < maxLines; k++ { for k := 0; k < maxLines; k++ {
if k > 0 { if k > 0 {
w.write("\n") write(w, "\n")
} }
for j := range lines { for j := range lines {
if j == 0 { if j > 0 {
w.write(timesn(" ", e.indent)) write(w, " | ")
} else {
w.write(" | ")
} }
var l string var l string
@ -297,54 +230,57 @@ func renderTTYTable(w writer, e Entry) {
l = lines[j][k] l = lines[j][k]
} }
w.write(padRight(l, columnWidths[j])) write(w, padRight(l, columnWidths[j]))
} }
} }
} }
if hasHeader && len(cellTexts) == 1 { if hasHeader && len(cellTexts) == 1 {
w.write("\n", timesn("=", totalWidth)) write(w, "\n", timesn("=", totalWidth))
} }
f()
} }
func renderTTYCode(w writer, e Entry) { func renderTTYCode(w io.Writer, e Entry) {
e.text.text = escapeTeletypePrev(e.text.text) w, f := writeWith(w, escapeTeletype(), indent(e.indent, e.indent))
writeLines(w, e.text.text, 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.topLevel = false
s.multiple = false s.multiple = false
renderTTYSyntaxItem(w, s) renderTTYSyntaxItem(w, s)
w.write("...") write(w, "...")
} }
func renderTTYRequired(w writer, s SyntaxItem) { func renderTTYRequired(w io.Writer, s SyntaxItem) {
s.delimited = true s.delimited = true
s.topLevel = false s.topLevel = false
s.required = false s.required = false
w.write("<") write(w, "<")
renderTTYSyntaxItem(w, s) renderTTYSyntaxItem(w, s)
w.write(">") write(w, ">")
} }
func renderTTYOptional(w writer, s SyntaxItem) { func renderTTYOptional(w io.Writer, s SyntaxItem) {
s.delimited = true s.delimited = true
s.topLevel = false s.topLevel = false
s.optional = false s.optional = false
w.write("[") write(w, "[")
renderTTYSyntaxItem(w, s) 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 { if !s.delimited && !s.topLevel {
w.write("(") write(w, "(")
} }
for i, item := range s.sequence { for i, item := range s.sequence {
if i > 0 { if i > 0 {
w.write(" ") write(w, " ")
} }
item.delimited = false item.delimited = false
@ -352,13 +288,13 @@ func renderTTYSequence(w writer, s SyntaxItem) {
} }
if !s.delimited && !s.topLevel { 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 { if !s.delimited && !s.topLevel {
w.write("(") write(w, "(")
} }
for i, item := range s.choice { for i, item := range s.choice {
@ -368,7 +304,7 @@ func renderTTYChoice(w writer, s SyntaxItem) {
separator = "\n" separator = "\n"
} }
w.write(separator) write(w, separator)
} }
item.delimited = false item.delimited = false
@ -376,15 +312,15 @@ func renderTTYChoice(w writer, s SyntaxItem) {
} }
if !s.delimited && !s.topLevel { if !s.delimited && !s.topLevel {
w.write(")") write(w, ")")
} }
} }
func renderTTYSymbol(w writer, s SyntaxItem) { func renderTTYSymbol(w io.Writer, s SyntaxItem) {
w.write(escapeTeletypePrev(s.symbol)) write(w, s.symbol)
} }
func renderTTYSyntaxItem(w writer, s SyntaxItem) { func renderTTYSyntaxItem(w io.Writer, s SyntaxItem) {
switch { switch {
// foo... // 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 := e.syntax
s.topLevel = true s.topLevel = true
w.write(timesn(" ", e.indent+e.indentFirst))
renderTTYSyntaxItem(w, s) renderTTYSyntaxItem(w, s)
f()
} }
func renderTeletype(out io.Writer, d Document) error { func renderTeletype(out io.Writer, d Document) error {
tw := &textWriter{out: out} w, f := writeWith(out, ttyNBSP(), errorHandler)
w := &editor{
out: tw,
replace: map[string]string{
"\u00a0": " ",
},
}
for i, e := range d.entries { for i, e := range d.entries {
if i > 0 { if i > 0 {
w.write("\n\n") write(w, "\n\n")
} }
switch e.typ { switch e.typ {
@ -459,8 +389,9 @@ func renderTeletype(out io.Writer, d Document) error {
} }
if len(d.entries) > 0 { 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.ZeroOrMore(textfmt.Symbol("Entry")),
textfmt.Symbol(")"), textfmt.Symbol(")"),
), ),
0,
8, 8,
0,
), ),
textfmt.Title(1, "Entries:"), textfmt.Title(1, "Entries:"),
@ -243,11 +243,17 @@ Entry explanations:
t.Fatal(err) t.Fatal(err)
} }
if b.String() != " Some sample\n text... on\n multiple\n lines.\n" { const expect = `
t.Fatal(b.String()) Some sample
text... on
multiple
lines.
`
if "\n"+b.String() != expect {
t.Fatal("\n" + b.String())
} }
}) })
}) })
t.Run("indent", func(t *testing.T) { t.Run("indent", func(t *testing.T) {

55
text.go
View File

@ -1,11 +1,6 @@
package textfmt package textfmt
import ( import "strings"
"bytes"
"fmt"
"strings"
"unicode"
)
func timesn(s string, n int) string { func timesn(s string, n int) string {
if n < 0 { if n < 0 {
@ -16,7 +11,6 @@ func timesn(s string, n int) string {
return strings.Join(ss, s) return strings.Join(ss, s)
} }
// non-negative numbers only
func numDigits(n int) int { func numDigits(n int) int {
if n == 0 { if n == 0 {
return 1 return 1
@ -51,53 +45,6 @@ func padRight(s string, n int) string {
return s + timesn("\u00a0", n) 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 { func itemToParagraph(list Entry, itemText Txt, prefix ...string) Entry {
p := Entry{ p := Entry{
typ: paragraph, 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 package textfmt
import ( import (
"bytes"
"code.squareroundforest.org/arpio/textedit"
"fmt" "fmt"
"io" "io"
"slices"
"strings"
) )
type writer interface { type writer interface {
@ -14,136 +14,44 @@ type writer interface {
setErr(err error) // TODO: remove 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 { type roffWriter struct {
w io.Writer w io.Writer
internal bool err error
err error
} }
type mdWriter struct { type mdWriter struct {
w io.Writer w io.Writer
internal bool err error
err error
} }
func (w *ttyWriter) write(a ...any) { type wrapper func(io.Writer) (io.Writer, func() error)
for _, ai := range a {
if w.err != nil {
return
}
s := fmt.Sprint(ai) type errorWriter struct {
r := []rune(s) out io.Writer
if !w.internal { err error
for i := range r { }
if r[i] == '\u00a0' {
r[i] = ' '
}
}
}
if _, err := w.w.Write([]byte(string(r))); err != nil { func (w *errorWriter) Write(p []byte) (int, error) {
w.err = err 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 &roffWriter{
return w.err w: textedit.New(
} out,
textedit.Replace("\u00a0", "\\~"),
func (w *ttyWriter) setErr(err error) { ),
w.err = err }
} }
func (w *roffWriter) write(a ...any) { func (w *roffWriter) write(a ...any) {
@ -152,29 +60,24 @@ func (w *roffWriter) write(a ...any) {
return return
} }
var rr []rune if _, err := w.w.Write([]byte(fmt.Sprint(ai))); err != nil {
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 {
w.err = err 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 { func (w *roffWriter) error() error {
return w.err return w.err
@ -184,28 +87,43 @@ func (w *roffWriter) setErr(err error) {
w.err = err 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) { func (w *mdWriter) write(a ...any) {
for _, ai := range a { for _, ai := range a {
if w.err != nil { if w.err != nil {
return return
} }
s := fmt.Sprint(ai) if _, err := w.w.Write([]byte(fmt.Sprint(ai))); err != nil {
r := []rune(s) w.err = err
if !w.internal { return
for i := range r {
if r[i] == '\u00a0' {
r[i] = ' '
}
}
} }
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 { func (w *mdWriter) error() error {
return w.err return w.err
@ -215,19 +133,66 @@ func (w *mdWriter) setErr(err error) {
w.err = err w.err = err
} }
func writeLines(w writer, txt string, indentFirst, indentRest int) { func writeWith(out io.Writer, w ...wrapper) (io.Writer, func() (io.Writer, error)) {
lines := strings.Split(txt, "\n") var f []func() error
for i, l := range lines { ww := out
if i > 0 { for i := len(w) - 1; i >= 0; i-- {
w.write("\n") 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 return out, nil
if i > 0 { }
indent = indentRest }
}
func errorHandler(out io.Writer) (io.Writer, func() error) {
w.write(timesn(" ", indent)) ew := errorWriter{out: out}
w.write(l) 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
}
} }
} }