Public: anyone can read this; the crew can change it.
A config file that does not parse says so in one plain line above its highlighted source, not a boxed alert card #10014
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/10009-a-config-file-that-does-not-parse-says-s → main 3 commits fork 2 threads · 2 unresolved
src/web/render/config.ts +5 −5
| @@ -25,11 +25,11 @@ function editHref(input: RenderInput): string | undefined { | ||
| 25 | 25 | |
| 26 | 26 | function brokenConfig(language: string, text: string, line: number, column: number, message: string): Html { |
| 27 | 27 | const lines = text.split("\n"); |
| 28 | return html`<div class="card" role="alert"> | |
| 29 | <p><strong>Not valid ${language}:</strong> ${message} at line ${String(line)}, column ${String(column)}.</p> | |
| 30 | <p class="muted small">The form needs valid ${language}. Source shows the file highlighted.</p> | |
| 31 | </div> | |
| 32 | <pre class="card mono">${lines.map((l, i) => (i + 1 === line ? html`<mark>${l}</mark>\n` : `${l}\n`))}</pre>`; | |
| 28 | return html`<p>Not valid ${language}: ${message} at line ${String(line)}, column ${String(column)}. | |
| 29 | <span class="muted small">The form needs valid ${language}.</span></p> | |
outdated
Principles 3 and 6, and phone use: the old |
||
| 30 | <pre class="mono config-diff" tabindex="0" aria-label="Source">${lines.map((l, i) => | |
| 31 | i + 1 === line ? html`<mark data-op="del">${l}</mark>\n` : `${l}\n`, | |
| 32 | )}</pre>`; | |
| 33 | 33 | } |
| 34 | 34 | |
| 35 | 35 | export function config(input: RenderInput): Rendered { |
test/config-broken-language.test.ts added +34
| @@ -0,0 +1,34 @@ | ||
| 1 | /** A config file that does not parse says so in one plain line above its source, no alert card (#10009). */ | |
| 2 | import { expect, test } from "bun:test"; | |
| 3 | import { config } from "../src/web/render/config.ts"; | |
| 4 | ||
| 5 | const input = (path: string, text: string) => { | |
| 6 | const body = new TextEncoder().encode(text); | |
| 7 | return { path, size: body.length, body, media: `/o/r/media/main/${path}` }; | |
| 8 | }; | |
| 9 | ||
| 10 | test("a broken YAML file is one plain line, then its source with the failing line marked", () => { | |
| 11 | const out = String(config(input("a.yml", "a: 1\nb: x: y\nc: 3\n")).content); | |
| 12 | expect(out).not.toContain('class="card'); | |
| 13 | expect(out).not.toContain('role="alert"'); | |
| 14 | const line = out.slice(out.indexOf("<p"), out.indexOf("</p>")); | |
| 15 | expect(line).toContain("Not valid YAML:"); | |
| 16 | expect(line).toMatch(/at line \d+, column \d+\./); | |
| 17 | expect(line).toContain('class="muted small"'); | |
| 18 | expect(line).not.toContain("Source shows"); | |
| 19 | expect(out.match(/<p[ >]/g)?.length).toBe(1); | |
| 20 | expect(out).toMatch(/<pre class="mono config-diff" tabindex="0" aria-label="Source">[\s\S]*<mark data-op="del">b: x: y<\/mark>/); | |
| 21 | }); | |
| 22 | ||
| 23 | test("the source block scrolls inside itself and its marked line wears a token, not the browser's yellow", async () => { | |
| 24 | const { content: CONTENT_CSS } = await import("../src/web/style/content.ts"); | |
| 25 | expect(CONTENT_CSS).toContain(".config-diff { overflow-x: auto; white-space: pre; margin: 0; }"); | |
| 26 | expect(CONTENT_CSS).toContain('.config-diff > [data-op="del"] { background: var(--del-bg); color: var(--del-fg); }'); | |
| 27 | }); | |
| 28 | ||
| 29 | test("the error line and the source stay escaped", () => { | |
| 30 | const out = String(config(input("a.json", '{"x": "<script>alert(1)</script>",\n')).content); | |
| 31 | expect(out).toContain("Not valid JSON"); | |
| 32 | expect(out).not.toContain("<script>"); | |
| 33 | expect(out).toContain("<script>"); | |
| 34 | }); |
Principle 1: the hint after the error line only restates the error and then says "Source shows the file highlighted" while the source sits right below it. Cut it to one clause, or drop it.
role="status"on server-rendered text announces nothing; it is harmless here, but plain<p>is more honest.