Skip to content

Commit 1dcd8ce

Browse files
authored
Merge pull request #343 from InvolutionHell/fix/escape-angles-idempotent
fix(prebuild): escape-angles idempotency for HTML tags with attributes
2 parents 9ee4e82 + 40f5b3f commit 1dcd8ce

1 file changed

Lines changed: 34 additions & 7 deletions

File tree

scripts/escape-angles.mjs

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,44 @@
22
* @description 转义尖括号脚本
33
* @author Siz Long
44
* @date 2025-09-27
5+
*
6+
* 2026-05 更新:修幂等性 bug
7+
* 旧 negative lookahead `(?![A-Za-z/][A-Za-z0-9:_-]*\s*\/?>)` 只接受
8+
* <div> / </div> / <br /> 这种**无属性**的标签,把 `<img src="..." />`、
9+
* `<a href="...">` 这种**带属性的合法 HTML** 误判为"可疑尖括号"并 escape。
10+
* 导致:每次 build 都把 leetcode markdown 注释里的 `<img>` 转 `&lt;img&gt;`,
11+
* working tree 永久脏。Backfill workflow(要求 git status 干净)反复被阻断。
12+
*
13+
* 修:lookahead 加属性段 `([ \t][^<>]*)?`,让 `<tagname attr="...">` 也被
14+
* 识别为正常 HTML 不 escape。
15+
*
16+
* 极简策略:
17+
* 1) 跳过 fenced code / inline code(保留原样)
18+
* 2) 仅在普通文本行内转义形如 <数字开头...> 或 <单词里含逗号/空格/数学符号...> 的片段
19+
* 3) 不动像 <Component> / <div> / <img src="..." />(含属性)这类"正常标签"
520
*/
621
import { promises as fs } from "node:fs";
722
import fg from "fast-glob";
823

24+
const files = await fg(["content/docs/**/*.md"], { dot: false });
25+
926
/**
10-
* 极简策略:
11-
* 1) 跳过 fenced code / inline code(保留原样)
12-
* 2) 仅在普通文本行内转义形如 <数字开头...> 或 <单词里含逗号/空格/数学符号...> 的片段
13-
* 3) 不动像 <Component> / <div> 这类“正常标签/组件名”的片段
27+
* 正常 HTML/JSX 标签匹配模式(用于 negative lookahead):
28+
* - 可选 `/` 表示闭合标签 (</div>)
29+
* - 标签名首字母为字母,后跟字母/数字/冒号/下划线/连字符
30+
* - 可选属性段:空格后跟任意非尖括号字符([^<>] 防 ReDoS)
31+
* - 可选 `/` 表示自闭合 (<br />)
32+
* - `>` 收尾
33+
*
34+
* 接受样例(lookahead 命中,不 escape):
35+
* <div> </div> <br />
36+
* <img src="..." /> <a href="x" title="y">
37+
* <Component prop="val" />
38+
*
39+
* 不接受样例(lookahead miss,进 escape 分支):
40+
* <8> <1,2,3> <x, y> <not a tag>
1441
*/
15-
const files = await fg(["content/docs/**/*.md"], { dot: false });
42+
const VALID_TAG_LOOKAHEAD = /\/?[A-Za-z][A-Za-z0-9:_-]*([ \t][^<>]*)?\s*\/?>/;
1643

1744
for (const file of files) {
1845
let src = await fs.readFile(file, "utf8");
@@ -30,14 +57,14 @@ for (const file of files) {
3057
return `__CODE_BLOCK_${blocks.length - 1}__`;
3158
});
3259

33-
// 在普通文本里做可疑尖括号的转义:
60+
// 在普通文本里做"可疑尖括号"的转义:
3461
// - <\d...> 如 <8>、<1,2,3>
3562
// - <[^\s/>][^>]*[,;+\-*/= ]+[^>]*> 含明显非标签符号的
3663
src = src
3764
.replace(/<\d[^>]*>/g, (m) =>
3865
m.replaceAll("<", "&lt;").replaceAll(">", "&gt;"),
3966
)
40-
.replace(/<(?![A-Za-z/][A-Za-z0-9:_-]*\s*\/?>)[^>]*>/g, (m) =>
67+
.replace(new RegExp(`<(?!${VALID_TAG_LOOKAHEAD.source})[^>]*>`, "g"), (m) =>
4168
m.replaceAll("<", "&lt;").replaceAll(">", "&gt;"),
4269
);
4370

0 commit comments

Comments
 (0)