okami.mom/glamour

git clone https://git.nkp.pet/okami.mom/glamour

nekomimi.petadd WithExtensions and WithElement so callers can parse and render their own nodes4046973

main
5.0 KiB177 linesraw
1package ansi
2
3import (
4	"fmt"
5	"io"
6	"net/url"
7	"strings"
8
9	east "github.com/yuin/goldmark-emoji/ast"
10	"github.com/yuin/goldmark/ast"
11	astext "github.com/yuin/goldmark/extension/ast"
12	"github.com/yuin/goldmark/renderer"
13	"github.com/yuin/goldmark/util"
14)
15
16// Options is used to configure an ANSIRenderer.
17type Options struct {
18	BaseURL          string
19	WordWrap         int
20	TableWrap        *bool
21	InlineTableLinks bool
22	PreserveNewLines bool
23	Styles           StyleConfig
24	ChromaFormatter  string
25	// Elements renders node kinds the caller adds through goldmark
26	// extensions, or replaces how a built-in kind renders.
27	Elements map[ast.NodeKind]ElementFunc
28}
29
30// ElementFunc builds the Element for one node. Children it renders itself
31// must not also be walked: give them no ElementFunc of their own.
32type ElementFunc func(node ast.Node, source []byte, ctx RenderContext) Element
33
34// ANSIRenderer renders markdown content as ANSI escaped sequences.
35type ANSIRenderer struct { //nolint: revive
36	context RenderContext
37}
38
39// NewRenderer returns a new ANSIRenderer with style and options set.
40func NewRenderer(options Options) *ANSIRenderer {
41	return &ANSIRenderer{
42		context: NewRenderContext(options),
43	}
44}
45
46// RegisterFuncs implements NodeRenderer.RegisterFuncs.
47func (r *ANSIRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
48	// blocks
49	reg.Register(ast.KindDocument, r.renderNode)
50	reg.Register(ast.KindHeading, r.renderNode)
51	reg.Register(ast.KindBlockquote, r.renderNode)
52	reg.Register(ast.KindCodeBlock, r.renderNode)
53	reg.Register(ast.KindFencedCodeBlock, r.renderNode)
54	reg.Register(ast.KindHTMLBlock, r.renderNode)
55	reg.Register(ast.KindList, r.renderNode)
56	reg.Register(ast.KindListItem, r.renderNode)
57	reg.Register(ast.KindParagraph, r.renderNode)
58	reg.Register(ast.KindTextBlock, r.renderNode)
59	reg.Register(ast.KindThematicBreak, r.renderNode)
60
61	// inlines
62	reg.Register(ast.KindAutoLink, r.renderNode)
63	reg.Register(ast.KindCodeSpan, r.renderNode)
64	reg.Register(ast.KindEmphasis, r.renderNode)
65	reg.Register(ast.KindImage, r.renderNode)
66	reg.Register(ast.KindLink, r.renderNode)
67	reg.Register(ast.KindRawHTML, r.renderNode)
68	reg.Register(ast.KindText, r.renderNode)
69	reg.Register(ast.KindString, r.renderNode)
70
71	// tables
72	reg.Register(astext.KindTable, r.renderNode)
73	reg.Register(astext.KindTableHeader, r.renderNode)
74	reg.Register(astext.KindTableRow, r.renderNode)
75	reg.Register(astext.KindTableCell, r.renderNode)
76
77	// definitions
78	reg.Register(astext.KindDefinitionList, r.renderNode)
79	reg.Register(astext.KindDefinitionTerm, r.renderNode)
80	reg.Register(astext.KindDefinitionDescription, r.renderNode)
81
82	// footnotes
83	reg.Register(astext.KindFootnote, r.renderNode)
84	reg.Register(astext.KindFootnoteList, r.renderNode)
85	reg.Register(astext.KindFootnoteLink, r.renderNode)
86	reg.Register(astext.KindFootnoteBacklink, r.renderNode)
87
88	// checkboxes
89	reg.Register(astext.KindTaskCheckBox, r.renderNode)
90
91	// strikethrough
92	reg.Register(astext.KindStrikethrough, r.renderNode)
93
94	// emoji
95	reg.Register(east.KindEmoji, r.renderNode)
96
97	for kind := range r.context.options.Elements {
98		reg.Register(kind, r.renderNode)
99	}
100}
101
102func (r *ANSIRenderer) renderNode(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
103	writeTo := io.Writer(w)
104	bs := r.context.blockStack
105
106	// children get rendered by their parent
107	if isChild(node) {
108		return ast.WalkContinue, nil
109	}
110
111	e := r.NewElement(node, source)
112	if entering { //nolint: nestif
113		// everything below the Document element gets rendered into a block buffer
114		if bs.Len() > 0 {
115			writeTo = io.Writer(bs.Current().Block)
116		}
117
118		_, _ = io.WriteString(writeTo, e.Entering)
119		if e.Renderer != nil {
120			err := e.Renderer.Render(writeTo, r.context)
121			if err != nil {
122				return ast.WalkStop, fmt.Errorf("glamour: error rendering: %w", err)
123			}
124		}
125	} else {
126		// everything below the Document element gets rendered into a block buffer
127		if bs.Len() > 0 {
128			writeTo = io.Writer(bs.Parent().Block)
129		}
130
131		// if we're finished rendering the entire document,
132		// flush to the real writer
133		if node.Type() == ast.TypeDocument {
134			writeTo = w
135		}
136
137		if e.Finisher != nil {
138			err := e.Finisher.Finish(writeTo, r.context)
139			if err != nil {
140				return ast.WalkStop, fmt.Errorf("glamour: error finishing render: %w", err)
141			}
142		}
143
144		_, _ = io.WriteString(bs.Current().Block, e.Exiting)
145	}
146
147	return ast.WalkContinue, nil
148}
149
150func isChild(node ast.Node) bool {
151	for n := node.Parent(); n != nil; n = n.Parent() {
152		// These types are already rendered by their parent
153		switch n.Kind() {
154		case ast.KindCodeSpan, ast.KindAutoLink, ast.KindLink, ast.KindImage, ast.KindEmphasis, astext.KindStrikethrough, astext.KindTableCell:
155			return true
156		}
157	}
158
159	return false
160}
161
162func resolveRelativeURL(baseURL string, rel string) string {
163	u, err := url.Parse(rel)
164	if err != nil {
165		return rel
166	}
167	if u.IsAbs() {
168		return rel
169	}
170	u.Path = strings.TrimPrefix(u.Path, "/")
171
172	base, err := url.Parse(baseURL)
173	if err != nil {
174		return rel
175	}
176	return base.ResolveReference(u).String()
177}