#!/usr/bin/env node /* * Copyright (c) Sandflow Consulting LLC * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ /* * Generates a static website that compares the HTML renders generated by * imscJS for the imsc-tests test suites with the reference PNGs included in * imsc-tests. * * imscJS renders the events of a test live, when the test is selected, in the * browser viewing the site, so that layout-dependent values (e.g. fillLineGap * paddings) are computed using that browser's fonts. The reference PNG is shown next to the render and can be * overlaid on top of it, either blended in difference mode or with adjustable * opacity. * * TTML documents are embedded in the pages, since browsers do not allow * fetch() from file:// pages, and the site can therefore be opened directly * from disk. * * Requires dist/imsc.debug.js (npm run build). * * Usage: * node src/test/script/gen-compare-site.mjs [imsc-tests/imsc1 imsc-tests/imsc1_1 ...] [--out=build/compare] * * Open /index.html in a browser. */ import fs from "node:fs"; import path from "node:path"; import { RESOURCES_DIR, IMSC_BUNDLE_PATH } from "./render-harness.mjs"; const DEFAULT_REFFILES_ROOTS = ["imsc-tests/imsc1", "imsc-tests/imsc1_1", "imsc-tests/imsc1_2", "imsc-tests/imsc1_3"]; /* Must match the dimensions of the reference PNGs */ const RENDER_WIDTH = 640; const RENDER_HEIGHT = 360; function escapeHTML(s) { return s.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } /* JSON that can be safely embedded in a

imscJS / ${escapeHTML(suiteName)}

↑/↓: previous/next test
`; } function indexPage(suites) { const items = suites .map(({ name, testCount }) => `
  • ${escapeHTML(name)} ` + `${testCount} tests
  • `) .join("\n"); return ` imscJS renders

    imscJS renders vs. imsc-tests reference PNGs

    Generated ${escapeHTML(new Date().toISOString())}

    `; } function generateSuite(reffilesRoot, outDir) { const suiteName = path.basename(reffilesRoot); const suiteDir = path.join(outDir, suiteName); const srcDir = path.join(RESOURCES_DIR, reffilesRoot); const testList = JSON.parse(fs.readFileSync(path.join(srcDir, "tests.json"), "utf8")); fs.rmSync(suiteDir, { recursive: true, force: true }); /* images referenced by the TTML documents are resolved relative to them */ fs.cpSync(path.join(srcDir, "ttml"), path.join(suiteDir, "ttml"), { recursive: true }); fs.cpSync(path.join(srcDir, "png"), path.join(suiteDir, "ref"), { recursive: true }); const tests = []; for (const finfo of testList) { const name = finfo.name || path.basename(finfo.path).split(".")[0]; const refDir = path.join(srcDir, "png", name); const refEvents = fs.existsSync(refDir) ? fs.readdirSync(refDir).filter((f) => f.endsWith(".png")).map((f) => f.slice(0, -4)) : []; tests.push({ name, path: finfo.path, params: finfo.params || {}, ttml: fs.readFileSync(path.join(srcDir, "ttml", finfo.path), "utf8"), ttmlDir: "ttml/" + path.posix.dirname(finfo.path) + "/", refDir: "ref/" + name + "/", refEvents, }); } writeFile(path.join(suiteDir, "index.html"), suitePage(suiteName, tests)); console.log(`Wrote ${suiteDir} (${tests.length} tests)`); return { name: suiteName, testCount: tests.length }; } function main() { if (!fs.existsSync(IMSC_BUNDLE_PATH)) { throw new Error(`${IMSC_BUNDLE_PATH} does not exist.`); } const args = process.argv.slice(2).filter((a) => !a.startsWith("--")); const reffilesRoots = args.length ? args : DEFAULT_REFFILES_ROOTS; const outArg = process.argv.find((a) => a.startsWith("--out=")); const outDir = path.resolve(outArg ? outArg.split("=")[1] : "build/compare"); fs.mkdirSync(outDir, { recursive: true }); fs.copyFileSync(IMSC_BUNDLE_PATH, path.join(outDir, "imsc.debug.js")); if (fs.existsSync(IMSC_BUNDLE_PATH + ".map")) { fs.copyFileSync(IMSC_BUNDLE_PATH + ".map", path.join(outDir, "imsc.debug.js.map")); } const suites = reffilesRoots.map((reffilesRoot) => generateSuite(reffilesRoot, outDir)); writeFile(path.join(outDir, "index.html"), indexPage(suites)); console.log(`Open ${path.join(outDir, "index.html")}`); } main();