From 40f5b3f740bea961d6115d3d6a011b2a881f65bc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 19:27:04 +0000 Subject: [PATCH] fix(prebuild): escape-angles idempotency for HTML tags with attributes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit prebuild 跑 escape-angles 每次都把 leetcode markdown 注释里的 \`\` / \`\` 改成 \`<img src="..." />\`, 但 commit 进 git 后 main 上文件又是 \`\` —— 永久脏。Backfill workflow ssh 进服务器要求 git status 干净,每次都在这两个文件上卡。 根因:negative lookahead /<(?![A-Za-z/][A-Za-z0-9:_-]*\s*\/?>)[^>]*>/ 只接受 \`
\` / \`
\` / \`
\` 这种**无属性**标签。带属性的合法 HTML(\`\`、\`
\`)会被误判为"可疑尖括号" 进入 escape 分支。 修:lookahead 加属性段 \`([ \\t][^<>]*)?\`,让 \`\` / \`\` 都被识别为正常标签不 escape。\`[^<>]\` 避免 ReDoS 嵌套量词。 验证:跑两次 escape-angles,working tree 不变。之前 142.环形链表 II 和类似文件每次 build 都会脏,现在稳定。 不影响仍要 escape 的真正"可疑尖括号":\`<8>\` / \`<1,2,3>\` / \`\` 仍被识别为非合法标签并 escape。 --- scripts/escape-angles.mjs | 41 ++++++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/scripts/escape-angles.mjs b/scripts/escape-angles.mjs index 8ba5c801..fdb3df1f 100644 --- a/scripts/escape-angles.mjs +++ b/scripts/escape-angles.mjs @@ -2,17 +2,44 @@ * @description 转义尖括号脚本 * @author Siz Long * @date 2025-09-27 + * + * 2026-05 更新:修幂等性 bug + * 旧 negative lookahead `(?![A-Za-z/][A-Za-z0-9:_-]*\s*\/?>)` 只接受 + *
/
/
这种**无属性**的标签,把 ``、 + * `
` 这种**带属性的合法 HTML** 误判为"可疑尖括号"并 escape。 + * 导致:每次 build 都把 leetcode markdown 注释里的 `` 转 `<img>`, + * working tree 永久脏。Backfill workflow(要求 git status 干净)反复被阻断。 + * + * 修:lookahead 加属性段 `([ \t][^<>]*)?`,让 `` 也被 + * 识别为正常 HTML 不 escape。 + * + * 极简策略: + * 1) 跳过 fenced code / inline code(保留原样) + * 2) 仅在普通文本行内转义形如 <数字开头...> 或 <单词里含逗号/空格/数学符号...> 的片段 + * 3) 不动像 /
/ (含属性)这类"正常标签" */ import { promises as fs } from "node:fs"; import fg from "fast-glob"; +const files = await fg(["content/docs/**/*.md"], { dot: false }); + /** - * 极简策略: - * 1) 跳过 fenced code / inline code(保留原样) - * 2) 仅在普通文本行内转义形如 <数字开头...> 或 <单词里含逗号/空格/数学符号...> 的片段 - * 3) 不动像 /
这类“正常标签/组件名”的片段 + * 正常 HTML/JSX 标签匹配模式(用于 negative lookahead): + * - 可选 `/` 表示闭合标签 (
) + * - 标签名首字母为字母,后跟字母/数字/冒号/下划线/连字符 + * - 可选属性段:空格后跟任意非尖括号字符([^<>] 防 ReDoS) + * - 可选 `/` 表示自闭合 (
) + * - `>` 收尾 + * + * 接受样例(lookahead 命中,不 escape): + *

+ *
+ * + * + * 不接受样例(lookahead miss,进 escape 分支): + * <8> <1,2,3> */ -const files = await fg(["content/docs/**/*.md"], { dot: false }); +const VALID_TAG_LOOKAHEAD = /\/?[A-Za-z][A-Za-z0-9:_-]*([ \t][^<>]*)?\s*\/?>/; for (const file of files) { let src = await fs.readFile(file, "utf8"); @@ -30,14 +57,14 @@ for (const file of files) { return `__CODE_BLOCK_${blocks.length - 1}__`; }); - // 在普通文本里做“可疑尖括号”的转义: + // 在普通文本里做"可疑尖括号"的转义: // - <\d...> 如 <8>、<1,2,3> // - <[^\s/>][^>]*[,;+\-*/= ]+[^>]*> 含明显非标签符号的 src = src .replace(/<\d[^>]*>/g, (m) => m.replaceAll("<", "<").replaceAll(">", ">"), ) - .replace(/<(?![A-Za-z/][A-Za-z0-9:_-]*\s*\/?>)[^>]*>/g, (m) => + .replace(new RegExp(`<(?!${VALID_TAG_LOOKAHEAD.source})[^>]*>`, "g"), (m) => m.replaceAll("<", "<").replaceAll(">", ">"), );