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"
24func xcheckf(err error, format string, args ...any) {
26 log.Fatalf("%s: %s", fmt.Sprintf(format, args...), err)
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)
41 flag.BoolVar(&release, "release", false, "generate cross-references for a release, highlighting the release version as active page")
43 log.Println("usage: go run xr.go destdir revision date latestrelease ../*.go ../*/*.go")
56 latestRelease := args[3]
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)
67 for _, files := range srcdirs {
70 var codeBuf bytes.Buffer
71 err := codeTemplate.Execute(&codeBuf, map[string]any{
74 xcheckf(err, "generating code.html")
75 xwritefile("code.html", codeBuf.Bytes())
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)
87 fmt.Fprint(&b, `<!doctype html>
90 <meta charset="utf-8" />
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; }
103 for i, line := range strings.Split(string(buf), "\n") {
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")
112 loc := re.FindStringIndex(line)
114 b.WriteString(htmltemplate.HTMLEscapeString(line))
117 s, e := loc[0], loc[1]
118 b.WriteString(htmltemplate.HTMLEscapeString(line[:s]))
121 t := strings.Split(match, ":")
124 v, err := strconv.ParseInt(t[1], 10, 31)
125 xcheckf(err, "parsing linenumber %q", t[1])
128 fmt.Fprintf(&b, `<a href="%s.html#L%d" target="rfc">%s</a>`, t[0], linenumber, htmltemplate.HTMLEscapeString(match))
131 fmt.Fprint(&b, "</div>\n")
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
145 xwritefile(dst, b.Bytes())
149 // Generate rfc index.
150 rfctext, err := os.ReadFile("index.txt")
151 xcheckf(err, "reading rfc index.txt")
156 topics := map[string][]rfc{}
158 for _, line := range strings.Split(string(rfctext), "\n") {
159 if strings.HasPrefix(line, "# ") {
163 t := strings.Split(line, "\t")
167 topics[topic] = append(topics[topic], rfc{strings.TrimSpace(t[0]), t[3]})
169 for _, l := range topics {
170 sort.Slice(l, func(i, j int) bool {
171 return l[i].File < l[j].File
174 var rfcBuf bytes.Buffer
175 err = rfcTemplate.Execute(&rfcBuf, map[string]any{
178 xcheckf(err, "generating rfc.html")
179 xwritefile("rfc.html", rfcBuf.Bytes())
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")
186 buf, err := os.ReadFile(rfc.File)
187 xcheckf(err, "reading rfc %s", rfc.File)
190 fmt.Fprint(&b, `<!doctype html>
193 <meta charset="utf-8" />
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; }
206 isRef := func(s string) bool {
207 return s[0] >= '0' && s[0] <= '9' || strings.HasPrefix(s, "../")
210 parseRef := func(s string) (string, int, bool) {
211 t := strings.Split(s, ":")
214 v, err := strconv.ParseInt(t[1], 10, 31)
215 xcheckf(err, "parsing linenumber")
218 isCode := strings.HasPrefix(t[0], "../")
219 return t[0], linenumber, isCode
222 for i, line := range strings.Split(string(buf), "\n") {
225 } else if len(line) < 80 || strings.Contains(rfc.File, "-") && i > 0 {
226 line = htmltemplate.HTMLEscapeString(line)
228 t := strings.Split(line[80:], " ")
229 line = htmltemplate.HTMLEscapeString(line[:80])
230 for i, s := range t {
234 if s == "" || !isRef(s) {
235 line += htmltemplate.HTMLEscapeString(s)
238 file, linenumber, isCode := parseRef(s)
241 target = ` target="code"`
243 line += fmt.Sprintf(` <a href="%s.html#L%d"%s>%s:%d</a>`, file, linenumber, target, file, linenumber)
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")
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
262 xwritefile(dst, b.Bytes())
266 // Generate overal file.
269 index = strings.ReplaceAll(index, "RELEASEWEIGHT", "bold")
270 index = strings.ReplaceAll(index, "REVISIONWEIGHT", "normal")
272 index = strings.ReplaceAll(index, "RELEASEWEIGHT", "normal")
273 index = strings.ReplaceAll(index, "REVISIONWEIGHT", "bold")
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))
281var indexHTML = `<!doctype html>
284 <meta charset="utf-8" />
285 <title>Cross-referenced code and RFCs - Mox</title>
286 <link rel="icon" href="noNeedlessFaviconRequestsPlease:" />
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; }
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>
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>
308const basepath = location.pathname
309function trimDotHTML(s) {
310 if (s.endsWith('.html')) {
311 return s.substring(s, s.length-'.html'.length)
315let changinghash = false
316function hashline(s) {
317 return s ? ':'+s.substring('#L'.length) : ''
319function updateRFCLink(s) {
320 const t = s.split('/')
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')
327 e = document.createElement('span')
330 e.style.fontWeight = 'bold'
331 rfcfile.replaceWith(e)
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)
338 // Safari and Chromium seem to raise hashchanged for the initial load. Skip if one
339 // of the iframes isn't loaded yet initially.
342 codefile.innerText = code
344 const nhash = '#' + code + ',' + rfc
345 if (location.hash === nhash || location.hash === '' && nhash === '#code,rfc') {
348 console.log('updating window hash', {code, rfc})
350 location.hash = nhash
351 window.setTimeout(() => {
355window.addEventListener('hashchange', function() {
356 console.log('window hashchange', location.hash, changinghash)
361function hashlink2src(s) {
362 const t = s.split(':')
363 if (t.length > 2 || t[0].startsWith('/') || t[0].includes('..')) {
367 if (t.length === 2) {
371 console.log('hashlink', s, h)
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) {
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')) {
387 iframe.contentWindow.addEventListener('hashchange', function(e) {
391 iframe.setAttribute('src', src)
392 o.replaceWith(iframe)
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]
404 if (rfciframe.src !== rfcsrc) {
405 rfciframe = replaceIframe(rfciframe, rfcsrc)
409window.addEventListener('load', function() {
417var codeTemplate = htmltemplate.Must(htmltemplate.New("code").Parse(`<!doctype html>
420 <meta charset="utf-8" />
421 <title>code index</title>
423* { font-size: inherit; font-family: 'ubuntu mono', monospace; margin: 0; padding: 0; box-sizing: border-box; }
424tr:nth-child(odd) { background-color: #ddd; }
429 <tr><th>Package</th><th>Files</th></tr>
430{{- range $dir, $files := .Dirs }}
435 <a href="{{ $dir }}/{{ . }}.html">{{ . }}</a>
445var rfcTemplate = htmltemplate.Must(htmltemplate.New("rfc").Parse(`<!doctype html>
448 <meta charset="utf-8" />
450* { font-size: inherit; font-family: 'ubuntu mono', monospace; margin: 0; padding: 0; }
451tr:nth-child(odd) { background-color: #ddd; }
456 <tr><th>Topic</th><th>RFC</th></tr>
457{{- range $topic, $rfcs := .Topics }}
459 <td>{{ $topic }}</td>
462 <a href="rfc/{{ .File }}.html" title="{{ .Title }}">{{ .File }}</a>