fix(parser): 修复 RE_FENCE_START 匹配但被 fencedCode 拒绝时主循环死循环导致 CI Run tests 卡死
CI / test (18.x) (push) Successful in 10m3s
CI / test (20.x) (push) Successful in 10m4s
CI / test (22.x) (push) Successful in 10m0s
CI / test (24.x) (push) Successful in 10m0s

根因:输入如 \\\ \code\ \\\ 匹配 RE_FENCE_START 使 isBlockStart 返回 true,但 fencedCode handler 因 info string 含反引号拒绝处理,导致主循环既无法处理也无法跳过该行,进入无限循环。修复:段落 fallback 中当 para 为空且行匹配 RE_FENCE_START 含反引号时强制收集,并增加兜底 i++ 跳过无法处理的非空行。src/parser.ts:353-373

同时更新 README.md 和 site 站点测试数为 725(真实全量测试数)
This commit is contained in:
2026-07-28 21:48:35 +08:00
parent cf0bc45055
commit 9fe209ba88
11 changed files with 52 additions and 13 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
[![version](https://img.shields.io/badge/version-0.2.4-blue)](https://git.metona.cn/MetonaTeam/-/packages/npm/@metona-team%2Fmetona-editor)
[![license](https://img.shields.io/badge/license-MIT-green)](./LICENSE)
[![tests](https://img.shields.io/badge/tests-353%20passed-brightgreen)](./tests)
[![tests](https://img.shields.io/badge/tests-725%20passed-brightgreen)](./tests)
[![coverage](https://img.shields.io/badge/coverage-parser%2099%25-brightgreen)](./tests)
[![types](https://img.shields.io/badge/types-TypeScript%20strict-blue)](./tsconfig.json)
+11 -1
View File
@@ -1140,8 +1140,16 @@ const parseTokens = (md) => {
const l = lines[i];
if (RE_EMPTY.test(l))
break;
if (isBlockStart(l))
if (isBlockStart(l)) {
// 防止死循环:若该行匹配 RE_FENCE_START 但被 fencedCode handler 拒绝(info string 含反引号),
// 且 para 为空,则强制当作段落首行收集,否则主循环将无限重复处理此行
if (para.length === 0 && RE_FENCE_START.test(l) && l.includes('`')) {
para.push(l);
i++;
continue;
}
break;
}
if (i + 1 < lines.length && (RE_SETEXT_H1.test(lines[i + 1]) || RE_SETEXT_H2.test(lines[i + 1])))
break;
if (/\|/.test(l) && i + 1 < lines.length && isTableSeparator(lines[i + 1]))
@@ -1151,6 +1159,8 @@ const parseTokens = (md) => {
}
if (para.length)
tokens.push({ type: 'paragraph', text: para.join('\n') });
else if (i < lines.length && !RE_EMPTY.test(lines[i]))
i++; // 兜底:跳过无法处理的非空行
}
return { tokens, footnotes, refs };
};
+1 -1
View File
File diff suppressed because one or more lines are too long
+11 -1
View File
@@ -1136,8 +1136,16 @@ const parseTokens = (md) => {
const l = lines[i];
if (RE_EMPTY.test(l))
break;
if (isBlockStart(l))
if (isBlockStart(l)) {
// 防止死循环:若该行匹配 RE_FENCE_START 但被 fencedCode handler 拒绝(info string 含反引号),
// 且 para 为空,则强制当作段落首行收集,否则主循环将无限重复处理此行
if (para.length === 0 && RE_FENCE_START.test(l) && l.includes('`')) {
para.push(l);
i++;
continue;
}
break;
}
if (i + 1 < lines.length && (RE_SETEXT_H1.test(lines[i + 1]) || RE_SETEXT_H2.test(lines[i + 1])))
break;
if (/\|/.test(l) && i + 1 < lines.length && isTableSeparator(lines[i + 1]))
@@ -1147,6 +1155,8 @@ const parseTokens = (md) => {
}
if (para.length)
tokens.push({ type: 'paragraph', text: para.join('\n') });
else if (i < lines.length && !RE_EMPTY.test(lines[i]))
i++; // 兜底:跳过无法处理的非空行
}
return { tokens, footnotes, refs };
};
+1 -1
View File
File diff suppressed because one or more lines are too long
+11 -1
View File
@@ -1142,8 +1142,16 @@
const l = lines[i];
if (RE_EMPTY.test(l))
break;
if (isBlockStart(l))
if (isBlockStart(l)) {
// 防止死循环:若该行匹配 RE_FENCE_START 但被 fencedCode handler 拒绝(info string 含反引号),
// 且 para 为空,则强制当作段落首行收集,否则主循环将无限重复处理此行
if (para.length === 0 && RE_FENCE_START.test(l) && l.includes('`')) {
para.push(l);
i++;
continue;
}
break;
}
if (i + 1 < lines.length && (RE_SETEXT_H1.test(lines[i + 1]) || RE_SETEXT_H2.test(lines[i + 1])))
break;
if (/\|/.test(l) && i + 1 < lines.length && isTableSeparator(lines[i + 1]))
@@ -1153,6 +1161,8 @@
}
if (para.length)
tokens.push({ type: 'paragraph', text: para.join('\n') });
else if (i < lines.length && !RE_EMPTY.test(lines[i]))
i++; // 兜底:跳过无法处理的非空行
}
return { tokens, footnotes, refs };
};
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -49,7 +49,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Micr
<div class="badges">
<span class="badge">TypeScript</span>
<span class="badge">零运行时依赖</span>
<span class="badge">353 tests</span>
<span class="badge">725 tests</span>
<span class="badge">6 个插件</span>
<span class="badge">桌面端优先</span>
</div>
@@ -102,7 +102,7 @@ var demoMd=[
'# 🚀 MetonaEditor v0.2.4 全功能演示',
'',
'> TypeScript 重构 · 零运行时依赖 · **桌面端** Markdown Editor 库。',
'> 全模块 TypeScript 严格模式,12 个源文件,353 个测试全部通过。',
'> 全模块 TypeScript 严格模式,12 个源文件,725 个测试全部通过。',
'',
'## ✨ v0.2.4 新特性',
'',
+2 -2
View File
@@ -109,7 +109,7 @@ footer a:hover{text-decoration:underline}
<span class="badge accent">v0.2.4</span>
<span class="badge">TypeScript</span>
<span class="badge">零运行时依赖</span>
<span class="badge">353 tests</span>
<span class="badge">725 tests</span>
<span class="badge">桌面端优先</span>
<span class="badge">MIT</span>
</div>
@@ -120,7 +120,7 @@ footer a:hover{text-decoration:underline}
<div class="stats">
<div class="stat"><div class="stat-num">0</div><div class="stat-label">运行时依赖</div></div>
<div class="stat"><div class="stat-num">99%</div><div class="stat-label">解析器行覆盖</div></div>
<div class="stat"><div class="stat-num">353</div><div class="stat-label">单元测试</div></div>
<div class="stat"><div class="stat-num">725</div><div class="stat-label">单元测试</div></div>
<div class="stat"><div class="stat-num">~30KB</div><div class="stat-label">gzip 体积</div></div>
</div>
</header>
+10 -1
View File
@@ -355,12 +355,21 @@ const parseTokens = (md: string | null | undefined): ParseResult => {
while (i < lines.length) {
const l = lines[i];
if (RE_EMPTY.test(l)) break;
if (isBlockStart(l)) break;
if (isBlockStart(l)) {
// 防止死循环:若该行匹配 RE_FENCE_START 但被 fencedCode handler 拒绝(info string 含反引号),
// 且 para 为空,则强制当作段落首行收集,否则主循环将无限重复处理此行
if (para.length === 0 && RE_FENCE_START.test(l) && l.includes('`')) {
para.push(l); i++;
continue;
}
break;
}
if (i + 1 < lines.length && (RE_SETEXT_H1.test(lines[i + 1]) || RE_SETEXT_H2.test(lines[i + 1]))) break;
if (/\|/.test(l) && i + 1 < lines.length && isTableSeparator(lines[i + 1])) break;
para.push(l); i++;
}
if (para.length) tokens.push({ type: 'paragraph', text: para.join('\n') });
else if (i < lines.length && !RE_EMPTY.test(lines[i])) i++; // 兜底:跳过无法处理的非空行
}
return { tokens, footnotes, refs };
};