UNPKG

@meldscience/meld

Version:

pipeable one-shot prompt scripting toolkit

591 lines (493 loc) 19 kB
import { describe, it, expect } from 'vitest'; import { unified } from 'unified'; import remarkParse from 'remark-parse'; import remarkStringify from 'remark-stringify'; import { remarkMeldDirectives } from '../src/plugins/remarkMeldDirectives'; import { remarkProcessMeldNodes } from '../src/plugins/remarkProcessMeldNodes'; import { remarkMeldDirectiveHandler } from '../src/plugins/remarkMeldDirectiveHandler'; import path from 'path'; describe('remarkMeldDirectives', () => { it('should parse @cmd directives', async () => { const content = 'Here is a command: @cmd[echo "hello"]'; const file = await unified() .use(remarkParse) .use(remarkMeldDirectives) .use(remarkMeldDirectiveHandler) .use(remarkStringify) .process(content); expect(String(file).trim()).toBe(content); const ast = file.data.ast as any; const meldNodes = []; const visit = (node: any) => { if (node.type === 'meldDirective') { meldNodes.push(node); } if (node.children) { node.children.forEach(visit); } }; visit(ast); expect(meldNodes.length).toBe(1); const node = meldNodes[0]; expect(node.meldType).toBe('cmd'); expect(node.data.command).toBe('echo "hello"'); }, 500); it('should parse @import directives with heading', async () => { const content = 'Import section: @import[test.md # Section]'; const file = await unified() .use(remarkParse) .use(remarkMeldDirectives) .use(remarkMeldDirectiveHandler) .use(remarkStringify) .process(content); expect(String(file).trim()).toBe(content); const ast = file.data.ast as any; const meldNodes = []; const visit = (node: any) => { if (node.type === 'meldDirective') { meldNodes.push(node); } if (node.children) { node.children.forEach(visit); } }; visit(ast); expect(meldNodes.length).toBe(1); const node = meldNodes[0]; expect(node.meldType).toBe('import'); expect(node.data.importPath).toBe('test.md'); expect(node.data.heading).toBe('Section'); }, 500); it('should parse @import directives with heading level override', async () => { const content = 'Import section: @import[test.md # Section] as ###'; const file = await unified() .use(remarkParse) .use(remarkMeldDirectives) .use(remarkMeldDirectiveHandler) .use(remarkStringify) .process(content); const ast = file.data.ast as any; const meldNodes = []; const visit = (node: any) => { if (node.type === 'meldDirective') { meldNodes.push(node); } if (node.children) { node.children.forEach(visit); } }; visit(ast); expect(meldNodes.length).toBe(1); const node = meldNodes[0]; expect(node.meldType).toBe('import'); expect(node.data.importPath).toBe('test.md'); expect(node.data.heading).toBe('Section'); expect(node.data.headingLevelOverride).toBe('###'); }, 500); it('should parse @import directives with heading text override', async () => { const content = 'Import section: @import[test.md # Section] as ### "New Title"'; const file = await unified() .use(remarkParse) .use(remarkMeldDirectives) .use(remarkMeldDirectiveHandler) .use(remarkStringify) .process(content); const ast = file.data.ast as any; const meldNodes = []; const visit = (node: any) => { if (node.type === 'meldDirective') { meldNodes.push(node); } if (node.children) { node.children.forEach(visit); } }; visit(ast); expect(meldNodes.length).toBe(1); const node = meldNodes[0]; expect(node.meldType).toBe('import'); expect(node.data.importPath).toBe('test.md'); expect(node.data.heading).toBe('Section'); expect(node.data.headingLevelOverride).toBe('###'); expect(node.data.headingTextOverride).toBe('New Title'); }, 500); it('should parse @import directives with code symbols', async () => { const content = 'Import symbols: @import[code.ts # [func1, func2]]'; const file = await unified() .use(remarkParse) .use(remarkMeldDirectives) .use(remarkMeldDirectiveHandler) .use(remarkStringify) .process(content); const ast = file.data.ast as any; const meldNodes = []; const visit = (node: any) => { if (node.type === 'meldDirective') { meldNodes.push(node); } if (node.children) { node.children.forEach(visit); } }; visit(ast); expect(meldNodes.length).toBe(1); const node = meldNodes[0]; expect(node.meldType).toBe('import'); expect(node.data.importPath).toBe('code.ts'); expect(node.data.importSymbolList).toEqual(['func1', 'func2']); }, 500); it('should parse @import directives with type/interface imports', async () => { const content = 'Import type: @import[code.ts # {type}]'; const file = await unified() .use(remarkParse) .use(remarkMeldDirectives) .use(remarkMeldDirectiveHandler) .use(remarkStringify) .process(content); const ast = file.data.ast as any; const meldNodes = []; const visit = (node: any) => { if (node.type === 'meldDirective') { meldNodes.push(node); } if (node.children) { node.children.forEach(visit); } }; visit(ast); expect(meldNodes.length).toBe(1); const node = meldNodes[0]; expect(node.meldType).toBe('import'); expect(node.data.importPath).toBe('code.ts'); expect(node.data.importSymbolList).toEqual(['type']); }, 500); it('should not parse directives inside code blocks', async () => { const content = '```\n@cmd[echo "hi"]\n@import[test.md # Section]\n```'; const file = await unified() .use(remarkParse) .use(remarkMeldDirectives) .use(remarkMeldDirectiveHandler) .use(remarkStringify) .process(content); const ast = file.data.ast as any; const meldNodes = []; const visit = (node: any) => { if (node.type === 'meldDirective') { meldNodes.push(node); } if (node.children) { node.children.forEach(visit); } }; visit(ast); expect(meldNodes.length).toBe(0); expect(String(file)).toContain('@cmd[echo "hi"]'); expect(String(file)).toContain('@import[test.md # Section]'); }, 500); it('should parse single-brace symbol imports', async () => { const content = 'Import interface: @import[test.ts # {interface}]'; const file = await unified() .use(remarkParse) .use(remarkMeldDirectives) .use(remarkMeldDirectiveHandler) .use(remarkStringify) .process(content); const ast = file.data.ast as any; const meldNodes = []; const visit = (node: any) => { if (node.type === 'meldDirective') { meldNodes.push(node); } if (node.children) { node.children.forEach(visit); } }; visit(ast); expect(meldNodes.length).toBe(1); const node = meldNodes[0]; expect(node.meldType).toBe('import'); expect(node.data.importPath).toBe('test.ts'); expect(node.data.importSymbolList).toEqual(['interface']); }, 500); it('should handle empty directives gracefully', async () => { const content = ` Here is an empty import: @import[] Here is an empty command: @cmd[] `.trim(); const file = await unified() .use(remarkParse) .use(remarkMeldDirectives) .use(remarkMeldDirectiveHandler) .use(remarkStringify) .process(content); expect(String(file)).toContain('Error: Invalid import format'); expect(String(file)).toContain('Error: Invalid command format'); }); it('should handle malformed directives gracefully', async () => { const content = ` Here is a malformed import: @import[test.md#] Here is another malformed import: @import[#Section] Here is a malformed command: @cmd[echo "test `.trim(); const file = await unified() .use(remarkParse) .use(remarkMeldDirectives) .use(remarkMeldDirectiveHandler) .use(remarkStringify) .process(content); expect(String(file)).toContain('Error: Invalid import format'); expect(String(file)).toContain('Error: Invalid command format'); }); }); describe('remarkProcessMeldNodes', () => { const testDir = path.join(__dirname, 'fixtures'); it('should process @cmd directives', async () => { const content = 'Output: @cmd[echo "test"]'; const file = await unified() .use(remarkParse) .use(remarkMeldDirectives) .use(remarkProcessMeldNodes, { currentFileDir: testDir }) .use(remarkStringify) .process(content); expect(String(file)).toContain('Output: test'); }, 1000); it('should handle command errors gracefully', async () => { const content = 'Output: @cmd[nonexistentcommand]'; const file = await unified() .use(remarkParse) .use(remarkMeldDirectives) .use(remarkProcessMeldNodes, { currentFileDir: testDir }) .use(remarkStringify) .process(content); expect(String(file)).toContain('Error: Error processing cmd directive'); expect((file.data as any).meldErrors.length).toBe(1); }, 1000); it('should import markdown sections', async () => { const content = 'Here is section one: @import[test.md # Section One]'; const file = await unified() .use(remarkParse) .use(remarkMeldDirectives) .use(remarkProcessMeldNodes, { currentFileDir: testDir }) .use(remarkStringify) .process(content); expect(String(file)).toContain('This is section one content'); expect((file.data as any).meldErrors.length).toBe(0); }, 1000); it('should import markdown sections with heading level override', async () => { const content = 'Here is section: @import[test.md # Section Two] as ###'; const file = await unified() .use(remarkParse) .use(remarkMeldDirectives) .use(remarkProcessMeldNodes, { currentFileDir: testDir }) .use(remarkStringify) .process(content); expect(String(file)).toContain('### Section Two'); expect(String(file)).toContain('This is section two content'); expect((file.data as any).meldErrors.length).toBe(0); }, 1000); it('should import markdown sections with heading text override', async () => { const content = 'Here is section: @import[test.md # Section Three] as ### "Custom Title"'; const file = await unified() .use(remarkParse) .use(remarkMeldDirectives) .use(remarkProcessMeldNodes, { currentFileDir: testDir }) .use(remarkStringify) .process(content); expect(String(file)).toContain('### Custom Title'); expect(String(file)).toContain('This is section three content'); expect((file.data as any).meldErrors.length).toBe(0); }, 1000); it('should handle missing markdown sections', async () => { const content = 'Here is section: @import[test.md # Nonexistent Section]'; const file = await unified() .use(remarkParse) .use(remarkMeldDirectives) .use(remarkProcessMeldNodes, { currentFileDir: testDir }) .use(remarkStringify) .process(content); expect(String(file)).toContain('Error: Error processing import directive'); expect((file.data as any).meldErrors.length).toBe(1); }, 1000); it('should import code symbols', async () => { const content = 'Here are functions: @import[test.ts # [testFunction1, testFunction2]]'; const file = await unified() .use(remarkParse) .use(remarkMeldDirectives) .use(remarkProcessMeldNodes, { currentFileDir: testDir }) .use(remarkStringify) .process(content); expect(String(file)).toContain('function testFunction1()'); expect(String(file)).toContain('function testFunction2(param: string)'); expect((file.data as any).meldErrors.length).toBe(0); }, 1000); it('should import interface definitions', async () => { const content = 'Here is interface: @import[test.ts # {interface}]'; const file = await unified() .use(remarkParse) .use(remarkMeldDirectives) .use(remarkProcessMeldNodes, { currentFileDir: testDir }) .use(remarkStringify) .process(content); expect(String(file)).toContain('interface TestInterface'); expect(String(file)).toContain('prop1: string'); expect((file.data as any).meldErrors.length).toBe(0); }, 1000); it('should import type definitions', async () => { const content = 'Here is type: @import[test.ts # {type}]'; const file = await unified() .use(remarkParse) .use(remarkMeldDirectives) .use(remarkProcessMeldNodes, { currentFileDir: testDir }) .use(remarkStringify) .process(content); expect(String(file)).toContain('type TestType'); expect(String(file)).toContain('field1: string'); expect((file.data as any).meldErrors.length).toBe(0); }, 1000); it('should handle missing code symbols', async () => { const content = 'Here is function: @import[test.ts # nonexistentFunction]'; const file = await unified() .use(remarkParse) .use(remarkMeldDirectives) .use(remarkProcessMeldNodes, { currentFileDir: testDir }) .use(remarkStringify) .process(content); expect(String(file)).toContain('Error: Error processing import directive'); expect((file.data as any).meldErrors.length).toBe(1); }, 1000); it('should handle multiple directives in one file', async () => { const content = ` Here is a command: @cmd[echo "test"] Here is a section: @import[test.md # Section One] Here is a function: @import[test.ts # testFunction1] `.trim(); const file = await unified() .use(remarkParse) .use(remarkMeldDirectives) .use(remarkProcessMeldNodes, { currentFileDir: testDir }) .use(remarkStringify) .process(content); expect(String(file)).toContain('test'); expect(String(file)).toContain('This is section one content'); expect(String(file)).toContain('function testFunction1()'); expect((file.data as any).meldErrors.length).toBe(0); }, 1000); it('should not process directives inside code blocks', async () => { const content = ` Here is some text. \`\`\` @cmd[echo "test"] @import[test.md # Section] \`\`\` More text. `.trim(); const file = await unified() .use(remarkParse) .use(remarkMeldDirectives) .use(remarkMeldDirectiveHandler) .use(remarkStringify) .process(content); const ast = file.data.ast as any; const meldNodes = []; const visit = (node: any) => { if (node.type === 'meldDirective') { meldNodes.push(node); } if (node.children) { node.children.forEach(visit); } }; visit(ast); expect(meldNodes.length).toBe(0); expect(String(file)).toContain('@cmd[echo "test"]'); expect(String(file)).toContain('@import[test.md # Section]'); }); it('should handle precise heading level overrides', async () => { const content = ` Here is a level 2 heading: @import[test.md # Section One] as ## Here is a level 4 heading: @import[test.md # Section Two] as #### `.trim(); const file = await unified() .use(remarkParse) .use(remarkMeldDirectives) .use(remarkMeldDirectiveHandler) .use(remarkProcessMeldNodes, { currentFileDir: testDir }) .use(remarkStringify) .process(content); expect(String(file)).toContain('## Section One'); expect(String(file)).toContain('#### Section Two'); expect((file.data as any).meldErrors.length).toBe(0); }); it('should handle overlapping imports correctly', async () => { const content = ` Here is a symbol imported twice: @import[test.ts # [testFunction1]] @import[test.ts # testFunction1] `.trim(); const file = await unified() .use(remarkParse) .use(remarkMeldDirectives) .use(remarkProcessMeldNodes, { currentFileDir: testDir }) .use(remarkStringify) .process(content); // Both imports should result in the same content const matches = String(file).match(/function testFunction1\(\)/g); expect(matches).toHaveLength(2); expect((file.data as any).meldErrors.length).toBe(0); }, 1000); it('should handle very deep heading overrides correctly', async () => { const content = 'Deep heading: @import[test.md # Section One] as ######'; const file = await unified() .use(remarkParse) .use(remarkMeldDirectives) .use(remarkProcessMeldNodes, { currentFileDir: testDir }) .use(remarkStringify) .process(content); expect(String(file)).toContain('###### Section One'); expect(String(file)).toContain('This is section one content'); expect((file.data as any).meldErrors.length).toBe(0); }, 1000); it('should handle heading text override with special characters correctly', async () => { const content = 'Special heading: @import[test.md # Section One] as ### "Title with: * & # special chars!"'; const file = await unified() .use(remarkParse) .use(remarkMeldDirectives) .use(remarkProcessMeldNodes, { currentFileDir: testDir }) .use(remarkStringify) .process(content); // Expect escaped special characters in the output expect(String(file)).toContain('### Title with: \\* & # special chars!'); expect(String(file)).toContain('This is section one content'); expect((file.data as any).meldErrors.length).toBe(0); }, 1000); it('should handle malformed directives gracefully in processing', async () => { const content = ` Here is a malformed import: @import[#] Here is another malformed import: @import[] Here is a malformed command: @cmd[] `.trim(); const file = await unified() .use(remarkParse) .use(remarkMeldDirectives) .use(remarkProcessMeldNodes, { currentFileDir: testDir }) .use(remarkStringify) .process(content); // The errors are returned in the content rather than meldErrors expect(String(file)).toContain('Error: Invalid import format'); expect(String(file)).toContain('Error: Invalid command format'); }, 1000); it('should handle malformed symbol imports gracefully in processing', async () => { const content = ` Here is a malformed symbol list: @import[test.ts # [func1, ] Here is another malformed symbol: @import[test.ts # {] `.trim(); const file = await unified() .use(remarkParse) .use(remarkMeldDirectives) .use(remarkProcessMeldNodes, { currentFileDir: testDir }) .use(remarkStringify) .process(content); // The errors are returned in the content rather than meldErrors expect(String(file)).toContain('Error: Invalid import format'); }, 1000); });