feat(voyage): vendor markdown-it/highlight.js + playground render-pipeline + scripts/render-artifact.mjs CLI — v4.2 Step 8 [skip-docs]
Vendored libs (locked headless via scripts/vendor-playground-libs.mjs;
plan-critic B3 — never use highlightjs.org website builder):
- playground/lib/markdown-it.min.js — markdown-it@14.1.0 UMD bundle
- playground/lib/markdown-it-front-matter.min.js — markdown-it-front-matter@0.2.4 IIFE-wrapped
- playground/lib/highlight.min.js — highlight.js@11.11.1 (5-lang bundle:
yaml/json/javascript/bash/markdown/diff)
- playground/lib/VENDOR-MANIFEST.json — pin record + audit trail
scripts/vendor-playground-libs.mjs implements the reproducible
CommonJS-to-IIFE wrapping. Re-vendoring requires only:
node scripts/vendor-playground-libs.mjs
Render pipeline in playground/voyage-playground.html (~330 LoC total):
- inline <script src=lib/...> for the three vendored bundles
- markdown-it init with html: true (preserves voyage:anchor comments)
- front-matter plugin with pre-render-then-wrap pattern (research/03)
- paste-import-row textarea + Render/Sample/Clear buttons
- voyage-viewport region with role + aria-live for A11Y
- localStorage key pattern: voyage_ann_<project>__<slug> (risk-assessor H7)
- inline sample plan (mirrors annotation-plan.md fixture)
scripts/render-artifact.mjs CLI (~200 LoC) — brief SC1 + SC11:
- reads input.md, runs same vendored pipeline server-side
- inlines DS CSS + (URL-stripped) highlight.js into output
- zero http://https:// URLs in output (verified by test)
- deterministic: two invocations -> byte-identical sha256
- default output: <input>.html next to input
Test coverage:
- tests/scripts/render-artifact.test.mjs — 5 cases (SC1/SC11)
- tests/playground/voyage-playground.test.mjs — +5 cases (Step 8 extension)
Verify: node --test tests/playground/voyage-playground.test.mjs
tests/scripts/render-artifact.test.mjs -> 18 pass / 0 fail.
Full npm test: 587 pass / 0 fail / 2 skipped (Docker).
Refs plan.md Step 8 + plan-critic B3 + scope-guardian B1.
This commit is contained in:
parent
c412f72605
commit
249142df2f
9 changed files with 996 additions and 6 deletions
98
plugins/voyage/tests/scripts/render-artifact.test.mjs
Normal file
98
plugins/voyage/tests/scripts/render-artifact.test.mjs
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
// tests/scripts/render-artifact.test.mjs
|
||||
// CLI renderer contract — brief SC1 (zero-network) + SC11 (self-eat).
|
||||
//
|
||||
// Verifies:
|
||||
// 1. CLI produces a non-empty .html file from a valid input.md
|
||||
// 2. Output has DOCTYPE + closing </html> + inlined <style> + inlined <script>
|
||||
// 3. Output contains NO http:// or https:// URLs (zero-network constraint)
|
||||
// 4. Output title comes from frontmatter (slug or task)
|
||||
// 5. Two invocations on the same input produce byte-identical output
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { existsSync, readFileSync, statSync, mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join, dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = resolve(HERE, '..', '..');
|
||||
const RENDERER = join(ROOT, 'scripts', 'render-artifact.mjs');
|
||||
const FIX_BRIEF = join(ROOT, 'tests', 'fixtures', 'annotation', 'annotation-brief.md');
|
||||
|
||||
function runRender(input, out) {
|
||||
return execFileSync('node', [RENDERER, input, '--out', out], { encoding: 'utf-8' });
|
||||
}
|
||||
|
||||
function sha256(p) {
|
||||
return createHash('sha256').update(readFileSync(p)).digest('hex');
|
||||
}
|
||||
|
||||
test('render-artifact CLI exits 0 and produces a non-empty .html file', () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'voyage-render-'));
|
||||
try {
|
||||
const out = join(dir, 'brief.html');
|
||||
const stdout = runRender(FIX_BRIEF, out);
|
||||
assert.match(stdout, /render-artifact: wrote/, 'CLI should announce written path');
|
||||
assert.ok(existsSync(out), 'output file must exist');
|
||||
assert.ok(statSync(out).size > 0, 'output file must be non-empty');
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('render-artifact output has DOCTYPE + closing </html> + inlined <style> + inlined <script>', () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'voyage-render-'));
|
||||
try {
|
||||
const out = join(dir, 'brief.html');
|
||||
runRender(FIX_BRIEF, out);
|
||||
const html = readFileSync(out, 'utf-8');
|
||||
assert.match(html, /^<!DOCTYPE html>/i, 'must start with DOCTYPE');
|
||||
assert.match(html, /<\/html>\s*$/, 'must end with </html>');
|
||||
assert.match(html, /<style>[\s\S]+<\/style>/, 'must inline <style>');
|
||||
assert.match(html, /<script>[\s\S]+<\/script>/, 'must inline <script>');
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('render-artifact output contains NO http:// or https:// URLs (zero-network SC1)', () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'voyage-render-'));
|
||||
try {
|
||||
const out = join(dir, 'brief.html');
|
||||
runRender(FIX_BRIEF, out);
|
||||
const html = readFileSync(out, 'utf-8');
|
||||
assert.ok(!/https?:\/\//.test(html), 'output must contain no http:// or https:// URLs');
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('render-artifact output title derives from frontmatter task/slug', () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'voyage-render-'));
|
||||
try {
|
||||
const out = join(dir, 'brief.html');
|
||||
runRender(FIX_BRIEF, out);
|
||||
const html = readFileSync(out, 'utf-8');
|
||||
// annotation-brief.md has task: "Demo task for annotation round-trip fixture"
|
||||
// and slug: annotation-brief-demo. Either should appear in <title>.
|
||||
assert.match(html, /<title>[^<]*(Demo task for annotation round-trip fixture|annotation-brief-demo)[^<]*<\/title>/);
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('render-artifact is deterministic (two invocations -> byte-identical sha256)', () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'voyage-render-'));
|
||||
try {
|
||||
const a = join(dir, 'brief-a.html');
|
||||
const b = join(dir, 'brief-b.html');
|
||||
runRender(FIX_BRIEF, a);
|
||||
runRender(FIX_BRIEF, b);
|
||||
assert.strictEqual(sha256(a), sha256(b), 'same input must produce byte-identical output');
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue