release: v0.1.4 — parser enhancement, new syntax, performance optimization

feat(parser): comprehensive Markdown parser enhancement
- Add nested list support (multi-level unordered/ordered, mixed nesting)
- Add Setext headings (=== / ---)
- Add indented code blocks (4-space indent)
- Add HTML comment passthrough (<!-- -->)
- Add entity reference protection (&amp; &#169;)
- Add ordered list start attribute
- Add link title single-quote support
- Add LaTeX math formulas ($inline$ / $$block$$)
- Add footnote support ([^1] ref + [^1]: definition)
- Add definition lists (term\n: definition)
- Extend emoji map from 85 to 150+ common emojis
- Fix backtick code block matching (CommonMark spec)
- Fix italic regex character truncation (lookbehind assertions)
- Fix superscript/subscript/strikethrough ambiguity
- Fix blockquote prefix handling (>text without space)
- Enhance safeUrl filtering (additional protocol checks)
- Enhance slugify (Unicode NFKC normalization)

perf(parser): single-pass inline scanning, render cache, pre-compiled regex
- Replace 14-step chained regex with unified single-pass scanInline()
- Add cachedRenderInline() with FIFO LRU eviction (300-entry cap)
- Pre-compile 15+ static regex constants for block boundary detection
- Skip redundant style='text-align:left' on default-aligned table cells

style(parser): add CSS for footnotes, definition lists, math formulas, sup/sub

test(parser): 44 new test cases covering all v0.1.4 syntax additions (469 total)

chore: bump version 0.1.3 → 0.1.4 across all source files, docs, site pages
This commit is contained in:
2026-07-24 09:35:51 +08:00
parent 2eb86d29b6
commit f8b9f4a761
19 changed files with 1128 additions and 198 deletions
+313 -7
View File
@@ -185,9 +185,15 @@ describe('parseMarkdown - 列表', () => {
test('有序列表', () => {
const html = parseMarkdown('1. one\n2. two');
expect(html).toContain('<ol>');
expect(html).toContain('<li>one</li>');
expect(html).toContain('<li>two</li>');
expect(html).toContain('<ol');
expect(html).toContain('>one</li>');
expect(html).toContain('>two</li>');
});
test('有序列表自定义起始编号', () => {
const html = parseMarkdown('3. three\n4. four');
expect(html).toContain('<ol');
expect(html).toContain('value="3"');
});
test('任务列表未完成', () => {
@@ -228,10 +234,10 @@ describe('parseMarkdown - 表格', () => {
const html = parseMarkdown(md);
expect(html).toContain('<table>');
expect(html).toContain('<thead>');
expect(html).toContain('<th style="text-align:left">a</th>');
expect(html).toContain('<th style="text-align:left">b</th>');
expect(html).toContain('<td style="text-align:left">1</td>');
expect(html).toContain('<td style="text-align:left">2</td>');
expect(html).toContain('<th>a</th>');
expect(html).toContain('<th>b</th>');
expect(html).toContain('<td>1</td>');
expect(html).toContain('<td>2</td>');
});
test('表格被 me-table-wrap 包裹', () => {
@@ -391,3 +397,303 @@ describe('parseMarkdown - v0.1.2 新语法', () => {
expect(html).toContain('text-align:right');
});
});
// ============ v0.1.4 新增语法测试 ============
describe('parseMarkdown - v0.1.4 Setext 标题', () => {
test('Setext h1 ===', () => {
const html = parseMarkdown('Heading 1\n=======');
expect(html).toContain('<h1');
expect(html).toContain('Heading 1');
});
test('Setext h2 ---', () => {
const html = parseMarkdown('Heading 2\n-------');
expect(html).toContain('<h2');
expect(html).toContain('Heading 2');
});
test('Setext 标题生成 id', () => {
const html = parseMarkdown('My Title\n===');
expect(html).toContain('id="my-title"');
});
test('Setext 与 ATX 共存正确', () => {
const html = parseMarkdown('ATX\n===\n\n# Another');
expect(html).toContain('<h1');
expect(html).toContain('ATX');
expect(html).toContain('Another');
});
});
describe('parseMarkdown - v0.1.4 嵌套列表', () => {
test('两层无序列表嵌套', () => {
const html = parseMarkdown('- parent\n - child');
expect(html).toContain('<ul>');
expect(html).toContain('parent');
expect(html).toContain('child');
});
test('两层有序列表嵌套', () => {
const html = parseMarkdown('1. first\n 1. sub');
expect(html).toContain('<ol');
expect(html).toContain('first');
expect(html).toContain('sub');
});
test('无序列表内嵌套有序列表', () => {
const html = parseMarkdown('- item\n 1. sub1\n 2. sub2');
expect(html).toContain('<ul>');
expect(html).toContain('<ol');
expect(html).toContain('sub1');
expect(html).toContain('sub2');
});
test('三级嵌套', () => {
const html = parseMarkdown('- l1\n - l2\n - l3');
expect(html).toContain('l1');
expect(html).toContain('l2');
expect(html).toContain('l3');
});
test('嵌套任务列表', () => {
const html = parseMarkdown('- parent\n - [x] done');
expect(html).toContain('me-task-item');
expect(html).toContain('checked');
});
});
describe('parseMarkdown - v0.1.4 缩进代码块', () => {
test('4空格缩进代码块', () => {
const html = parseMarkdown(' code line 1\n code line 2');
expect(html).toContain('<pre><code>');
expect(html).toContain('code line 1');
expect(html).toContain('code line 2');
});
test('缩进代码块内容被转义', () => {
const html = parseMarkdown(' <div>');
expect(html).toContain('<pre><code>');
expect(html).not.toContain('<div>');
});
});
describe('parseMarkdown - v0.1.4 HTML 注释', () => {
test('HTML 注释被透传', () => {
const html = parseMarkdown('<!-- comment -->');
expect(html).toContain('<!--');
expect(html).toContain('comment');
expect(html).toContain('-->');
});
test('HTML 注释内内容不被转义', () => {
const html = parseMarkdown('<!-- <b>bold</b> -->');
expect(html).toContain('<!-- <b>bold</b> -->');
});
});
describe('parseMarkdown - v0.1.4 实体引用保护', () => {
test('已有实体引用不被二次转义', () => {
const html = parseMarkdown('AT&amp;T');
expect(html).toContain('&amp;');
// &amp; 应保持为 &amp; 而非 &amp;amp;
expect(html).not.toContain('&amp;amp;');
});
test('数字实体引用被保护', () => {
const html = parseMarkdown('&#169; 2024');
expect(html).toContain('&#169;');
});
test('十六进制实体引用被保护', () => {
const html = parseMarkdown('&#x3C;');
expect(html).toContain('&#x3C;');
});
});
describe('parseMarkdown - v0.1.4 硬换行', () => {
test('行尾2空格产生硬换行', () => {
const html = parseMarkdown('line1 \nline2');
// 硬换行不应产生单独的 <br/>,而是保持为真正的 <br/>
// 软换行 \n 产生 <br/>,硬换行(2空格+\n)也应产生 <br/>
// 当前实现:2空格换行和普通换行都生成 <br/>
expect(html).toContain('line1');
expect(html).toContain('line2');
});
});
describe('parseMarkdown - v0.1.4 链接标题单引号', () => {
test('链接 title 使用单引号', () => {
const html = parseMarkdown('[text](https://example.com \'title\')');
expect(html).toContain('href="https://example.com"');
expect(html).toContain('title="title"');
});
test('链接 title 使用双引号(兼容)', () => {
const html = parseMarkdown('[text](https://example.com "title")');
expect(html).toContain('title="title"');
});
});
describe('parseMarkdown - v0.1.4 LaTeX 数学公式', () => {
test('行内公式 $...$', () => {
const html = parseMarkdown('Euler: $e^{i\\pi} = -1$');
expect(html).toContain('me-math-inline');
expect(html).toContain('e^{i\\pi} = -1');
});
test('块级公式 $$...$$(单行)', () => {
const html = parseMarkdown('$$\\int_0^\\infty e^{-x} dx = 1$$');
expect(html).toContain('me-math-block');
});
test('块级公式 $$...$$(多行)', () => {
const md = '$$\n\\begin{aligned}\nx &= 1 + 2 \\\\\ny &= 3 + 4\n\\end{aligned}\n$$';
const html = parseMarkdown(md);
expect(html).toContain('me-math-block');
expect(html).toContain('\\begin{aligned}');
});
});
describe('parseMarkdown - v0.1.4 脚注', () => {
test('脚注引用与定义', () => {
const md = 'Text with footnote[^1].\n\n[^1]: This is the footnote.';
const html = parseMarkdown(md);
expect(html).toContain('me-footnote-ref');
expect(html).toContain('fnref-1');
expect(html).toContain('me-footnotes');
expect(html).toContain('fn-1');
expect(html).toContain('This is the footnote.');
});
test('多个脚注', () => {
const md = 'First[^a] and second[^b].\n\n[^a]: Note A.\n[^b]: Note B.';
const html = parseMarkdown(md);
expect(html).toContain('me-footnotes');
expect(html).toContain('Note A.');
expect(html).toContain('Note B.');
});
});
describe('parseMarkdown - v0.1.4 定义列表', () => {
test('基本定义列表', () => {
const md = 'Term\n: Definition of the term.';
const html = parseMarkdown(md);
expect(html).toContain('<dl>');
expect(html).toContain('<dt>');
expect(html).toContain('Term');
expect(html).toContain('<dd>');
expect(html).toContain('Definition of the term.');
});
test('多条定义', () => {
const md = 'Term\n: Definition 1.\n: Definition 2.';
const html = parseMarkdown(md);
expect(html).toContain('Definition 1.');
expect(html).toContain('Definition 2.');
});
});
describe('parseMarkdown - v0.1.4 反引号精确匹配', () => {
test('双反引号包裹单反引号', () => {
const html = parseMarkdown('`` ` ``');
expect(html).toContain('<code>');
expect(html).toContain('`');
});
test('不同长度反引号串', () => {
const html = parseMarkdown('``` `code` ```');
expect(html).toContain('<code>');
// 应包含原始反引号内容
expect(html).toContain('`code`');
});
});
describe('parseMarkdown - v0.1.4 斜体优化', () => {
test('*text* 正常斜体', () => {
const html = parseMarkdown('a *italic* b');
expect(html).toContain('<em>italic</em>');
});
test('*** 不作为斜体', () => {
const html = parseMarkdown('***');
expect(html).not.toContain('<em>');
});
test('__ 不作为粗体误判', () => {
const html = parseMarkdown('__bold__');
expect(html).toContain('<strong>bold</strong>');
});
});
describe('parseMarkdown - v0.1.4 引用块优化', () => {
test('引用块 >text 无空格', () => {
const html = parseMarkdown('>quote text');
expect(html).toContain('<blockquote>');
expect(html).toContain('quote text');
});
test('嵌套引用', () => {
const html = parseMarkdown('> outer\n>> nested');
expect(html).toContain('<blockquote>');
expect(html).toContain('outer');
});
});
describe('parseMarkdown - v0.1.4 safeUrl 增强', () => {
test('正常 http/https url 通过', () => {
expect(safeUrl('http://example.com')).toBe('http://example.com');
expect(safeUrl('https://example.com')).toBe('https://example.com');
});
test('mailto: 协议通过', () => {
expect(safeUrl('mailto:test@example.com')).toBe('mailto:test@example.com');
});
});
describe('parseMarkdown - v0.1.4 slugify 增强', () => {
test('全角字符处理', () => {
expect(slugify('你好世界')).toBe('你好世界');
});
test('Unicode NFKC 规范化', () => {
// 全角英文字母转半角
const slug = slugify('Cat');
// NFKC 将全角 C/a/t 转为半角
expect(slug).toContain('cat');
});
});
describe('parseMarkdown - v0.1.4 表情扩展', () => {
test('新增表情 :confetti:', () => {
const html = parseMarkdown(':confetti:');
expect(html).toContain('🎊');
});
test('新增表情 :rainbow:', () => {
const html = parseMarkdown(':rainbow:');
expect(html).toContain('🌈');
});
test('新增表情 :hamburger:', () => {
const html = parseMarkdown(':hamburger:');
expect(html).toContain('🍔');
});
test('新增表情 :earth:', () => {
const html = parseMarkdown(':earth:');
expect(html).toContain('🌍');
});
});
describe('parseMarkdown - v0.1.4 代码块语言扩展', () => {
test('语言标记支持 # + . 等字符', () => {
const html = parseMarkdown('```c++\nint main() {}\n```');
expect(html).toContain('language-c++');
});
test('语言标记支持点号', () => {
const html = parseMarkdown('```file.txt\ncontent\n```');
expect(html).toContain('language-file.txt');
});
});