OOKNET                             [ /  search the index  ]  
──────────────────────────────────────────────────────────────────────────────────────
══════════════════════════════════════════════════════════════════════════════════════
OOKNET   [ /  search  ]  
────────────────────────────────────────────────
════════════════════════════════════════════════
 
HASH      6461469503df
DATE      2026-07-18
SUBJECT   web: lenient readme pipeline, degrade grid-defeating constructs per
          construct
FILES     4 CHANGED
HASH      6461469503df
DATE      2026-07-18
SUBJECT   web: lenient readme pipeline, degrade
          grid-defeating constructs per
          construct
FILES     4 CHANGED
 

diff --git a/ooknet-design/src/lib/markdown.test.ts b/ooknet-design/src/lib/markdo
wn.test.ts
new file mode 100644
index 0000000..ff91281
--- /dev/null
+++ b/ooknet-design/src/lib/markdown.test.ts
@@ -0,0 +1,57 @@
+import { describe, expect, it } from "vitest";
+import { unified } from "unified";
+import remarkParse from "remark-parse";
+import remarkGfm from "remark-gfm";
+import remarkRehype from "remark-rehype";
+import rehypeStringify from "rehype-stringify";
+import { renderMarkdown } from "./markdown";
+import { rehypeAscii } from "./rehype-ascii";
+
+const cols = Array.from({ length: 12 }, (_, i) => `c${i + 1}`);
+const wideTable = [
+  "# title",
+  "",
+  `| ${cols.join(" | ")} |`,
+  `| ${cols.map(() => "---").join(" | ")} |`,
+  `| ${cols.join(" | ")} |`,
+].join("\n");
+
+describe("renderMarkdown", () => {
+  it("renders gfm with the ascii chrome", () => {
+    const html = renderMarkdown("# hi\n\n| a | b |\n| --- | --- |\n| 1 | 2 |\n");
+    expect(html).toContain("head-rule");
+    expect(html).toContain("table-frame");
+  });
+
+  it("transliterates foreign glyphs before the grid checks", () => {
+    expect(renderMarkdown("a - b")).toContain("a - b");
+  });
+
+  it("degrades a too-wide table to a code frame, keeps the rest rich", () => {
+    const html = renderMarkdown(wideTable);
+    expect(html).toContain("head-rule");
+    expect(html).toContain("code-block");
+    expect(html).toContain(" TABLE ");
+    expect(html).toContain("sr-only");
+    expect(html).toContain("c1 | c2");
+  });
+
+  it("degrades an oversized diagram fence to a code frame", () => {
+    const html = renderMarkdown("```diagram\n" + "x".repeat(100) + "\n```\n");
+    expect(html).toContain("code-block");
+    expect(html).not.toContain("diagram-art");
+  });
+});
+
+describe("rehypeAscii strict", () => {
+  const strict = unified()
+    .use(remarkParse)
+    .use(remarkGfm)
+    .use(remarkRehype)
+    .use(rehypeAscii)
+    .use(rehypeStringify);
+
+  it("still throws on grid-defeating tables", () => {
+    expect(() => strict.processSync(wideTable)).toThrow();
+  });
+});

diff --git a/ooknet-design/src/lib/markdown.
test.ts b/ooknet-design/src/lib/markdown.tes
t.ts
new file mode 100644
index 0000000..ff91281
--- /dev/null
+++ b/ooknet-design/src/lib/markdown.test.ts
@@ -0,0 +1,57 @@
+import { describe, expect, it } from "vites
t";
+import { unified } from "unified";
+import remarkParse from "remark-parse";
+import remarkGfm from "remark-gfm";
+import remarkRehype from "remark-rehype";
+import rehypeStringify from "rehype-stringi
fy";
+import { renderMarkdown } from "./markdown"
;
+import { rehypeAscii } from "./rehype-ascii
";
+
+const cols = Array.from({ length: 12 }, (_,
 i) => `c${i + 1}`);
+const wideTable = [
+  "# title",
+  "",
+  `| ${cols.join(" | ")} |`,
+  `| ${cols.map(() => "---").join(" | ")} |
`,
+  `| ${cols.join(" | ")} |`,
+].join("\n");
+
+describe("renderMarkdown", () => {
+  it("renders gfm with the ascii chrome", (
) => {
+    const html = renderMarkdown("# hi\n\n| 
a | b |\n| --- | --- |\n| 1 | 2 |\n");
+    expect(html).toContain("head-rule");
+    expect(html).toContain("table-frame");
+  });
+
+  it("transliterates foreign glyphs before 
the grid checks", () => {
+    expect(renderMarkdown("a - b")).toConta
in("a - b");
+  });
+
+  it("degrades a too-wide table to a code f
rame, keeps the rest rich", () => {
+    const html = renderMarkdown(wideTable);
+    expect(html).toContain("head-rule");
+    expect(html).toContain("code-block");
+    expect(html).toContain(" TABLE ");
+    expect(html).toContain("sr-only");
+    expect(html).toContain("c1 | c2");
+  });
+
+  it("degrades an oversized diagram fence t
o a code frame", () => {
+    const html = renderMarkdown("```diagram
\n" + "x".repeat(100) + "\n```\n");
+    expect(html).toContain("code-block");
+    expect(html).not.toContain("diagram-art
");
+  });
+});
+
+describe("rehypeAscii strict", () => {
+  const strict = unified()
+    .use(remarkParse)
+    .use(remarkGfm)
+    .use(remarkRehype)
+    .use(rehypeAscii)
+    .use(rehypeStringify);
+
+  it("still throws on grid-defeating tables
", () => {
+    expect(() => strict.processSync(wideTab
le)).toThrow();
+  });
+});
 

diff --git a/ooknet-design/src/lib/markdown.ts b/ooknet-design/src/lib/markdown.ts
index 8853e06..2600744 100644
--- a/ooknet-design/src/lib/markdown.ts
+++ b/ooknet-design/src/lib/markdown.ts
@@ -2,7 +2,10 @@
 // content collections (repo READMEs). Same shape as the astro
 // pipeline: gfm, then rehype-ascii for head rules, code frames, and
 // ascii tables. Input goes through toGrid first so foreign text can't
-// fail the grid checks the pipeline runs.
+// fail the grid checks the pipeline runs, and rehype-ascii runs
+// lenient: a construct that still defeats the grid (a table too wide
+// for the frame) degrades to a plain code frame on its own instead of
+// failing the whole document.
 import { unified } from "unified";
 import remarkParse from "remark-parse";
 import remarkGfm from "remark-gfm";
@@ -15,7 +18,7 @@ const pipeline = unified()
   .use(remarkParse)
   .use(remarkGfm)
   .use(remarkRehype)
-  .use(rehypeAscii)
+  .use(rehypeAscii, { lenient: true })
   .use(rehypeStringify);
 
 export function renderMarkdown(md: string): string {

diff --git a/ooknet-design/src/lib/markdown.
ts b/ooknet-design/src/lib/markdown.ts
index 8853e06..2600744 100644
--- a/ooknet-design/src/lib/markdown.ts
+++ b/ooknet-design/src/lib/markdown.ts
@@ -2,7 +2,10 @@
 // content collections (repo READMEs). Same
 shape as the astro
 // pipeline: gfm, then rehype-ascii for hea
d rules, code frames, and
 // ascii tables. Input goes through toGrid 
first so foreign text can't
-// fail the grid checks the pipeline runs.
+// fail the grid checks the pipeline runs, 
and rehype-ascii runs
+// lenient: a construct that still defeats 
the grid (a table too wide
+// for the frame) degrades to a plain code 
frame on its own instead of
+// failing the whole document.
 import { unified } from "unified";
 import remarkParse from "remark-parse";
 import remarkGfm from "remark-gfm";
@@ -15,7 +18,7 @@ const pipeline = unified()
   .use(remarkParse)
   .use(remarkGfm)
   .use(remarkRehype)
-  .use(rehypeAscii)
+  .use(rehypeAscii, { lenient: true })
   .use(rehypeStringify);
 
 export function renderMarkdown(md: string):
 string {
 

diff --git a/ooknet-design/src/lib/rehype-ascii.ts b/ooknet-design/src/lib/rehype-
ascii.ts
index f94d6f0..76befbd 100644
--- a/ooknet-design/src/lib/rehype-ascii.ts
+++ b/ooknet-design/src/lib/rehype-ascii.ts
@@ -232,7 +232,12 @@ function parseFence(text: string): { body: string; captionTex
t: string | null }
     : { body: trimmed, captionText: null };
 }
 
-export function rehypeAscii() {
+// Lenient mode is for markdown we don't own (repo readmes): a
+// construct that defeats the grid degrades to a plain code frame
+// instead of failing the build. Site content never sets it and keeps
+// failing loudly.
+export function rehypeAscii(opts: { lenient?: boolean } = {}) {
+  const lenient = opts.lenient === true;
   return (tree: Root) => {
     let fig = 0;
 
@@ -253,29 +258,38 @@ export function rehypeAscii() {
         // through mermaid-ascii. Art wider than the narrow frame
         // scrolls.
         if (lang === "diagram" || lang === "mermaid") {
-          fig++;
           const { body, captionText } = parseFence(text);
-          const caption = captionText ? `FIG. ${fig} - ${captionText.toUpperCase(
)}` : `FIG. ${fig}`;
-          const art = lang === "mermaid" ? mermaidToAscii(body, FRAME_W) : body;
-          checkGrid(art, FRAME_W, "diagram");
-
-          node.tagName = "figure";
-          node.properties = { className: ["figure-frame", "diagram-frame"], role:
 "img", "aria-label": caption };
-          node.children = [
-            ruleSpan(FRAME_W, "frame-wide", "fig-rule"),
-            ruleSpan(MOBILE_FRAME_W, "frame-narrow", "fig-rule"),
-            {
-              type: "element",
-              tagName: "pre",
-              properties: { className: ["ascii", "diagram-art"], "aria-hidden": "
true" },
-              children: [{ type: "text", value: art }],
-            },
-            captionSpan(caption, FRAME_W, "frame-wide"),
-            captionSpan(caption, MOBILE_FRAME_W, "frame-narrow"),
-            ruleSpan(FRAME_W, "frame-wide", "fig-rule"),
-            ruleSpan(MOBILE_FRAME_W, "frame-narrow", "fig-rule"),
-          ];
-          return;
+          const caption = captionText ? `FIG. ${fig + 1} - ${captionText.toUpperC
ase()}` : `FIG. ${fig + 1}`;
+          let figChildren: ElementContent[] | null = null;
+          try {
+            const art = lang === "mermaid" ? mermaidToAscii(body, FRAME_W) : body
;
+            checkGrid(art, FRAME_W, "diagram");
+            figChildren = [
+              ruleSpan(FRAME_W, "frame-wide", "fig-rule"),
+              ruleSpan(MOBILE_FRAME_W, "frame-narrow", "fig-rule"),
+              {
+                type: "element",
+                tagName: "pre",
+                properties: { className: ["ascii", "diagram-art"], "aria-hidden":
 "true" },
+                children: [{ type: "text", value: art }],
+              },
+              captionSpan(caption, FRAME_W, "frame-wide"),
+              captionSpan(caption, MOBILE_FRAME_W, "frame-narrow"),
+              ruleSpan(FRAME_W, "frame-wide", "fig-rule"),
+              ruleSpan(MOBILE_FRAME_W, "frame-narrow", "fig-rule"),
+            ];
+          } catch (e) {
+            // lenient: fall through and render the fence as a plain
+            // code frame instead
+            if (!lenient) throw e;
+          }
+          if (figChildren) {
+            fig++;
+            node.tagName = "figure";
+            node.properties = { className: ["figure-frame", "diagram-frame"], rol
e: "img", "aria-label": caption };
+            node.children = figChildren;
+            return;
+          }
         }
 
         node.tagName = "div";
@@ -292,6 +306,24 @@ export function rehypeAscii() {
         const { rows, headerRows } = tableCells(node);
         if (!rows.length) return;
 
+        // A table with too many columns for the frame defeats the
+        // grid; lenient mode degrades just this table to a code frame
+        // whose rows hard-wrap, so it can't fail.
+        let pres: Element[];
+        try {
+          pres = [
+            makeTablePre(buildAsciiTable(rows, headerRows, FRAME_W), "frame-wide"
),
+            makeTablePre(buildAsciiTable(rows, headerRows, MOBILE_FRAME_W), "fram
e-narrow"),
+          ];
+        } catch (e) {
+          if (!lenient) throw e;
+          const text = rows.map((r) => r.map((c) => c.text).join(" | ")).join("\n
");
+          pres = [
+            makeCodePre(text, "table", FRAME_W, "frame-wide"),
+            makeCodePre(text, "table", MOBILE_FRAME_W, "frame-narrow"),
+          ];
+        }
+
         // Keep the semantic table for screen readers; the ASCII pres
         // are presentation only.
         const original: Element = {
@@ -302,11 +334,7 @@ export function rehypeAscii() {
         };
         node.tagName = "div";
         node.properties = { className: ["table-frames"] };
-        node.children = [
-          makeTablePre(buildAsciiTable(rows, headerRows, FRAME_W), "frame-wide"),
-          makeTablePre(buildAsciiTable(rows, headerRows, MOBILE_FRAME_W), "frame-
narrow"),
-          original,
-        ];
+        node.children = [...pres, original];
         return;
       }
 
@@ -340,17 +368,28 @@ export function rehypeAscii() {
         const img = kids[0];
         if (kids.length !== 1 || img.type !== "element" || img.tagName !== "img")
 return;
 
-        fig++;
         const alt = String((img.properties as Properties | undefined)?.alt ?? "")
.trim();
-        const caption = alt ? `FIG. ${fig} - ${alt.toUpperCase()}` : `FIG. ${fig}
`;
+        const caption = alt ? `FIG. ${fig + 1} - ${alt.toUpperCase()}` : `FIG. ${
fig + 1}`;
+        let caps: Element[];
+        try {
+          caps = [
+            captionSpan(caption, FRAME_W, "frame-wide"),
+            captionSpan(caption, MOBILE_FRAME_W, "frame-narrow"),
+          ];
+        } catch (e) {
+          // lenient: an unbreakable alt defeats the caption - leave
+          // the image as a plain paragraph
+          if (!lenient) throw e;
+          return;
+        }
+        fig++;
         node.tagName = "figure";
         node.properties = { className: ["figure-frame"] };
         node.children = [
           ruleSpan(FRAME_W, "frame-wide", "fig-rule"),
           ruleSpan(MOBILE_FRAME_W, "frame-narrow", "fig-rule"),
           img,
-          captionSpan(caption, FRAME_W, "frame-wide"),
-          captionSpan(caption, MOBILE_FRAME_W, "frame-narrow"),
+          ...caps,
           ruleSpan(FRAME_W, "frame-wide", "fig-rule"),
           ruleSpan(MOBILE_FRAME_W, "frame-narrow", "fig-rule"),
         ];

diff --git a/ooknet-design/src/lib/rehype-as
cii.ts b/ooknet-design/src/lib/rehype-ascii.
ts
index f94d6f0..76befbd 100644
--- a/ooknet-design/src/lib/rehype-ascii.ts
+++ b/ooknet-design/src/lib/rehype-ascii.ts
@@ -232,7 +232,12 @@ function parseFence(tex
t: string): { body: string; captionText: str
ing | null }
     : { body: trimmed, captionText: null };
 }
 
-export function rehypeAscii() {
+// Lenient mode is for markdown we don't ow
n (repo readmes): a
+// construct that defeats the grid degrades
 to a plain code frame
+// instead of failing the build. Site conte
nt never sets it and keeps
+// failing loudly.
+export function rehypeAscii(opts: { lenient
?: boolean } = {}) {
+  const lenient = opts.lenient === true;
   return (tree: Root) => {
     let fig = 0;
 
@@ -253,29 +258,38 @@ export function rehype
Ascii() {
         // through mermaid-ascii. Art wider
 than the narrow frame
         // scrolls.
         if (lang === "diagram" || lang === 
"mermaid") {
-          fig++;
           const { body, captionText } = par
seFence(text);
-          const caption = captionText ? `FI
G. ${fig} - ${captionText.toUpperCase()}` : 
`FIG. ${fig}`;
-          const art = lang === "mermaid" ? 
mermaidToAscii(body, FRAME_W) : body;
-          checkGrid(art, FRAME_W, "diagram"
);
-
-          node.tagName = "figure";
-          node.properties = { className: ["
figure-frame", "diagram-frame"], role: "img"
, "aria-label": caption };
-          node.children = [
-            ruleSpan(FRAME_W, "frame-wide",
 "fig-rule"),
-            ruleSpan(MOBILE_FRAME_W, "frame
-narrow", "fig-rule"),
-            {
-              type: "element",
-              tagName: "pre",
-              properties: { className: ["as
cii", "diagram-art"], "aria-hidden": "true" 
},
-              children: [{ type: "text", va
lue: art }],
-            },
-            captionSpan(caption, FRAME_W, "
frame-wide"),
-            captionSpan(caption, MOBILE_FRA
ME_W, "frame-narrow"),
-            ruleSpan(FRAME_W, "frame-wide",
 "fig-rule"),
-            ruleSpan(MOBILE_FRAME_W, "frame
-narrow", "fig-rule"),
-          ];
-          return;
+          const caption = captionText ? `FI
G. ${fig + 1} - ${captionText.toUpperCase()}
` : `FIG. ${fig + 1}`;
+          let figChildren: ElementContent[]
 | null = null;
+          try {
+            const art = lang === "mermaid" 
? mermaidToAscii(body, FRAME_W) : body;
+            checkGrid(art, FRAME_W, "diagra
m");
+            figChildren = [
+              ruleSpan(FRAME_W, "frame-wide
", "fig-rule"),
+              ruleSpan(MOBILE_FRAME_W, "fra
me-narrow", "fig-rule"),
+              {
+                type: "element",
+                tagName: "pre",
+                properties: { className: ["
ascii", "diagram-art"], "aria-hidden": "true
" },
+                children: [{ type: "text", 
value: art }],
+              },
+              captionSpan(caption, FRAME_W,
 "frame-wide"),
+              captionSpan(caption, MOBILE_F
RAME_W, "frame-narrow"),
+              ruleSpan(FRAME_W, "frame-wide
", "fig-rule"),
+              ruleSpan(MOBILE_FRAME_W, "fra
me-narrow", "fig-rule"),
+            ];
+          } catch (e) {
+            // lenient: fall through and re
nder the fence as a plain
+            // code frame instead
+            if (!lenient) throw e;
+          }
+          if (figChildren) {
+            fig++;
+            node.tagName = "figure";
+            node.properties = { className: 
["figure-frame", "diagram-frame"], role: "im
g", "aria-label": caption };
+            node.children = figChildren;
+            return;
+          }
         }
 
         node.tagName = "div";
@@ -292,6 +306,24 @@ export function rehypeA
scii() {
         const { rows, headerRows } = tableC
ells(node);
         if (!rows.length) return;
 
+        // A table with too many columns fo
r the frame defeats the
+        // grid; lenient mode degrades just
 this table to a code frame
+        // whose rows hard-wrap, so it can'
t fail.
+        let pres: Element[];
+        try {
+          pres = [
+            makeTablePre(buildAsciiTable(ro
ws, headerRows, FRAME_W), "frame-wide"),
+            makeTablePre(buildAsciiTable(ro
ws, headerRows, MOBILE_FRAME_W), "frame-narr
ow"),
+          ];
+        } catch (e) {
+          if (!lenient) throw e;
+          const text = rows.map((r) => r.ma
p((c) => c.text).join(" | ")).join("\n");
+          pres = [
+            makeCodePre(text, "table", FRAM
E_W, "frame-wide"),
+            makeCodePre(text, "table", MOBI
LE_FRAME_W, "frame-narrow"),
+          ];
+        }
+
         // Keep the semantic table for scre
en readers; the ASCII pres
         // are presentation only.
         const original: Element = {
@@ -302,11 +334,7 @@ export function rehypeA
scii() {
         };
         node.tagName = "div";
         node.properties = { className: ["ta
ble-frames"] };
-        node.children = [
-          makeTablePre(buildAsciiTable(rows
, headerRows, FRAME_W), "frame-wide"),
-          makeTablePre(buildAsciiTable(rows
, headerRows, MOBILE_FRAME_W), "frame-narrow
"),
-          original,
-        ];
+        node.children = [...pres, original]
;
         return;
       }
 
@@ -340,17 +368,28 @@ export function rehype
Ascii() {
         const img = kids[0];
         if (kids.length !== 1 || img.type !
== "element" || img.tagName !== "img") retur
n;
 
-        fig++;
         const alt = String((img.properties 
as Properties | undefined)?.alt ?? "").trim(
);
-        const caption = alt ? `FIG. ${fig} 
- ${alt.toUpperCase()}` : `FIG. ${fig}`;
+        const caption = alt ? `FIG. ${fig +
 1} - ${alt.toUpperCase()}` : `FIG. ${fig + 
1}`;
+        let caps: Element[];
+        try {
+          caps = [
+            captionSpan(caption, FRAME_W, "
frame-wide"),
+            captionSpan(caption, MOBILE_FRA
ME_W, "frame-narrow"),
+          ];
+        } catch (e) {
+          // lenient: an unbreakable alt de
feats the caption - leave
+          // the image as a plain paragraph
+          if (!lenient) throw e;
+          return;
+        }
+        fig++;
         node.tagName = "figure";
         node.properties = { className: ["fi
gure-frame"] };
         node.children = [
           ruleSpan(FRAME_W, "frame-wide", "
fig-rule"),
           ruleSpan(MOBILE_FRAME_W, "frame-n
arrow", "fig-rule"),
           img,
-          captionSpan(caption, FRAME_W, "fr
ame-wide"),
-          captionSpan(caption, MOBILE_FRAME
_W, "frame-narrow"),
+          ...caps,
           ruleSpan(FRAME_W, "frame-wide", "
fig-rule"),
           ruleSpan(MOBILE_FRAME_W, "frame-n
arrow", "fig-rule"),
         ];
 

diff --git a/ooknet-design/src/pages/git/[repo]/index.astro b/ooknet-design/src/pa
ges/git/[repo]/index.astro
index 1bd6fad..367118b 100644
--- a/ooknet-design/src/pages/git/[repo]/index.astro
+++ b/ooknet-design/src/pages/git/[repo]/index.astro
@@ -30,8 +30,9 @@ const branch = info.branch;
 const entries = dirEntries(repo.path, "", branch);
 const log = repoLog(repo.path, 5, `/git/${repo.slug}/`, branch);
 const readme = readReadme(repo.path, branch);
-// foreign readmes can defeat the grid (wide tables, exotic markdown);
-// degrade to plain text rather than failing the whole build
+// renderMarkdown runs lenient, so a construct that defeats the grid
+// degrades on its own; the catch is last-resort insurance so a readme
+// can never fail the whole build
 let readmeHtml: string | null = null;
 if (readme) {
   try {

diff --git a/ooknet-design/src/pages/git/[re
po]/index.astro b/ooknet-design/src/pages/gi
t/[repo]/index.astro
index 1bd6fad..367118b 100644
--- a/ooknet-design/src/pages/git/[repo]/ind
ex.astro
+++ b/ooknet-design/src/pages/git/[repo]/ind
ex.astro
@@ -30,8 +30,9 @@ const branch = info.branch
;
 const entries = dirEntries(repo.path, "", b
ranch);
 const log = repoLog(repo.path, 5, `/git/${r
epo.slug}/`, branch);
 const readme = readReadme(repo.path, branch
);
-// foreign readmes can defeat the grid (wid
e tables, exotic markdown);
-// degrade to plain text rather than failin
g the whole build
+// renderMarkdown runs lenient, so a constr
uct that defeats the grid
+// degrades on its own; the catch is last-r
esort insurance so a readme
+// can never fail the whole build
 let readmeHtml: string | null = null;
 if (readme) {
   try {
 
[ BACK TO LOG ]
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET