UNPKG

pandemics

Version:

Simplified academic writing.

365 lines (305 loc) 12.8 kB
const fs = require('fs-extra'); const path = require('path'); const config = require('../config.js'); const getYamlOptions = require('../lib/get-yaml-options.js'); const jsYaml = require('js-yaml'); const { spawnSync } = require('child_process'); const loadPublishingEnv = require('../lib/load-publishing-env.js'); const Recipe = require('../lib/Recipe.js'); const PandocCommand = require('../lib/PandocCommand.js'); const bibZoteroAutogen = require('../lib/bib-zotero-autogen.js'); const assets = require('../lib/assets.js'); const which = require('which') const timer = require('../lib/timer.js'); const getSourceVersion = require('../lib/get-source-version.js'); function publish ({ source, target, recipe: fingerprint, format, logger = () => {} }) { return new Promise((resolve, reject) => { logger(`§§§ Begin §§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§`); try{ // Checks // ------------------------------------------------------------------------- logger ('identifying files...') timer.start () // check that source exists if (! fs.existsSync (source)) { throw new Error (`Could not find the source file ${source}.`); } const sourceDir = path.dirname (source); // create target directory if necessary target = target ? path.resolve (process.cwd (), target) : path.join( path.dirname (source), config.TARGET_PATH, path.basename(source, '.md') ); const targetDir = path.dirname (target); if (! fs.existsSync (targetDir)){ try { fs.ensureDirSync (targetDir); } catch (err) { throw new Error (`Could not create target directory (${err.message}).`); } } // create cache folder to store the recipe if (! fs.existsSync (config.RECIPES_PATH)){ try { fs.ensureDirSync (config.RECIPES_PATH); } catch (err) { throw new Error(`Could not create cache directory (${err.message}).`); } } // verbose logger (`source-file: ${source}`); logger (`target-directory: ${targetDir}`); logger (`cache-directory: ${config.RECIPES_PATH}`); logger(`done in ${timer.stop ()}ms.`) // Options and instructions // ------------------------------------------------------------------------- logger (`parsing metadata...`) timer.start (); // if no options provided, check yaml header var {frontMatter, content} = getYamlOptions (source); if (!frontMatter["pandemics"]) { frontMatter["pandemics"] = {}; } if (!frontMatter["header-includes"]) { frontMatter["header-includes"] = ""; } logger (`done in ${timer.stop ()}ms.`) logger (`loading recipe...`) timer.start () // load recipe manager const recipe = new Recipe ( fingerprint || frontMatter.pandemics.recipe, format || frontMatter.pandemics.format ); logger (`YAML-header: ${JSON.stringify (frontMatter, null, 2)}`); logger (recipe.toString ()); logger (`done in ${timer.stop ()}ms.`) /* ####################################################################### * * PREPARE PANDOC COMMAND * ####################################################################### */ logger (`prepare pandoc command:`) // start from the beginning let command = new PandocCommand(); // target file if (path.extname (target) !== `.${recipe.instructions.extension}`) { target += '.' + recipe.instructions.extension } command.pushOption('output', target); command.pushOption('from', 'markdown+rebase_relative_paths'); // BIBLIOGRAPHY // ========================================================================= logger (`setup bibliography...`) timer.start () // Make list of bibliography files // ------------------------------------------------------------------------- let bibliography; // use bibligraphy file(s) from the yaml header, if provided // ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' if (frontMatter.bibliography) { // ensure it's an array bibliography = [].concat (frontMatter.bibliography); // otherwise, list all candidates in the source folder // ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' } else { bibliography = fs .readdirSync(sourceDir) .filter(f => ['.bib','.biblatex'].includes(path.extname(f))) } // Extend using zotero library if autobib flag set // ------------------------------------------------------------------------- if (frontMatter.pandemics.autobib) { // if no .bib file yet, use a default if (bibliography.length == 0) { bibliography = ['bibliography.bib'] } // expand first .bib file using zotero // <TODO>: check it's not a URL? bibZoteroAutogen (source, bibliography[0], logger); } // Add all bib resources to the pandoc command // ------------------------------------------------------------------------- bibliography.forEach (bibFile => { if (bibFile.startsWith ('http')) { command.pushBibliography (bibFile); } else { command.pushBibliography (path.resolve (sourceDir, bibFile)); } }); logger (`done in ${timer.stop ()}ms.`) // TEMPLATE // ========================================================================= logger (`look for template...`) timer.start () // use template if (recipe.instructions.template) { if (['doc', 'docx', 'odt'].includes (recipe.instructions.extension)) { command.pushOption ('reference-doc', recipe.instructions.template); } else { command.pushOption ('template', recipe.instructions.template); } } // ensure pandemicsIncludeError env is defined logger (`done in ${timer.stop ()}ms.`) // OPTIONS // ========================================================================= logger (`parse pandoc options...`) timer.start () // add pandoc options let options = recipe.instructions.options || []; if (recipe.useLatex ()) { // add first to ensure the error env is defined before recipe redefinition options.unshift (`--include-in-header="${assets.file['error-env.tex']}"`) } if (recipe.instructions.options) { // any "include-in-header" option will overwrite the header-includes meta // which needs to be left untouched for pandoc-crossref to work. // as a work around, we directly write the content in the yaml header... rHeaderOpt = /^\s*(-H\s+|--include-in-header=)(?<q>")?(?<filename>.+)\k<q>\s*$/ recipe.instructions.options.forEach (opt => { if (res = rHeaderOpt.exec(opt)) { let headerFile = path.resolve ( path.join (config.RECIPES_PATH, recipe.path), res.groups.filename ); headerContent = fs.readFileSync (headerFile) frontMatter["header-includes"] += headerContent.toString().trim() + `\n`; } else { command.pushString(opt); } }); } // include source directory in search path (allow relative path to images) command.pushOption ('resource-path', sourceDir); // pass on source directory to complement Pandoc structural var 'curdir' command.pushVariable ('sourcedir', sourceDir + path.sep); // add correct pdf engine if (recipe.instructions.extension == 'pdf') { var pdf_engine if ( recipe.instructions.template && path.extname (recipe.instructions.template) === '.xelatex' ) { pdf_engine = which.sync ('xelatex'); } else { pdf_engine = which.sync ('pdflatex'); } command.pushOption ('pdf-engine', pdf_engine); } // title, mandatory for html const pagetitle = frontMatter.title || path.basename (source, '.md'); command.pushOption ('metadata', 'pagetitle', pagetitle.replace (/"/g, '\\\"')); // verbose for debugging command.pushOption ('verbose'); logger (`done in ${timer.stop ()}ms.`) // VERSION // ========================================================================= logger (`generate source version...`) timer.start () // try and get commit sha of the source var sourceSHA = getSourceVersion (sourceDir); // pass SHA as pandoc variable command.pushVariable ('sourceSHA', sourceSHA); // for latex, create macro definition \theversion if (recipe.useLatex ()) { frontMatter["header-includes"] += `\\def\\theversion{${sourceSHA || ''}}\n`; } logger (`done in ${timer.stop ()}ms.`) // FILTERS // ========================================================================= logger (`list filters...`) timer.start () // Make list of filters // ------------------------------------------------------------------------- const defaultFilters = [ assets.bin['pandoc-crossref'] ]; const filters = recipe.instructions.filters || defaultFilters; // convert pandemics error fenced divs to dedicated latex env if (recipe.useLatex ()) { filters.push (assets.file['latex-div.lua']); } // add filters to command // ------------------------------------------------------------------------- filters.forEach ((filter) => { // if name of local binaries requested, eg crossref, use local path filter = assets.bin[filter] || filter command.pushFilter (filter) }); // put after the filters to avoid conflicts with crossref command.pushOption ('citeproc'); logger (`done in ${timer.stop ()}ms.`) // LOG // ========================================================================= logger (`pandoc-command: ${command}`); /* ####################################################################### * * SET PROCESSING PIPELINE * ####################################################################### */ // start with preprocessing const defaultPreprocessing = [ 'pandemics-include', // before to avoid mustache parsing the inlcudes 'pandemics-mustache' ]; const pipeline = recipe.instructions.preprocessing || defaultPreprocessing; // finally, call to pandoc pipeline.push (command.toString ()); /* ####################################################################### * * COMPILE * ####################################################################### */ // load filters, binaries, and pass on path info to preprocessing const publishingEnv = loadPublishingEnv (); publishingEnv.PANDOC_SOURCE_PATH = path.dirname (source); publishingEnv.PANDOC_TARGET_PATH = path.dirname (target); publishingEnv.PANDOC_RECIPE_PATH = recipe.fullPath; logger(`environment: ${JSON.stringify (publishingEnv, null, 2)}`); // start with reading the raw source document let output = `---\n` + jsYaml.dump(frontMatter) + `---\n` + content; // for each processin step pipeline.forEach ((processor) => { logger (`§§§ processing step §§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§\n`); logger (`°°° command °°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°\n`); logger (processor); // apply processor timer.start (); const child = spawnSync ( 'cross-env', processor.split (' '), { input: output, cwd: recipe.fullPath, env: publishingEnv, shell: true, maxBuffer: 1024 * 1024 * 10 } ); // set output as new input output = child.stdout; logger (`\n°°° logs °°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°\n`); logger (child.stderr.toString('utf8')); logger (`done in ${timer.stop ()}ms.\n`) logger (`°°° output °°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°\n`); logger (output.toString('utf8')); if (child.status) { throw new Error(`Failed to compile document when executing: \n ${processor} \n ${child.stderr.toString('utf8')}`); } }); // all went well logger (`§§§ Done §§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§\n`); return resolve (); } catch (err) { logger (`§§§ Failed §§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§\n`); return reject (err); } }); }; module.exports = publish;