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
|
||||
|
||||
build: $(SOURCES)
|
||||
build: $(sources)
|
||||
go build
|
||||
|
||||
fmt: $(SOURCES)
|
||||
fmt: $(sources)
|
||||
go fmt
|
||||
|
||||
check: $(SOURCES)
|
||||
check: $(sources)
|
||||
go test -count 1
|
||||
|
||||
.cover: $(SOURCES)
|
||||
.cover: $(sources)
|
||||
go test -count 1 -coverprofile .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) {
|
||||
}
|
||||
98
markdown.go
98
markdown.go
@ -5,11 +5,98 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func escapeMarkdown(s string, additional ...rune) string {
|
||||
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
|
||||
@ -69,6 +156,7 @@ func escapeMarkdown(s string, additional ...rune) string {
|
||||
}
|
||||
|
||||
return string(rr)
|
||||
*/
|
||||
}
|
||||
|
||||
func mdTextToString(text Txt) (string, error) {
|
||||
@ -128,9 +216,9 @@ func renderMDText(w writer, text Txt) {
|
||||
}
|
||||
|
||||
text.text = singleLine(text.text)
|
||||
text.text = escapeMarkdown(text.text)
|
||||
text.text = escapeMarkdownPrev(text.text)
|
||||
text.link = singleLine(text.link)
|
||||
text.link = escapeMarkdown(text.link)
|
||||
text.link = escapeMarkdownPrev(text.link)
|
||||
if text.bold {
|
||||
w.write("**")
|
||||
}
|
||||
@ -381,7 +469,7 @@ func renderMDChoice(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) {
|
||||
|
||||
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?
|
||||
indentation for syntax may not require non-break spaces
|
||||
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"
|
||||
)
|
||||
|
||||
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"
|
||||
|
||||
var (
|
||||
@ -75,6 +129,7 @@ func escapeRoff(s string, additional ...string) string {
|
||||
}
|
||||
|
||||
return string(e)
|
||||
*/
|
||||
}
|
||||
|
||||
func manPageDate(d time.Time) string {
|
||||
@ -83,7 +138,7 @@ func manPageDate(d time.Time) string {
|
||||
|
||||
func roffString(s string, additionalEscape ...string) string {
|
||||
s = singleLine(s)
|
||||
return escapeRoff(s, additionalEscape...)
|
||||
return escapeRoffPrev(s, additionalEscape...)
|
||||
}
|
||||
|
||||
func renderRoffString(w writer, s string, additionalEscape ...string) {
|
||||
@ -341,7 +396,7 @@ func renderRoffTable(w writer, e Entry) {
|
||||
func renderRoffCode(w writer, e Entry) {
|
||||
w.write(".nf\n")
|
||||
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)
|
||||
}
|
||||
|
||||
@ -414,7 +469,7 @@ func renderRoffChoice(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) {
|
||||
|
||||
65
teletype.go
65
teletype.go
@ -8,7 +8,32 @@ import (
|
||||
"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)
|
||||
for i := range r {
|
||||
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)
|
||||
*/
|
||||
}
|
||||
|
||||
func ttyTextToString(text Txt) (string, error) {
|
||||
@ -62,9 +88,9 @@ func renderTTYText(w writer, text Txt) {
|
||||
}
|
||||
|
||||
text.text = singleLine(text.text)
|
||||
text.text = escapeTeletype(text.text)
|
||||
text.text = escapeTeletypePrev(text.text)
|
||||
text.link = singleLine(text.link)
|
||||
text.link = escapeTeletype(text.link)
|
||||
text.link = escapeTeletypePrev(text.link)
|
||||
if text.link != "" {
|
||||
if text.text != "" {
|
||||
w.write(text.text)
|
||||
@ -282,7 +308,7 @@ func renderTTYTable(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)
|
||||
}
|
||||
|
||||
@ -355,7 +381,7 @@ func renderTTYChoice(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) {
|
||||
@ -395,7 +421,14 @@ func renderTTYSyntax(w writer, e Entry) {
|
||||
}
|
||||
|
||||
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 {
|
||||
if i > 0 {
|
||||
w.write("\n\n")
|
||||
@ -403,23 +436,23 @@ func renderTeletype(out io.Writer, d Document) error {
|
||||
|
||||
switch e.typ {
|
||||
case title:
|
||||
renderTTYTitle(&w, e)
|
||||
renderTTYTitle(w, e)
|
||||
case paragraph:
|
||||
renderTTYParagraph(&w, e)
|
||||
renderTTYParagraph(w, e)
|
||||
case list:
|
||||
renderTTYList(&w, e)
|
||||
renderTTYList(w, e)
|
||||
case numberedList:
|
||||
renderTTYNumberedList(&w, e)
|
||||
renderTTYNumberedList(w, e)
|
||||
case definitions:
|
||||
renderTTYDefinitions(&w, e)
|
||||
renderTTYDefinitions(w, e)
|
||||
case numberedDefinitions:
|
||||
renderTTYNumberedDefinitions(&w, e)
|
||||
renderTTYNumberedDefinitions(w, e)
|
||||
case table:
|
||||
renderTTYTable(&w, e)
|
||||
renderTTYTable(w, e)
|
||||
case code:
|
||||
renderTTYCode(&w, e)
|
||||
renderTTYCode(w, e)
|
||||
case syntax:
|
||||
renderTTYSyntax(&w, e)
|
||||
renderTTYSyntax(w, e)
|
||||
default:
|
||||
return errors.New("invalid entry")
|
||||
}
|
||||
@ -429,5 +462,5 @@ func renderTeletype(out io.Writer, d Document) error {
|
||||
w.write("\n")
|
||||
}
|
||||
|
||||
return w.err
|
||||
return w.error()
|
||||
}
|
||||
|
||||
92
write.go
92
write.go
@ -3,15 +3,99 @@ package textfmt
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type writer interface {
|
||||
write(...any)
|
||||
flush()
|
||||
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 {
|
||||
w io.Writer
|
||||
internal bool
|
||||
@ -52,6 +136,8 @@ func (w *ttyWriter) write(a ...any) {
|
||||
}
|
||||
}
|
||||
|
||||
func (w *ttyWriter) flush() {}
|
||||
|
||||
func (w *ttyWriter) error() error {
|
||||
return w.err
|
||||
}
|
||||
@ -88,6 +174,8 @@ func (w *roffWriter) write(a ...any) {
|
||||
}
|
||||
}
|
||||
|
||||
func (w *roffWriter) flush() {}
|
||||
|
||||
func (w *roffWriter) error() error {
|
||||
return w.err
|
||||
}
|
||||
@ -117,6 +205,8 @@ func (w *mdWriter) write(a ...any) {
|
||||
}
|
||||
}
|
||||
|
||||
func (w *mdWriter) flush() {}
|
||||
|
||||
func (w *mdWriter) error() error {
|
||||
return w.err
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user