feat: v0.2.1 — reference links, context menu, RTL, ja/ko, regex search, hooks, copy API
CI / test (16.x) (push) Canceled after 0s
CI / test (18.x) (push) Canceled after 0s
CI / test (20.x) (push) Canceled after 0s

## Added
- Reference link/image resolution: [text][ref] + ![alt][ref] with [ref]: url definitions
- Right-click context menu: undo/redo/cut/copy/paste/selectAll + custom items
- RTL CSS layout support for Arabic, Hebrew, Persian etc.
- Japanese (ja) and Korean (ko) locales with 60+ keys each
- Divider position localStorage persistence
- Regex search toggle in search/replace panel
- Export HTML with embedded CSS styles
- beforeChange / afterChange lifecycle hooks (instance + global)
- copyAsMarkdown() / copyAsHTML() clipboard APIs
- CHANGELOG.md, CONTRIBUTING.md, CI workflow (.github/workflows/ci.yml)
- 2 new test suites: index.test.ts, styles.test.ts (684 total tests, +74)

## Changed
- autoSave plugin: closure-based state per instance instead of this context
- Plugin install() now receives options as second argument
- RTL locale detection: now uses language prefix (ar-SA → RTL)
- Rollup dev mode: only builds UMD format
- prepublishOnly now includes typecheck + test
- Version bumped to 0.2.1

## Fixed
- [text][ref] now correctly renders as link (was raw text)
- ![alt][ref] no longer produces empty src
- autoSave plugin state isolation across multiple editor instances
- Footnote definitions no longer consumed by refDef handler
This commit is contained in:
2026-07-25 08:53:58 +08:00
parent 16464af0ae
commit 1cd5b63174
22 changed files with 1448 additions and 116 deletions
+181
View File
@@ -0,0 +1,181 @@
/**
* index.ts 单元测试
* 覆盖全局 APIcreate / use / on / off / setTheme / setLocale / destroy / getStatus
*/
import MeEditor, {
create, use, on, off, setTheme, setLocale, destroy, getStatus,
parseMarkdown, parseTokens, renderTokens, safeUrl, slugify, clearRenderCache,
registerBlockHandler, topologicalSort, validateConfig,
VERSION,
} from '../src/index';
import { MarkdownEditor } from '../src/core';
describe('index.ts - 全局 API', () => {
let container: HTMLElement;
beforeEach(() => {
document.body.innerHTML = '';
container = document.createElement('div');
container.id = 'test-host';
document.body.appendChild(container);
// Set MeEditor on window as index.ts does
(window as any).MeEditor = MeEditor;
});
afterEach(() => {
document.body.innerHTML = '';
});
test('VERSION 是字符串', () => {
expect(typeof VERSION).toBe('string');
expect(VERSION).toBe('0.2.1');
});
test('api.default 是 api 本身', () => {
expect(MeEditor).toBeDefined();
expect(MeEditor.VERSION).toBe(VERSION);
});
test('create 创建编辑器实例', () => {
const editor = create('#test-host', { value: '# Hello' });
expect(editor).toBeInstanceOf(MarkdownEditor);
expect(editor.getValue()).toBe('# Hello');
editor.destroy();
});
test('create 接受 HTMLElement', () => {
const editor = create(container, { value: 'text' });
expect(editor.getValue()).toBe('text');
editor.destroy();
});
test('use 注册全局插件(通过字符串)', () => {
const result = use('autoSave', { delay: 100 });
expect(result).toBe(MeEditor); // 返回 api(链式)
const status = getStatus();
expect(status.globalPlugins).toContain('autoSave');
});
test('use 未知预设插件 warn', () => {
const spy = jest.spyOn(console, 'warn').mockImplementation(() => {});
use('non-existent-preset');
expect(spy).toHaveBeenCalled();
spy.mockRestore();
});
test('use 无效插件对象 warn', () => {
const spy = jest.spyOn(console, 'warn').mockImplementation(() => {});
use(null);
expect(spy).toHaveBeenCalled();
spy.mockRestore();
});
test('全局插件被注入到新实例', () => {
// Reset global plugins by destroying
destroy();
use('autoSave');
const ed = create(container, { value: 'test' });
const plugins = ed.getPlugins();
expect(plugins.some((p: any) => p.name === 'autoSave')).toBe(true);
ed.destroy();
});
test('on / off 全局钩子', () => {
const fn = jest.fn();
const unsub = on('afterCreate', fn);
expect(typeof unsub).toBe('function');
const ed = create(container, {});
expect(fn).toHaveBeenCalled();
off('afterCreate', fn);
ed.destroy();
});
test('setTheme 切换全局主题', () => {
expect(() => setTheme('dark')).not.toThrow();
});
test('setLocale 切换全局语言', () => {
expect(() => setLocale('en-US')).not.toThrow();
});
test('getStatus 返回状态对象', () => {
const status = getStatus();
expect(status).toHaveProperty('version');
expect(status).toHaveProperty('theme');
expect(status).toHaveProperty('locale');
expect(status).toHaveProperty('globalPlugins');
expect(status).toHaveProperty('presetPlugins');
expect(Array.isArray(status.globalPlugins)).toBe(true);
expect(Array.isArray(status.presetPlugins)).toBe(true);
});
test('destroy 清理全局资源', () => {
expect(() => destroy()).not.toThrow();
const status = getStatus();
expect(status.globalPlugins.length).toBe(0);
});
test('window.MeEditor 被设置', () => {
expect((window as any).MeEditor).toBeDefined();
expect((window as any).MeEditor.VERSION).toBe(VERSION);
});
});
describe('index.ts - 解析器导出', () => {
test('parseMarkdown 可直接调用', () => {
const html = parseMarkdown('# Hello');
expect(html).toContain('<h1');
});
test('parseTokens 可直接调用', () => {
const { tokens, footnotes, refs } = parseTokens('# Hi\n\n**bold**');
expect(Array.isArray(tokens)).toBe(true);
expect(tokens.length).toBeGreaterThan(0);
expect(typeof footnotes).toBe('object');
expect(typeof refs).toBe('object');
});
test('renderTokens 可直接调用', () => {
const { tokens } = parseTokens('**hello**');
const html = renderTokens(tokens);
expect(html).toContain('<strong>');
});
test('safeUrl 过滤危险协议', () => {
expect(safeUrl('javascript:alert(1)')).toBe('');
expect(safeUrl('https://example.com')).toBe('https://example.com');
});
test('slugify 生成锚点 id', () => {
expect(slugify('Hello World')).toBe('hello-world');
});
test('clearRenderCache 不抛错', () => {
expect(() => clearRenderCache()).not.toThrow();
});
test('registerBlockHandler 注册自定义块', () => {
registerBlockHandler({
name: 'testBlock',
priority: 50,
test: () => null,
parse: (_l, i) => ({ token: null, newIndex: i }),
});
// 不抛错即通过
expect(true).toBe(true);
});
});
describe('index.ts - 具名导出', () => {
test('create 是函数', () => expect(typeof create).toBe('function'));
test('use 是函数', () => expect(typeof use).toBe('function'));
test('on 是函数', () => expect(typeof on).toBe('function'));
test('off 是函数', () => expect(typeof off).toBe('function'));
test('setTheme 是函数', () => expect(typeof setTheme).toBe('function'));
test('setLocale 是函数', () => expect(typeof setLocale).toBe('function'));
test('destroy 是函数', () => expect(typeof destroy).toBe('function'));
test('getStatus 是函数', () => expect(typeof getStatus).toBe('function'));
test('topologicalSort 是函数', () => expect(typeof topologicalSort).toBe('function'));
test('validateConfig 是函数', () => expect(typeof validateConfig).toBe('function'));
});