Remove lodash, replace with escape-html

f72d8586f14c8215c0c0c01fc254ddab9e35aeb6

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 &lt;. Doing the diff first, with
the HTML characters in the string, and *then* escaping them afterwards
fixes this.
js_templates/feed.ts:1
Before
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'
After
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'
js_templates/feed.ts:26
Before
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)),
          ])
        })
      ])
After
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),
          ])
        })
      ])
package-lock.json:12
Before
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"
After
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"
package-lock.json:1080
Before
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",
After
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",
package-lock.json:1282
Before
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",
After
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",
package.json:29
Before
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"
After
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"
src/helpers.ts:1
Before
0
1
2
import _ from 'lodash'
import * as Diff from 'diff'
import {type Repository} from './dataTypes.ts'
import { type ReposConfiguration } from './configTypes.ts'
After
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'
src/helpers.ts:37
Before
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>"
        }
      })
After
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>"
        }
      })
wiki/index.md:12
Before
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
After
11
12
13
14







15

## Works in Progress


⁣
⁣
⁣
⁣
⁣
⁣
⁣
## Ideas and Todos
wiki/index.md:68
Before
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 virtual
After
67
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 virtual
wiki/projects/node-18.md:1
Before
0
1


2
3
# Node 18 / Getting Rid of Lodash

⁣
⁣
Git branch: `node-18`

> Goal: Get this plugin working on node 18
After
0
1
2
3
4
5
# Node 18 / Getting Rid of Lodash

\#completed

Git branch: `node-18`

> Goal: Get this plugin working on node 18
wiki/projects/node-18.md:17
Before
16
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 matching
After
16
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