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.
12 htmltemplate "html/template"
21 "golang.org/x/exp/maps"
26func xcheckf(err error, format string, args ...any) {
28 log.Fatalf("%s: %s", fmt.Sprintf(format, args...), err)
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)
43 flag.BoolVar(&release, "release", false, "generate cross-references for a release, highlighting the release version as active page")
45 log.Println("usage: go run xr.go destdir revision date latestrelease ../*.go ../*/*.go")
58 latestRelease := args[3]
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)
69 for _, files := range srcdirs {
72 dirs := maps.Keys(srcdirs)
74 var codeBuf bytes.Buffer
75 err := codeTemplate.Execute(&codeBuf, map[string]any{
78 xcheckf(err, "generating code.html")
79 xwritefile("code.html", codeBuf.Bytes())
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)
91 fmt.Fprint(&b, `<!doctype html>
94 <meta charset="utf-8" />
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; }
107 for i, line := range strings.Split(string(buf), "\n") {
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")
116 loc := re.FindStringIndex(line)
118 b.WriteString(htmltemplate.HTMLEscapeString(line))
121 s, e := loc[0], loc[1]
122 b.WriteString(htmltemplate.HTMLEscapeString(line[:s]))
125 t := strings.Split(match, ":")
128 v, err := strconv.ParseInt(t[1], 10, 31)
129 xcheckf(err, "parsing linenumber %q", t[1])
132 fmt.Fprintf(&b, `<a href="%s.html#L%d" target="rfc">%s</a>`, t[0], linenumber, htmltemplate.HTMLEscapeString(match))
135 fmt.Fprint(&b, "</div>\n")
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
149 xwritefile(dst, b.Bytes())
153 // Generate rfc index.
154 rfctext, err := os.ReadFile("index.txt")
155 xcheckf(err, "reading rfc index.txt")
160 topics := map[string][]rfc{}
162 for _, line := range strings.Split(string(rfctext), "\n") {
163 if strings.HasPrefix(line, "# ") {
167 t := strings.Split(line, "\t")
171 topics[topic] = append(topics[topic], rfc{strings.TrimSpace(t[0]), t[3]})
173 for _, l := range topics {
174 sort.Slice(l, func(i, j int) bool {
175 return l[i].File < l[j].File
178 var rfcBuf bytes.Buffer
179 err = rfcTemplate.Execute(&rfcBuf, map[string]any{
182 xcheckf(err, "generating rfc.html")
183 xwritefile("rfc.html", rfcBuf.Bytes())
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")
190 buf, err := os.ReadFile(rfc.File)
191 xcheckf(err, "reading rfc %s", rfc.File)
194 fmt.Fprint(&b, `<!doctype html>
197 <meta charset="utf-8" />
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; }
210 isRef := func(s string) bool {
211 return s[0] >= '0' && s[0] <= '9' || strings.HasPrefix(s, "../")
214 parseRef := func(s string) (string, int, bool) {
215 t := strings.Split(s, ":")
218 v, err := strconv.ParseInt(t[1], 10, 31)
219 xcheckf(err, "parsing linenumber")
222 isCode := strings.HasPrefix(t[0], "../")
223 return t[0], linenumber, isCode
226 for i, line := range strings.Split(string(buf), "\n") {
229 } else if len(line) < 80 || strings.Contains(rfc.File, "-") && i > 0 {
230 line = htmltemplate.HTMLEscapeString(line)
232 t := strings.Split(line[80:], " ")
233 line = htmltemplate.HTMLEscapeString(line[:80])
234 for i, s := range t {
238 if s == "" || !isRef(s) {
239 line += htmltemplate.HTMLEscapeString(s)
242 file, linenumber, isCode := parseRef(s)
245 target = ` target="code"`
247 line += fmt.Sprintf(` <a href="%s.html#L%d"%s>%s:%d</a>`, file, linenumber, target, file, linenumber)
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")
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
266 xwritefile(dst, b.Bytes())
270 // Generate overal file.
273 index = strings.ReplaceAll(index, "RELEASEWEIGHT", "bold")
274 index = strings.ReplaceAll(index, "REVISIONWEIGHT", "normal")
276 index = strings.ReplaceAll(index, "RELEASEWEIGHT", "normal")
277 index = strings.ReplaceAll(index, "REVISIONWEIGHT", "bold")
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))
285var indexHTML = `<!doctype html>
288 <meta charset="utf-8" />
289 <title>Cross-referenced code and RFCs - Mox</title>
290 <link rel="icon" href="noNeedlessFaviconRequestsPlease:" />
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; }
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>
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>
312const basepath = location.pathname
313function trimDotHTML(s) {
314 if (s.endsWith('.html')) {
315 return s.substring(s, s.length-'.html'.length)
319let changinghash = false
320function hashline(s) {
321 return s ? ':'+s.substring('#L'.length) : ''
323function updateRFCLink(s) {
324 const t = s.split('/')
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')
331 e = document.createElement('span')
334 e.style.fontWeight = 'bold'
335 rfcfile.replaceWith(e)
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)
342 // Safari and Chromium seem to raise hashchanged for the initial load. Skip if one
343 // of the iframes isn't loaded yet initially.
346 codefile.innerText = code
348 const nhash = '#' + code + ',' + rfc
349 if (location.hash === nhash || location.hash === '' && nhash === '#code,rfc') {
352 console.log('updating window hash', {code, rfc})
354 location.hash = nhash
355 window.setTimeout(() => {
359window.addEventListener('hashchange', function() {
360 console.log('window hashchange', location.hash, changinghash)
365function hashlink2src(s) {
366 const t = s.split(':')
367 if (t.length > 2 || t[0].startsWith('/') || t[0].includes('..')) {
371 if (t.length === 2) {
375 console.log('hashlink', s, h)
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) {
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')) {
391 iframe.contentWindow.addEventListener('hashchange', function(e) {
395 iframe.setAttribute('src', src)
396 o.replaceWith(iframe)
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]
408 if (rfciframe.src !== rfcsrc) {
409 rfciframe = replaceIframe(rfciframe, rfcsrc)
413window.addEventListener('load', function() {
421var codeTemplate = htmltemplate.Must(htmltemplate.New("code").Parse(`<!doctype html>
424 <meta charset="utf-8" />
425 <title>code index</title>
427* { font-size: inherit; font-family: 'ubuntu mono', monospace; margin: 0; padding: 0; box-sizing: border-box; }
428tr:nth-child(odd) { background-color: #ddd; }
433 <tr><th>Package</th><th>Files</th></tr>
434{{- range $dir, $files := .Dirs }}
439 <a href="{{ $dir }}/{{ . }}.html">{{ . }}</a>
449var rfcTemplate = htmltemplate.Must(htmltemplate.New("rfc").Parse(`<!doctype html>
452 <meta charset="utf-8" />
454* { font-size: inherit; font-family: 'ubuntu mono', monospace; margin: 0; padding: 0; }
455tr:nth-child(odd) { background-color: #ddd; }
460 <tr><th>Topic</th><th>RFC</th></tr>
461{{- range $topic, $rfcs := .Topics }}
463 <td>{{ $topic }}</td>
466 <a href="rfc/{{ .File }}.html" title="{{ .Title }}">{{ .File }}</a>