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 }