1
0
This commit is contained in:
Arpad Ryszka 2025-10-23 02:55:38 +02:00
parent 59084d8292
commit ace14e534c
8 changed files with 2579 additions and 27 deletions

8
lib.go
View File

@ -141,7 +141,7 @@ func Title(level int, text string, manInfo ...TitleInfo) Entry {
return e return e
} }
func ManualSection(s int) TitleInfo { func ManSection(s int) TitleInfo {
return TitleInfo{section: s} return TitleInfo{section: s}
} }
@ -153,7 +153,7 @@ func ReleaseVersion(v string) TitleInfo {
return TitleInfo{version: v} return TitleInfo{version: v}
} }
func ManualCategory(c string) TitleInfo { func ManCategory(c string) TitleInfo {
return TitleInfo{category: c} return TitleInfo{category: c}
} }
@ -275,8 +275,8 @@ func Teletype(out io.Writer, d Document) error {
// //
// Text is always wrapped, or as controlled by the roff processor, except for tables. The Wrap instrunction has // Text is always wrapped, or as controlled by the roff processor, except for tables. The Wrap instrunction has
// no effect, except for tables. // no effect, except for tables.
func Runoff(io.Writer, Document) error { func Runoff(out io.Writer, d Document) error {
return nil return renderRoff(out, d)
} }
func Markdown(io.Writer, Document) error { func Markdown(io.Writer, Document) error {

5
notes.txt Normal file
View File

@ -0,0 +1,5 @@
indentation for syntax in tty and roff
verify if empty document doesn't print even an empty line. E.g. empty table, empty paragraph. What should those
print?
does the table need the non-breaking space for the filling in roff?
indentation for syntax may not require non-break spaces

236
runoff.go
View File

@ -5,6 +5,8 @@ import (
"errors" "errors"
"time" "time"
"fmt" "fmt"
"bytes"
"strings"
) )
func escapeRoff(s string, additional ...string) string { func escapeRoff(s string, additional ...string) string {
@ -98,6 +100,22 @@ func roffDefinitionNames(d []DefinitionItem) []string {
return n return n
} }
func roffCellTexts(r []TableRow) ([][]string, error) {
var cellTexts [][]string
for _, row := range r {
var c []string
for _, cell := range row.cells {
var b bytes.Buffer
renderRoffText(&roffWriter{w: &b, internal: true}, cell.text)
c = append(c, b.String())
}
cellTexts = append(cellTexts, c)
}
return cellTexts, nil
}
func renderRoffText(w writer, text Txt, additionalEscape ...string) { func renderRoffText(w writer, text Txt, additionalEscape ...string) {
if len(text.cat) > 0 { if len(text.cat) > 0 {
for i, tc := range text.cat { for i, tc := range text.cat {
@ -125,10 +143,8 @@ func renderRoffText(w writer, text Txt, additionalEscape ...string) {
if text.link != "" { if text.link != "" {
if text.text != "" { if text.text != "" {
w.write(text.text)
renderRoffString(w, text.text, additionalEscape...) renderRoffString(w, text.text, additionalEscape...)
w.write(" (") w.write(" (")
w.write(text.link)
renderRoffString(w, text.link, additionalEscape...) renderRoffString(w, text.link, additionalEscape...)
w.write(")") w.write(")")
return return
@ -143,12 +159,12 @@ func renderRoffText(w writer, text Txt, additionalEscape ...string) {
func renderRoffTitle(w writer, e Entry) { func renderRoffTitle(w writer, e Entry) {
if e.titleLevel != 0 || e.man.section == 0 { if e.titleLevel != 0 || e.man.section == 0 {
e.text.bold = true
p := Entry{ p := Entry{
typ: paragraph, typ: paragraph,
text: e.text, text: e.text,
indent: e.indent, indent: e.indent,
indentFirst: e.indentFirst, indentFirst: e.indentFirst,
bold: true,
} }
renderRoffParagraph(w, p) renderRoffParagraph(w, p)
@ -172,17 +188,17 @@ func renderRoffTitle(w writer, e Entry) {
} }
func renderRoffParagraph(w writer, e Entry) { func renderRoffParagraph(w writer, e Entry) {
w.write(".in ", e.indent, "\n.tin ", e.indent + e.indentFirst, "\n") w.write(".in ", e.indent, "\n.ti ", e.indent + e.indentFirst, "\n")
renderRoffText(w, e.text) renderRoffText(w, e.text)
} }
func renderRoffList(w writer, e Entry) { func renderRoffList(w writer, e Entry) {
for i, item := range e.items { for i, item := range e.items {
if i > 0 { if i > 0 {
w.write(".br\n") w.write("\n.br\n")
} }
w.write(".in ", e.indent + 2, "\n.tin ", e.indent + e.indentFirst, "\n") w.write(".in ", e.indent + 2, "\n.ti ", e.indent + e.indentFirst, "\n")
w.write("\\(bu ") w.write("\\(bu ")
renderRoffText(w, item.text) renderRoffText(w, item.text)
} }
@ -192,10 +208,10 @@ func renderRoffNumberedList(w 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(".br\n") w.write("\n.br\n")
} }
w.write(".in ", e.indent + maxDigits + 2, "\n.tin ", e.indent + e.indentFirst, "\n") w.write(".in ", e.indent + maxDigits + 2, "\n.ti ", e.indent + e.indentFirst, "\n")
w.write(padRight(fmt.Sprintf("%d.", i + 1), maxDigits + 2)) w.write(padRight(fmt.Sprintf("%d.", i + 1), maxDigits + 2))
renderRoffText(w, item.text) renderRoffText(w, item.text)
} }
@ -206,10 +222,10 @@ func renderRoffDefinitions(w writer, e Entry) {
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(".br\n") w.write("\n.br\n")
} }
w.write(".in ", e.indent + maxNameLength + 4, "\n.tin ", e.indent + e.indentFirst, "\n") w.write(".in ", e.indent + maxNameLength + 4, "\n.ti ", e.indent + e.indentFirst, "\n")
w.write("\\(bu ") w.write("\\(bu ")
renderRoffText(w, definition.name) renderRoffText(w, definition.name)
w.write(":", timesn("\\~", maxNameLength - len([]rune(names[i])) + 1)) w.write(":", timesn("\\~", maxNameLength - len([]rune(names[i])) + 1))
@ -223,10 +239,10 @@ func renderRoffNumberedDefinitions(w writer, e Entry) {
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(".br\n") w.write("\n.br\n")
} }
w.write(".in ", e.indent + maxDigits + maxNameLength + 4, "\n.tin ", e.indent + e.indentFirst, "\n") w.write(".in ", e.indent + maxDigits + maxNameLength + 4, "\n.ti ", e.indent + e.indentFirst, "\n")
w.write(padRight(fmt.Sprintf("%d.", i + 1), maxDigits + 2)) w.write(padRight(fmt.Sprintf("%d.", i + 1), maxDigits + 2))
renderRoffText(w, definition.name) renderRoffText(w, definition.name)
w.write(":", timesn("\\~", maxNameLength - len([]rune(names[i])) + 1)) w.write(":", timesn("\\~", maxNameLength - len([]rune(names[i])) + 1))
@ -235,12 +251,208 @@ func renderRoffNumberedDefinitions(w writer, e Entry) {
} }
func renderRoffTable(w writer, e Entry) { func renderRoffTable(w writer, e Entry) {
if len(e.rows) == 0 {
return
}
e.rows = normalizeTable(e.rows)
if len(e.rows[0].cells) == 0 {
return
}
w.write(".nf\n")
defer w.write("\n.fi")
cellTexts, err := roffCellTexts(e.rows)
if err != nil {
w.setErr(err)
}
totalSeparatorWidth := (len(cellTexts[0]) - 1) * 3
if e.wrapWidth > 0 {
allocatedWidth := e.wrapWidth - e.indent - totalSeparatorWidth
columnWeights := columnWeights(cellTexts)
targetColumnWidths := targetColumnWidths(allocatedWidth, columnWeights)
for i := range cellTexts {
for j := range cellTexts[i] {
cellTexts[i][j] = wrap(cellTexts[i][j], targetColumnWidths[j], 0, 0)
}
}
}
columnWidths := columnWidths(cellTexts)
totalWidth := totalSeparatorWidth
for i := range columnWidths {
totalWidth += columnWidths[i]
}
hasHeader := e.rows[0].header
for i := range cellTexts {
if i > 0 {
sep := "-"
if hasHeader && i == 1 {
sep = "="
}
w.write("\n")
w.write(timesn(" ", e.indent), timesn(sep, totalWidth))
w.write("\n")
}
lines := make([][]string, len(cellTexts[i]))
for j := range cellTexts[i] {
lines[j] = strings.Split(cellTexts[i][j], "\n")
}
var maxLines int
for j := range lines {
if len(lines[j]) > maxLines {
maxLines = len(lines[j])
}
}
for k := 0; k < maxLines; k++ {
if k > 0 {
w.write("\n")
}
for j := range lines {
if j == 0 {
w.write(timesn(" ", e.indent))
} else {
w.write(" | ")
}
var l string
if k < len(lines[j]) {
l = lines[j][k]
}
w.write(padRight(l, columnWidths[j]))
}
}
}
if hasHeader && len(cellTexts) == 1 {
w.write("\n", timesn("=", totalWidth))
}
} }
func renderRoffCode(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)
writeLines(w, e.text.text, e.indent, e.indent)
}
func renderRoffMultiple(w writer, s SyntaxItem) {
s.topLevel = false
s.multiple = false
renderRoffSyntaxItem(w, s)
w.write("...")
}
func renderRoffRequired(w writer, s SyntaxItem) {
s.delimited = true
s.topLevel = false
s.required = false
w.write("<")
renderRoffSyntaxItem(w, s)
w.write(">")
}
func renderRoffOptional(w writer, s SyntaxItem) {
s.delimited = true
s.topLevel = false
s.optional = false
w.write("[")
renderRoffSyntaxItem(w, s)
w.write("]")
}
func renderRoffSequence(w writer, s SyntaxItem) {
if !s.delimited && !s.topLevel {
w.write("(")
}
for i, item := range s.sequence {
if i > 0 {
w.write(" ")
}
item.delimited = false
renderRoffSyntaxItem(w, item)
}
if !s.delimited && !s.topLevel {
w.write(")")
}
}
func renderRoffChoice(w writer, s SyntaxItem) {
if !s.delimited && !s.topLevel {
w.write("(")
}
for i, item := range s.choice {
if i > 0 {
separator := "|"
if s.topLevel {
separator = "\n"
}
w.write(separator)
}
item.delimited = false
renderRoffSyntaxItem(w, item)
}
if !s.delimited && !s.topLevel {
w.write(")")
}
}
func renderRoffSymbol(w writer, s SyntaxItem) {
w.write(escapeRoff(s.symbol))
}
func renderRoffSyntaxItem(w writer, s SyntaxItem) {
switch {
// foo...
case s.multiple:
renderRoffMultiple(w, s)
// <foo>
case s.required:
renderRoffRequired(w, s)
// [foo]
case s.optional:
renderRoffOptional(w, s)
// foo bar baz or (foo bar baz)
case len(s.sequence) > 0:
renderRoffSequence(w, s)
// foo|bar|baz or (foo|bar|baz)
case len(s.choice) > 0:
renderRoffChoice(w, s)
// foo
default:
renderRoffSymbol(w, s)
}
} }
func renderRoffSyntax(w writer, e Entry) { func renderRoffSyntax(w writer, e Entry) {
s := e.syntax
s.topLevel = true
w.write(".nf\n")
defer w.write("\n.fi")
w.write(timesn("\u00a0", e.indent + e.indentFirst))
renderRoffSyntaxItem(w, s)
} }
func renderRoff(out io.Writer, d Document) error { func renderRoff(out io.Writer, d Document) error {

2300
runoff_test.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -400,6 +400,7 @@ func renderTTYSyntaxItem(w writer, s SyntaxItem) {
func renderTTYSyntax(w writer, e Entry) { func renderTTYSyntax(w writer, e Entry) {
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)
} }

View File

@ -537,8 +537,8 @@ Entry explanations:
textfmt.Item(textfmt.Text("this is another item")), textfmt.Item(textfmt.Text("this is another item")),
textfmt.Item(textfmt.Text("this is a third item")), textfmt.Item(textfmt.Text("this is a third item")),
), ),
0,
4, 4,
0,
), ),
) )
@ -688,8 +688,8 @@ third item
textfmt.Item(textfmt.Text("this is another item")), textfmt.Item(textfmt.Text("this is another item")),
textfmt.Item(textfmt.Text("this is a third item")), textfmt.Item(textfmt.Text("this is a third item")),
), ),
0,
4, 4,
0,
), ),
) )
@ -880,8 +880,8 @@ third item
textfmt.Definition(textfmt.Text("green"), textfmt.Text("looks like grass")), textfmt.Definition(textfmt.Text("green"), textfmt.Text("looks like grass")),
textfmt.Definition(textfmt.Text("blue"), textfmt.Text("looks like sky")), textfmt.Definition(textfmt.Text("blue"), textfmt.Text("looks like sky")),
), ),
0,
4, 4,
0,
), ),
) )
@ -1953,5 +1953,33 @@ and silver birch | their canopies creating | and shadow on the
} }
}) })
t.Run("example indented", func(t *testing.T) {
doc := textfmt.Doc(
textfmt.Indent(
textfmt.Syntax(
textfmt.Symbol("foo"),
textfmt.ZeroOrMore(textfmt.Symbol("options")),
textfmt.Required(textfmt.Symbol("filename")),
textfmt.ZeroOrMore(
textfmt.Choice(
textfmt.Symbol("string"),
textfmt.Symbol("number"),
),
),
),
4,
0,
),
)
var b bytes.Buffer
if err := textfmt.Teletype(&b, doc); err != nil {
t.Fatal(err)
}
if b.String() != " foo [options]... <filename> [string|number]...\n" {
t.Fatal(b.String())
}
})
}) })
} }

View File

@ -3,6 +3,8 @@ package textfmt
import ( import (
"strings" "strings"
"unicode" "unicode"
"fmt"
"bytes"
) )
func timesn(s string, n int) string { func timesn(s string, n int) string {

View File

@ -19,7 +19,7 @@ type ttyWriter struct {
} }
type roffWriter struct { type roffWriter struct {
w *bufio.Writer w io.Writer
internal bool internal bool
err error err error
} }
@ -63,6 +63,9 @@ func (w *roffWriter) write(a ...any) {
var rr []rune var rr []rune
s := fmt.Sprint(ai) s := fmt.Sprint(ai)
r := []rune(s) r := []rune(s)
if w.internal {
rr = r
} else {
for i := range r { for i := range r {
if r[i] == '\u00a0' { if r[i] == '\u00a0' {
rr = append(rr, []rune("\\~")...) rr = append(rr, []rune("\\~")...)
@ -71,6 +74,7 @@ func (w *roffWriter) write(a ...any) {
rr = append(rr, r[i]) rr = append(rr, r[i])
} }
}
if _, err := w.w.Write([]byte(string(rr))); err != nil { if _, err := w.w.Write([]byte(string(rr))); err != nil {
w.err = err w.err = err