1// Package markup is an umbrella package for all markups and their renderers.
2package markup
3
4import (
5 "bytes"
6 "net/url"
7 "path"
8
9 "github.com/yuin/goldmark"
10 "github.com/yuin/goldmark/ast"
11 "github.com/yuin/goldmark/extension"
12 "github.com/yuin/goldmark/parser"
13 "github.com/yuin/goldmark/text"
14 "github.com/yuin/goldmark/util"
15 "tangled.sh/tangled.sh/core/appview/pages/repoinfo"
16)
17
18// RendererType defines the type of renderer to use based on context
19type RendererType int
20
21const (
22 // RendererTypeRepoMarkdown is for repository documentation markdown files
23 RendererTypeRepoMarkdown RendererType = iota
24 // RendererTypeDefault is non-repo markdown, like issues/pulls/comments.
25 RendererTypeDefault
26)
27
28// RenderContext holds the contextual data for rendering markdown.
29// It can be initialized empty, and that'll skip any transformations.
30type RenderContext struct {
31 CamoUrl string
32 CamoSecret string
33 repoinfo.RepoInfo
34 IsDev bool
35 RendererType RendererType
36}
37
38func (rctx *RenderContext) RenderMarkdown(source string) string {
39 md := goldmark.New(
40 goldmark.WithExtensions(extension.GFM),
41 goldmark.WithParserOptions(
42 parser.WithAutoHeadingID(),
43 ),
44 )
45
46 if rctx != nil {
47 var transformers []util.PrioritizedValue
48
49 transformers = append(transformers, util.Prioritized(&MarkdownTransformer{rctx: rctx}, 10000))
50
51 md.Parser().AddOptions(
52 parser.WithASTTransformers(transformers...),
53 )
54 }
55
56 var buf bytes.Buffer
57 if err := md.Convert([]byte(source), &buf); err != nil {
58 return source
59 }
60 return buf.String()
61}
62
63type MarkdownTransformer struct {
64 rctx *RenderContext
65}
66
67func (a *MarkdownTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
68 _ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
69 if !entering {
70 return ast.WalkContinue, nil
71 }
72
73 switch a.rctx.RendererType {
74 case RendererTypeRepoMarkdown:
75 switch n.(type) {
76 case *ast.Link:
77 a.rctx.relativeLinkTransformer(n.(*ast.Link))
78 case *ast.Image:
79 a.rctx.imageFromKnotTransformer(n.(*ast.Image))
80 a.rctx.camoImageLinkTransformer(n.(*ast.Image))
81 }
82
83 case RendererTypeDefault:
84 switch n.(type) {
85 case *ast.Image:
86 a.rctx.imageFromKnotTransformer(n.(*ast.Image))
87 a.rctx.camoImageLinkTransformer(n.(*ast.Image))
88 }
89 }
90
91 return ast.WalkContinue, nil
92 })
93}
94
95func (rctx *RenderContext) relativeLinkTransformer(link *ast.Link) {
96 dst := string(link.Destination)
97
98 if isAbsoluteUrl(dst) {
99 return
100 }
101
102 newPath := path.Join("/", rctx.RepoInfo.FullName(), "tree", rctx.RepoInfo.Ref, dst)
103 link.Destination = []byte(newPath)
104}
105
106func (rctx *RenderContext) imageFromKnotTransformer(img *ast.Image) {
107 dst := string(img.Destination)
108
109 if isAbsoluteUrl(dst) {
110 return
111 }
112
113 // strip leading './'
114 if len(dst) >= 2 && dst[0:2] == "./" {
115 dst = dst[2:]
116 }
117
118 scheme := "https"
119 if rctx.IsDev {
120 scheme = "http"
121 }
122 parsedURL := &url.URL{
123 Scheme: scheme,
124 Host: rctx.Knot,
125 Path: path.Join("/",
126 rctx.RepoInfo.OwnerDid,
127 rctx.RepoInfo.Name,
128 "raw",
129 url.PathEscape(rctx.RepoInfo.Ref),
130 dst),
131 }
132 newPath := parsedURL.String()
133 img.Destination = []byte(newPath)
134}
135
136func isAbsoluteUrl(link string) bool {
137 parsed, err := url.Parse(link)
138 if err != nil {
139 return false
140 }
141 return parsed.IsAbs()
142}