@@ -122,8 +122,13 @@ function cleanBody(body) {
122122 s = s . replace ( / ` ` ` [ \s \S ] * ?` ` ` / g, "" ) ;
123123 // 行内代码
124124 s = s . replace ( / ` [ ^ ` \n ] + ` / g, "" ) ;
125- // MDX/HTML 标签(粗暴去掉所有 <...>)
126- s = s . replace ( / < [ ^ > ] + > / g, "" ) ;
125+ // MDX/HTML 标签:循环 replace 直到 stable,避免嵌套残留如 "<<script>>"
126+ // (单次 replace 后剩 "<script>" 仍含 < — CodeQL js/incomplete-multi-character-sanitization)
127+ let prev ;
128+ do {
129+ prev = s ;
130+ s = s . replace ( / < [ ^ < > ] * > / g, "" ) ;
131+ } while ( s !== prev ) ;
127132 // 图片/链接的 markdown 语法,保留可读文本
128133 s = s . replace ( / ! \[ [ ^ \] ] * \] \( [ ^ ) ] * \) / g, "" ) ;
129134 s = s . replace ( / \[ ( [ ^ \] ] + ) \] \( [ ^ ) ] + \) / g, "$1" ) ;
@@ -423,28 +428,53 @@ function writeFrontmatterDescription(relPath, newDescription) {
423428
424429 const [ , open , body , close , rest ] = fmMatch ;
425430
426- // 在 body 里定位 description 块:
427- // "description:" 起头行 + 0 或多个缩进续行(YAML block scalar 或 quoted multi-line)
428- // 终止于下一个顶级 yaml 键(行首匹配 `\w+:`)或 body 结尾
429- const descriptionBlockRe =
430- / ^ d e s c r i p t i o n : .* (?: \n (?: [ \t ] + .* | \s * ) ) * ?(? = \n [ \w - ] + : | $ ) / m;
431+ // 在 body 里定位 description 块用**逐行扫描**,不用单一巨型正则:
432+ // 之前用 /^description:.*(?:\n(?:[ \t]+.*|\s*))*?(?=\n[\w-]+:|$)/m 触发了
433+ // CodeQL js/redos —— 内层 (?:[ \t]+.*|\s*) 在 \n 上 ambiguous,指数回溯。
434+ // 改逐行:找 "description:" 起始行,往下吃缩进续行(YAML block scalar /
435+ // multi-line quoted),遇到下一个顶级 yaml 键(行首 `\w+:`)或 body 结尾停。
436+ const lines = body . split ( "\n" ) ;
437+ const TOP_LEVEL_KEY_RE = / ^ [ \w - ] + : / ; // 顶级 yaml 键的标志
438+ let descStart = - 1 ;
439+ for ( let i = 0 ; i < lines . length ; i ++ ) {
440+ if ( lines [ i ] . startsWith ( "description:" ) ) {
441+ descStart = i ;
442+ break ;
443+ }
444+ }
431445
432446 let newBody ;
433- if ( descriptionBlockRe . test ( body ) ) {
434- newBody = body . replace ( descriptionBlockRe , newLine ) ;
447+ if ( descStart >= 0 ) {
448+ // 找 description 块结束:descStart+1 起,第一行命中顶级键或空行段后再命中顶级键的位置
449+ let descEnd = descStart ;
450+ for ( let i = descStart + 1 ; i < lines . length ; i ++ ) {
451+ const line = lines [ i ] ;
452+ // 缩进/空行视为 description 续行
453+ if ( line === "" || / ^ [ \t ] / . test ( line ) ) {
454+ descEnd = i ;
455+ continue ;
456+ }
457+ // 顶级键出现:description 块到 descEnd 为止
458+ if ( TOP_LEVEL_KEY_RE . test ( line ) ) break ;
459+ // 其他情况(理论上不应该出现):也归 description 续行兜底
460+ descEnd = i ;
461+ }
462+ // 替换 [descStart, descEnd] 这段为单行 newLine
463+ const before = lines . slice ( 0 , descStart ) ;
464+ const after = lines . slice ( descEnd + 1 ) ;
465+ newBody = [ ...before , newLine , ...after ] . join ( "\n" ) ;
435466 } else {
436- // 没有 description 字段,插在首行(一般 title 后)
437- const lines = body . split ( "\n" ) ;
438- // 找到第一个非空 yaml 键行后插入(保持 title 在前的常见风格)
467+ // 没有 description 字段,插在首个顶级 yaml 键行后(一般是 title 后)
439468 let insertAt = 1 ;
440469 for ( let i = 0 ; i < lines . length ; i ++ ) {
441- if ( / ^ [ \w - ] + : / . test ( lines [ i ] ) ) {
470+ if ( TOP_LEVEL_KEY_RE . test ( lines [ i ] ) ) {
442471 insertAt = i + 1 ;
443472 break ;
444473 }
445474 }
446- lines . splice ( insertAt , 0 , newLine ) ;
447- newBody = lines . join ( "\n" ) ;
475+ const newLines = [ ...lines ] ;
476+ newLines . splice ( insertAt , 0 , newLine ) ;
477+ newBody = newLines . join ( "\n" ) ;
448478 }
449479
450480 fs . writeFileSync ( abs , open + newBody + close + rest , "utf-8" ) ;
0 commit comments