263 lines
7.9 KiB
JavaScript
263 lines
7.9 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
const fs = require('fs').promises;
|
|
const process = require('process');
|
|
const puppeteer = require('puppeteer');
|
|
const quality = require('./quality.js');
|
|
const uuid = require('uuid').v4;
|
|
|
|
// Size of the rendering of the web page
|
|
const size = { width: 1280, height: 720 };
|
|
|
|
// Logging helper
|
|
function write(stream, chunk, encoding='utf8') {
|
|
return new Promise((resolve, reject) => {
|
|
const errListener = (err) => {
|
|
stream.removeListener('error', errListener);
|
|
reject(err);
|
|
};
|
|
stream.addListener('error', errListener);
|
|
const callback = () => {
|
|
stream.removeListener('error', errListener);
|
|
resolve(undefined);
|
|
};
|
|
stream.write(chunk, encoding, callback);
|
|
});
|
|
}
|
|
|
|
async function print(data) {
|
|
await write(process.stdout, data);
|
|
}
|
|
|
|
async function eprint(data) {
|
|
await write(process.stderr, data);
|
|
}
|
|
|
|
async function println(data) {
|
|
await write(process.stdout, data + '\n');
|
|
}
|
|
|
|
async function eprintln(data) {
|
|
await write(process.stderr, data + '\n');
|
|
}
|
|
|
|
async function main() {
|
|
|
|
let outputDir = null;
|
|
let filename = process.argv[2];
|
|
|
|
if (process.argv[2] === "-o" || process.argv[2] === "--output") {
|
|
outputDir = process.argv[3];
|
|
filename = process.argv[4];
|
|
}
|
|
|
|
try {
|
|
await fs.mkdir(outputDir);
|
|
} catch (e) {
|
|
eprintln("Couldn't create directory " + outputDir + ": " + e);
|
|
process.exit(1);
|
|
}
|
|
|
|
if (filename === undefined) {
|
|
eprintln('This program expects an argument.');
|
|
eprintln('USAGE: locator <path-to-HTML-file>');
|
|
process.exit(1);
|
|
}
|
|
|
|
// Path to the HTML file to analyse (given as relative path from current directory)
|
|
// We need the full path so that puppeteer is able to access it
|
|
const path = filename.startsWith('/') ? filename : process.cwd() + '/' + filename;
|
|
|
|
// Check that the file exists
|
|
try {
|
|
await fs.access(path, fs.constants.F_OK);
|
|
} catch (e) {
|
|
eprintln('No such file: ' + path);
|
|
process.exit(1);
|
|
}
|
|
|
|
// Initialize browser
|
|
const browser = await puppeteer.launch();
|
|
const page = await browser.newPage();
|
|
await page.setViewport(size);
|
|
await page.goto('file://' + path);
|
|
|
|
// Only consider the first slide (#\\331 === #1, which is the id of the first slide)
|
|
// We don't take into account the other slides because it will mess up with our screenshot varification
|
|
let root = await page.$('#\\31');
|
|
|
|
// If there is no slide, try to run on HTML body
|
|
if (root === null) {
|
|
eprintln('Not a marp HTML file.');
|
|
process.exit(1);
|
|
}
|
|
|
|
// Hide slides controls
|
|
await page.evaluate(() => {
|
|
for (let elt of document.getElementsByClassName('bespoke-marp-osc')) {
|
|
elt.style.visibility = "hidden";
|
|
}
|
|
});
|
|
|
|
// Take a first screenshot
|
|
await page.screenshot({path: (outputDir === null ? __dirname : outputDir) + '/' + 'screenshot1.png'});
|
|
|
|
// Edit the page to shrink elements in order to get better bounding boxes
|
|
let withSpan = await addSpan(root);
|
|
|
|
// Take another screenshot and check the modification we made didn't change the layout of the page
|
|
await page.screenshot({path: (outputDir === null ? __dirname : outputDir) + '/' + 'screenshot2.png'});
|
|
|
|
// Compare both screenshots
|
|
let file1 = await fs.readFile(__dirname + '/' + 'screenshot1.png');
|
|
let file2 = await fs.readFile(__dirname + '/' + 'screenshot2.png');
|
|
let filesAreSame = file1.map((x, i) => x === file2[i]).reduce((a, b) => a && b, true);
|
|
|
|
if (!filesAreSame) {
|
|
// Check psnr
|
|
let psnr = await quality.psnr(__dirname + '/' + 'screenshot1.png', __dirname + '/' + 'screenshot2.png');
|
|
|
|
// Crash if they're different
|
|
if (psnr > 70) {
|
|
eprintln("\x1b[33mWarning: " + filename + " produced slight diff: psnr = " + psnr + '\x1b[0m');
|
|
} else {
|
|
await eprintln("\x1b[31mError: age edit changed the layout: psnr = " + psnr + '\x1b[0m');
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
// Analyse the root and output the result
|
|
let analyse = await analyseElement(root, page, outputDir);
|
|
|
|
if (process.argv[3] === '--flatten') {
|
|
analyse = flatten(analyse);
|
|
}
|
|
|
|
let json = JSON.stringify(analyse, undefined, 4);
|
|
if (outputDir === null) {
|
|
console.log();
|
|
} else {
|
|
await fs.writeFile(outputDir + '/annotations.json', json);
|
|
}
|
|
|
|
await browser.close();
|
|
|
|
}
|
|
|
|
// Traverses the text nodes of the element and put every text into a single span.
|
|
async function addSpan(element) {
|
|
let elts = await element.$$('*');
|
|
|
|
for (let elt of elts) {
|
|
let value = await elt.evaluate(el => el.textContent, element);
|
|
if (value !== "") {
|
|
let html = await elt.evaluate(el => {
|
|
if (el.innerHTML.indexOf('<li>') === -1 && el.innerHTML.indexOf('<p>') === -1) {
|
|
let tmp = el.innerHTML;
|
|
el.innerHTML = '<span class="sized-span">' + el.innerHTML + '</span>';
|
|
return [tmp, el.innerHTML];
|
|
} else {
|
|
return null;
|
|
}
|
|
});
|
|
|
|
if (html !== null) {
|
|
await eprintln("\x1b[36mReplaced " + JSON.stringify(html[0]) + " by " + JSON.stringify(html[1]) + '\x1b[0m');
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// Recursive function to analyse an HTML element.
|
|
// The output is written in hierarchy.
|
|
async function analyseElement(element, page, outputDir = null) {
|
|
// Get some information on the element
|
|
let tagAttr = await element.getProperty('tagName');
|
|
let tagName = await tagAttr.jsonValue();
|
|
|
|
let classAttr = await element.getProperty('className');
|
|
let className = await classAttr.jsonValue();
|
|
|
|
let textContent = await element.evaluate(el => el.textContent, element);
|
|
|
|
let box = await element.boundingBox();
|
|
|
|
// Register it into the return value
|
|
let analyse = {};
|
|
analyse.tag = tagName;
|
|
analyse.class = className;
|
|
analyse.uuid = uuid();
|
|
analyse.box = box;
|
|
box.x /= size.width;
|
|
box.width /= size.width;
|
|
box.y /= size.height;
|
|
box.height /= size.height;
|
|
analyse.children = [];
|
|
|
|
if (outputDir !== null) {
|
|
// Perform a screenshot where the element is hidden
|
|
let previousVisibility = await element.evaluate(el => {
|
|
let previousVisibility = el.style.visibility;
|
|
el.style.visibility = "hidden";
|
|
return previousVisibility;
|
|
});
|
|
|
|
await page.screenshot({path: outputDir + '/' + analyse.uuid + '.png'});
|
|
|
|
await element.evaluate((el, previousVisibility) => {
|
|
el.style.visibility = previousVisibility;
|
|
}, [ previousVisibility ]);
|
|
}
|
|
|
|
// Extract the text content if it is a span (we made those spans by ourselves in the addSpan function)
|
|
if (tagName === 'SPAN' && textContent !== "") {
|
|
analyse.text = textContent;
|
|
} else {
|
|
analyse.text = null;
|
|
}
|
|
|
|
// Select the children of this HTML element.
|
|
let children = await element.$$('> *');
|
|
|
|
for (let child of children) {
|
|
// Recursively analyse the children
|
|
analyse.children.push(await analyseElement(child, page, outputDir));
|
|
}
|
|
|
|
return analyse;
|
|
}
|
|
|
|
// Flattens the tree into a list.
|
|
function flatten(input, acc = []) {
|
|
let children = input.children;
|
|
let child = children[0];
|
|
delete input["children"];
|
|
|
|
switch (children.length) {
|
|
case 0:
|
|
acc.push(input);
|
|
break;
|
|
|
|
case 1:
|
|
if (child.tag === "SPAN" && child.class.indexOf("sized-span") !== -1) {
|
|
child.tag = input.tag;
|
|
acc.push(child);
|
|
break;
|
|
}
|
|
// There is purposefully no break here, if the condition above is false, we want to do the default treatment
|
|
|
|
default:
|
|
acc.push(input);
|
|
for (let child of children) {
|
|
flatten(child, acc);
|
|
}
|
|
break;
|
|
}
|
|
|
|
return acc;
|
|
}
|
|
|
|
|
|
main();
|