From 72b13290874cf8a1636982c330f2810a62c7a6b9 Mon Sep 17 00:00:00 2001 From: stanlyzoolo <51911715+stanlyzoolo@users.noreply.github.com> Date: Tue, 28 Jul 2026 00:25:06 +0300 Subject: [PATCH 1/5] docs: add README house-style implementation plan Preprocessor (badges/HTML/emoji removal, link unwrapping) + custom glamour theme in keepkit palette for panel [3]. Design approved in brainstorm, hardened by plan review (URL hiding moved from style config to preprocessor after the review's spike showed autolink deletion). Co-Authored-By: Claude Fable 5 --- docs/plans/20260728-readme-house-style.md | 323 ++++++++++++++++++++++ 1 file changed, 323 insertions(+) create mode 100644 docs/plans/20260728-readme-house-style.md diff --git a/docs/plans/20260728-readme-house-style.md b/docs/plans/20260728-readme-house-style.md new file mode 100644 index 0000000..9027cd0 --- /dev/null +++ b/docs/plans/20260728-readme-house-style.md @@ -0,0 +1,323 @@ +# README house style for panel [3]: markdown preprocessor + custom glamour theme + +## Overview + +Panel `[3]` renders each tool's README through glamour's standard `dark`/`light` style. +The result is noisy and inconsistent: badge/logo/HTML junk at the top of a typical +README, full link URLs bloating paragraphs in a narrow panel, bright standard-theme +colors that clash with keepkit's palette, and emoji scattered through headings. This +plan gives every README one calm "house style": a markdown cleanup pass before glamour +plus a custom glamour theme derived from keepkit's palette. + +Key insight driving the cleanup rules: **no image can ever render in a TTY**, so all +images (badges, logos, screenshots) are removable without any badge-vs-logo heuristics. +Same for `[text](url)` links: panel `[3]` links are not clickable, so the href half is +pure noise — the preprocessor unwraps them to their text. Autolinks and bare URLs are +different: the URL *is* their content, so they stay visible. + +All design decisions were approved in a brainstorm session (2026-07-28) and hardened by +a plan review the same day; do not re-litigate them during implementation. + +**Acceptance criteria** (checked in Task 5): +- badge/logo/screenshot images gone, including reference-style (`![Build][badge]`) +- HTML junk gone; inline-tag text (`Ctrl` → `Ctrl`) preserved +- `[text](url)` renders as `text` with no URL and no double space; autolinks and bare + URLs still visible; no table-link footnote stubs +- emoji and known `:shortcodes:` gone, no double-space/blank-line artifacts; `✅/❌/☑` + translated to `✓/✗/✓` +- headings/rules/blockquotes in keepkit palette, no background plates +- code blocks and inline spans byte-identical through the preprocessor +- badge-only README → existing `No README for . Press [h] for --help.` placeholder +- `styles.DarkStyleConfig`/`LightStyleConfig` globals unmutated + +### Decision log + +- **URL hiding mechanism — resolved 2026-07-28 by plan review, approved by user.** The + style-level route (`Styles.Link.Format` rendering empty) was spiked against glamour + v1.0.0 and rejected: glamour routes autolinks and linkified bare URLs through the same + `Link` primitive with no separate link text, so blanking it silently deletes them + (`See here` → `See here`); it also leaves a double space + where an inline link's href was, and table links leave footnote stubs. Chosen instead: + the preprocessor unwraps `[text](url)` / `[text][ref]` → `text`. `Conceal`/`Faint` + exist on `StylePrimitive` but the v1.0.0 renderer never applies them; `SkipHref` is an + element field unreachable from styles. +- **4-space indented code blocks are NOT protected** by the segmentation pass (explicit + decision): they are ambiguous with nested-list continuation lines, and modern READMEs + overwhelmingly use fences. Accepted limitation, documented here. + +## Context (from discovery) + +- `internal/model/readme.go` — `renderReadme`: `cleanTerminalOutput` → glamour + (`WithStandardStyle(readmeStyleName(dark))`, `WithWordWrap`, `WithColorProfile`); + memoized by `readmeRenderCache` keyed `(name, raw, width, dark)`; test seam + `testReadmeStyle` (unknown style name → constructor error → plain-text fallback path). +- `internal/model/readme_test.go` — fallback assertions at lines ~84 and ~147 compare + against `cleanTerminalOutput(raw)`; `TestRenderReadmeStyleFollowsBackground` (~line 90) + tests `readmeStyleName`'s dark/light branches. +- `internal/ui/styles.go` — palette vars `ColorPrimary #DA7756`, `ColorCategory + #E8A87C`, `ColorMeta #5588AA`, `ColorMuted #AAAAAA`, `ColorDim #888888`, `ColorBorder + #555555`, `ColorText #E8E8E8` — the theme derives from these (`string(ui.ColorPrimary)`), + never from hex literals. +- `go.mod` — `glamour v1.0.0` (direct), `goldmark-emoji v1.0.6` (indirect; carries the + GitHub shortcode dictionary via `definition.Github()` — note the exported name is + `Github`, and it is `sync.Once`-guarded internally, so no caching wrapper is needed). +- `version.getReadme` truncates at `readmeMaxBytes` (512 KiB) and appends a marker — the + cut can land **mid-fence**, so the segmentation pass must handle an unterminated fence. +- Empty-render guard reused, not extended: a README that cleans down to nothing renders + `""` and the `data.content != "" && m.helpBase != ""` placeholder logic already shows + `No README for . Press [h] for --help.`. +- Glamour-side helpers verified present in v1.0.0: `glamour.WithStyles`, + `glamour.WithInlineTableLinks`. + +## Development Approach + +- **testing approach**: Regular (code first, then tests in the same task) +- complete each task fully before moving to the next +- make small, focused changes +- **CRITICAL: every task MUST include new/updated tests** for code changes in that task + - tests are not optional — they are a required part of the checklist + - table-driven tests for the pure preprocessor; struct-level tests for the theme + - tests cover both success and error scenarios +- **CRITICAL: all tests must pass before starting next task** — no exceptions +- **CRITICAL: update this plan file when scope changes during implementation** +- run `go test -race ./...` after each change (version package has real mutex-guarded + state — keep `-race`) +- maintain backward compatibility: the `testReadmeStyle` seam behavior and the + glamour-failure fallback path survive (the fallback now returns *preprocessed* text) +- the preprocessor runs synchronously inside `Update()` on up to 512 KiB of markdown: + all regexes are compiled at package level (the `helpTokenRe` idiom in + `internal/model/textutil.go`), never per call + +## Testing Strategy + +- **unit tests**: required for every task (see Development Approach above) +- no e2e framework in this project; the TUI-level check is manual (see Post-Completion) +- color assertions on rendered output are deliberately avoided (no TTY in tests → + `lipgloss.ColorProfile()` is Ascii → glamour strips colors). The theme is covered at + the **struct level** instead: assert `keepkitStyle(...)` field values against + `ui.Color*` — the only real coverage the theme can get, and it needs no TTY. + +## Progress Tracking + +- mark completed items with `[x]` immediately when done +- add newly discovered tasks with ➕ prefix +- document issues/blockers with ⚠️ prefix +- update plan if implementation deviates from original scope + +## Solution Overview + +Two independent layers, composed in `renderReadme`: + +1. **Preprocessor** — pure `cleanReadmeMarkdown(raw string) string` in a new + `internal/model/readme_clean.go`, called after `cleanTerminalOutput`, before glamour. + Segmentation first (fences and inline spans are inviolable), then removal rules on + the cleanable segments only. +2. **Theme** — `keepkitStyle(dark bool) ansi.StyleConfig` in a new + `internal/model/readme_style.go`, built by **cloning** `styles.DarkStyleConfig` / + `styles.LightStyleConfig` and overriding accents. Clone-and-override, not + from-scratch: StyleConfig has dozens of fields and inheriting defaults protects + against "forgotten field = broken render" on glamour upgrades. **The globals hold + pointers** (`Color`, `BackgroundColor`, `Margin`, `Chroma`), and `styles.DefaultStyles` + aliases them — every override assigns a **fresh pointer**; writing through a cloned + pointer would mutate the shared style for the whole process. + +`renderReadme` switches to `WithStyles(keepkitStyle(dark))` + `WithInlineTableLinks(true)`; +a non-empty `testReadmeStyle` keeps routing through `WithStandardStyle(name)` so the +existing constructor-failure fallback test still works. With production no longer +choosing a style *name*, `readmeStyleName` degenerates to the test branch — it is +deleted, `renderReadme` branches on `testReadmeStyle != ""` directly, and +`TestRenderReadmeStyleFollowsBackground` goes with it (the project removes dead branches +*and their test rows* — see CLAUDE.md on `restart_unix.go`). + +The `readmeRenderCache` key stays `(name, raw, width, dark)` — `raw` is the +pre-cleanup input and the pipeline is deterministic, so memoization stays correct. + +## Technical Details + +### Segmentation (the inviolable-code rule) + +- Fenced blocks: opener is 3+ backticks or 3+ tildes (info string allowed); the closer + is the **same character with length ≥ the opener's** — a ````` ```` ````` fence + wrapping ``` examples (how READMEs document markdown itself) stays one block. +- An **unterminated fence protects to EOF** (real case: the 512 KiB truncation can cut + mid-fence). +- Inline `` `spans` `` are protected within cleanable segments. +- 4-space indented code blocks are *not* protected (see Decision log). + +### Removal rules (cleanable segments only) + +- **Images**: inline `![alt](url)`, linked `[![alt](img)](target)`, and + reference-style `![alt][ref]` / `![alt][]` removed whole; lines left empty collapse + so a 10-badge header vanishes. +- **Link-reference definitions**: standalone `[label]: url "title"` lines removed — + they are pure metadata, and after image removal and link unwrapping nothing uses them. +- **Links**: `[text](url)` and `[text][ref]` unwrapped to `text`. Autolinks + ``, `` and bare URLs are **left untouched** — their URL is the + content (see Decision log). +- **HTML**: comments `` removed whole; ``/``/`