Feat v0.6.5 renderer pipeline (#40)
All checks were successful
CI/CD / detect-changes (push) Successful in 4s
CI/CD / test-frontend (push) Successful in 6s
CI/CD / test-go-pg (push) Successful in 2m48s
CI/CD / test-sqlite (push) Successful in 2m52s
CI/CD / build-and-deploy (push) Successful in 1m20s

Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit was merged in pull request #40.
This commit is contained in:
2026-03-31 16:37:33 +00:00
committed by xcaliber
parent 36d6158940
commit 81c28a50bf
33 changed files with 2089 additions and 1974 deletions

View File

@@ -0,0 +1,136 @@
const { describe, it } = require('node:test');
const assert = require('node:assert');
const fs = require('fs');
const path = require('path');
const vm = require('vm');
const SRC = path.join(__dirname, '..');
/**
* Load the renderers module into a VM context and return the factory.
*/
function loadRenderers() {
const source = fs.readFileSync(path.join(SRC, 'sw/sdk/renderers.js'), 'utf-8');
// Strip ESM syntax for VM execution
const cjs = source.replace(/^export /gm, '');
const sandbox = { console, module: { exports: {} } };
vm.createContext(sandbox);
vm.runInContext(cjs, sandbox);
return sandbox.createRenderers;
}
describe('sw.renderers', () => {
const createRenderers = loadRenderers();
it('register() adds a block renderer', () => {
const events = [];
const r = createRenderers((name, data, opts) => events.push({ name, data }));
r.register('test-block', { type: 'block', pattern: 'mermaid', render() {} });
const info = r.list();
assert.strictEqual(info.block.length, 1);
assert.strictEqual(info.block[0].name, 'test-block');
assert.strictEqual(events.length, 1);
assert.strictEqual(events[0].data.action, 'register');
});
it('register() adds a post renderer', () => {
const r = createRenderers(() => {});
r.register('test-post', { type: 'post', render() {} });
const info = r.list();
assert.strictEqual(info.post.length, 1);
assert.strictEqual(info.post[0].name, 'test-post');
});
it('register() replaces duplicate by name', () => {
const r = createRenderers(() => {});
r.register('dup', { type: 'block', pattern: 'x', render() {} });
r.register('dup', { type: 'block', pattern: 'y', render() {} });
const info = r.list();
assert.strictEqual(info.block.length, 1);
assert.strictEqual(info.block[0].pattern, 'y');
});
it('unregister() removes a renderer', () => {
const r = createRenderers(() => {});
r.register('rm-me', { type: 'block', pattern: 'x', render() {} });
r.unregister('rm-me');
assert.strictEqual(r.list().block.length, 0);
});
it('register() returns an unregister function', () => {
const r = createRenderers(() => {});
const unreg = r.register('auto-unreg', { type: 'post', render() {} });
assert.strictEqual(r.list().post.length, 1);
unreg();
assert.strictEqual(r.list().post.length, 0);
});
it('runBlockRenderers() matches by pattern', () => {
const r = createRenderers(() => {});
let called = false;
r.register('m', { type: 'block', pattern: 'mermaid', render() { called = true; } });
const result = r.runBlockRenderers('mermaid', 'code', {});
assert.ok(result);
assert.ok(called);
});
it('runBlockRenderers() matches by match function', () => {
const r = createRenderers(() => {});
let called = false;
r.register('k', {
type: 'block',
match(lang) { return lang === 'latex' || lang === 'math'; },
render() { called = true; },
});
assert.ok(r.runBlockRenderers('latex', 'code', {}));
assert.ok(called);
});
it('runBlockRenderers() returns false when no match', () => {
const r = createRenderers(() => {});
r.register('m', { type: 'block', pattern: 'mermaid', render() {} });
assert.strictEqual(r.runBlockRenderers('javascript', 'code', {}), false);
});
it('runBlockRenderers() case-insensitive on lang', () => {
const r = createRenderers(() => {});
let called = false;
r.register('m', { type: 'block', pattern: 'mermaid', render() { called = true; } });
assert.ok(r.runBlockRenderers('Mermaid', 'code', {}));
assert.ok(called);
});
it('runPostRenderers() calls all post renderers', () => {
const r = createRenderers(() => {});
const order = [];
r.register('a', { type: 'post', priority: 20, render() { order.push('a'); } });
r.register('b', { type: 'post', priority: 10, render() { order.push('b'); } });
r.runPostRenderers({});
assert.deepStrictEqual(order, ['b', 'a']); // priority 10 first
});
it('block renderers respect priority (first match wins)', () => {
const r = createRenderers(() => {});
let winner = '';
r.register('low', { type: 'block', pattern: 'test', priority: 50, render() { winner = 'low'; } });
r.register('high', { type: 'block', pattern: 'test', priority: 10, render() { winner = 'high'; } });
r.runBlockRenderers('test', '', {});
assert.strictEqual(winner, 'high');
});
it('runBlockRenderers() catches render errors gracefully', () => {
const r = createRenderers(() => {});
r.register('bad', { type: 'block', pattern: 'err', render() { throw new Error('boom'); } });
const result = r.runBlockRenderers('err', 'code', {});
assert.strictEqual(result, false); // error → returns false
});
it('runPostRenderers() catches errors and continues', () => {
const r = createRenderers(() => {});
const order = [];
r.register('bad', { type: 'post', priority: 10, render() { throw new Error('boom'); } });
r.register('good', { type: 'post', priority: 20, render() { order.push('good'); } });
r.runPostRenderers({});
assert.deepStrictEqual(order, ['good']); // continues after error
});
});