Migration This is my new public git source viewer. Source coming soon. Please contact with concerns/bugs. Contact me

internal/modules/markdown.go

d658fcc4ea9f3bb562fd00dce8f863934560c56c · 2.7 KB · 83 lines raw

1 package modules
2
3 import (
4 "bytes"
5 "html/template"
6 "path"
7 "strings"
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 )
15
16 // The options here mirror Hugo's goldmark defaults on purpose: GFM,
17 // typographer, and automatic heading IDs. Same renderer, same extensions,
18 // so a README renders identically to the same file on your blog.
19 //
20 // WithUnsafe is deliberately NOT set -- also Hugo's default. Anyone who can
21 // push can write a README, so raw HTML in markdown is untrusted input;
22 // goldmark drops it rather than passing a <script> through into the page.
23 var md = goldmark.New(
24 goldmark.WithExtensions(extension.GFM, extension.Typographer),
25 goldmark.WithParserOptions(parser.WithAutoHeadingID()),
26 )
27
28 // Markdown renders README source.
29 //
30 // base — "/slug", the repository root URL
31 // ref — the ref being viewed, for building links
32 // dir — directory the source file lives in, for resolving relative links
33 //
34 // Swap this variable to change engines; nothing else imports goldmark.
35 var Markdown = func(src []byte, base, ref, dir string) (template.HTML, error) {
36 doc := md.Parser().Parse(text.NewReader(src))
37 rewriteLinks(doc, base, ref, dir)
38
39 var buf bytes.Buffer
40 if err := md.Renderer().Render(&buf, src, doc); err != nil {
41 return "", err
42 }
43 return template.HTML(buf.String()), nil
44 }
45
46 // rewriteLinks points relative markdown links at the browser instead of
47 // leaving them dangling. A link to docs/design.md becomes the blob view;
48 // an image becomes /raw/, because the blob view is an HTML page and an
49 // <img> needs actual bytes.
50 func rewriteLinks(doc ast.Node, base, ref, dir string) {
51 ast.Walk(doc, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
52 if !entering {
53 return ast.WalkContinue, nil
54 }
55 switch v := n.(type) {
56 case *ast.Link:
57 if p, ok := resolve(string(v.Destination), dir); ok {
58 v.Destination = []byte(base + "/blob/" + ref + "/" + p)
59 }
60 case *ast.Image:
61 if p, ok := resolve(string(v.Destination), dir); ok {
62 v.Destination = []byte(base + "/raw/" + ref + "/" + p)
63 }
64 }
65 return ast.WalkContinue, nil
66 })
67 }
68
69 // resolve reports whether dest is repo-relative, and if so its path from the
70 // repo root. Absolute URLs, protocol-relative URLs, site-absolute paths and
71 // bare fragments are left alone.
72 func resolve(dest, dir string) (string, bool) {
73 if dest == "" || strings.HasPrefix(dest, "#") || strings.HasPrefix(dest, "/") ||
74 strings.HasPrefix(dest, "//") || strings.Contains(dest, "://") ||
75 strings.HasPrefix(dest, "mailto:") {
76 return "", false
77 }
78 p := path.Clean(path.Join(dir, dest))
79 if p == "." || strings.HasPrefix(p, "..") {
80 return "", false // escapes the repo root
81 }
82 return p, true
83 }