package textfmt import ( "bytes" "errors" "fmt" "io" "strings" "time" ) 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 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) { 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 != "" { renderRoffString(w, text.text, additionalEscape...) w.write(" (") 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 { e.text.bold = true p := Entry{ typ: paragraph, text: e.text, indent: e.indent, indentFirst: e.indentFirst, } 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.ti ", 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("\n.br\n") } w.write(".in ", e.indent+2, "\n.ti ", 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("\n.br\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)) 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("\n.br\n") } w.write(".in ", e.indent+maxNameLength+4, "\n.ti ", 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("\n.br\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)) renderRoffText(w, definition.name) w.write(":", timesn("\\~", maxNameLength-len([]rune(names[i]))+1)) renderRoffText(w, definition.value) } } 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) { 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) // 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) { 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 { 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 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) default: return errors.New("invalid entry") } } if len(d.entries) > 0 { w.write("\n") } return w.err }