@@ -3,126 +3,162 @@ package docs
3
3
import (
4
4
"bytes"
5
5
"embed"
6
- "fmt "
6
+ "html/template "
7
7
"io"
8
+ "log"
8
9
"net/http"
9
10
"strings"
10
11
11
- "github.com/Depado/bfchroma/v2"
12
- "github.com/alecthomas/chroma/v2/formatters/html"
13
- bf "github.com/russross/blackfriday/v2"
12
+ toc "github.com/abhinav/goldmark-toc"
13
+ chromahtml "github.com/alecthomas/chroma/formatters/html"
14
+ "github.com/digineo/texd"
15
+ "github.com/microcosm-cc/bluemonday"
16
+ "github.com/yuin/goldmark"
17
+ highlighting "github.com/yuin/goldmark-highlighting"
18
+ "github.com/yuin/goldmark/extension"
19
+ "github.com/yuin/goldmark/parser"
20
+ "github.com/yuin/goldmark/renderer/html"
21
+ "github.com/yuin/goldmark/text"
14
22
"gopkg.in/yaml.v3"
15
23
)
16
24
17
- //go:embed docs.yml *.md **/*.md
25
+ //go:embed *.md **/*.md
18
26
var sources embed.FS
19
27
28
+ //go:embed docs.yml
29
+ var config []byte
30
+
31
+ //go:embed docs.html
32
+ var rawLayout string
33
+ var tplLayout = template .Must (template .New ("layout" ).Parse (rawLayout ))
34
+
20
35
type page struct {
21
36
Title string
22
37
Breadcrumbs []string
23
- Body string
38
+ TOC * toc.TOC
39
+ CSS []byte
40
+ Body []byte
24
41
File string
25
42
Route string
26
43
Children []* page
27
44
}
45
+ type pageRoutes map [string ]* page
28
46
29
- var root = func () page {
30
- structure , err := sources .Open ("docs.yml" )
31
- if err != nil {
32
- panic (err )
33
- }
34
- defer structure .Close ()
35
-
47
+ var root , routes = func () (page , pageRoutes ) {
36
48
var menu page
37
- dec := yaml .NewDecoder (structure )
49
+ dec := yaml .NewDecoder (bytes . NewReader ( config ) )
38
50
dec .KnownFields (true )
39
51
if err := dec .Decode (& menu ); err != nil {
40
52
panic (err )
41
53
}
42
54
43
- menu .init ()
44
- return menu
55
+ r := make (pageRoutes )
56
+ menu .init (r )
57
+ return menu , r
45
58
}()
46
59
47
- func (pg * page ) init (crumbs ... string ) {
60
+ func (pg * page ) init (r pageRoutes , crumbs ... string ) {
48
61
if pg .File != "" {
49
- if r := strings .TrimSuffix (pg .File , ".md" ); r == "index " {
62
+ if r := strings .TrimSuffix (pg .File , ".md" ); r == "README " {
50
63
pg .Route = ""
51
64
} else {
52
65
pg .Route = "/" + r
53
66
}
54
-
67
+ r [ pg . Route ] = pg
55
68
pg .parseFile ()
56
69
}
57
70
if pg .Title != "" {
58
- pg .Breadcrumbs = append (pg .Breadcrumbs , pg . Title )
71
+ pg .Breadcrumbs = append ([] string { pg .Title }, crumbs ... )
59
72
}
60
73
for _ , child := range pg .Children {
61
- child .init (pg .Breadcrumbs ... )
74
+ child .init (r , pg .Breadcrumbs ... )
62
75
}
63
76
}
64
77
78
+ var sanitize = func () func (io.Reader ) * bytes.Buffer {
79
+ p := bluemonday .UGCPolicy ()
80
+ p .AllowAttrs ("class" ).Globally ()
81
+ return p .SanitizeReader
82
+ }()
83
+
65
84
func (pg * page ) parseFile () {
66
- body , err := sources .ReadFile (pg .File )
85
+ raw , err := sources .ReadFile (pg .File )
67
86
if err != nil {
68
87
panic (err )
69
88
}
70
89
71
- r := bfchroma .NewRenderer (
72
- bfchroma .WithoutAutodetect (),
73
- bfchroma .ChromaOptions (
74
- html .WithLineNumbers (true ),
90
+ var css , body bytes.Buffer
91
+ md := goldmark .New (
92
+ goldmark .WithParserOptions (
93
+ parser .WithAutoHeadingID (),
94
+ ),
95
+ goldmark .WithRendererOptions (
96
+ html .WithUnsafe (),
97
+ ),
98
+ goldmark .WithExtensions (
99
+ extension .GFM ,
100
+ highlighting .NewHighlighting (
101
+ highlighting .WithCSSWriter (& css ),
102
+ highlighting .WithStyle ("github" ),
103
+ highlighting .WithFormatOptions (
104
+ chromahtml .WithLineNumbers (true ),
105
+ chromahtml .WithClasses (true ),
106
+ ),
107
+ ),
75
108
),
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
109
)
84
110
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" ))
100
- }
111
+ doc := md .Parser ().Parse (text .NewReader (raw ))
112
+ tree , err := toc .Inspect (doc , raw )
113
+ if err != nil {
114
+ panic (err )
115
+ }
116
+ if pg .Title == "" {
117
+ if len (tree .Items ) > 0 {
118
+ pg .Title = string (tree .Items [0 ].Title )
101
119
}
102
- return r .RenderNode (& buf , node , entering )
103
- })
104
-
105
- pg .Body = buf .String ()
106
- }
107
-
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 )
112
-
113
- for _ , c := range pg .Children {
114
- c .Dump (w )
115
120
}
121
+ if err := md .Renderer ().Render (& body , raw , doc ); err != nil {
122
+ panic (err )
123
+ }
124
+ pg .TOC = tree
125
+ pg .CSS = css .Bytes ()
126
+ pg .Body = sanitize (& body ).Bytes ()
116
127
}
117
128
118
129
func Handler () http.Handler {
130
+ type pageVars struct {
131
+ Version string
132
+ Title string
133
+ CSS template.CSS
134
+ Content template.HTML
135
+ }
136
+
119
137
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 )
138
+ pg := routes [r .URL .Path ]
139
+ if pg == nil {
140
+ http .NotFound (w , r )
141
+ return
142
+ }
123
143
124
- fmt .Fprintf (w , "%#v\n \n " , r .URL )
144
+ var buf bytes.Buffer
145
+ err := tplLayout .Execute (& buf , & pageVars {
146
+ Version : texd .Version (),
147
+ Title : strings .Join (pg .Breadcrumbs , " · " ),
148
+ CSS : template .CSS (pg .CSS ),
149
+ Content : template .HTML (pg .Body ),
150
+ })
151
+
152
+ if err != nil {
153
+ log .Println (err )
154
+ code := http .StatusInternalServerError
155
+ http .Error (w , http .StatusText (code ), code )
156
+ return
157
+ }
125
158
126
- root .Dump (w )
159
+ w .Header ().Set ("Content-Type" , "text/html; charset=utf-8" )
160
+ w .Header ().Set ("X-Content-Type-Options" , "nosniff" )
161
+ w .WriteHeader (http .StatusOK )
162
+ _ , _ = buf .WriteTo (w )
127
163
})
128
164
}
0 commit comments