forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
at enable-html 3.4 kB view raw
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}