Branch

make clone work, add static JS files for frontend

Sun Aug 24 2025

Tucker McKnight <tucker.mcknight@gmail.com>


    

79d4a6719b7cafb44aea60b1544f0de50c471a24

Side-by-side
Stacked
frontend/main.ts:0
Before
0
After
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 88 89
(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
0
After
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
(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
0
After
0 1 2 3 4
{
  "compilerOptions": {
    "outDir": "../dist/frontend",
  }
}
main.ts:21
Before
21
After
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
  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)`)
          }
        }
      }
    }
  );
main.ts:166
Before
166
      branches: branchesData,
After
166
main.ts:221
Before
221
      branches: branchesData,
After
221
main.ts:248
Before
248
      branches: branchesData,
After
248
main.ts:265
Before
265
After
265
  eleventyConfig.addGlobalData("branches", branchesData)
make.sh:5
Before
5
After
5
cp -r vendor dist/
package.json:3
Before
3
    "build": "./make.sh && npx tsc"
After
3
    "build": "./make.sh && npx tsc &amp;& npx tsc --project frontend&quot;
partial_templates/main_bottom.njk:24
Before
24
    <script src="/static/main.js"></script>
After
24
    <script src="/frontend/main.js"></script>
partial_templates/main_top.njk:29
Before
29 30
    <script src="/static/top.js"></script>
    <link rel="stylesheet" id="prism-theme" type="text/css" href="/prism.css" />
After
29 30
    <script src="/frontend/top.js"></script>
    <link rel="stylesheet" id="prism-theme" type="text/css" href="/vendor/prism.css" />
vendor/prism.css:0
Before
0
After
0 1 2 3
/* 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
0
After
0 1 2 3
/* 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}