no screaming case
This commit is contained in:
parent
e4dd3ca0df
commit
c2b0d69b5b
10
Makefile
10
Makefile
@ -1,17 +1,17 @@
|
|||||||
SOURCES = $(shell find . -name "*.go")
|
sources = $(shell find . -name "*.go")
|
||||||
|
|
||||||
default: build
|
default: build
|
||||||
|
|
||||||
build: $(SOURCES)
|
build: $(sources)
|
||||||
go build
|
go build
|
||||||
|
|
||||||
fmt: $(SOURCES)
|
fmt: $(sources)
|
||||||
go fmt
|
go fmt
|
||||||
|
|
||||||
check: $(SOURCES)
|
check: $(sources)
|
||||||
go test -count 1
|
go test -count 1
|
||||||
|
|
||||||
.cover: $(SOURCES)
|
.cover: $(sources)
|
||||||
go test -count 1 -coverprofile .cover
|
go test -count 1 -coverprofile .cover
|
||||||
|
|
||||||
cover: .cover
|
cover: .cover
|
||||||
|
|||||||
72
escape.go
Normal file
72
escape.go
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
output, found = e.escape[ri]
|
||||||
|
if !found {
|
||||||
|
output, found = e.inEscapeRange(ri)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
conditional := e.conditionalEscape[ri]
|
||||||
|
if conditional != nil {
|
||||||
|
output, found = conditional(e.state, ri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
output = string(ri)
|
||||||
|
}
|
||||||
|
|
||||||
|
e.out.write(output)
|
||||||
|
if e.updateState != nil {
|
||||||
|
e.state = e.updateState(e.state, ri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *escape[S]) flush() {
|
||||||
|
e.out.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *escape[S]) error() error {
|
||||||
|
return e.out.error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *escape[S]) setErr(err error) {
|
||||||
|
}
|
||||||
82
indent.go
Normal file
82
indent.go
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
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) {
|
||||||
|
}
|
||||||
208
markdown.go
208
markdown.go
@ -5,70 +5,158 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"slices"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func escapeMarkdown(s string, additional ...rune) string {
|
type mdEscapeState struct {
|
||||||
var (
|
newLine bool
|
||||||
rr []rune
|
numberOnNewLine bool
|
||||||
isNumberOnNewLine bool
|
linkValue bool
|
||||||
isLinkOpen, isLinkClosed, isLinkValue bool
|
linkClosed bool
|
||||||
)
|
linkOpen bool
|
||||||
|
}
|
||||||
|
|
||||||
isNewLine := true
|
func updateMDEscapeState(s mdEscapeState, r rune) mdEscapeState {
|
||||||
r := []rune(s)
|
return mdEscapeState{
|
||||||
for _, ri := range r {
|
numberOnNewLine: (s.newLine || s.numberOnNewLine) && r >= '0' && r <= '9',
|
||||||
switch ri {
|
newLine: r == '\n',
|
||||||
case '\\', '`', '*', '_', '[', ']', '#', '<', '>':
|
linkValue: s.linkClosed && r == '(' || s.linkValue && r != ')',
|
||||||
rr = append(rr, '\\', ri)
|
linkClosed: s.linkOpen && r == ']',
|
||||||
default:
|
linkOpen: !s.linkValue && r == '[' || s.linkOpen && r != ']',
|
||||||
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'
|
func escapeMarkdown(out writer, additional ...rune) writer {
|
||||||
isNewLine = ri == '\n'
|
esc := map[rune]string{
|
||||||
isLinkValue = isLinkClosed && ri == '(' || isLinkValue && ri != ')'
|
'\\': "\\\\",
|
||||||
isLinkClosed = isLinkOpen && ri == ']'
|
'`': "\\`",
|
||||||
isLinkOpen = !isLinkValue && ri == '[' || isLinkOpen && ri != ']'
|
'*': "\\*",
|
||||||
|
'_': "\\_",
|
||||||
|
'[': "\\[",
|
||||||
|
']': "\\]",
|
||||||
|
'#': "\\#",
|
||||||
|
'<': "\\<",
|
||||||
|
'>': "\\>",
|
||||||
}
|
}
|
||||||
|
|
||||||
return string(rr)
|
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) {
|
||||||
@ -128,9 +216,9 @@ func renderMDText(w writer, text Txt) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
text.text = singleLine(text.text)
|
text.text = singleLine(text.text)
|
||||||
text.text = escapeMarkdown(text.text)
|
text.text = escapeMarkdownPrev(text.text)
|
||||||
text.link = singleLine(text.link)
|
text.link = singleLine(text.link)
|
||||||
text.link = escapeMarkdown(text.link)
|
text.link = escapeMarkdownPrev(text.link)
|
||||||
if text.bold {
|
if text.bold {
|
||||||
w.write("**")
|
w.write("**")
|
||||||
}
|
}
|
||||||
@ -381,7 +469,7 @@ func renderMDChoice(w writer, s SyntaxItem) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func renderMDSymbol(w writer, s SyntaxItem) {
|
func renderMDSymbol(w writer, s SyntaxItem) {
|
||||||
w.write(escapeTeletype(s.symbol))
|
w.write(escapeMarkdownPrev(s.symbol))
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderMDSyntaxItem(w writer, s SyntaxItem) {
|
func renderMDSyntaxItem(w writer, s SyntaxItem) {
|
||||||
|
|||||||
38
notes.txt
38
notes.txt
@ -2,4 +2,40 @@ 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
|
||||||
show top level choice on separate lines in the same block
|
improve wrapping of list paragraphs by allowing different first line wrap
|
||||||
|
there should be no errors other than actual IO
|
||||||
|
|
||||||
|
[refactor]
|
||||||
|
stop on errors earlier where possible
|
||||||
|
denormalize individual render functions
|
||||||
|
support single line in indent, indentOnly option
|
||||||
|
escape is more generic than editor:
|
||||||
|
- could replace editor
|
||||||
|
- maybe indent, too
|
||||||
|
- could become its own library
|
||||||
|
- could be used in HTML, too
|
||||||
|
collect the common transformations:
|
||||||
|
- escape
|
||||||
|
- wrap
|
||||||
|
- collapse to single line
|
||||||
|
- fill spaces
|
||||||
|
- convert spaces
|
||||||
|
- width and height calculations
|
||||||
|
- ...
|
||||||
|
identify the order of these transformations while rendering the different entries:
|
||||||
|
- tty:
|
||||||
|
- title
|
||||||
|
- paragraph
|
||||||
|
- ...
|
||||||
|
- roff:
|
||||||
|
- title
|
||||||
|
- paragraph
|
||||||
|
- ...
|
||||||
|
- md:
|
||||||
|
- title
|
||||||
|
- paragraph
|
||||||
|
- ...
|
||||||
|
- html:
|
||||||
|
- title
|
||||||
|
- paragraph
|
||||||
|
- ...
|
||||||
|
|||||||
63
runoff.go
63
runoff.go
@ -9,7 +9,61 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func escapeRoff(s string, additional ...string) string {
|
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"
|
const invalidAdditional = "invalid additional escape definition"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -75,6 +129,7 @@ func escapeRoff(s string, additional ...string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return string(e)
|
return string(e)
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
func manPageDate(d time.Time) string {
|
func manPageDate(d time.Time) string {
|
||||||
@ -83,7 +138,7 @@ func manPageDate(d time.Time) string {
|
|||||||
|
|
||||||
func roffString(s string, additionalEscape ...string) string {
|
func roffString(s string, additionalEscape ...string) string {
|
||||||
s = singleLine(s)
|
s = singleLine(s)
|
||||||
return escapeRoff(s, additionalEscape...)
|
return escapeRoffPrev(s, additionalEscape...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderRoffString(w writer, s string, additionalEscape ...string) {
|
func renderRoffString(w writer, s string, additionalEscape ...string) {
|
||||||
@ -341,7 +396,7 @@ 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 = escapeRoff(e.text.text)
|
e.text.text = escapeRoffPrev(e.text.text)
|
||||||
writeLines(w, e.text.text, e.indent, e.indent)
|
writeLines(w, e.text.text, e.indent, e.indent)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -414,7 +469,7 @@ func renderRoffChoice(w writer, s SyntaxItem) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func renderRoffSymbol(w writer, s SyntaxItem) {
|
func renderRoffSymbol(w writer, s SyntaxItem) {
|
||||||
w.write(escapeRoff(s.symbol))
|
w.write(escapeRoffPrev(s.symbol))
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderRoffSyntaxItem(w writer, s SyntaxItem) {
|
func renderRoffSyntaxItem(w writer, s SyntaxItem) {
|
||||||
|
|||||||
65
teletype.go
65
teletype.go
@ -8,7 +8,32 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func escapeTeletype(s string) string {
|
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 {
|
||||||
|
var b bytes.Buffer
|
||||||
|
w := &textWriter{out: &b}
|
||||||
|
e := escapeTeletype(w)
|
||||||
|
e.write(s)
|
||||||
|
return b.String()
|
||||||
|
/*
|
||||||
r := []rune(s)
|
r := []rune(s)
|
||||||
for i := range r {
|
for i := range r {
|
||||||
if r[i] >= 0x00 && r[i] <= 0x1f && r[i] != '\n' && r[i] != '\t' {
|
if r[i] >= 0x00 && r[i] <= 0x1f && r[i] != '\n' && r[i] != '\t' {
|
||||||
@ -21,6 +46,7 @@ func escapeTeletype(s string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return string(r)
|
return string(r)
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
func ttyTextToString(text Txt) (string, error) {
|
func ttyTextToString(text Txt) (string, error) {
|
||||||
@ -62,9 +88,9 @@ func renderTTYText(w writer, text Txt) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
text.text = singleLine(text.text)
|
text.text = singleLine(text.text)
|
||||||
text.text = escapeTeletype(text.text)
|
text.text = escapeTeletypePrev(text.text)
|
||||||
text.link = singleLine(text.link)
|
text.link = singleLine(text.link)
|
||||||
text.link = escapeTeletype(text.link)
|
text.link = escapeTeletypePrev(text.link)
|
||||||
if text.link != "" {
|
if text.link != "" {
|
||||||
if text.text != "" {
|
if text.text != "" {
|
||||||
w.write(text.text)
|
w.write(text.text)
|
||||||
@ -282,7 +308,7 @@ func renderTTYTable(w writer, e Entry) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func renderTTYCode(w writer, e Entry) {
|
func renderTTYCode(w writer, e Entry) {
|
||||||
e.text.text = escapeTeletype(e.text.text)
|
e.text.text = escapeTeletypePrev(e.text.text)
|
||||||
writeLines(w, e.text.text, e.indent, e.indent)
|
writeLines(w, e.text.text, e.indent, e.indent)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -355,7 +381,7 @@ func renderTTYChoice(w writer, s SyntaxItem) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func renderTTYSymbol(w writer, s SyntaxItem) {
|
func renderTTYSymbol(w writer, s SyntaxItem) {
|
||||||
w.write(escapeTeletype(s.symbol))
|
w.write(escapeTeletypePrev(s.symbol))
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderTTYSyntaxItem(w writer, s SyntaxItem) {
|
func renderTTYSyntaxItem(w writer, s SyntaxItem) {
|
||||||
@ -395,7 +421,14 @@ func renderTTYSyntax(w writer, e Entry) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func renderTeletype(out io.Writer, d Document) error {
|
func renderTeletype(out io.Writer, d Document) error {
|
||||||
w := ttyWriter{w: out}
|
tw := &textWriter{out: out}
|
||||||
|
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")
|
w.write("\n\n")
|
||||||
@ -403,23 +436,23 @@ func renderTeletype(out io.Writer, d Document) error {
|
|||||||
|
|
||||||
switch e.typ {
|
switch e.typ {
|
||||||
case title:
|
case title:
|
||||||
renderTTYTitle(&w, e)
|
renderTTYTitle(w, e)
|
||||||
case paragraph:
|
case paragraph:
|
||||||
renderTTYParagraph(&w, e)
|
renderTTYParagraph(w, e)
|
||||||
case list:
|
case list:
|
||||||
renderTTYList(&w, e)
|
renderTTYList(w, e)
|
||||||
case numberedList:
|
case numberedList:
|
||||||
renderTTYNumberedList(&w, e)
|
renderTTYNumberedList(w, e)
|
||||||
case definitions:
|
case definitions:
|
||||||
renderTTYDefinitions(&w, e)
|
renderTTYDefinitions(w, e)
|
||||||
case numberedDefinitions:
|
case numberedDefinitions:
|
||||||
renderTTYNumberedDefinitions(&w, e)
|
renderTTYNumberedDefinitions(w, e)
|
||||||
case table:
|
case table:
|
||||||
renderTTYTable(&w, e)
|
renderTTYTable(w, e)
|
||||||
case code:
|
case code:
|
||||||
renderTTYCode(&w, e)
|
renderTTYCode(w, e)
|
||||||
case syntax:
|
case syntax:
|
||||||
renderTTYSyntax(&w, e)
|
renderTTYSyntax(w, e)
|
||||||
default:
|
default:
|
||||||
return errors.New("invalid entry")
|
return errors.New("invalid entry")
|
||||||
}
|
}
|
||||||
@ -429,5 +462,5 @@ func renderTeletype(out io.Writer, d Document) error {
|
|||||||
w.write("\n")
|
w.write("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
return w.err
|
return w.error()
|
||||||
}
|
}
|
||||||
|
|||||||
92
write.go
92
write.go
@ -3,15 +3,99 @@ package textfmt
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type writer interface {
|
type writer interface {
|
||||||
write(...any)
|
write(...any)
|
||||||
|
flush()
|
||||||
error() error
|
error() error
|
||||||
setErr(error)
|
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 {
|
type ttyWriter struct {
|
||||||
w io.Writer
|
w io.Writer
|
||||||
internal bool
|
internal bool
|
||||||
@ -52,6 +136,8 @@ func (w *ttyWriter) write(a ...any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *ttyWriter) flush() {}
|
||||||
|
|
||||||
func (w *ttyWriter) error() error {
|
func (w *ttyWriter) error() error {
|
||||||
return w.err
|
return w.err
|
||||||
}
|
}
|
||||||
@ -88,6 +174,8 @@ func (w *roffWriter) write(a ...any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *roffWriter) flush() {}
|
||||||
|
|
||||||
func (w *roffWriter) error() error {
|
func (w *roffWriter) error() error {
|
||||||
return w.err
|
return w.err
|
||||||
}
|
}
|
||||||
@ -117,6 +205,8 @@ func (w *mdWriter) write(a ...any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *mdWriter) flush() {}
|
||||||
|
|
||||||
func (w *mdWriter) error() error {
|
func (w *mdWriter) error() error {
|
||||||
return w.err
|
return w.err
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user