feat: v0.2.2 — plugin refactor, i18n, outline tracking, formatTable, code title, fr locale
## Added
- French (fr) locale with 60+ translations
- Outline panel scroll tracking with active heading highlight
- exec('formatTable') for auto-aligning Markdown table columns
- Code block title rendering via ```js title=hello.js```
- Escaped pipe \| support in table cells
- Custom block token fallback rendering as <div>
- Gutter incremental DOM update (no more full innerHTML)
- 694 tests (+10)
## Changed
- All 6 preset plugins refactored to closure-based state (no this pollution)
- shortcutHelp plugin now i18n-aware (shortcut labels follow locale)
- Context menu labels (Cut/Copy/Paste/Select All) i18n-translated
- Warm theme now has progressBg + closeHoverBg fields
This commit is contained in:
@@ -1947,3 +1947,72 @@ describe('MarkdownEditor - v0.2.1 分隔条持久化', () => {
|
||||
ed.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
// ============ v0.2.2 formatTable 测试 ============
|
||||
|
||||
describe('MarkdownEditor - v0.2.2 formatTable', () => {
|
||||
test('exec formatTable 对齐表格列', () => {
|
||||
document.body.innerHTML = '';
|
||||
const c = document.createElement('div');
|
||||
document.body.appendChild(c);
|
||||
const ed = new MarkdownEditor(c, { value: '| a | b |\n| --- | --- |\n| x | yyy |' });
|
||||
ed.textarea.setSelectionRange(10, 10); // 光标在表格内
|
||||
ed.exec('formatTable');
|
||||
const v = ed.getValue();
|
||||
// Both columns padded to max width
|
||||
expect(v).toMatch(/\| a\s+\| b\s+\|/);
|
||||
expect(v).toMatch(/\| x\s+\| yyy\s+\|/);
|
||||
ed.destroy();
|
||||
});
|
||||
|
||||
test('exec formatTable 无表格时不变', () => {
|
||||
document.body.innerHTML = '';
|
||||
const c = document.createElement('div');
|
||||
document.body.appendChild(c);
|
||||
const ed = new MarkdownEditor(c, { value: 'just some text' });
|
||||
ed.exec('formatTable');
|
||||
expect(ed.getValue()).toBe('just some text');
|
||||
ed.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
// ============ v0.2.2 gutter 增量更新 ============
|
||||
|
||||
describe('MarkdownEditor - v0.2.2 gutter 增量更新', () => {
|
||||
test('行数减少时 gutter 正确移除多余行', () => {
|
||||
document.body.innerHTML = '';
|
||||
const c = document.createElement('div');
|
||||
document.body.appendChild(c);
|
||||
const ed = new MarkdownEditor(c, { value: 'a\nb\nc', lineNumbers: true });
|
||||
expect(ed.gutter.children.length).toBe(3);
|
||||
ed.setValue('a');
|
||||
expect(ed.gutter.children.length).toBe(1);
|
||||
ed.destroy();
|
||||
});
|
||||
|
||||
test('行数增加时 gutter 正确追加', () => {
|
||||
document.body.innerHTML = '';
|
||||
const c = document.createElement('div');
|
||||
document.body.appendChild(c);
|
||||
const ed = new MarkdownEditor(c, { value: 'a', lineNumbers: true });
|
||||
expect(ed.gutter.children.length).toBe(1);
|
||||
ed.setValue('a\nb\nc\nd');
|
||||
expect(ed.gutter.children.length).toBe(4);
|
||||
ed.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
// ============ v0.2.2 大纲滚动跟踪 ============
|
||||
|
||||
describe('MarkdownEditor - v0.2.2 大纲滚动跟踪', () => {
|
||||
test('_trackOutlineScroll 注册滚动监听', () => {
|
||||
document.body.innerHTML = '';
|
||||
const c = document.createElement('div');
|
||||
document.body.appendChild(c);
|
||||
const ed = new MarkdownEditor(c, { value: '# H1\n## H2\n### H3', outline: true, mode: 'split' });
|
||||
ed._buildOutline();
|
||||
// 滚动 preview 应不抛错
|
||||
expect(() => ed.previewPane.dispatchEvent(new Event('scroll'))).not.toThrow();
|
||||
ed.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
+47
-2
@@ -1022,10 +1022,10 @@ describe('parseMarkdown - 覆盖率:链接引用回退', () => {
|
||||
});
|
||||
|
||||
describe('parseMarkdown - 覆盖率:自定义 token default 渲染', () => {
|
||||
test('未识别 token type 返回空字符串', () => {
|
||||
test('未识别 token type 渲染为回退 div', () => {
|
||||
const tokens = [{ type: 'unknownType', text: 'whatever' }];
|
||||
const html = renderTokens(tokens);
|
||||
expect(html).toBe('');
|
||||
expect(html).toContain('me-block-unknownType');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1225,3 +1225,48 @@ describe('parseMarkdown - v0.2.1 引用链接容错', () => {
|
||||
expect(() => renderTokens([{ type: 'paragraph', text: '[x][bad]' }], { refs: { bad: 'not-json' } })).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
// ============ v0.2.2 表格转义 ============
|
||||
|
||||
describe('parseMarkdown - v0.2.2 表格转义竖线', () => {
|
||||
test('表格单元格内 \\| 不被分割', () => {
|
||||
const md = '| Name | Value |\n| --- | --- |\n| a\\|b | c |';
|
||||
const html = parseMarkdown(md);
|
||||
// a\|b 应该作为单个单元格内容
|
||||
expect(html).toContain('a|b');
|
||||
});
|
||||
});
|
||||
|
||||
// ============ v0.2.2 代码块 info string ============
|
||||
|
||||
describe('parseMarkdown - v0.2.2 代码块属性', () => {
|
||||
test('代码块 title 属性被提取', () => {
|
||||
const { tokens } = parseTokens('```js title=hello.js\nconsole.log(1)\n```');
|
||||
const codeToken = tokens.find((t) => t.type === 'code');
|
||||
expect(codeToken).toBeDefined();
|
||||
expect(codeToken.lang).toBe('js');
|
||||
expect(codeToken.attrs).toBeDefined();
|
||||
expect(codeToken.attrs.title).toBe('hello.js');
|
||||
});
|
||||
|
||||
test('代码块 title 渲染为标题栏', () => {
|
||||
const html = parseMarkdown('```js title=demo.js\nvar x = 1;\n```');
|
||||
expect(html).toContain('me-code-title');
|
||||
expect(html).toContain('demo.js');
|
||||
});
|
||||
|
||||
test('无 title 时不渲染标题栏', () => {
|
||||
const html = parseMarkdown('```js\nvar x = 1;\n```');
|
||||
expect(html).not.toContain('me-code-title');
|
||||
});
|
||||
});
|
||||
|
||||
// ============ v0.2.2 自定义 token 回退渲染 ============
|
||||
|
||||
describe('parseMarkdown - v0.2.2 自定义 token 渲染', () => {
|
||||
test('自定义 token 包含 content 时渲染内容', () => {
|
||||
const html = renderTokens([{ type: 'customAlert', content: '**Important!**' }]);
|
||||
expect(html).toContain('me-block-customAlert');
|
||||
expect(html).toContain('<strong>Important!</strong>');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -820,17 +820,13 @@ describe('零散分支补全', () => {
|
||||
document.body.appendChild(c);
|
||||
const ed = new MarkdownEditor(c, { value: 'hello world' });
|
||||
ed.use(presetPlugins.searchReplace);
|
||||
const plugin = ed.getPlugins().find((p) => p.name === 'searchReplace');
|
||||
// Ctrl+F 打开面板
|
||||
ed.textarea.dispatchEvent(new KeyboardEvent('keydown', { key: 'f', ctrlKey: true, bubbles: true }));
|
||||
expect(ed.el.querySelector('.me-search')).not.toBeNull();
|
||||
expect(plugin._panel).toBeDefined();
|
||||
// spy _close 验证 textarea 上 Ctrl+Escape 分支被触发
|
||||
// 注:plugins.js 中 escape 分支在 if (!mod) return 之后,必须带 Ctrl/Meta
|
||||
const closeSpy = jest.spyOn(plugin, '_close');
|
||||
expect((ed as any).__srState._panel).toBeDefined();
|
||||
// Ctrl+Escape 关闭面板(带 Ctrl 才会进入分支)
|
||||
ed.textarea.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', ctrlKey: true, bubbles: true }));
|
||||
expect(closeSpy).toHaveBeenCalled();
|
||||
closeSpy.mockRestore();
|
||||
expect(ed.el.querySelector('.me-search')).toBeNull();
|
||||
ed.destroy();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user