diff --git a/decktape.js b/decktape.js index 5c63af8..73cd676 100755 --- a/decktape.js +++ b/decktape.js @@ -2,22 +2,22 @@ 'use strict'; -const chalk = require('chalk'), - crypto = require('crypto'), - Font = require('fonteditor-core').Font, - fs = require('fs'), - os = require('os'), - parser = require('./libs/nomnom'), - path = require('path'), - puppeteer = require('puppeteer'), - URI = require('urijs'), - util = require('util'); +import chalk from 'chalk'; +import chalkTemplate from 'chalk-template'; +import crypto from 'crypto'; +import { Font } from 'fonteditor-core'; +import fs from 'fs'; +import os from 'os'; +import parser from './libs/nomnom.js'; +import path from 'path'; +import puppeteer from 'puppeteer'; +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'); - -const plugins = loadAvailablePlugins(path.join(path.dirname(__filename), 'plugins')); +import { delay, pause } from './libs/util.js'; parser.script('decktape').options({ url : { @@ -184,7 +184,8 @@ parser.command('version') .root(true) .help('Display decktape package version') .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(); }); parser.nocommand() @@ -198,19 +199,10 @@ parser.command('automatic') `Iterates over the available plugins, picks the compatible one for presentation at the specified and uses it to export and write the PDF into the specified .` ); -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 if (os.name === 'windows') parser.nocolors(); -const options = parser.parse(process.argv.slice(2)); const color = type => { switch (type) { @@ -226,6 +218,18 @@ process.on('unhandledRejection', error => { }); (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({ headless : true, @@ -255,9 +259,9 @@ process.on('unhandledRejection', error => { .on('requestfailed', request => { // do not output warning for cancelled requests 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, '...'); const load = page.waitForNavigation({ waitUntil: 'load', timeout: options.urlLoadTimeout }); @@ -275,274 +279,275 @@ process.on('unhandledRejection', error => { .then(_ => exportSlides(plugin, page, pdf)) .then(async context => { 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(); process.exit(); })) .catch(error => { - console.log(chalk`{red \n${error}}`); + console.log(chalkTemplate`{red \n${error}}`); browser.close(); process.exit(1); }); -})(); -async function writePdf(filename, pdf) { - const pdfDir = path.dirname(filename); - try { - fs.accessSync(pdfDir, fs.constants.F_OK); - } catch { - fs.mkdirSync(pdfDir, { recursive: true }); + async function writePdf(filename, pdf) { + const pdfDir = path.dirname(filename); + try { + fs.accessSync(pdfDir, fs.constants.F_OK); + } catch { + fs.mkdirSync(pdfDir, { recursive: true }); + } + fs.writeFileSync(filename, await pdf.save({ addDefaultPage: false })); } - fs.writeFileSync(filename, await pdf.save({ addDefaultPage: false })); -} -function loadAvailablePlugins(pluginsPath) { - return fs.readdirSync(pluginsPath).reduce((plugins, pluginPath) => { - const [, plugin] = pluginPath.match(/^(.*)\.js$/); - if (plugin && fs.statSync(path.join(pluginsPath, pluginPath)).isFile()) { - plugins[plugin] = require('./plugins/' + plugin); - } - return plugins; - }, {}); -} + async function loadAvailablePlugins(pluginsPath) { + const plugins = await fs.promises.readdir(pluginsPath); + const entries = await Promise.all(plugins.map(async pluginPath => { + const [, plugin] = pluginPath.match(/^(.*)\.js$/); + if (plugin && (await fs.promises.stat(path.join(pluginsPath, pluginPath))).isFile()) { + return [plugin, await import (`./plugins/${pluginPath}`)]; + } + })); + return Object.fromEntries(entries.filter(Boolean)); + } -async function createPlugin(page) { - let plugin; - if (!options.command || options.command === 'automatic') { - plugin = await createActivePlugin(page); - if (!plugin) { - console.log('No supported DeckTape plugin detected, falling back to generic plugin'); - plugin = plugins['generic'].create(page, options); + async function createPlugin(page) { + let plugin; + if (!options.command || options.command === 'automatic') { + plugin = await createActivePlugin(page); + if (!plugin) { + console.log('No supported DeckTape plugin detected, falling back to generic plugin'); + 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 { - 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}`); + console.log(chalkTemplate`{cyan {bold ${plugin.getName()}} plugin activated}`); + return plugin; + } + + 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) { - for (let id in plugins) { - if (id === 'generic') continue; - const plugin = plugins[id].create(page, options); - if (await plugin.isActive()) return plugin; + async function configurePage(plugin, page) { + if (!options.size) { + options.size = typeof plugin.size === 'function' ? await plugin.size() : { width: 1280, height: 720 }; + } + await page.setViewport(options.size); } -} -async function configurePage(plugin, page) { - if (!options.size) { - options.size = typeof plugin.size === 'function' ? await plugin.size() : { width: 1280, height: 720 }; + async function configurePlugin(plugin) { + if (typeof plugin.configure === 'function') { + await plugin.configure(); + } } - await page.setViewport(options.size); -} -async function configurePlugin(plugin) { - if (typeof plugin.configure === 'function') { - await plugin.configure(); - } -} - -async function exportSlides(plugin, page, pdf) { - const context = { - progressBarOverflow : 0, - currentSlide : 1, - exportedSlides : 0, - pdfFonts : {}, - pdfXObjects : {}, - 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); + async function exportSlides(plugin, page, pdf) { + const context = { + progressBarOverflow : 0, + currentSlide : 1, + exportedSlides : 0, + pdfFonts : {}, + pdfXObjects : {}, + 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); } - hasNext = await hasNextSlide(plugin, context); - } - // 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; -} - -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; + 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]) { + process.stdout.write('\r' + await progressBar(plugin, context, { skip: true })); } else { - xObject.set(name, context.pdfXObjects[digest]); - duplicatedEntries.push(entry); + await exportSlide(plugin, page, pdf, context); } - } else { - parseResources(object.dict); + hasNext = await hasNextSlide(plugin, context); } - }; + // 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]) { - 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); + 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 { - 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) { - if (font.data.glyf.length <= index) { - for (let i = font.data.glyf.length; i < index; i++) { - font.data.glyf.push({ contours: Array(0), advanceWidth: 0, leftSideBearing: 0 }); + function mergeGlyph(font, index, glyf) { + if (font.data.glyf.length <= index) { + 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(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) { - 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, ''); + async function hasNextSlide(plugin, context) { + if (typeof plugin.hasNextSlide === 'function') { + return await plugin.hasNextSlide(); + } else { + return context.currentSlide < context.totalSlides; + } } -} -async function hasNextSlide(plugin, context) { - if (typeof plugin.hasNextSlide === 'function') { - return await plugin.hasNextSlide(); - } else { - return context.currentSlide < context.totalSlides; + async function nextSlide(plugin, context) { + context.currentSlide++; + return plugin.nextSlide(); } -} -async function nextSlide(plugin, context) { - context.currentSlide++; - return plugin.nextSlide(); -} - -// TODO: add progress bar, duration, ETA and file size -async function progressBar(plugin, context, { skip } = { skip : false }) { - const cols = []; - const index = await plugin.currentSlideIndex(); - cols.push(`${skip ? 'Skipping' : 'Printing'} slide `); - cols.push(`#${index}`.padEnd(8)); - cols.push(' ('); - cols.push(`${context.currentSlide}`.padStart(context.totalSlides ? context.totalSlides.toString().length : 3)); - cols.push('/'); - cols.push(context.totalSlides || ' ?'); - 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(''); -} + // TODO: add progress bar, duration, ETA and file size + async function progressBar(plugin, context, { skip } = { skip : false }) { + const cols = []; + const index = await plugin.currentSlideIndex(); + cols.push(`${skip ? 'Skipping' : 'Printing'} slide `); + cols.push(`#${index}`.padEnd(8)); + cols.push(' ('); + cols.push(`${context.currentSlide}`.padStart(context.totalSlides ? context.totalSlides.toString().length : 3)); + cols.push('/'); + cols.push(context.totalSlides || ' ?'); + 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(''); + } +})(); diff --git a/libs/nomnom.js b/libs/nomnom.js index fbd3fee..ce866fb 100644 --- a/libs/nomnom.js +++ b/libs/nomnom.js @@ -1,13 +1,12 @@ -var chalk = require('chalk'); +import chalk from 'chalk'; -function ArgParser() { - this.commands = {}; // expected commands - this.specs = {}; // option specifications -} - -ArgParser.prototype = { +class ArgParser { + constructor() { + this.commands = {}; // expected commands + this.specs = {}; // option specifications + } /* Add a command to the expected commands */ - command : function(name) { + command(name) { var command; if (name) { command = this.commands[name] = { @@ -23,114 +22,99 @@ ArgParser.prototype = { // facilitates command('name').options().cb().help() var chain = { - options : function(specs) { + options: function (specs) { command.specs = specs; return chain; }, - opts : function(specs) { + opts: function (specs) { // old API return this.options(specs); }, - option : function(name, spec) { + option: function (name, spec) { command.specs[name] = spec; return chain; }, - callback : function(cb) { + callback: function (cb) { command.cb = cb; return chain; }, - help : function(help) { + help: function (help) { command.help = help; return chain; }, - usage : function(usage) { + usage: function (usage) { command._usage = usage; return chain; }, - root : function(root) { + root: function (root) { command.root = root; return chain; } }; return chain; - }, - - nocommand : function() { + } + nocommand() { return this.command(); - }, - - options : function(specs) { + } + options(specs) { this.specs = specs; return this; - }, - - opts : function(specs) { + } + opts(specs) { // old API return this.options(specs); - }, - - globalOpts : function(specs) { + } + globalOpts(specs) { // old API return this.options(specs); - }, - - option : function(name, spec) { + } + option(name, spec) { this.specs[name] = spec; return this; - }, - - usage : function(usage) { + } + usage(usage) { this._usage = usage; return this; - }, - - printer : function(print) { + } + printer(print) { this.print = print; return this; - }, - - script : function(script) { + } + script(script) { this._script = script; return this; - }, - - scriptName : function(script) { + } + scriptName(script) { // old API return this.script(script); - }, - - help : function(help) { + } + help(help) { this._help = help; return this; - }, - - colors: function() { + } + colors() { // deprecated - colors are on by default now return this; - }, - - nocolors : function() { + } + nocolors() { this._nocolors = true; return this; - }, - - parseArgs : function(argv) { + } + parseArgs(argv) { // old API return this.parse(argv); - }, - - nom : function(argv) { + } + nom(argv) { return this.parse(argv); - }, - - parse : function(argv) { - this.print = this.print || function(str, code) { + } + parse(argv) { + this.print = this.print || function (str, code) { console.log(str); process.exit(code || 0); }; this._help = this._help || ""; this._script = this._script || process.argv[0] + " " - + require('path').basename(process.argv[1]); + + require('path').basename(process.argv[1]); this.specs = this.specs || {}; var argv = argv || process.argv.slice(2); @@ -140,72 +124,72 @@ ArgParser.prototype = { var commandExpected = Object.keys(this.commands).length > 0; if (commandExpected) { - if (command) { - if (command.root) { - this.specs = command.specs; - } else { - Object.assign(this.specs, command.specs); + if (command) { + if (command.root) { + this.specs = command.specs; + } else { + 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, - // display them in a two column table; otherwise use the brief version. - // The arbitrary choice of "20" comes from the number commands git - // displays as "common commands" - var helpType = 'list'; - if (Object.keys(this.commands).length <= 20) { - if (Object.values(this.commands).every(cmd => cmd.help)) { - helpType = 'twoColumn'; - } + // 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. + // The arbitrary choice of "20" comes from the number commands git + // displays as "common commands" + var helpType = 'list'; + if (Object.keys(this.commands).length <= 20) { + if (Object.values(this.commands).every(cmd => cmd.help)) { + helpType = 'twoColumn'; } + } - this.specs.command = { - name: 'command', - position: 0, - help: helpStringBuilder[helpType].call(this) - }; + this.specs.command = { + name: 'command', + position: 0, + help: helpStringBuilder[helpType].call(this) + }; - if (this.fallback) { - Object.assign(this.specs, this.fallback.specs); - this._help = this.fallback.help; - } else { - this.specs.command.required = true; - } - } + if (this.fallback) { + Object.assign(this.specs, this.fallback.specs); + this._help = this.fallback.help; + } else { + this.specs.command.required = true; + } + } } if (this.specs.length === undefined) { @@ -228,7 +212,7 @@ ArgParser.prototype = { /* parse the args */ var that = this; - args.reduce(function(arg, val) { + args.reduce(function (arg, val) { /* positional */ if (arg.isValue) { positionals.push(arg.value); @@ -237,20 +221,20 @@ ArgParser.prototype = { var last = arg.chars.pop(); /* -cfv */ - (arg.chars).forEach(function(ch) { + (arg.chars).forEach(function (ch) { that.setOption(options, ch, true); }); /* -v key */ if (!that.opt(last).flag) { - if (val.isValue) { - that.setOption(options, last, val.value); - return Arg(); // skip next turn - swallow arg - } - else { - that.print("'-" + (that.opt(last).name || last) + "'" - + " expects a value\n\n" + that.getUsage(), 1); - } + if (val.isValue) { + that.setOption(options, last, val.value); + return Arg(); // skip next turn - swallow arg + } + else { + that.print("'-" + (that.opt(last).name || last) + "'" + + " expects a value\n\n" + that.getUsage(), 1); + } } else { /* -v */ @@ -291,25 +275,25 @@ ArgParser.prototype = { if (!command && this.fallback) positionals.unshift(undefined); - positionals.forEach(function(pos, index) { + positionals.forEach(function (pos, index) { this.setOption(options, index, pos); }, this); options._ = positionals; - this.specs.forEach(function(opt) { + this.specs.forEach(function (opt) { if (opt.default !== undefined && options[opt.name] === undefined) { this.setOption(options, opt.name, opt.default); } }, this); // exit if required arg isn't present - this.specs.forEach(function(opt) { + this.specs.forEach(function (opt) { if (opt.required && options[opt.name] === undefined) { - var msg = opt.name + " argument is required"; - msg = this._nocolors ? msg : chalk.red(msg); + var msg = opt.name + " argument is required"; + msg = this._nocolors ? msg : chalk.red(msg); - this.print("\n" + msg + "\n" + this.getUsage(), 1); + this.print("\n" + msg + "\n" + this.getUsage(), 1); } }, this); @@ -321,9 +305,8 @@ ArgParser.prototype = { } return options; - }, - - getUsage : function() { + } + getUsage() { if (this.command && this.command._usage) { return this.command._usage; } @@ -360,7 +343,7 @@ ArgParser.prototype = { } // assume there are no gaps in the specified pos. args - positionals.forEach(function(pos) { + positionals.forEach(function (pos) { str += " "; var posStr = pos.string; if (!posStr) { @@ -396,14 +379,14 @@ ArgParser.prototype = { } var longest = Math.max(...positionals.map(pos => pos.name.length)); - positionals.forEach(function(pos) { + positionals.forEach(function (pos) { var posStr = pos.string || pos.name; str += posStr + spaces(longest - posStr.length) + " "; if (!this._nocolors) { - str += chalk.grey(pos.help || "") + str += chalk.grey(pos.help || ""); } else { - str += (pos.help || "") + str += (pos.help || ""); } str += "\n"; }, this); @@ -422,13 +405,13 @@ ArgParser.prototype = { longest = Math.max(...options.map(opt => opt.string.length)); - options.forEach(function(opt) { + options.forEach(function (opt) { if (!opt.hidden) { str += " " + opt.string + spaces(longest - opt.string.length) + " "; var defaults = (opt.default != null ? " [" + opt.default + "]" : ""); var help = opt.help ? opt.help + defaults : ""; - str += this._nocolors ? help: chalk.grey(help); + str += this._nocolors ? help : chalk.grey(help); str += "\n"; } @@ -440,58 +423,59 @@ ArgParser.prototype = { } 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) { - // get the specified opt for this parsed arg - var match = Opt({}); - this.specs.forEach(function(opt) { - if (opt.matches(arg)) { - match = opt; + if (typeof message == "string") { + this.print(message, 1); + } } - }); - return match; -}; -ArgParser.prototype.setOption = function(options, arg, value) { - var option = this.opt(arg); - if (option.callback) { - var message = option.callback(value); - - if (typeof message == "string") { - this.print(message, 1); + if (option.type != "string") { + try { + // infer type by JSON parsing the string + value = JSON.parse(value); + } + catch (e) { } } - } - if (option.type != "string") { - try { - // infer type by JSON parsing the string - value = JSON.parse(value) - } - catch(e) {} - } + if (option.transform) { + value = option.transform(value); + } - if (option.transform) { - value = option.transform(value); - } + var name = option.name || arg; + 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.choices && option.choices.indexOf(value) == -1) { - this.print(name + " must be one of: " + option.choices.join(", "), 1); - } - - if (option.list) { - if (!options[name]) { - options[name] = [value]; + if (option.list) { + if (!options[name]) { + options[name] = [value]; + } + else { + options[name].push(value); + } } 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 @@ -585,4 +569,4 @@ var createParser = function() { return new ArgParser(); } -module.exports = createParser(); +export default createParser(); diff --git a/libs/util.js b/libs/util.js index 499ac54..54156a4 100644 --- a/libs/util.js +++ b/libs/util.js @@ -1,45 +1,7 @@ '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); - -// 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); - } - }; -} \ No newline at end of file +export const wait = ms => () => delay(ms); diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index b6977d2..87c5489 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -9,7 +9,8 @@ "version": "3.5.0", "license": "MIT", "dependencies": { - "chalk": "^4.1.2", + "chalk": "~5.1.2", + "chalk-template": "^0.4.0", "fonteditor-core": "2.1.10", "pdf-lib": "1.17.1", "puppeteer": "18.2.1", @@ -162,6 +163,31 @@ } }, "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", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", @@ -775,12 +801,27 @@ "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==" }, "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.1.2.tgz", + "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": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "chalk": "^4.1.2" + }, + "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": { diff --git a/package.json b/package.json index 393216d..4747078 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "homepage": "https://github.com/astefanutti/decktape", "license": "MIT", "main": "decktape.js", + "type": "module", "bin": { "decktape": "decktape.js" }, @@ -20,7 +21,8 @@ "url": "https://github.com/astefanutti/decktape/issues" }, "dependencies": { - "chalk": "^4.1.2", + "chalk": "~5.1.2", + "chalk-template": "^0.4.0", "fonteditor-core": "2.1.10", "pdf-lib": "1.17.1", "puppeteer": "18.2.1", @@ -28,6 +30,6 @@ "urijs": "1.19.11" }, "engines": { - "node": ">=12.0.0" + "node": ">=12.20" } } diff --git a/plugins/bespoke.js b/plugins/bespoke.js index 0d99d23..f06bdb1 100644 --- a/plugins/bespoke.js +++ b/plugins/bespoke.js @@ -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 'bespoke' and provides access to the collection of deck instances via 'bespoke.decks and the most recent deck via 'bespoke.deck'.`; -exports.create = page => new Bespoke(page); +export const create = page => new Bespoke(page); class Bespoke { diff --git a/plugins/deck.js b/plugins/deck.js index 7803df3..efa9e68 100644 --- a/plugins/deck.js +++ b/plugins/deck.js @@ -1,4 +1,4 @@ -exports.create = page => new Deck(page); +export const create = page => new Deck(page); class Deck { diff --git a/plugins/dzslides.js b/plugins/dzslides.js index 5704508..85669b7 100644 --- a/plugins/dzslides.js +++ b/plugins/dzslides.js @@ -1,4 +1,4 @@ -exports.create = page => new DZSlides(page); +export const create = page => new DZSlides(page); class DZSlides { diff --git a/plugins/flowtime.js b/plugins/flowtime.js index 199206f..962064b 100644 --- a/plugins/flowtime.js +++ b/plugins/flowtime.js @@ -1,4 +1,4 @@ -exports.create = page => new Flowtime(page); +export const create = page => new Flowtime(page); class Flowtime { diff --git a/plugins/generic.js b/plugins/generic.js index d607cc2..ec1bb53 100644 --- a/plugins/generic.js +++ b/plugins/generic.js @@ -2,9 +2,9 @@ // and detects changes to the DOM. The deck is considered over when no change // is detected afterward. -const { pause } = require('../libs/util'); +import { pause } from '../libs/util.js'; -exports.options = { +export const options = { key : { default : 'ArrowRight', metavar : '', @@ -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 and iterates over the presentation as long as: - Any change to the DOM is detected by observing mutation events targeting the body element and its subtree, - 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 { constructor(page, options) { @@ -39,8 +39,8 @@ class Generic { this.options = options; this.currentSlide = 1; this.isNextSlideDetected = false; - this.key = this.options.key || exports.options.key.default; - this.media = this.options.media || exports.options.media.default; + this.key = this.options.key || options.key.default; + this.media = this.options.media || options.media.default; } getName() { diff --git a/plugins/impress.js b/plugins/impress.js index 9fac704..d0f0325 100644 --- a/plugins/impress.js +++ b/plugins/impress.js @@ -1,4 +1,4 @@ -exports.create = page => new Impress(page); +export const create = page => new Impress(page); class Impress { diff --git a/plugins/inspire.js b/plugins/inspire.js index 1ca9103..b0d72f6 100644 --- a/plugins/inspire.js +++ b/plugins/inspire.js @@ -1,4 +1,4 @@ -exports.create = page => new Inspire(page); +export const create = page => new Inspire(page); class Inspire { diff --git a/plugins/nuedeck.js b/plugins/nuedeck.js index 29dbc69..136ccbd 100644 --- a/plugins/nuedeck.js +++ b/plugins/nuedeck.js @@ -1,4 +1,4 @@ -exports.create = page => new NueDeck(page); +export const create = page => new NueDeck(page); class NueDeck { diff --git a/plugins/remark.js b/plugins/remark.js index bf8ca3a..ff7b324 100644 --- a/plugins/remark.js +++ b/plugins/remark.js @@ -1,4 +1,4 @@ -exports.create = page => new Remark(page); +export const create = page => new Remark(page); class Remark { diff --git a/plugins/reveal.js b/plugins/reveal.js index 3cc7184..8e30757 100644 --- a/plugins/reveal.js +++ b/plugins/reveal.js @@ -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 { diff --git a/plugins/rise.js b/plugins/rise.js index 46e64c6..4b4f124 100644 --- a/plugins/rise.js +++ b/plugins/rise.js @@ -1,4 +1,4 @@ -exports.create = page => new RISE(page); +export const create = page => new RISE(page); class RISE { diff --git a/plugins/shower-1.x.js b/plugins/shower-1.x.js index 9832191..7b0dc5b 100644 --- a/plugins/shower-1.x.js +++ b/plugins/shower-1.x.js @@ -1,4 +1,4 @@ -exports.create = page => new Shower(page); +export const create = page => new Shower(page); class Shower { diff --git a/plugins/shower-2.x.js b/plugins/shower-2.x.js index f2db071..e1085ad 100644 --- a/plugins/shower-2.x.js +++ b/plugins/shower-2.x.js @@ -1,4 +1,4 @@ -exports.create = page => new Shower(page); +export const create = page => new Shower(page); class Shower { diff --git a/plugins/slidy.js b/plugins/slidy.js index b35577f..9bdb11d 100644 --- a/plugins/slidy.js +++ b/plugins/slidy.js @@ -1,4 +1,4 @@ -exports.create = page => new Slidy(page); +export const create = page => new Slidy(page); class Slidy { diff --git a/plugins/webslides.js b/plugins/webslides.js index 9766073..9ee016b 100644 --- a/plugins/webslides.js +++ b/plugins/webslides.js @@ -1,4 +1,4 @@ -exports.create = page => new WebSlides(page); +export const create = page => new WebSlides(page); class WebSlides {