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>` 转 `<img>`,
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 */
621import { promises as fs } from "node:fs" ;
722import 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 - Z a - z ] [ A - Z a - z 0 - 9 : _ - ] * ( [ \t ] [ ^ < > ] * ) ? \s * \/ ? > / ;
1643
1744for ( 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 ( "<" , "<" ) . replaceAll ( ">" , ">" ) ,
3966 )
40- . replace ( / < (? ! [ A - Z a - z / ] [ A - Z a - z 0 - 9 : _ - ] * \s * \/ ? > ) [ ^ > ] * > / g , ( m ) =>
67+ . replace ( new RegExp ( ` <(?!${ VALID_TAG_LOOKAHEAD . source } )[^>]*>` , "g" ) , ( m ) =>
4168 m . replaceAll ( "<" , "<" ) . replaceAll ( ">" , ">" ) ,
4269 ) ;
4370
0 commit comments