table_links.go

  1package ansi
  2
  3import (
  4	"bytes"
  5	"fmt"
  6	"io"
  7	"net/url"
  8	"slices"
  9	"strconv"
 10	"strings"
 11
 12	"github.com/charmbracelet/glamour/v2/internal/autolink"
 13	xansi "github.com/charmbracelet/x/ansi"
 14	"github.com/charmbracelet/x/exp/slice"
 15	"github.com/yuin/goldmark/ast"
 16	astext "github.com/yuin/goldmark/extension/ast"
 17)
 18
 19type tableLink struct {
 20	href     string
 21	title    string
 22	content  string
 23	linkType linkType
 24}
 25
 26type linkType int
 27
 28const (
 29	_ linkType = iota
 30	linkTypeAuto
 31	linkTypeImage
 32	linkTypeRegular
 33)
 34
 35func (e *TableElement) printTableLinks(ctx RenderContext) {
 36	if !e.shouldPrintTableLinks(ctx) {
 37		return
 38	}
 39
 40	w := ctx.blockStack.Current().Block
 41	termWidth := int(ctx.blockStack.Width(ctx)) //nolint: gosec
 42
 43	renderLinkText := func(link tableLink, position, padding int) string {
 44		token := strings.Repeat(" ", padding)
 45		style := ctx.options.Styles.LinkText
 46
 47		switch link.linkType {
 48		case linkTypeAuto, linkTypeRegular:
 49			token += fmt.Sprintf("[%d]: %s", position, link.content)
 50		case linkTypeImage:
 51			token += link.content
 52			style = ctx.options.Styles.ImageText
 53			style.Prefix = fmt.Sprintf("[%d]: %s", position, style.Prefix)
 54		}
 55
 56		var b bytes.Buffer
 57		el := &BaseElement{Token: token, Style: style}
 58		_ = el.Render(io.MultiWriter(w, &b), ctx)
 59		return b.String()
 60	}
 61
 62	renderLinkHref := func(link tableLink, linkText string) {
 63		hyperlink, resetHyperlink, _ := makeHyperlink(link.href)
 64
 65		style := ctx.options.Styles.Link
 66		if link.linkType == linkTypeImage {
 67			style = ctx.options.Styles.Image
 68		}
 69
 70		linkMaxWidth := max(termWidth-xansi.StringWidth(linkText)-1, 0)
 71		token := hyperlink + xansi.Truncate(link.href, linkMaxWidth, "…") + resetHyperlink
 72
 73		el := &BaseElement{Token: token, Style: style}
 74		_ = el.Render(w, ctx)
 75	}
 76
 77	renderString := func(str string) {
 78		_, _ = renderText(w, ctx.blockStack.Current().Style.StylePrimitive, str)
 79	}
 80
 81	paddingFor := func(total, position int) int {
 82		totalSize := len(strconv.Itoa(total))
 83		positionSize := len(strconv.Itoa(position))
 84
 85		return max(totalSize-positionSize, 0)
 86	}
 87
 88	renderList := func(list []tableLink) {
 89		for i, item := range list {
 90			position := i + 1
 91			padding := paddingFor(len(list), position)
 92
 93			renderString("\n")
 94			linkText := renderLinkText(item, position, padding)
 95			renderString(" ")
 96			renderLinkHref(item, linkText)
 97		}
 98	}
 99
100	if len(ctx.table.tableLinks) > 0 {
101		renderString("\n")
102	}
103	renderList(ctx.table.tableLinks)
104
105	if len(ctx.table.tableImages) > 0 {
106		renderString("\n")
107	}
108	renderList(ctx.table.tableImages)
109}
110
111func (e *TableElement) shouldPrintTableLinks(ctx RenderContext) bool {
112	if ctx.options.InlineTableLinks {
113		return false
114	}
115	if len(ctx.table.tableLinks) == 0 && len(ctx.table.tableImages) == 0 {
116		return false
117	}
118	return true
119}
120
121func (e *TableElement) collectLinksAndImages(ctx RenderContext) error {
122	images := make([]tableLink, 0)
123	links := make([]tableLink, 0)
124
125	err := ast.Walk(e.table, func(node ast.Node, entering bool) (ast.WalkStatus, error) {
126		if !entering {
127			return ast.WalkContinue, nil
128		}
129
130		switch n := node.(type) {
131		case *ast.AutoLink:
132			uri := string(n.URL(e.source))
133			autoLink := tableLink{
134				href:     uri,
135				content:  linkDomain(uri),
136				linkType: linkTypeAuto,
137			}
138			if shortned, ok := autolink.Detect(uri); ok {
139				autoLink.content = shortned
140			}
141			links = append(links, autoLink)
142		case *ast.Image:
143			content, err := nodeContent(node, e.source)
144			if err != nil {
145				return ast.WalkStop, err
146			}
147			image := tableLink{
148				href:     string(n.Destination),
149				title:    string(n.Title),
150				content:  string(content),
151				linkType: linkTypeImage,
152			}
153			if image.content == "" {
154				image.content = linkDomain(image.href)
155			}
156			images = append(images, image)
157		case *ast.Link:
158			content, err := nodeContent(node, e.source)
159			if err != nil {
160				return ast.WalkStop, err
161			}
162			link := tableLink{
163				href:     string(n.Destination),
164				title:    string(n.Title),
165				content:  string(content),
166				linkType: linkTypeRegular,
167			}
168			links = append(links, link)
169		}
170
171		return ast.WalkContinue, nil
172	})
173	if err != nil {
174		return fmt.Errorf("glamour: error collecting links: %w", err)
175	}
176
177	ctx.table.tableImages = slice.Uniq(images)
178	ctx.table.tableLinks = slice.Uniq(links)
179	return nil
180}
181
182func isInsideTable(node ast.Node) bool {
183	parent := node.Parent()
184	for parent != nil {
185		switch parent.Kind() {
186		case astext.KindTable, astext.KindTableHeader, astext.KindTableRow, astext.KindTableCell:
187			return true
188		default:
189			parent = parent.Parent()
190		}
191	}
192	return false
193}
194
195func nodeContent(node ast.Node, source []byte) ([]byte, error) {
196	var builder bytes.Buffer
197
198	var traverse func(node ast.Node) error
199	traverse = func(node ast.Node) error {
200		for n := node.FirstChild(); n != nil; n = n.NextSibling() {
201			switch nn := n.(type) {
202			case *ast.Text:
203				if _, err := builder.Write(nn.Segment.Value(source)); err != nil {
204					return fmt.Errorf("glamour: error writing text node: %w", err)
205				}
206			default:
207				if err := traverse(nn); err != nil {
208					return err
209				}
210			}
211		}
212		return nil
213	}
214	if err := traverse(node); err != nil {
215		return nil, err
216	}
217
218	return builder.Bytes(), nil
219}
220
221func linkDomain(href string) string {
222	if uri, err := url.Parse(href); err == nil {
223		return uri.Hostname()
224	}
225	return "link"
226}
227
228func linkWithSuffix(tl tableLink, list []tableLink) string {
229	index := slices.Index(list, tl)
230	if index == -1 {
231		return tl.content
232	}
233	return fmt.Sprintf("%s[%d]", tl.content, index+1)
234}