package textfmt import ( "bytes" "errors" "fmt" "io" "strings" "time" ) func manPageDate(d time.Time) string { return fmt.Sprintf("%v %d", d.Month(), d.Year()) } func escapeRoffString(s string, additionalEscape ...string) string { var b bytes.Buffer w, f := writeWith(&b, escapeRoff(additionalEscape...)) write(w, s) f() return b.String() } func roffTextLength(t Txt) int { var l int if len(t.cat) > 0 { for i, tc := range t.cat { if i > 0 { l++ } l += roffTextLength(tc) } return l } if t.link == "" { return len([]rune(t.text)) } if t.text == "" { return len([]rune(t.link)) } return len([]rune(t.text)) + len([]rune(t.link)) + 3 } func roffTextToString(t Txt) string { var b bytes.Buffer renderRoffText(&b, t) return b.String() } func roffCellTexts(r []TableRow) [][]string { var cellTexts [][]string for _, row := range r { var c []string for _, cell := range row.cells { c = append(c, roffTextToString(cell.text)) } cellTexts = append(cellTexts, c) } return cellTexts } func renderRoffText(w io.Writer, text Txt, additionalEscape ...string) { if len(text.cat) > 0 { for i, tc := range text.cat { if i > 0 { write(w, " ") } renderRoffText(w, tc) } return } if text.bold { write(w, "\\fB") } if text.italic { write(w, "\\fI") } if text.bold || text.italic { defer write(w, "\\fR") } w, f := writeWith(w, singleLine(), escapeRoff(additionalEscape...)) if text.link == "" { write(w, text.text) f() return } if text.text == "" { write(w, text.link) f() return } write(w, text.text) write(w, " (") write(w, text.link) write(w, ")") f() } func renderRoffTitle(w io.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 } write(w, ".TH \"") renderRoffText(w, e.text, "\"", "\\(dq") write(w, "\" ") w, f := writeWith(w, singleLine(), escapeRoff("\"", "\\(dq")) write(w, fmt.Sprint(e.man.section)) w, _ = f() write(w, " \"") if !e.man.date.IsZero() { write(w, manPageDate(e.man.date)) } write(w, "\" \"") w, f = writeWith(w, singleLine(), escapeRoff("\"", "\\(dq")) write(w, e.man.version) w, _ = f() write(w, "\" \"") w, f = writeWith(w, singleLine(), escapeRoff("\"", "\\(dq")) write(w, e.man.category) w, _ = f() write(w, "\"") } func renderRoffParagraph(w io.Writer, e Entry) { write(w, ".in ", e.indent, "\n.ti ", e.indent+e.indentFirst, "\n") renderRoffText(w, e.text) } func renderRoffList(w io.Writer, e Entry) { bullets := make([]string, len(e.items)) bulletLengths := make([]int, len(e.items)) for i := range e.items { itemStyle := mergeItemStyles(e.items[i].style) if itemStyle.noBullet { continue } if itemStyle.bullet == "" { bullets[i] = "\\(bu" bulletLengths[i] = 1 continue } bullets[i] = escapeRoffString(itemStyle.bullet, " ", "\\~") bulletLengths[i] = len([]rune(itemStyle.bullet)) } var maxBulletLength int for _, l := range bulletLengths { if l > maxBulletLength { maxBulletLength = l } } for i := range bullets { if bulletLengths[i] == maxBulletLength { continue } bullets[i] = fmt.Sprintf( "%s%s", bullets[i], timesn("\\~", maxBulletLength-bulletLengths[i]), ) } for i, item := range e.items { if i > 0 { write(w, "\n.br\n") } indent := e.indent if maxBulletLength > 0 { indent += maxBulletLength + 1 } write(w, ".in ", indent, "\n.ti ", e.indent+e.indentFirst, "\n") write(w, bullets[i]) if maxBulletLength > 0 { write(w, "\\~") } renderRoffText(w, item.text) } } func renderRoffNumberedList(w io.Writer, e Entry) { items := make([]ListItem, len(e.items)) for i := range e.items { items[i] = Item( e.items[i].text, append(e.items[i].style, Bullet(fmt.Sprintf("%d.", i+1)))..., ) } e.typ = list e.items = items renderRoffList(w, e) } func renderRoffDefinitions(w io.Writer, e Entry) { itemStyles := make([]ItemStyle, len(e.definitions)) for i := range e.definitions { itemStyles[i] = mergeItemStyles(e.definitions[i].style) } bullets := make([]string, len(itemStyles)) bulletLengths := make([]int, len(itemStyles)) for i := range itemStyles { if itemStyles[i].noBullet { continue } if itemStyles[i].bullet == "" { bullets[i] = "\\(bu" bulletLengths[i] = 1 continue } bullets[i] = escapeRoffString(itemStyles[i].bullet, " ", "\\~") bulletLengths[i] = len([]rune(itemStyles[i].bullet)) } var maxBulletLength int for i := range bulletLengths { if bulletLengths[i] > maxBulletLength { maxBulletLength = bulletLengths[i] } } nameLengths := make([]int, len(e.definitions)) for i := range e.definitions { nameLengths[i] = roffTextLength(e.definitions[i].name) } var maxNameLength int for i := range nameLengths { if nameLengths[i] > maxNameLength { maxNameLength = nameLengths[i] } } for i, definition := range e.definitions { if i > 0 { write(w, "\n.br\n") } indent := e.indent + maxNameLength + 2 if maxBulletLength > 0 { indent += maxBulletLength + 1 } write(w, ".in ", indent, "\n.ti ", e.indent+e.indentFirst, "\n") write(w, bullets[i]) if maxBulletLength > 0 { write(w, timesn("\\~", maxBulletLength-bulletLengths[i]+1)) } renderRoffText(w, definition.name) write(w, ":", timesn("\\~", maxNameLength-nameLengths[i]+1)) renderRoffText(w, definition.value) } } func renderRoffNumberedDefinitions(w io.Writer, e Entry) { defs := make([]DefinitionItem, len(e.definitions)) for i := range e.definitions { defs[i] = Definition( e.definitions[i].name, e.definitions[i].value, append(e.definitions[i].style, Bullet(fmt.Sprintf("%d.", i+1)))..., ) } e.typ = definitions e.definitions = defs renderRoffDefinitions(w, e) } func renderRoffTable(w io.Writer, e Entry) { if len(e.rows) == 0 { return } e.rows = normalizeTable(e.rows) if len(e.rows[0].cells) == 0 { return } write(w, ".nf\n") defer write(w, "\n.fi") cellTexts := roffCellTexts(e.rows) 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] = editString(cellTexts[i][j], wrap(targetColumnWidths[j], targetColumnWidths[j])) } } } columnWidths := columnWidths(cellTexts) totalWidth := totalSeparatorWidth for i := range columnWidths { totalWidth += columnWidths[i] } w, f := writeWith(w, indent(e.indent, e.indent)) hasHeader := e.rows[0].header for i := range cellTexts { if i > 0 { sep := "-" if hasHeader && i == 1 { sep = "=" } write(w, "\n") write(w, timesn(sep, totalWidth)) write(w, "\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 { write(w, "\n") } for j := range lines { if j > 0 { write(w, " | ") } var l string if k < len(lines[j]) { l = lines[j][k] } write(w, padRight(l, columnWidths[j])) } } } if hasHeader && len(cellTexts) == 1 { write(w, "\n", timesn("=", totalWidth)) } f() } func renderRoffCode(w io.Writer, e Entry) { write(w, ".nf\n") defer write(w, "\n.fi") w, f := writeWith(w, escapeRoff(), indent(e.indent, e.indent)) write(w, e.text.text) f() } func renderRoffMultiple(w io.Writer, s SyntaxItem) { s.topLevel = false s.multiple = false renderRoffSyntaxItem(w, s) write(w, "...") } func renderRoffRequired(w io.Writer, s SyntaxItem) { s.delimited = true s.topLevel = false s.required = false write(w, "<") renderRoffSyntaxItem(w, s) write(w, ">") } func renderRoffOptional(w io.Writer, s SyntaxItem) { s.delimited = true s.topLevel = false s.optional = false write(w, "[") renderRoffSyntaxItem(w, s) write(w, "]") } func renderRoffSequence(w io.Writer, s SyntaxItem) { if !s.delimited && !s.topLevel { write(w, "(") } for i, item := range s.sequence { if i > 0 { write(w, " ") } item.delimited = false renderRoffSyntaxItem(w, item) } if !s.delimited && !s.topLevel { write(w, ")") } } func renderRoffChoice(w io.Writer, s SyntaxItem) { if !s.delimited && !s.topLevel { write(w, "(") } for i, item := range s.choice { if i > 0 { separator := "|" if s.topLevel { separator = "\n" } write(w, separator) } item.delimited = false item.topLevel = s.topLevel renderRoffSyntaxItem(w, item) } if !s.delimited && !s.topLevel { write(w, ")") } } func renderRoffSymbol(w io.Writer, s SyntaxItem) { write(w, s.symbol) } func renderRoffSyntaxItem(w io.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 io.Writer, e Entry) { s := e.syntax s.topLevel = true write(w, ".nf\n") defer write(w, "\n.fi") w, f := writeWith(w, escapeRoff(), indent(e.indent, e.indent)) renderRoffSyntaxItem(w, s) f() } func renderRoff(out io.Writer, d Document) error { w, f := writeWith(out, roffNBSP(), errorHandler) for i, e := range d.entries { if i > 0 { write(w, "\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 { write(w, "\n") } _, err := f() return err }