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)
25
26// RenderContext holds the contextual data for rendering markdown.
27// It can be initialized empty, and that'll skip any transformations.
28type RenderContext struct {
29 repoinfo.RepoInfo
30 IsDev bool
31 RendererType RendererType
32}
33
34func (rctx *RenderContext) RenderMarkdown(source string) string {
35 md := goldmark.New(
36 goldmark.WithExtensions(extension.GFM),
37 goldmark.WithParserOptions(
38 parser.WithAutoHeadingID(),
39 ),
40 )
41
42 if rctx != nil {
43 var transformers []util.PrioritizedValue
44
45 transformers = append(transformers, util.Prioritized(&MarkdownTransformer{rctx: rctx}, 10000))
46
47 md.Parser().AddOptions(
48 parser.WithASTTransformers(transformers...),
49 )
50 }
51
52 var buf bytes.Buffer
53 if err := md.Convert([]byte(source), &buf); err != nil {
54 return source
55 }
56 return buf.String()
57}
58
59type MarkdownTransformer struct {
60 rctx *RenderContext
61}
62
63func (a *MarkdownTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
64 _ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
65 if !entering {
66 return ast.WalkContinue, nil
67 }
68
69 switch a.rctx.RendererType {
70 case RendererTypeRepoMarkdown:
71 switch n.(type) {
72 case *ast.Link:
73 a.rctx.relativeLinkTransformer(n.(*ast.Link))
74 case *ast.Image:
75 a.rctx.imageFromKnotTransformer(n.(*ast.Image))
76 }
77 // more types here like RendererTypeIssue/Pull etc.
78 }
79
80 return ast.WalkContinue, nil
81 })
82}
83
84func (rctx *RenderContext) relativeLinkTransformer(link *ast.Link) {
85 dst := string(link.Destination)
86
87 if isAbsoluteUrl(dst) {
88 return
89 }
90
91 newPath := path.Join("/", rctx.RepoInfo.FullName(), "tree", rctx.RepoInfo.Ref, dst)
92 link.Destination = []byte(newPath)
93}
94
95func (rctx *RenderContext) imageFromKnotTransformer(img *ast.Image) {
96 dst := string(img.Destination)
97
98 if isAbsoluteUrl(dst) {
99 return
100 }
101
102 // strip leading './'
103 if len(dst) >= 2 && dst[0:2] == "./" {
104 dst = dst[2:]
105 }
106
107 scheme := "https"
108 if rctx.IsDev {
109 scheme = "http"
110 }
111 parsedURL := &url.URL{
112 Scheme: scheme,
113 Host: rctx.Knot,
114 Path: path.Join("/",
115 rctx.RepoInfo.OwnerDid,
116 rctx.RepoInfo.Name,
117 "raw",
118 url.PathEscape(rctx.RepoInfo.Ref),
119 dst),
120 }
121 newPath := parsedURL.String()
122 img.Destination = []byte(newPath)
123}
124
125func isAbsoluteUrl(link string) bool {
126 parsed, err := url.Parse(link)
127 if err != nil {
128 return false
129 }
130 return parsed.IsAbs()
131}