Turn some NJK templates into typescript files

88b515f196bbaeb8cc0cdcdc1768f0ba15622a50

Tucker McKnight <tucker@pangolin.lan> | Sun Dec 21 2025

Turn some NJK templates into typescript files

Added an htmlPage wrapper for the common header/nav elements.

Repo, files, and file pages done so far.
frontend/main.js:1
Before
0
1
2
import {branchesListItems} from '../dist/js_templates/repo.js'

const setCheckbox = window.setCheckbox
const currentTheme = window.currentTheme
After
0
1
2
import {branchesListItems} from '../dist/js_templates/common/htmlPage.js'

const setCheckbox = window.setCheckbox
const currentTheme = window.currentTheme
frontend/top.js:16
Before
15
16
17
18
19

20
    window['currentTheme'] = mode
  };

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

⁣
  window['setMode'](window['currentTheme'])
After
15
16
17
18
19
20
21
    window['currentTheme'] = mode
  };

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

  window['setMode'](window['currentTheme'])
js_templates/common/htmlPage.ts:55
Before
54
55
56
57
58
59
                      <a class="nav-link" href="${nav.rootPath()}">&larr; All repositories</a>
                    </li>
                    <li class="nav-item">
                      <a class="nav-link" href="#">${repo.name}</a>
                    </li>
                    <li class="nav-item">
                      <span class="nav-link d-inline-block">Branch:</span>
After
54
55
56
57
58
59
                      <a class="nav-link" href="${nav.rootPath()}">&larr; All repositories</a>
                    </li>
                    <li class="nav-item">
                      <a class="nav-link" href="${nav.repoCurrentBranchHome()}">${repo.name}</a>
                    </li>
                    <li class="nav-item">
                      <span class="nav-link d-inline-block">Branch:</span>
js_templates/common/htmlPage.ts:104
Before
103
104
105
106
107
108
109
110
111
                <nav class="navbar navbar-expand">
                  <ul class="main-nav navbar-nav flex-wrap">
                    <li class="nav-item">
                      <a class="nav-link active" aria-current="page" href="${nav.repoCurrentBranchHome()}">Home</a>
                    </li>
                    <li class="nav-item">
                      <a class="nav-link" href="${nav.repoCurrentBranchFiles()}">Files</a>
                    </li>
                    <li class="nav-item">
                      <a class="nav-link" href="${nav.repoCurrentBranchCommits()}">Commits</a>
After
103
104
105
106
107
108
109
110
111
                <nav class="navbar navbar-expand">
                  <ul class="main-nav navbar-nav flex-wrap">
                    <li class="nav-item">
                      <a class="nav-link ${data.navTab === 'home' ? 'active' : ''}" aria-current="page" href="${nav.repoCurrentBranchHome()}">Home</a>
                    </li>
                    <li class="nav-item">
                      <a class="nav-link ${data.navTab === 'files' ? 'active' : ''}" href="${nav.repoCurrentBranchFiles()}">Files</a>
                    </li>
                    <li class="nav-item">
                      <a class="nav-link" href="${nav.repoCurrentBranchCommits()}">Commits</a>
js_templates/file.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
88
89
90
export default async (eleventyConfig: any, data: any) => {
  const getFileName = eleventyConfig.getFilter("getFileName")
  const isDirectory = eleventyConfig.getFilter("isDirectory")
  const topLevelFilesOnly = eleventyConfig.getFilter("topLevelFilesOnly")
  const getDirectoryContents = eleventyConfig.getFilter("getDirectoryContents")
  const getFileContents = eleventyConfig.getFilter("getFileContents")
  const getFileLastTouchInfo = eleventyConfig.getFilter("getFileLastTouchInfo")
  const getRelativePath = eleventyConfig.getFilter("getRelativePath")
  const slugify = eleventyConfig.getFilter("slugify")
  const lineNumbers = eleventyConfig.getFilter("lineNumbers")
  const highlightCode = eleventyConfig.getFilter("highlightCode")
  const languageExtension = eleventyConfig.getFilter("languageExtension")

  return `
    <h3>./${data.fileInfo.file}</h3>
    <p>Files snapshot from <span class="font-monospace">${data.fileInfo.branchName}</span></p>
    ${isDirectory(data.fileInfo.file, data.fileInfo.repoName, data.fileInfo.branchName) ?
    `<ul class="list-group">
    ${topLevelFilesOnly(getDirectoryContents(data.fileInfo.repoName, data.fileInfo.branchName, data.fileInfo.file), data.fileInfo.file + '/').map((dir) => {
      return `<li class="list-group-item">
        ${dir.isDirectory ?
        `<i class="bi bi-folder-fill"></i>`
        : `<i class="bi bi-file-earmark"></i>`}
        <a href="${data.reposPath}/${slugify(data.fileInfo.repoName)}/branches/${slugify(data.fileInfo.branchName)}/files/${slugify(dir.fullPath)}.html">${getRelativePath(data.fileInfo.file, dir.name)}</a>
      </li>`
    }).join('')}
    </ul>`
    :
    `<div class="row py-2">
      <div class="col-auto">
        <div class="form-check form-switch">
          <input class="form-check-input" type="checkbox" role="switch" id="showLastTouch">
          <label class="form-check-label" for="showLastTouch">Show last line change</label>
        </div>
      </div>
      <div class="col">
        <a href="${data.reposPath}/${slugify(data.fileInfo.repoName)}/branches/${slugify(data.fileInfo.branchName)}/raw/${slugify(data.fileInfo.file)}">View raw file</a>
      </div>
    </div>
    <div class="row">
      <div class="col-auto p-0">
        <code style="white-space: pre;"><pre class="language-text">${lineNumbers(await getFileContents(data.fileInfo.repoName, data.fileInfo.branchName, data.fileInfo.file)).map((lineNumber) => {
          return lineNumber
        }).join('\n')}</pre></code>
      </div>
      <div id="annotations" class="col-auto d-none p-0">
        <code style="white-space: pre;"><pre class="language-text">${
          (await getFileLastTouchInfo(
            data.fileInfo.repoName,
            data.fileInfo.branchName,
            data.fileInfo.file
          )).map(
            (annotation) => {
              return `<a href="${data.reposPath}/${slugify(data.fileInfo.repoName)}/branches/${slugify(data.fileInfo.branchName)}/patches/${annotation.sha}">${annotation.sha.substr(0, 6)}</a> ${annotation.author}`
            }
          ).join('\n')
        }</pre></code>
      </div>
      <div class="col overflow-scroll p-0">
        <code>
          ${
            highlightCode(
              await getFileContents(
                data.fileInfo.repoName,
                data.fileInfo.branchName,
                data.fileInfo.file
              ),
              languageExtension(
                data.fileInfo.file,
                data.fileInfo.repoName
              )
            )
          }
        </code>
      </div>
    </div>
    `}
    
    <script type="text/javascript">
      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)
    </script>
  `
}
js_templates/files.ts:1
Before

0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
⁣
⁣
export default async (eleventyConfig: any, data: any) => {
  const branch: Repository['branches'][0] = data.currentBranch
  const topLevelFilesOnly = eleventyConfig.getFilter("topLevelFilesOnly")
  const slugify = eleventyConfig.getFilter("slugify")

  const files = topLevelFilesOnly('', branch.fileList)

  return `
    <div class="row">
      <div class="col">
        <ul class="list-group">
        ${files.map(files) => {
            return `
              <li class="list-group-item">
                ${file.isDirectory ? '<i class="bi bi-folder-fill"></i>' : '<i class="bi bi-file-earmark"></i>'
                <a href="${data.reposPath}/${slugify(data.branchInfo.repoName)}/branches/${slugify(branchInfo.branchName)}/files/${slugify(file.fullPath)}.html">${file.name}</a>
              </li>
            `
        }}
        </ul>
      </div>
    </div>
  `
}
After
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21


22
import { type SortedFileList, type Repository } from "../src/dataTypes.ts"

export default async (eleventyConfig: any, data: any) => {
  const branch: Repository['branches'][0] = data.currentBranch
  const topLevelFilesOnly = eleventyConfig.getFilter("topLevelFilesOnly")
  const slugify = eleventyConfig.getFilter("slugify")

  const files: SortedFileList = topLevelFilesOnly(branch.fileList, '')

  return `
    <h3>./</h3>
    <p>Files snapshot from <span class="font-monospace">${data.branchInfo.branchName}</span></p>
    <ul class="list-group">
    ${files.map((file) => {
        return `
          <li class="list-group-item">
            ${file.isDirectory ? '<span>&#x1F4C1;</span>' : ''}
            <a href="${data.reposPath}/${slugify(data.branchInfo.repoName)}/branches/${slugify(data.branchInfo.branchName)}/files/${slugify(file.fullPath)}.html">${file.name}</a>
          </li>
        `
    }).join('')}
    </ul>
⁣
⁣
  `
}
js_templates/repo.ts:1
Before
0
1
2
import { type ReposConfiguration } from '../src/configTypes.ts'
import { type Repository } from '../src/dataTypes.ts'

export default async (eleventyConfig: any, data: any) => {
After
0
1
i⁣
mport { type Repository } from '../src/dataTypes.ts'

export default async (eleventyConfig: any, data: any) => {
main.ts:9
Before
8
9
10

11
12
13
14
15


16
import {getLocation} from './src/helpers.ts'
import * as operations from './src/vcses/git/operations.ts'
import {ReposConfiguration} from './src/configTypes.ts'
⁣
import {Ajv} from 'ajv'
import ConfigSchema from './schemas/ReposConfiguration.json' with { type: 'json' }
import htmlPage from './js_templates/common/htmlPage.ts'
import repoJsTemplate from './js_templates/repo.ts'

⁣
⁣
const ajv = new Ajv()
const exec = util.promisify(childProcess.exec)
After
8
9
10
11
12
13
14
15
16
17
18
19
import {getLocation} from './src/helpers.ts'
import * as operations from './src/vcses/git/operations.ts'
import {ReposConfiguration} from './src/configTypes.ts'
import { type SortedFileList } from './src/dataTypes.ts'
import {Ajv} from 'ajv'
import ConfigSchema from './schemas/ReposConfiguration.json' with { type: 'json' }
import htmlPage from './js_templates/common/htmlPage.ts'
import repoJsTemplate from './js_templates/repo.ts'
import filesJsTemplate from './js_templates/files.ts'
import fileJsTemplate from './js_templates/file.ts'

const ajv = new Ajv()
const exec = util.promisify(childProcess.exec)
main.ts:113
Before
112
113
114






115
116
  })

  eleventyConfig.addFilter("lineNumbers", (code: string) => {
⁣
⁣
⁣
⁣
⁣
⁣
    const numLines = code.split('\n').length
    const lineNumbers = []
    for (let i = 1; i <= numLines; i++) {
After
112
113
114
115
116
117
118
119
120
121
122
  })

  eleventyConfig.addFilter("lineNumbers", (code: string) => {
    try {
      code.split('')
    }
    catch (error) {
      console.log(code)
    }
    const numLines = code.split('\n').length
    const lineNumbers = []
    for (let i = 1; i <= numLines; i++) {
main.ts:149
Before
148
149
150
151
152
153
    return extensionsConfig && extensionsConfig[extension] ? extensionsConfig[extension] : extension
  })

  eleventyConfig.addFilter("topLevelFilesOnly", (files: Array<string>, currentLevel: string) => {
    const onlyUnique = (value, index, array) => {
      return array.findIndex(test => test.name === value.name) === index;
    }
After
148
149
150
151
152
153
    return extensionsConfig && extensionsConfig[extension] ? extensionsConfig[extension] : extension
  })

  eleventyConfig.addFilter("topLevelFilesOnly", (files: Array<string>, currentLevel: string): SortedFileList => {
    const onlyUnique = (value, index, array) => {
      return array.findIndex(test => test.name === value.name) === index;
    }
main.ts:286
Before
285
286
287
288
289
290
291
292
293
294
295
    }
  )

  // FILE.NJK
  const fileTemplate = fsImport.readFileSync(`${import.meta.dirname}/templates/file.njk`).toString()
  const flatFilesData = flatFiles(reposData)
  eleventyConfig.addTemplate(
    'repos/file.njk',
    topLayoutPartial + fileTemplate + bottomLayoutPartial,
    {
      pagination: {
        data: "flatFiles",
After
285
286
287
288

289
290
291
292
293
294
    }
  )

  // FILE.TS
⁣
  const flatFilesData = flatFiles(reposData)
  eleventyConfig.addTemplate(
    'repos/file.11ty.js',
    htmlPage(reposConfiguration, eleventyConfig, fileJsTemplate),
    {
      pagination: {
        data: "flatFiles",
main.ts:344
Before
343
344
345
346
347
348
349
350
351
352
    }
  )

  // FILES.NJK
  const filesTemplate = fsImport.readFileSync(`${import.meta.dirname}/templates/files.njk`).toString()
  eleventyConfig.addTemplate(
    'repos/files.njk',
    topLayoutPartial + filesTemplate + bottomLayoutPartial,
    {
      pagination: {
        data: "branches",
After
343
344
345
346

347
348
349
350
351
    }
  )

  // FILES.TS
⁣
  eleventyConfig.addTemplate(
    'repos/files.11ty.js',
    htmlPage(reposConfiguration, eleventyConfig, filesJsTemplate),
    {
      pagination: {
        data: "branches",
main.ts:379
Before
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
    }
  )

  // REPO.NJK
  const repoTemplate = fsImport.readFileSync(`${import.meta.dirname}/templates/repo.njk`).toString()
  eleventyConfig.addTemplate(
    'repos/repo.njk',
    topLayoutPartial + repoTemplate + bottomLayoutPartial,
    {
      pagination: {
        data: "branches",
        size: 1,
        alias: "branch",
      },
      permalink: (data) => {
        const repoName = data.branch.repoName
        const branchName = data.branch.branchName
        return `${reposPath}/${eleventyConfig.getFilter("slugify")(repoName)}/branches/${eleventyConfig.getFilter("slugify")(branchName)}/`
      },
      eleventyComputed: {
        nav: {
          repoName: (data) => data.branch.repoName,
          branchName: (data) => data.branch.branchName,
          path: "",
        },
        currentRepo: (data) => reposData.find(repo => {
          return repo.name === data.branch.repoName
        }),
        currentBranch: (data) => reposData.find(repo => {
          return repo.name === data.branch.repoName
        }).branches.find(branch => {
          return branch.name === data.branch.branchName
        }),
      },
      navTab: "landing",
    }
  )

  // REPO.TS
  const template = htmlPage(reposConfiguration, eleventyConfig, repoJsTemplate)

  eleventyConfig.addTemplate(
    'repos/repo.11ty.js',
    template,
    {
      pagination: {
        data: "branches",
After
378
379
380
381

382
383



































384

385
386
    }
  )

  // REPO.TS
⁣
  eleventyConfig.addTemplate(
    'repos/repo.11ty.js',
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
    htmlPage(reposConfiguration, eleventyConfig, repoJsTemplate),
⁣
    {
      pagination: {
        data: "branches",
main.ts:429
Before
428
429
430
431
432
433
      permalink: (data) => {
        const repoName = data.branch.repoName
        const branchName = data.branch.branchName
        return `${reposPath}/${eleventyConfig.getFilter("slugify")(repoName)}/new/${eleventyConfig.getFilter("slugify")(branchName)}/`
      },
      eleventyComputed: {
        nav: {
After
428
429
430
431
432
433
      permalink: (data) => {
        const repoName = data.branch.repoName
        const branchName = data.branch.branchName
        return `${reposPath}/${eleventyConfig.getFilter("slugify")(repoName)}/branches/${eleventyConfig.getFilter("slugify")(branchName)}/`
      },
      eleventyComputed: {
        nav: {
main.ts:446
Before
445
446
447

448
449
          return branch.name === data.branch.branchName
        }),
      },
⁣
    }
  )
After
445
446
447
448
449
450
          return branch.name === data.branch.branchName
        }),
      },
      navTab: "home"
    }
  )
scss/design-board.scss:11
Before
10
11
12
13
14
15
16
$enable-transitions: false;

$gray: #EFF2E4; /* this is... not gray? */
$blue: #7388FA;
$lightblue: #73B2FA;
$purple: #852cdf;
$lilac: #E273FA;
$magenta: #FF549B;
After
10
11
12
13
14
15
16
$enable-transitions: false;

$gray: #EFF2E4; /* this is... not gray? */
$blue: #556DF0;
$lightblue: #7388FA;
$purple: #852cdf;
$lilac: #E273FA;
$magenta: #FF549B;
scss/design-board.scss:30
Before
29
30
31
32




33
34
35
36
37
38
39
40

@import "../node_modules/bootstrap/scss/bootstrap";


⁣
⁣
⁣
⁣
.bezel {
  background-color: $blue;
  border-top: 6px solid color.adjust($blue, $lightness: 15%);
  border-left: 6px solid color.adjust($blue, $lightness: 13%);
  border-bottom: 6px solid color.adjust($blue, $lightness: -10%);
  border-right: 6px solid color.adjust($blue, $lightness: -13%);
}

.bezel-purple {
After
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

@import "../node_modules/bootstrap/scss/bootstrap";

.list-group {
  --bs-list-group-border-width: 1px;
  --bs-list-group-border-color: #BAC1CA;
}

.bezel {
  background-color: $lightblue;
  border-top: 6px solid color.adjust($lightblue, $lightness: 15%);
  border-left: 6px solid color.adjust($lightblue, $lightness: 13%);
  border-bottom: 6px solid color.adjust($lightblue, $lightness: -10%);
  border-right: 6px solid color.adjust($lightblue, $lightness: -13%);
}

.bezel-purple {
src/dataTypes.ts:27
Before
26
27





    }>
  }>,
⁣
⁣
⁣
⁣
⁣
⁣
}
After
26
27
28
29
30
31
32
33
    }>
  }>,
}

export type SortedFileList = Array<{
  name: string,
  fullPath: string,
  isDirectory: boolean,
}>