feat: v0.4.0 — 安全修复×6 + 增量性能×3 + Playwright E2E + 实例级 i18n
CI / test-parser (push) Successful in 9m27s
CI / test-core (push) Successful in 9m34s
CI / test-rest (push) Successful in 9m27s
CI / e2e (push) Failing after 5m15s
CI / verify (20.x) (push) Successful in 9m54s
CI / verify (18.x) (push) Successful in 9m56s
CI / verify (24.x) (push) Successful in 9m48s

0.3.0 修复版:
- 脚注 id 属性注入 XSS 防护(行内引用 + 脚注区)
- 全局插件注入移至 afterCreate(searchReplace 等依赖 textarea 的插件真正生效)+ use() 同名防重
- 行内渲染缓存附加 refs 指纹,防跨文档引用链接串数据
- 白名单裸标签 <u>/</u> 透传,underline(Ctrl+U)预览可见
- getStatus()/渲染 env 使用实例 locale;replaceAll 替换文本按字面处理
- replaceAllRegex 保留 $1 捕获组语义

0.3.1 性能版:
- 高亮规则按语言缓存(registerLanguage 覆盖失效),bench +33%
- 统计增量计算:字数/词数/行数差异区间,击键零全量扫描
- outline 树构建 O(n²) → 迭代栈 O(n)
- 拖放图片 >500KB 拦截、scrollToLine 真实行高、undo/redo 光标恢复
- selectionChange/cursorMove 与浮动工具栏解耦、unregisterShortcut 大小写归一
- toast 接入 7 种 ANIMATIONS、实例主题订阅随 destroy 断开(dispose)

0.4.0 工程版:
- Playwright 真实浏览器冒烟测试(e2e/,22 项断言)+ CI e2e job
- sideEffects: false 便于 tree-shaking
- MeEditor.destroy() 全面复位(全局钩子清理 + 内部注入钩子重建)
- 实例 locale 统一作用于全部 UI 文案(状态栏/工具栏/右键菜单/大纲)

测试 782 → 826,全绿;typecheck/lint/build 通过
This commit is contained in:
2026-08-09 12:31:14 +08:00
parent 64aedb9546
commit cb0caff4cf
24 changed files with 1122 additions and 129 deletions
+87
View File
@@ -1329,6 +1329,93 @@ describe('parseMarkdown - v0.2.5 title 转义', () => {
});
});
describe('parseMarkdown - v0.2.5 脚注 id 属性注入防护', () => {
test('脚注引用 id 中的引号不能注入属性', () => {
const html = parseMarkdown('[^a" onclick="alert(1)]\n\n[^a" onclick="alert(1)]: note');
expect(html).not.toContain(' onclick="');
expect(html).not.toContain('onclick="alert(1)"');
expect(html).toContain('&quot; onclick');
});
test('脚注区 id 中的引号被转义', () => {
const html = parseMarkdown('x[^a" onload="x]\n\n[^a" onload="x]: note');
expect(html).not.toContain(' onload="');
expect(html).toContain('&quot; onload');
});
test('正常脚注 id 不受影响', () => {
const html = parseMarkdown('text[^1]\n\n[^1]: note');
expect(html).toContain('id="fnref-1"');
expect(html).toContain('id="fn-1"');
});
});
// ============ v0.3.0 白名单裸标签透传(underline ============
describe('parseMarkdown - v0.3.0 underline 渲染', () => {
test('裸 <u> 标签透传为下划线', () => {
const html = parseMarkdown('<u>underlined</u>');
expect(html).toContain('<p><u>underlined</u></p>');
});
test('Ctrl+U 命令输出在预览中可见', () => {
const html = parseMarkdown('<u>hello</u> world');
expect(html).toContain('<u>hello</u>');
expect(html).not.toContain('&lt;u&gt;');
});
test('带属性的 <u onclick> 仍被转义', () => {
const html = parseMarkdown('<u onclick="alert(1)">x</u>');
expect(html).not.toContain('<u onclick');
expect(html).toContain('&lt;u');
});
test('行内代码中的 <u> 不被透传', () => {
const html = parseMarkdown('`<u>x</u>`');
expect(html).toContain('<code>&lt;u&gt;x&lt;/u&gt;</code>');
expect(html).not.toContain('<u>');
});
test('代码块中的 <u> 不被透传', () => {
const html = parseMarkdown('```\n<u>x</u>\n```');
expect(html).toContain('&lt;u&gt;');
expect(html).not.toContain('<u>');
});
test('<script> 仍被转义(白名单外)', () => {
const html = parseMarkdown('<script>alert(1)</script>');
expect(html).not.toContain('<script>');
});
});
// ============ v0.3.0 渲染缓存 refs 指纹 ============
describe('parseMarkdown - v0.3.0 渲染缓存 refs 指纹', () => {
test('相同文本不同引用定义互不串数据', () => {
clearRenderCache();
const htmlA = parseMarkdown('[x][r]\n\n[r]: https://a.com');
const htmlB = parseMarkdown('[x][r]\n\n[r]: https://b.com');
expect(htmlA).toContain('href="https://a.com"');
expect(htmlB).toContain('href="https://b.com"');
expect(htmlA).not.toContain('href="https://b.com"');
expect(htmlB).not.toContain('href="https://a.com"');
});
test('无引用定义的文本仍可命中缓存', () => {
clearRenderCache();
expect(parseMarkdown('**bold** text')).toBe(parseMarkdown('**bold** text'));
});
test('引用链接与无引用文本混合时各自正确', () => {
clearRenderCache();
const htmlA = parseMarkdown('[x][r] and **plain**\n\n[r]: https://a.com');
const htmlB = parseMarkdown('[x][r] and **plain**\n\n[r]: https://b.com');
expect(htmlA).toContain('https://a.com');
expect(htmlB).toContain('https://b.com');
expect(htmlB).toContain('<strong>plain</strong>');
});
});
describe('parseMarkdown - v0.2.5 URL 属性无双重转义', () => {
test('链接 URL 中的 & 不双重转义', () => {
const html = parseMarkdown('[x](https://example.com/?a=1&b=2)');