phase: 165_prose_word_wrap
All verification complete. Final report:
**Phase 165 — Prose word wrap (final verification pass)**
- Verified: `PROSE_FORMATS={txt,text}` + `isProseDoc` (single-site, image arm wins); `renderDocument` three-way branch (md → `.doc-md`, prose → `.doc-prose` text node, else `pre.doc-raw` byte-identical); `.doc-prose` CSS = 72rem column, `pre-wrap` + `overflow-wrap: anywhere`, tokens only, zero literals; `[data-view="pages"]` hide list includes `.doc-prose` as one grouped declaration; phase-116 scrollbar rules intact and modal-scoped; no `innerHTML` in the prose path.
- Tests/lint (all 100% green):
- `uv run pytest` → **3854 passed** (unit + integration)
- `uv run pytest --cov=app --cov-report=term-missing` → **TOTAL 99%** (>90%)
- `uv run ruff check .` → All checks passed; `uv run pyright` → **0 errors**
- `uv run pytest tests/e2e/test_prose_word_wrap.py -v --no-cov` → **5 passed** (isolation)
- Isolation regressions: document_viewer 8, image_documents 4, pdf_pages_in_sources 4, pdf_strip_additive_selfheal 5, edit_summaries 4, summary_in_viewer 4, chat_answer_clusters 4, garbage_pdf 3, pdf_support 6, sticky_navbar 3, smoke 3 — all passed
- Completion criteria: image description wraps, full text, both surfaces ✔; `.txt` wraps same way ✔; `py`/`json`/code keep `pre.doc-raw` + horizontal overflow + themed modal scrollbar (E2E negative control + pins) ✔; no innerHTML, AA `--ink`/`--surface` pair ✔; full gates green ✔; commit + phase-dir move are harness-owned (not executed per pipeline rules).
- Documentation: README already updated by task 03 (viewer section + image-docs section); full README/AGENTS.md review found no further inconsistencies — **verified consistent, no changes needed**.
- Notable deviations: `.doc-prose` omits `font-family: inherit` from 00_phase D2's snippet — task 01's authoritative "exact property set" excludes it and `div` inherits fonts anyway (behavior identical). PDF deferral (A4) intact: `format == "pdf"` stays in `.doc-raw`, owner call recorded.
- Next pending phase: `166_hide_empty_answer_bubble`.
This commit is contained in:
@@ -9,10 +9,14 @@
|
||||
* so the two surfaces can never drift:
|
||||
*
|
||||
* • md / markdown → the shared escape-first renderer (markdown.js)
|
||||
* in a ≤46rem centered column;
|
||||
* in the 72rem centered reading column;
|
||||
* • prose (phase 165, LOCKED A1 — isProseDoc: an image doc's
|
||||
* description or a txt/text doc) → the content
|
||||
* as a text node inside <div class="doc-prose">,
|
||||
* pre-wrap: it WRAPS at the reading column;
|
||||
* • any other → the raw content as a text node inside
|
||||
* <pre class="doc-raw"> (mono, horizontal
|
||||
* scroll);
|
||||
* scroll — locked A3: code never wraps);
|
||||
* • doc.summary non-empty (phase 36 — phase 30 summaries exist
|
||||
* only on non-markdown docs) → a labeled
|
||||
* .doc-summary section ABOVE the content;
|
||||
@@ -111,7 +115,8 @@
|
||||
* the summary, the WCAG alt contract; a NULL summary falls back to
|
||||
* the title), and the description (doc.content — the ONLY readable
|
||||
* text of the doc) follows in the EXISTING plain-content slot below
|
||||
* (the .doc-raw path; a description is prose, not markdown). The
|
||||
* (phase 165: the .doc-prose path — a description IS prose, so it
|
||||
* wraps; it was never markdown, and never belonged in the pre). The
|
||||
* labeled Summary panel is suppressed for the verbatim-description
|
||||
* case (summary === content — the importer invariant; the panel
|
||||
* would duplicate the text right below the image); an admin-edited
|
||||
@@ -313,6 +318,26 @@ function copyPathButton() {
|
||||
return b;
|
||||
}
|
||||
|
||||
/* ---------- prose set (phase 165, task 01 — LOCKED A1) ----------
|
||||
* The ONE place the prose rule lives (TODO.md L3: "Need word wrap for
|
||||
* image explanations and other long text rather than scrolling
|
||||
* horizontally"). The defect: the description used to land in the
|
||||
* `pre` and each generated line became one unbreakable line — a
|
||||
* horizontal scrollbar where every other text surface wraps.
|
||||
*
|
||||
* The image arm is UNCONDITIONAL (phase 122 A3: an image doc's
|
||||
* `content` IS the vision description — prose by construction,
|
||||
* whatever its extension). The `format` arm covers plain-text docs;
|
||||
* `doc.format` comes from the server's doc_format() (the lowercased
|
||||
* suffix). Code and code-ish formats (py, yaml, json, j2, the quadlet
|
||||
* family, pdf — locked A1/A3: phase 116's scroll contract stands)
|
||||
* never enter this set. */
|
||||
const PROSE_FORMATS = new Set(["txt", "text"]);
|
||||
|
||||
function isProseDoc(doc) {
|
||||
return doc.is_image === true || PROSE_FORMATS.has(doc.format);
|
||||
}
|
||||
|
||||
/* ---------- shared renderer (phase 26, task 02) ----------
|
||||
* Populates the three elements every render surface provides: a title,
|
||||
* a .doc-meta badge row (source · format · mono path · created ·
|
||||
@@ -321,7 +346,8 @@ function copyPathButton() {
|
||||
* section first (phase 36: only when doc.summary is non-empty —
|
||||
* markdown docs and fail-soft rows carry none, so they render exactly
|
||||
* as before), then .doc-md for md/markdown (the shared escape-first
|
||||
* renderer), <pre class="doc-raw"> otherwise. The XSS contract:
|
||||
* renderer), <div class="doc-prose"> for the prose set (phase 165 —
|
||||
* isProseDoc), <pre class="doc-raw"> otherwise. The XSS contract:
|
||||
* innerHTML only through renderMarkdown; every document-derived
|
||||
* string (summary text included) is a text node. */
|
||||
export function renderDocument(doc, { titleEl, metaEl, contentEl }) {
|
||||
@@ -385,7 +411,7 @@ export function renderDocument(doc, { titleEl, metaEl, contentEl }) {
|
||||
// document) is the ONE branch that replaces the content slot (phase
|
||||
// 163: the PRIMARY class only — an "additive" doc falls through to
|
||||
// the text rendering with the strip behind a toggle, D4/A1): the
|
||||
// markdown/.doc-raw content block AND the labeled Summary panel
|
||||
// markdown/.doc-prose/.doc-raw content block AND the labeled Summary panel
|
||||
// (including its admin edit wiring — moot without the panel) are
|
||||
// NOT built. The per-page descriptions stay in the DB for Q&A (the
|
||||
// importer's content/embeddings/summary are untouched); they are
|
||||
@@ -467,6 +493,15 @@ export function renderDocument(doc, { titleEl, metaEl, contentEl }) {
|
||||
wrap.className = "doc-md";
|
||||
wrap.innerHTML = renderMarkdown(doc.content); // escape-first: XSS-safe
|
||||
slot.appendChild(wrap);
|
||||
} else if (isProseDoc(doc)) {
|
||||
// Phase 165 (task 01, LOCKED A2): the prose arm — a TEXT NODE in
|
||||
// the wrapping .doc-prose block (pre-wrap carries the newlines;
|
||||
// no markdown parsing of a description). The XSS contract is
|
||||
// untouched: the content is never parsed as HTML.
|
||||
const prose = document.createElement("div");
|
||||
prose.className = "doc-prose";
|
||||
prose.textContent = doc.content; // text node: never parsed as HTML
|
||||
slot.appendChild(prose);
|
||||
} else {
|
||||
const pre = document.createElement("pre");
|
||||
pre.className = "doc-raw";
|
||||
|
||||
Reference in New Issue
Block a user