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