this repo has no description
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/microcosm-cc/bluemonday"
10 "github.com/yuin/goldmark"
11 "github.com/yuin/goldmark/ast"
12 "github.com/yuin/goldmark/extension"
13 "github.com/yuin/goldmark/parser"
14 "github.com/yuin/goldmark/renderer/html"
15 "github.com/yuin/goldmark/text"
16 "github.com/yuin/goldmark/util"
17
18 "tangled.sh/tangled.sh/core/appview/pages/repoinfo"
19)
20
21// RendererType defines the type of renderer to use based on context
22type RendererType int
23
24const (
25 // RendererTypeRepoMarkdown is for repository documentation markdown files
26 RendererTypeRepoMarkdown RendererType = iota
27 // RendererTypeDefault is non-repo markdown, like issues/pulls/comments.
28 RendererTypeDefault
29)
30
31// RenderContext holds the contextual data for rendering markdown.
32// It can be initialized empty, and that'll skip any transformations.
33type RenderContext struct {
34 CamoUrl string
35 CamoSecret string
36 repoinfo.RepoInfo
37 IsDev bool
38 RendererType RendererType
39}
40
41func (rctx *RenderContext) RenderMarkdown(source string) string {
42 md := goldmark.New(
43 goldmark.WithExtensions(extension.GFM),
44 goldmark.WithParserOptions(
45 parser.WithAutoHeadingID(),
46 ),
47 goldmark.WithRendererOptions(html.WithUnsafe()),
48 )
49
50 if rctx != nil {
51 var transformers []util.PrioritizedValue
52
53 transformers = append(transformers, util.Prioritized(&MarkdownTransformer{rctx: rctx}, 10000))
54
55 md.Parser().AddOptions(
56 parser.WithASTTransformers(transformers...),
57 )
58 }
59
60 var buf bytes.Buffer
61 if err := md.Convert([]byte(source), &buf); err != nil {
62 return source
63 }
64 return buf.String()
65}
66
67func (rctx *RenderContext) Sanitize(html string) string {
68 policy := bluemonday.UGCPolicy()
69 policy.AllowAttrs("align", "style").Globally()
70 policy.AllowStyles(
71 "margin",
72 "padding",
73 "text-align",
74 "font-weight",
75 "text-decoration",
76 "padding-left",
77 "padding-right",
78 "padding-top",
79 "padding-bottom",
80 "margin-left",
81 "margin-right",
82 "margin-top",
83 "margin-bottom",
84 )
85 return policy.Sanitize(html)
86}
87
88type MarkdownTransformer struct {
89 rctx *RenderContext
90}
91
92func (a *MarkdownTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
93 _ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
94 if !entering {
95 return ast.WalkContinue, nil
96 }
97
98 switch a.rctx.RendererType {
99 case RendererTypeRepoMarkdown:
100 switch n := n.(type) {
101 case *ast.Link:
102 a.rctx.relativeLinkTransformer(n)
103 case *ast.Image:
104 a.rctx.imageFromKnotTransformer(n)
105 a.rctx.camoImageLinkTransformer(n)
106 }
107 case RendererTypeDefault:
108 switch n := n.(type) {
109 case *ast.Image:
110 a.rctx.imageFromKnotTransformer(n)
111 a.rctx.camoImageLinkTransformer(n)
112 }
113 }
114
115 return ast.WalkContinue, nil
116 })
117}
118
119func (rctx *RenderContext) relativeLinkTransformer(link *ast.Link) {
120
121 dst := string(link.Destination)
122
123 if isAbsoluteUrl(dst) {
124 return
125 }
126
127 actualPath := rctx.actualPath(dst)
128
129 newPath := path.Join("/", rctx.RepoInfo.FullName(), "tree", rctx.RepoInfo.Ref, actualPath)
130 link.Destination = []byte(newPath)
131}
132
133func (rctx *RenderContext) imageFromKnotTransformer(img *ast.Image) {
134 dst := string(img.Destination)
135
136 if isAbsoluteUrl(dst) {
137 return
138 }
139
140 scheme := "https"
141 if rctx.IsDev {
142 scheme = "http"
143 }
144
145 actualPath := rctx.actualPath(dst)
146
147 parsedURL := &url.URL{
148 Scheme: scheme,
149 Host: rctx.Knot,
150 Path: path.Join("/",
151 rctx.RepoInfo.OwnerDid,
152 rctx.RepoInfo.Name,
153 "raw",
154 url.PathEscape(rctx.RepoInfo.Ref),
155 actualPath),
156 }
157 newPath := parsedURL.String()
158 img.Destination = []byte(newPath)
159}
160
161// actualPath decides when to join the file path with the
162// current repository directory (essentially only when the link
163// destination is relative. if it's absolute then we assume the
164// user knows what they're doing.)
165func (rctx *RenderContext) actualPath(dst string) string {
166 if path.IsAbs(dst) {
167 return dst
168 }
169
170 return path.Join(rctx.CurrentDir, dst)
171}
172
173func isAbsoluteUrl(link string) bool {
174 parsed, err := url.Parse(link)
175 if err != nil {
176 return false
177 }
178 return parsed.IsAbs()
179}