Use data- attributes to preserve macro configs

This commit is contained in:
gsb
2026-04-29 05:19:17 +00:00
parent 2b88d2c10b
commit 98719ec8cd
3 changed files with 121 additions and 70 deletions
+34 -20
View File
@@ -6,8 +6,6 @@ const macros = [
{
name: 'user',
toHTML: () => '<a href="/user">TestUser</a>',
selector: 'A[href="/user"]',
toMarkdown: () => '@user',
},
{
name: 'npc',
@@ -15,14 +13,10 @@ const macros = [
const name = keywords.join(' ');
return '<a href="/NPC/' + name.replace(/ /g, '') + '">' + name + '</a>';
},
selector: 'A[href^="/NPC/"]',
toMarkdown: (el: any) => '@npc(' + el.textContent + ')',
},
{
name: 'style',
toHTML: ({ keywords, content }: any) => '<div class="' + keywords.join(' ') + '">' + (content || '') + '</div>',
selector: 'DIV[class]',
toMarkdown: (el: any, convert: any) => '\n\n@style(' + el.className + '\n' + convert.children(el) + '\n)\n\n',
},
{
name: 'toc',
@@ -36,10 +30,12 @@ const M = (html: string) => h.toMarkdown(html);
describe('Macros', () => {
describe('self-closing', () => {
it('bare name', () => expect(H('hello @user world')).toBe('<p>hello <a href="/user">TestUser</a> world</p>'));
it('empty parens', () => expect(H('hello @user() world')).toBe('<p>hello <a href="/user">TestUser</a> world</p>'));
it('keywords', () => expect(H('@npc(Goblin King)')).toBe('<p><a href="/NPC/GoblinKing">Goblin King</a></p>'));
it('params', () => expect(H('@toc(depth="2")')).toContain('data-depth="2"'));
it('bare name renders', () => expect(H('hello @user world')).toContain('<a href="/user">TestUser</a>'));
it('bare name wrapped', () => expect(H('hello @user world')).toContain('data-macro="user"'));
it('empty parens', () => expect(H('hello @user() world')).toContain('data-macro="user"'));
it('keywords', () => expect(H('@npc(Goblin King)')).toContain('Goblin King'));
it('keywords in data attr', () => expect(H('@npc(Goblin King)')).toContain('data-keywords="Goblin King"'));
it('params', () => expect(H('@toc(depth="2")')).toContain('data-param-depth="2"'));
});
describe('unknown macros', () => {
@@ -52,8 +48,8 @@ describe('Macros', () => {
describe('block macros', () => {
it('content processed', () => expect(H('@style(box\n**bold**\n)')).toContain('<strong>bold</strong>'));
it('wraps in div', () => expect(H('@style(box\ncontent\n)')).toContain('<div class="box">'));
it('multiple keywords', () => expect(H('@style(box center\ncontent\n)')).toContain('class="box center"'));
it('wrapped with data-macro', () => expect(H('@style(box\ncontent\n)')).toContain('data-macro="style"'));
it('keywords in data attr', () => expect(H('@style(box center\ncontent\n)')).toContain('data-keywords="box center"'));
});
describe('verbatim', () => {
@@ -61,23 +57,41 @@ describe('Macros', () => {
it('no strong tag', () => expect(H('@style(box verbatim\n**bold**\n)')).not.toContain('<strong>'));
it('escapes html', () => expect(H('@style(box verbatim\n<b>tag</b>\n)')).toContain('&lt;b&gt;'));
it('preserves newlines', () => expect(H('@style(box verbatim\nline1\nline2\n)')).toContain('line1<br>'));
it('strips keyword', () => expect(H('@style(box verbatim\ncontent\n)')).not.toContain('verbatim'));
it('data-verbatim set', () => expect(H('@style(box verbatim\ncontent\n)')).toContain('data-verbatim="true"'));
it('keyword stripped from data-keywords', () => {
const html = H('@style(box verbatim\ncontent\n)');
expect(html).toContain('data-keywords="box"');
expect(html).not.toMatch(/data-keywords="[^"]*verbatim/);
});
});
describe('nesting', () => {
it('inline inside bold', () => expect(H('**@npc(Goblin King)**')).toContain('<strong><a href="/NPC/GoblinKing">'));
it('inline inside bold', () => expect(H('**@npc(Goblin King)**')).toContain('<strong>'));
it('block contains list', () => expect(H('@style(box\n- item 1\n- item 2\n)')).toContain('<ul>'));
it('inline inside block', () => expect(H('@style(box\nhello @user world\n)')).toContain('<a href="/user">TestUser</a>'));
it('inline inside block', () => expect(H('@style(box\nhello @user world\n)')).toContain('data-macro="user"'));
});
describe('fenced code protection', () => {
it('not in code block', () => expect(H('```\n@user\n```')).not.toContain('<a href="/user">'));
it('not in code block', () => expect(H('```\n@user\n```')).not.toContain('data-macro'));
it('literal in code block', () => expect(H('```\n@user\n```')).toContain('@user'));
it('not in inline code', () => expect(H('`@user`')).not.toContain('<a href="/user">'));
it('not in inline code', () => expect(H('`@user`')).not.toContain('data-macro'));
});
describe('round-trips', () => {
it('npc', () => expect(M(H('@npc(Goblin King)'))).toBe('@npc(Goblin King)'));
it('user', () => expect(M(H('hello @user world'))).toBe('hello @user world'));
describe('generic round-trip via data- attributes', () => {
it('inline macro', () => expect(M(H('hello @user world'))).toBe('hello @user world'));
it('inline with keywords', () => expect(M(H('@npc(Goblin King)'))).toBe('@npc(Goblin King)'));
it('inline with params', () => expect(M(H('@toc(depth="2")'))).toBe('@toc(depth="2")'));
it('block macro', () => {
const md = '@style(box\n**bold** content\n)';
const result = M(H(md)).trim();
expect(result).toContain('@style(box');
expect(result).toContain('**bold** content');
expect(result).toContain(')');
});
it('verbatim round-trip preserves keyword', () => {
const md = '@style(box verbatim\n<b>literal</b>\n)';
const result = M(H(md)).trim();
expect(result).toContain('@style(box verbatim');
});
});
});