1
0

no screaming case

This commit is contained in:
Arpad Ryszka 2025-10-31 20:24:37 +01:00
parent e4dd3ca0df
commit c2b0d69b5b
8 changed files with 543 additions and 87 deletions

View File

@ -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
View 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
View 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) {
}

View File

@ -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) {

View File

@ -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
- ...

View File

@ -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) {

View File

@ -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()
}

View File

@ -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
}