Skip to content

Commit 1a7436b

Browse files
committed
docs: switch markdown renderer [wip]
1 parent 726bab5 commit 1a7436b

File tree

10 files changed

+247
-89
lines changed

10 files changed

+247
-89
lines changed

docs/index.md docs/README.md

File renamed without changes.

docs/docs.go

+159-72
Original file line numberDiff line numberDiff line change
@@ -4,125 +4,212 @@ import (
44
"bytes"
55
"embed"
66
"fmt"
7+
"html/template"
78
"io"
9+
"log"
810
"net/http"
911
"strings"
1012

11-
"github.com/Depado/bfchroma/v2"
12-
"github.com/alecthomas/chroma/v2/formatters/html"
13-
bf "github.com/russross/blackfriday/v2"
13+
toc "github.com/abhinav/goldmark-toc"
14+
chromahtml "github.com/alecthomas/chroma/formatters/html"
15+
"github.com/digineo/texd"
16+
"github.com/microcosm-cc/bluemonday"
17+
"github.com/yuin/goldmark"
18+
highlighting "github.com/yuin/goldmark-highlighting"
19+
"github.com/yuin/goldmark/ast"
20+
"github.com/yuin/goldmark/extension"
21+
"github.com/yuin/goldmark/parser"
22+
"github.com/yuin/goldmark/renderer/html"
23+
"github.com/yuin/goldmark/text"
24+
"github.com/yuin/goldmark/util"
1425
"gopkg.in/yaml.v3"
1526
)
1627

17-
//go:embed docs.yml *.md **/*.md
28+
//go:embed *.md **/*.md
1829
var sources embed.FS
1930

31+
//go:embed docs.yml
32+
var config []byte
33+
34+
//go:embed docs.html
35+
var rawLayout string
36+
var tplLayout = template.Must(template.New("layout").Parse(rawLayout))
37+
2038
type page struct {
2139
Title string
2240
Breadcrumbs []string
23-
Body string
41+
TOC *toc.TOC
42+
CSS []byte
43+
Body []byte
2444
File string
2545
Route string
2646
Children []*page
2747
}
2848

29-
var root = func() page {
30-
structure, err := sources.Open("docs.yml")
31-
if err != nil {
32-
panic(err)
33-
}
34-
defer structure.Close()
49+
type pageRoutes map[string]*page
3550

51+
func getRoutes(urlPrefix string) (pageRoutes, error) {
3652
var menu page
37-
dec := yaml.NewDecoder(structure)
53+
dec := yaml.NewDecoder(bytes.NewReader(config))
3854
dec.KnownFields(true)
3955
if err := dec.Decode(&menu); err != nil {
40-
panic(err)
56+
return nil, err
4157
}
4258

43-
menu.init()
44-
return menu
45-
}()
59+
urlPrefix = strings.TrimSuffix(urlPrefix, "/")
60+
return menu.init(urlPrefix, make(pageRoutes))
61+
}
4662

47-
func (pg *page) init(crumbs ...string) {
63+
func (pg *page) init(urlPrefix string, r pageRoutes, crumbs ...string) (pageRoutes, error) {
4864
if pg.File != "" {
49-
if r := strings.TrimSuffix(pg.File, ".md"); r == "index" {
50-
pg.Route = ""
65+
if r := strings.TrimSuffix(pg.File, ".md"); r == "README" {
66+
pg.Route = urlPrefix
5167
} else {
52-
pg.Route = "/" + r
68+
pg.Route = urlPrefix + "/" + r
69+
}
70+
r[pg.Route] = pg
71+
err := pg.parseFile(urlPrefix)
72+
if err != nil {
73+
return nil, err
5374
}
54-
55-
pg.parseFile()
5675
}
5776
if pg.Title != "" {
58-
pg.Breadcrumbs = append(pg.Breadcrumbs, pg.Title)
77+
pg.Breadcrumbs = append([]string{pg.Title}, crumbs...)
5978
}
6079
for _, child := range pg.Children {
61-
child.init(pg.Breadcrumbs...)
80+
_, err := child.init(urlPrefix, r, pg.Breadcrumbs...)
81+
if err != nil {
82+
return nil, err
83+
}
6284
}
85+
return r, nil
6386
}
6487

65-
func (pg *page) parseFile() {
66-
body, err := sources.ReadFile(pg.File)
67-
if err != nil {
68-
panic(err)
69-
}
88+
type localLinkTransformer struct {
89+
prefix string
90+
}
7091

71-
r := bfchroma.NewRenderer(
72-
bfchroma.WithoutAutodetect(),
73-
bfchroma.ChromaOptions(
74-
html.WithLineNumbers(true),
75-
),
76-
bfchroma.Extend(bf.NewHTMLRenderer(bf.HTMLRendererParameters{
77-
Flags: bf.CommonHTMLFlags & ^bf.UseXHTML & ^bf.CompletePage,
78-
})),
79-
)
80-
parser := bf.New(
81-
bf.WithExtensions(bf.CommonExtensions),
82-
bf.WithRenderer(r),
83-
)
92+
var _ parser.ASTTransformer = (*localLinkTransformer)(nil)
8493

85-
ast := parser.Parse(body)
86-
var buf bytes.Buffer
87-
var inH1 bool
88-
89-
ast.Walk(func(node *bf.Node, entering bool) bf.WalkStatus {
90-
switch node.Type {
91-
case bf.Heading:
92-
inH1 = entering && node.HeadingData.Level == 1 && pg.Title == ""
93-
case bf.Text:
94-
if inH1 {
95-
pg.Title = string(node.Literal)
96-
}
97-
case bf.Link:
98-
if entering && bytes.HasPrefix(node.LinkData.Destination, []byte("./")) {
99-
node.LinkData.Destination = bytes.TrimSuffix(node.LinkData.Destination, []byte(".md"))
94+
func (link *localLinkTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
95+
_ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
96+
if entering && n.Kind() == ast.KindLink {
97+
if l, ok := n.(*ast.Link); ok {
98+
link.rewrite(l)
10099
}
101100
}
102-
return r.RenderNode(&buf, node, entering)
101+
return ast.WalkContinue, nil
103102
})
103+
}
104104

105-
pg.Body = buf.String()
105+
const (
106+
localLinkPrefix = "./"
107+
localLinkSuffix = ".md"
108+
)
109+
110+
func (link *localLinkTransformer) rewrite(l *ast.Link) {
111+
dst := string(l.Destination)
112+
if strings.HasPrefix(dst, localLinkPrefix) && strings.HasSuffix(dst, localLinkSuffix) {
113+
dst = strings.TrimPrefix(dst, localLinkPrefix)
114+
dst = strings.TrimSuffix(dst, localLinkSuffix)
115+
l.Destination = []byte(link.prefix + "/" + dst)
116+
}
106117
}
107118

108-
func (pg *page) Dump(w io.Writer) {
109-
fmt.Fprintf(w, "- %s (%s)\n", pg.Title, pg.Route)
110-
fmt.Fprintln(w, pg.Body)
111-
fmt.Fprintln(w)
119+
var sanitize = func() func(io.Reader) *bytes.Buffer {
120+
p := bluemonday.UGCPolicy()
121+
p.AllowAttrs("class").Globally()
122+
return p.SanitizeReader
123+
}()
112124

113-
for _, c := range pg.Children {
114-
c.Dump(w)
125+
func (pg *page) parseFile(urlPrefix string) error {
126+
raw, err := sources.ReadFile(pg.File)
127+
if err != nil {
128+
return err
115129
}
130+
131+
var css, body bytes.Buffer
132+
md := goldmark.New(
133+
goldmark.WithParserOptions(
134+
parser.WithAutoHeadingID(),
135+
parser.WithASTTransformers(util.PrioritizedValue{
136+
Value: &localLinkTransformer{urlPrefix},
137+
Priority: 999,
138+
}),
139+
),
140+
goldmark.WithRendererOptions(
141+
html.WithUnsafe(),
142+
),
143+
goldmark.WithExtensions(
144+
extension.GFM,
145+
highlighting.NewHighlighting(
146+
highlighting.WithCSSWriter(&css),
147+
highlighting.WithStyle("github"),
148+
highlighting.WithFormatOptions(
149+
chromahtml.WithLineNumbers(true),
150+
chromahtml.WithClasses(true),
151+
),
152+
),
153+
),
154+
)
155+
156+
doc := md.Parser().Parse(text.NewReader(raw))
157+
tree, err := toc.Inspect(doc, raw)
158+
if err != nil {
159+
return err
160+
}
161+
if pg.Title == "" {
162+
if len(tree.Items) > 0 {
163+
pg.Title = string(tree.Items[0].Title)
164+
}
165+
}
166+
if err := md.Renderer().Render(&body, raw, doc); err != nil {
167+
return err
168+
}
169+
pg.TOC = tree
170+
pg.CSS = css.Bytes()
171+
pg.Body = sanitize(&body).Bytes()
172+
return nil
116173
}
117174

118-
func Handler() http.Handler {
175+
func Handler(prefix string) (http.Handler, error) {
176+
type pageVars struct {
177+
Version string
178+
Title string
179+
CSS template.CSS
180+
Content template.HTML
181+
}
182+
183+
routes, err := getRoutes(prefix)
184+
if err != nil {
185+
return nil, fmt.Errorf("failed to build docs: %w", err)
186+
}
187+
119188
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
120-
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
121-
w.Header().Set("X-Content-Type-Options", "nosniff")
122-
w.WriteHeader(http.StatusOK)
189+
pg := routes[r.URL.Path]
190+
if pg == nil {
191+
http.NotFound(w, r)
192+
return
193+
}
123194

124-
fmt.Fprintf(w, "%#v\n\n", r.URL)
195+
var buf bytes.Buffer
196+
err := tplLayout.Execute(&buf, &pageVars{
197+
Version: texd.Version(),
198+
Title: strings.Join(pg.Breadcrumbs, " · "),
199+
CSS: template.CSS(pg.CSS),
200+
Content: template.HTML(pg.Body),
201+
})
202+
203+
if err != nil {
204+
log.Println(err)
205+
code := http.StatusInternalServerError
206+
http.Error(w, http.StatusText(code), code)
207+
return
208+
}
125209

126-
root.Dump(w)
127-
})
210+
w.Header().Set("Content-Type", "text/html; charset=utf-8")
211+
w.Header().Set("X-Content-Type-Options", "nosniff")
212+
w.WriteHeader(http.StatusOK)
213+
_, _ = buf.WriteTo(w)
214+
}), nil
128215
}

docs/docs.html

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7+
<title>{{ .Title }}</title>
8+
9+
<link rel="stylesheet" href="/assets/bootstrap-5.1.3.min.css">
10+
<style>{{ .CSS }}</style>
11+
</head>
12+
13+
<body>
14+
<div id="app" class="pb-5">
15+
<nav class="navbar navbar-light navbar-expand-sm bg-light">
16+
<div class="container-fluid">
17+
<a href="https://github.com/digineo/texd" class="navbar-brand">texd</a>
18+
19+
<ul class="navbar-nav me-auto">
20+
<li class="nav-item">
21+
<a class="nav-link" href="/">Play</a>
22+
</li>
23+
<li class="nav-item">
24+
<a class="nav-link active" href="/docs">Documentation</a>
25+
</li>
26+
</ul>
27+
28+
<span class="navbar-text">
29+
{{ .Version }}
30+
</span>
31+
</div>
32+
</nav>
33+
34+
<div class="container">
35+
{{ .Content }}
36+
</div>
37+
</div>
38+
</body>
39+
</html>

docs/docs.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
file: index.md
2+
file: README.md
33
children:
44
- file: operation-modes.md
55
- file: cli-options.md

go.mod

+7-3
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,32 @@ module github.com/digineo/texd
33
go 1.18
44

55
require (
6-
github.com/Depado/bfchroma/v2 v2.0.0
7-
github.com/alecthomas/chroma/v2 v2.2.0
6+
github.com/abhinav/goldmark-toc v0.2.1
7+
github.com/alecthomas/chroma v0.10.0
88
github.com/bahlo/generic-list-go v0.2.0
99
github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d
1010
github.com/docker/docker v20.10.17+incompatible
1111
github.com/docker/go-units v0.4.0
1212
github.com/gorilla/handlers v1.5.1
1313
github.com/gorilla/mux v1.8.0
14+
github.com/microcosm-cc/bluemonday v1.0.19
1415
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6
1516
github.com/opencontainers/image-spec v1.0.2
1617
github.com/prometheus/client_golang v1.12.2
17-
github.com/russross/blackfriday/v2 v2.1.0
1818
github.com/spf13/afero v1.8.2
1919
github.com/spf13/pflag v1.0.5
2020
github.com/stretchr/testify v1.8.0
2121
github.com/thediveo/enumflag v0.10.1
22+
github.com/yuin/goldmark v1.4.13
23+
github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594
2224
go.uber.org/zap v1.21.0
2325
gopkg.in/yaml.v3 v3.0.1
2426
)
2527

2628
require (
2729
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
2830
github.com/Microsoft/go-winio v0.5.2 // indirect
31+
github.com/aymerick/douceur v0.2.0 // indirect
2932
github.com/beorn7/perks v1.0.1 // indirect
3033
github.com/cespare/xxhash/v2 v2.1.2 // indirect
3134
github.com/davecgh/go-spew v1.1.1 // indirect
@@ -36,6 +39,7 @@ require (
3639
github.com/gogo/protobuf v1.3.2 // indirect
3740
github.com/golang/protobuf v1.5.2 // indirect
3841
github.com/google/go-cmp v0.5.6 // indirect
42+
github.com/gorilla/css v1.0.0 // indirect
3943
github.com/kr/text v0.2.0 // indirect
4044
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
4145
github.com/morikuni/aec v1.0.0 // indirect

0 commit comments

Comments
 (0)