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 definitionNamesValues(d []DefinitionItem) ([]string, []string, error) { var n, v []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 } n = append(n, name) v = append(v, value) } return n, v, nil } 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 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 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.indent) } writeLines(w, txt, e.indentFirst, e.indent) } func renderTTYList(w *writer, e Entry) { const bullet = "- " indent := e.indent + 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), e.indentFirst, indent) } w.write(timesn(" ", e.indentFirst)) w.write(bullet) writeLines(w, txt, 0, indent) } } 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) } } func renderTTYDefinitions(w *writer, e Entry) { const ( bullet = "- " sep = ": " ) names, values, err := definitionNamesValues(e.definitions) if err != nil { w.err = 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 { 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, ) } } func renderTTYNumberedDefinitions(w *writer, e Entry) { const ( dot = ". " sep = ": " ) names, values, err := definitionNamesValues(e.definitions) if err != nil { w.err = err return } 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 { 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, ) } } 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.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 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) { 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.choice { 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) // 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) } } if len(d.entries) > 0 { w.write("\n") } return w.err }