feat(esm): migrate this package to a pure ESM

BREAKING CHANGE: Please run node 12.20 or higher
BREAKING CHANGE: This package is now a pure esm, please [read this](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c)
This commit is contained in:
Nico Jansen 2022-12-05 23:06:22 +01:00 committed by Antonin Stefanutti
parent 866daced5e
commit 056002fcdc
20 changed files with 513 additions and 519 deletions

View File

@ -2,22 +2,22 @@
'use strict'; 'use strict';
const chalk = require('chalk'), import chalk from 'chalk';
crypto = require('crypto'), import chalkTemplate from 'chalk-template';
Font = require('fonteditor-core').Font, import crypto from 'crypto';
fs = require('fs'), import { Font } from 'fonteditor-core';
os = require('os'), import fs from 'fs';
parser = require('./libs/nomnom'), import os from 'os';
path = require('path'), import parser from './libs/nomnom.js';
puppeteer = require('puppeteer'), import path from 'path';
URI = require('urijs'), import puppeteer from 'puppeteer';
util = require('util'); import URI from 'urijs';
import util from 'util';
import { fileURLToPath } from 'url';
const { PDFDocument, PDFName, ParseSpeeds, decodePDFRawStream } = require('pdf-lib'); import { PDFDocument, PDFName, ParseSpeeds, decodePDFRawStream } from 'pdf-lib';
const { delay, pause } = require('./libs/util'); import { delay, pause } from './libs/util.js';
const plugins = loadAvailablePlugins(path.join(path.dirname(__filename), 'plugins'));
parser.script('decktape').options({ parser.script('decktape').options({
url : { url : {
@ -184,7 +184,8 @@ parser.command('version')
.root(true) .root(true)
.help('Display decktape package version') .help('Display decktape package version')
.callback(_ => { .callback(_ => {
console.log(require('./package.json').version); const pkg = JSON.parse(fs.readFileSync(new URL('./package.json', import.meta.url)));
console.log(pkg.version);
process.exit(); process.exit();
}); });
parser.nocommand() parser.nocommand()
@ -198,19 +199,10 @@ parser.command('automatic')
`Iterates over the available plugins, picks the compatible one for presentation at the `Iterates over the available plugins, picks the compatible one for presentation at the
specified <url> and uses it to export and write the PDF into the specified <filename>.` specified <url> and uses it to export and write the PDF into the specified <filename>.`
); );
Object.entries(plugins).forEach(([id, plugin]) => {
const command = parser.command(id);
if (typeof plugin.options === 'object') {
command.options(plugin.options);
}
if (typeof plugin.help === 'string') {
command.help(plugin.help);
}
});
// TODO: should be deactivated as well when it does not execute in a TTY context // TODO: should be deactivated as well when it does not execute in a TTY context
if (os.name === 'windows') parser.nocolors(); if (os.name === 'windows') parser.nocolors();
const options = parser.parse(process.argv.slice(2));
const color = type => { const color = type => {
switch (type) { switch (type) {
@ -226,6 +218,18 @@ process.on('unhandledRejection', error => {
}); });
(async () => { (async () => {
const plugins = await loadAvailablePlugins(path.join(path.dirname(fileURLToPath(import.meta.url)), 'plugins'));
Object.entries(plugins).forEach(([id, plugin]) => {
const command = parser.command(id);
if (typeof plugin.options === 'object') {
command.options(plugin.options);
}
if (typeof plugin.help === 'string') {
command.help(plugin.help);
}
});
const options = parser.parse(process.argv.slice(2));
const browser = await puppeteer.launch({ const browser = await puppeteer.launch({
headless : true, headless : true,
@ -255,9 +259,9 @@ process.on('unhandledRejection', error => {
.on('requestfailed', request => { .on('requestfailed', request => {
// do not output warning for cancelled requests // do not output warning for cancelled requests
if (request.failure() && request.failure().errorText === 'net::ERR_ABORTED') return; if (request.failure() && request.failure().errorText === 'net::ERR_ABORTED') return;
console.log(chalk`\n{keyword('orange') Unable to load resource from URL: ${request.url()}}`); console.log(chalkTemplate`\n{keyword('orange') Unable to load resource from URL: ${request.url()}}`);
}) })
.on('pageerror', error => console.log(chalk`\n{red Page error: ${error.message}}`)); .on('pageerror', error => console.log(chalkTemplate`\n{red Page error: ${error.message}}`));
console.log('Loading page', options.url, '...'); console.log('Loading page', options.url, '...');
const load = page.waitForNavigation({ waitUntil: 'load', timeout: options.urlLoadTimeout }); const load = page.waitForNavigation({ waitUntil: 'load', timeout: options.urlLoadTimeout });
@ -275,274 +279,275 @@ process.on('unhandledRejection', error => {
.then(_ => exportSlides(plugin, page, pdf)) .then(_ => exportSlides(plugin, page, pdf))
.then(async context => { .then(async context => {
await writePdf(options.filename, pdf); await writePdf(options.filename, pdf);
console.log(chalk`{green \nPrinted {bold ${context.exportedSlides}} slides}`); console.log(chalkTemplate`{green \nPrinted {bold ${context.exportedSlides}} slides}`);
browser.close(); browser.close();
process.exit(); process.exit();
})) }))
.catch(error => { .catch(error => {
console.log(chalk`{red \n${error}}`); console.log(chalkTemplate`{red \n${error}}`);
browser.close(); browser.close();
process.exit(1); process.exit(1);
}); });
})();
async function writePdf(filename, pdf) { async function writePdf(filename, pdf) {
const pdfDir = path.dirname(filename); const pdfDir = path.dirname(filename);
try { try {
fs.accessSync(pdfDir, fs.constants.F_OK); fs.accessSync(pdfDir, fs.constants.F_OK);
} catch { } catch {
fs.mkdirSync(pdfDir, { recursive: true }); fs.mkdirSync(pdfDir, { recursive: true });
}
fs.writeFileSync(filename, await pdf.save({ addDefaultPage: false }));
} }
fs.writeFileSync(filename, await pdf.save({ addDefaultPage: false }));
}
function loadAvailablePlugins(pluginsPath) { async function loadAvailablePlugins(pluginsPath) {
return fs.readdirSync(pluginsPath).reduce((plugins, pluginPath) => { const plugins = await fs.promises.readdir(pluginsPath);
const [, plugin] = pluginPath.match(/^(.*)\.js$/); const entries = await Promise.all(plugins.map(async pluginPath => {
if (plugin && fs.statSync(path.join(pluginsPath, pluginPath)).isFile()) { const [, plugin] = pluginPath.match(/^(.*)\.js$/);
plugins[plugin] = require('./plugins/' + plugin); if (plugin && (await fs.promises.stat(path.join(pluginsPath, pluginPath))).isFile()) {
} return [plugin, await import (`./plugins/${pluginPath}`)];
return plugins; }
}, {}); }));
} return Object.fromEntries(entries.filter(Boolean));
}
async function createPlugin(page) { async function createPlugin(page) {
let plugin; let plugin;
if (!options.command || options.command === 'automatic') { if (!options.command || options.command === 'automatic') {
plugin = await createActivePlugin(page); plugin = await createActivePlugin(page);
if (!plugin) { if (!plugin) {
console.log('No supported DeckTape plugin detected, falling back to generic plugin'); console.log('No supported DeckTape plugin detected, falling back to generic plugin');
plugin = plugins['generic'].create(page, options); plugin = plugins['generic'].create(page, options);
}
} else {
plugin = plugins[options.command].create(page, options);
if (!await plugin.isActive()) {
throw Error(`Unable to activate the ${plugin.getName()} DeckTape plugin for the address: ${options.url}`);
}
} }
} else { console.log(chalkTemplate`{cyan {bold ${plugin.getName()}} plugin activated}`);
plugin = plugins[options.command].create(page, options); return plugin;
if (!await plugin.isActive()) { }
throw Error(`Unable to activate the ${plugin.getName()} DeckTape plugin for the address: ${options.url}`);
async function createActivePlugin(page) {
for (let id in plugins) {
if (id === 'generic') continue;
const plugin = plugins[id].create(page, options);
if (await plugin.isActive()) return plugin;
} }
} }
console.log(chalk`{cyan {bold ${plugin.getName()}} plugin activated}`);
return plugin;
}
async function createActivePlugin(page) { async function configurePage(plugin, page) {
for (let id in plugins) { if (!options.size) {
if (id === 'generic') continue; options.size = typeof plugin.size === 'function' ? await plugin.size() : { width: 1280, height: 720 };
const plugin = plugins[id].create(page, options); }
if (await plugin.isActive()) return plugin; await page.setViewport(options.size);
} }
}
async function configurePage(plugin, page) { async function configurePlugin(plugin) {
if (!options.size) { if (typeof plugin.configure === 'function') {
options.size = typeof plugin.size === 'function' ? await plugin.size() : { width: 1280, height: 720 }; await plugin.configure();
}
} }
await page.setViewport(options.size);
}
async function configurePlugin(plugin) { async function exportSlides(plugin, page, pdf) {
if (typeof plugin.configure === 'function') { const context = {
await plugin.configure(); progressBarOverflow : 0,
} currentSlide : 1,
} exportedSlides : 0,
pdfFonts : {},
async function exportSlides(plugin, page, pdf) { pdfXObjects : {},
const context = { totalSlides : await plugin.slideCount(),
progressBarOverflow : 0, };
currentSlide : 1, // TODO: support a more advanced "fragment to pause" mapping
exportedSlides : 0, // for special use cases like GIF animations
pdfFonts : {}, // TODO: support plugin optional promise to wait until a particular mutation
pdfXObjects : {}, // instead of a pause
totalSlides : await plugin.slideCount(),
};
// TODO: support a more advanced "fragment to pause" mapping
// for special use cases like GIF animations
// TODO: support plugin optional promise to wait until a particular mutation
// instead of a pause
if (options.slides && !options.slides[context.currentSlide]) {
process.stdout.write('\r' + await progressBar(plugin, context, { skip: true }));
} else {
await pause(options.pause);
await exportSlide(plugin, page, pdf, context);
}
const maxSlide = options.slides ? Math.max(...Object.keys(options.slides)) : Infinity;
let hasNext = await hasNextSlide(plugin, context);
while (hasNext && context.currentSlide < maxSlide) {
await nextSlide(plugin, context);
await pause(options.pause);
if (options.slides && !options.slides[context.currentSlide]) { if (options.slides && !options.slides[context.currentSlide]) {
process.stdout.write('\r' + await progressBar(plugin, context, { skip: true })); process.stdout.write('\r' + await progressBar(plugin, context, { skip: true }));
} else { } else {
await pause(options.pause);
await exportSlide(plugin, page, pdf, context); await exportSlide(plugin, page, pdf, context);
} }
hasNext = await hasNextSlide(plugin, context); const maxSlide = options.slides ? Math.max(...Object.keys(options.slides)) : Infinity;
} let hasNext = await hasNextSlide(plugin, context);
// Flush consolidated fonts while (hasNext && context.currentSlide < maxSlide) {
Object.values(context.pdfFonts).forEach(({ ref, font }) => { await nextSlide(plugin, context);
pdf.context.assign(ref, pdf.context.flateStream(font.write({ type: 'ttf', hinting: true }))); await pause(options.pause);
}); if (options.slides && !options.slides[context.currentSlide]) {
return context; process.stdout.write('\r' + await progressBar(plugin, context, { skip: true }));
}
async function exportSlide(plugin, page, pdf, context) {
process.stdout.write('\r' + await progressBar(plugin, context));
const buffer = await page.pdf({
width : options.size.width,
height : options.size.height,
printBackground : true,
pageRanges : '1',
displayHeaderFooter : false,
timeout : options.bufferTimeout,
});
await printSlide(pdf, await PDFDocument.load(buffer, { parseSpeed: ParseSpeeds.Fastest }), context);
context.exportedSlides++;
if (options.screenshots) {
for (let resolution of options.screenshotSizes || [options.size]) {
await page.setViewport(resolution);
// Delay page rendering to wait for the resize event to complete,
// e.g. for impress.js (may be needed to be configurable)
await pause(1000);
await page.screenshot({
path: path.join(options.screenshotDirectory, options.filename
.replace('.pdf', `_${context.currentSlide}_${resolution.width}x${resolution.height}.${options.screenshotFormat}`)),
fullPage: false,
omitBackground: true,
});
await page.setViewport(options.size);
await pause(1000);
}
}
}
async function printSlide(pdf, slide, context) {
const duplicatedEntries = [];
const [page] = await pdf.copyPages(slide, [0]);
pdf.addPage(page);
// Traverse the page to consolidate duplicates
parseResources(page.node);
// And delete all the collected duplicates
duplicatedEntries.forEach(ref => pdf.context.delete(ref));
function parseResources(dictionary) {
const resources = dictionary.get(PDFName.Resources);
if (resources.has(PDFName.XObject)) {
const xObject = resources.get(PDFName.XObject);
xObject.entries().forEach(entry => parseXObject(entry, xObject));
}
if (resources.has(PDFName.Font)) {
resources.get(PDFName.Font).entries().forEach(parseFont);
}
}
function parseXObject([name, entry], xObject) {
const object = page.node.context.lookup(entry);
const subtype = object.dict.get(PDFName.of('Subtype'));
if (subtype === PDFName.of('Image')) {
const digest = crypto.createHash('SHA1').update(object.contents).digest('hex');
if (!context.pdfXObjects[digest]) {
context.pdfXObjects[digest] = entry;
} else { } else {
xObject.set(name, context.pdfXObjects[digest]); await exportSlide(plugin, page, pdf, context);
duplicatedEntries.push(entry);
} }
} else { hasNext = await hasNextSlide(plugin, context);
parseResources(object.dict);
} }
}; // Flush consolidated fonts
Object.values(context.pdfFonts).forEach(({ ref, font }) => {
pdf.context.assign(ref, pdf.context.flateStream(font.write({ type: 'ttf', hinting: true })));
});
return context;
}
function parseFont([_, entry]) { async function exportSlide(plugin, page, pdf, context) {
const object = page.node.context.lookup(entry); process.stdout.write('\r' + await progressBar(plugin, context));
const subtype = object.get(PDFName.of('Subtype'));
// See "Introduction to Font Data Structures" from PDF specification const buffer = await page.pdf({
if (subtype === PDFName.of('Type0')) { width : options.size.width,
// TODO: properly support composite fonts with multiple descendants height : options.size.height,
const descendant = page.node.context.lookup(object.get(PDFName.of('DescendantFonts')).get(0)); printBackground : true,
if (descendant.get(PDFName.of('Subtype')) === PDFName.of('CIDFontType2')) { pageRanges : '1',
const descriptor = page.node.context.lookup(descendant.get(PDFName.of('FontDescriptor'))); displayHeaderFooter : false,
const ref = descriptor.get(PDFName.of('FontFile2')); timeout : options.bufferTimeout,
const file = page.node.context.lookup(ref); });
if (!file) { await printSlide(pdf, await PDFDocument.load(buffer, { parseSpeed: ParseSpeeds.Fastest }), context);
// The font has already been processed and removed context.exportedSlides++;
return;
} if (options.screenshots) {
const bytes = decodePDFRawStream(file).decode(); for (let resolution of options.screenshotSizes || [options.size]) {
const font = Font.create(Buffer.from(bytes), { type: 'ttf', hinting: true }); await page.setViewport(resolution);
// Some fonts happen to have no metadata, which is required by fonteditor // Delay page rendering to wait for the resize event to complete,
if (!font.data.name) { // e.g. for impress.js (may be needed to be configurable)
font.data.name = {}; await pause(1000);
} await page.screenshot({
// PDF font name does not contain sub family on Windows 10, path: path.join(options.screenshotDirectory, options.filename
// so a more robust key is computed from the font metadata .replace('.pdf', `_${context.currentSlide}_${resolution.width}x${resolution.height}.${options.screenshotFormat}`)),
const id = descriptor.get(PDFName.of('FontName')).value() + ' - ' + fontMetadataKey(font.data.name); fullPage: false,
if (context.pdfFonts[id]) { omitBackground: true,
const f = context.pdfFonts[id].font; });
font.data.glyf.forEach((g, i) => { await page.setViewport(options.size);
if (g.contours && g.contours.length > 0) { await pause(1000);
if (!f.data.glyf[i] || !f.data.glyf[i].contours || f.data.glyf[i].contours.length === 0) { }
mergeGlyph(f, i, g); }
} }
} else if (g.compound) {
if (!f.data.glyf[i] || typeof f.data.glyf[i].compound === 'undefined') { async function printSlide(pdf, slide, context) {
mergeGlyph(f, i, g); const duplicatedEntries = [];
} const [page] = await pdf.copyPages(slide, [0]);
} pdf.addPage(page);
}); // Traverse the page to consolidate duplicates
descriptor.set(PDFName.of('FontFile2'), context.pdfFonts[id].ref); parseResources(page.node);
duplicatedEntries.push(ref); // And delete all the collected duplicates
duplicatedEntries.forEach(ref => pdf.context.delete(ref));
function parseResources(dictionary) {
const resources = dictionary.get(PDFName.Resources);
if (resources.has(PDFName.XObject)) {
const xObject = resources.get(PDFName.XObject);
xObject.entries().forEach(entry => parseXObject(entry, xObject));
}
if (resources.has(PDFName.Font)) {
resources.get(PDFName.Font).entries().forEach(parseFont);
}
}
function parseXObject([name, entry], xObject) {
const object = page.node.context.lookup(entry);
const subtype = object.dict.get(PDFName.of('Subtype'));
if (subtype === PDFName.of('Image')) {
const digest = crypto.createHash('SHA1').update(object.contents).digest('hex');
if (!context.pdfXObjects[digest]) {
context.pdfXObjects[digest] = entry;
} else { } else {
context.pdfFonts[id] = { ref: ref, font: font }; xObject.set(name, context.pdfXObjects[digest]);
duplicatedEntries.push(entry);
}
} else {
parseResources(object.dict);
}
};
function parseFont([_, entry]) {
const object = page.node.context.lookup(entry);
const subtype = object.get(PDFName.of('Subtype'));
// See "Introduction to Font Data Structures" from PDF specification
if (subtype === PDFName.of('Type0')) {
// TODO: properly support composite fonts with multiple descendants
const descendant = page.node.context.lookup(object.get(PDFName.of('DescendantFonts')).get(0));
if (descendant.get(PDFName.of('Subtype')) === PDFName.of('CIDFontType2')) {
const descriptor = page.node.context.lookup(descendant.get(PDFName.of('FontDescriptor')));
const ref = descriptor.get(PDFName.of('FontFile2'));
const file = page.node.context.lookup(ref);
if (!file) {
// The font has already been processed and removed
return;
}
const bytes = decodePDFRawStream(file).decode();
const font = Font.create(Buffer.from(bytes), { type: 'ttf', hinting: true });
// Some fonts happen to have no metadata, which is required by fonteditor
if (!font.data.name) {
font.data.name = {};
}
// PDF font name does not contain sub family on Windows 10,
// so a more robust key is computed from the font metadata
const id = descriptor.get(PDFName.of('FontName')).value() + ' - ' + fontMetadataKey(font.data.name);
if (context.pdfFonts[id]) {
const f = context.pdfFonts[id].font;
font.data.glyf.forEach((g, i) => {
if (g.contours && g.contours.length > 0) {
if (!f.data.glyf[i] || !f.data.glyf[i].contours || f.data.glyf[i].contours.length === 0) {
mergeGlyph(f, i, g);
}
} else if (g.compound) {
if (!f.data.glyf[i] || typeof f.data.glyf[i].compound === 'undefined') {
mergeGlyph(f, i, g);
}
}
});
descriptor.set(PDFName.of('FontFile2'), context.pdfFonts[id].ref);
duplicatedEntries.push(ref);
} else {
context.pdfFonts[id] = { ref: ref, font: font };
}
} }
} }
} };
};
function mergeGlyph(font, index, glyf) { function mergeGlyph(font, index, glyf) {
if (font.data.glyf.length <= index) { if (font.data.glyf.length <= index) {
for (let i = font.data.glyf.length; i < index; i++) { for (let i = font.data.glyf.length; i < index; i++) {
font.data.glyf.push({ contours: Array(0), advanceWidth: 0, leftSideBearing: 0 }); font.data.glyf.push({ contours: Array(0), advanceWidth: 0, leftSideBearing: 0 });
}
font.data.glyf.push(glyf);
} else {
font.data.glyf[index] = glyf;
} }
font.data.glyf.push(glyf); }
} else {
font.data.glyf[index] = glyf; function fontMetadataKey(font) {
const keys = ['fontFamily', 'fontSubFamily', 'fullName', 'preferredFamily', 'preferredSubFamily', 'uniqueSubFamily'];
return Object.entries(font)
.filter(([key, _]) => keys.includes(key))
.reduce((r, [k, v], i) => r + (i > 0 ? ',' : '') + k + '=' + v, '');
} }
} }
function fontMetadataKey(font) { async function hasNextSlide(plugin, context) {
const keys = ['fontFamily', 'fontSubFamily', 'fullName', 'preferredFamily', 'preferredSubFamily', 'uniqueSubFamily']; if (typeof plugin.hasNextSlide === 'function') {
return Object.entries(font) return await plugin.hasNextSlide();
.filter(([key, _]) => keys.includes(key)) } else {
.reduce((r, [k, v], i) => r + (i > 0 ? ',' : '') + k + '=' + v, ''); return context.currentSlide < context.totalSlides;
}
} }
}
async function hasNextSlide(plugin, context) { async function nextSlide(plugin, context) {
if (typeof plugin.hasNextSlide === 'function') { context.currentSlide++;
return await plugin.hasNextSlide(); return plugin.nextSlide();
} else {
return context.currentSlide < context.totalSlides;
} }
}
async function nextSlide(plugin, context) { // TODO: add progress bar, duration, ETA and file size
context.currentSlide++; async function progressBar(plugin, context, { skip } = { skip : false }) {
return plugin.nextSlide(); const cols = [];
} const index = await plugin.currentSlideIndex();
cols.push(`${skip ? 'Skipping' : 'Printing'} slide `);
// TODO: add progress bar, duration, ETA and file size cols.push(`#${index}`.padEnd(8));
async function progressBar(plugin, context, { skip } = { skip : false }) { cols.push(' (');
const cols = []; cols.push(`${context.currentSlide}`.padStart(context.totalSlides ? context.totalSlides.toString().length : 3));
const index = await plugin.currentSlideIndex(); cols.push('/');
cols.push(`${skip ? 'Skipping' : 'Printing'} slide `); cols.push(context.totalSlides || ' ?');
cols.push(`#${index}`.padEnd(8)); cols.push(') ...');
cols.push(' ('); // erase overflowing slide fragments
cols.push(`${context.currentSlide}`.padStart(context.totalSlides ? context.totalSlides.toString().length : 3)); cols.push(' '.repeat(Math.max(context.progressBarOverflow - Math.max(index.length + 1 - 8, 0), 0)));
cols.push('/'); context.progressBarOverflow = Math.max(index.length + 1 - 8, 0);
cols.push(context.totalSlides || ' ?'); return cols.join('');
cols.push(') ...'); }
// erase overflowing slide fragments })();
cols.push(' '.repeat(Math.max(context.progressBarOverflow - Math.max(index.length + 1 - 8, 0), 0)));
context.progressBarOverflow = Math.max(index.length + 1 - 8, 0);
return cols.join('');
}

View File

@ -1,13 +1,12 @@
var chalk = require('chalk'); import chalk from 'chalk';
function ArgParser() { class ArgParser {
this.commands = {}; // expected commands constructor() {
this.specs = {}; // option specifications this.commands = {}; // expected commands
} this.specs = {}; // option specifications
}
ArgParser.prototype = {
/* Add a command to the expected commands */ /* Add a command to the expected commands */
command : function(name) { command(name) {
var command; var command;
if (name) { if (name) {
command = this.commands[name] = { command = this.commands[name] = {
@ -23,114 +22,99 @@ ArgParser.prototype = {
// facilitates command('name').options().cb().help() // facilitates command('name').options().cb().help()
var chain = { var chain = {
options : function(specs) { options: function (specs) {
command.specs = specs; command.specs = specs;
return chain; return chain;
}, },
opts : function(specs) { opts: function (specs) {
// old API // old API
return this.options(specs); return this.options(specs);
}, },
option : function(name, spec) { option: function (name, spec) {
command.specs[name] = spec; command.specs[name] = spec;
return chain; return chain;
}, },
callback : function(cb) { callback: function (cb) {
command.cb = cb; command.cb = cb;
return chain; return chain;
}, },
help : function(help) { help: function (help) {
command.help = help; command.help = help;
return chain; return chain;
}, },
usage : function(usage) { usage: function (usage) {
command._usage = usage; command._usage = usage;
return chain; return chain;
}, },
root : function(root) { root: function (root) {
command.root = root; command.root = root;
return chain; return chain;
} }
}; };
return chain; return chain;
}, }
nocommand() {
nocommand : function() {
return this.command(); return this.command();
}, }
options(specs) {
options : function(specs) {
this.specs = specs; this.specs = specs;
return this; return this;
}, }
opts(specs) {
opts : function(specs) {
// old API // old API
return this.options(specs); return this.options(specs);
}, }
globalOpts(specs) {
globalOpts : function(specs) {
// old API // old API
return this.options(specs); return this.options(specs);
}, }
option(name, spec) {
option : function(name, spec) {
this.specs[name] = spec; this.specs[name] = spec;
return this; return this;
}, }
usage(usage) {
usage : function(usage) {
this._usage = usage; this._usage = usage;
return this; return this;
}, }
printer(print) {
printer : function(print) {
this.print = print; this.print = print;
return this; return this;
}, }
script(script) {
script : function(script) {
this._script = script; this._script = script;
return this; return this;
}, }
scriptName(script) {
scriptName : function(script) {
// old API // old API
return this.script(script); return this.script(script);
}, }
help(help) {
help : function(help) {
this._help = help; this._help = help;
return this; return this;
}, }
colors() {
colors: function() {
// deprecated - colors are on by default now // deprecated - colors are on by default now
return this; return this;
}, }
nocolors() {
nocolors : function() {
this._nocolors = true; this._nocolors = true;
return this; return this;
}, }
parseArgs(argv) {
parseArgs : function(argv) {
// old API // old API
return this.parse(argv); return this.parse(argv);
}, }
nom(argv) {
nom : function(argv) {
return this.parse(argv); return this.parse(argv);
}, }
parse(argv) {
parse : function(argv) { this.print = this.print || function (str, code) {
this.print = this.print || function(str, code) {
console.log(str); console.log(str);
process.exit(code || 0); process.exit(code || 0);
}; };
this._help = this._help || ""; this._help = this._help || "";
this._script = this._script || process.argv[0] + " " this._script = this._script || process.argv[0] + " "
+ require('path').basename(process.argv[1]); + require('path').basename(process.argv[1]);
this.specs = this.specs || {}; this.specs = this.specs || {};
var argv = argv || process.argv.slice(2); var argv = argv || process.argv.slice(2);
@ -140,72 +124,72 @@ ArgParser.prototype = {
var commandExpected = Object.keys(this.commands).length > 0; var commandExpected = Object.keys(this.commands).length > 0;
if (commandExpected) { if (commandExpected) {
if (command) { if (command) {
if (command.root) { if (command.root) {
this.specs = command.specs; this.specs = command.specs;
} else { } else {
Object.assign(this.specs, command.specs); Object.assign(this.specs, command.specs);
}
this.subcommand = true;
this._script += " " + command.name;
if (command.help) {
this._help = command.help;
}
this.specs.command = {
hidden: true,
name: 'command',
position: 0,
help: command.help
};
}
else if (!this.fallback) {
return this.print(this._script + ": command expected", 1);
}
else {
// no command but command expected e.g. 'git -v'
var helpStringBuilder = {
list: function () {
return 'one of: ' + Object.entries(this.commands)
.filter(([key, cmd]) => !cmd.root)
.map(([key, cmd]) => key).join(', ');
},
twoColumn: function () {
// find the longest command name to ensure horizontal alignment
var maxLength = Math.max(...Object.values(this.commands).map(cmd => cmd.name.length));
// create the two column text strings
var cmdHelp = Object.entries(this.commands).map(([name, cmd]) => {
var diff = maxLength - name.length;
var pad = new Array(diff + 4).join(" ");
return " " + [name, pad, cmd.help].join(" ");
});
return "\n" + cmdHelp.join("\n");
} }
this.subcommand = true; };
this._script += " " + command.name;
if (command.help) {
this._help = command.help;
}
this.specs.command = {
hidden: true,
name: 'command',
position: 0,
help: command.help
};
}
else if (!this.fallback) {
return this.print(this._script + ": command expected", 1);
}
else {
// no command but command expected e.g. 'git -v'
var helpStringBuilder = {
list : function() {
return 'one of: ' + Object.entries(this.commands)
.filter(([key, cmd]) => !cmd.root)
.map(([key, cmd]) => key).join(', ');
},
twoColumn : function() {
// find the longest command name to ensure horizontal alignment
var maxLength = Math.max(...Object.values(this.commands).map(cmd => cmd.name.length));
// create the two column text strings
var cmdHelp = Object.entries(this.commands).map(([name, cmd]) => {
var diff = maxLength - name.length;
var pad = new Array(diff + 4).join(" ");
return " " + [ name, pad, cmd.help ].join(" ");
});
return "\n" + cmdHelp.join("\n");
}
};
// if there are a small number of commands and all have help strings, // if there are a small number of commands and all have help strings,
// display them in a two column table; otherwise use the brief version. // display them in a two column table; otherwise use the brief version.
// The arbitrary choice of "20" comes from the number commands git // The arbitrary choice of "20" comes from the number commands git
// displays as "common commands" // displays as "common commands"
var helpType = 'list'; var helpType = 'list';
if (Object.keys(this.commands).length <= 20) { if (Object.keys(this.commands).length <= 20) {
if (Object.values(this.commands).every(cmd => cmd.help)) { if (Object.values(this.commands).every(cmd => cmd.help)) {
helpType = 'twoColumn'; helpType = 'twoColumn';
}
} }
}
this.specs.command = { this.specs.command = {
name: 'command', name: 'command',
position: 0, position: 0,
help: helpStringBuilder[helpType].call(this) help: helpStringBuilder[helpType].call(this)
}; };
if (this.fallback) { if (this.fallback) {
Object.assign(this.specs, this.fallback.specs); Object.assign(this.specs, this.fallback.specs);
this._help = this.fallback.help; this._help = this.fallback.help;
} else { } else {
this.specs.command.required = true; this.specs.command.required = true;
} }
} }
} }
if (this.specs.length === undefined) { if (this.specs.length === undefined) {
@ -228,7 +212,7 @@ ArgParser.prototype = {
/* parse the args */ /* parse the args */
var that = this; var that = this;
args.reduce(function(arg, val) { args.reduce(function (arg, val) {
/* positional */ /* positional */
if (arg.isValue) { if (arg.isValue) {
positionals.push(arg.value); positionals.push(arg.value);
@ -237,20 +221,20 @@ ArgParser.prototype = {
var last = arg.chars.pop(); var last = arg.chars.pop();
/* -cfv */ /* -cfv */
(arg.chars).forEach(function(ch) { (arg.chars).forEach(function (ch) {
that.setOption(options, ch, true); that.setOption(options, ch, true);
}); });
/* -v key */ /* -v key */
if (!that.opt(last).flag) { if (!that.opt(last).flag) {
if (val.isValue) { if (val.isValue) {
that.setOption(options, last, val.value); that.setOption(options, last, val.value);
return Arg(); // skip next turn - swallow arg return Arg(); // skip next turn - swallow arg
} }
else { else {
that.print("'-" + (that.opt(last).name || last) + "'" that.print("'-" + (that.opt(last).name || last) + "'"
+ " expects a value\n\n" + that.getUsage(), 1); + " expects a value\n\n" + that.getUsage(), 1);
} }
} }
else { else {
/* -v */ /* -v */
@ -291,25 +275,25 @@ ArgParser.prototype = {
if (!command && this.fallback) if (!command && this.fallback)
positionals.unshift(undefined); positionals.unshift(undefined);
positionals.forEach(function(pos, index) { positionals.forEach(function (pos, index) {
this.setOption(options, index, pos); this.setOption(options, index, pos);
}, this); }, this);
options._ = positionals; options._ = positionals;
this.specs.forEach(function(opt) { this.specs.forEach(function (opt) {
if (opt.default !== undefined && options[opt.name] === undefined) { if (opt.default !== undefined && options[opt.name] === undefined) {
this.setOption(options, opt.name, opt.default); this.setOption(options, opt.name, opt.default);
} }
}, this); }, this);
// exit if required arg isn't present // exit if required arg isn't present
this.specs.forEach(function(opt) { this.specs.forEach(function (opt) {
if (opt.required && options[opt.name] === undefined) { if (opt.required && options[opt.name] === undefined) {
var msg = opt.name + " argument is required"; var msg = opt.name + " argument is required";
msg = this._nocolors ? msg : chalk.red(msg); msg = this._nocolors ? msg : chalk.red(msg);
this.print("\n" + msg + "\n" + this.getUsage(), 1); this.print("\n" + msg + "\n" + this.getUsage(), 1);
} }
}, this); }, this);
@ -321,9 +305,8 @@ ArgParser.prototype = {
} }
return options; return options;
}, }
getUsage() {
getUsage : function() {
if (this.command && this.command._usage) { if (this.command && this.command._usage) {
return this.command._usage; return this.command._usage;
} }
@ -360,7 +343,7 @@ ArgParser.prototype = {
} }
// assume there are no gaps in the specified pos. args // assume there are no gaps in the specified pos. args
positionals.forEach(function(pos) { positionals.forEach(function (pos) {
str += " "; str += " ";
var posStr = pos.string; var posStr = pos.string;
if (!posStr) { if (!posStr) {
@ -396,14 +379,14 @@ ArgParser.prototype = {
} }
var longest = Math.max(...positionals.map(pos => pos.name.length)); var longest = Math.max(...positionals.map(pos => pos.name.length));
positionals.forEach(function(pos) { positionals.forEach(function (pos) {
var posStr = pos.string || pos.name; var posStr = pos.string || pos.name;
str += posStr + spaces(longest - posStr.length) + " "; str += posStr + spaces(longest - posStr.length) + " ";
if (!this._nocolors) { if (!this._nocolors) {
str += chalk.grey(pos.help || "") str += chalk.grey(pos.help || "");
} }
else { else {
str += (pos.help || "") str += (pos.help || "");
} }
str += "\n"; str += "\n";
}, this); }, this);
@ -422,13 +405,13 @@ ArgParser.prototype = {
longest = Math.max(...options.map(opt => opt.string.length)); longest = Math.max(...options.map(opt => opt.string.length));
options.forEach(function(opt) { options.forEach(function (opt) {
if (!opt.hidden) { if (!opt.hidden) {
str += " " + opt.string + spaces(longest - opt.string.length) + " "; str += " " + opt.string + spaces(longest - opt.string.length) + " ";
var defaults = (opt.default != null ? " [" + opt.default + "]" : ""); var defaults = (opt.default != null ? " [" + opt.default + "]" : "");
var help = opt.help ? opt.help + defaults : ""; var help = opt.help ? opt.help + defaults : "";
str += this._nocolors ? help: chalk.grey(help); str += this._nocolors ? help : chalk.grey(help);
str += "\n"; str += "\n";
} }
@ -440,58 +423,59 @@ ArgParser.prototype = {
} }
return str; return str;
} }
}; opt(arg) {
// get the specified opt for this parsed arg
var match = Opt({});
this.specs.forEach(function (opt) {
if (opt.matches(arg)) {
match = opt;
}
});
return match;
}
setOption(options, arg, value) {
var option = this.opt(arg);
if (option.callback) {
var message = option.callback(value);
ArgParser.prototype.opt = function(arg) { if (typeof message == "string") {
// get the specified opt for this parsed arg this.print(message, 1);
var match = Opt({}); }
this.specs.forEach(function(opt) {
if (opt.matches(arg)) {
match = opt;
} }
});
return match;
};
ArgParser.prototype.setOption = function(options, arg, value) { if (option.type != "string") {
var option = this.opt(arg); try {
if (option.callback) { // infer type by JSON parsing the string
var message = option.callback(value); value = JSON.parse(value);
}
if (typeof message == "string") { catch (e) { }
this.print(message, 1);
} }
}
if (option.type != "string") { if (option.transform) {
try { value = option.transform(value);
// infer type by JSON parsing the string }
value = JSON.parse(value)
}
catch(e) {}
}
if (option.transform) { var name = option.name || arg;
value = option.transform(value); if (option.choices && option.choices.indexOf(value) == -1) {
} this.print(name + " must be one of: " + option.choices.join(", "), 1);
}
var name = option.name || arg; if (option.list) {
if (option.choices && option.choices.indexOf(value) == -1) { if (!options[name]) {
this.print(name + " must be one of: " + option.choices.join(", "), 1); options[name] = [value];
} }
else {
if (option.list) { options[name].push(value);
if (!options[name]) { }
options[name] = [value];
} }
else { else {
options[name].push(value); options[name] = value;
} }
} }
else { }
options[name] = value;
}
};
/* an arg is an item that's actually parsed from the command line /* an arg is an item that's actually parsed from the command line
@ -585,4 +569,4 @@ var createParser = function() {
return new ArgParser(); return new ArgParser();
} }
module.exports = createParser(); export default createParser();

View File

@ -1,45 +1,7 @@
'use strict'; 'use strict';
module.exports.delay = delay => value => new Promise(resolve => setTimeout(resolve, delay, value)); export const delay = delay => value => new Promise(resolve => setTimeout(resolve, delay, value));
module.exports.pause = ms => module.exports.delay(ms)(); export const pause = ms => delay(ms)();
module.exports.wait = ms => () => module.exports.delay(ms); export const wait = ms => () => delay(ms);
// Can be removed when Node 8 becomes a requirement
// https://github.com/uxitten/polyfill/blob/master/string.polyfill.js
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart
if (!String.prototype.padStart) {
String.prototype.padStart = function padStart(targetLength, padString) {
targetLength = targetLength >> 0; //floor if number or convert non-number to 0;
padString = String(padString || ' ');
if (this.length > targetLength) {
return String(this);
} else {
targetLength = targetLength - this.length;
if (targetLength > padString.length) {
padString += padString.repeat(targetLength / padString.length);
}
return padString.slice(0, targetLength) + String(this);
}
};
}
// https://github.com/uxitten/polyfill/blob/master/string.polyfill.js
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat
if (!String.prototype.padEnd) {
String.prototype.padEnd = function padEnd(targetLength, padString) {
targetLength = targetLength >> 0; //floor if number or convert non-number to 0;
padString = String(padString || ' ');
if (this.length > targetLength) {
return String(this);
} else {
targetLength = targetLength - this.length;
if (targetLength > padString.length) {
padString += padString.repeat(targetLength / padString.length);
}
return String(this) + padString.slice(0, targetLength);
}
};
}

53
npm-shrinkwrap.json generated
View File

@ -9,7 +9,8 @@
"version": "3.5.0", "version": "3.5.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"chalk": "^4.1.2", "chalk": "~5.1.2",
"chalk-template": "^0.4.0",
"fonteditor-core": "2.1.10", "fonteditor-core": "2.1.10",
"pdf-lib": "1.17.1", "pdf-lib": "1.17.1",
"puppeteer": "18.2.1", "puppeteer": "18.2.1",
@ -162,6 +163,31 @@
} }
}, },
"node_modules/chalk": { "node_modules/chalk": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.1.2.tgz",
"integrity": "sha512-E5CkT4jWURs1Vy5qGJye+XwCkNj7Od3Af7CP6SujMetSMkLs8Do2RWJK5yx1wamHV/op8Rz+9rltjaTQWDnEFQ==",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/chalk-template": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz",
"integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==",
"dependencies": {
"chalk": "^4.1.2"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/chalk-template?sponsor=1"
}
},
"node_modules/chalk-template/node_modules/chalk": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
@ -775,12 +801,27 @@
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==" "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="
}, },
"chalk": { "chalk": {
"version": "4.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "integrity": "sha512-E5CkT4jWURs1Vy5qGJye+XwCkNj7Od3Af7CP6SujMetSMkLs8Do2RWJK5yx1wamHV/op8Rz+9rltjaTQWDnEFQ=="
},
"chalk-template": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz",
"integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==",
"requires": { "requires": {
"ansi-styles": "^4.1.0", "chalk": "^4.1.2"
"supports-color": "^7.1.0" },
"dependencies": {
"chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
}
} }
}, },
"chownr": { "chownr": {

View File

@ -6,6 +6,7 @@
"homepage": "https://github.com/astefanutti/decktape", "homepage": "https://github.com/astefanutti/decktape",
"license": "MIT", "license": "MIT",
"main": "decktape.js", "main": "decktape.js",
"type": "module",
"bin": { "bin": {
"decktape": "decktape.js" "decktape": "decktape.js"
}, },
@ -20,7 +21,8 @@
"url": "https://github.com/astefanutti/decktape/issues" "url": "https://github.com/astefanutti/decktape/issues"
}, },
"dependencies": { "dependencies": {
"chalk": "^4.1.2", "chalk": "~5.1.2",
"chalk-template": "^0.4.0",
"fonteditor-core": "2.1.10", "fonteditor-core": "2.1.10",
"pdf-lib": "1.17.1", "pdf-lib": "1.17.1",
"puppeteer": "18.2.1", "puppeteer": "18.2.1",
@ -28,6 +30,6 @@
"urijs": "1.19.11" "urijs": "1.19.11"
}, },
"engines": { "engines": {
"node": ">=12.0.0" "node": ">=12.20"
} }
} }

View File

@ -1,9 +1,9 @@
exports.help = export const help =
`Requires the bespoke-extern module to expose the Bespoke.js API to a global variable named `Requires the bespoke-extern module to expose the Bespoke.js API to a global variable named
'bespoke' and provides access to the collection of deck instances via 'bespoke.decks 'bespoke' and provides access to the collection of deck instances via 'bespoke.decks
and the most recent deck via 'bespoke.deck'.`; and the most recent deck via 'bespoke.deck'.`;
exports.create = page => new Bespoke(page); export const create = page => new Bespoke(page);
class Bespoke { class Bespoke {

View File

@ -1,4 +1,4 @@
exports.create = page => new Deck(page); export const create = page => new Deck(page);
class Deck { class Deck {

View File

@ -1,4 +1,4 @@
exports.create = page => new DZSlides(page); export const create = page => new DZSlides(page);
class DZSlides { class DZSlides {

View File

@ -1,4 +1,4 @@
exports.create = page => new Flowtime(page); export const create = page => new Flowtime(page);
class Flowtime { class Flowtime {

View File

@ -2,9 +2,9 @@
// and detects changes to the DOM. The deck is considered over when no change // and detects changes to the DOM. The deck is considered over when no change
// is detected afterward. // is detected afterward.
const { pause } = require('../libs/util'); import { pause } from '../libs/util.js';
exports.options = { export const options = {
key : { key : {
default : 'ArrowRight', default : 'ArrowRight',
metavar : '<key>', metavar : '<key>',
@ -23,15 +23,15 @@ exports.options = {
}, },
}; };
exports.help = export const help =
`Emulates the end-user interaction by pressing the key with the specified --key option `Emulates the end-user interaction by pressing the key with the specified --key option
and iterates over the presentation as long as: and iterates over the presentation as long as:
- Any change to the DOM is detected by observing mutation events targeting the body element - Any change to the DOM is detected by observing mutation events targeting the body element
and its subtree, and its subtree,
- Nor the number of slides exported has reached the specified --max-slides option. - Nor the number of slides exported has reached the specified --max-slides option.
The --key option must be one of the 'KeyboardEvent' keys and defaults to [${exports.options.key.default}].`; The --key option must be one of the 'KeyboardEvent' keys and defaults to [${options.key.default}].`;
exports.create = (page, options) => new Generic(page, options); export const create = (page, options) => new Generic(page, options);
class Generic { class Generic {
constructor(page, options) { constructor(page, options) {
@ -39,8 +39,8 @@ class Generic {
this.options = options; this.options = options;
this.currentSlide = 1; this.currentSlide = 1;
this.isNextSlideDetected = false; this.isNextSlideDetected = false;
this.key = this.options.key || exports.options.key.default; this.key = this.options.key || options.key.default;
this.media = this.options.media || exports.options.media.default; this.media = this.options.media || options.media.default;
} }
getName() { getName() {

View File

@ -1,4 +1,4 @@
exports.create = page => new Impress(page); export const create = page => new Impress(page);
class Impress { class Impress {

View File

@ -1,4 +1,4 @@
exports.create = page => new Inspire(page); export const create = page => new Inspire(page);
class Inspire { class Inspire {

View File

@ -1,4 +1,4 @@
exports.create = page => new NueDeck(page); export const create = page => new NueDeck(page);
class NueDeck { class NueDeck {

View File

@ -1,4 +1,4 @@
exports.create = page => new Remark(page); export const create = page => new Remark(page);
class Remark { class Remark {

View File

@ -1,6 +1,6 @@
const URI = require('urijs'); import URI from 'urijs';
exports.create = page => new Reveal(page); export const create = page => new Reveal(page);
class Reveal { class Reveal {

View File

@ -1,4 +1,4 @@
exports.create = page => new RISE(page); export const create = page => new RISE(page);
class RISE { class RISE {

View File

@ -1,4 +1,4 @@
exports.create = page => new Shower(page); export const create = page => new Shower(page);
class Shower { class Shower {

View File

@ -1,4 +1,4 @@
exports.create = page => new Shower(page); export const create = page => new Shower(page);
class Shower { class Shower {

View File

@ -1,4 +1,4 @@
exports.create = page => new Slidy(page); export const create = page => new Slidy(page);
class Slidy { class Slidy {

View File

@ -1,4 +1,4 @@
exports.create = page => new WebSlides(page); export const create = page => new WebSlides(page);
class WebSlides { class WebSlides {