phase: 138_markdown_blockquotes
**Phase 138 final verification pass — all green, no defects found.** - Verified: blockquote pass in `frontend/assets/markdown.js` (step 2b, between escape+inline and paragraphs), guard extended to `|blockquote`, docstring updated; `.md-quote` CSS (left border + padding + `--ink-soft`, no fill, palette vars only, contrast comments, user-bubble override); unit pins + node behavior; dedicated E2E (chat / viewer / restore); mock `QUOTE_TRIGGER`/`QUOTE_ANSWER`; fixture `tests/fixtures/quote_docs/`. Thinking block confirmed on the same shared renderer (app.js L2025/L3095). - `uv run pytest tests/e2e/test_markdown_blockquotes.py -v --no-cov` → 3 passed (isolation) - `uv run pytest --cov=app --cov-report=term-missing` → 3268 passed, 9 skipped; TOTAL **99%** (>90%) - `uv run ruff check . && uv run pyright` → All checks passed; 0 errors - Criteria: quotes render in chat/viewer/restore ✅ (E2E) + thinking ✅ (shared renderer + unscoped AA CSS); `>>` collapses, fence/table `>` untouched ✅ (unit+e2e); non-quote byte-identical ✅ (exact fixture; phase-44/renderer suites pass **unedited**, confirmed by empty `git diff --stat`) - No deviations; commit + phase-dir move left to the harness per pipeline rules (working tree clean of stray changes — only phase-138 diffs present) - Next pending phase: **139_read_range_tool_lines**
This commit is contained in:
@@ -16,6 +16,22 @@
|
||||
* and restores it alongside the code blocks in the final step. Fences
|
||||
* win over tables (they are protected first), and everything that is
|
||||
* not a pipe table renders byte-identically to the original app.js copy.
|
||||
*
|
||||
* Phase 138 (2026-09-30, TODO.md L3) added the blockquote pass between
|
||||
* step 2 (escape+inline) and step 3 (paragraphs): the maximal run of
|
||||
* consecutive >-prefixed lines becomes ONE
|
||||
* <blockquote class="md-quote"> with the members joined with <br>.
|
||||
* The pass order is now fences → tables → escape+inline → blockquotes
|
||||
* → paragraphs → restore. Group rule: a BLANK line terminates the
|
||||
* group (a re-quoted line starts a new <blockquote>), and nested
|
||||
* >> / >>> collapses to a single level (locked A1). Documented limit:
|
||||
* a markdown STRUCTURE line inside a quote (a "- list" / "# heading")
|
||||
* renders as plain text — the inline chain ran first, so its
|
||||
* line-start transforms never see the post-strip text; inline
|
||||
* code/strong/em still work. Fences and tables keep their precedence:
|
||||
* a > inside a code fence never reaches the pass (step 1 placeholder),
|
||||
* and a > in a table cell is restored in step 4. Everything without a
|
||||
* quote line renders byte-identically to the pre-phase renderer.
|
||||
*/
|
||||
|
||||
function escapeHtml(s) {
|
||||
@@ -123,13 +139,46 @@ function renderMarkdown(md) {
|
||||
// 2. Escape everything else, then apply the inline + block transforms.
|
||||
text = inline(escapeHtml(text));
|
||||
|
||||
// 2b. Blockquotes (phase 138, 2026-09-30, TODO.md L3). Runs AFTER
|
||||
// the escape+inline step — a quote line now starts with the escaped
|
||||
// marker >, and inline code/strong/em already work inside quotes
|
||||
// for free (this pass never re-transforms) — and BEFORE the
|
||||
// paragraph pass, which the extended step-3 guard leaves alone. The
|
||||
// maximal run of consecutive lines matching ^\s*(?:>)+ ? is ONE
|
||||
// <blockquote class="md-quote">: the leading (?:>)+ run plus ONE
|
||||
// optional space is stripped from each member (nested >> / >>>
|
||||
// collapses to a single level — locked A1), members are joined with
|
||||
// <br> (the paragraph convention), and a BLANK line terminates the
|
||||
// group (a re-quoted line starts a new <blockquote>). Documented
|
||||
// limit: a "- list" / "# heading" line INSIDE a quote renders as
|
||||
// plain text — the inline chain ran first. Fences (step 1) and
|
||||
// tables (step 1b) keep their precedence: a > inside a fence never
|
||||
// reaches this pass, and a > in a table cell is restored in step 4.
|
||||
const quoteLines = text.split("\n");
|
||||
const quoted = [];
|
||||
let qpos = 0;
|
||||
while (qpos < quoteLines.length) {
|
||||
if (!/^\s*(?:>)+ ?/.test(quoteLines[qpos])) {
|
||||
quoted.push(quoteLines[qpos]);
|
||||
qpos += 1;
|
||||
continue;
|
||||
}
|
||||
const members = [];
|
||||
while (qpos < quoteLines.length && /^\s*(?:>)+ ?/.test(quoteLines[qpos])) {
|
||||
members.push(quoteLines[qpos].replace(/^\s*(?:>)+ ?/, ""));
|
||||
qpos += 1;
|
||||
}
|
||||
quoted.push(`<blockquote class="md-quote">${members.join("<br>")}</blockquote>`);
|
||||
}
|
||||
text = quoted.join("\n");
|
||||
|
||||
// 3. Paragraphs (double newline separated).
|
||||
text = text
|
||||
.split(/\n{2,}/)
|
||||
.map((block) => {
|
||||
const b = block.trim();
|
||||
if (!b) return "";
|
||||
if (/^<(h\d|ul|ol|pre|li)/.test(b)) return b;
|
||||
if (/^<(h\d|ul|ol|pre|li|blockquote)/.test(b)) return b;
|
||||
return `<p>${b.replace(/\n/g, "<br>")}</p>`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
@@ -541,6 +541,37 @@ body::before {
|
||||
.md-table th, .md-table td { border: 1px solid var(--line); padding: 0.4rem 0.6rem; text-align: left; vertical-align: top; }
|
||||
.md-table thead th { background: var(--surface); color: var(--ink); }
|
||||
|
||||
/* Markdown blockquotes (phase 138, 2026-09-30, TODO L3): the shared
|
||||
renderer (markdown.js step 2b) turns a run of "> "-prefixed lines
|
||||
into <blockquote class="md-quote"> — ONE rule set serves all three
|
||||
rendering contexts (the chat .bubble, the details.thinking
|
||||
scratchpad, the document viewer's .doc-md): every one sits on the
|
||||
--surface canvas (the deflected bubble's --accent-bg holds too —
|
||||
ink-soft there is 6.9:1), so a single AA pairing covers them all.
|
||||
A visible-but-restrained left border + padding + a muted ink — NO
|
||||
background fill (locked A2). Palette vars only (the :root-drift
|
||||
test parses the built-ins from this file — no hex literal here):
|
||||
border-left --brand-ink — 9.9:1 on --surface, the rose accent bar
|
||||
(--line would be invisible: 1.14:1 on the surface;
|
||||
plain --brand vanishes on the user bubble, 1:1);
|
||||
color --ink-soft — 8.2:1 on --surface, the same AA pairing
|
||||
the --ink-soft :root comment documents.
|
||||
Static content — no animation (nothing for prefers-reduced-motion
|
||||
to still). */
|
||||
.md-quote {
|
||||
border-left: 3px solid var(--brand-ink);
|
||||
padding: 0.25rem 0.75rem;
|
||||
margin: 0.5rem 0;
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
/* The USER bubble also runs the question through the shared renderer
|
||||
(app.js addMessage), on the --brand fill with --bg text: --ink-soft
|
||||
there is 1.6:1 — an AA failure — so the quote keeps the bubble's
|
||||
own dark ink, 5.2:1 on --brand (the :root --bg pairing). The
|
||||
--brand-ink bar holds 1.9:1 against the fill — still visible, the
|
||||
quote stays distinct. */
|
||||
.msg.user .bubble .md-quote { color: var(--bg); }
|
||||
|
||||
.msg.user { justify-content: flex-end; }
|
||||
.msg.user .msg-body { align-items: flex-end; }
|
||||
.msg.user .bubble {
|
||||
|
||||
Reference in New Issue
Block a user