package textfmt import ( "bytes" "errors" "fmt" "io" "strings" ) type mdEscapeState struct { newLine bool numberOnNewLine bool linkValue bool linkClosed bool linkOpen bool } func updateMDEscapeState(s mdEscapeState, r rune) mdEscapeState { return mdEscapeState{ numberOnNewLine: (s.newLine || s.numberOnNewLine) && r >= '0' && r <= '9', newLine: r == '\n', linkValue: s.linkClosed && r == '(' || s.linkValue && r != ')', linkClosed: s.linkOpen && r == ']', linkOpen: !s.linkValue && r == '[' || s.linkOpen && r != ']', } } func escapeMarkdown(out writer, additional ...rune) writer { esc := map[rune]string{ '\\': "\\\\", '`': "\\`", '*': "\\*", '_': "\\_", '[': "\\[", ']': "\\]", '#': "\\#", '<': "\\<", '>': "\\>", } for _, a := range additional { esc[a] = string([]rune{'\\', a}) } return &escape[mdEscapeState]{ out: out, state: mdEscapeState{ newLine: true, }, escape: esc, conditionalEscape: map[rune]func(mdEscapeState, rune) (string, bool){ '+': func(s mdEscapeState, _ rune) (string, bool) { if s.newLine { return "\\+", true } return "", false }, '-': func(s mdEscapeState, _ rune) (string, bool) { if s.newLine { return "\\-", true } return "", false }, '.': func(s mdEscapeState, _ rune) (string, bool) { if s.numberOnNewLine { return "\\.", true } return "", false }, '(': func(s mdEscapeState, _ rune) (string, bool) { if s.linkClosed { return "\\(", true } return "", false }, ')': func(s mdEscapeState, _ rune) (string, bool) { if s.linkValue { return "\\)", true } return "", false }, }, updateState: updateMDEscapeState, } } func escapeMarkdownPrev(s string, additional ...rune) string { var b bytes.Buffer w := &textWriter{out: &b} e := escapeMarkdown(w, additional...) e.write(s) return b.String() /* var ( rr []rune isNumberOnNewLine bool isLinkOpen, isLinkClosed, isLinkValue bool ) isNewLine := true r := []rune(s) for _, ri := range r { switch ri { case '\\', '`', '*', '_', '[', ']', '#', '<', '>': rr = append(rr, '\\', ri) default: switch { case isNewLine: switch ri { case '+', '-': rr = append(rr, '\\', ri) default: rr = append(rr, ri) } case isNumberOnNewLine: switch ri { case '.': rr = append(rr, '\\', ri) default: rr = append(rr, ri) } case isLinkClosed: switch ri { case '(': rr = append(rr, '\\', ri) default: rr = append(rr, ri) } case isLinkValue: switch ri { case ')': rr = append(rr, '\\', ri) default: rr = append(rr, ri) } default: if slices.Contains(additional, ri) { rr = append(rr, '\\', ri) } else { rr = append(rr, ri) } } } isNumberOnNewLine = (isNewLine || isNumberOnNewLine) && ri >= '0' && ri <= '9' isNewLine = ri == '\n' isLinkValue = isLinkClosed && ri == '(' || isLinkValue && ri != ')' isLinkClosed = isLinkOpen && ri == ']' isLinkOpen = !isLinkValue && ri == '[' || isLinkOpen && ri != ']' } return string(rr) */ } func mdTextToString(text Txt) (string, error) { var b bytes.Buffer w := mdWriter{w: &b, internal: true} renderMDText(&w, text) if w.err != nil { return "", w.err } return b.String(), nil } func mdCellTexts(rows []TableRow) ([][]string, error) { var texts [][]string for _, row := range rows { var rowTexts []string for _, cell := range row.cells { txt, err := mdTextToString(cell.text) if err != nil { return nil, err } rowTexts = append(rowTexts, txt) } texts = append(texts, rowTexts) } return texts, nil } func mdEnsureHeaderTexts(h []string) []string { var hh []string for _, t := range h { if strings.TrimSpace(t) == "" { t = "\\-" } hh = append(hh, t) } return hh } func renderMDText(w writer, text Txt) { if len(text.cat) > 0 { for i, tc := range text.cat { if i > 0 { w.write(" ") } renderMDText(w, tc) } return } text.text = singleLine(text.text) text.text = escapeMarkdownPrev(text.text) text.link = singleLine(text.link) text.link = escapeMarkdownPrev(text.link) if text.bold { w.write("**") } if text.italic { w.write("_") } defer func() { if text.italic { w.write("_") } if text.bold { w.write("**") } }() if text.link != "" { if text.text != "" { w.write("[") w.write(text.text) w.write("](") w.write(text.link) w.write(")") return } w.write(text.link) return } w.write(text.text) } func renderMDTitle(w writer, e Entry) { hashes := e.titleLevel + 1 if hashes > 6 { hashes = 6 } w.write(timesn("#", hashes), " ") renderMDText(w, e.text) } func renderMDParagraphIndent(w writer, e Entry) { txt, err := mdTextToString(e.text) if err != nil { w.setErr(err) } indentFirst := e.indent + e.indentFirst if e.wrapWidth > 0 { txt = wrap(txt, e.wrapWidth, indentFirst, e.indent) } writeLines(w, txt, indentFirst, e.indent) } func renderMDParagraph(w writer, e Entry) { e.indent = 0 e.indentFirst = 0 renderMDParagraphIndent(w, e) } func renderMDList(w writer, e Entry) { e.indent = 2 e.indentFirst = -2 if e.wrapWidth > 2 { e.wrapWidth -= 2 } for i, item := range e.items { if i > 0 { w.write("\n") } w.write("- ") p := itemToParagraph(e, item.text) renderMDParagraphIndent(w, p) } } func renderMDNumberedList(w writer, e Entry) { maxDigits := numDigits(len(e.items)) e.indent = maxDigits + 2 e.indentFirst = 0 - maxDigits - 2 if e.wrapWidth > maxDigits+2 { e.wrapWidth -= maxDigits + 2 } for i, item := range e.items { if i > 0 { w.write("\n") } w.write(padRight(fmt.Sprintf("%d.", i+1), maxDigits+2)) p := itemToParagraph(e, item.text) renderMDParagraphIndent(w, p) } } func renderMDDefinitions(w writer, e Entry) { for _, d := range e.definitions { e.items = append( e.items, Item(Cat(Text(fmt.Sprintf("%s:", d.name.text)), d.value)), ) } renderMDList(w, e) } func renderMDNumberedDefinitions(w writer, e Entry) { for _, d := range e.definitions { e.items = append( e.items, Item(Cat(Text(fmt.Sprintf("%s:", d.name.text)), d.value)), ) } renderMDNumberedList(w, e) } func renderMDTable(w writer, e Entry) { e.rows = normalizeTable(e.rows) e.rows = ensureHeader(e.rows) if len(e.rows) == 0 || len(e.rows[0].cells) == 0 { return } headerTexts, err := mdCellTexts(e.rows[:1]) if err != nil { w.setErr(err) return } cellTexts, err := mdCellTexts(e.rows[1:]) if err != nil { w.setErr(err) return } headerTexts[0] = mdEnsureHeaderTexts(headerTexts[0]) columns := columnWidths(headerTexts) cellColumns := columnWidths(cellTexts) if len(cellColumns) > 0 { for i := range columns { if cellColumns[i] > columns[i] { columns[i] = cellColumns[i] } } } w.write("|") for i, h := range headerTexts[0] { w.write(" ", padRight(h, columns[i])) w.write(" |") } w.write("\n|") for _, c := range columns { w.write(timesn("-", c+1)) w.write("-|") } for _, row := range cellTexts { w.write("\n|") for i, cell := range row { w.write(" ", padRight(cell, columns[i])) w.write(" |") } } } func renderMDCode(w writer, e Entry) { w.write("```\n") w.write(e.text.text) w.write("\n```") } func renderMDMultiple(w writer, s SyntaxItem) { s.topLevel = false s.multiple = false renderMDSyntaxItem(w, s) w.write("...") } func renderMDRequired(w writer, s SyntaxItem) { s.delimited = true s.topLevel = false s.required = false w.write("<") renderMDSyntaxItem(w, s) w.write(">") } func renderMDOptional(w writer, s SyntaxItem) { s.delimited = true s.topLevel = false s.optional = false w.write("[") renderMDSyntaxItem(w, s) w.write("]") } func renderMDSequence(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 renderMDSyntaxItem(w, item) } if !s.delimited && !s.topLevel { w.write(")") } } func renderMDChoice(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 renderMDSyntaxItem(w, item) } if !s.delimited && !s.topLevel { w.write(")") } } func renderMDSymbol(w writer, s SyntaxItem) { w.write(escapeMarkdownPrev(s.symbol)) } func renderMDSyntaxItem(w writer, s SyntaxItem) { switch { // foo... case s.multiple: renderMDMultiple(w, s) // case s.required: renderMDRequired(w, s) // [foo] case s.optional: renderMDOptional(w, s) // foo bar baz or (foo bar baz) case len(s.sequence) > 0: renderMDSequence(w, s) // foo|bar|baz or (foo|bar|baz) case len(s.choice) > 0: renderMDChoice(w, s) // foo default: renderMDSymbol(w, s) } } func renderMDSyntax(w writer, e Entry) { s := e.syntax s.topLevel = true w.write("```\n") renderMDSyntaxItem(w, s) w.write("\n```") } func renderMarkdown(out io.Writer, d Document) error { w := mdWriter{w: out} for i, e := range d.entries { if i > 0 { w.write("\n\n") } switch e.typ { case title: renderMDTitle(&w, e) case paragraph: renderMDParagraph(&w, e) case list: renderMDList(&w, e) case numberedList: renderMDNumberedList(&w, e) case definitions: renderMDDefinitions(&w, e) case numberedDefinitions: renderMDNumberedDefinitions(&w, e) case table: renderMDTable(&w, e) case code: renderMDCode(&w, e) case syntax: renderMDSyntax(&w, e) default: return errors.New("invalid entry") } } if len(d.entries) > 0 { w.write("\n") } return w.err }