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 updateHash() {
324 const code = trimDotHTML(codeiframe.contentWindow.location.pathname.substring(basepath.length))+hashline(codeiframe.contentWindow.location.hash)
325 const rfc = trimDotHTML(rfciframe.contentWindow.location.pathname.substring(basepath.length))+hashline(rfciframe.contentWindow.location.hash)
327 // Safari and Chromium seem to raise hashchanged for the initial load. Skip if one
328 // of the iframes isn't loaded yet initially.
331 codefile.innerText = code
332 rfcfile.innerText = rfc
333 const nhash = '#' + code + ',' + rfc
334 if (location.hash === nhash || location.hash === '' && nhash === '#code,rfc') {
337 console.log('updating window hash', {code, rfc})
339 location.hash = nhash
340 window.setTimeout(() => {
344window.addEventListener('hashchange', function() {
345 console.log('window hashchange', location.hash, changinghash)
350function hashlink2src(s) {
351 const t = s.split(':')
352 if (t.length > 2 || t[0].startsWith('/') || t[0].includes('..')) {
356 if (t.length === 2) {
360 console.log('hashlink', s, h)
363// We need to replace iframes. Before, we replaced the "src" attribute. But
364// that adds a new entry to the history, while replacing an iframe element does
365// not. The added entries would break the browser back button...
366function replaceIframe(iframe, src) {
368 let prevsrc = o ? o.src : undefined
369 iframe = document.createElement('iframe')
370 iframe.classList.add('iframe')
371 iframe.setAttribute('name', o.getAttribute('name'))
372 iframe.addEventListener('load', function() {
373 if (prevsrc !== iframe.src && (prevsrc || prevsrc !== 'code.html' && prevsrc !== 'rfc.html')) {
376 iframe.contentWindow.addEventListener('hashchange', function(e) {
380 iframe.setAttribute('src', src)
381 o.replaceWith(iframe)
384function updateIframes() {
385 const h = location.hash.length > 1 ? location.hash.substring(1) : 'code,rfc'
386 const t = h.split(',')
387 const codesrc = hashlink2src(t[0])
388 const rfcsrc = hashlink2src(t[1])
389 if (codeiframe.src !== codesrc) {
390 codeiframe = replaceIframe(codeiframe, codesrc)
391 codefile.innerText = t[0]
393 if (rfciframe.src !== rfcsrc) {
394 rfciframe = replaceIframe(rfciframe, rfcsrc)
395 rfcfile.innerText = t[1]
398window.addEventListener('load', function() {
406var codeTemplate = htmltemplate.Must(htmltemplate.New("code").Parse(`<!doctype html>
409 <meta charset="utf-8" />
410 <title>code index</title>
412* { font-size: inherit; font-family: 'ubuntu mono', monospace; margin: 0; padding: 0; box-sizing: border-box; }
413tr:nth-child(odd) { background-color: #ddd; }
418 <tr><th>Package</th><th>Files</th></tr>
419{{- range $dir, $files := .Dirs }}
424 <a href="{{ $dir }}/{{ . }}.html">{{ . }}</a>
434var rfcTemplate = htmltemplate.Must(htmltemplate.New("rfc").Parse(`<!doctype html>
437 <meta charset="utf-8" />
439* { font-size: inherit; font-family: 'ubuntu mono', monospace; margin: 0; padding: 0; }
440tr:nth-child(odd) { background-color: #ddd; }
445 <tr><th>Topic</th><th>RFC</th></tr>
446{{- range $topic, $rfcs := .Topics }}
448 <td>{{ $topic }}</td>
451 <a href="rfc/{{ .File }}.html" title="{{ .Title }}">{{ .File }}</a>