feat: v0.2.5 — 内置语法高亮 + exportPDF + benchmark + CI 并行

- 新增 src/highlight.ts 零依赖轻量高亮器:13 种语言,先转义后
  着色保证安全,输出 me-hl-* 类,MeEditor.highlight 即插即用
- exportTool 新增 editor.exportPDF()(隐藏 iframe + window.print),
  exportHTML/exportPDF 共用 HTML 构建逻辑
- imagePaste 支持 maxSizeKB 限制,超限 toast 警告
- npm run bench 性能基准:parseMarkdown 1MB ≈ 51ms
- CI 测试拆分为 parser/core/rest 三个并行 job + 跨版本 verify job
- docs.html/README 补齐新 API、事件、高亮文档;取消跟踪 dist 产物;
  版本 0.2.4 → 0.2.5,测试 725 → 768
This commit is contained in:
2026-08-09 08:59:55 +08:00
parent fae31bba60
commit 5919dd9f66
13 changed files with 641 additions and 58 deletions
+101
View File
@@ -0,0 +1,101 @@
/**
* highlight.ts 单元测试
* 覆盖内置轻量语法高亮器的安全性与标记输出
*/
import { highlight, normalizeLanguage, registerLanguage, getSupportedLanguages } from '../src/highlight';
describe('highlight - 基础', () => {
test('未知语言返回转义纯文本', () => {
const out = highlight('const x = 1;', 'nonexistent');
expect(out).toBe('const x = 1;');
});
test('支持的语言别名归一化', () => {
expect(normalizeLanguage('js')).toBe('javascript');
expect(normalizeLanguage('ts')).toBe('typescript');
expect(normalizeLanguage('py')).toBe('python');
expect(normalizeLanguage('sh')).toBe('bash');
expect(normalizeLanguage('yml')).toBe('yaml');
expect(normalizeLanguage('Java')).toBe('java');
expect(normalizeLanguage('unknown')).toBe('');
expect(normalizeLanguage('')).toBe('');
});
test('关键词被标记', () => {
const out = highlight('const x = 1', 'js');
expect(out).toContain('<span class="me-hl-keyword">const</span>');
});
test('字符串被标记', () => {
const out = highlight('const s = "hello"', 'js');
expect(out).toContain('<span class="me-hl-string">"hello"</span>');
});
test('注释被标记', () => {
const out = highlight('// comment\nconst x = 1', 'js');
expect(out).toContain('<span class="me-hl-comment">// comment</span>');
});
test('数字被标记', () => {
const out = highlight('const n = 42', 'js');
expect(out).toContain('<span class="me-hl-number">42</span>');
});
test('函数调用被标记', () => {
const out = highlight('foo(bar)', 'js');
expect(out).toContain('<span class="me-hl-function">foo</span>');
});
test('python 井号注释与 def 关键词', () => {
const out = highlight('# note\ndef main():', 'python');
expect(out).toContain('<span class="me-hl-comment"># note</span>');
expect(out).toContain('<span class="me-hl-keyword">def</span>');
});
test('内置全局对象被标记', () => {
const out = highlight('console.log("x")', 'js');
expect(out).toContain('<span class="me-hl-builtin">console</span>');
});
});
describe('highlight - 安全性', () => {
test('HTML 特殊字符被转义,不产生裸标签', () => {
const out = highlight('<script>alert(1)</script>', 'js');
expect(out).not.toContain('<script>');
expect(out).not.toContain('<script');
expect(out).toContain('&lt;script&gt;');
});
test('字符串中的引号安全', () => {
const out = highlight('const s = "a<b>&"', 'js');
expect(out).not.toContain('a<b');
expect(out).toContain('a&lt;b&gt;');
});
test('恶意输入不破坏 span 结构', () => {
const out = highlight('"</span><script>x</script>"', 'js');
// 所有 span 配对(每开一个 <span 就有一个 </span>
const opens = (out.match(/<span/g) || []).length;
const closes = (out.match(/<\/span>/g) || []).length;
expect(opens).toBe(closes);
expect(out).not.toContain('<script');
});
});
describe('highlight - 语言注册', () => {
test('注册自定义语言', () => {
registerLanguage('foo', { keywords: ['frobnicate'], builtins: [] });
expect(normalizeLanguage('foo')).toBe('foo');
const out = highlight('frobnicate the widget', 'foo');
expect(out).toContain('<span class="me-hl-keyword">frobnicate</span>');
});
test('getSupportedLanguages 返回语言列表', () => {
const langs = getSupportedLanguages();
expect(Array.isArray(langs)).toBe(true);
expect(langs).toContain('javascript');
expect(langs).toContain('typescript');
expect(langs).toContain('python');
});
});
+1 -1
View File
@@ -29,7 +29,7 @@ describe('index.ts - 全局 API', () => {
test('VERSION 是字符串', () => {
expect(typeof VERSION).toBe('string');
expect(VERSION).toBe('0.2.4');
expect(VERSION).toBe('0.2.5');
});
test('api.default 是 api 本身', () => {