1//go:build xr
2
3package main
4
5// xr reads source files and rfc files and generates html versions, a code and
6// rfc index file, and an overal index file to view code and rfc side by side.
7
8import (
9 "bytes"
10 "flag"
11 "fmt"
12 htmltemplate "html/template"
13 "log"
14 "os"
15 "path/filepath"
16 "regexp"
17 "sort"
18 "strconv"
19 "strings"
20
21 "golang.org/x/exp/maps"
22)
23
24var destdir string
25
26func xcheckf(err error, format string, args ...any) {
27 if err != nil {
28 log.Fatalf("%s: %s", fmt.Sprintf(format, args...), err)
29 }
30}
31
32func xwritefile(path string, buf []byte) {
33 p := filepath.Join(destdir, path)
34 os.MkdirAll(filepath.Dir(p), 0755)
35 err := os.WriteFile(p, buf, 0644)
36 xcheckf(err, "writing file %s", p)
37}
38
39func main() {
40 log.SetFlags(0)
41
42 var release bool
43 flag.BoolVar(&release, "release", false, "generate cross-references for a release, highlighting the release version as active page")
44 flag.Usage = func() {
45 log.Println("usage: go run xr.go destdir revision date latestrelease ../*.go ../*/*.go")
46 flag.PrintDefaults()
47 os.Exit(2)
48 }
49 flag.Parse()
50 args := flag.Args()
51 if len(args) < 4 {
52 flag.Usage()
53 }
54
55 destdir = args[0]
56 revision := args[1]
57 date := args[2]
58 latestRelease := args[3]
59 srcfiles := args[4:]
60
61 // Generate code.html index.
62 srcdirs := map[string][]string{}
63 for _, arg := range srcfiles {
64 arg = strings.TrimPrefix(arg, "../")
65 dir := filepath.Dir(arg)
66 file := filepath.Base(arg)
67 srcdirs[dir] = append(srcdirs[dir], file)
68 }
69 for _, files := range srcdirs {
70 sort.Strings(files)
71 }
72 dirs := maps.Keys(srcdirs)
73 sort.Strings(dirs)
74 var codeBuf bytes.Buffer
75 err := codeTemplate.Execute(&codeBuf, map[string]any{
76 "Dirs": srcdirs,
77 })
78 xcheckf(err, "generating code.html")
79 xwritefile("code.html", codeBuf.Bytes())
80
81 // Generate code html files.
82 re := regexp.MustCompile(`(\.\./)?rfc/[0-9]{3,5}(-[^ :]*)?(:[0-9]+)?`)
83 for dir, files := range srcdirs {
84 for _, file := range files {
85 src := filepath.Join("..", dir, file)
86 dst := filepath.Join(dir, file+".html")
87 buf, err := os.ReadFile(src)
88 xcheckf(err, "reading file %s", src)
89
90 var b bytes.Buffer
91 fmt.Fprint(&b, `<!doctype html>
92<html>
93 <head>
94 <meta charset="utf-8" />
95 <style>
96html { scroll-padding-top: 35%; }
97body { font-family: 'ubuntu mono', monospace; }
98.ln { position: absolute; display: none; background-color: #eee; padding-right: .5em; }
99.l { white-space: pre-wrap; }
100.l:hover .ln { display: inline; }
101.l:target { background-color: gold; }
102 </style>
103 </head>
104 <body>
105`)
106
107 for i, line := range strings.Split(string(buf), "\n") {
108 n := i + 1
109 _, err := fmt.Fprintf(&b, `<div id="L%d" class="l"><a href="#L%d" class="ln">%d</a>`, n, n, n)
110 xcheckf(err, "writing source line")
111
112 if line == "" {
113 b.WriteString("\n")
114 } else {
115 for line != "" {
116 loc := re.FindStringIndex(line)
117 if loc == nil {
118 b.WriteString(htmltemplate.HTMLEscapeString(line))
119 break
120 }
121 s, e := loc[0], loc[1]
122 b.WriteString(htmltemplate.HTMLEscapeString(line[:s]))
123 match := line[s:e]
124 line = line[e:]
125 t := strings.Split(match, ":")
126 linenumber := 1
127 if len(t) == 2 {
128 v, err := strconv.ParseInt(t[1], 10, 31)
129 xcheckf(err, "parsing linenumber %q", t[1])
130 linenumber = int(v)
131 }
132 fmt.Fprintf(&b, `<a href="%s.html#L%d" target="rfc">%s</a>`, t[0], linenumber, htmltemplate.HTMLEscapeString(match))
133 }
134 }
135 fmt.Fprint(&b, "</div>\n")
136 }
137
138 fmt.Fprint(&b, `<script>
139for (const a of document.querySelectorAll('a')) {
140 a.addEventListener('click', function(e) {
141 location.hash = '#'+e.target.closest('.l').id
142 })
143}
144</script>
145 </body>
146</html>
147`)
148
149 xwritefile(dst, b.Bytes())
150 }
151 }
152
153 // Generate rfc index.
154 rfctext, err := os.ReadFile("index.txt")
155 xcheckf(err, "reading rfc index.txt")
156 type rfc struct {
157 File string
158 Title string
159 }
160 topics := map[string][]rfc{}
161 var topic string
162 for _, line := range strings.Split(string(rfctext), "\n") {
163 if strings.HasPrefix(line, "# ") {
164 topic = line[2:]
165 continue
166 }
167 t := strings.Split(line, "\t")
168 if len(t) != 4 {
169 continue
170 }
171 topics[topic] = append(topics[topic], rfc{strings.TrimSpace(t[0]), t[3]})
172 }
173 for _, l := range topics {
174 sort.Slice(l, func(i, j int) bool {
175 return l[i].File < l[j].File
176 })
177 }
178 var rfcBuf bytes.Buffer
179 err = rfcTemplate.Execute(&rfcBuf, map[string]any{
180 "Topics": topics,
181 })
182 xcheckf(err, "generating rfc.html")
183 xwritefile("rfc.html", rfcBuf.Bytes())
184
185 // Process each rfc file into html.
186 for _, rfcs := range topics {
187 for _, rfc := range rfcs {
188 dst := filepath.Join("rfc", rfc.File+".html")
189
190 buf, err := os.ReadFile(rfc.File)
191 xcheckf(err, "reading rfc %s", rfc.File)
192
193 var b bytes.Buffer
194 fmt.Fprint(&b, `<!doctype html>
195<html>
196 <head>
197 <meta charset="utf-8" />
198 <style>
199html { scroll-padding-top: 35%; }
200body { font-family: 'ubuntu mono', monospace; }
201.ln { position: absolute; display: none; background-color: #eee; padding-right: .5em; }
202.l { white-space: pre-wrap; }
203.l:hover .ln { display: inline; }
204.l:target { background-color: gold; }
205 </style>
206 </head>
207 <body>
208`)
209
210 isRef := func(s string) bool {
211 return s[0] >= '0' && s[0] <= '9' || strings.HasPrefix(s, "../")
212 }
213
214 parseRef := func(s string) (string, int, bool) {
215 t := strings.Split(s, ":")
216 linenumber := 1
217 if len(t) == 2 {
218 v, err := strconv.ParseInt(t[1], 10, 31)
219 xcheckf(err, "parsing linenumber")
220 linenumber = int(v)
221 }
222 isCode := strings.HasPrefix(t[0], "../")
223 return t[0], linenumber, isCode
224 }
225
226 for i, line := range strings.Split(string(buf), "\n") {
227 if line == "" {
228 line = "\n"
229 } else if len(line) < 80 || strings.Contains(rfc.File, "-") && i > 0 {
230 line = htmltemplate.HTMLEscapeString(line)
231 } else {
232 t := strings.Split(line[80:], " ")
233 line = htmltemplate.HTMLEscapeString(line[:80])
234 for i, s := range t {
235 if i > 0 {
236 line += " "
237 }
238 if s == "" || !isRef(s) {
239 line += htmltemplate.HTMLEscapeString(s)
240 continue
241 }
242 file, linenumber, isCode := parseRef(s)
243 target := ""
244 if isCode {
245 target = ` target="code"`
246 }
247 line += fmt.Sprintf(` <a href="%s.html#L%d"%s>%s:%d</a>`, file, linenumber, target, file, linenumber)
248 }
249 }
250 n := i + 1
251 _, err := fmt.Fprintf(&b, `<div id="L%d" class="l"><a href="#L%d" class="ln">%d</a>%s</div>%s`, n, n, n, line, "\n")
252 xcheckf(err, "writing rfc line")
253 }
254
255 fmt.Fprint(&b, `<script>
256for (const a of document.querySelectorAll('a')) {
257 a.addEventListener('click', function(e) {
258 location.hash = '#'+e.target.closest('.l').id
259 })
260}
261</script>
262 </body>
263</html>
264`)
265
266 xwritefile(dst, b.Bytes())
267 }
268 }
269
270 // Generate overal file.
271 index := indexHTML
272 if release {
273 index = strings.ReplaceAll(index, "RELEASEWEIGHT", "bold")
274 index = strings.ReplaceAll(index, "REVISIONWEIGHT", "normal")
275 } else {
276 index = strings.ReplaceAll(index, "RELEASEWEIGHT", "normal")
277 index = strings.ReplaceAll(index, "REVISIONWEIGHT", "bold")
278 }
279 index = strings.ReplaceAll(index, "REVISION", revision)
280 index = strings.ReplaceAll(index, "DATE", date)
281 index = strings.ReplaceAll(index, "RELEASE", latestRelease)
282 xwritefile("index.html", []byte(index))
283}
284
285var indexHTML = `<!doctype html>
286<html>
287 <head>
288 <meta charset="utf-8" />
289 <title>Cross-referenced code and RFCs - Mox</title>
290 <link rel="icon" href="noNeedlessFaviconRequestsPlease:" />
291 <style>
292body { margin: 0; padding: 0; font-family: 'ubuntu', 'lato', sans-serif; }
293[title] { text-decoration: underline; text-decoration-style: dotted; }
294.iframe { border: 1px solid #aaa; width: 100%; height: 100%; background-color: #eee; border-radius: .25em; }
295 </style>
296 </head>
297 <body>
298 <div style="display: flex; flex-direction: column; height: 100vh">
299 <div style="padding: .5em"><a href="../../">mox</a>, <span title="The mox code contains references to RFCs, often with specific line numbers. RFCs are generated that point back to the source code. This page shows code and RFCs side by side, with cross-references hyperlinked.">cross-referenced code and RFCs</span>: <a href="../RELEASE/" style="font-weight: RELEASEWEIGHT" title="released version">RELEASE</a> <a href="../dev/" style="font-weight: REVISIONWEIGHT" title="branch main">dev</a> (<a href="https://github.com/mjl-/mox/commit/REVISION" title="Source code commit for this revision.">commit REVISION</a>, DATE)</div>
300 <div style="flex-grow: 1; display: flex; align-items: stretch">
301 <div style="flex-grow: 1; margin: 1ex; position: relative; display: flex; flex-direction: column">
302 <div style="margin-bottom: .5ex"><span id="codefile" style="font-weight: bold">...</span>, <a href="code.html" target="code">index</a></div>
303 <iframe name="code" id="codeiframe" class="iframe"></iframe>
304 </div>
305 <div style="flex-grow: 1; margin: 1ex; position: relative; display: flex; flex-direction: column">
306 <div style="margin-bottom: .5ex"><span id="rfcfile" style="font-weight: bold">...</span>, <a href="rfc.html" target="rfc">index</a></div>
307 <iframe name="rfc" id="rfciframe" class="iframe"></iframe>
308 </div>
309 </div>
310 </div>
311 <script>
312const basepath = location.pathname
313function trimDotHTML(s) {
314 if (s.endsWith('.html')) {
315 return s.substring(s, s.length-'.html'.length)
316 }
317 return s
318}
319let changinghash = false
320function hashline(s) {
321 return s ? ':'+s.substring('#L'.length) : ''
322}
323function updateRFCLink(s) {
324 const t = s.split('/')
325 let e
326 if (t.length === 2 && t[0] === 'rfc' && ''+parseInt(t[1]) === t[1]) {
327 e = document.createElement('a')
328 e.setAttribute('href', 'https://datatracker.ietf.org/doc/html/rfc'+t[1])
329 e.setAttribute('rel', 'noopener')
330 } else {
331 e = document.createElement('span')
332 }
333 e.innerText = s
334 e.style.fontWeight = 'bold'
335 rfcfile.replaceWith(e)
336 rfcfile = e
337}
338function updateHash() {
339 const code = trimDotHTML(codeiframe.contentWindow.location.pathname.substring(basepath.length))+hashline(codeiframe.contentWindow.location.hash)
340 const rfc = trimDotHTML(rfciframe.contentWindow.location.pathname.substring(basepath.length))+hashline(rfciframe.contentWindow.location.hash)
341 if (!code || !rfc) {
342 // Safari and Chromium seem to raise hashchanged for the initial load. Skip if one
343 // of the iframes isn't loaded yet initially.
344 return
345 }
346 codefile.innerText = code
347 updateRFCLink(rfc)
348 const nhash = '#' + code + ',' + rfc
349 if (location.hash === nhash || location.hash === '' && nhash === '#code,rfc') {
350 return
351 }
352 console.log('updating window hash', {code, rfc})
353 changinghash = true
354 location.hash = nhash
355 window.setTimeout(() => {
356 changinghash = false
357 }, 0)
358}
359window.addEventListener('hashchange', function() {
360 console.log('window hashchange', location.hash, changinghash)
361 if (!changinghash) {
362 updateIframes()
363 }
364})
365function hashlink2src(s) {
366 const t = s.split(':')
367 if (t.length > 2 || t[0].startsWith('/') || t[0].includes('..')) {
368 return ''
369 }
370 let h = t[0]+'.html'
371 if (t.length === 2) {
372 h += '#L'+t[1]
373 }
374 h = './'+h
375 console.log('hashlink', s, h)
376 return h
377}
378// We need to replace iframes. Before, we replaced the "src" attribute. But
379// that adds a new entry to the history, while replacing an iframe element does
380// not. The added entries would break the browser back button...
381function replaceIframe(iframe, src) {
382 const o = iframe
383 let prevsrc = o ? o.src : undefined
384 iframe = document.createElement('iframe')
385 iframe.classList.add('iframe')
386 iframe.setAttribute('name', o.getAttribute('name'))
387 iframe.addEventListener('load', function() {
388 if (prevsrc !== iframe.src && (prevsrc || prevsrc !== 'code.html' && prevsrc !== 'rfc.html')) {
389 updateHash()
390 }
391 iframe.contentWindow.addEventListener('hashchange', function(e) {
392 updateHash()
393 })
394 })
395 iframe.setAttribute('src', src)
396 o.replaceWith(iframe)
397 return iframe
398}
399function updateIframes() {
400 const h = location.hash.length > 1 ? location.hash.substring(1) : 'code,rfc'
401 const t = h.split(',')
402 const codesrc = hashlink2src(t[0])
403 const rfcsrc = hashlink2src(t[1])
404 if (codeiframe.src !== codesrc) {
405 codeiframe = replaceIframe(codeiframe, codesrc)
406 codefile.innerText = t[0]
407 }
408 if (rfciframe.src !== rfcsrc) {
409 rfciframe = replaceIframe(rfciframe, rfcsrc)
410 updateRFCLink(t[1])
411 }
412}
413window.addEventListener('load', function() {
414 updateIframes()
415})
416 </script>
417 </body>
418</html>
419`
420
421var codeTemplate = htmltemplate.Must(htmltemplate.New("code").Parse(`<!doctype html>
422<html>
423 <head>
424 <meta charset="utf-8" />
425 <title>code index</title>
426 <style>
427* { font-size: inherit; font-family: 'ubuntu mono', monospace; margin: 0; padding: 0; box-sizing: border-box; }
428tr:nth-child(odd) { background-color: #ddd; }
429 </style>
430 </head>
431 <body>
432 <table>
433 <tr><th>Package</th><th>Files</th></tr>
434{{- range $dir, $files := .Dirs }}
435 <tr>
436 <td>{{ $dir }}/</td>
437 <td>
438 {{- range $files }}
439 <a href="{{ $dir }}/{{ . }}.html">{{ . }}</a>
440 {{- end }}
441 </td>
442 </tr>
443{{- end }}
444 </table>
445 </body>
446</html>
447`))
448
449var rfcTemplate = htmltemplate.Must(htmltemplate.New("rfc").Parse(`<!doctype html>
450<html>
451 <head>
452 <meta charset="utf-8" />
453 <style>
454* { font-size: inherit; font-family: 'ubuntu mono', monospace; margin: 0; padding: 0; }
455tr:nth-child(odd) { background-color: #ddd; }
456 </style>
457 </head>
458 <body>
459 <table>
460 <tr><th>Topic</th><th>RFC</th></tr>
461{{- range $topic, $rfcs := .Topics }}
462 <tr>
463 <td>{{ $topic }}</td>
464 <td>
465 {{- range $rfcs }}
466 <a href="rfc/{{ .File }}.html" title="{{ .Title }}">{{ .File }}</a>
467 {{- end }}
468 </td>
469 </tr>
470{{- end }}
471 </table>
472 </body>
473</html>
474`))
475