elements.go

  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}