Jesse A. Green / notebook

Live demo: the DocCompiler guide library as a static, read-only snapshot (October 2026). No backend, no accounts, nothing to submit.

generate-github-markdown-css Administrator & Developer Guide

Generated April 2026 from sindresorhus/generate-github-markdown-css at 58d00ca

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

generate-github-markdown-css is a small, single-purpose Node.js tool with a deceptively interesting design: rather than maintaining a static snapshot of GitHub's Markdown CSS, it scrapes the live GitHub.com repository page on every invocation, parses every linked stylesheet, applies an allow-list filter built from GitHub's own HTML sanitizer rules, runs a rendered-fixture pass through the public Markdown API to capture dynamically-generated classes, and emits a cleaned, scoped stylesheet. This guide is for engineers who need to operate, integrate, extend, or audit that pipeline.

The codebase is intentionally tiny — five JavaScript files (cli.js, index.js, constants.js, utilities.js, test.js) totalling about 28 KB of source. The runtime dependencies are equally lean: meow for CLI parsing and postcss for CSS transformation. The whole engine fits comfortably in a single mental model, which makes it useful both as a production tool and as a worked example of "scraping a UI, not an API."

1. Architecture Overview

The tool is one process, one HTTP-driven pipeline, and one stdout stream. There are no servers, no queues, no background workers, no headless browsers, no databases. The work happens in memory and the only persistent state is a content-addressed disk cache.

1.1 Data flow

                         +--------------------------------+
   user invokes  ---->   |   cli.js  (meow flag parser)   |
                         |   normalises light/dark/theme  |
                         +---------------+----------------+
                                         | options object
                                         v
                         +--------------------------------+
                         |   index.js  default export     |
                         |   getCSS({...options})         |
                         +-+------------+-----------------+
                           |            |
   +-----------------------+            +-----------------------+
   |  cachedFetch(repoPage)             |  renderMarkdown()     |
   |  utilities.js                      |  utilities.js         |
   |                                    |                       |
   |  GET https://github.com/           |  POST                 |
   |    sindresorhus/                   |    https://api.github.|
   |    generate-github-markdown-css    |    com/markdown       |
   |                                    |    (body=fixture.md)  |
   |  -> HTML body                      |  -> rendered HTML     |
   +----------+-------------------------+-----------+-----------+
              |                                     |
              |  regex: /(?<=href=").+?\.css/g      |  getUniqueClasses(html)
              v                                     v
   +-----------------------+              +-------------------------+
   |  list of CSS file     |              |  Set of class names     |
   |  URLs (Primer + theme |              |  used by GFM features   |
   |  variables + fixture) |              +-----------+-------------+
   +----------+------------+                          |
              |  Promise.all + cachedFetch            |
              v                                       |
   +--------------------------------+                 |
   |  raw CSS text per URL          |                 |
   +-+------------+-----------------+                 |
     |            |                                   |
     | name regex: /(?<=\/)[-\w]+(?=-\w+\.css$)/      |
     v                                                |
   classify by filename:                              |
     ^light.*$    -> theme variables                  |
     ^dark.*$     -> theme variables                  |
     primer*.css  -> shared variables + styles        |
     other        -> styles                           |
              |                                       |
              v                                       v
   +--------------------------------+      +-------------------------+
   |  extractStyles(rules, css,     |<-----+ extraAllowableClasses   |
   |    {extraAllowableClasses})    |                                |
   |  - PostCSS walkRules           |                                |
   |  - drop everything not under   |                                |
   |    .markdown-body / ALLOW_TAGS |                                |
   |    / ALLOW_CLASS / fixture-    |                                |
   |    derived classes             |                                |
   |  - drop @keyframes, keep @layer|                                |
   |  - drop QueryBuilder sub-rules |                                |
   |  - rewrite html/body/:root     |                                |
   |  - collapse var() defaults     |                                |
   |  - duplicate -webkit-appearance|                                |
   +-+------------------------------+                                |
     |
     |  extractVariables(themes, name, css)
     v
   +--------------------------------+
   |  classifyRules(rules)          |
   |  - reverseUnique by toString() |
   |  - merge same-selector rules   |
   |  - prefix non-.markdown-body   |
   |    selectors with .markdown-   |
   |    body                        |
   |  - drop empty rules            |
   +-+------------------------------+
     |
     v
   +--------------------------------+
   |  if transparentBackground:     |
   |    swap --bgColor-default      |
   |  emit prefers-color-scheme     |
   |    blocks (auto)               |
   |  or inline variables (single)  |
   |  prepend manuallyAddedStyle    |
   |  s/.markdown-body/{rootSel}/g  |
   +-+------------------------------+
     |
     v
   stdout (or returned string)

Note

The whole architecture rests on one assumption: GitHub's repository page links to all the CSS files that style Markdown rendering, and each of those files has a discriminator in its filename — light* or dark* for theme variables, primer* for shared rules, anything else for view-specific styles. If GitHub reorganises that asset pipeline, the regex /(?<=\/)[-\w]+(?=-\w+\.css$)/ and the classifier in index.js are the first things that need to be updated.

1.2 Component inventory

Component Path Role
CLI entry cli.js Parses flags via meow, normalises the theme/light/dark triple, calls getCSS, prints to stdout.
Library entry index.js The getCSS function. Coordinates fetches, runs PostCSS passes, emits the final string.
Allow-lists constants.js ALLOW_TAGS, ALLOW_CLASS, and manuallyAddedStyle. The opinionated heart of the filter.
Helpers utilities.js cachedFetch, renderMarkdown, unique, reverseUnique, zip, getUniqueClasses. Disk cache lives here.
Test harness test.js Generates the entire dist/ artifact set used by maintainers to inspect output by eye, plus seven node:test assertions.
Fixture fixture.md Hand-curated Markdown input sent to GitHub's Markdown API. Determines which classes the fixture pass discovers.

1.3 Technology stack

Concern Implementation
Language JavaScript, ES2022 (top-level await, native Set, Promise.all)
Module system ES modules ("type": "module" in package.json)
Runtime Node.js >= 18 (engines pin)
HTTP client Node built-in globalThis.fetch
CSS parser postcss ^8.4.47
CLI parser meow ^12.1.1
Lint / test xo ^0.56.0; node:test; node:assert/strict
Cache Filesystem under node_modules/.cache/generate-github-markdown-css/
Output Single string written to stdout (CLI) or returned (library)

1.4 Modes

Mode Trigger Output structure
Auto light !== dark (or --theme=auto) Hoisted shared-variables rule, then @media (prefers-color-scheme: light) { … }, @media (prefers-color-scheme: dark) { … }, then rules referencing variables
Single light === dark (or --theme=<name>) One variable block, one body of rules — variables inlined unless preserveVariables is set
Variables-only --only-variables The prefers-color-scheme blocks (or single variable block) only — no rule definitions
Styles-only --only-style Rule definitions only — no variable blocks

Note

Auto mode is forced to preserve variables: the prefers-color-scheme blocks need them to switch theme palettes at runtime. The variable preservation is handled implicitly inside getCSS regardless of the preserveVariables flag.

2. System Requirements

2.1 Runtime

Requirement Minimum Notes
Node.js 18.0.0 Required for global fetch, top-level await, node:test. Declared in engines.node.
npm 8.x+ For installing the runtime dependencies.
Disk <10 MB Source + node_modules + cache.
Memory <100 MB resident PostCSS parses each CSS file once; output is a single string.
Network HTTPS to github.com, api.github.com, and any subdomain serving Primer/Markdown CSS bundles No outbound traffic outside those hosts.

2.2 OS support

The tool runs anywhere Node.js 18 runs: Linux, macOS, Windows, BSD, Docker images, Lambda-style environments — provided outbound HTTPS is available. There is no native code, no platform-specific path handling beyond path.join, and no shell invocation.

2.3 Network requirements

Endpoint Method Auth Purpose
https://github.com/sindresorhus/generate-github-markdown-css GET none Source of CSS link references
https://github.githubassets.com/... (and similar Primer/static asset CDNs) GET none The actual stylesheet downloads
https://api.github.com/markdown POST none Renders fixture.md to derive dynamically-generated classes

Important

All requests are unauthenticated. There is no token configuration. If you need to operate in a network where GitHub requires auth (private mirror, enterprise install behind SSO), you will need to fork the tool and add header injection — there is no built-in mechanism.

3. Configuration Reference

3.1 No file-based configuration

The tool reads no configuration file (no .rc, no .config, no package.json field). Every behaviour is driven by CLI flags or library options.

3.2 CLI flag reference

Every flag declared in cli.js meow invocation:

Flag meow type Default Aliases Notes
--theme <name> string (none) --type Single-theme shortcut. 'auto' is treated specially.
--light <name> string 'light' (in JS API) — Light slot.
--dark <name> string 'dark' (in JS API) — Dark slot.
--list boolean (none) — Prints theme list and exits.
--only-style boolean (none) — Maps to JS option onlyStyles (note plural).
--only-variables boolean (none) — Maps to JS option onlyVariables.
--preserve-variables boolean (none) — Forced on by --only-style / --only-variables.
--root-selector <selector> string .markdown-body — Empty string is rejected with exit 1.
--use-fixture / --no-use-fixture boolean true — meow boolean with default: true plus the --no- prefix syntax.
--transparent-background boolean (none) — Swaps --bgColor-default to transparent.

3.3 JS API option reference

The default export is an async function with this signature:

async function getCSS({
  light = 'light',
  dark = 'dark',
  list = false,
  preserveVariables = false,
  onlyVariables = false,
  onlyStyles = false,
  useFixture = true,
  rootSelector = '.markdown-body',
  transparentBackground = false,
} = {}): Promise<string>
Option Type Default Throws on
light string 'light' (no validation; bad theme name produces empty output)
dark string 'dark' (no validation; bad theme name produces empty output)
list boolean false —
preserveVariables boolean false —
onlyVariables boolean false When also onlyStyles === true: Error('May not specify onlyVariables and onlyStyles at the same time')
onlyStyles boolean false (same paired error)
useFixture boolean true —
rootSelector string '.markdown-body' When === '': Error('rootSelector may not be an empty string')
transparentBackground boolean false —

Important

The CLI --only-style (singular) maps to the JS onlyStyles (plural). cli.js does the rename in the call site: onlyStyles: onlyStyle. This is the only naming asymmetry between the two surfaces — every other CLI flag is the kebab-case form of its JS option name.

3.4 Mode-resolution truth table

cli.js ships an embedded comment table that is the canonical contract for mode resolution. Reproduced verbatim:

theme light dark Outcome
✓ Single mode, use Theme
✓ ✓ Not allowed, can't determine theme
✓ ✓ Not allowed, can't determine theme
✓ ✓ ✓ Not allowed, can't determine theme
Auto, default themes
✓ Single mode, use Light
✓ Single mode, use Dark
✓ ✓ Auto, use Light and Dark
✓ ✓ Single mode if Light === Dark
auto Auto, default themes
auto ✓ Auto, use Light, default dark
auto ✓ Auto, use Dark, default light
auto ✓ ✓ Auto, use Light and Dark
auto ✓ ✓ Single mode if Light === Dark

When a non-auto --theme is set together with --light or --dark, cli.js prints You may not specify light and/or dark unless type/theme is set to "auto" to stderr and exits with status 1.

3.5 Themes (live-discovered)

The list of themes is always fetched live from GitHub. The current set as of commit 58d00ca:

Theme name Mode
light light
dark dark
dark_dimmed dark
dark_high_contrast dark
dark_colorblind dark
light_colorblind light

The classifier in index.js decides "this theme is dark" by light.startsWith('dark') — so any future theme starting with dark will be tagged dark, and anything else will be tagged light. A theme called auto would be tagged light by this logic, which is fine because auto is intercepted earlier in cli.js.

3.6 Allow-list constants

constants.js exposes three exports that govern which CSS rules survive the filter pass.

ALLOW_TAGS — HTML element names whose generated CSS rules are kept. The list mirrors GitHub's own HTML sanitizer (the comment cites gjtorikian/html-pipeline's sanitization_filter.rb). Confirmed contents:

h1 h2 h3 h4 h5 h6 br b i strong em a pre code img tt div ins del sup sub p
picture ol ul table thead tbody tfoot blockquote dl dt dd kbd q samp var hr
ruby rt rp li tr td th s strike summary details caption figure figcaption
abbr bdo cite dfn mark small source span time wbr body html g-emoji input

ALLOW_CLASS — CSS class names whose rules are kept regardless of element. Confirmed contents:

.anchor .g-emoji .highlight .octicon .octicon-link .contains-task-list
.task-list-item .task-list-item-checkbox .octicon-info .octicon-light-bulb
.octicon-report .octicon-alert .octicon-stop .markdown-alert
.markdown-alert-title .markdown-alert-note .markdown-alert-tip
.markdown-alert-important .markdown-alert-warning .markdown-alert-caution
.mr-2

manuallyAddedStyle — A CSS string appended after the root rule on every run. It defines:

  • Underline + offset for .markdown-body a (because GitHub itself strips this in some contexts).
  • display: inline-block; fill: currentColor; vertical-align: text-bottom for .markdown-body .octicon.
  • A heading-anchor link icon (<svg> octicon as a base64-free data: URL).

Important

Adding a new dynamically-generated GitHub class without including it in ALLOW_CLASS (or in the live fixture) means rules involving that class will silently disappear from the output. When updating to a new GitHub UI version, audit extractStyles against any new selectors GitHub introduces.

4. Deployment & Scaling

4.1 Deployment topology

There is no service to deploy. The tool is a one-shot command-line process that produces a CSS string and exits. The deployment patterns are therefore consumption patterns:

Pattern When Notes
Manual run Ad-hoc regeneration on a workstation npx generate-github-markdown-css > github-markdown.css
Build-step Static-site generators, frontend bundlers Wire into npm run build so the CSS is regenerated before the page build.
Scheduled CI Tracking upstream GitHub UI changes A weekly cron in GitHub Actions (or any CI) regenerates and opens a PR if the file diffs.
Container image Reproducible CI within Docker Bake node:20-alpine plus npm install -g generate-github-markdown-css@<version> and call from docker run.

4.2 Scaling considerations

The work is bound by a few HTTPS round-trips and one PostCSS parse pass per CSS file. Each invocation is independent — no shared state, no locks, no fan-out concerns.

Resource Scaling pattern
CPU <1 second of PostCSS work per run on modern CPUs. Not a hot path.
Network One request per CSS link on the GitHub repo page (~5–10 in practice). One POST to api.github.com/markdown.
Cache hit rate High in steady-state CI: the CSS files cache for 24 hours, the fixture for 7 days.
Concurrency Within one process: Promise.all across all CSS link fetches. Across processes: each runs its own fetch and cache; no coordination.

Tip

If you are scaling out across many CI runners, share the node_modules/.cache/ directory via the runner's caching mechanism (e.g. actions/cache@v4 on ~/.npm and your project node_modules) to maximise cache hit rates and keep traffic to GitHub low.

4.3 Embedding inside a service (anti-pattern)

Caution

Do not call getCSS() on every web request in a server. The function does live HTTPS fetches; running it inline with a request will couple your latency to GitHub availability and will rate-limit you on busy traffic. Generate the stylesheet at build time, ship the result as a static asset.

If you do need on-the-fly generation (e.g. user-selected themes), call getCSS() once per process at startup, cache the result in memory keyed by (theme, rootSelector, …), and refresh on a slow background timer.

5. Operations

5.1 Daily / build-time runbook

A typical build-step integration looks like:

  • [ ] CI starts on a fresh runner.
  • [ ] actions/setup-node@v4 with node-version: 20 (or any 18+).
  • [ ] Restore node_modules/.cache/ from the previous run if available.
  • [ ] Run npx generate-github-markdown-css > public/github-markdown.css.
  • [ ] Optionally run other variant builds (--theme=dark, --theme=dark_high_contrast, --only-style, --only-variables).
  • [ ] Persist node_modules/.cache/ for the next run.
  • [ ] Continue with the rest of the site build.

5.2 Cache management

The disk cache is at node_modules/.cache/generate-github-markdown-css/ (created by findCacheDir in utilities.js). It contains:

Pattern Source TTL (maxAge)
<basename>.css.txt Each CSS file fetched from GitHub 1 day (ONE_DAY_IN_MILLISECONDS)
fixture.md.txt Output from https://api.github.com/markdown 7 days (ONE_DAY_IN_MILLISECONDS * 7)

The cache key is the basename of the URL — github-markdown.css and assets/foo/github-markdown.css collide. In practice GitHub's URLs include cache-busting hashes in the filename, so collisions are not observed in the wild, but be aware of the limit if you ever extend the tool.

Invalidate the cache by deleting the directory:

rm -rf node_modules/.cache/generate-github-markdown-css/

Note

The cache is written even when the network call returns a non-OK status — actually, it's not: cachedFetch only writes when response.ok is true (utilities.js lines 60-66). A failing fetch never poisons the cache.

5.3 Periodic refresh

Tracking upstream changes is the central operational loop. GitHub ships CSS updates without notice; if you've vendored an output file, it will drift. A weekly schedule is a good baseline; daily is safe; every commit is unnecessary unless your downstream is exceptionally tight.

# .github/workflows/refresh-markdown-css.yml
on:
  schedule:
    - cron: '0 6 * * 1'    # Monday 06:00 UTC

Pair the regeneration with a diff guard so trivial whitespace changes don't open a PR.

5.4 Monitoring

There is nothing to monitor in steady state — the tool either succeeds or throws. In CI, watch for:

  • Non-zero exit codes from npx generate-github-markdown-css (treat as a build failure).
  • An unusually large diff on the generated file (suggests a meaningful upstream change).
  • A consistent failure pattern over several weekly runs (suggests GitHub broke an assumption — file an issue).

Tip

A small CI step that runs wc -l public/github-markdown.css and compares against the committed file is a cheap "did the size jump" alarm. If the new file is half or double the previous, pause the auto-merge.

5.5 Logging

The tool writes nothing to stderr unless an error occurs. The two stderr-paths in cli.js are:

  • --rootSelector cannot be an empty string (exit 1)
  • You may not specify light and/or dark unless type/theme is set to "auto" (exit 1)

The library throws Error instances; the CLI never catches them, so a thrown error propagates as a Node-default unhandled-rejection stack trace and an exit code of 1.

6. Performance

6.1 Steady-state cost

Cold cache (every fetch hits the network):

Step Typical duration
Repo-page fetch 100–400 ms
Per-CSS-file fetch (5–10 files in parallel) 100–500 ms (parallel)
Markdown API render 200–800 ms
PostCSS parse + walk <100 ms total across all files
Final string assembly <10 ms
End-to-end cold ~1–2 seconds

Warm cache (everything served from node_modules/.cache/):

Step Typical duration
Disk read per cached file 1–5 ms
PostCSS parse + walk <100 ms
Final string assembly <10 ms
End-to-end warm ~150–300 milliseconds

Note

The dominant cost is network. Once the cache is warm, the run is bound by PostCSS parse time, which is small and bounded by the size of the live CSS bundles.

6.2 Memory profile

The tool holds:

  • One copy of the GitHub repo HTML (~few hundred KB).
  • One copy of each fetched CSS file (typically 50–500 KB each).
  • The PostCSS AST for each CSS file (proportional to the source).
  • A growing array of selected rules.
  • The final concatenated string.

Total peak resident memory is well under 100 MB on representative inputs.

6.3 Optimisation levers

Lever Trade-off
--no-use-fixture Skips one network round-trip and one PostCSS pass over the rendered fixture. Output omits dynamically-generated classes.
Long-lived runner cache Eliminates network for CSS up to 1 day, fixture up to 7 days.
--only-style + per-theme --only-variables Smaller per-file output; bigger total output across N theme files. Saves bytes on the wire when only one theme is active.
Skip rebuild when file unchanged Wrap getCSS() in a content-hash compare and only write if different. Avoids cache-busting downstream assets unnecessarily.

7. Security

7.1 Threat model

This tool is a CSS generator, not a runtime. It:

  • Has no inbound surface. It accepts no user requests.
  • Writes only to stdout and to node_modules/.cache/ under the working directory.
  • Calls only github.com and api.github.com (and whichever Primer/asset CDNs GitHub links to from its repository page).
  • Has zero authentication — every call is anonymous.

The principal threats are therefore:

Threat Severity Mitigation
GitHub returns malicious CSS that, when consumed by a browser, causes harm Low (output is CSS, not JS) The PostCSS pipeline drops @keyframes (only @layer is kept); the allow-list of tags + classes is conservative. CSS-injection risk is bounded to whatever GitHub itself accepts.
GitHub is compromised and serves CSS that points to malicious URLs (e.g. background: url(evil)) Low Does not affect the generator host. Affects pages that consume the generated CSS.
An attacker MITMs the connection to github.com Low All connections are HTTPS. Node's default certificate trust applies.
The cache directory is shared across mutually-distrusting tenants Low Each tenant's working directory has its own node_modules/.cache/. Cross-contamination requires a shared node_modules, which is itself a misconfiguration.
Rate-limiting / IP banning from GitHub Medium Use the cache. Don't run on every page load. Stagger CI cron times across an org.
GitHub's TOS Medium This tool reads public, unauthenticated repository pages and the public Markdown API. Both are robots-friendly. Heavy automated traffic is still subject to abuse-prevention rules.

Warning

This tool scrapes GitHub.com on every uncached run. GitHub may rate-limit, return CAPTCHAs, or change the page structure at any time. Running it on every web request, in a fan-out tight loop, or in a way that disregards the cache is a way to get your IP rate-limited (and to break for everyone running the tool from the same egress IP).

7.2 Cache-poisoning resistance

cachedFetch writes to disk only when the HTTP response is ok:

if (response.ok) {
  fs.writeFileSync(filename, body);
  return body;
}
throw new Error(`Failed to fetch ${url}: ${body}`);

This means a 5xx from GitHub does not poison the cache with an error page. The next run will retry the network. The implication for operators: a transient GitHub outage doesn't leave you with bad CSS forever.

7.3 Secret handling

There are no secrets. The tool never reads environment variables for credentials, never accepts an API token, and never writes anything that could leak sensitive data. The only environment variables that affect behaviour are the standard HTTPS_PROXY / HTTP_PROXY honoured by Node's fetch.

7.4 Dependency surface

Dependency Version range Surface
meow ^12.1.1 Parses CLI flags. Trusted Sindre Sorhus-maintained library.
postcss ^8.4.47 Parses every byte of CSS fetched from GitHub. The single largest external trust surface.
xo ^0.56.0 (devDep) Lint only. Not in runtime path.

Both runtime dependencies are pinned with ^ ranges, which means semver-minor and -patch updates are accepted automatically. Audit them with npm audit periodically; PostCSS in particular has a long history of careful security maintenance.

7.5 Recommendations for production use

Tip

Vendor the output file (the generated CSS) into your repo. That way a compromise of the generator process — or an unexpected upstream GitHub change — never reaches your users until you deliberately re-run, diff, and merge.

Caution

Do not run the generator with elevated privileges. It writes only under the working directory, but as a defence-in-depth measure, run it as the same low-privilege user that owns your build artifacts.

7.6 GitHub Terms of Service

The tool fetches public, unauthenticated GitHub assets. This is well within the TOS for normal automation. Two practical tenets:

  • Cache aggressively. Don't make GitHub serve you the same file thousands of times per day.
  • Identify yourself politely. Node's default User-Agent is Node.js; the fixture pass sets User-Agent: Node.js explicitly. If you fork heavily or run at scale, set a more descriptive User-Agent.

8. Developer Internals

8.1 Repository layout

/
├── cli.js                  # Entry point (~155 lines). Flag parsing, mode normalisation.
├── index.js                # Library entry (~210 lines). The pipeline.
├── constants.js            # ALLOW_TAGS, ALLOW_CLASS, manuallyAddedStyle.
├── utilities.js            # cachedFetch, renderMarkdown, unique helpers.
├── test.js                 # Generates dist/, runs node:test assertions.
├── fixture.md              # Markdown fixture for the API render pass.
├── package.json            # ESM, Node 18+, two deps.
├── readme.md               # Public README (intentionally minimal).
├── license                 # MIT.
├── .editorconfig
├── .gitattributes
├── .gitignore
├── .npmrc

8.2 The five files in detail

cli.js

  • Imports meow and the default export of ./index.js.
  • Declares the help text including the embedded "single vs auto" truth table (reproduced in section 3.4).
  • Maps meow flags 1:1 to the JS API options, except --only-style (onlyStyle in meow) → onlyStyles (plural).
  • Implements the theme / light / dark normalisation:
    • Non-auto theme with light or dark: error + exit 1.
    • theme set to a non-auto name: copies it to both light and dark, forcing single mode.
    • theme unset and only one of light/dark set: copies the set value to the other slot, forcing single mode.
  • Validates rootSelector !== '' (defence-in-depth; index.js validates again).
  • console.logs the awaited result. Standard streams handle the rest.

index.js — getCSS({…})

The single function exported. The flow inside:

  1. Validate mutually-exclusive options and rootSelector !== ''.
  2. Force preserveVariables on if onlyVariables or onlyStyles is set.
  3. Fetch the repo page (https://github.com/sindresorhus/generate-github-markdown-css) via cachedFetch. The comment notes "Do not use the landing page (https://github.com/) to exclude styles on it" — the repo page reflects only Markdown-relevant assets.
  4. Extract CSS link URLs with unique(body.match(/(?<=href=").+?\.css/g)).
  5. Kick off the fixture render (unless useFixture === false) in parallel with the CSS fetches.
  6. Fetch every CSS link in parallel via Promise.all.
  7. Compute fixture classes with getUniqueClasses(fixtureHtml).
  8. Iterate over each (url, cssText) pair:
    • Parse the filename: /(?<=\/)[-\w]+(?=-\w+\.css$)/. Skip if no match.
    • If the basename starts with light or dark and list is true, push it to the colors list.
    • Else if it starts with light/dark: extract variable definitions into colors[name].
    • Else: extract style rules. If the basename starts with primer, also extract its variables into colors.shared.
  9. Short-circuit for --list: return colors.join('\n').
  10. Apply transparentBackground by mutating the --bgColor-default declaration in the relevant theme arrays.
  11. Deduplicate rules via reverseUnique(rules, rule => rule.toString()) so the last occurrence of each duplicate selector wins (matching the cascade).
  12. Classify rules via classifyRules: extract per-theme rules (selectors of the form [*-theme*=name] or [data-color-mode=name]), then merge same-selector rules and prefix non-.markdown-body selectors with .markdown-body .
  13. Compute usedVariables by walking every declaration and capturing var(--name) references.
  14. Filter the variable blocks to only declarations whose name appears in usedVariables.
  15. Emit either prefers-color-scheme blocks (auto), inlined values (single, no preserveVariables), or a hoisted variable block plus rules referencing it (single, preserveVariables).
  16. Clean PostCSS raws, join multi-selector rules with ,\n, build the final string.
  17. Insert manuallyAddedStyle right after the closing } of the root rule (unless onlyVariables).
  18. Substitute rootSelector with a global string replace if it differs from .markdown-body.

The function is long but not deep — there's no recursion, no callback nesting, no cleverness beyond the regexes.

constants.js

  • Comment cites GitHub's HTML sanitizer ruby file as the source for ALLOW_TAGS.
  • ALLOW_TAGS is a Set of HTML element names.
  • ALLOW_CLASS is a Set of .dot-prefixed class selectors.
  • octicon is a hand-written SVG octicon for heading anchor links.
  • manuallyAddedStyle is a tagged template literal that bakes the octicon as a data:image/svg+xml,… URL via two CSS variables (-webkit-mask-image + mask-image).

utilities.js

  • zip(a, b) — [a[i], b[i]] for i in range(len(a))].
  • unique(array, by) — first-wins de-dup with optional key-fn.
  • reverseUnique(array, by) — reverses, de-dups (so last wins), reverses back.
  • findCacheDir() — mkdirSync('node_modules/.cache/generate-github-markdown-css', {recursive: true}), returns a path.join factory.
  • isCached(filename, maxAge) — Date.now() - mtime < maxAge.
  • cachedFetch(url) — disk read if cached, else fetch(url), write on response.ok, throw on not-ok.
  • renderMarkdown() — fs.readFileSync('fixture.md', 'utf8') → POST https://api.github.com/markdown with mode: 'gfm' and context: 'sindresorhus/generate-github-markdown-css'. 7-day cache.
  • getUniqueClasses(html) — extracts class="…" attributes and converts to Set of .dot-prefixed selectors.

test.js

A test runner and a manual-inspection asset builder. Behaviour:

  1. Creates dist/.
  2. Generates dist/auto.css from githubMarkdownCss() (default).
  3. Calls githubMarkdownCss({list: true}) to get the theme list.
  4. Generates dist/<theme>.css for every theme via light: theme, dark: theme (single mode, inlined).
  5. Generates dist/auto_colorblind.css with the colorblind pair.
  6. Renders the fixture, builds dist/index.html — a self-contained test harness with a <select> that swaps stylesheets at runtime.
  7. Generates dist/vars/base.css (onlyStyles: true) and dist/vars/auto-vars.css (onlyVariables: true).
  8. Generates per-theme variables files: dist/vars/<theme>-vars.css.
  9. Builds dist/vars/index.html — same as dist/index.html but using the base+vars split.
  10. Runs seven assertions via node:test:
    • transparentBackground — auto mode swaps --bgColor-default to transparent.
    • transparentBackground — single mode inlines background-color: transparent.
    • transparentBackground — single + preserveVariables keeps the variable.
    • transparentBackground — onlyVariables keeps the variable.
    • link underline styles are present in default output.
    • link underline styles are present in onlyStyles output.
    • link underline styles are not in onlyVariables output (it's variables only).
    • default output has no --bgColor-default: transparent (regression guard).

npm test runs xo && node test.js, so lint must pass before the harness runs.

8.3 Adding a new theme

You don't add themes here. The list is whatever GitHub itself exposes. Two practical scenarios:

Scenario Action
GitHub ships a new theme upstream The --list output should pick it up automatically next time the cache expires. If not, check that the new theme's CSS filename starts with light or dark; if it doesn't (e.g. system_dark), the regex `/^(light
You want a custom-coloured theme Run --only-style to get the rules, then ship your own variables file via --root-selector scoping or your own CSS. The generator does not support pluggable themes.

8.4 Adding new allow-listed tags or classes

If GitHub introduces a new feature (e.g. a new alert type, a new emoji renderer) and you want the rules to flow through:

  1. Identify the tag or class in the GitHub-rendered HTML.
  2. Add it to ALLOW_TAGS (for tags) or ALLOW_CLASS (for classes) in constants.js.
  3. Run npm test and inspect dist/auto.css for the new rules.
  4. Open a PR.

Note

The fixture pass already captures dynamically-generated classes that appear in the rendered fixture.md. If a new feature appears in the fixture, no allow-list change is needed — just regenerate.

8.5 Coding conventions

  • ESM throughout.
  • Top-level await is used in cli.js and test.js and the JS API call.
  • 2-space indent, tab characters where the existing codebase already uses them.
  • xo enforces import ordering, prefer-const, node: protocol prefixes.
  • The single // eslint-disable-next-line complexity lives above getCSS because the function has many branches by design.

8.6 Tests

The repository's test approach mixes:

  • Functional regression tests (via node:test assertions in test.js) for the transparentBackground and link-underline behaviours.
  • Visual harness (dist/index.html, dist/vars/index.html) for human inspection of every theme.
  • Lint (xo) for static checks before tests run.

There is no formal CSS-snapshot diff test. If you contribute, run npm test, manually open dist/index.html in a browser, and compare against the live GitHub rendering of fixture.md.

8.7 Potential improvements

Idea Impact
Add a snapshot test that pins the byte count of dist/<theme>.css Catches accidental regression in the rule extractor.
Replace cachedFetch's fs.readFileSync with async fs.readFile Tiny perf win, less event-loop blocking.
Allow a custom GitHub URL (e.g. an Enterprise install) via env var Enables enterprise consumers without forking.
Add a --cache-dir flag Useful for CI where node_modules is volatile but a project-level cache directory exists.
Emit a manifest with the live CSS hashes used Reproducibility audit for "what did I render against?"

These are not on any roadmap; they are observations from reading the source.

8.8 Contributing

The project is small, opinionated, and maintained by a single author. To contribute:

  1. Open a focused PR with a single behaviour change.
  2. Update or add tests in test.js.
  3. Run npm test locally — xo rejects style violations before tests run.
  4. Inspect dist/auto.css and dist/<theme>.css in a browser via dist/index.html.

Issues are filed and triaged at github.com/sindresorhus/generate-github-markdown-css/issues.

9. Integration Points

9.1 Inputs

Input Source Format
CLI flags argv via meow Strings and booleans
JS API options First argument to default export Plain object with the keys in section 3.3
fixture.md Bundled in the package (package.json files field) Markdown
GitHub repo page HTML Live HTTPS GET HTML
GitHub-served CSS files Live HTTPS GET CSS
Markdown API rendering Live HTTPS POST HTML

9.2 Outputs

Output Sink Format
Generated CSS stdout (CLI) or returned string (JS API) CSS text
Theme list stdout / returned string when --list / list: true Newline-delimited theme names
Disk cache node_modules/.cache/generate-github-markdown-css/ Plain text files (*.css.txt, fixture.md.txt)
dist/* (test only) dist/, dist/vars/ CSS, HTML

9.3 Downstream-consumer contract

A consumer of the generated CSS commits to:

  • Wrapping rendered Markdown in an element matching the --root-selector value (default .markdown-body).
  • Following GitHub's HTML structure for advanced features:
    • Task lists: ul.contains-task-list > li.task-list-item > input.task-list-item-checkbox.
    • Alerts: div.markdown-alert.markdown-alert-{note|tip|important|warning|caution} with a p.markdown-alert-title containing an svg.octicon.octicon-{info|light-bulb|report|alert|stop}.
    • Anchor links: h* containing a.anchor with svg.octicon.octicon-link.
    • Emoji: g-emoji element or img.emoji.
  • Hosting in a Node 18+ environment when using the JS API. The output CSS itself is browser-agnostic.

9.4 Forward-compatibility notes

The tool follows GitHub. When GitHub:

  • Adds a new theme: appears in --list automatically, generated successfully if the filename starts with light or dark.
  • Renames a theme: the old name becomes invalid; pass the new one. --list always reflects current truth.
  • Changes the page markup: the href regex may stop matching; the tool will then have nothing to extract and produce a near-empty stylesheet. This is the canary for an upstream change.
  • Adds a new alert / feature class: include it in ALLOW_CLASS (or rely on the fixture pass to catch it via fixture.md).

Important

Treat the tool as a cache of the live GitHub rendering — not as a stable API contract. Every output is a snapshot of one moment in GitHub's evolution. Run periodically, diff carefully, and version-pin downstream wherever consistency matters.