internal/termtext/markdown.go

197 lines · 4636 bytes

  1package termtext
  2
  3import (
  4	"fmt"
  5	"strings"
  6
  7	"github.com/yuin/goldmark"
  8	"github.com/yuin/goldmark/ast"
  9	"github.com/yuin/goldmark/extension"
 10	east "github.com/yuin/goldmark/extension/ast"
 11	"github.com/yuin/goldmark/text"
 12)
 13
 14// md parses as the web does (CommonMark plus GFM); raw HTML is dropped
 15// there and here.
 16var md = goldmark.New(goldmark.WithExtensions(extension.GFM))
 17
 18func Markdown(src string, o Options) string {
 19	return renderMarkdown(src, &out{o: o})
 20}
 21
 22func renderMarkdown(src string, w *out) string {
 23	source := []byte(src)
 24	doc := md.Parser().Parse(text.NewReader(source))
 25	r := mdRenderer{w: w, src: source}
 26	r.blocks(doc, "", "")
 27	return w.String()
 28}
 29
 30// Inline is src as one line of plain text, links reduced to their
 31// text, for event lines.
 32func Inline(src, format string) string {
 33	w := &out{noURLs: true}
 34	var s string
 35	if format == "org" {
 36		s = renderOrg(src, w)
 37	} else {
 38		s = renderMarkdown(src, w)
 39	}
 40	return strings.Join(strings.Fields(s), " ")
 41}
 42
 43type mdRenderer struct {
 44	w   *out
 45	src []byte
 46}
 47
 48// blocks renders n's children with a blank line between them. The
 49// first child's first line is prefixed by first, every other line by
 50// rest.
 51func (r mdRenderer) blocks(n ast.Node, first, rest string) {
 52	p := first
 53	for c := n.FirstChild(); c != nil; c = c.NextSibling() {
 54		if c != n.FirstChild() {
 55			r.w.blank()
 56		}
 57		r.block(c, p, rest)
 58		p = rest
 59	}
 60}
 61
 62func (r mdRenderer) block(n ast.Node, first, rest string) {
 63	switch n := n.(type) {
 64	case *ast.Heading:
 65		r.w.para(r.w.paint(sgrBold, r.inline(n)), first, rest)
 66	case *ast.Paragraph:
 67		r.w.para(r.inline(n), first, rest)
 68	case *ast.TextBlock:
 69		r.w.para(r.inline(n), first, rest)
 70	case *ast.List:
 71		i := n.Start
 72		p := first
 73		for item := n.FirstChild(); item != nil; item = item.NextSibling() {
 74			marker := "• "
 75			if n.IsOrdered() {
 76				marker = fmt.Sprintf("%d. ", i)
 77				i++
 78			}
 79			hang := rest + strings.Repeat(" ", cells(marker))
 80			for c := item.FirstChild(); c != nil; c = c.NextSibling() {
 81				if c == item.FirstChild() {
 82					r.block(c, p+marker, hang)
 83				} else {
 84					if !n.IsTight {
 85						r.w.blank()
 86					}
 87					r.block(c, hang, hang)
 88				}
 89			}
 90			p = rest
 91		}
 92	case *ast.FencedCodeBlock:
 93		r.w.code(r.lines(n), string(n.Language(r.src)), rest)
 94	case *ast.CodeBlock:
 95		r.w.code(r.lines(n), "", rest)
 96	case *ast.Blockquote:
 97		bar := r.w.paint(sgrDim, "│ ")
 98		r.blocks(n, first+bar, rest+bar)
 99	case *ast.ThematicBreak:
100		r.w.rule(first)
101	case *ast.HTMLBlock:
102		// Dropped, as the web drops it.
103	default:
104		// GFM tables and anything else: the source, as a code block.
105		// Tables (and their rows/cells) don't carry Lines() themselves,
106		// so span the raw source under the node instead.
107		r.w.code(r.raw(n), "", rest)
108	}
109}
110
111func (r mdRenderer) lines(n ast.Node) string {
112	var b strings.Builder
113	ls := n.Lines()
114	for i := 0; i < ls.Len(); i++ {
115		seg := ls.At(i)
116		b.Write(seg.Value(r.src))
117	}
118	return b.String()
119}
120
121// raw returns the source text spanned by n and its descendants,
122// extended to whole lines.
123func (r mdRenderer) raw(n ast.Node) string {
124	start, end := -1, -1
125	var walk func(ast.Node)
126	walk = func(x ast.Node) {
127		if x.Type() == ast.TypeBlock {
128			ls := x.Lines()
129			for i := 0; i < ls.Len(); i++ {
130				seg := ls.At(i)
131				if start == -1 || seg.Start < start {
132					start = seg.Start
133				}
134				if seg.Stop > end {
135					end = seg.Stop
136				}
137			}
138		}
139		for c := x.FirstChild(); c != nil; c = c.NextSibling() {
140			walk(c)
141		}
142	}
143	walk(n)
144	if start == -1 {
145		return ""
146	}
147	for start > 0 && r.src[start-1] != '\n' {
148		start--
149	}
150	for end < len(r.src) && r.src[end] != '\n' {
151		end++
152	}
153	return string(r.src[start:end])
154}
155
156func (r mdRenderer) inline(n ast.Node) string {
157	var b strings.Builder
158	for c := n.FirstChild(); c != nil; c = c.NextSibling() {
159		switch c := c.(type) {
160		case *ast.Text:
161			b.Write(c.Segment.Value(r.src))
162			switch {
163			case c.HardLineBreak():
164				b.WriteString("\n")
165			case c.SoftLineBreak():
166				b.WriteString(" ")
167			}
168		case *ast.String:
169			b.Write(c.Value)
170		case *ast.CodeSpan:
171			b.WriteString(r.inline(c))
172		case *ast.Emphasis:
173			sgr := sgrUnderline
174			if c.Level == 2 {
175				sgr = sgrBold
176			}
177			b.WriteString(r.w.paint(sgr, r.inline(c)))
178		case *ast.Link:
179			b.WriteString(r.w.link(r.inline(c), string(c.Destination)))
180		case *ast.AutoLink:
181			u := string(c.URL(r.src))
182			b.WriteString(r.w.link(u, u))
183		case *ast.Image:
184			b.WriteString("[image: " + r.inline(c) + "]")
185		case *ast.RawHTML:
186		case *east.TaskCheckBox:
187			if c.IsChecked {
188				b.WriteString("[x] ")
189			} else {
190				b.WriteString("[ ] ")
191			}
192		default:
193			b.WriteString(r.inline(c))
194		}
195	}
196	return b.String()
197}