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