@@ -4,125 +4,212 @@ import (
4
4
"bytes"
5
5
"embed"
6
6
"fmt"
7
+ "html/template"
7
8
"io"
9
+ "log"
8
10
"net/http"
9
11
"strings"
10
12
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"
14
25
"gopkg.in/yaml.v3"
15
26
)
16
27
17
- //go:embed docs.yml *.md **/*.md
28
+ //go:embed *.md **/*.md
18
29
var sources embed.FS
19
30
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
+
20
38
type page struct {
21
39
Title string
22
40
Breadcrumbs []string
23
- Body string
41
+ TOC * toc.TOC
42
+ CSS []byte
43
+ Body []byte
24
44
File string
25
45
Route string
26
46
Children []* page
27
47
}
28
48
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
35
50
51
+ func getRoutes (urlPrefix string ) (pageRoutes , error ) {
36
52
var menu page
37
- dec := yaml .NewDecoder (structure )
53
+ dec := yaml .NewDecoder (bytes . NewReader ( config ) )
38
54
dec .KnownFields (true )
39
55
if err := dec .Decode (& menu ); err != nil {
40
- panic ( err )
56
+ return nil , err
41
57
}
42
58
43
- menu . init ( )
44
- return menu
45
- }()
59
+ urlPrefix = strings . TrimSuffix ( urlPrefix , "/" )
60
+ return menu . init ( urlPrefix , make ( pageRoutes ))
61
+ }
46
62
47
- func (pg * page ) init (crumbs ... string ) {
63
+ func (pg * page ) init (urlPrefix string , r pageRoutes , crumbs ... string ) ( pageRoutes , error ) {
48
64
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
51
67
} 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
53
74
}
54
-
55
- pg .parseFile ()
56
75
}
57
76
if pg .Title != "" {
58
- pg .Breadcrumbs = append (pg .Breadcrumbs , pg . Title )
77
+ pg .Breadcrumbs = append ([] string { pg .Title }, crumbs ... )
59
78
}
60
79
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
+ }
62
84
}
85
+ return r , nil
63
86
}
64
87
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
+ }
70
91
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 )
84
93
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 )
100
99
}
101
100
}
102
- return r . RenderNode ( & buf , node , entering )
101
+ return ast . WalkContinue , nil
103
102
})
103
+ }
104
104
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
+ }
106
117
}
107
118
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
+ }()
112
124
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
115
129
}
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
116
173
}
117
174
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
+
119
188
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
+ }
123
194
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
+ }
125
209
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
128
215
}
0 commit comments