make clone work, add static JS files for frontend

79d4a6719b7cafb44aea60b1544f0de50c471a24

Tucker McKnight <tucker.mcknight@gmail.com> | Sun Aug 24 2025

make clone work, add static JS files for frontend
frontend/main.ts: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
83
84
85
86
87
(function() {
  const setCheckbox = (<any>window).setCheckbox
  const currentTheme = (<any>window).currentTheme

  setCheckbox(currentTheme, document.getElementById('dark-mode-switch'))

  const copyCommand = (event) => {
    const elem = event.target
    const cloneText = elem.dataset.cloneUrl
    const originalInnerText = elem.innerText
    navigator.clipboard.writeText(cloneText).then(() => {
      elem.innerText = "Copied"
    })

    window.setTimeout(() => {
      elem.innerText = originalInnerText
    }, 5000)
  }

  const createClonePopover = (currentRepo: string) => {
    const div = document.createElement('div')
    div.id = "clone-popover"
    div.innerHTML = jsVars.cloneDiv
    const popoverBtn = document.getElementById("clone-popover-btn")
    const bsPopover = new bootstrap.Popover(popoverBtn, {
      sanitize: false,
      html: true,
      content: div,
      title: 'Clone',
    })

    div.querySelector("#clone-button").addEventListener('click', copyCommand)

    document.body.addEventListener('click', (event) => {
      const target = event.target as HTMLElement
      // If they didn't click the #clone-popover-btn or if we're not inside of
      // popover, or if we *are* inside of a popover but a different one than the
      // current one, then close the popover.
      const parentPopover = target.closest(".popover")
      if (
        target.id !== "clone-popover-btn"
        && (
          parentPopover === null
          || parentPopover !== bsPopover.tip)
      ) {
        bsPopover.hide()
      }
    })
  }

  const toggleLastTouch = (event) => {
    const isOn = event.target.checked
    const annotations = document.getElementById("annotations")
    if (isOn) {
      annotations.classList.remove("d-none")
    } else {
      annotations.classList.add("d-none")
    }
  }

  document.getElementById("showLastTouch")?.addEventListener('click', toggleLastTouch)

  const copyPull = (event) => {
    const hash = event.target.dataset.hash
    const isDarcs = event.target.dataset.vcs === "darcs"
    const copiedPrefix = isDarcs ? `darcs pull ${jsVars.baseUrl} -h ` : ""
    const originalInnerHtml = event.target.innerHTML
    const copiedAlert = document.createElement('span')
    copiedAlert.innerText = "Copied"

    navigator.clipboard.writeText(`${copiedPrefix}${hash}`).then(() => {
      event.target.parentElement.appendChild(copiedAlert)
    })

    window.setTimeout(() => {
      copiedAlert.remove()
    }, 5000)
  }

  document.querySelectorAll(".copy-btn").forEach((element) => {
    element.addEventListener("click", copyPull)
  })

  const bootstrap = (<any>window).bootstrap
  const jsVars = (<any>window).jsVars

  if (jsVars.nav.repoName) {
    createClonePopover(jsVars.nav.repoName)
  }
}())
frontend/top.ts: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
(function() {
  const htmlElem = document.querySelector('html')
  window['setMode'] = (mode: string) => {
    localStorage.setItem('theme', mode)
    if (mode === 'dark') {
      htmlElem.setAttribute('data-bs-theme', mode)
    }
    else if (mode === 'light') {
      htmlElem.setAttribute('data-bs-theme', mode)
    }
    else if (mode === 'auto') {

      const preferred = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
      htmlElem.setAttribute('data-bs-theme', preferred)
    }
    window['currentTheme'] = mode
  };

  window['currentTheme'] = localStorage.getItem("theme") || "auto"

  window['setMode'](window['currentTheme'])

  window['setCheckbox'] = (mode, element) => {
    if (mode === 'light') {
      element.innerHTML = `<i class="bi bi-brightness-high"></i>`
    }
    if (mode === 'dark') {
      element.innerHTML = `<i class="bi bi-moon"></i>`
    }
    if (mode === 'auto') {
      element.innerHTML = `<i class="bi bi-yin-yang"></i>`
    }
    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
    window['setMode'](clickedOption)
    window['setCheckbox'](clickedOption, document.getElementById('dark-mode-switch'))
  }
}())
frontend/tsconfig.json:0
Before


⁣
⁣
⁣
⁣
After
-1
0
1
2
{
  "compilerOptions": {
    "outDir": "../dist/frontend",
  }
}
main.ts:21
Before
20
21
22















































23
24
    return pathParts[pathParts.length - 1]
  })

⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
  eleventyConfig.addFilter("getDirectoryContents", (repo, branch, dirPath) => {
    return reposData[repo].branches[branch].files.filter(file => file.startsWith(dirPath) && file !== dirPath)
  })
After
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
    return pathParts[pathParts.length - 1]
  })

  const vendor = `${__dirname}/vendor`
  const frontend = `${__dirname}/frontend`

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

  eleventyConfig.on(
    "eleventy.after",
    async ({ directories, results, runMode, outputMode }) => {
      // Check to see if there is already a repo in all of the locations
      // that should have one.
      for (let repoName in reposConfiguration.repos) {
        const repoConfig = reposConfiguration.repos[repoName]
        if (repoConfig._type === "darcs") {
          for (let branch in repoConfig.branches) {
            const repoPath = eleventyConfig.dir.output + "/repos/" + eleventyConfig.getFilter("slugify")(repoName) + "/branches/" + branch
            const originalLocation = repoConfig.branches[branch].location
            // If it is there, do darcs pull
            if (fsImport.existsSync(repoPath + "/_darcs")) {
              await exec(`(cd ${repoPath} && darcs pull --no-interactive)`)
            } else {
              // If it is not there, do darcs clone
              await exec(`(cd ${repoPath} && mkdir -p temp; darcs clone ${originalLocation} temp/${branch} --no-working-dir; mv temp/${branch}/_darcs .; rm -R temp)`)
            }
          }
        } else if (repoConfig._type === "git") {
          const repoPath = eleventyConfig.dir.output + "/repos/" + eleventyConfig.getFilter("slugify")(repoName)
          const gitRepoName = eleventyConfig.getFilter("slugify")(repoName) + ".git"
          // If it is there, do git pull
          if (fsImport.existsSync(repoPath + ".git")) {
            // git repos are just in the repos folder, not in their subdir
            // create string of commands saying 'git fetch origin branch:branch' for each branch
            const fetchCommands = repoConfig.branchesToPull.map(branch => `git fetch origin ${branch}:${branch}`).join('; ')
            await exec(`(cd ${eleventyConfig.dir.output + "/repos/" + gitRepoName} && ${fetchCommands}; git update-server-info)`)
          } else {
            // If it is not there, do git clone
            const originalLocation = repoConfig.location
            await exec(`(cd ${eleventyConfig.dir.output + "/repos/"} && git clone ${originalLocation} ${gitRepoName} --bare)`)
            await exec(`(cd ${eleventyConfig.dir.output + "/repos/" + gitRepoName} && git update-server-info)`)
          }
        }
      }
    }
  );

  eleventyConfig.addFilter("getDirectoryContents", (repo, branch, dirPath) => {
    return reposData[repo].branches[branch].files.filter(file => file.startsWith(dirPath) && file !== dirPath)
  })
main.ts:166
Before
165
166
167
168
169
170
        size: 1,
        alias: "branchInfo",
      },
      branches: branchesData,
      permalink: (data) => {
        const repoName = data.branchInfo.repoName
        const branchName = data.branchInfo.branchName
After
165
166
167

168
169
        size: 1,
        alias: "branchInfo",
      },
⁣
      permalink: (data) => {
        const repoName = data.branchInfo.repoName
        const branchName = data.branchInfo.branchName
main.ts:221
Before
220
221
222
223
224
225
        size: 1,
        alias: "branchInfo",
      },
      branches: branchesData,
      permalink: (data) => {
        const repoName = data.branchInfo.repoName
        const branchName = data.branchInfo.branchName
After
220
221
222

223
224
        size: 1,
        alias: "branchInfo",
      },
⁣
      permalink: (data) => {
        const repoName = data.branchInfo.repoName
        const branchName = data.branchInfo.branchName
main.ts:248
Before
247
248
249
250
251
252
        size: 1,
        alias: "branch",
      },
      branches: branchesData,
      permalink: (data) => {
        const repoName = data.branch.repoName
        const branchName = data.branch.branchName
After
247
248
249

250
251
        size: 1,
        alias: "branch",
      },
⁣
      permalink: (data) => {
        const repoName = data.branch.repoName
        const branchName = data.branch.branchName
main.ts:265
Before
264
265
266
267
  )
  eleventyConfig.addGlobalData("repos", reposData)
  eleventyConfig.addGlobalData("reposConfig", reposConfiguration)

⁣
}
After
264
265
266
267
268
  )
  eleventyConfig.addGlobalData("repos", reposData)
  eleventyConfig.addGlobalData("reposConfig", reposConfiguration)
  eleventyConfig.addGlobalData("branches", branchesData)

}
make.sh:5
Before
4
5
6
mkdir dist/partial_templates
cp templates/*.njk dist/templates
cp partial_templates/*.njk dist/partial_templates
⁣
After
4
5
6
7
mkdir dist/partial_templates
cp templates/*.njk dist/templates
cp partial_templates/*.njk dist/partial_templates
cp -r vendor dist/
package.json:3
Before
2
3
4
5
6
7
  "version": "1.0.0",
  "main": "dist/main.js",
  "scripts": {
    "build": "./make.sh && npx tsc"
  },
  "keywords": [],
  "author": "",
After
2
3
4
5
6
7
  "version": "1.0.0",
  "main": "dist/main.js",
  "scripts": {
    "build": "./make.sh && npx tsc && npx tsc --project frontend"
  },
  "keywords": [],
  "author": "",
partial_templates/main_bottom.njk:24
Before
23
24
25
26
27
        window.location = `/repos/${values[0]}/branches/${values[1]}/${values[2]}`
      }
    </script>
    <script src="/static/main.js"></script>
  </body>
</html>
After
23
24
25
26
27
        window.location = `/repos/${values[0]}/branches/${values[1]}/${values[2]}`
      }
    </script>
    <script src="/frontend/main.js"></script>
  </body>
</html>
partial_templates/main_top.njk:29
Before
28
29
30
31
32
33
34
    
      {% endif %}
    </script>
    <script src="/static/top.js"></script>
    <link rel="stylesheet" id="prism-theme" type="text/css" href="/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>
After
28
29
30
31
32
33
34
    
      {% 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>
vendor/prism.css:0
Before

⁣
⁣
⁣
After
-1
0
1
/* PrismJS 1.30.0
https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript&plugins=line-numbers+keep-markup */
code[class*=language-],pre[class*=language-]{color:#000;background:0 0;text-shadow:0 1px #fff;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{text-shadow:none;background:#b3d4fc}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{text-shadow:none;background:#b3d4fc}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:none}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#708090}.token.punctuation{color:#999}.token.namespace{opacity:.7}.token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{color:#905}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#690}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url{color:#9a6e3a;background:hsla(0,0%,100%,.5)}.token.atrule,.token.attr-value,.token.keyword{color:#07a}.token.class-name,.token.function{color:#dd4a68}.token.important,.token.regex,.token.variable{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}
pre[class*=language-].line-numbers{position:relative;padding-left:3.8em;counter-reset:linenumber}pre[class*=language-].line-numbers>code{position:relative;white-space:inherit}.line-numbers .line-numbers-rows{position:absolute;pointer-events:none;top:0;font-size:100%;left:-3.8em;width:3em;letter-spacing:-1px;border-right:1px solid #999;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.line-numbers-rows>span{display:block;counter-increment:linenumber}.line-numbers-rows>span:before{content:counter(linenumber);color:#999;display:block;padding-right:.8em;text-align:right}
vendor/prism_dark.css:0
Before

⁣
⁣
⁣
After
-1
0
1
/* PrismJS 1.30.0
https://prismjs.com/download.html#themes=prism-okaidia&languages=markup+css+clike+javascript&plugins=line-numbers+keep-markup */
code[class*=language-],pre[class*=language-]{color:#f8f8f2;background:0 0;text-shadow:0 1px rgba(0,0,0,.3);font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto;border-radius:.3em}:not(pre)>code[class*=language-],pre[class*=language-]{background:none}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#8292a2}.token.punctuation{color:#f8f8f2}.token.namespace{opacity:.7}.token.constant,.token.deleted,.token.property,.token.symbol,.token.tag{color:#f92672}.token.boolean,.token.number{color:#ae81ff}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#a6e22e}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url,.token.variable{color:#f8f8f2}.token.atrule,.token.attr-value,.token.class-name,.token.function{color:#e6db74}.token.keyword{color:#66d9ef}.token.important,.token.regex{color:#fd971f}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}
pre[class*=language-].line-numbers{position:relative;padding-left:3.8em;counter-reset:linenumber}pre[class*=language-].line-numbers>code{position:relative;white-space:inherit}.line-numbers .line-numbers-rows{position:absolute;pointer-events:none;top:0;font-size:100%;left:-3.8em;width:3em;letter-spacing:-1px;border-right:1px solid #999;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.line-numbers-rows>span{display:block;counter-increment:linenumber}.line-numbers-rows>span:before{content:counter(linenumber);color:#999;display:block;padding-right:.8em;text-align:right}