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.
0 1 2
import {branchesListItems} from '../dist/js_templates/repo.js'
const setCheckbox = window.setCheckbox
const currentTheme = window.currentTheme0 1 2
import {branchesListItems} from '../dist/js_templates/common/htmlPage.js'
const setCheckbox = window.setCheckbox
const currentTheme = window.currentTheme15 16 17 18 19 20
window['currentTheme'] = mode
};
window['currentTheme'] = localStorage.getItem("theme") || "auto"
window['setMode'](window['currentTheme'])
15 16 17 18 19 20 21
window['currentTheme'] = mode
};
// window['currentTheme'] = localStorage.getItem("theme") || "auto"
window['currentTheme'] = "light"
window['setMode'](window['currentTheme'])
54 55 56 57 58 59
<a class="nav-link" href="${nav.rootPath()}">← 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>54 55 56 57 58 59
<a class="nav-link" href="${nav.rootPath()}">← 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>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>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>
-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>
`
}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>
`
}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>📁</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>
`
}0 1 2
import { type ReposConfiguration } from '../src/configTypes.ts'
import { type Repository } from '../src/dataTypes.ts'
export default async (eleventyConfig: any, data: any) => {0 1
i
mport { type Repository } from '../src/dataTypes.ts'
export default async (eleventyConfig: any, data: any) => {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)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)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++) {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++) {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;
}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;
}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",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",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",343 344 345 346 347 348 349 350 351
}
)
// FILES.TS
eleventyConfig.addTemplate(
'repos/files.11ty.js',
htmlPage(reposConfiguration, eleventyConfig, filesJsTemplate),
{
pagination: {
data: "branches",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",378 379 380 381 382 383 384 385 386
}
)
// REPO.TS
eleventyConfig.addTemplate(
'repos/repo.11ty.js',
htmlPage(reposConfiguration, eleventyConfig, repoJsTemplate),
{
pagination: {
data: "branches",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: {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: {445 446 447 448 449
return branch.name === data.branch.branchName
}),
},
}
)
445 446 447 448 449 450
return branch.name === data.branch.branchName
}),
},
navTab: "home"
}
)
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;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;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 {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 {26 27
}>
}>,
}26 27 28 29 30 31 32 33
}>
}>,
}
export type SortedFileList = Array<{
name: string,
fullPath: string,
isDirectory: boolean,
}>