const { diagramToSVG } = require("../../markdeep-diagram.js");
const { execSync, spawnSync } = require("child_process");
const fs = require("fs");
const path = require("path");
const FIXTURES_DIR = path.join(__dirname);
const PROJECT_ROOT = path.join(__dirname, "../..");
function parseSvgStructure(svg) {
const structure = {
paths: [],
pathCommands: 0, polygons: [],
circles: [],
texts: [],
rects: [],
viewBox: null,
width: null,
height: null,
};
const widthMatch = svg.match(/width="(\d+)"/);
const heightMatch = svg.match(/height="(\d+)"/);
if (widthMatch) structure.width = parseInt(widthMatch[1], 10);
if (heightMatch) structure.height = parseInt(heightMatch[1], 10);
const viewBoxMatch = svg.match(/viewBox="([^"]+)"/);
if (viewBoxMatch) {
structure.viewBox = viewBoxMatch[1];
}
const pathRegex = /<path[^>]*d="([^"]+)"[^>]*>/g;
let match;
while ((match = pathRegex.exec(svg)) !== null) {
const pathData = normalizePath(match[1]);
structure.paths.push(pathData);
structure.pathCommands += (pathData.match(/M /g) || []).length;
}
const polygonRegex = /<polygon[^>]*points="([^"]+)"[^>]*>/g;
while ((match = polygonRegex.exec(svg)) !== null) {
structure.polygons.push(normalizePoints(match[1]));
}
const circleRegex = /<circle[^>]*cx="([^"]+)"[^>]*cy="([^"]+)"[^>]*r="([^"]+)"[^>]*>/g;
while ((match = circleRegex.exec(svg)) !== null) {
structure.circles.push({
cx: roundNum(match[1]),
cy: roundNum(match[2]),
r: roundNum(match[3])
});
}
const circleRegex2 = /<circle[^>]*r="([^"]+)"[^>]*cx="([^"]+)"[^>]*cy="([^"]+)"[^>]*>/g;
while ((match = circleRegex2.exec(svg)) !== null) {
structure.circles.push({
cx: roundNum(match[2]),
cy: roundNum(match[3]),
r: roundNum(match[1])
});
}
const textRegex = /<text[^>]*>([^<]*)<\/text>/g;
while ((match = textRegex.exec(svg)) !== null) {
structure.texts.push(match[1]);
}
const rectRegex = /<rect[^>]*/g;
while ((match = rectRegex.exec(svg)) !== null) {
structure.rects.push(match[0]);
}
return structure;
}
function roundNum(s) {
return Math.round(parseFloat(s) * 10) / 10;
}
function normalizePath(d) {
return d
.replace(/([0-9]+\.[0-9])[0-9]+/g, '$1') .replace(/\s+/g, ' ')
.trim();
}
function normalizePoints(pts) {
return pts
.replace(/([0-9]+\.[0-9])[0-9]+/g, '$1')
.replace(/\s+/g, ' ')
.trim();
}
function runRust(input) {
const result = spawnSync("cargo", ["run", "--example", "conformance_runner", "--release"], {
cwd: PROJECT_ROOT,
input: input,
encoding: 'utf8',
maxBuffer: 10 * 1024 * 1024,
stdio: ['pipe', 'pipe', 'pipe']
});
if (result.error) {
throw result.error;
}
if (result.status !== 0) {
throw new Error(`Rust build/run failed: ${result.stderr}`);
}
return result.stdout;
}
function compareStructures(jsStruct, rustStruct, fixtureName) {
const issues = [];
if (jsStruct.width !== rustStruct.width) {
issues.push(`Width: JS=${jsStruct.width} vs Rust=${rustStruct.width}`);
}
if (jsStruct.height !== rustStruct.height) {
issues.push(`Height: JS=${jsStruct.height} vs Rust=${rustStruct.height}`);
}
if (jsStruct.pathCommands !== rustStruct.pathCommands) {
issues.push(`Path commands: JS=${jsStruct.pathCommands} vs Rust=${rustStruct.pathCommands}`);
}
if (jsStruct.polygons.length !== rustStruct.polygons.length) {
issues.push(`Polygon count: JS=${jsStruct.polygons.length} vs Rust=${rustStruct.polygons.length}`);
}
if (jsStruct.circles.length !== rustStruct.circles.length) {
issues.push(`Circle count: JS=${jsStruct.circles.length} vs Rust=${rustStruct.circles.length}`);
}
if (jsStruct.texts.length !== rustStruct.texts.length) {
issues.push(`Text count: JS=${jsStruct.texts.length} vs Rust=${rustStruct.texts.length}`);
}
const jsTexts = jsStruct.texts.slice().sort();
const rustTexts = rustStruct.texts.slice().sort();
for (let i = 0; i < Math.min(jsTexts.length, rustTexts.length); i++) {
if (jsTexts[i] !== rustTexts[i]) {
issues.push(`Text mismatch: JS="${jsTexts[i]}" vs Rust="${rustTexts[i]}"`);
}
}
return issues;
}
async function main() {
console.log("Building Rust crate...");
try {
execSync("cargo build --release --example conformance_runner", {
cwd: PROJECT_ROOT,
stdio: 'pipe'
});
} catch (e) {
console.error("Failed to build Rust crate:", e.message);
process.exit(1);
}
const fixtures = fs.readdirSync(FIXTURES_DIR)
.filter(f => f.endsWith('.txt'))
.sort();
console.log(`\nRunning conformance tests on ${fixtures.length} fixtures...\n`);
let passed = 0;
let failed = 0;
const failures = [];
for (const fixture of fixtures) {
const fixturePath = path.join(FIXTURES_DIR, fixture);
const input = fs.readFileSync(fixturePath, 'utf8');
const name = fixture.replace('.txt', '');
try {
const jsOptions = { stretch: true, spaces: 2, style: {} };
const jsSvg = diagramToSVG(input, jsOptions);
const jsStruct = parseSvgStructure(jsSvg);
const rustSvg = runRust(input);
const rustStruct = parseSvgStructure(rustSvg);
const issues = compareStructures(jsStruct, rustStruct, name);
if (issues.length === 0) {
console.log(`✓ ${name}`);
console.log(` Paths: ${jsStruct.pathCommands}, Arrows: ${jsStruct.polygons.length}, Points: ${jsStruct.circles.length}, Text: ${jsStruct.texts.length}`);
passed++;
} else {
console.log(`✗ ${name}`);
for (const issue of issues) {
console.log(` - ${issue}`);
}
failures.push({ name, issues });
failed++;
const jsOutPath = path.join(FIXTURES_DIR, `${name}.js.svg`);
const rustOutPath = path.join(FIXTURES_DIR, `${name}.rust.svg`);
fs.writeFileSync(jsOutPath, jsSvg);
fs.writeFileSync(rustOutPath, rustSvg);
console.log(` Saved: ${name}.js.svg, ${name}.rust.svg`);
}
} catch (e) {
console.log(`✗ ${name}: ${e.message}`);
failures.push({ name, error: e.message });
failed++;
}
}
console.log(`\n${'='.repeat(60)}`);
console.log(`Results: ${passed} passed, ${failed} failed`);
if (failures.length > 0) {
console.log('\nSummary of failures:');
for (const f of failures) {
if (f.error) {
console.log(` - ${f.name}: ${f.error}`);
} else {
console.log(` - ${f.name}: ${f.issues.length} issue(s)`);
}
}
process.exit(1);
}
}
main().catch(e => {
console.error(e);
process.exit(1);
});