wip
This commit is contained in:
parent
57ef6b1267
commit
59084d8292
86
lib.go
86
lib.go
@ -1,6 +1,9 @@
|
||||
package textfmt
|
||||
|
||||
import "io"
|
||||
import (
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
invalid = iota
|
||||
@ -58,9 +61,22 @@ type Entry struct {
|
||||
definitions []DefinitionItem
|
||||
rows []TableRow
|
||||
syntax SyntaxItem
|
||||
indentFirst int
|
||||
indent int
|
||||
wrapWidth int
|
||||
indent int
|
||||
indentFirst int
|
||||
man struct{
|
||||
section int
|
||||
date time.Time
|
||||
version string
|
||||
category string
|
||||
}
|
||||
}
|
||||
|
||||
type TitleInfo struct {
|
||||
section int
|
||||
date time.Time
|
||||
version string
|
||||
category string
|
||||
}
|
||||
|
||||
type Document struct {
|
||||
@ -89,12 +105,56 @@ func Cat(t ...Txt) Txt {
|
||||
return Txt{cat: t}
|
||||
}
|
||||
|
||||
func Title(level int, text string) Entry {
|
||||
return Entry{
|
||||
func Title(level int, text string, manInfo ...TitleInfo) Entry {
|
||||
if level != 0 {
|
||||
return Entry{
|
||||
typ: title,
|
||||
titleLevel: level,
|
||||
text: Text(text),
|
||||
}
|
||||
}
|
||||
|
||||
e := Entry{
|
||||
typ: title,
|
||||
titleLevel: level,
|
||||
text: Text(text),
|
||||
}
|
||||
|
||||
for _, m := range manInfo {
|
||||
if m.section != 0 {
|
||||
e.man.section = m.section
|
||||
}
|
||||
|
||||
if !m.date.IsZero() {
|
||||
e.man.date = m.date
|
||||
}
|
||||
|
||||
if m.version != "" {
|
||||
e.man.version = m.version
|
||||
}
|
||||
|
||||
if m.category != "" {
|
||||
e.man.category = m.category
|
||||
}
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
func ManualSection(s int) TitleInfo {
|
||||
return TitleInfo{section: s}
|
||||
}
|
||||
|
||||
func ReleaseDate(d time.Time) TitleInfo {
|
||||
return TitleInfo{date: d}
|
||||
}
|
||||
|
||||
func ReleaseVersion(v string) TitleInfo {
|
||||
return TitleInfo{version: v}
|
||||
}
|
||||
|
||||
func ManualCategory(c string) TitleInfo {
|
||||
return TitleInfo{category: c}
|
||||
}
|
||||
|
||||
func Paragraph(t Txt) Entry {
|
||||
@ -191,13 +251,14 @@ func Syntax(items ...SyntaxItem) Entry {
|
||||
return Entry{typ: syntax, syntax: Sequence(items...)}
|
||||
}
|
||||
|
||||
func Indent(e Entry, first, rest int) Entry {
|
||||
e.indentFirst, e.indent = first, rest
|
||||
func Wrap(e Entry, width int) Entry {
|
||||
e.wrapWidth = width
|
||||
return e
|
||||
}
|
||||
|
||||
func Wrap(e Entry, width int) Entry {
|
||||
e.wrapWidth = width
|
||||
// indentFirst is relative to indent
|
||||
func Indent(e Entry, indent, indentFirst int) Entry {
|
||||
e.indent, e.indentFirst = indent, indentFirst
|
||||
return e
|
||||
}
|
||||
|
||||
@ -209,7 +270,12 @@ func Teletype(out io.Writer, d Document) error {
|
||||
return renderTeletype(out, d)
|
||||
}
|
||||
|
||||
func Roff(io.Writer, Document) error {
|
||||
// Runoff is an attempt to render roff format. It is primarily targeting man pages. While it may be possible to
|
||||
// use for other purposes, the man macro will likely be required to render the output.
|
||||
//
|
||||
// Text is always wrapped, or as controlled by the roff processor, except for tables. The Wrap instrunction has
|
||||
// no effect, except for tables.
|
||||
func Runoff(io.Writer, Document) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
282
runoff.go
Normal file
282
runoff.go
Normal file
@ -0,0 +1,282 @@
|
||||
package textfmt
|
||||
|
||||
import (
|
||||
"io"
|
||||
"errors"
|
||||
"time"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func escapeRoff(s string, additional ...string) 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)
|
||||
for i := 0; i > len(additional); i += 2 {
|
||||
r := []rune(additional[i])
|
||||
if len(r) != 1 {
|
||||
panic(errors.New(invalidAdditional))
|
||||
}
|
||||
|
||||
am[r[0]] = []rune(additional[i + 1])
|
||||
}
|
||||
|
||||
for _, r := range []rune(s) {
|
||||
switch r {
|
||||
case '\\':
|
||||
e = append(e, '\\', '\\')
|
||||
continue
|
||||
case '.':
|
||||
if lineStarted {
|
||||
e = append(e, '.')
|
||||
continue
|
||||
}
|
||||
|
||||
e = append(e, []rune("\\&.")...)
|
||||
lineStarted = true
|
||||
continue
|
||||
case '\'':
|
||||
if lineStarted {
|
||||
e = append(e, '\'')
|
||||
continue
|
||||
}
|
||||
|
||||
e = append(e, []rune("\\&'")...)
|
||||
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 {
|
||||
e = append(e, a...)
|
||||
lineStarted = true
|
||||
continue
|
||||
}
|
||||
|
||||
e = append(e, r)
|
||||
lineStarted = true
|
||||
}
|
||||
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func manPageDate(d time.Time) string {
|
||||
return fmt.Sprintf("%v %d", d.Month(), d.Year())
|
||||
}
|
||||
|
||||
func roffString(s string, additionalEscape ...string) string {
|
||||
s = singleLine(s)
|
||||
return escapeRoff(s, additionalEscape...)
|
||||
}
|
||||
|
||||
func renderRoffString(w writer, s string, additionalEscape ...string) {
|
||||
s = roffString(s, additionalEscape...)
|
||||
w.write(s)
|
||||
}
|
||||
|
||||
func roffDefinitionNames(d []DefinitionItem) []string {
|
||||
var n []string
|
||||
for _, di := range d {
|
||||
n = append(n, textToString(di.name))
|
||||
}
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
func renderRoffText(w writer, text Txt, additionalEscape ...string) {
|
||||
if len(text.cat) > 0 {
|
||||
for i, tc := range text.cat {
|
||||
if i > 0 {
|
||||
w.write(" ")
|
||||
}
|
||||
|
||||
renderRoffText(w, tc)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if text.bold {
|
||||
w.write("\\fB")
|
||||
}
|
||||
|
||||
if text.italic {
|
||||
w.write("\\fI")
|
||||
}
|
||||
|
||||
if text.bold || text.italic {
|
||||
defer w.write("\\fR")
|
||||
}
|
||||
|
||||
if text.link != "" {
|
||||
if text.text != "" {
|
||||
w.write(text.text)
|
||||
renderRoffString(w, text.text, additionalEscape...)
|
||||
w.write(" (")
|
||||
w.write(text.link)
|
||||
renderRoffString(w, text.link, additionalEscape...)
|
||||
w.write(")")
|
||||
return
|
||||
}
|
||||
|
||||
renderRoffString(w, text.link, additionalEscape...)
|
||||
return
|
||||
}
|
||||
|
||||
renderRoffString(w, text.text, additionalEscape...)
|
||||
}
|
||||
|
||||
func renderRoffTitle(w writer, e Entry) {
|
||||
if e.titleLevel != 0 || e.man.section == 0 {
|
||||
p := Entry{
|
||||
typ: paragraph,
|
||||
text: e.text,
|
||||
indent: e.indent,
|
||||
indentFirst: e.indentFirst,
|
||||
bold: true,
|
||||
}
|
||||
|
||||
renderRoffParagraph(w, p)
|
||||
return
|
||||
}
|
||||
|
||||
w.write(".TH \"")
|
||||
renderRoffText(w, e.text, "\"", "\\(dq")
|
||||
w.write("\" ")
|
||||
renderRoffString(w, fmt.Sprint(e.man.section), "\"", "\\(dq")
|
||||
w.write(" \"")
|
||||
if !e.man.date.IsZero() {
|
||||
w.write(manPageDate(e.man.date))
|
||||
}
|
||||
|
||||
w.write("\" \"")
|
||||
renderRoffString(w, e.man.version, "\"", "\\(dq")
|
||||
w.write("\" \"")
|
||||
renderRoffString(w, e.man.category, "\"", "\\(dq")
|
||||
w.write("\"")
|
||||
}
|
||||
|
||||
func renderRoffParagraph(w writer, e Entry) {
|
||||
w.write(".in ", e.indent, "\n.tin ", e.indent + e.indentFirst, "\n")
|
||||
renderRoffText(w, e.text)
|
||||
}
|
||||
|
||||
func renderRoffList(w writer, e Entry) {
|
||||
for i, item := range e.items {
|
||||
if i > 0 {
|
||||
w.write(".br\n")
|
||||
}
|
||||
|
||||
w.write(".in ", e.indent + 2, "\n.tin ", e.indent + e.indentFirst, "\n")
|
||||
w.write("\\(bu ")
|
||||
renderRoffText(w, item.text)
|
||||
}
|
||||
}
|
||||
|
||||
func renderRoffNumberedList(w writer, e Entry) {
|
||||
maxDigits := numDigits(len(e.items))
|
||||
for i, item := range e.items {
|
||||
if i > 0 {
|
||||
w.write(".br\n")
|
||||
}
|
||||
|
||||
w.write(".in ", e.indent + maxDigits + 2, "\n.tin ", e.indent + e.indentFirst, "\n")
|
||||
w.write(padRight(fmt.Sprintf("%d.", i + 1), maxDigits + 2))
|
||||
renderRoffText(w, item.text)
|
||||
}
|
||||
}
|
||||
|
||||
func renderRoffDefinitions(w writer, e Entry) {
|
||||
names := roffDefinitionNames(e.definitions)
|
||||
maxNameLength := maxLength(names)
|
||||
for i, definition := range e.definitions {
|
||||
if i > 0 {
|
||||
w.write(".br\n")
|
||||
}
|
||||
|
||||
w.write(".in ", e.indent + maxNameLength + 4, "\n.tin ", e.indent + e.indentFirst, "\n")
|
||||
w.write("\\(bu ")
|
||||
renderRoffText(w, definition.name)
|
||||
w.write(":", timesn("\\~", maxNameLength - len([]rune(names[i])) + 1))
|
||||
renderRoffText(w, definition.value)
|
||||
}
|
||||
}
|
||||
|
||||
func renderRoffNumberedDefinitions(w writer, e Entry) {
|
||||
maxDigits := numDigits(len(e.definitions))
|
||||
names := roffDefinitionNames(e.definitions)
|
||||
maxNameLength := maxLength(names)
|
||||
for i, definition := range e.definitions {
|
||||
if i > 0 {
|
||||
w.write(".br\n")
|
||||
}
|
||||
|
||||
w.write(".in ", e.indent + maxDigits + maxNameLength + 4, "\n.tin ", e.indent + e.indentFirst, "\n")
|
||||
w.write(padRight(fmt.Sprintf("%d.", i + 1), maxDigits + 2))
|
||||
renderRoffText(w, definition.name)
|
||||
w.write(":", timesn("\\~", maxNameLength - len([]rune(names[i])) + 1))
|
||||
renderRoffText(w, definition.value)
|
||||
}
|
||||
}
|
||||
|
||||
func renderRoffTable(w writer, e Entry) {
|
||||
}
|
||||
|
||||
func renderRoffCode(w writer, e Entry) {
|
||||
}
|
||||
|
||||
func renderRoffSyntax(w writer, e Entry) {
|
||||
}
|
||||
|
||||
func renderRoff(out io.Writer, d Document) error {
|
||||
w := roffWriter{w: out}
|
||||
for i, e := range d.entries {
|
||||
if i > 0 {
|
||||
w.write("\n.br\n.sp 1v\n")
|
||||
}
|
||||
|
||||
switch e.typ {
|
||||
case invalid:
|
||||
return errors.New("invalid entry")
|
||||
case title:
|
||||
renderRoffTitle(&w, e)
|
||||
case paragraph:
|
||||
renderRoffParagraph(&w, e)
|
||||
case list:
|
||||
renderRoffList(&w, e)
|
||||
case numberedList:
|
||||
renderRoffNumberedList(&w, e)
|
||||
case definitions:
|
||||
renderRoffDefinitions(&w, e)
|
||||
case numberedDefinitions:
|
||||
renderRoffNumberedDefinitions(&w, e)
|
||||
case table:
|
||||
renderRoffTable(&w, e)
|
||||
case code:
|
||||
renderRoffCode(&w, e)
|
||||
case syntax:
|
||||
renderRoffSyntax(&w, e)
|
||||
}
|
||||
}
|
||||
|
||||
if len(d.entries) > 0 {
|
||||
w.write("\n")
|
||||
}
|
||||
|
||||
return w.err
|
||||
}
|
||||
180
teletype.go
180
teletype.go
@ -23,29 +23,23 @@ func escapeTeletype(s string) string {
|
||||
return string(r)
|
||||
}
|
||||
|
||||
func definitionNamesValues(d []DefinitionItem) ([]string, []string, error) {
|
||||
var n, v []string
|
||||
func ttyDefinitionNames(d []DefinitionItem) ([]string, error) {
|
||||
var n []string
|
||||
for _, di := range d {
|
||||
name, err := ttyTextToString(di.name)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
value, err := ttyTextToString(di.value)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
n = append(n, name)
|
||||
v = append(v, value)
|
||||
}
|
||||
|
||||
return n, v, nil
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func ttyTextToString(text Txt) (string, error) {
|
||||
var b bytes.Buffer
|
||||
w := writer{w: &b}
|
||||
w := ttyWriter{w: &b, internal: true}
|
||||
renderTTYText(&w, text)
|
||||
if w.err != nil {
|
||||
return "", w.err
|
||||
@ -54,7 +48,7 @@ func ttyTextToString(text Txt) (string, error) {
|
||||
return b.String(), nil
|
||||
}
|
||||
|
||||
func renderTTYText(w *writer, text Txt) {
|
||||
func renderTTYText(w writer, text Txt) {
|
||||
if len(text.cat) > 0 {
|
||||
for i, tc := range text.cat {
|
||||
if i > 0 {
|
||||
@ -85,135 +79,111 @@ func renderTTYText(w *writer, text Txt) {
|
||||
w.write(text.text)
|
||||
}
|
||||
|
||||
func renderTTYTitle(w *writer, e Entry) {
|
||||
w.write(timesn(" ", e.indentFirst))
|
||||
func itemToParagraph(list Entry, itemText Txt, prefix string) Entry {
|
||||
p := Entry{
|
||||
typ: paragraph,
|
||||
wrapWidth: list.wrapWidth,
|
||||
indent: list.indent + len([]rune(prefix)) + 1,
|
||||
indentFirst: list.indentFirst - len([]rune(prefix)) - 1,
|
||||
}
|
||||
|
||||
p.text.cat = []Txt{Text(prefix), itemText}
|
||||
return p
|
||||
}
|
||||
|
||||
func renderTTYTitle(w writer, e Entry) {
|
||||
w.write(timesn(" ", e.indent))
|
||||
renderTTYText(w, e.text)
|
||||
}
|
||||
|
||||
func renderTTYParagraph(w *writer, e Entry) {
|
||||
var txt string
|
||||
txt, w.err = ttyTextToString(e.text)
|
||||
func renderTTYParagraph(w writer, e Entry) {
|
||||
txt, err := ttyTextToString(e.text)
|
||||
if err != nil {
|
||||
w.setErr(err)
|
||||
}
|
||||
|
||||
indentFirst := e.indent + e.indentFirst
|
||||
if e.wrapWidth > 0 {
|
||||
txt = wrap(txt, e.wrapWidth, e.indentFirst, e.indent)
|
||||
txt = wrap(txt, e.wrapWidth, indentFirst, e.indent)
|
||||
}
|
||||
|
||||
writeLines(w, txt, e.indentFirst, e.indent)
|
||||
writeLines(w, txt, indentFirst, e.indent)
|
||||
}
|
||||
|
||||
func renderTTYList(w *writer, e Entry) {
|
||||
const bullet = "- "
|
||||
indent := e.indent + len(bullet)
|
||||
func renderTTYList(w writer, e Entry) {
|
||||
for i, item := range e.items {
|
||||
if i > 0 {
|
||||
w.write("\n")
|
||||
}
|
||||
|
||||
var txt string
|
||||
txt, w.err = ttyTextToString(item.text)
|
||||
if e.wrapWidth > 0 {
|
||||
txt = wrap(txt, e.wrapWidth-len(bullet), e.indentFirst, indent)
|
||||
}
|
||||
|
||||
w.write(timesn(" ", e.indentFirst))
|
||||
w.write(bullet)
|
||||
writeLines(w, txt, 0, indent)
|
||||
p := itemToParagraph(e, item.text, "-")
|
||||
renderTTYParagraph(w, p)
|
||||
}
|
||||
}
|
||||
|
||||
func renderTTYNumberedList(w *writer, e Entry) {
|
||||
func renderTTYNumberedList(w writer, e Entry) {
|
||||
maxDigits := numDigits(len(e.items))
|
||||
indent := e.indent + maxDigits + 2
|
||||
for i, item := range e.items {
|
||||
if i > 0 {
|
||||
w.write("\n")
|
||||
}
|
||||
|
||||
var txt string
|
||||
txt, w.err = ttyTextToString(item.text)
|
||||
if e.wrapWidth > 0 {
|
||||
txt = wrap(txt, e.wrapWidth-maxDigits-2, e.indentFirst, indent)
|
||||
}
|
||||
|
||||
w.write(timesn(" ", e.indentFirst))
|
||||
w.write(padRight(fmt.Sprintf("%d.", i+1), maxDigits+2))
|
||||
writeLines(w, txt, 0, indent)
|
||||
p := itemToParagraph(e, item.text, padRight(fmt.Sprintf("%d.", i + 1), maxDigits + 1))
|
||||
renderTTYParagraph(w, p)
|
||||
}
|
||||
}
|
||||
|
||||
func renderTTYDefinitions(w *writer, e Entry) {
|
||||
const (
|
||||
bullet = "- "
|
||||
sep = ": "
|
||||
)
|
||||
|
||||
names, values, err := definitionNamesValues(e.definitions)
|
||||
func renderTTYDefinitions(w writer, e Entry) {
|
||||
names, err := ttyDefinitionNames(e.definitions)
|
||||
if err != nil {
|
||||
w.err = err
|
||||
w.setErr(err)
|
||||
return
|
||||
}
|
||||
|
||||
maxNameLength := maxLength(names)
|
||||
nameColWidth := maxNameLength + e.indentFirst + len(bullet) + len(sep)
|
||||
valueWidth := e.wrapWidth
|
||||
if valueWidth > 0 {
|
||||
valueWidth -= nameColWidth
|
||||
}
|
||||
|
||||
for i := range names {
|
||||
for i, definition := range e.definitions {
|
||||
if i > 0 {
|
||||
w.write("\n")
|
||||
}
|
||||
|
||||
w.write(timesn(" ", e.indentFirst), bullet, names[i], sep)
|
||||
if valueWidth > 0 {
|
||||
values[i] = wrap(values[i], valueWidth, 0, e.indent)
|
||||
}
|
||||
|
||||
writeLines(
|
||||
w,
|
||||
values[i],
|
||||
maxNameLength-len([]rune(names[i])),
|
||||
nameColWidth+e.indent,
|
||||
p := itemToParagraph(
|
||||
e,
|
||||
definition.value,
|
||||
padRight(fmt.Sprintf("- %s:", names[i]), maxNameLength + 3),
|
||||
)
|
||||
|
||||
renderTTYParagraph(w, p)
|
||||
}
|
||||
}
|
||||
|
||||
func renderTTYNumberedDefinitions(w *writer, e Entry) {
|
||||
const (
|
||||
dot = ". "
|
||||
sep = ": "
|
||||
)
|
||||
|
||||
names, values, err := definitionNamesValues(e.definitions)
|
||||
func renderTTYNumberedDefinitions(w writer, e Entry) {
|
||||
names, err := ttyDefinitionNames(e.definitions)
|
||||
if err != nil {
|
||||
w.err = err
|
||||
w.setErr(err)
|
||||
return
|
||||
}
|
||||
|
||||
maxNameLength := maxLength(names)
|
||||
maxDigits := numDigits(len(e.definitions))
|
||||
maxNameLength := maxLength(names)
|
||||
nameColWidth := maxNameLength + e.indentFirst + maxDigits + len(dot) + len(sep)
|
||||
valueWidth := e.wrapWidth
|
||||
if valueWidth > 0 {
|
||||
valueWidth -= nameColWidth
|
||||
}
|
||||
|
||||
for i := range names {
|
||||
for i, definition := range e.definitions {
|
||||
if i > 0 {
|
||||
w.write("\n")
|
||||
}
|
||||
|
||||
w.write(timesn(" ", e.indentFirst), padRight(fmt.Sprintf("%d.", i+1), maxDigits+2), names[i], sep)
|
||||
if valueWidth > 0 {
|
||||
values[i] = wrap(values[i], valueWidth, 0, e.indent)
|
||||
}
|
||||
|
||||
writeLines(
|
||||
w,
|
||||
values[i],
|
||||
maxNameLength-len([]rune(names[i])),
|
||||
nameColWidth+e.indent,
|
||||
p := itemToParagraph(
|
||||
e,
|
||||
definition.value,
|
||||
padRight(
|
||||
fmt.Sprintf(
|
||||
"%s %s:",
|
||||
padRight(fmt.Sprintf("%d.", i + 1), maxDigits + 1),
|
||||
names[i],
|
||||
),
|
||||
maxNameLength + maxDigits + 3,
|
||||
),
|
||||
)
|
||||
|
||||
renderTTYParagraph(w, p)
|
||||
}
|
||||
}
|
||||
|
||||
@ -236,7 +206,7 @@ func ttyCellTexts(rows []TableRow) ([][]string, error) {
|
||||
return cellTexts, nil
|
||||
}
|
||||
|
||||
func renderTTYTable(w *writer, e Entry) {
|
||||
func renderTTYTable(w writer, e Entry) {
|
||||
if len(e.rows) == 0 {
|
||||
return
|
||||
}
|
||||
@ -248,7 +218,7 @@ func renderTTYTable(w *writer, e Entry) {
|
||||
|
||||
cellTexts, err := ttyCellTexts(e.rows)
|
||||
if err != nil {
|
||||
w.err = err
|
||||
w.setErr(err)
|
||||
}
|
||||
|
||||
totalSeparatorWidth := (len(cellTexts[0]) - 1) * 3
|
||||
@ -321,19 +291,19 @@ 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)
|
||||
writeLines(w, e.text.text, e.indent, e.indent)
|
||||
}
|
||||
|
||||
func renderTTYMultiple(w *writer, s SyntaxItem) {
|
||||
func renderTTYMultiple(w writer, s SyntaxItem) {
|
||||
s.topLevel = false
|
||||
s.multiple = false
|
||||
renderTTYSyntaxItem(w, s)
|
||||
w.write("...")
|
||||
}
|
||||
|
||||
func renderTTYRequired(w *writer, s SyntaxItem) {
|
||||
func renderTTYRequired(w writer, s SyntaxItem) {
|
||||
s.delimited = true
|
||||
s.topLevel = false
|
||||
s.required = false
|
||||
@ -342,7 +312,7 @@ func renderTTYRequired(w *writer, s SyntaxItem) {
|
||||
w.write(">")
|
||||
}
|
||||
|
||||
func renderTTYOptional(w *writer, s SyntaxItem) {
|
||||
func renderTTYOptional(w writer, s SyntaxItem) {
|
||||
s.delimited = true
|
||||
s.topLevel = false
|
||||
s.optional = false
|
||||
@ -351,7 +321,7 @@ func renderTTYOptional(w *writer, s SyntaxItem) {
|
||||
w.write("]")
|
||||
}
|
||||
|
||||
func renderTTYSequence(w *writer, s SyntaxItem) {
|
||||
func renderTTYSequence(w writer, s SyntaxItem) {
|
||||
if !s.delimited && !s.topLevel {
|
||||
w.write("(")
|
||||
}
|
||||
@ -370,7 +340,7 @@ func renderTTYSequence(w *writer, s SyntaxItem) {
|
||||
}
|
||||
}
|
||||
|
||||
func renderTTYChoice(w *writer, s SyntaxItem) {
|
||||
func renderTTYChoice(w writer, s SyntaxItem) {
|
||||
if !s.delimited && !s.topLevel {
|
||||
w.write("(")
|
||||
}
|
||||
@ -394,11 +364,11 @@ func renderTTYChoice(w *writer, s SyntaxItem) {
|
||||
}
|
||||
}
|
||||
|
||||
func renderTTYSymbol(w *writer, s SyntaxItem) {
|
||||
func renderTTYSymbol(w writer, s SyntaxItem) {
|
||||
w.write(escapeTeletype(s.symbol))
|
||||
}
|
||||
|
||||
func renderTTYSyntaxItem(w *writer, s SyntaxItem) {
|
||||
func renderTTYSyntaxItem(w writer, s SyntaxItem) {
|
||||
switch {
|
||||
|
||||
// foo...
|
||||
@ -427,14 +397,14 @@ func renderTTYSyntaxItem(w *writer, s SyntaxItem) {
|
||||
}
|
||||
}
|
||||
|
||||
func renderTTYSyntax(w *writer, e Entry) {
|
||||
func renderTTYSyntax(w writer, e Entry) {
|
||||
s := e.syntax
|
||||
s.topLevel = true
|
||||
renderTTYSyntaxItem(w, s)
|
||||
}
|
||||
|
||||
func renderTeletype(out io.Writer, d Document) error {
|
||||
w := writer{w: out}
|
||||
w := ttyWriter{w: out}
|
||||
for i, e := range d.entries {
|
||||
if i > 0 {
|
||||
w.write("\n\n")
|
||||
|
||||
158
teletype_test.go
158
teletype_test.go
@ -32,8 +32,8 @@ func TestTeletype(t *testing.T) {
|
||||
textfmt.Wrap(
|
||||
textfmt.Indent(
|
||||
textfmt.Paragraph(textfmt.Text("Below you can find some test text, with various text items.")),
|
||||
8,
|
||||
0,
|
||||
8,
|
||||
),
|
||||
30,
|
||||
),
|
||||
@ -47,8 +47,8 @@ func TestTeletype(t *testing.T) {
|
||||
textfmt.ZeroOrMore(textfmt.Symbol("Entry")),
|
||||
textfmt.Symbol(")"),
|
||||
),
|
||||
8,
|
||||
0,
|
||||
8,
|
||||
),
|
||||
|
||||
textfmt.Title(1, "Entries:"),
|
||||
@ -234,7 +234,7 @@ Entry explanations:
|
||||
doc := textfmt.Doc(
|
||||
textfmt.Indent(
|
||||
textfmt.Wrap(textfmt.Paragraph(textfmt.Text("Some sample text...\n on multiple lines.")), 15),
|
||||
4,
|
||||
2,
|
||||
2,
|
||||
),
|
||||
)
|
||||
@ -257,7 +257,7 @@ Entry explanations:
|
||||
textfmt.Indent(
|
||||
textfmt.Wrap(textfmt.Paragraph(textfmt.Text("Some sample text...\non multiple lines.")), 15),
|
||||
2,
|
||||
2,
|
||||
0,
|
||||
),
|
||||
)
|
||||
|
||||
@ -275,7 +275,7 @@ Entry explanations:
|
||||
doc := textfmt.Doc(
|
||||
textfmt.Indent(
|
||||
textfmt.Wrap(textfmt.Paragraph(textfmt.Text("Some sample text...\non multiple lines.")), 15),
|
||||
4,
|
||||
2,
|
||||
2,
|
||||
),
|
||||
)
|
||||
@ -294,8 +294,8 @@ Entry explanations:
|
||||
doc := textfmt.Doc(
|
||||
textfmt.Indent(
|
||||
textfmt.Wrap(textfmt.Paragraph(textfmt.Text("Some sample text...\non multiple lines.")), 15),
|
||||
0,
|
||||
2,
|
||||
-2,
|
||||
),
|
||||
)
|
||||
|
||||
@ -365,6 +365,18 @@ Entry explanations:
|
||||
t.Fatal(b.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("newline in link", func(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
doc := textfmt.Doc(textfmt.Paragraph(textfmt.Link("a\nlink", "https://sqrndfst.org\n/foo")))
|
||||
if err := textfmt.Teletype(&b, doc); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if b.String() != "a link (https://sqrndfst.org\n/foo)\n" {
|
||||
t.Fatal(b.String())
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("title", func(t *testing.T) {
|
||||
@ -410,7 +422,7 @@ Entry explanations:
|
||||
t.Run("indent without wrapping", func(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
doc := textfmt.Doc(
|
||||
textfmt.Indent(textfmt.Paragraph(textfmt.Text("This is a paragraph.")), 4, 0),
|
||||
textfmt.Indent(textfmt.Paragraph(textfmt.Text("This is a paragraph.")), 0, 4),
|
||||
)
|
||||
|
||||
if err := textfmt.Teletype(&b, doc); err != nil {
|
||||
@ -427,8 +439,8 @@ Entry explanations:
|
||||
doc := textfmt.Doc(
|
||||
textfmt.Indent(
|
||||
textfmt.Wrap(textfmt.Paragraph(textfmt.Text("This is a paragraph.")), 12),
|
||||
4,
|
||||
0,
|
||||
4,
|
||||
),
|
||||
)
|
||||
|
||||
@ -446,8 +458,8 @@ Entry explanations:
|
||||
doc := textfmt.Doc(
|
||||
textfmt.Indent(
|
||||
textfmt.Wrap(textfmt.Paragraph(textfmt.Text("This is a paragraph.")), 12),
|
||||
0,
|
||||
4,
|
||||
-4,
|
||||
),
|
||||
)
|
||||
|
||||
@ -509,8 +521,7 @@ Entry explanations:
|
||||
another
|
||||
item
|
||||
- this is a
|
||||
third
|
||||
item
|
||||
third item
|
||||
`
|
||||
|
||||
if b.String() != expect {
|
||||
@ -526,8 +537,8 @@ Entry explanations:
|
||||
textfmt.Item(textfmt.Text("this is another item")),
|
||||
textfmt.Item(textfmt.Text("this is a third item")),
|
||||
),
|
||||
4,
|
||||
0,
|
||||
4,
|
||||
),
|
||||
)
|
||||
|
||||
@ -557,8 +568,8 @@ Entry explanations:
|
||||
),
|
||||
18,
|
||||
),
|
||||
4,
|
||||
-2,
|
||||
6,
|
||||
),
|
||||
)
|
||||
|
||||
@ -591,8 +602,8 @@ third item
|
||||
),
|
||||
18,
|
||||
),
|
||||
0,
|
||||
2,
|
||||
-2,
|
||||
),
|
||||
)
|
||||
|
||||
@ -659,11 +670,9 @@ third item
|
||||
const expect = `1. this is an
|
||||
item
|
||||
2. this is
|
||||
another
|
||||
item
|
||||
another item
|
||||
3. this is a
|
||||
third
|
||||
item
|
||||
third item
|
||||
`
|
||||
|
||||
if b.String() != expect {
|
||||
@ -679,8 +688,8 @@ third item
|
||||
textfmt.Item(textfmt.Text("this is another item")),
|
||||
textfmt.Item(textfmt.Text("this is a third item")),
|
||||
),
|
||||
4,
|
||||
0,
|
||||
4,
|
||||
),
|
||||
)
|
||||
|
||||
@ -710,8 +719,8 @@ third item
|
||||
),
|
||||
21,
|
||||
),
|
||||
4,
|
||||
-3,
|
||||
7,
|
||||
),
|
||||
)
|
||||
|
||||
@ -744,8 +753,8 @@ third item
|
||||
),
|
||||
21,
|
||||
),
|
||||
0,
|
||||
3,
|
||||
-3,
|
||||
),
|
||||
)
|
||||
|
||||
@ -871,8 +880,8 @@ third item
|
||||
textfmt.Definition(textfmt.Text("green"), textfmt.Text("looks like grass")),
|
||||
textfmt.Definition(textfmt.Text("blue"), textfmt.Text("looks like sky")),
|
||||
),
|
||||
4,
|
||||
0,
|
||||
4,
|
||||
),
|
||||
)
|
||||
|
||||
@ -902,8 +911,8 @@ third item
|
||||
),
|
||||
21,
|
||||
),
|
||||
3,
|
||||
-9,
|
||||
-6,
|
||||
9,
|
||||
),
|
||||
)
|
||||
|
||||
@ -912,7 +921,8 @@ third item
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
const expect = ` - red: looks
|
||||
const expect = `
|
||||
- red: looks
|
||||
like strawberry
|
||||
- green: looks
|
||||
like grass
|
||||
@ -920,8 +930,8 @@ third item
|
||||
like sky
|
||||
`
|
||||
|
||||
if b.String() != expect {
|
||||
t.Fatal(b.String())
|
||||
if "\n" + b.String() != expect {
|
||||
t.Fatal("\n" + b.String())
|
||||
}
|
||||
})
|
||||
|
||||
@ -936,8 +946,8 @@ third item
|
||||
),
|
||||
21,
|
||||
),
|
||||
0,
|
||||
4,
|
||||
-4,
|
||||
),
|
||||
)
|
||||
|
||||
@ -946,7 +956,8 @@ third item
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
const expect = `- red: looks like
|
||||
const expect = `
|
||||
- red: looks like
|
||||
strawberry
|
||||
- green: looks like
|
||||
grass
|
||||
@ -954,8 +965,8 @@ third item
|
||||
sky
|
||||
`
|
||||
|
||||
if b.String() != expect {
|
||||
t.Fatal(b.String())
|
||||
if "\n" + b.String() != expect {
|
||||
t.Fatal("\n" + b.String())
|
||||
}
|
||||
})
|
||||
})
|
||||
@ -1022,8 +1033,8 @@ third item
|
||||
textfmt.Definition(textfmt.Text("green"), textfmt.Text("looks like grass")),
|
||||
textfmt.Definition(textfmt.Text("blue"), textfmt.Text("looks like sky")),
|
||||
),
|
||||
4,
|
||||
0,
|
||||
4,
|
||||
),
|
||||
)
|
||||
|
||||
@ -1053,8 +1064,8 @@ third item
|
||||
),
|
||||
21,
|
||||
),
|
||||
3,
|
||||
-9,
|
||||
-6,
|
||||
9,
|
||||
),
|
||||
)
|
||||
|
||||
@ -1063,7 +1074,8 @@ third item
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
const expect = ` 1. red: looks
|
||||
const expect = `
|
||||
1. red: looks
|
||||
like strawberry
|
||||
2. green: looks
|
||||
like grass
|
||||
@ -1071,8 +1083,8 @@ third item
|
||||
like sky
|
||||
`
|
||||
|
||||
if b.String() != expect {
|
||||
t.Fatal(b.String())
|
||||
if "\n" + b.String() != expect {
|
||||
t.Fatal("\n" + b.String())
|
||||
}
|
||||
})
|
||||
|
||||
@ -1087,8 +1099,8 @@ third item
|
||||
),
|
||||
21,
|
||||
),
|
||||
0,
|
||||
4,
|
||||
-4,
|
||||
),
|
||||
)
|
||||
|
||||
@ -1097,7 +1109,8 @@ third item
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
const expect = `1. red: looks like
|
||||
const expect = `
|
||||
1. red: looks like
|
||||
strawberry
|
||||
2. green: looks like
|
||||
grass
|
||||
@ -1105,8 +1118,8 @@ third item
|
||||
sky
|
||||
`
|
||||
|
||||
if b.String() != expect {
|
||||
t.Fatal(b.String())
|
||||
if "\n" + b.String() != expect {
|
||||
t.Fatal("\n" + b.String())
|
||||
}
|
||||
})
|
||||
|
||||
@ -1556,8 +1569,8 @@ Walking through the mixed forests of Brandenburg in early autumn, one notices th
|
||||
textfmt.Cell(textfmt.Text("and shadow on the forest floor")),
|
||||
),
|
||||
),
|
||||
0,
|
||||
4,
|
||||
0,
|
||||
),
|
||||
)
|
||||
|
||||
@ -1652,8 +1665,8 @@ and silver birch | their canopies creating | and shadow on the
|
||||
),
|
||||
72,
|
||||
),
|
||||
0,
|
||||
4,
|
||||
0,
|
||||
),
|
||||
)
|
||||
|
||||
@ -1683,20 +1696,63 @@ and silver birch | their canopies creating | and shadow on the
|
||||
})
|
||||
|
||||
t.Run("code", func(t *testing.T) {
|
||||
const code = `func() textfmt.Document {
|
||||
t.Run("unindented", func(t *testing.T) {
|
||||
const code = `func() textfmt.Document {
|
||||
return textfmt.Document(
|
||||
textfmt.Paragraph(textfmt.Text("Hello, world!")),
|
||||
)
|
||||
}`
|
||||
|
||||
var b bytes.Buffer
|
||||
if err := textfmt.Teletype(&b, textfmt.Doc(textfmt.CodeBlock(code))); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var b bytes.Buffer
|
||||
if err := textfmt.Teletype(&b, textfmt.Doc(textfmt.CodeBlock(code))); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if b.String() != code+"\n" {
|
||||
t.Fatal(b.String())
|
||||
}
|
||||
if b.String() != code+"\n" {
|
||||
t.Fatal(b.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("indented", func(t *testing.T) {
|
||||
const code = `func() textfmt.Document {
|
||||
return textfmt.Document(
|
||||
textfmt.Paragraph(textfmt.Text("Hello, world!")),
|
||||
)
|
||||
}`
|
||||
|
||||
var b bytes.Buffer
|
||||
if err := textfmt.Teletype(&b, textfmt.Doc(textfmt.Indent(textfmt.CodeBlock(code), 4, 0))); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
const expect = ` func() textfmt.Document {
|
||||
return textfmt.Document(
|
||||
textfmt.Paragraph(textfmt.Text("Hello, world!")),
|
||||
)
|
||||
}
|
||||
`
|
||||
|
||||
if b.String() != expect {
|
||||
t.Fatal(b.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("wrap has no effect", func(t *testing.T) {
|
||||
const code = `func() textfmt.Document {
|
||||
return textfmt.Document(
|
||||
textfmt.Paragraph(textfmt.Text("Hello, world!")),
|
||||
)
|
||||
}`
|
||||
|
||||
var b bytes.Buffer
|
||||
if err := textfmt.Teletype(&b, textfmt.Doc(textfmt.Wrap(textfmt.CodeBlock(code), 12))); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if b.String() != code+"\n" {
|
||||
t.Fatal(b.String())
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("syntax", func(t *testing.T) {
|
||||
|
||||
59
text.go
59
text.go
@ -1,12 +1,20 @@
|
||||
package textfmt
|
||||
|
||||
import "strings"
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
func timesn(s string, n int) string {
|
||||
if n < 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
ss := make([]string, n+1)
|
||||
return strings.Join(ss, s)
|
||||
}
|
||||
|
||||
// non-negative numbers only
|
||||
func numDigits(n int) int {
|
||||
if n == 0 {
|
||||
return 1
|
||||
@ -33,19 +41,26 @@ func maxLength(names []string) int {
|
||||
}
|
||||
|
||||
func padRight(s string, n int) string {
|
||||
if len(s) >= n {
|
||||
if len([]rune(s)) >= n {
|
||||
return s
|
||||
}
|
||||
|
||||
n -= len([]rune(s))
|
||||
return s + timesn(" ", 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 = strings.TrimSpace(part)
|
||||
part = trim(part)
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
@ -56,19 +71,27 @@ func singleLine(text string) string {
|
||||
return strings.Join(l, " ")
|
||||
}
|
||||
|
||||
func writeLines(w *writer, txt string, indentFirst, indent int) {
|
||||
lines := strings.Split(txt, "\n")
|
||||
for i, l := range lines {
|
||||
if i > 0 {
|
||||
w.write("\n")
|
||||
}
|
||||
|
||||
ind := indentFirst
|
||||
if i > 0 {
|
||||
ind = indent
|
||||
}
|
||||
|
||||
w.write(timesn(" ", ind))
|
||||
w.write(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())
|
||||
}
|
||||
|
||||
8
wrap.go
8
wrap.go
@ -27,7 +27,7 @@ func wrap(text string, width, firstIndent, restIndent int) string {
|
||||
for _, w := range words {
|
||||
if len(currentLine) == 0 {
|
||||
currentLine = []string{w}
|
||||
lineLen = len(w)
|
||||
lineLen = len([]rune(w))
|
||||
continue
|
||||
}
|
||||
|
||||
@ -36,15 +36,15 @@ func wrap(text string, width, firstIndent, restIndent int) string {
|
||||
maxw = width - firstIndent
|
||||
}
|
||||
|
||||
if lineLen+1+len(w) > maxw {
|
||||
if lineLen+1+len([]rune(w)) > maxw {
|
||||
lines = append(lines, strings.Join(currentLine, " "))
|
||||
currentLine = []string{w}
|
||||
lineLen = len(w)
|
||||
lineLen = len([]rune(w))
|
||||
continue
|
||||
}
|
||||
|
||||
currentLine = append(currentLine, w)
|
||||
lineLen += 1 + len(w)
|
||||
lineLen += 1 + len([]rune(w))
|
||||
}
|
||||
|
||||
lines = append(lines, strings.Join(currentLine, " "))
|
||||
|
||||
104
write.go
Normal file
104
write.go
Normal file
@ -0,0 +1,104 @@
|
||||
package textfmt
|
||||
|
||||
import (
|
||||
"io"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type writer interface {
|
||||
write(...any)
|
||||
error() error
|
||||
setErr(error)
|
||||
}
|
||||
|
||||
type ttyWriter struct {
|
||||
w io.Writer
|
||||
internal bool
|
||||
err error
|
||||
}
|
||||
|
||||
type roffWriter struct {
|
||||
w *bufio.Writer
|
||||
internal bool
|
||||
err error
|
||||
}
|
||||
|
||||
func (w *ttyWriter) write(a ...any) {
|
||||
for _, ai := range a {
|
||||
if w.err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
s := fmt.Sprint(ai)
|
||||
r := []rune(s)
|
||||
if !w.internal {
|
||||
for i := range r {
|
||||
if r[i] == '\u00a0' {
|
||||
r[i] = ' '
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := w.w.Write([]byte(string(r))); err != nil {
|
||||
w.err = err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *ttyWriter) error() error {
|
||||
return w.err
|
||||
}
|
||||
|
||||
func (w *ttyWriter) setErr(err error) {
|
||||
w.err = err
|
||||
}
|
||||
|
||||
func (w *roffWriter) write(a ...any) {
|
||||
for _, ai := range a {
|
||||
if w.err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var rr []rune
|
||||
s := fmt.Sprint(ai)
|
||||
r := []rune(s)
|
||||
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 {
|
||||
w.err = err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *roffWriter) error() error {
|
||||
return w.err
|
||||
}
|
||||
|
||||
func (w *roffWriter) setErr(err error) {
|
||||
w.err = err
|
||||
}
|
||||
|
||||
func writeLines(w writer, txt string, indentFirst, indentRest int) {
|
||||
lines := strings.Split(txt, "\n")
|
||||
for i, l := range lines {
|
||||
if i > 0 {
|
||||
w.write("\n")
|
||||
}
|
||||
|
||||
indent := indentFirst
|
||||
if i > 0 {
|
||||
indent = indentRest
|
||||
}
|
||||
|
||||
w.write(timesn(" ", indent))
|
||||
w.write(l)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user