refactor tty rendering
This commit is contained in:
parent
c2b0d69b5b
commit
4c0d034620
183
escape.go
183
escape.go
@ -1,72 +1,149 @@
|
|||||||
package textfmt
|
package textfmt
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"code.squareroundforest.org/arpio/textedit"
|
||||||
type escapeRange struct {
|
"errors"
|
||||||
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
|
|
||||||
)
|
)
|
||||||
|
|
||||||
output, found = e.escape[ri]
|
type mdEscapeState struct {
|
||||||
if !found {
|
lineStarted bool
|
||||||
output, found = e.inEscapeRange(ri)
|
numberOnNewLine bool
|
||||||
|
linkValue bool
|
||||||
|
linkClosed bool
|
||||||
|
linkOpen bool
|
||||||
}
|
}
|
||||||
|
|
||||||
if !found {
|
func escapeTeletypeEdit(r rune, s struct{}) ([]rune, struct{}) {
|
||||||
conditional := e.conditionalEscape[ri]
|
if r >= 0x00 && r <= 0x1f && r != '\n' && r != '\t' {
|
||||||
if conditional != nil {
|
return []rune{0xb7}, s
|
||||||
output, found = conditional(e.state, ri)
|
}
|
||||||
|
|
||||||
|
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 {
|
func escapeRoff(additional ...string) wrapper {
|
||||||
output = string(ri)
|
return editor(
|
||||||
|
textedit.Func(
|
||||||
|
escapeRoffEdit(additional...),
|
||||||
|
func(bool) []rune { return nil },
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
e.out.write(output)
|
func escapeMarkdownEdit(r rune, s mdEscapeState) ([]rune, mdEscapeState) {
|
||||||
if e.updateState != nil {
|
var ret []rune
|
||||||
e.state = e.updateState(e.state, ri)
|
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() {
|
s.numberOnNewLine = (!s.lineStarted || s.numberOnNewLine) && r >= '0' && r <= '9'
|
||||||
e.out.flush()
|
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 {
|
func escapeMarkdown() wrapper {
|
||||||
return e.out.error()
|
return editor(
|
||||||
}
|
textedit.Func(
|
||||||
|
escapeMarkdownEdit,
|
||||||
func (e *escape[S]) setErr(err error) {
|
func(mdEscapeState) []rune { return nil },
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
10
go.mod
10
go.mod
@ -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
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-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=
|
||||||
|
|||||||
4
html.go
4
html.go
@ -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)...),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
67
html_test.go
67
html_test.go
@ -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>
|
||||||
`
|
`
|
||||||
|
|||||||
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
|
rows []TableRow
|
||||||
syntax SyntaxItem
|
syntax SyntaxItem
|
||||||
wrapWidth int
|
wrapWidth int
|
||||||
|
wrapWidthFirst int
|
||||||
indent int
|
indent int
|
||||||
indentFirst int
|
indentFirst int
|
||||||
man struct {
|
man struct {
|
||||||
|
|||||||
197
markdown.go
197
markdown.go
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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:"),
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
174
runoff.go
174
runoff.go
@ -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 {
|
|
||||||
panic(errors.New(invalidAdditional))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
am := make(map[rune][]rune)
|
func textToString(t Txt) string {
|
||||||
for i := 0; i > len(additional); i += 2 {
|
if len(t.cat) == 0 && t.link == "" {
|
||||||
r := []rune(additional[i])
|
return trim(t.text)
|
||||||
if len(r) != 1 {
|
|
||||||
panic(errors.New(invalidAdditional))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
am[r[0]] = []rune(additional[i+1])
|
if len(t.cat) == 0 && t.text == "" {
|
||||||
|
return trim(t.link)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, r := range []rune(s) {
|
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("\\&.")...)
|
b := bytes.NewBuffer(nil)
|
||||||
lineStarted = true
|
for i := range t.cat {
|
||||||
continue
|
if i > 0 {
|
||||||
case '\'':
|
b.WriteRune(' ')
|
||||||
if lineStarted {
|
|
||||||
e = append(e, '\'')
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
e = append(e, []rune("\\&'")...)
|
b.WriteString(textToString(t.cat[i]))
|
||||||
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 {
|
return editString(b.String(), singleLine())
|
||||||
e = append(e, a...)
|
|
||||||
lineStarted = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
e = append(e, r)
|
|
||||||
lineStarted = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return string(e)
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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:"),
|
||||||
|
|||||||
253
teletype.go
253
teletype.go
@ -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 {
|
func ttyDefinitionNames(d []DefinitionItem) []string {
|
||||||
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) {
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
return n, nil
|
func renderTTYText(w io.Writer, text Txt) {
|
||||||
}
|
|
||||||
|
|
||||||
func renderTTYText(w 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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.write(text.link)
|
if text.text == "" {
|
||||||
|
write(w, text.link)
|
||||||
|
f()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.write(text.text)
|
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))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderTTYCode(w writer, e Entry) {
|
f()
|
||||||
e.text.text = escapeTeletypePrev(e.text.text)
|
|
||||||
writeLines(w, e.text.text, e.indent, e.indent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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.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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
55
text.go
@ -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
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
|
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 {
|
|
||||||
|
type errorWriter struct {
|
||||||
|
out io.Writer
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *errorWriter) Write(p []byte) (int, error) {
|
||||||
if w.err != nil {
|
if w.err != nil {
|
||||||
return
|
return 0, w.err
|
||||||
}
|
}
|
||||||
|
|
||||||
s := fmt.Sprint(ai)
|
var n int
|
||||||
r := []rune(s)
|
n, w.err = w.out.Write(p)
|
||||||
if !w.internal {
|
return n, w.err
|
||||||
for i := range r {
|
|
||||||
if r[i] == '\u00a0' {
|
|
||||||
r[i] = ' '
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := w.w.Write([]byte(string(r))); err != nil {
|
func newRoffWriter(out io.Writer, internal bool) *roffWriter {
|
||||||
w.err = err
|
if internal {
|
||||||
}
|
return &roffWriter{w: out}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *ttyWriter) flush() {}
|
return &roffWriter{
|
||||||
|
w: textedit.New(
|
||||||
func (w *ttyWriter) error() error {
|
out,
|
||||||
return w.err
|
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,30 +60,25 @@ 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)
|
w.err = err
|
||||||
r := []rune(s)
|
return
|
||||||
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 {
|
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
|
w.err = err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *roffWriter) flush() {}
|
|
||||||
|
|
||||||
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)
|
func (w *mdWriter) flush() {
|
||||||
_, w.err = w.w.Write([]byte(s))
|
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 {
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
indent := indentFirst
|
return ww, func() (io.Writer, error) {
|
||||||
if i > 0 {
|
for _, fi := range f {
|
||||||
indent = indentRest
|
if err := fi(); err != nil {
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
w.write(timesn(" ", indent))
|
return out, nil
|
||||||
w.write(l)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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