wip
This commit is contained in:
commit
dda5dc6884
9
Makefile
Normal file
9
Makefile
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
SOURCES = $(shell find . -name "*.go")
|
||||||
|
|
||||||
|
default: build
|
||||||
|
|
||||||
|
build: $(SOURCES)
|
||||||
|
go build
|
||||||
|
|
||||||
|
fmt: $(SOURCES)
|
||||||
|
go fmt
|
||||||
3
go.mod
Normal file
3
go.mod
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
module code.squareroundforest.org/arpio/textfmt
|
||||||
|
|
||||||
|
go 1.25.0
|
||||||
224
lib.go
Normal file
224
lib.go
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
package textfmt
|
||||||
|
|
||||||
|
import "io"
|
||||||
|
|
||||||
|
const (
|
||||||
|
invalid = iota
|
||||||
|
title
|
||||||
|
paragraph
|
||||||
|
list
|
||||||
|
numberedList
|
||||||
|
definitions
|
||||||
|
numberedDefinitions
|
||||||
|
table
|
||||||
|
code
|
||||||
|
syntax
|
||||||
|
)
|
||||||
|
|
||||||
|
type Txt struct {
|
||||||
|
text string
|
||||||
|
link string
|
||||||
|
bold, italic bool
|
||||||
|
cat []Txt
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListItem struct {
|
||||||
|
text Txt
|
||||||
|
}
|
||||||
|
|
||||||
|
type DefinitionItem struct {
|
||||||
|
name, value Txt
|
||||||
|
}
|
||||||
|
|
||||||
|
type TableCell struct {
|
||||||
|
text Txt
|
||||||
|
}
|
||||||
|
|
||||||
|
type TableRow struct {
|
||||||
|
cells []TableCell
|
||||||
|
header bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type SyntaxItem struct {
|
||||||
|
symbol string
|
||||||
|
multiple bool
|
||||||
|
required bool
|
||||||
|
optional bool
|
||||||
|
sequence []SyntaxItem
|
||||||
|
choice []SyntaxItem
|
||||||
|
topLevel bool
|
||||||
|
delimited bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Entry struct {
|
||||||
|
typ int
|
||||||
|
text Txt
|
||||||
|
titleLevel int
|
||||||
|
items []ListItem
|
||||||
|
definitions []DefinitionItem
|
||||||
|
rows []TableRow
|
||||||
|
syntax SyntaxItem
|
||||||
|
indentFirst int
|
||||||
|
indentRest int
|
||||||
|
wrapWidth int
|
||||||
|
}
|
||||||
|
|
||||||
|
type Document struct {
|
||||||
|
entries []Entry
|
||||||
|
}
|
||||||
|
|
||||||
|
func Text(text string) Txt {
|
||||||
|
return Txt{text: text}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Link(title, uri string) Txt {
|
||||||
|
return Txt{text: title, link: uri}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Bold(t Txt) Txt {
|
||||||
|
t.bold = true
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func Italic(t Txt) Txt {
|
||||||
|
t.italic = true
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func Cat(t ...Txt) Txt {
|
||||||
|
return Txt{cat: t}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Title(level int, text string) Entry {
|
||||||
|
return Entry{
|
||||||
|
typ: title,
|
||||||
|
titleLevel: level,
|
||||||
|
text: Text(text),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Paragraph(t Txt) Entry {
|
||||||
|
return Entry{typ: paragraph, text: t}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Item(text Txt) ListItem {
|
||||||
|
return ListItem{text: text}
|
||||||
|
}
|
||||||
|
|
||||||
|
func List(items ...ListItem) Entry {
|
||||||
|
return Entry{typ: list, items: items}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NumberedList(items ...ListItem) Entry {
|
||||||
|
return Entry{typ: numberedList, items: items}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Definition(name, value Txt) DefinitionItem {
|
||||||
|
return DefinitionItem{name: name, value: value}
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefinitionList(items ...DefinitionItem) Entry {
|
||||||
|
return Entry{typ: definitions, definitions: items}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NumberedDefinitionList(items ...DefinitionItem) Entry {
|
||||||
|
return Entry{typ: numberedDefinitions, definitions: items}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Cell(text Txt) TableCell {
|
||||||
|
return TableCell{text: text}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Header(cells ...TableCell) TableRow {
|
||||||
|
return TableRow{cells: cells, header: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Row(cells ...TableCell) TableRow {
|
||||||
|
return TableRow{cells: cells}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Table(rows ...TableRow) Entry {
|
||||||
|
return Entry{typ: table, rows: rows}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CodeBlock(codeBlock string) Entry {
|
||||||
|
return Entry{typ: code, text: Text(codeBlock)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Symbol(text string) SyntaxItem {
|
||||||
|
return SyntaxItem{symbol: text}
|
||||||
|
}
|
||||||
|
|
||||||
|
func OneOrMore(item SyntaxItem) SyntaxItem {
|
||||||
|
item.required = true
|
||||||
|
item.optional = false
|
||||||
|
item.multiple = true
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
func ZeroOrMore(item SyntaxItem) SyntaxItem {
|
||||||
|
item.required = false
|
||||||
|
item.optional = true
|
||||||
|
item.multiple = true
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
func Required(item SyntaxItem) SyntaxItem {
|
||||||
|
item.required = true
|
||||||
|
item.optional = false
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
func Optional(item SyntaxItem) SyntaxItem {
|
||||||
|
item.required = false
|
||||||
|
item.optional = true
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
func Sequence(items ...SyntaxItem) SyntaxItem {
|
||||||
|
return SyntaxItem{sequence: items}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Choice(items ...SyntaxItem) SyntaxItem {
|
||||||
|
return SyntaxItem{choice: items}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Syntax(items ...SyntaxItem) Entry {
|
||||||
|
return Entry{typ: syntax, syntax: Sequence(items...)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Indent(e Entry, first, rest int) Entry {
|
||||||
|
e.indentFirst, e.indentRest = first, rest
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func Wrap(e Entry, width int) Entry {
|
||||||
|
e.wrapWidth = width
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func Doc(e ...Entry) Document {
|
||||||
|
return Document{entries: e}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Teletype(out io.Writer, d Document) error {
|
||||||
|
return renderTeletype(out, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Roff(io.Writer, Document) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Markdown(io.Writer, Document) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func HTML(io.Writer, Document) error {
|
||||||
|
// with the won HTML library
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func HTMLFragment(io.Writer, Document) error {
|
||||||
|
// with the won HTML library
|
||||||
|
return nil
|
||||||
|
}
|
||||||
78
table.go
Normal file
78
table.go
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
package textfmt
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
func normalizeTable(rows []TableRow) []TableRow {
|
||||||
|
var maxColumns int
|
||||||
|
for _, row := range rows {
|
||||||
|
if len(row.cells) > maxColumns {
|
||||||
|
maxColumns = len(row.cells)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalized []TableRow
|
||||||
|
for _, row := range rows {
|
||||||
|
row.cells = append(
|
||||||
|
row.cells,
|
||||||
|
make([]TableCell, maxColumns-len(row.cells))...,
|
||||||
|
)
|
||||||
|
|
||||||
|
normalized = append(normalized, row)
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
func columnWeights(cells [][]string) []int {
|
||||||
|
if len(cells) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
w := make([]int, len(cells[0]))
|
||||||
|
for _, row := range cells {
|
||||||
|
for i, cell := range row {
|
||||||
|
w[i] += len([]rune(cell))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
func targetColumnWidths(tableWidth int, weights []int) []int {
|
||||||
|
var weightSum int
|
||||||
|
for _, w := range weights {
|
||||||
|
weightSum += w
|
||||||
|
}
|
||||||
|
|
||||||
|
widths := make([]int, len(weights))
|
||||||
|
for i := range weights {
|
||||||
|
widths[i] = (weights[i] * tableWidth) / weightSum
|
||||||
|
}
|
||||||
|
|
||||||
|
return widths
|
||||||
|
}
|
||||||
|
|
||||||
|
func columnWidths(rows [][]string) []int {
|
||||||
|
if len(rows) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rows[0]) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
widths := make([]int, len(rows[0]))
|
||||||
|
for i := range rows {
|
||||||
|
for j := range rows[i] {
|
||||||
|
l := strings.Split(rows[i][j], "\n")
|
||||||
|
for k := range l {
|
||||||
|
lk := len([]rune(l[k]))
|
||||||
|
if lk > widths[j] {
|
||||||
|
widths[j] = lk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return widths
|
||||||
|
}
|
||||||
440
teletype.go
Normal file
440
teletype.go
Normal file
@ -0,0 +1,440 @@
|
|||||||
|
package textfmt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func escapeTeletype(s string) string {
|
||||||
|
r := []rune(s)
|
||||||
|
for i := range r {
|
||||||
|
if r[i] >= 0x00 && r[i] <= 0x1f && r[i] != '\n' && r[i] != '\t' {
|
||||||
|
r[i] = 0xb7
|
||||||
|
}
|
||||||
|
|
||||||
|
if r[i] >= 0x7f && r[i] <= 0x9f {
|
||||||
|
r[i] = 0xb7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderTTYText(w *writer, text Txt) {
|
||||||
|
if len(text.cat) > 0 {
|
||||||
|
for i, tc := range text.cat {
|
||||||
|
if i > 0 {
|
||||||
|
w.write(" ")
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTTYText(w, tc)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
text.text = singleLine(text.text)
|
||||||
|
text.text = escapeTeletype(text.text)
|
||||||
|
if text.link != "" {
|
||||||
|
if text.text != "" {
|
||||||
|
w.write(text.text)
|
||||||
|
w.write(" (")
|
||||||
|
w.write(text.link)
|
||||||
|
w.write(")")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.write(text.link)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.write(text.text)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ttyTextToString(text Txt) (string, error) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
w := writer{w: &b}
|
||||||
|
renderTTYText(&w, text)
|
||||||
|
if w.err != nil {
|
||||||
|
return "", w.err
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func definitionNames(d []DefinitionItem) ([]string, error) {
|
||||||
|
var n []string
|
||||||
|
for _, di := range d {
|
||||||
|
name, err := ttyTextToString(di.name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
n = append(n, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderTTYTitle(w *writer, e Entry) {
|
||||||
|
w.write(timesn(" ", e.indentFirst))
|
||||||
|
renderTTYText(w, e.text)
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderTTYParagraph(w *writer, e Entry) {
|
||||||
|
var txt string
|
||||||
|
txt, w.err = ttyTextToString(e.text)
|
||||||
|
if e.wrapWidth > 0 {
|
||||||
|
txt = wrap(txt, e.wrapWidth, e.indentFirst, e.indentRest)
|
||||||
|
}
|
||||||
|
|
||||||
|
writeLines(w, txt, e.indentFirst, e.indentRest)
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderTTYList(w *writer, e Entry) {
|
||||||
|
const bullet = "- "
|
||||||
|
indentFirst := e.indentFirst
|
||||||
|
indentRest := e.indentRest + len(bullet)
|
||||||
|
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), indentFirst, indentRest)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.write(timesn(" ", indentFirst))
|
||||||
|
w.write(bullet)
|
||||||
|
writeLines(w, txt, 0, indentRest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderTTYNumberedList(w *writer, e Entry) {
|
||||||
|
maxDigits := maxDigits(len(e.items))
|
||||||
|
indentFirst := e.indentFirst
|
||||||
|
indentRest := e.indentRest + 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, indentFirst, indentRest)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.write(timesn(" ", indentFirst))
|
||||||
|
w.write(fmt.Sprintf("%d. ", i))
|
||||||
|
writeLines(w, txt, 0, indentRest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderTTYDefinitions(w *writer, e Entry) {
|
||||||
|
names, err := definitionNames(e.definitions)
|
||||||
|
if err != nil {
|
||||||
|
w.err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
maxLength := maxLength(names)
|
||||||
|
indentFirst := e.indentFirst
|
||||||
|
indentRest := e.indentRest + maxLength + 4
|
||||||
|
for i, def := range e.definitions {
|
||||||
|
if i > 0 {
|
||||||
|
w.write("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
var value string
|
||||||
|
value, w.err = ttyTextToString(def.value)
|
||||||
|
if e.wrapWidth > 0 {
|
||||||
|
value = wrap(value, e.wrapWidth-maxLength-4, indentFirst, indentRest)
|
||||||
|
}
|
||||||
|
|
||||||
|
name := names[i]
|
||||||
|
w.write("- ")
|
||||||
|
w.write(name)
|
||||||
|
w.write(": ")
|
||||||
|
writeLines(w, value, 0, indentRest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderTTYNumberedDefinitions(w *writer, e Entry) {
|
||||||
|
maxDigits := maxDigits(len(e.definitions))
|
||||||
|
names, err := definitionNames(e.definitions)
|
||||||
|
if err != nil {
|
||||||
|
w.err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
maxLength := maxLength(names)
|
||||||
|
indentFirst := e.indentFirst
|
||||||
|
indentRest := e.indentRest + maxLength + maxDigits + 4
|
||||||
|
for i, def := range e.definitions {
|
||||||
|
if i > 0 {
|
||||||
|
w.write("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
var value string
|
||||||
|
value, w.err = ttyTextToString(def.value)
|
||||||
|
if e.wrapWidth > 0 {
|
||||||
|
value = wrap(value, e.wrapWidth-maxLength-4, indentFirst, indentRest)
|
||||||
|
}
|
||||||
|
|
||||||
|
name := names[i]
|
||||||
|
w.write(fmt.Sprintf("%d. ", i))
|
||||||
|
w.write(name)
|
||||||
|
w.write(": ")
|
||||||
|
writeLines(w, value, 0, indentRest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ttyCellTexts(rows []TableRow) ([][]string, error) {
|
||||||
|
var cellTexts [][]string
|
||||||
|
for _, row := range rows {
|
||||||
|
var c []string
|
||||||
|
for _, cell := range row.cells {
|
||||||
|
txt, err := ttyTextToString(cell.text)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c = append(c, txt)
|
||||||
|
}
|
||||||
|
|
||||||
|
cellTexts = append(cellTexts, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cellTexts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderTTYTable(w *writer, e Entry) {
|
||||||
|
if len(e.rows) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.rows = normalizeTable(e.rows)
|
||||||
|
if len(e.rows[0].cells) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cellTexts, err := ttyCellTexts(e.rows)
|
||||||
|
if err != nil {
|
||||||
|
w.err = err
|
||||||
|
}
|
||||||
|
|
||||||
|
totalSeparatorWidth := (len(cellTexts[0]) - 1) * 3
|
||||||
|
if e.wrapWidth > 0 {
|
||||||
|
allocatedWidth := e.wrapWidth - e.indentFirst - 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(sep, totalWidth))
|
||||||
|
w.write("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
for j := range cellTexts[i] {
|
||||||
|
if j > 0 {
|
||||||
|
w.write(" | ")
|
||||||
|
}
|
||||||
|
|
||||||
|
w.write(padRight(cellTexts[i][j], columnWidths[j]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasHeader && len(cellTexts) == 1 {
|
||||||
|
w.write(timesn("=", totalWidth))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderTTYCode(w *writer, e Entry) {
|
||||||
|
var txt string
|
||||||
|
txt, w.err = ttyTextToString(e.text)
|
||||||
|
writeLines(w, txt, e.indentFirst, e.indentFirst)
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderTTYMultiple(w *writer, s SyntaxItem) {
|
||||||
|
s.topLevel = false
|
||||||
|
s.multiple = false
|
||||||
|
renderTTYSyntaxItem(w, s)
|
||||||
|
w.write("...")
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderTTYRequired(w *writer, s SyntaxItem) {
|
||||||
|
s.delimited = true
|
||||||
|
s.topLevel = false
|
||||||
|
s.required = false
|
||||||
|
w.write("<")
|
||||||
|
renderTTYSyntaxItem(w, s)
|
||||||
|
w.write(">")
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderTTYOptional(w *writer, s SyntaxItem) {
|
||||||
|
s.delimited = true
|
||||||
|
s.topLevel = false
|
||||||
|
s.optional = false
|
||||||
|
w.write("[")
|
||||||
|
renderTTYSyntaxItem(w, s)
|
||||||
|
w.write("]")
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderTTYSequence(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
|
||||||
|
renderTTYSyntaxItem(w, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.delimited && !s.topLevel {
|
||||||
|
w.write(")")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderTTYChoice(w *writer, s SyntaxItem) {
|
||||||
|
if !s.delimited && !s.topLevel {
|
||||||
|
w.write("(")
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, item := range s.sequence {
|
||||||
|
if i > 0 {
|
||||||
|
separator := "|"
|
||||||
|
if s.topLevel {
|
||||||
|
separator = "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
w.write(separator)
|
||||||
|
}
|
||||||
|
|
||||||
|
item.delimited = false
|
||||||
|
renderTTYSyntaxItem(w, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.delimited && !s.topLevel {
|
||||||
|
w.write(")")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderTTYSymbol(w *writer, s SyntaxItem) {
|
||||||
|
w.write(escapeTeletype(s.symbol))
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderTTYSyntaxItem(w *writer, s SyntaxItem) {
|
||||||
|
switch {
|
||||||
|
|
||||||
|
// foo...
|
||||||
|
case s.multiple:
|
||||||
|
renderTTYMultiple(w, s)
|
||||||
|
|
||||||
|
// <foo>
|
||||||
|
case s.required:
|
||||||
|
renderTTYRequired(w, s)
|
||||||
|
|
||||||
|
// [foo]
|
||||||
|
case s.optional:
|
||||||
|
renderTTYOptional(w, s)
|
||||||
|
|
||||||
|
// foo bar baz or (foo bar baz)
|
||||||
|
case len(s.sequence) > 0:
|
||||||
|
renderTTYSequence(w, s)
|
||||||
|
|
||||||
|
// foo|bar|baz or (foo|bar|baz)
|
||||||
|
case len(s.choice) > 0:
|
||||||
|
renderTTYChoice(w, s)
|
||||||
|
|
||||||
|
// foo
|
||||||
|
default:
|
||||||
|
renderTTYSymbol(w, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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}
|
||||||
|
for i, e := range d.entries {
|
||||||
|
if i > 0 {
|
||||||
|
w.write("\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch e.typ {
|
||||||
|
case invalid:
|
||||||
|
return errors.New("invalid entry")
|
||||||
|
case title:
|
||||||
|
renderTTYTitle(&w, e)
|
||||||
|
case paragraph:
|
||||||
|
renderTTYParagraph(&w, e)
|
||||||
|
case list:
|
||||||
|
renderTTYList(&w, e)
|
||||||
|
case numberedList:
|
||||||
|
renderTTYNumberedList(&w, e)
|
||||||
|
case definitions:
|
||||||
|
renderTTYDefinitions(&w, e)
|
||||||
|
case numberedDefinitions:
|
||||||
|
renderTTYNumberedDefinitions(&w, e)
|
||||||
|
case table:
|
||||||
|
renderTTYTable(&w, e)
|
||||||
|
case code:
|
||||||
|
renderTTYCode(&w, e)
|
||||||
|
case syntax:
|
||||||
|
renderTTYSyntax(&w, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.write("\n")
|
||||||
|
return w.err
|
||||||
|
}
|
||||||
38
text.go
Normal file
38
text.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package textfmt
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
func timesn(s string, n int) string {
|
||||||
|
ss := make([]string, n+1)
|
||||||
|
return strings.Join(ss, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func maxDigits(n int) int {
|
||||||
|
if n == 0 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
var d int
|
||||||
|
for n > 0 {
|
||||||
|
d++
|
||||||
|
n /= 10
|
||||||
|
}
|
||||||
|
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
func maxLength(names []string) int {
|
||||||
|
var m int
|
||||||
|
for _, n := range names {
|
||||||
|
if len([]rune(n)) > m {
|
||||||
|
m = len([]rune(n))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func padRight(s string, n int) string {
|
||||||
|
n -= len([]rune(s))
|
||||||
|
return s + timesn(" ", n)
|
||||||
|
}
|
||||||
75
wrap.go
Normal file
75
wrap.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
package textfmt
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
func getWords(text string) []string {
|
||||||
|
var words []string
|
||||||
|
raw := strings.Split(text, " ")
|
||||||
|
for _, r := range raw {
|
||||||
|
if r == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
words = append(words, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
return words
|
||||||
|
}
|
||||||
|
|
||||||
|
func lineLength(words []string) int {
|
||||||
|
if len(words) == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var l int
|
||||||
|
for _, w := range words {
|
||||||
|
r := []rune(w)
|
||||||
|
l += len(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
return l + len(words) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func singleLine(text string) string {
|
||||||
|
var l []string
|
||||||
|
p := strings.Split(text, "\n")
|
||||||
|
for _, part := range p {
|
||||||
|
part = strings.TrimSpace(part)
|
||||||
|
if part == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
l = append(l, part)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(l, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func wrap(text string, width, firstIndent, restIndent int) string {
|
||||||
|
var (
|
||||||
|
lines []string
|
||||||
|
currentLine []string
|
||||||
|
currentLen int
|
||||||
|
)
|
||||||
|
|
||||||
|
words := getWords(text)
|
||||||
|
for _, w := range words {
|
||||||
|
maxw := width - restIndent
|
||||||
|
if len(lines) == 0 {
|
||||||
|
maxw = width - firstIndent
|
||||||
|
}
|
||||||
|
|
||||||
|
currentLine = append(currentLine, w)
|
||||||
|
if lineLength(currentLine) > maxw {
|
||||||
|
currentLine = currentLine[:len(currentLine)-1]
|
||||||
|
lines = append(lines, strings.Join(currentLine, " "))
|
||||||
|
currentLine = []string{w}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(currentLine) > 0 {
|
||||||
|
lines = append(lines, strings.Join(currentLine, " "))
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(lines, "\n")
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user