Tucker McKnight <tucker@pangolin.lan> | Mon Feb 02 2026
Remove lodash, replace with escape-html escape-html is a comparatively tiny library. All I was using of lodash was just the HTML escape function. Incidentally, I fixed a bug that was happening that would sometimes cause escaped HTML characters to show up. This was because a <mark> was being inserted for the red/green diff highlighting, and it was being inserted in between the characters for an escape character. E.g. something like &<mark>lt; which would then not get rendered as a < because it doesn't actually say <. Doing the diff first, with the HTML characters in the string, and *then* escaping them afterwards fixes this.
0 1 2 3 4
import m from 'mithril'
import render from 'mithril-node-render'
import _ from 'lodash'
import { type Repository } from '../src/dataTypes.ts'
import branches from '../src/branches.ts'
import { dateToRfc3339 } from '@11ty/eleventy-plugin-rss'0 1 2 3
import m from 'mithril'
import render from 'mithril-node-render'
import { type Repository } from '../src/dataTypes.ts'
import branches from '../src/branches.ts'
import { dateToRfc3339 } from '@11ty/eleventy-plugin-rss'25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
m('updated', dateToRfc3339(currentRepo.commits.get(currentBranch.head).date)),
m('id', data.reposConfig.baseUrl),
m('author',
m('name', `${_.escape(currentRepo.name)} contributors`)
),
currentBranchCommits.map((commit) => {
const commitUrl = data.reposConfig.baseUrl + '/repos/' + slugify(branch.repoName) + '/branches/' + slugify(branch.branchName) + '/commits/' + commit.commit.hash
return m('entry', [
m('title', _.escape(commit.commit.message.split('\n')[0])),
m('author', m('name', _.escape(commit.commit.author))),
m.trust(`<link href="${commitUrl}" />`),
m('updated', dateToRfc3339(commit.commit.date)),
m('id', commitUrl),
m('content', {type: "text"}, _.escape(commit.commit.message)),
])
})
])25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
m('updated', dateToRfc3339(currentRepo.commits.get(currentBranch.head).date)),
m('id', data.reposConfig.baseUrl),
m('author',
m('name', `${currentRepo.name} contributors`)
),
currentBranchCommits.map((commit) => {
const commitUrl = data.reposConfig.baseUrl + '/repos/' + slugify(branch.repoName) + '/branches/' + slugify(branch.branchName) + '/commits/' + commit.commit.hash
return m('entry', [
m('title', commit.commit.message.split('\n')[0]),
m('author', m('name', commit.commit.author)),
m.trust(`<link href="${commitUrl}" />`),
m('updated', dateToRfc3339(commit.commit.date)),
m('id', commitUrl),
m('content', {type: "text"}, commit.commit.message),
])
})
])11 12 13 14 15 16
"@11ty/eleventy-plugin-rss": "^2.0.4",
"ajv": "^8.17.1",
"diff": "^8.0.2",
"lodash": "^4.17.21",
"minimatch": "^10.1.1",
"mithril": "^2.3.8",
"mithril-node-render": "^3.0.2"11 12 13 14 15 16
"@11ty/eleventy-plugin-rss": "^2.0.4",
"ajv": "^8.17.1",
"diff": "^8.0.2",
"escape-html": "^1.0.3",
"minimatch": "^10.1.1",
"mithril": "^2.3.8",
"mithril-node-render": "^3.0.2"1079 1080 1081 1082 1083
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/evaluate-value": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/evaluate-value/-/evaluate-value-2.0.0.tgz",1079 1080 1081 1082 1083 1084 1085 1086 1087 1088
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
},
"node_modules/evaluate-value": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/evaluate-value/-/evaluate-value-2.0.0.tgz",1281 1282 1283 1284 1285 1286 1287 1288 1289 1290
"resolved": "https://registry.npmjs.org/list-to-array/-/list-to-array-1.1.0.tgz",
"integrity": "sha512-+dAZZ2mM+/m+vY9ezfoueVvrgnHIGi5FvgSymbIgJOFwiznWyA59mav95L+Mc6xPtL3s9gm5eNTlNtxJLbNM1g=="
},
"node_modules/lodash": {
"version": "4.17.23",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="
},
"node_modules/lru-cache": {
"version": "11.2.5",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz",1281 1282 1283 1284 1285
"resolved": "https://registry.npmjs.org/list-to-array/-/list-to-array-1.1.0.tgz",
"integrity": "sha512-+dAZZ2mM+/m+vY9ezfoueVvrgnHIGi5FvgSymbIgJOFwiznWyA59mav95L+Mc6xPtL3s9gm5eNTlNtxJLbNM1g=="
},
"node_modules/lru-cache": {
"version": "11.2.5",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz",28 29 30 31 32 33
"@11ty/eleventy-plugin-rss": "^2.0.4",
"ajv": "^8.17.1",
"diff": "^8.0.2",
"lodash": "^4.17.21",
"minimatch": "^10.1.1",
"mithril": "^2.3.8",
"mithril-node-render": "^3.0.2"28 29 30 31 32 33
"@11ty/eleventy-plugin-rss": "^2.0.4",
"ajv": "^8.17.1",
"diff": "^8.0.2",
"escape-html": "^1.0.3",
"minimatch": "^10.1.1",
"mithril": "^2.3.8",
"mithril-node-render": "^3.0.2"0 1 2
import _ from 'lodash'
import * as Diff from 'diff'
import {type Repository} from './dataTypes.ts'
import { type ReposConfiguration } from './configTypes.ts'0 1 2
import escape from 'escape-html'
import * as Diff from 'diff'
import {type Repository} from './dataTypes.ts'
import { type ReposConfiguration } from './configTypes.ts'36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
const lastHunk = lines.slice(previousHunk, hunkEndIndex)
let lastHunkBefore = lastHunk.filter(line => line.startsWith("-")).map(str => str.replace("-", "")).join("\n")
let lastHunkAfter = lastHunk.filter(line => line.startsWith("+")).map(str => str.replace("+", "")).join("\n")
lastHunkBefore = _.escape(lastHunkBefore)
lastHunkAfter = _.escape(lastHunkAfter)
const changeObject = Diff.diffWordsWithSpace(lastHunkBefore, lastHunkAfter)
let beforeText = ""
let afterText = ""
changeObject.forEach((obj) => {
if (!obj.added && !obj.removed) {
beforeText = beforeText + obj.value
afterText = afterText + obj.value
}
if (obj.added) {
afterText = afterText + "<mark>" + obj.value + "</mark>"
}
if (obj.removed) {
beforeText = beforeText + "<mark>" + obj.value + "</mark>"
}
})
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
const lastHunk = lines.slice(previousHunk, hunkEndIndex)
let lastHunkBefore = lastHunk.filter(line => line.startsWith("-")).map(str => str.replace("-", "")).join("\n")
let lastHunkAfter = lastHunk.filter(line => line.startsWith("+")).map(str => str.replace("+", "")).join("\n")
const changeObject = Diff.diffWordsWithSpace(lastHunkBefore, lastHunkAfter)
let beforeText = ""
let afterText = ""
changeObject.forEach((obj) => {
if (!obj.added && !obj.removed) {
beforeText = beforeText + escape(obj.value)
afterText = afterText + escape(obj.value)
}
if (obj.added) {
afterText = afterText + "<mark>" + escape(obj.value) + "</mark>"
}
if (obj.removed) {
beforeText = beforeText + "<mark>" + escape(obj.value) + "</mark>"
}
})
11 12 13 14 15 16 17 18 19 20 21 22
## Works in Progress
- [ ] get rid of lodash
- [Project page](./projects/node-18.md.html)
- also use globs npm module instead of built-in node version, built-in requires
node 20+.
- add a Dockerfile for node 18, which is the lowest 11ty supports, and see if
this is useable on that.
- lodash is one of the larger (largest?) dependencies right now. With mithril auto-escaping strings, it might not be necessary anymore. Look into which things actually need to be escaped (e.g. any user-input from commit messages) and see if we can just rely on mithril to escape those
## Ideas and Todos
11 12 13 14 15
## Works in Progress
## Ideas and Todos
67 68 69 70 71
### Completed
- [x] Better Template Readability
- git-branch: `mithril-server-side-rendering`
- Goal: Do not simply have a bunch of strings as the HTML for the default virtual67 68 69 70 71 72 73 74 75 76 77 78
### Completed
- [x] get rid of lodash
- [Project page](./projects/node-18.md.html)
- also use globs npm module instead of built-in node version, built-in requires
node 20+.
- add a Dockerfile for node 18, which is the lowest 11ty supports, and see if
this is useable on that.
- lodash is one of the larger (largest?) dependencies right now. With mithril auto-escaping strings, it might not be necessary anymore. Look into which things actually need to be escaped (e.g. any user-input from commit messages) and see if we can just rely on mithril to escape those
- [x] Better Template Readability
- git-branch: `mithril-server-side-rendering`
- Goal: Do not simply have a bunch of strings as the HTML for the default virtual0 1 2 3
# Node 18 / Getting Rid of Lodash
Git branch: `node-18`
> Goal: Get this plugin working on node 180 1 2 3 4 5
# Node 18 / Getting Rid of Lodash
\#completed
Git branch: `node-18`
> Goal: Get this plugin working on node 1816 17
- https://github.com/rollup/rollup/issues/5497 mentions that rollup v4 works with node 18
- node's `path.matchesGlob` function is not available in node 18.
- there is a library, [glob](https://www.npmjs.com/package/glob) that is a regular JS implementation for glob matching16 17 18 19 20 21 22 23
- https://github.com/rollup/rollup/issues/5497 mentions that rollup v4 works with node 18
- node's `path.matchesGlob` function is not available in node 18.
- there is a library, [glob](https://www.npmjs.com/package/glob) that is a regular JS implementation for glob matching
## Feb 1, 2026
- Replaced webpack with rollup
- Replaced lodash with escape-html
- Only a few functions needed to be changed to to work with node 18