Fix codespell and CodeQL findings in website components

- Fix real typos flagged by codespell: 'Couldn't' -> 'Couldn't'
  (plain apostrophe, matching convention elsewhere in JSX) and
  'Unparseable' -> 'Unparsable' in catalogFilters.ts
- DetailChassis.tsx: replace sequential HTML entity unescaping with a
  single-pass replace to avoid double-unescape/injection risk flagged
  by CodeQL
- SyntaxHighlightedCode.tsx: make the markup HTML comment regex match
  newlines so multi-line comments cannot break out of the token
  (Bad HTML filtering regexp)
- pagefindSearch.ts: strip HTML tags in a loop until stable so nested/
  malformed markup can't survive a single-pass strip (Incomplete
  multi-character sanitization)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 80686fef-efe3-4cdd-8cd6-bfa61a5d0af6
This commit is contained in:
Aaron Powell
2026-08-19 11:14:13 +10:00
parent 14fc807adb
commit 5c50251e78
6 changed files with 32 additions and 10 deletions
@@ -368,7 +368,7 @@ function RecipeFileView({
if (status === "error" || text === undefined) { if (status === "error" || text === undefined) {
return ( return (
<Text as="p" size="200" variant="muted"> <Text as="p" size="200" variant="muted">
Couldn&apos;t load this file. <a href={githubUrl}>View it on GitHub</a>. Couldn't load this file. <a href={githubUrl}>View it on GitHub</a>.
</Text> </Text>
); );
} }
+14 -5
View File
@@ -402,11 +402,20 @@ export function buildDetailToc(markdownHtml: string): {
(match, attrs: string, inner: string) => { (match, attrs: string, inner: string) => {
const label = inner const label = inner
.replace(/<[^>]+>/g, "") .replace(/<[^>]+>/g, "")
.replace(/&amp;/g, "&") .replace(/&lt;|&gt;|&quot;|&#39;|&amp;/g, (entity) => {
.replace(/&lt;/g, "<") switch (entity) {
.replace(/&gt;/g, ">") case "&lt;":
.replace(/&quot;/g, '"') return "<";
.replace(/&#39;/g, "'") case "&gt;":
return ">";
case "&quot;":
return '"';
case "&#39;":
return "'";
default:
return "&";
}
})
.trim(); .trim();
if (!label) return match; if (!label) return match;
+1 -1
View File
@@ -480,7 +480,7 @@ function FileView({
if (status === "error" || text === undefined) { if (status === "error" || text === undefined) {
return ( return (
<Text as="p" size="200" variant="muted"> <Text as="p" size="200" variant="muted">
Couldn&apos;t load this file. <a href={githubUrl}>View it on GitHub</a>. Couldn't load this file. <a href={githubUrl}>View it on GitHub</a>.
</Text> </Text>
); );
} }
@@ -52,7 +52,7 @@ const rules: Record<CodeLanguage, readonly TokenRule[]> = {
{ kind: "punctuation", expression: /[{}[\],:]/y }, { kind: "punctuation", expression: /[{}[\],:]/y },
], ],
markup: [ markup: [
{ kind: "comment", expression: /<!--.*?-->/y }, { kind: "comment", expression: /<!--[\s\S]*?-->/y },
{ kind: "tag", expression: /<\/?[A-Za-z][\w:-]*/y }, { kind: "tag", expression: /<\/?[A-Za-z][\w:-]*/y },
{ kind: "attr-name", expression: /[A-Za-z_:][\w:.-]*(?=\s*=)/y }, { kind: "attr-name", expression: /[A-Za-z_:][\w:.-]*(?=\s*=)/y },
{ kind: "string", expression: /"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/y }, { kind: "string", expression: /"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/y },
@@ -14,7 +14,7 @@ export const updatedBuckets: { label: string; max: number }[] = [
{ label: "Older", max: Number.POSITIVE_INFINITY }, { label: "Older", max: Number.POSITIVE_INFINITY },
]; ];
/** Whole days between `lastUpdated` and now. Unparseable dates sort as oldest. */ /** Whole days between `lastUpdated` and now. Unparsable dates sort as oldest. */
export function daysSince(lastUpdated: string | undefined): number { export function daysSince(lastUpdated: string | undefined): number {
if (!lastUpdated) return Number.POSITIVE_INFINITY; if (!lastUpdated) return Number.POSITIVE_INFINITY;
const then = Date.parse(lastUpdated); const then = Date.parse(lastUpdated);
+14 -1
View File
@@ -15,6 +15,19 @@ type PagefindResultData = {
meta?: Record<string, string | undefined>; meta?: Record<string, string | undefined>;
}; };
/** Strip HTML tags, repeating until no more tags remain so a malformed or
* nested markup fragment (e.g. `<<script>script>`) can't survive a single pass. */
function stripTags(html: string): string {
let text = html;
let previous: string;
do {
previous = text;
text = text.replace(/<[^>]*>/g, " ");
} while (text !== previous);
return text;
}
type PagefindModule = { type PagefindModule = {
options?: (opts: Record<string, unknown>) => Promise<void>; options?: (opts: Record<string, unknown>) => Promise<void>;
init?: () => Promise<void>; init?: () => Promise<void>;
@@ -127,7 +140,7 @@ export async function searchPagefind(
if (!title) continue; if (!title) continue;
items.push({ items.push({
title, title,
description: entry.excerpt?.replace(/<[^>]*>/g, "") ?? "", description: entry.excerpt ? stripTags(entry.excerpt) : "",
category: categoryOf(entry.url.split(/[?#]/)[0]), category: categoryOf(entry.url.split(/[?#]/)[0]),
href: entry.url, href: entry.url,
}); });