refactor tty rendering
This commit is contained in:
parent
c2b0d69b5b
commit
4c0d034620
183
escape.go
183
escape.go
@ -1,72 +1,149 @@
|
||||
package textfmt
|
||||
|
||||
import "fmt"
|
||||
|
||||
type escapeRange struct {
|
||||
from, to rune
|
||||
replacement string
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
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
|
||||
import (
|
||||
"code.squareroundforest.org/arpio/textedit"
|
||||
"errors"
|
||||
)
|
||||
|
||||
output, found = e.escape[ri]
|
||||
if !found {
|
||||
output, found = e.inEscapeRange(ri)
|
||||
type mdEscapeState struct {
|
||||
lineStarted bool
|
||||
numberOnNewLine bool
|
||||
linkValue bool
|
||||
linkClosed bool
|
||||
linkOpen bool
|
||||
}
|
||||
|
||||
if !found {
|
||||
conditional := e.conditionalEscape[ri]
|
||||
if conditional != nil {
|
||||
output, found = conditional(e.state, ri)
|
||||
func escapeTeletypeEdit(r rune, s struct{}) ([]rune, struct{}) {
|
||||
if r >= 0x00 && r <= 0x1f && r != '\n' && r != '\t' {
|
||||
return []rune{0xb7}, s
|
||||
}
|
||||
|
||||
if r >= 0x7f && r <= 0x9f {
|
||||
return []rune{0xb7}, s
|
||||
}
|
||||
|
||||
return []rune{r}, s
|
||||
}
|
||||
|
||||
func escapeTeletype() wrapper {
|
||||
return editor(
|
||||
textedit.Func(
|
||||
escapeTeletypeEdit,
|
||||
func(struct{}) []rune { return nil },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func escapeRoffEdit(additional ...string) func(rune, bool) ([]rune, bool) {
|
||||
const invalidAdditional = "invalid additional escape definition"
|
||||
if len(additional)%2 != 0 {
|
||||
panic(errors.New(invalidAdditional))
|
||||
}
|
||||
|
||||
esc := map[rune][]rune{
|
||||
'\\': []rune("\\\\"),
|
||||
'\u00a0': []rune("\\~"),
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
output = string(ri)
|
||||
func escapeRoff(additional ...string) wrapper {
|
||||
return editor(
|
||||
textedit.Func(
|
||||
escapeRoffEdit(additional...),
|
||||
func(bool) []rune { return nil },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
e.out.write(output)
|
||||
if e.updateState != nil {
|
||||
e.state = e.updateState(e.state, ri)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *escape[S]) flush() {
|
||||
e.out.flush()
|
||||
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]) error() error {
|
||||
return e.out.error()
|
||||
}
|
||||
|
||||
func (e *escape[S]) setErr(err error) {
|
||||
func escapeMarkdown() wrapper {
|
||||
return editor(
|
||||
textedit.Func(
|
||||
escapeMarkdownEdit,
|
||||
func(mdEscapeState) []rune { return nil },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
10
go.mod
10
go.mod
@ -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
8
go.sum
@ -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=
|
||||
|
||||
4
html.go
4
html.go
@ -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)...),
|
||||
)
|
||||
}
|
||||
|
||||
67
html_test.go
67
html_test.go
@ -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>
|
||||
`
|
||||
|
||||
82
indent.go
82
indent.go
@ -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) {
|
||||
}
|
||||
1
lib.go
1
lib.go
@ -62,6 +62,7 @@ type Entry struct {
|
||||
rows []TableRow
|
||||
syntax SyntaxItem
|
||||
wrapWidth int
|
||||
wrapWidthFirst int
|
||||
indent int
|
||||
indentFirst int
|
||||
man struct {
|
||||
|
||||
197
markdown.go
197
markdown.go
@ -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
|
||||
}
|
||||
|
||||
@ -47,8 +47,8 @@ func TestMarkdown(t *testing.T) {
|
||||
textfmt.ZeroOrMore(textfmt.Symbol("Entry")),
|
||||
textfmt.Symbol(")"),
|
||||
),
|
||||
0,
|
||||
8,
|
||||
0,
|
||||
),
|
||||
|
||||
textfmt.Title(1, "Entries:"),
|
||||
|
||||
@ -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
|
||||
|
||||
174
runoff.go
174
runoff.go
@ -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))
|
||||
}
|
||||
|
||||
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))
|
||||
func textToString(t Txt) string {
|
||||
if len(t.cat) == 0 && t.link == "" {
|
||||
return trim(t.text)
|
||||
}
|
||||
|
||||
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
|
||||
if len(t.cat) == 0 {
|
||||
return fmt.Sprintf("%s (%s)", t.text, t.link)
|
||||
}
|
||||
|
||||
e = append(e, []rune("\\&.")...)
|
||||
lineStarted = true
|
||||
continue
|
||||
case '\'':
|
||||
if lineStarted {
|
||||
e = append(e, '\'')
|
||||
continue
|
||||
b := bytes.NewBuffer(nil)
|
||||
for i := range t.cat {
|
||||
if i > 0 {
|
||||
b.WriteRune(' ')
|
||||
}
|
||||
|
||||
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
|
||||
b.WriteString(textToString(t.cat[i]))
|
||||
}
|
||||
|
||||
if a, ok := am[r]; ok {
|
||||
e = append(e, a...)
|
||||
lineStarted = true
|
||||
continue
|
||||
}
|
||||
|
||||
e = append(e, r)
|
||||
lineStarted = true
|
||||
}
|
||||
|
||||
return string(e)
|
||||
*/
|
||||
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
|
||||
}
|
||||
|
||||
@ -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:"),
|
||||
|
||||
253
teletype.go
253
teletype.go
@ -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, ttyTextToString(di.name))
|
||||
}
|
||||
|
||||
n = append(n, name)
|
||||
return n
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
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(")")
|
||||
var f func() (io.Writer, error)
|
||||
w, f = writeWith(w, escapeTeletype(), singleLine())
|
||||
if text.link == "" {
|
||||
write(w, text.text)
|
||||
f()
|
||||
return
|
||||
}
|
||||
|
||||
w.write(text.link)
|
||||
if text.text == "" {
|
||||
write(w, text.link)
|
||||
f()
|
||||
return
|
||||
}
|
||||
|
||||
w.write(text.text)
|
||||
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))
|
||||
}
|
||||
|
||||
func renderTTYCode(w writer, e Entry) {
|
||||
e.text.text = escapeTeletypePrev(e.text.text)
|
||||
writeLines(w, e.text.text, e.indent, e.indent)
|
||||
f()
|
||||
}
|
||||
|
||||
func renderTTYMultiple(w writer, s SyntaxItem) {
|
||||
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 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
|
||||
}
|
||||
|
||||
@ -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
55
text.go
@ -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
52
wrap.go
@ -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")
|
||||
}
|
||||
263
write.go
263
write.go
@ -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
|
||||
}
|
||||
|
||||
type mdWriter struct {
|
||||
w io.Writer
|
||||
internal bool
|
||||
err error
|
||||
}
|
||||
|
||||
func (w *ttyWriter) write(a ...any) {
|
||||
for _, ai := range a {
|
||||
type wrapper func(io.Writer) (io.Writer, func() error)
|
||||
|
||||
type errorWriter struct {
|
||||
out io.Writer
|
||||
err error
|
||||
}
|
||||
|
||||
func (w *errorWriter) Write(p []byte) (int, error) {
|
||||
if w.err != nil {
|
||||
return
|
||||
return 0, w.err
|
||||
}
|
||||
|
||||
s := fmt.Sprint(ai)
|
||||
r := []rune(s)
|
||||
if !w.internal {
|
||||
for i := range r {
|
||||
if r[i] == '\u00a0' {
|
||||
r[i] = ' '
|
||||
}
|
||||
}
|
||||
var n int
|
||||
n, w.err = w.out.Write(p)
|
||||
return n, w.err
|
||||
}
|
||||
|
||||
if _, err := w.w.Write([]byte(string(r))); err != nil {
|
||||
w.err = err
|
||||
}
|
||||
}
|
||||
func newRoffWriter(out io.Writer, internal bool) *roffWriter {
|
||||
if internal {
|
||||
return &roffWriter{w: out}
|
||||
}
|
||||
|
||||
func (w *ttyWriter) flush() {}
|
||||
|
||||
func (w *ttyWriter) error() error {
|
||||
return w.err
|
||||
return &roffWriter{
|
||||
w: textedit.New(
|
||||
out,
|
||||
textedit.Replace("\u00a0", "\\~"),
|
||||
),
|
||||
}
|
||||
|
||||
func (w *ttyWriter) setErr(err error) {
|
||||
w.err = err
|
||||
}
|
||||
|
||||
func (w *roffWriter) write(a ...any) {
|
||||
@ -152,30 +60,25 @@ 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
|
||||
if _, err := w.w.Write([]byte(fmt.Sprint(ai))); err != nil {
|
||||
w.err = err
|
||||
return
|
||||
}
|
||||
|
||||
rr = append(rr, r[i])
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := w.w.Write([]byte(string(rr))); err != nil {
|
||||
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) flush() {}
|
||||
|
||||
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() {
|
||||
if w.err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
func (w *mdWriter) flush() {}
|
||||
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)
|
||||
}
|
||||
|
||||
indent := indentFirst
|
||||
if i > 0 {
|
||||
indent = indentRest
|
||||
return ww, func() (io.Writer, error) {
|
||||
for _, fi := range f {
|
||||
if err := fi(); err != nil {
|
||||
return out, err
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user