@meldscience/meld
Version:
pipeable one-shot prompt scripting toolkit
591 lines (493 loc) • 19 kB
text/typescript
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);
});