Add patch.njk page

4ccb6eca9b4d6b4e497cc558eb73652a69989e4e

Tucker McKnight <tucker.mcknight@gmail.com> | Tue Sep 02 2025

Add patch.njk page

Fix some references to static files that weren't using the
reposPath variable.

Put main.css in the vendors folder, which should be renamed at some
point, but for now it gets main.css into a dist/ location that can
be used by the virutal templates.

Change the way the light/dark theme switcher works; just replace
"prism.css" or "prism_dark.css" instead of replacing the full path.
This avoids having to know the reposPath variable in a JS file.
frontend/top.ts:32
Before
31
32
33
34
35
36
37
    }
    const link: any = document.getElementById("prism-theme")
    const preferred = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
    const stylesheet = window['currentTheme'] === 'dark' || (window['currentTheme'] === 'auto' && preferred === 'dark') ? "/vendor/prism_dark.css" : "/vendor/prism.css"
    link.href = stylesheet
  }
  window['toggleDarkMode'] = (button) => {
    const clickedOption = button.dataset.themePref
After
31
32
33
34
35
36
37
    }
    const link: any = document.getElementById("prism-theme")
    const preferred = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
    const stylesheet = window['currentTheme'] === 'dark' || (window['currentTheme'] === 'auto' && preferred === 'dark') ? "prism_dark.css" : "prism.css"
    link.href = link.href.split("/").slice(0, -1).concat(stylesheet).join("/")
  }
  window['toggleDarkMode'] = (button) => {
    const clickedOption = button.dataset.themePref
main.ts:4
Before
3
4
5

6
7
import repos from './src/repos.ts'
import branches from './src/branches.ts'
import flatFiles from './src/flatFiles.ts'
⁣
import paginatedPatches from './src/paginatedPatches.ts'
import {getLocation} from './src/helpers.ts'
import repoOperations from './src/vcses/operations.ts'
After
3
4
5
6
7
8
import repos from './src/repos.ts'
import branches from './src/branches.ts'
import flatFiles from './src/flatFiles.ts'
import flatPatches from './src/flatPatches.ts'
import paginatedPatches from './src/paginatedPatches.ts'
import {getLocation} from './src/helpers.ts'
import repoOperations from './src/vcses/operations.ts'
main.ts:34
Before
33
34
35
36
37
38
39
  const frontend = `${__dirname}/frontend`

  eleventyConfig.addPassthroughCopy({
    [vendor]: "vendor",
    [frontend]: "frontend"
  })

  eleventyConfig.on(
After
33
34
35
36
37
38
39
  const frontend = `${__dirname}/frontend`

  eleventyConfig.addPassthroughCopy({
    [vendor]: `${reposPath}/vendor`,
    [frontend]: `${reposPath}/frontend`,
  })

  eleventyConfig.on(
main.ts:177
Before
176
177
178
179



180
181


182
183
184
    return res.stdout
  })

  eleventyConfig.addAsyncFilter("getReadMe", async (repoName, branchName) => {
⁣
⁣
⁣
    console.log("getreadme")
    console.log(branchName)
⁣
⁣
    console.log(repoName)
    const location = getLocation(reposConfiguration, branchName, repoName)
    const config = reposConfiguration.repos[repoName]
    let command = ''
After
176
177
178
179
180
181
182
183
184
185
186
187
188
189
    return res.stdout
  })

  eleventyConfig.addFilter("pagesJustForBranch", (pages, repoName, branchName) => {
    return pages.filter(page => page.repoName === repoName && page.branchName === branchName)
  })

  eleventyConfig.addFilter("date", (dateString) => {
    return new Date(dateString).toDateString()
  })

  eleventyConfig.addAsyncFilter("getReadMe", async (repoName, branchName) => {
    const location = getLocation(reposConfiguration, branchName, repoName)
    const config = reposConfiguration.repos[repoName]
    let command = ''
main.ts:304
Before
303
304
305
306
307
308
        size: 1,
        alias: "branch",
      },
      branches: branchesData,
      permalink: (data) => {
        const repoName = data.branch.repoName
        const branchName = data.branch.branchName
After
303
304
305

306
307
        size: 1,
        alias: "branch",
      },
⁣
      permalink: (data) => {
        const repoName = data.branch.repoName
        const branchName = data.branch.branchName
main.ts:325
Before
324
325
326
327
328
329
  const paginatedPatchesData = await paginatedPatches(reposData)
  eleventyConfig.addTemplate(
    `repos/patches.njk`,
    topLayoutPartial + repoTemplate + bottomLayoutPartial,
    {
      pagination: {
        data: "paginatedPatches",
After
324
325
326
327
328
329
  const paginatedPatchesData = await paginatedPatches(reposData)
  eleventyConfig.addTemplate(
    `repos/patches.njk`,
    topLayoutPartial + patchesTemplate + bottomLayoutPartial,
    {
      pagination: {
        data: "paginatedPatches",
main.ts:336
Before
335
336
337
338
339
340
      permalink: (data) => {
        const repoName = data.patchPage.repoName
        const branchName = data.patchPage.branchName
        return `${reposPath}/${eleventyConfig.getFilter("slugify")(repoName)}/branches/${eleventyConfig.getFilter("slugify")(branchName)}/patches/page${data.patchPage.pageNumber}`
      },
      eleventyComputed: {
        nav: {
After
335
336
337
338
339
340
      permalink: (data) => {
        const repoName = data.patchPage.repoName
        const branchName = data.patchPage.branchName
        return `${reposPath}/${eleventyConfig.getFilter("slugify")(repoName)}/branches/${eleventyConfig.getFilter("slugify")(branchName)}/patches/page${data.patchPage.pageNumber}/`
      },
      eleventyComputed: {
        nav: {
main.ts:347
Before
346
347
348




























      navTab: "patches",
    }
  )
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
}
After
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
      navTab: "patches",
    }
  )

  // PATCH.NJK
  const patchTemplate = fsImport.readFileSync(`${__dirname}/templates/patch.njk`).toString()
  const flatPatchesData = await flatPatches(reposConfiguration)
  eleventyConfig.addTemplate(
    `repos/patch.njk`,
    topLayoutPartial + patchTemplate + bottomLayoutPartial,
    {
      pagination: {
        data: "flatPatches",
        size: 1,
        alias: "patchInfo",
      },
      flatPatches: flatPatchesData,
      permalink: (data) => {
        const repoName = data.patchInfo.repoName
        const branchName = data.patchInfo.branchName
        return `${reposPath}/${eleventyConfig.getFilter("slugify")(repoName)}/branches/${eleventyConfig.getFilter("slugify")(branchName)}/patches/${data.patchInfo.patch.hash}/`
      },
      eleventyComputed: {
        nav: {
          repoName: (data) => data.patchInfo.repoName,
          branchName: (data) => data.patchInfo.branchName,
        }
      },
      width: "full",
      navTab: "patches",
    }
  )
}
partial_templates/main_bottom.njk:21
Before
20
21
22
23
24
25
26
27

      const selectBranch = (e) => {
        const values = e.value.split(",")
        window.location = `/repos/${values[0]}/branches/${values[1]}/${values[2]}`
      }
    </script>
    <script src="/frontend/main.js"></script>
  </body>
</html>
After
20
21
22
23
24
25
26
27

      const selectBranch = (e) => {
        const values = e.value.split(",")
        window.location = `{{reposPath}}/${values[0]}/branches/${values[1]}/${values[2]}`
      }
    </script>
    <script src="{{reposPath}}/frontend/main.js"></script>
  </body>
</html>
partial_templates/main_top.njk:29
Before
28
29
30
31
32
33
34
35
36
37
38
    
      {% endif %}
    </script>
    <script src="/frontend/top.js"></script>
    <link rel="stylesheet" id="prism-theme" type="text/css" href="/vendor/prism.css" />
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.5/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-SgOJa3DmI69IUzQ2PVdRZhwQ+dy64/BUtbMJw1MZ8t5HZApcHrRKUc4W0kG879m7" crossorigin="anonymous">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/js/bootstrap.bundle.min.js" integrity="sha384-j1CDi7MgGQ12Z7Qab0qlWQ/Qqz24Gc6BM0thvEMVjHnfYGF0rmFCozFSxQBxwHKO" crossorigin="anonymous"></script>
    <link rel="stylesheet" type="text/css" href="/static/main.css" />
    <meta name="viewport" content="width=device-width, initial-scale=1">
  </head>
After
28
29
30
31
32
33
34
35
36
37
38
    
      {% endif %}
    </script>
    <script src="{{reposPath}}/frontend/top.js"></script>
    <link rel="stylesheet" id="prism-theme" type="text/css" href="{{reposPath}}/vendor/prism.css" />
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.5/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-SgOJa3DmI69IUzQ2PVdRZhwQ+dy64/BUtbMJw1MZ8t5HZApcHrRKUc4W0kG879m7" crossorigin="anonymous">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/js/bootstrap.bundle.min.js" integrity="sha384-j1CDi7MgGQ12Z7Qab0qlWQ/Qqz24Gc6BM0thvEMVjHnfYGF0rmFCozFSxQBxwHKO" crossorigin="anonymous"></script>
    <link rel="stylesheet" type="text/css" href="{{reposPath}}/vendor/main.css" />
    <meta name="viewport" content="width=device-width, initial-scale=1">
  </head>
src/helpers.ts:137
Before
136
137
138
139
140
141
}

const getLocation = (reposConfig: any, branchName: string, repoName: string): string => {
  console.log(repoName)
  const config = reposConfig.repos[repoName]
  if (config._type === "darcs") {
    return config.branches[branchName].location
After
136
137
138

139
140
}

const getLocation = (reposConfig: any, branchName: string, repoName: string): string => {
⁣
  const config = reposConfig.repos[repoName]
  if (config._type === "darcs") {
    return config.branches[branchName].location
templates/patch.njk:0
Before


















































































⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
After
-1
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<div class="container-lg">
  <div class="row">
    <div class="col-auto">
      <h1>{{patchInfo.patch.name}}</h2>
      <p>{{patchInfo.patch.date | date }}</p>
      <p>{{patchInfo.patch.author }}</p>
      <pre>{{patchInfo.patch.description}}</pre>
    </div>
  </div>
  <div class="row">
    <div class="col-auto">
      <p class="font-monospace fw-bold text-secondary">{{patchInfo.patch.hash}}</p>
      {% if reposConfig.repos[patchInfo.repoName]._type == "darcs" %}
      <div class="input-group mb-3 d-flex flex-nowrap">
        <span id="clone-command" class="clone input-group-text overflow-hidden">
          {% set url = [reposConfig.baseUrl, "/repos/", patchInfo.repoName | slugify, "/branches/", patchInfo.branchName | slugify] | join | url %}
          darcs pull {{ url }} -h {{patchInfo.patch.hash}}
        </span>
        <button class="btn btn-primary" id="clone-button" onclick="copyCommand()">Copy</button>
      </div>
      {% endif %}
    </div>
  </div>
</div>
<div class="container-fluid border-top">
  <div class="row my-2">
    <div class="col-auto d-flex align-items-center fs-4">
      <i class="bi bi-layout-split"></i><span class="fs-6 mx-1">Side-by-side</span>
      <div class="form-check form-switch mb-1 px-2">
        <input class="form-check-input mx-0" type="checkbox" id="unifiedModeSwitch" value="unified" onchange="toggleUnifiedMode(this)" aria-label="Toggle switch for stacked or side by side diff view">
      </div>
      <i class="bi bi-hr"></i><span class="fs-6 mx-1">Stacked</span>
    </div>
  </div>
  <div class="row" id="diffs">
  {% set patchHunks = patchInfo.patch.diffs %}
  {% for hunk in patchHunks %}
    <div class=hunk>
      <span class="font-monospace fw-bold"><a href="/repos/{{patchInfo.repoName | slugify}}/branches/{{patchInfo.branchName | slugify}}/files/{{ hunk.file | slugify}}.html">{{ hunk.file }}:{{ hunk.lineNumber }}</a></span>
      <div class="diff d-flex">
        <div class="flex-grow-1 diff-left pe-2">
          <span class='font-monospace text-secondary'>Before</span>
          <div class="row">
            <div class="col-auto border-end">
              <code>
{%- for lineNumber in hunk.previousText | lineNumbers -%}
{{ lineNumber + hunk.lineNumber - 1}}
{% endfor -%}
              </code>
            </div>
            <div class="col overflow-scroll">
              <pre data-start="{{hunk.lineNumber}}"><code data-type="before" class="line-numbers language-{{hunk.file | languageExtension(patchInfo.repoName)}}">{{hunk.previousText | safe}}</code></pre>
            </div>
          </div>
        </div>
        <div class="diff-right flex-grow-1 ps-2 border-start">
          <span class='font-monospace text-secondary'>After</span>
          <div class="row">
            <div class="col-auto border-end">
              <code>
{%- for lineNumber in hunk.afterText | lineNumbers -%}
{{ lineNumber + hunk.lineNumber - 1}}
{% endfor -%}
              </code>
            </div>
            <div class="col overflow-scroll">
              <pre data-start="{{hunk.lineNumber}}"><code data-type="after" class="line-numbers language-{{hunk.file | languageExtension(patchInfo.repoName)}}">{{ hunk.afterText | safe}}</code></pre>
            </div>
          </div>
        </div>
      </div>
    </div>
  {% endfor %}
  </div>
</div>

<script>
  const copyCommand = () => {
    const text = document.getElementById("clone-command").innerText
    const button = document.getElementById("clone-button")
    navigator.clipboard.writeText(text).then(() => {
      button.innerText = "Copied"
    })
  }
</script>
templates/patches.njk:12
Before
11
12
13
14
15
16
  {% for patch in patchPage.patches %}
    <li class="patch">
      <div>
        <span class="patch-name"><a href="/repos/{{patchPage.repoName | slugify}}/branches/{{patchPage.branchName | slugify}}/patches/{{patch.hash}}">{{patch.name}}</a></span>
        <br />
        <span>{{patch.date | date}}</span>
        <br />
After
11
12
13
14
15
16
  {% for patch in patchPage.patches %}
    <li class="patch">
      <div>
        <span class="patch-name"><a href="{{reposPath}}/{{patchPage.repoName | slugify}}/branches/{{patchPage.branchName | slugify}}/patches/{{patch.hash}}">{{patch.name}}</a></span>
        <br />
        <span>{{patch.date | date}}</span>
        <br />
templates/patches.njk:27
Before
26
27
28
29
30
31
  <ul class="pagination">
    {% for pageObj in (paginatedPatches | pagesJustForBranch(patchPage.repoName, patchPage.branchName)) %}
    <li class="page-item">
      <a class="page-link {% if pageObj.pageNumber == patchPage.pageNumber %}active{% endif %}" href="/repos/{{patchPage.repoName | slugify}}/branches/{{patchPage.branchName}}/patches/page{{pageObj.pageNumber}}">{{ pageObj.pageNumber }}</a>
    </li>
    {% endfor %}
  </ul>
After
26
27
28
29
30
31
  <ul class="pagination">
    {% for pageObj in (paginatedPatches | pagesJustForBranch(patchPage.repoName, patchPage.branchName)) %}
    <li class="page-item">
      <a class="page-link {% if pageObj.pageNumber == patchPage.pageNumber %}active{% endif %}" href="{{reposPath}}/{{patchPage.repoName | slugify}}/branches/{{patchPage.branchName}}/patches/page{{pageObj.pageNumber}}">{{ pageObj.pageNumber }}</a>
    </li>
    {% endfor %}
  </ul>
vendor/main.css:0
Before






















































⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
After
-1
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
pre {
  font-size: 14px !important;
}
code {
  white-space: pre;
}
blockquote {
  margin-left: 0.25rem;
  padding-left: 0.5rem;
  border-left: 3px solid royalblue;
  background: rgba(125, 125, 125, 0.07);
  overflow: hidden;
}
blockquote p {
  margin: 0.5rem;
}
.hunk {
  box-sizing: border-box;
  margin-bottom: 25px;
}
[data-type=before] mark {
  background-color: rgba(252, 78, 78, .15);
}
[data-type=after] mark {
  background-color: rgba(151, 247, 203, .4);
}
[data-bs-theme=dark] [data-type=after] mark {
  background-color: rgba(51, 247, 160, .1); /* different green for dark mode; looks better */
}
.unified .diff {
  flex-wrap: wrap;
}
.diff-left, .diff-right {
  flex: 1;
  overflow: hidden;
}
.unified .diff-left, .unified .diff-right {
  flex-basis: 100%;
}
.patch {
  margin: 12px 0;
}
.patch-name {
  font-size: 18px;
  font-weight: bold;
}
.patch-hash {
  font-size: 14px;
  margin: 6px 0;
}
.clone {
  font-family: monospace;
  font-size: 12px;
}
.popover {
  max-width: 100%;
}