release: v0.1.3 — bug fixes, performance, hardening

Bug Fixes:
- fix(core): exec() now supports _customActions registered via addToolbarButton
- fix(core): _emit('focus'/'blur') no longer passes editor argument twice
- fix(core): getHTML() now triggers beforeRender/afterRender hooks
- fix(core): _wrapSelection defaults to 'text' instead of i18n placeholder
- fix(core): addToolbarButton onClick+action no longer double-fires
- fix(core): null-safety on focus/blur/enable/disable after destroy
- fix(parser): empty headings (# ) no longer produce empty <h1> tags
- fix(parser): slugify returns 'heading' fallback for empty/special inputs
- fix(i18n): setCurrentLocale no longer mutates input parameter
- fix(core): autofocus skipped when readOnly is enabled

Performance:
- perf(core): cache last rendered value, skip parsing when content unchanged
- perf(core): add refresh() API to force re-render after theme/locale changes

Hardening:
- feat(core): tabSize dynamically applied to textarea via style.tabSize
- feat(core): getStatus() now includes readOnly field

Tests:
- 7 new test cases: customActions, refresh, focus args, destroy safety,
  getStatus readOnly, autofocus+readOnly, slugify edge cases
- Total: 425 tests passing (+7 from v0.1.2)

Types:
- EditorStatus includes readOnly field
- MarkdownEditor.refresh() added to type definitions
This commit is contained in:
2026-07-23 21:00:09 +08:00
parent 12875921eb
commit 8d2e172289
18 changed files with 146 additions and 40 deletions
+73
View File
@@ -1278,4 +1278,77 @@ describe('MarkdownEditor - v0.1.2 readOnly 模式', () => {
expect(ed.getValue()).toBe('# hello');
ed.destroy();
});
test('getStatus 包含 readOnly 字段', () => {
document.body.innerHTML = '';
const c = document.createElement('div');
document.body.appendChild(c);
const ed = new MarkdownEditor(c, { readOnly: true });
expect(ed.getStatus().readOnly).toBe(true);
ed.destroy();
});
test('autofocus 在 readOnly 模式下不聚焦', () => {
document.body.innerHTML = '';
const c = document.createElement('div');
document.body.appendChild(c);
const ed = new MarkdownEditor(c, { readOnly: true, autofocus: true });
// readOnly textarea 不应被 focusautofocus 被跳过)
expect(ed.textarea.readOnly).toBe(true);
ed.destroy();
});
});
describe('MarkdownEditor - v0.1.3 修复验证', () => {
test('exec 支持 _customActions 自定义动作', () => {
document.body.innerHTML = '';
const c = document.createElement('div');
document.body.appendChild(c);
const ed = new MarkdownEditor(c, { value: 'hello' });
let called = false;
ed._customActions = { myAction: () => { called = true; } };
ed.exec('myAction');
expect(called).toBe(true);
ed.destroy();
});
test('refresh 清除渲染缓存并强制重渲染', () => {
document.body.innerHTML = '';
const c = document.createElement('div');
document.body.appendChild(c);
const ed = new MarkdownEditor(c, { value: '# test', mode: 'split' });
const first = ed.previewEl.innerHTML;
// setValue 不改变内容,正常 _render 会跳过
ed.setValue('# test', { silent: true });
// refresh 强制重渲染
ed.refresh();
// 内容一致,预览应一致
expect(ed.previewEl.innerHTML).toBe(first);
ed.destroy();
});
test('focus/blur 监听器接收正确参数', () => {
document.body.innerHTML = '';
const c = document.createElement('div');
document.body.appendChild(c);
const ed = new MarkdownEditor(c, {});
let focusArg;
ed.on('focus', (...args) => { focusArg = args; });
ed.textarea.dispatchEvent(new Event('focus', { bubbles: true }));
// focus 事件只传 editor 一次
expect(focusArg.length).toBe(1);
expect(focusArg[0]).toBe(ed);
ed.destroy();
});
test('destroy 后 focus/blur 不抛错', () => {
document.body.innerHTML = '';
const c = document.createElement('div');
document.body.appendChild(c);
const ed = new MarkdownEditor(c, {});
ed.destroy();
expect(() => ed.focus()).not.toThrow();
expect(() => ed.blur()).not.toThrow();
expect(ed.isDisabled()).toBe(false);
});
});
+5
View File
@@ -350,6 +350,11 @@ describe('slugify', () => {
test('去除首尾连字符', () => {
expect(slugify('-hello-')).toBe('hello');
});
test('slugify 空/特殊字符输入返回兜底值', () => {
expect(slugify('')).toBe('heading');
expect(slugify('###')).toBe('heading');
});
});
describe('parseMarkdown - v0.1.2 新语法', () => {