feat: v0.2.1 — reference links, context menu, RTL, ja/ko, regex search, hooks, copy API
## 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:
@@ -1118,3 +1118,110 @@ describe('parseMarkdown - 分支:嵌套列表子项渲染', () => {
|
||||
expect(innerUlCount).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
// ============ v0.2.1 引用链接/图片测试 ============
|
||||
|
||||
describe('parseMarkdown - v0.2.1 引用链接', () => {
|
||||
test('引用链接 [text][ref] 解析为链接', () => {
|
||||
const md = 'Click [here][1]\n\n[1]: https://example.com';
|
||||
const html = parseMarkdown(md);
|
||||
expect(html).toContain('<a href="https://example.com"');
|
||||
expect(html).toContain('>here</a>');
|
||||
});
|
||||
|
||||
test('引用链接忽略大小写', () => {
|
||||
const md = 'Visit [Site][MyRef]\n\n[myref]: https://example.com';
|
||||
const html = parseMarkdown(md);
|
||||
expect(html).toContain('<a href="https://example.com"');
|
||||
});
|
||||
|
||||
test('引用链接无定义时保留原文', () => {
|
||||
const md = 'Click [here][undef]';
|
||||
const html = parseMarkdown(md);
|
||||
expect(html).not.toContain('<a href=');
|
||||
expect(html).toContain('here');
|
||||
});
|
||||
|
||||
test('引用链接使用隐式引用(同文本)', () => {
|
||||
const md = 'Visit [example][]\n\n[example]: https://example.com';
|
||||
const html = parseMarkdown(md);
|
||||
expect(html).toContain('<a href="https://example.com"');
|
||||
});
|
||||
|
||||
test('引用图片 ![alt][ref] 解析', () => {
|
||||
const md = '![logo][img1]\n\n[img1]: https://example.com/logo.png';
|
||||
const html = parseMarkdown(md);
|
||||
expect(html).toContain('<img src="https://example.com/logo.png"');
|
||||
expect(html).toContain('alt="logo"');
|
||||
});
|
||||
|
||||
test('引用图片带 title 通过 parseMarkdown', () => {
|
||||
clearRenderCache();
|
||||
const md = '![logo][img1]\n\n[img1]: https://example.com/logo.png "MyLogo"';
|
||||
const html = parseMarkdown(md);
|
||||
expect(html).toContain('src="https://example.com/logo.png"');
|
||||
expect(html).toContain('title="MyLogo"');
|
||||
});
|
||||
|
||||
test('引用链接带 title 通过 parseMarkdown', () => {
|
||||
clearRenderCache();
|
||||
const md = '[link][1]\n\n[1]: https://example.com "MyTitle"';
|
||||
const html = parseMarkdown(md);
|
||||
expect(html).toContain('href="https://example.com"');
|
||||
expect(html).toContain('title="MyTitle"');
|
||||
});
|
||||
|
||||
test('引用链接危险协议被过滤', () => {
|
||||
const md = 'Click [bad][1]\n\n[1]: javascript:alert(1)';
|
||||
const html = parseMarkdown(md);
|
||||
expect(html).not.toContain('href="javascript:');
|
||||
});
|
||||
|
||||
test('引用图片无定义时使用空 src', () => {
|
||||
const md = '![logo][nope]';
|
||||
const html = parseMarkdown(md);
|
||||
expect(html).toContain('src=""');
|
||||
expect(html).toContain('me-img-ref');
|
||||
});
|
||||
|
||||
test('引用定义不匹配脚注', () => {
|
||||
const md = 'Text[^1]\n\n[^1]: footnote text\n\n[link][lnk]\n\n[lnk]: https://example.com';
|
||||
const html = parseMarkdown(md);
|
||||
expect(html).toContain('me-footnotes');
|
||||
expect(html).toContain('footnote text');
|
||||
expect(html).toContain('<a href="https://example.com"');
|
||||
});
|
||||
});
|
||||
|
||||
// ============ v0.2.1 data:image 超限测试 ============
|
||||
|
||||
describe('parseMarkdown - v0.2.1 safeUrl 增强', () => {
|
||||
test('超长 data:image URL 被过滤', () => {
|
||||
// 构造一个超过 500000 字符的 data:image URL
|
||||
const longData = 'data:image/png;base64,' + 'A'.repeat(500001);
|
||||
const url = safeUrl(longData);
|
||||
expect(url).toBe('');
|
||||
});
|
||||
|
||||
test('正常 data:image 通过', () => {
|
||||
const url = safeUrl('data:image/png;base64,abc123');
|
||||
expect(url).toBe('data:image/png;base64,abc123');
|
||||
});
|
||||
});
|
||||
|
||||
// ============ v0.2.1 引用链接格式错误容错 ============
|
||||
|
||||
describe('parseMarkdown - v0.2.1 引用链接容错', () => {
|
||||
test('格式错误的引用图片不崩溃', () => {
|
||||
const html = parseMarkdown('![broken ref');
|
||||
expect(html).toContain('broken ref');
|
||||
});
|
||||
|
||||
test('引用定义中 JSON 解析错误被容错', () => {
|
||||
// 正常情况下 refs 中的值是 JSON 字符串
|
||||
const { refs } = parseTokens('[link][test]\n\n[test]: https://example.com');
|
||||
expect(refs['test']).toBeDefined();
|
||||
// 不应抛错
|
||||
expect(() => renderTokens([{ type: 'paragraph', text: '[x][bad]' }], { refs: { bad: 'not-json' } })).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user