Skip to content

legacy-html: $$ in the docs is rendered as $ #1181

Closed

Description

@ovflowd

The legacy HTML generators (legacy-html, one <api>.html per module, and legacy-html-all, which writes all.html) fill their template with a chain of String.prototype.replace(placeholder, value) calls. When the second argument is a string, JavaScript treats some $ sequences in it as replacement patterns:

In the value Becomes
$$ $
$& the matched placeholder (e.g. __CONTENT__)
$` everything in the template before the placeholder
$' everything in the template after it

The whole rendered page body is passed that way, so any doc containing those sequences comes out wrong. This already shows on nodejs.org: the Makefile example in C++ addons (doc/api/addons.md, line 95 in nodejs/node) has $$(command -v node), and the page renders it as $(command -v node). $$ is how Make writes a literal $, so copying the snippet gives a broken Makefile.

Where to look

  • packages/node-legacy/src/legacy-html/utils/replaceTemplateValues.mjs:34: .replace('__CONTENT__', content), the main culprit.
  • The same file, lines 27-43: the rest of the chain does the same with __SECTION__, __TOC__, __GTOC__, __TOC_PICKER__, __GTOC_PICKER__, __ALTDOCS__ and __EDIT_ON_GITHUB__.
  • It's called from legacy-html/generate.mjs:127 (every page) and legacy-html-all/generate.mjs:59 (all.html), so one fix covers both.
  • Same pattern, lower risk, worth fixing in the same PR: packages/node/src/man-page/generate.mjs:71-72 and packages/react/src/sitemap/generate.mjs:53-56,60.

How to reproduce

const template = '<pre>__CONTENT__</pre>';

template.replace('__CONTENT__', 'dirname "$$(command -v node)"');
// '<pre>dirname "$(command -v node)"</pre>'  <- one `$` is gone

template.replace('__CONTENT__', "x $' y");
// '<pre>x </pre> y</pre>'  <- the rest of the template got pasted in

template.replace('__CONTENT__', () => 'dirname "$$(command -v node)"');
// '<pre>dirname "$$(command -v node)"</pre>'  <- a function replacer keeps it as-is

Suggested approach

Pass each value through a replacer function. Its return value is inserted as-is, without looking at $:

.replace('__CONTENT__', () => content)
.replace(/__GTOC__/g, () => nav)
// ...and the same for every other line in the chain

Using () => on every line keeps it consistent. Keep the /g regexes where they are, since the template uses some placeholders more than once. Then do the same in man-page and sitemap.

How to test

There's no test for replaceTemplateValues yet. Create packages/node-legacy/src/legacy-html/utils/__tests__/replaceTemplateValues.test.mjs, following buildContent.test.mjs next to it (node:test and node:assert/strict):

  • Call setConfig({}) in a before hook, because buildVersions reads the legacy-html config.
  • Pass a small template like '<main>__CONTENT__</main><nav>__GTOC__</nav>', the config { version: { version: '1.0.0' }, changelog: [] } and { skipGitHub: true }.
  • Assert that content containing $$, $&, $' and $` comes out unchanged, and add one case for nav or section.

Run the suite with node --run test, then node --run lint and node --run format:check.

Things to watch out for

  • Only $$, $&, $` and $' are special here, so the test needs those exact sequences. Shiki often splits code into <span>s, which is why most $ in the docs survive and this was easy to miss.
  • legacy-html-all reuses the same function, so don't duplicate the fix there.
  • In tests, pass skipGitHub: true (or set editURL in the config), because the "Edit on GitHub" link needs one.

Getting started

New to doc-kit? CONTRIBUTING.md covers setup, running the tests and adding a changeset (node --run changeset, a patch for @node-core/doc-kit-legacy). Leave a comment here if you'd like to take it on.

Activity

  1. added theissue type on Oct 26, 2026
  2. added a commit that references this issue on Oct 28, 2026
    0cd7414
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Legacy Generator`legacy-html`, `legacy-json`, and their `-all` variantsgood first issueGood for newcomershelp wantedExtra attention is needed

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions