# generate-github-markdown-css Administrator & Developer Guide

`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:

```js
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:

```text
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:

```text
.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:

```bash
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.

```yaml
# .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`**:

```js
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.log`s 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|dark)/` in `index.js` won't classify it, and you'll need to update that test. |
| 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](https://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.

---

_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. Unofficial; not affiliated with or reviewed by the project._
