1package ansi
2
3import (
4 "bytes"
5 "fmt"
6 "html"
7 "io"
8 "strings"
9
10 "github.com/charmbracelet/glamour/v2/internal/autolink"
11 east "github.com/yuin/goldmark-emoji/ast"
12 "github.com/yuin/goldmark/ast"
13 astext "github.com/yuin/goldmark/extension/ast"
14)
15
16// ElementRenderer is called when entering a markdown node.
17type ElementRenderer interface {
18 Render(w io.Writer, ctx RenderContext) error
19}
20
21// StyleOverriderElementRenderer is called when entering a markdown node with a specific style.
22type StyleOverriderElementRenderer interface {
23 StyleOverrideRender(w io.Writer, ctx RenderContext, style StylePrimitive) error
24}
25
26// ElementFinisher is called when leaving a markdown node.
27type ElementFinisher interface {
28 Finish(w io.Writer, ctx RenderContext) error
29}
30
31// An Element is used to instruct the renderer how to handle individual markdown
32// nodes.
33type Element struct {
34 Entering string
35 Exiting string
36 Renderer ElementRenderer
37 Finisher ElementFinisher
38}
39
40// NewElement returns the appropriate render Element for a given node.
41func (tr *ANSIRenderer) NewElement(node ast.Node, source []byte) Element {
42 ctx := tr.context
43
44 switch node.Kind() {
45 // Document
46 case ast.KindDocument:
47 e := &BlockElement{
48 Block: &bytes.Buffer{},
49 Style: ctx.options.Styles.Document,
50 Margin: true,
51 }
52 return Element{
53 Renderer: e,
54 Finisher: e,
55 }
56
57 // Heading
58 case ast.KindHeading:
59 n := node.(*ast.Heading)
60 he := &HeadingElement{
61 Level: n.Level,
62 First: node.PreviousSibling() == nil,
63 }
64 return Element{
65 Exiting: "",
66 Renderer: he,
67 Finisher: he,
68 }
69
70 // Paragraph
71 case ast.KindParagraph:
72 if node.Parent() != nil {
73 kind := node.Parent().Kind()
74 if kind == ast.KindListItem {
75 return Element{}
76 }
77 }
78 return Element{
79 Renderer: &ParagraphElement{
80 First: node.PreviousSibling() == nil,
81 },
82 Finisher: &ParagraphElement{},
83 }
84
85 // Blockquote
86 case ast.KindBlockquote:
87 e := &BlockElement{
88 Block: &bytes.Buffer{},
89 Style: cascadeStyle(ctx.blockStack.Current().Style, ctx.options.Styles.BlockQuote, false),
90 Margin: true,
91 }
92 return Element{
93 Entering: "\n",
94 Renderer: e,
95 Finisher: e,
96 }
97
98 // Lists
99 case ast.KindList:
100 s := ctx.options.Styles.List.StyleBlock
101 if s.Indent == nil {
102 var i uint
103 s.Indent = &i
104 }
105 n := node.Parent()
106 for n != nil {
107 if n.Kind() == ast.KindList {
108 i := ctx.options.Styles.List.LevelIndent
109 s.Indent = &i
110 break
111 }
112 n = n.Parent()
113 }
114
115 e := &BlockElement{
116 Block: &bytes.Buffer{},
117 Style: cascadeStyle(ctx.blockStack.Current().Style, s, false),
118 Margin: true,
119 Newline: true,
120 }
121 return Element{
122 Entering: "\n",
123 Renderer: e,
124 Finisher: e,
125 }
126
127 case ast.KindListItem:
128 var l uint
129 var e uint
130 l = 1
131 n := node
132 for n.PreviousSibling() != nil && (n.PreviousSibling().Kind() == ast.KindListItem) {
133 l++
134 n = n.PreviousSibling()
135 }
136 if node.Parent().(*ast.List).IsOrdered() {
137 e = l
138 if node.Parent().(*ast.List).Start != 1 {
139 e += uint(node.Parent().(*ast.List).Start) - 1 //nolint: gosec
140 }
141 }
142
143 post := "\n"
144 if (node.LastChild() != nil && node.LastChild().Kind() == ast.KindList) ||
145 node.NextSibling() == nil {
146 post = ""
147 }
148
149 if node.FirstChild() != nil &&
150 node.FirstChild().FirstChild() != nil &&
151 node.FirstChild().FirstChild().Kind() == astext.KindTaskCheckBox {
152 nc := node.FirstChild().FirstChild().(*astext.TaskCheckBox)
153
154 return Element{
155 Exiting: post,
156 Renderer: &TaskElement{
157 Checked: nc.IsChecked,
158 },
159 }
160 }
161
162 return Element{
163 Exiting: post,
164 Renderer: &ItemElement{
165 IsOrdered: node.Parent().(*ast.List).IsOrdered(),
166 Enumeration: e,
167 },
168 }
169
170 // Text Elements
171 case ast.KindText:
172 n := node.(*ast.Text)
173 s := string(n.Segment.Value(source))
174
175 if n.HardLineBreak() || (n.SoftLineBreak()) {
176 s += "\n"
177 }
178 return Element{
179 Renderer: &BaseElement{
180 Token: html.UnescapeString(s),
181 Style: ctx.options.Styles.Text,
182 },
183 }
184
185 case ast.KindEmphasis:
186 n := node.(*ast.Emphasis)
187 var children []ElementRenderer
188 nn := n.FirstChild()
189 for nn != nil {
190 children = append(children, tr.NewElement(nn, source).Renderer)
191 nn = nn.NextSibling()
192 }
193 return Element{
194 Renderer: &EmphasisElement{
195 Level: n.Level,
196 Children: children,
197 },
198 }
199
200 case astext.KindStrikethrough:
201 n := node.(*astext.Strikethrough)
202 s := string(n.Text(source)) //nolint: staticcheck
203 style := ctx.options.Styles.Strikethrough
204
205 return Element{
206 Renderer: &BaseElement{
207 Token: html.UnescapeString(s),
208 Style: style,
209 },
210 }
211
212 case ast.KindThematicBreak:
213 return Element{
214 Entering: "",
215 Exiting: "",
216 Renderer: &BaseElement{
217 Style: ctx.options.Styles.HorizontalRule,
218 },
219 }
220
221 // Links
222 case ast.KindLink:
223 n := node.(*ast.Link)
224 isFooterLinks := !ctx.options.InlineTableLinks && isInsideTable(node)
225
226 var children []ElementRenderer
227 content, err := nodeContent(node, source)
228
229 if isFooterLinks && err == nil {
230 text := string(content)
231 tl := tableLink{
232 content: text,
233 href: string(n.Destination),
234 title: string(n.Title),
235 linkType: linkTypeRegular,
236 }
237 text = linkWithSuffix(tl, ctx.table.tableLinks)
238 children = []ElementRenderer{&BaseElement{Token: text}}
239 } else {
240 nn := n.FirstChild()
241 for nn != nil {
242 children = append(children, tr.NewElement(nn, source).Renderer)
243 nn = nn.NextSibling()
244 }
245 }
246
247 return Element{
248 Renderer: &LinkElement{
249 BaseURL: ctx.options.BaseURL,
250 URL: string(n.Destination),
251 Children: children,
252 SkipHref: isFooterLinks,
253 },
254 }
255 case ast.KindAutoLink:
256 n := node.(*ast.AutoLink)
257 u := string(n.URL(source))
258 isFooterLinks := !ctx.options.InlineTableLinks && isInsideTable(node)
259
260 var children []ElementRenderer
261 nn := n.FirstChild()
262 for nn != nil {
263 children = append(children, tr.NewElement(nn, source).Renderer)
264 nn = nn.NextSibling()
265 }
266
267 if len(children) == 0 {
268 children = append(children, &BaseElement{Token: u})
269 }
270
271 if n.AutoLinkType == ast.AutoLinkEmail && !strings.HasPrefix(strings.ToLower(u), "mailto:") {
272 u = "mailto:" + u
273 }
274
275 var renderer ElementRenderer
276 if isFooterLinks {
277 domain := linkDomain(u)
278 tl := tableLink{
279 content: domain,
280 href: u,
281 linkType: linkTypeAuto,
282 }
283 if shortned, ok := autolink.Detect(u); ok {
284 tl.content = shortned
285 }
286 text := linkWithSuffix(tl, ctx.table.tableLinks)
287
288 renderer = &LinkElement{
289 Children: []ElementRenderer{&BaseElement{Token: text}},
290 URL: u,
291 SkipHref: true,
292 }
293 } else {
294 renderer = &LinkElement{
295 Children: children,
296 URL: u,
297 SkipText: n.AutoLinkType != ast.AutoLinkEmail,
298 }
299 }
300 return Element{Renderer: renderer}
301
302 // Images
303 case ast.KindImage:
304 n := node.(*ast.Image)
305 text := string(n.Text(source)) //nolint: staticcheck
306 isFooterLinks := !ctx.options.InlineTableLinks && isInsideTable(node)
307
308 if isFooterLinks {
309 if text == "" {
310 text = linkDomain(string(n.Destination))
311 }
312 tl := tableLink{
313 title: string(n.Title),
314 content: text,
315 href: string(n.Destination),
316 linkType: linkTypeImage,
317 }
318 text = linkWithSuffix(tl, ctx.table.tableImages)
319 }
320
321 return Element{
322 Renderer: &ImageElement{
323 Text: text,
324 BaseURL: ctx.options.BaseURL,
325 URL: string(n.Destination),
326 TextOnly: isFooterLinks,
327 },
328 }
329
330 // Code
331 case ast.KindFencedCodeBlock:
332 n := node.(*ast.FencedCodeBlock)
333 l := n.Lines().Len()
334 s := ""
335 for i := 0; i < l; i++ {
336 line := n.Lines().At(i)
337 s += string(line.Value(source))
338 }
339 return Element{
340 Entering: "\n",
341 Renderer: &CodeBlockElement{
342 Code: s,
343 Language: string(n.Language(source)),
344 },
345 }
346
347 case ast.KindCodeBlock:
348 n := node.(*ast.CodeBlock)
349 l := n.Lines().Len()
350 s := ""
351 for i := 0; i < l; i++ {
352 line := n.Lines().At(i)
353 s += string(line.Value(source))
354 }
355 return Element{
356 Entering: "\n",
357 Renderer: &CodeBlockElement{
358 Code: s,
359 },
360 }
361
362 case ast.KindCodeSpan:
363 n := node.(*ast.CodeSpan)
364 s := string(n.Text(source)) //nolint: staticcheck
365 return Element{
366 Renderer: &CodeSpanElement{
367 Text: html.UnescapeString(s),
368 Style: cascadeStyle(ctx.blockStack.Current().Style, ctx.options.Styles.Code, false).StylePrimitive,
369 },
370 }
371
372 // Tables
373 case astext.KindTable:
374 table := node.(*astext.Table)
375 te := &TableElement{
376 table: table,
377 source: source,
378 }
379 return Element{
380 Entering: "\n",
381 Exiting: "\n",
382 Renderer: te,
383 Finisher: te,
384 }
385
386 case astext.KindTableCell:
387 n := node.(*astext.TableCell)
388 var children []ElementRenderer
389 nn := n.FirstChild()
390 for nn != nil {
391 children = append(children, tr.NewElement(nn, source).Renderer)
392 nn = nn.NextSibling()
393 }
394
395 r := &TableCellElement{
396 Children: children,
397 Head: node.Parent().Kind() == astext.KindTableHeader,
398 }
399 return Element{
400 Renderer: r,
401 }
402
403 case astext.KindTableHeader:
404 return Element{
405 Finisher: &TableHeadElement{},
406 }
407 case astext.KindTableRow:
408 return Element{
409 Finisher: &TableRowElement{},
410 }
411
412 // HTML Elements
413 case ast.KindHTMLBlock:
414 n := node.(*ast.HTMLBlock)
415 return Element{
416 Renderer: &BaseElement{
417 Token: ctx.SanitizeHTML(string(n.Text(source)), true), //nolint: staticcheck
418 Style: ctx.options.Styles.HTMLBlock.StylePrimitive,
419 },
420 }
421 case ast.KindRawHTML:
422 n := node.(*ast.RawHTML)
423 return Element{
424 Renderer: &BaseElement{
425 Token: ctx.SanitizeHTML(string(n.Text(source)), true), //nolint: staticcheck
426 Style: ctx.options.Styles.HTMLSpan.StylePrimitive,
427 },
428 }
429
430 // Definition Lists
431 case astext.KindDefinitionList:
432 e := &BlockElement{
433 Block: &bytes.Buffer{},
434 Style: cascadeStyle(ctx.blockStack.Current().Style, ctx.options.Styles.DefinitionList, false),
435 Margin: true,
436 Newline: true,
437 }
438 return Element{
439 Renderer: e,
440 Finisher: e,
441 }
442
443 case astext.KindDefinitionTerm:
444 return Element{
445 Entering: "\n",
446 Renderer: &BaseElement{
447 Style: ctx.options.Styles.DefinitionTerm,
448 },
449 }
450
451 case astext.KindDefinitionDescription:
452 return Element{
453 Exiting: "\n",
454 Renderer: &BaseElement{
455 Style: ctx.options.Styles.DefinitionDescription,
456 },
457 }
458
459 // Handled by parents
460 case astext.KindTaskCheckBox:
461 // handled by KindListItem
462 return Element{}
463 case ast.KindTextBlock:
464 return Element{}
465
466 case east.KindEmoji:
467 n := node.(*east.Emoji)
468 return Element{
469 Renderer: &BaseElement{
470 Token: string(n.Value.Unicode),
471 },
472 }
473
474 // Unknown case
475 default:
476 fmt.Println("Warning: unhandled element", node.Kind().String())
477 return Element{}
478 }
479}