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/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}