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}