1
0
textfmt/markdown.go

461 lines
7.9 KiB
Go
Raw Normal View History

2025-10-28 00:47:41 +01:00
package textfmt
import (
"bytes"
"errors"
"fmt"
"io"
"slices"
"strings"
)
func escapeMarkdown(s string, additional ...rune) string {
var (
rr []rune
isNumberOnNewLine bool
isLinkOpen, isLinkClosed, isLinkLabel 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 isLinkLabel:
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'
isLinkOpen = !isLinkLabel && ri == '[' || isLinkOpen && ri != ']'
isLinkClosed = isLinkOpen && ri == ']'
isLinkLabel = isLinkClosed && ri == '(' || isLinkLabel && 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 = escapeMarkdown(text.text)
text.link = singleLine(text.link)
text.link = escapeMarkdown(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(escapeTeletype(s.symbol))
}
func renderMDSyntaxItem(w writer, s SyntaxItem) {
switch {
// foo...
case s.multiple:
renderMDMultiple(w, s)
// <foo>
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 invalid:
return errors.New("invalid entry")
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)
}
}
if len(d.entries) > 0 {
w.write("\n")
}
return w.err
}