From 8667956a0b9978fda83a3a8767a4c60551569f8a Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Sat, 24 Oct 2026 00:47:48 +0200 Subject: [PATCH] fix: break long paths at their separators --- website/src/components/PageTitle.astro | 6 +- website/src/plugins/rehype-short-code.mjs | 74 ++++++++++++++++++++--- 2 files changed, 72 insertions(+), 8 deletions(-) diff --git a/website/src/components/PageTitle.astro b/website/src/components/PageTitle.astro index b4795bb3..3b4486a2 100644 --- a/website/src/components/PageTitle.astro +++ b/website/src/components/PageTitle.astro @@ -8,6 +8,10 @@ // gets its initials in the same square, as its tile does (sitemarkup.py // platform_tiles). // +// Each crumb's label sits in a span: the item keeps its "/" (retrobios.css, +// `li { white-space: nowrap }`) and only the label wraps, so a long label +// never leaves the separator alone at the start of a line. +// // The crumbs and the title stay out of the search index: every excerpt // began with the title and the crumbs matched "bios" on every page. The // title still names the result, as the page's title field. @@ -53,7 +57,7 @@ const setup = !logoSrc && setups.find((entry) => id === `platforms/${entry.platf diff --git a/website/src/plugins/rehype-short-code.mjs b/website/src/plugins/rehype-short-code.mjs index 72b4f57b..ab724a8b 100644 --- a/website/src/plugins/rehype-short-code.mjs +++ b/website/src/plugins/rehype-short-code.mjs @@ -4,7 +4,14 @@ // - Inline code of SHORT_CODE characters or fewer gets `rb-short` and never // breaks: `--restore` split into `--` and `restore` on the home page at // 1440 px, and `--list-` / `systems` in the installer's options table. -// Longer inline code (a path, a full command) still wraps. +// Longer inline code (a path, a full command) still wraps, and gets a +// `` after each `/` and `\` so a path breaks at a separator: +// `~/.var/app/org.libretro.RetroArch/config/r` then `etroarch/system/` on a +// 390 px phone. The break adds no character to a copy. Each `wbr` is an +// element of the page, and a page of file cards holds thousands of paths +// (bluemsx: 3,097 breaks on 9,369 elements, against a budget of 10,000). +// So the build writes the first BREAK_BUDGET of a page, those of its first +// screens, and site.ts writes the others as their block nears the screen. // - A table whose first column is "Option" gets `rb-options`, so its flags // keep their column. // @@ -40,24 +47,71 @@ const firstHeader = (ctx, table) => { // A code element is inline unless a `pre` holds it. export const isShort = (text, inPre) => !inPre && text.length > 0 && text.length <= SHORT_CODE; +export const BREAK_BUDGET = 200; +const SEPARATOR = /[/\\]/; +const SEPARATORS = /([/\\])(?!$)/g; + +// Inline code long enough to wrap, with a separator to wrap at. +export const breaksAtSeparators = (text, inPre) => !inPre && text.length > SHORT_CODE && SEPARATOR.test(text); + +const escapeText = (text) => text.replace(/&/g, '&').replace(//g, '>'); + +// The text as HTML with a break after each separator but the last character. +export const withBreaks = (html) => html.replace(SEPARATORS, '$1'); + +// How many breaks withBreaks() writes in the text. +export const breakCount = (text) => text.match(SEPARATORS)?.length ?? 0; + +// Takes *count* breaks from the page's budget, when it still holds them. +const spend = (room, count) => { + if (count > room.left) return false; + room.left -= count; + return true; +}; + // The cards and tables the generator writes as HTML reach this step as raw // text: their code elements are marked by rewriting the tag. A block that // holds a `pre` is left as written. const RAW_CODE = /]*)?>([^<]*)<\/code>/g; const ENTITY = /&(?:#\d+|#x[0-9a-f]+|[a-z]+);/gi; -export const markRawCode = (html) => { +export const markRawCode = (html, room = { left: Infinity }) => { if (html.includes(' { - if (/\bclass=/.test(attributes) || !isShort(text.replace(ENTITY, '_'), false)) return whole; + const plain = text.replace(ENTITY, '_'); + if (breaksAtSeparators(plain, false)) { + return spend(room, breakCount(text)) ? `${withBreaks(text)}` : whole; + } + if (/\bclass=/.test(attributes) || !isShort(plain, false)) return whole; return `${text}`; }); }; +// A code element whose children are all text gets them back split, a `wbr` +// element after each separator. One already split is left alone. +const breakAtSeparators = (ctx, code, room) => { + const children = code.children ?? []; + if (!children.length || children.some((child) => child.type !== 'text')) return; + if (!spend(room, breakCount(children.map((child) => child.value).join('')))) return; + // split() with a capturing group puts each separator at an odd index. + const parts = children.map((child) => child.value).join('').split(SEPARATORS).flatMap((piece, index) => [ + ...(piece ? [{ type: 'text', value: piece }] : []), + ...(index % 2 ? [{ type: 'element', tagName: 'wbr', properties: {}, children: [] }] : []), + ]); + ctx.replaceNode(code, { type: 'element', tagName: 'code', properties: { ...(code.properties ?? {}) }, children: parts }); +}; + +// The page's budget of breaks, kept with the page being built. +const breakRoom = (ctx) => { + ctx.data.rbBreaks ??= { left: BREAK_BUDGET }; + return ctx.data.rbBreaks; +}; + export default function shortCode() { return { name: 'rb-short-code', raw(node, ctx) { + const room = breakRoom(ctx); // Inline HTML in Markdown (a hash in a table cell) arrives in three // nodes: the opening tag, the text, the closing tag. const open = /^]*)?>$/.exec(node.value ?? ''); @@ -65,13 +119,17 @@ export default function shortCode() { const siblings = ctx.parent(node)?.children ?? []; const index = ctx.indexOf(node) ?? -1; const [text, close] = [siblings[index + 1], siblings[index + 2]]; - if (text?.type === 'text' && close?.type === 'raw' && close.value === '' - && !/\bclass=/.test(open[1] ?? '') && isShort(text.value, false)) { + if (text?.type !== 'text' || close?.type !== 'raw' || close.value !== '') return; + if (breaksAtSeparators(text.value, false)) { + if (spend(room, breakCount(text.value))) { + ctx.replaceNode(text, { type: 'raw', value: withBreaks(escapeText(text.value)) }); + } + } else if (!/\bclass=/.test(open[1] ?? '') && isShort(text.value, false)) { ctx.replaceNode(node, { type: 'raw', value: `` }); } return; } - const value = markRawCode(node.value ?? ''); + const value = markRawCode(node.value ?? '', room); if (value !== node.value) ctx.replaceNode(node, { type: 'raw', value }); }, element: [ @@ -80,7 +138,9 @@ export default function shortCode() { visit(code, ctx) { const parent = ctx.parent(code); const inPre = parent?.type === 'element' && parent.tagName === 'pre'; - if (isShort(ctx.textContent(code), inPre)) addClass(ctx, code, 'rb-short'); + const text = ctx.textContent(code); + if (isShort(text, inPre)) addClass(ctx, code, 'rb-short'); + else if (breaksAtSeparators(text, inPre)) breakAtSeparators(ctx, code, breakRoom(ctx)); }, }, {