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}