pandemics
Version:
Simplified academic writing.
365 lines (305 loc) • 12.8 kB
JavaScript
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;