[
  {
    "path": ".github/workflows/npmpublish.yml",
    "content": "name: Node.js Package\n\non:\n  release:\n    types: [created]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v1\n      - uses: actions/setup-node@v1\n        with:\n          node-version: 12\n      - run: npm ci\n      - run: npm test\n\n  publish-npm:\n    needs: build\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v1\n      - uses: actions/setup-node@v1\n        with:\n          node-version: 12\n          registry-url: https://registry.npmjs.org/\n      - run: npm ci\n      - run: npm publish\n        env:\n          NODE_AUTH_TOKEN: ${{secrets.npm_token}}\n\n  publish-gpr:\n    needs: build\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v1\n      - uses: actions/setup-node@v1\n        with:\n          node-version: 12\n          registry-url: https://npm.pkg.github.com/\n          scope: '@your-github-username'\n      - run: npm ci\n      - run: npm publish\n        env:\n          NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}\n"
  },
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n\n# nyc test coverage\n.nyc_output\n\n# OS specific temp files\n.DS_Store\nThumbs.db\n\n# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# Typescript v1 declaration files\ntypings/\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variables file\n.env\n\n# next.js build output\n.next\n\ntest/samples/*/*/master.pdf\ntest/samples/*/*/master.png\ntest/samples/*/*/last_test_*\ntest/samples/*/*/diff.png\ntest/samples/*/*/master_temp.htm\n*.htm\n\npackage-lock.json\n\n# linux .directory\n.directory\n\n# VS code\n.vscode\n.eslintrc.*\n\n*.local.bak\n"
  },
  {
    "path": ".npmignore",
    "content": "# https://docs.npmjs.com/misc/developers#keeping-files-out-of-your-package\n\n#tests\ntest\ncoverage\n\n#build tools\n.travis.yml\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: node_js\n\nnode_js:\n  - \"lts/*\"\n  - \"stable\"\n\nbefore_install:\n  - sudo apt-get -qq update\n  - sudo apt-get install imagemagick ghostscript poppler-utils graphicsmagick\n\nbefore_script:\n  - npm link\n"
  },
  {
    "path": "LICENCE.txt",
    "content": "\nISC License (ISC)\n\nCopyright 2018, Zulko\n\nPermission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\"><img width='270px' src=\"https://github.com/RelaxedJS/ReLaXed/raw/master/logo-blue.png\" /></p>\n\n# ReLaXed\n\n[![Build Status](https://travis-ci.org/RelaxedJS/ReLaXed.svg?branch=master)](https://travis-ci.org/RelaxedJS/ReLaXed)\n\nReLaXed creates PDF documents interactively using HTML or [Pug](https://pugjs.org/api/getting-started.html) (a shorthand for HTML). It allows complex layouts to be defined with CSS and JavaScript, while writing the content in a friendly, minimal syntax close to Markdown or LaTeX.\n\nHere it is in action in the Atom editor:\n\n<p align='center'><img src=\"https://i.imgur.com/4N4fSYY.gif\" title=\"source: imgur.com\" /></p>\n\nAnd here are a few output examples:\n\n<table>\n  <tr align=\"center\">\n    <td width=\"25%\">\n      <a href=\"https://github.com/RelaxedJS/ReLaXed-examples/blob/master/examples/book/book.pdf\">\n        <img src=\"https://github.com/RelaxedJS/ReLaXed-examples/raw/master/examples/book/book_screenshot.png\" />\n      </a>\n      Book -\n      <a href=\"https://github.com/RelaxedJS/ReLaXed-examples/tree/master/examples/book/\"> source </a> /\n      <a href=\"https://github.com/RelaxedJS/ReLaXed-examples/blob/master/examples/book/book.pdf\"> PDF </a>\n    </td>\n    <td width=\"25%\">\n      <a href=\"https://github.com/RelaxedJS/ReLaXed-examples/blob/master/examples/letter/letter.pdf\">\n        <img src=\"https://github.com/RelaxedJS/ReLaXed-examples/raw/master/examples/letter/letter_screenshot.png\" />\n      </a>\n      Letter -\n      <a href=\"https://github.com/RelaxedJS/ReLaXed-examples/tree/master/examples/letter/\"> Source </a> /\n      <a href=\"https://github.com/RelaxedJS/ReLaXed-examples/blob/master/examples/letter/letter.pdf\"> PDF </a>\n    </td>\n    <td width=\"25%\">\n      <a href=\"https://github.com/RelaxedJS/ReLaXed-examples/blob/master/examples/resume/resume.pdf\">\n        <img src=\"https://github.com/RelaxedJS/ReLaXed-examples/raw/master/examples/resume/resume_screenshot.png\" />\n      </a>\n      Resume -\n      <a href=\"https://github.com/RelaxedJS/ReLaXed-examples/tree/master/examples/resume/\"> Source </a> /\n      <a href=\"https://github.com/RelaxedJS/ReLaXed-examples/blob/master/examples/resume/resume.pdf\"> PDF </a>\n    </td>\n    <td width=\"25%\">\n      <a href=\"https://github.com/RelaxedJS/ReLaXed-examples/blob/master/examples/business-card/business-card.pdf\">\n      <img src=\"https://github.com/RelaxedJS/ReLaXed-examples/raw/master/examples/business-card/businesscard_screenshot.png\" /></a>\n      Visit card -\n      <a href=\"https://github.com/RelaxedJS/ReLaXed-examples/tree/master/examples/business-card/\"> Source </a> /\n      <a href=\"https://github.com/RelaxedJS/ReLaXed-examples/blob/master/examples/business-card/business-card.pdf\"> PDF </a>\n    </td>\n  </tr>\n</table>\n<table>\n  <tr align=\"center\">\n    <td width=\"25%\">\n      <a href=\"https://github.com/RelaxedJS/ReLaXed-examples/blob/master/examples/slides/slides.pdf\">\n        <img src=\"https://github.com/RelaxedJS/ReLaXed-examples/raw/master/examples/slides/slides_screenshot.png\" />\n      </a>\n      Slides -\n      <a href=\"https://github.com/RelaxedJS/ReLaXed-examples/tree/master/examples/slides/\"> Source </a> /\n      <a href=\"https://github.com/RelaxedJS/ReLaXed-examples/blob/master/examples/slides/slides.pdf\"> PDF </a>\n    </td>\n    <td width=\"25%\">\n      <a href=\"https://github.com/RelaxedJS/ReLaXed-examples/blob/master/examples/report/report.pdf\">\n        <img src=\"https://github.com/RelaxedJS/ReLaXed-examples/raw/master/examples/report/report_screenshot.png\" />\n      </a>\n      Report -\n      <a href=\"https://github.com/RelaxedJS/ReLaXed-examples/tree/master/examples/report/\"> Source </a> /\n      <a href=\"https://github.com/RelaxedJS/ReLaXed-examples/blob/master/examples/report/report.pdf\"> PDF </a>\n    </td>\n    <td width=\"25%\">\n       <a href=\"https://github.com/RelaxedJS/ReLaXed-examples/blob/master/examples/paper/paper.pdf\">\n         <img src=\"https://github.com/RelaxedJS/ReLaXed-examples/raw/master/examples/paper/paper_screenshot.png\" />\n       </a>\n       Paper -\n       <a href=\"https://github.com/RelaxedJS/ReLaXed-examples/tree/master/examples/paper/\"> Source </a> /\n       <a href=\"https://github.com/RelaxedJS/ReLaXed-examples/blob/master/examples/paper/paper.pdf\"> PDF </a>\n     </td>\n   <td width=\"25%\">\n     <a href=\"https://github.com/RelaxedJS/ReLaXed-examples/blob/master/examples/poster/poster.pdf\">\n       <img src=\"https://github.com/RelaxedJS/ReLaXed-examples/raw/master/examples/poster/poster_screenshot.png\" />\n     </a>\n     Poster -\n     <a href=\"https://github.com/RelaxedJS/ReLaXed-examples/tree/master/examples/poster/\"> Source </a> /\n     <a href=\"https://github.com/RelaxedJS/ReLaXed-examples/blob/master/examples/poster/poster.pdf\"> PDF </a>\n   </td>\n  </tr>\n</table>\n\nReLaXed has support for Markdown, LaTeX-style mathematical equations (via [MathJax](https://www.mathjax.org/)), CSV conversion to HTML tables, plot generation (via [Vega-Lite](https://vega.github.io/vega-lite/) or [Chart.js](https://www.chartjs.org/)), and diagram generation (via [mermaid](https://mermaidjs.github.io/)). Many more features can be added simply by importing an existing JavaScript or CSS framework.\n\n## Installing ReLaXed\n\nInstall ReLaXed via [NPM](https://www.npmjs.com/) with this command (do not use ``sudo``):\n\n```\nnpm i -g relaxedjs\n```\n\nThis will provide your system with the ``relaxed`` command. If the installation fails, refer to the [troubleshooting page](https://github.com/RelaxedJS/ReLaXed/wiki/Troubleshooting). You can also use ReLaXed via Docker (see [this repository](https://github.com/jonathanasquier/ReLaXed-docker/blob/master/Dockerfile))\n\n\n## Getting started\n\nTo start a project, create a new document ``my_document.pug`` with the following Pug content:\n\n```pug\nh1 My document's title\np A paragraph in my document\n```\n\nThen start ReLaXed from a terminal:\n\n```\nrelaxed my_document.pug\n```\n\nReLaXed will generate ``my_document.pdf`` from ``my_document.pug``, then watch its directory and subdirectories so that every time a file changes, ``my_document.pdf`` will be re-generated.\n\nIt is also possible to generate the PDF file just once, with no sub-sequent file-watching, with this command:\n\n```\nrelaxed my_document.pug --build-once\n```\n\nTo go further:\n\n- Read more about [usage and options](https://github.com/RelaxedJS/ReLaXed/wiki/Command-line-options) of the ``relaxed`` command.\n- Learn more about the capabilities of the [Pug language](https://pugjs.org/api/getting-started.html)\n- Learn how to use or write [ReLaXed plugins](https://github.com/RelaxedJS/ReLaXed/wiki/Plugins)\n- Browse the [examples](https://github.com/RelaxedJS/ReLaXed-examples)\n- Read about our [recommended setup](https://github.com/RelaxedJS/ReLaXed/wiki/Tips-and-recommendations) to use ReLaXed\n- read about [special file rendering](https://github.com/RelaxedJS/ReLaXed/wiki/Special-file-renderings) in ReLaxed\n- Read [these comparisons](https://github.com/RelaxedJS/ReLaXed/wiki/ReLaXed-vs-other-solutions) between ReLaXed and other document-editing systems\n\n## Why yet another PDF document creator?\n\nMany of us prefer markup languages (Markdown, LaTeX, etc.) to GUI document-editors like MS Office or Google Docs. This is because markup languages make it easier to quickly write documents in a consistent style.\n\nHowever, Markdown is limited to the title/sections/paragraphs structure, and LaTeX has obscure syntax and errors that also make it difficult to stray from the beaten track.\n\nOn the other hand, web technologies have never looked so good.\n\n- Beautiful CSS frameworks make sure your documents look clean and modern.\n- There are JavaScript libraries for pretty much anything: plotting, highlight code, rendering equations...\n- Millions of people (and growing) know how to use these.\n- Shorthand languages like Pug and SCSS are finally making it fun to write HTML and CSS.\n- (Headless) web browsers can easily turn web documents into PDF, on any platform.\n\nReLaXed is an attempt at finding the most comfortable way to leverage this for desktop PDF creation.\n\n## How ReLaXed works\n\nReLaXed consists of a few lines of code binding together other software. It uses [Chokidar](https://github.com/paulmillr/chokidar) to watch the file system. When a file is changed, several JavaScript libraries are used to compile SCSS, Pug, Markdown, and diagram files (mermaid, flowchart.js, Chart.js) into an HTML page which is then printed to a PDF file by a headless instance of Chromium (via [Puppeteer](https://github.com/GoogleChrome/puppeteer)).\n\n<p align=\"center\"><img width='600px' src=\"https://github.com/RelaxedJS/ReLaXed/raw/master/docs/relaxed_stack.png\" /></p>\n\n## Using it as a Node Module\n**MasterToPDF.js** is exposed by default as main package, which can be used directly.\n\nAn Example: \n\n```javascript\nconst { masterToPDF } = require('relaxedjs');\nconst puppeteer = require('puppeteer');\nconst plugins = require('relaxedjs/src/plugins');\nconst path = require('path');\n\nclass HTML2PDF {\n  constructor() {\n    this.puppeteerConfig = {\n      headless: true,\n      args: [\n        '--no-sandbox',\n        '--disable-translate',\n        '--disable-extensions',\n        '--disable-sync',\n      ],\n    };\n\n    this.relaxedGlobals = {\n      busy: false,\n      config: {},\n      configPlugins: [],\n    };\n\n    this._initializedPlugins = false;\n  }\n\n  async _initializePlugins() {\n    if (this._initializedPlugins) return; // Do not initialize plugins twice\n    for (const [i, plugin] of plugins.builtinDefaultPlugins.entries()) {\n      plugins.builtinDefaultPlugins[i] = await plugin.constructor();\n    }\n    await plugins.updateRegisteredPlugins(this.relaxedGlobals, '/');\n\n    const chrome = await puppeteer.launch(this.puppeteerConfig);\n    this.relaxedGlobals.puppeteerPage = await chrome.newPage();\n    this._initializedPlugins = true;\n  }\n\n  async pdf(templatePath, json_data, tempHtmlPath, outputPdfPath) {\n    await this._initializePlugins();\n    if (this._initializedPlugins) {\n      // Paths must be absolute\n      const defaultTempHtmlPath = tempHtmlPath || path.resolve('temp.html');\n      const defaultOutputPdfPath =\n        outputPdfPath || path.resolve('output.pdf');\n\n      await masterToPDF(\n        templatePath,\n        this.relaxedGlobals,\n        defaultTempHtmlPath,\n        defaultOutputPdfPath,\n        json_data\n      );\n    }\n  }\n}\n\nmodule.exports = new HTML2PDF();\n```\nUsage:\n\n```javascript\nconst HTML2PDF = require('./HTML2PDF.js');\n(async () => {\n    await HTML2PDF.pdf('./template.pug', {\"a\":\"b\", \"c\":\"d\"});\n})();\n```\n\n\n## Contribute!\n\nReLaXed is an open-source framework originally written by [Zulko](https://github.com/Zulko) and released on [Github](https://github.com/RelaxedJS/ReLaXed) under the ISC licence. Everyone is welcome to contribute!\n\nFor bugs and feature requests, open a Github issue. For support or Pug/HTML-related questions, ask on Stackoverflow or on the brand new [reddit/r/relaxedjs](https://www.reddit.com/r/relaxedjs/) forum, which can be used for any kind of discussion.\n\n**Projects members:**\n\n- [@Zulko](https://github.com/Zulko) (Owner)\n- [@Drew-S](https://github.com/Drew-S) (architecture, plugins)\n- [@DanielRuf](https://github.com/DanielRuf)\n- [@benperiton](https://github.com/benperiton)\n\n## License\n\n[ISC](https://github.com/RelaxedJS/ReLaXed/blob/master/LICENCE.txt)\n"
  },
  {
    "path": "changelog.md",
    "content": "# Changelog\n\n## v0.1.6\n\nFeatures:\n\n- Now exposing ``require`` for use in in-Pug javascript\n- Bibliography system, built-in.\n- Shorthand ``--bo`` for ``--build-once``\n\nFixes:\n\n- fixed typo breaking special converters (mermaid, etc.)\n- faster PDF rendering by using chromium's DOM instead of cheerio.\n- better (faster) stepSVG mixin\n\nInternal:\n\n- Lots of code reorganization, mostly by @Drew-S, and marks for future plugins.\n- A test suite ! But not for interactive usage yet.\n- Added project members to readme.\n\n## v 0.1.5\n\n- Some console output fixes\n- Now avoiding new renderings when already busy.\n- New \"built-in\" mixin ``stepsSVG`` for including progressive\n  (i.e. animated) SVGs into slides\n- New command-line utility ``pretty-pdf-thumbnail`` shipped with ReLaXed.\n\n## v 0.1.4\n\nImportant release with speed and features improvements.\n\nBreaking API changes:\n\n- Now using ``template#page-footer``, ``template#page-header`` to define\n  page header and footer.\n- MathJax de-activated by default\n\nNew features:\n\n- Command-line parameter ``--build-once`` for one-time builds\n- New exposed javascript globals in templates:\n  - Packages: ``fs``, ``cheerio``\n  - Variables: ``basedir`` (indicating the base path of the master file)\n- Experimental Katex filter now available\n- Files with extension ``.o.svg`` are automatically converted to optimized\n  svgs (``*_optimized.svg``).\n- Console now shows a breakdown of rendering time.\n\nOther changes in the code:\n\n- First test suite !!\n- Removed some jstransformer dependencies.\n- Refactoring with utils.py\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"relaxedjs\",\n  \"version\": \"0.2.6\",\n  \"description\": \"Create PDF documents using web technologies (PDF/SCSS)\",\n  \"main\": \"src/masterToPDF.js\",\n  \"bin\": {\n    \"relaxed\": \"src/index.js\",\n    \"pretty-pdf-thumbnail\": \"src/cli-tools/pretty-pdf-thumbnail.js\",\n    \"pdf2gif\": \"src/cli-tools/pdf2gif.js\"\n  },\n  \"scripts\": {\n    \"test\": \"mocha\"\n  },\n  \"author\": \"Zulko\",\n  \"homepage\": \"https://github.com/RelaxedJS\",\n  \"license\": \"ISC\",\n  \"dependencies\": {\n    \"@iktakahiro/markdown-it-katex\": \"^4.0.1\",\n    \"cheerio\": \"^1.1.2\",\n    \"chokidar\": \"^4.0.3\",\n    \"colors\": \"^1.4.0\",\n    \"commander\": \"^14.0.0\",\n    \"csvtojson\": \"^2.0.10\",\n    \"deptree\": \"^1.0.0\",\n    \"filesize\": \"^11.0.2\",\n    \"html2jade\": \"^0.8.6\",\n    \"jimp\": \"^1.6.0\",\n    \"js-yaml\": \"^4.1.0\",\n    \"jstransformer-highlight\": \"^2.0.0\",\n    \"jstransformer-markdown-it\": \"^3.0.0\",\n    \"katex\": \"^0.16.22\",\n    \"markdown-it\": \"^14.1.0\",\n    \"markdown-it-footnote\": \"^4.0.0\",\n    \"mathjax-node-page\": \"^3.2.0\",\n    \"mermaid\": \"^11.9.0\",\n    \"pug\": \"^3.0.3\",\n    \"puppeteer\": \"^24.15.0\",\n    \"sass\": \"^1.89.2\"\n  },\n  \"devDependencies\": {\n    \"diff\": \"^8.0.2\",\n    \"mocha\": \"^11.7.1\",\n    \"pdf-image\": \"^2.0.0\",\n    \"pixel-diff\": \"^1.0.1\"\n  }\n}\n"
  },
  {
    "path": "src/builtin_plugins/chartjs/index.js",
    "content": "const pug = require('pug')\nconst fs = require('fs')\nconst path = require('path')\n\nexports.constructor = async function (params) {\n  return {\n    watchers: [\n      {\n        extensions: ['.chart.js'],\n        handler: chartjsHandler\n      }\n    ]\n  }\n}\n\n\nvar chartjsHandler = async function (chartjsPath, page) {\n  var chartSpec = fs.readFileSync(chartjsPath, 'utf8')\n  var html = pug.renderFile(path.join(__dirname, 'template.pug'), { chartSpec })\n  var tempHTML = chartjsPath + '.htm'\n\n  fs.writeFileSync(tempHTML, html)\n  await page.setContent(html)\n  await page.waitForFunction(() => window.pngData)\n\n  const dataUrl = await page.evaluate(() => window.pngData)\n  const { buffer } = parseDataUrl(dataUrl)\n  var pngPath = chartjsPath.substr(0, chartjsPath.length - '.chart.js'.length) + '.png'\n  fs.writeFileSync(pngPath, buffer, 'base64')\n}\n// Scrape (pull) images from the web\n// from https://intoli.com/blog/saving-images/\nvar parseDataUrl = function (dataUrl) {\n  const matches = dataUrl.match(/^data:(.+);base64,(.+)$/)\n  if (matches.length !== 3) {\n    throw new Error('Could not parse data URL.')\n  }\n  return {\n    mime: matches[1],\n    buffer: Buffer.from(matches[2], 'base64')\n  }\n}\n"
  },
  {
    "path": "src/builtin_plugins/chartjs/template.pug",
    "content": "script(src='https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.2/Chart.min.js')\n\n#chartContainer\n  canvas#myChart\nscript.\n  window.pngData = false\n  var canvas = document.getElementById(\"myChart\")\n  var container = document.getElementById(\"chartContainer\")\n  function onComplete () {\n    window.pngData = canvas.toDataURL();\n  }\n  var config=!{chartSpec};\n  config.options.animation = {duration: 0, onComplete: onComplete}\n  if (config.options.width) {\n    canvas.style.width = config.options.width\n    container.style.width = config.options.width\n  }\n  if (config.options.height) {\n    canvas.style.height = config.options.height\n    container.style.height = config.options.height\n  }\n  \n  var ctx = document.getElementById(\"myChart\").getContext('2d');\n  var myChart = new Chart(ctx, config)\n"
  },
  {
    "path": "src/builtin_plugins/flowchart/index.js",
    "content": "const pug = require('pug')\nconst fs = require('fs')\nconst path = require('path')\n\nexports.constructor = async function (params) {\n  return {\n    watchers: [\n      {\n        extensions: ['.flowchart', '.flowchart.json'],\n        handler: flowchartHandler\n      }\n    ]\n  }\n}\n\nvar flowchartHandler = async function (flowchartPath, page) {\n  if (flowchartPath.endsWith('.flowchart.json')) {\n    flowchartPath = flowchartPath.substr(0, flowchartPath.length - 5)\n  }\n  var flowchartSpec = fs.readFileSync(flowchartPath, 'utf8')\n  var flowchartConf = '{}'\n\n  var possibleConfs = [\n    path.join(path.resolve(flowchartPath, '..'), 'flowchart.default.json'),\n    flowchartPath + '.json'\n  ]\n\n  for (var myPath of possibleConfs) {\n    if (fs.existsSync(myPath)) {\n      flowchartConf = fs.readFileSync(myPath, 'utf8')\n    }\n  }\n\n  var html = pug.renderFile(path.join(__dirname, 'template.pug'), {\n    flowchartSpec,\n    flowchartConf\n  })\n\n  await page.setContent(html)\n  await page.waitForSelector('#chart svg')\n\n  var svg = await page.evaluate(function () {\n    var el = document.querySelector('#chart svg')\n    el.removeAttribute('height')\n    el.removeAttribute('width')\n    el.classList.add('flowchart-svg')\n    return el.outerHTML\n  })\n  var svgPath = flowchartPath.substr(0, flowchartPath.lastIndexOf('.')) + '.svg'\n  fs.writeFileSync(svgPath, svg)\n}\n"
  },
  {
    "path": "src/builtin_plugins/flowchart/template.pug",
    "content": "script(src='http://cdnjs.cloudflare.com/ajax/libs/raphael/2.2.0/raphael-min.js')\nscript(src='http://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.0/jquery.min.js')\nscript(src='http://flowchart.js.org/flowchart-latest.js')\nscript.\n  $(document).ready(function() {\n    var chart = flowchart.parse(`!{flowchartSpec}`)\n    var conf = {'symbols': []}\n    for (var c of ['start', 'end', 'operation', 'subroutine', 'condition', 'inputoutput']) {\n      conf.symbols[c] = {'class': c + '-element'}  \n    }\n    conf = Object.assign({}, conf, !{flowchartConf})\n    chart.drawSVG('chart', conf)\n  });\nbody\n  #chart\n"
  },
  {
    "path": "src/builtin_plugins/index.js",
    "content": "const chartjs = require('./chartjs')\nconst table = require('./table')\nconst mathjax = require('./mathjax')\nconst markdown = require('./markdown')\nconst vegalite = require('./vegalite')\nconst mermaid = require('./mermaid')\nconst flowchart = require('./flowchart')\nconst scss = require('./scss')\nconst katex = require('./katex')\n// THESE ARE PLUGINS THAT CAN BE LOADED VIA CONFIG.PY\n// WE WILL CERTAINLY TAKE OUT MOST OF THEM, AS SEPARATE PLUGINS\n\nexports.plugins = {\n  mathjax,\n  katex,\n  markdown\n}\n\n// THESE ARE PLUGINS ADDING NO OVERHEAD, SO SAFE TO BE USED BY DEFAULT\n// WE WILL ALSO CERTAINLY TAKE OUT MOST OF THEM, AS SEPARATE PLUGINS,\n// TO AVOID TOO MANY DEPENDENCIES\n\nexports.defaultPlugins = [\n  table,\n  chartjs,\n  vegalite,\n  flowchart,\n  mermaid,\n  scss,\n  markdown\n]\n"
  },
  {
    "path": "src/builtin_plugins/katex/head.html",
    "content": "<link\n  rel=\"stylesheet\"\n  href=\"https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/katex.min.css\"\n  integrity=\"sha384-zB1R0rpPzHqg7Kpt0Aljp8JPLqbXI3bhnPWROx27a9N0Ll6ZP/+DiW/UqRcLbRjq\"\n  crossorigin=\"anonymous\" />\n"
  },
  {
    "path": "src/builtin_plugins/katex/index.js",
    "content": "const katex = require('katex')\nconst fs = require('fs')\nconst path = require('path')\n\nexports.constructor = async function (params) {\n  return {\n    headElements: fs.readFileSync(path.join(__dirname, 'head.html')),\n    pugFilters: {\n      katex (text, options) { return katex.renderToString(text) }\n    }\n  }\n}\n"
  },
  {
    "path": "src/builtin_plugins/markdown/index.js",
    "content": "var hljs = require('highlight.js') // https://highlightjs.org/\nvar markdown = require('markdown-it')\nvar mdFootnote = require('markdown-it-footnote')\nvar mdKatex = require('@iktakahiro/markdown-it-katex')\n\nexports.constructor = async function (params) {\n  var href = \"https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/katex.min.css\"\n  return {\n    pugFilters: { markdown: MarkdownPugFilter },\n    headElements: `<link rel=\"stylesheet\" href=\"${href}\" />`\n  }\n}\nfunction MarkdownPugFilter (text, options) {\n  var md = markdown({\n    ...options,\n    highlight: function (str, lang) {\n      if (lang && hljs.getLanguage(lang)) {\n        try {\n          return '<pre class=\"hljs\"><code>' +\n                  hljs.highlight(lang, str, true).value +\n                  '</code></pre>';\n        } catch (__) {}\n      }\n      return '<pre class=\"hljs\"><code>' + md.utils.escapeHtml(str) + '</code></pre>'\n    }\n  })\n  md.use(mdFootnote)\n  md.use(mdKatex)\n  return md.render(text)\n}\n"
  },
  {
    "path": "src/builtin_plugins/mathjax/index.js",
    "content": "const mjpage = require('mathjax-node-page')\n\n// TODO: would this work better if applied to the page instead of the HTML ?\n\nexports.constructor = async function (params) {\n  return {\n    htmlModifiers: [ asyncMathjax ]\n  }\n}\n\nvar asyncMathjax = async function (html) {\n  return new Promise(resolve => {\n    mjpage.mjpage(html, {\n      format: ['TeX']\n    }, {\n      mml: true,\n      css: true,\n      html: true\n    }, response => resolve(response))\n  })\n}\n"
  },
  {
    "path": "src/builtin_plugins/mermaid/index.js",
    "content": "const pug = require('pug')\nconst fs = require('fs')\nconst path = require('path')\n\nexports.constructor = async function (params) {\n  return {\n    watchers: [\n      {\n        extensions: ['.mermaid'],\n        handler: mermaidHandler\n      }\n    ]\n  }\n}\n\nvar mermaidHandler = async function (mermaidPath, page) {\n  var mermaidSpec = fs.readFileSync(mermaidPath, 'utf8')\n  var html = pug.renderFile(path.join(__dirname, 'template.pug'), {\n    mermaidSpec\n  })\n\n  await page.setContent(html)\n  await page.waitForSelector('#graph svg')\n\n  var svg = await page.evaluate(function () {\n    var el = document.querySelector('#graph svg')\n    el.removeAttribute('height')\n    el.classList.add('mermaid-svg')\n    return el.outerHTML\n  })\n\n  var svgPath = mermaidPath.substr(0, mermaidPath.lastIndexOf('.')) + '.svg'\n\n  fs.writeFileSync(svgPath, svg)\n}\n"
  },
  {
    "path": "src/builtin_plugins/mermaid/template.pug",
    "content": "script(src=\"https://cdnjs.cloudflare.com/ajax/libs/mermaid/7.1.2/mermaid.min.js\")\n#graph.mermaid #{mermaidSpec}\n"
  },
  {
    "path": "src/builtin_plugins/scss/index.js",
    "content": "const sass = require('sass')\n\nexports.constructor = async function (params) {\n  return {\n    pugFilters: { scss: ScssPugFilter }\n  }\n}\n\nfunction ScssPugFilter (text, options) {\n  if (options.filename.endsWith('scss'))\n    return sass.compile(options.filename).css\n  return sass.compileString(text).css\n}\n"
  },
  {
    "path": "src/builtin_plugins/table/index.js",
    "content": "\nconst pug = require('pug')\nconst fs = require('fs')\nconst path = require('path')\nconst csv = require('csvtojson')\nvar markdown = require('markdown-it')\nconst html2jade = require('html2jade')\n\nexports.constructor = async function (params) {\n  return {\n    watchers: [\n      {\n        extensions: ['.table.csv', '.htable.csv', '.htable.md.csv'],\n        async handler (tablePath, page) {\n          await csvTtableToPug(tablePath)\n        }\n      }\n    ]\n  }\n}\n\nvar csvTtableToPug = async function (tablePath) {\n  const rows = await csv({output: 'csv', noheader: true, delimiter: 'auto'}).fromFile(tablePath)\n  var isMarkdown = tablePath.includes('.md.')\n  if (isMarkdown) {\n    console.log('ah')\n    md = markdown({html: true})\n    rows.forEach(row => {\n      row.forEach((data, i) => {\n        row[i] = md.render(data)\n      })\n    })\n  }\n  var extension, header\n  if (tablePath.endsWith('.htable.csv')) {\n    extension = '.htable.csv'\n    header = rows.shift()\n  } else {\n    extension = '.table.csv'\n    header = null\n  }\n  var html = await pug.renderFile(path.join(__dirname, 'template.pug'), {\n    header: header,\n    tbody: rows\n  })\n  var pugPath = tablePath.substr(0, tablePath.length - extension.length - 3*isMarkdown) + '.pug'\n  var jade = await new Promise(resolve => {\n    html2jade.convertHtml(html, { bodyless: true }, function (err, jade) {\n      if (err) {\n        console.log('Error:', err)\n        resolve(false)\n      } else {\n        resolve(jade)\n      }\n    })\n  })\n  fs.writeFileSync(pugPath, jade)\n}\n"
  },
  {
    "path": "src/builtin_plugins/table/template.pug",
    "content": "if header\n  thead\n    for th in header\n      th!= th\ntbody\n  for tr in tbody\n    tr\n      for td in tr\n        td!= td\n"
  },
  {
    "path": "src/builtin_plugins/vegalite/index.js",
    "content": "const pug = require('pug')\nconst fs = require('fs')\nconst path = require('path')\n\nexports.constructor = async function (params) {\n  return {\n    watchers: [\n      {\n        extensions: ['.vegalite.json'],\n        handler: vegaliteHandler\n      }\n    ]\n  }\n}\n\nvar vegaliteHandler = async function (vegalitePath, page) {\n  var vegaliteSpec = fs.readFileSync(vegalitePath, 'utf8')\n  var html = pug.renderFile(path.join(__dirname, 'template.pug'), { vegaliteSpec })\n\n  await page.setContent(html)\n  await page.waitForSelector('#vis svg')\n\n  var svg = await page.evaluate(function () {\n    var el = document.querySelector('#vis svg')\n\n    el.removeAttribute('height')\n    el.removeAttribute('width')\n\n    return el.outerHTML\n  })\n\n  var svgPath = vegalitePath.substr(0, vegalitePath.length - '.vegalite.json'.length) + '.svg'\n  fs.writeFileSync(svgPath, svg)\n}\n"
  },
  {
    "path": "src/builtin_plugins/vegalite/template.pug",
    "content": "script(src='https://cdn.jsdelivr.net/npm/vega@5.21.0')\nscript(src='https://cdn.jsdelivr.net/npm/vega-lite@5.2.0')\nscript(src='https://cdn.jsdelivr.net/npm/vega-embed@6.20.2')\n#vis\nscript(type='text/javascript').\n  vegaEmbed('#vis', !{vegaliteSpec}, {'renderer': 'svg'});\n"
  },
  {
    "path": "src/cli-tools/pdf2gif.js",
    "content": "#!/usr/bin/env node\n\nconst colors = require('colors')\nconst program = require('commander')\nconst path = require('path')\nconst { spawn } = require('child_process')\nconst version = require('../../package.json').version\n\nvar input, output\n\nprogram\n  .version('From ReLaXed ' + version)\n  .usage('<input> [output] [options]')\n  .arguments('<input> [output] [options]')\n  .option('--width, -w', 'width in pixels')\n  .option('--delay, -d', 'delay between frames')\n  .option('--colors, -c', 'number of colors')\n  .action(function (inp, out) {\n    input = inp\n    output = out\n  })\nprogram.parse(process.argv)\noutput = output || (input.slice(0, input.length - 4) + '.gif')\nvar width = (program.width || 400).toString()\nvar delay = (100 * (program.delay || 1.0)).toString()\nvar ncolors = (program.colors || 256).toString()\nvar subprocess = spawn('convert', [\n  '-delay', delay,\n  '-resize', width,\n  '-colors', ncolors,\n  '-layers', 'optimize',\n  input,\n  output\n])\n\nsubprocess\n  .on('data', function (data) {\n    console.log(data)\n  })\n  .on('close', async function (code) {\n    if (code) {\n      console.log(code)\n    } else {\n      console.log('...done.')\n    }\n  })\n"
  },
  {
    "path": "src/cli-tools/pretty-pdf-thumbnail.js",
    "content": "#!/usr/bin/env node\n\nconst colors = require('colors')\nconst program = require('commander')\nconst path = require('path')\nconst { spawn } = require('child_process')\nconst version = require('../../package.json').version\n\nvar input, output\n\nprogram\n  .version('From ReLaXed ' + version)\n  .usage('<input> [output] [options]')\n  .arguments('<input> [output] [options]')\n  .option('--width, -w', 'width in pixels')\n  .option('--shadow, -s', 'shadow size in pixels', [])\n  .action(function (inp, out) {\n    input = inp\n    output = out\n  })\n\nprogram.parse(process.argv)\n\nprogram.shadow = program.shadow || 15\nvar subprocess = spawn('convert', [\n  '-density', '300',\n  input + '[0]',\n  '-resize', ((program.size || 600) - 4 * program.shadow).toString(),\n  `(`, '+clone', '-background', 'black',\n  '-shadow', `${program.shadow}x${program.shadow}+1+1`, `)`,\n  '+swap',\n  '-background', 'white',\n  '-layers', 'merge',\n  '+repage',\n  output\n])\n\nsubprocess\n  .on('data', function (data) {\n    console.log(data)\n  })\n  .on('close', async function (code) {\n    if (code) {\n      console.log(code)\n    } else {\n      console.log('...done.')\n    }\n  })\n"
  },
  {
    "path": "src/index.js",
    "content": "#!/usr/bin/env node\n\nconst colors = require('colors/safe')\nconst { program } = require('commander')\nconst chokidar = require('chokidar')\nconst puppeteer = require('puppeteer')\nconst yaml = require('js-yaml')\nconst { performance } = require('perf_hooks')\nconst path = require('path')\nconst fs = require('fs')\nconst plugins = require('./plugins')\nconst { masterToPDF } = require('./masterToPDF.js')\nconst { parseLocals, isLastLocalJsonPath } = require('./parseLocals');\nconst { exec } = require('child_process')\nvar input, output\nconst version = require('../package.json').version\n\nprogram\n  .version(version)\n  .usage('<input> [output] [options]')\n  .arguments('<input> [output] [options]')\n  .option('--no-sandbox', 'disable puppeteer sandboxing')\n  .option('-w, --watch <locations>', 'Watch other locations', [])\n  .option('-t, --temp [location]', 'Directory for temp file')\n  .option('--bo, --build-once', 'Build once only, do not watch')\n  .option('-l, --locals <json>', 'Json locals for pug rendering, string or path to .json file')\n  .option('--basedir <location>', 'Base directory for absolute paths, e.g. /')\n\n  .action(function (inp, out) {\n    input = inp\n    output = out\n  })\n\n// ARGUMENTS PARSING AND SETUP\n\nprogram.parse(process.argv)\nconst options = program.opts()\n\nif (!input || fs.lstatSync(input).isDirectory()) {\n  input = autodetectMasterFile(input)\n}\n\nconst inputPath = path.resolve(input)\nconst inputDir = path.resolve(inputPath, '..')\nconst inputFilenameNoExt = path.basename(input, path.extname(input))\n\nvar configPath\nfor (var filename of ['config.yml', 'config.json']) {\n  let possiblePath = path.join(inputDir, filename)\n  if (fs.existsSync(possiblePath)) {\n    configPath = possiblePath\n  }\n}\n\n\n// Output file, path, and temp html path\nif (!output) {\n  output = path.join(inputDir, inputFilenameNoExt + '.pdf')\n}\nconst outputPath = path.resolve(output)\n\nvar tempDir\nif (options.temp) {\n  var validTempPath = fs.existsSync(options.temp) && fs.statSync(options.temp).isDirectory()\n\n  if (validTempPath) {\n    tempDir = path.resolve(options.temp)\n  } else {\n    console.error(colors.red('ReLaXed error: Could not find specified --temp directory: ' +\n      options.temp))\n    process.exit(1)\n  }\n} else {\n  tempDir = inputDir\n}\n\nconst tempHTMLPath = path.join(tempDir, inputFilenameNoExt + '_temp.htm')\n\n// Default and additional watch locations\nlet watchLocations = [inputDir]\nif (options.watch) {\n  watchLocations = watchLocations.concat(options.watch)\n}\n\nlet locals = parseLocals(options.locals, inputDir)\n\n// Google Chrome headless configuration\nconst puppeteerConfig = {\n  headless: true,\n  args: (!options.sandbox ? ['--no-sandbox'] : []).concat([\n    '--disable-translate',\n    '--disable-extensions',\n    '--disable-sync'\n  ])\n}\n\n\n/*\n * ==============================================================\n *                         MAIN\n * ==============================================================\n */\n\nconst relaxedGlobals = {\n  busy: false,\n  config: {},\n  configPlugins: [],\n  basedir: options.basedir || inputDir\n}\n\nvar updateConfig = async function () {\n  if (configPath) {\n    console.log(colors.magenta('... Reading config file'))\n    var data = fs.readFileSync(configPath, 'utf8')\n    if (configPath.endsWith('.json')) {\n      relaxedGlobals.config = JSON.parse(data)\n    } else {\n      relaxedGlobals.config = yaml.load(data)\n    }\n  }\n  await plugins.updateRegisteredPlugins(relaxedGlobals, inputDir)\n}\n\n\n\nasync function main () {\n  console.log(colors.magenta.bold('Launching ReLaXed...'))\n\n  // LOAD BUILT-IN \"ALWAYS-ON\" PLUGINS\n  for (var [i, plugin] of plugins.builtinDefaultPlugins.entries()) {\n    plugins.builtinDefaultPlugins[i] = await plugin.constructor()\n  }\n  await updateConfig()\n  const browser = await puppeteer.launch(puppeteerConfig)\n  relaxedGlobals.puppeteerPage = await browser.newPage()\n\n  relaxedGlobals.puppeteerPage.on('pageerror', function (err) {\n    console.log(colors.red('Page error: ' + err.toString()))\n  }).on('error', function (err) {\n    console.log(colors.red('Error: ' + err.toString()))\n  })\n\n  const buildError = await build(inputPath)\n\n  if (program.buildOnce) {\n    process.exit(buildError ? 1 : 0)\n  } else {\n    watch()\n  }\n}\n\n/*\n * ==============================================================\n *                         BUILD\n * ==============================================================\n */\n\nasync function build (filepath) {\n  var shortFileName = filepath.replace(inputDir, '')\n  if ((path.basename(filepath) === 'config.yml') || (filepath.endsWith('.plugin.js'))) {\n    await updateConfig()\n    return\n  }\n\n  var updatedLocals = false\n  if (isLastLocalJsonPath(filepath)) {\n    locals = parseLocals(options.locals, inputDir)\n    updatedLocals = true\n  }\n\n  var page = relaxedGlobals.puppeteerPage\n  // Ignore the call if ReLaXed is already busy processing other files.\n\n  if (!updatedLocals && !(relaxedGlobals.watchedExtensions.some(ext => filepath.endsWith(ext)))) {\n    if (!(['.pdf', '.htm'].some(ext => filepath.endsWith(ext)))) {\n      console.log(colors.grey(`No process defined for file ${shortFileName}.`))\n    }\n    return\n  }\n\n  if (relaxedGlobals.busy) {\n    console.log(colors.grey(`File ${shortFileName}: ignoring trigger, too busy.`))\n    return\n  }\n\n  console.log(colors.magenta.bold(`\\nProcessing ${shortFileName}...`))\n  relaxedGlobals.busy = true\n  var t0 = performance.now()\n\n\n  var taskPromise = null\n\n  for (var watcher of relaxedGlobals.pluginHooks.watchers) {\n    if (watcher.instance.extensions.some(ext => filepath.endsWith(ext))) {\n      taskPromise = watcher.instance.handler(filepath, page)\n      break\n    }\n  }\n  var generatingPDF = !taskPromise\n  if (generatingPDF) {\n    taskPromise = masterToPDF(inputPath, relaxedGlobals, tempHTMLPath, outputPath, locals)\n  }\n  const generateError = await taskPromise\n  var duration = ((performance.now() - t0) / 1000).toFixed(2)\n  console.log(colors.magenta.bold(`... Done in ${duration}s`))\n  if (generatingPDF && relaxedGlobals.config.after && !generateError) {\n    console.log(colors.magenta.bold(\"Running 'after' command...\"))\n    var subprocess = exec(relaxedGlobals.config.after, cwd=relaxedGlobals.basedir)\n    \n    subprocess.stdout.on('data', (data) => {\n      console.log(`after-stdout: ${data}`);\n    });\n    subprocess.stderr.on('data', (data) => {\n      console.error(`after-stderr: ${data}`);\n    });\n    var promise = new Promise(resolve => {\n      subprocess.on('close', async function (code) {\n        resolve()\n      })\n    })\n    await promise\n    console.log(colors.magenta.bold(\"...done running 'after' command.\"))\n  }\n  relaxedGlobals.busy = false\n  return generateError\n}\n\n/**\n * Watch `watchLocations` paths for changes and continuously rebuild\n *\n * @param {puppeteer.Page} page\n */\n\n/*\n * ==============================================================\n *                         WATCH\n * ==============================================================\n */\n\nfunction watch () {\n  console.log(colors.magenta(`\\nNow idle and waiting for file changes.`))\n  chokidar.watch(watchLocations, {\n    awaitWriteFinish: {\n      stabilityThreshold: 50,\n      pollInterval: 100\n    }\n  }).on('change', build)\n}\n\nfunction autodetectMasterFile (input) {\n  var dir = input || '.'\n  var files = fs.readdirSync(dir).filter((name) => name.endsWith('.pug'))\n  var filename\n  if (files.length === 1) {\n    filename = files[0]\n  } else if (files.indexOf('master.pug') >= 0) {\n    filename = 'master.pug'\n  } else {\n    var error\n    if (input) {\n      error = `Could not find a master file in the provided directory ${input}`\n    } else {\n      error = `No input provided and could not find a master file in the current directory`\n    }\n    console.log(colors.red.bold(error))\n    program.help()\n    process.exit(1)\n  }\n  return path.join(dir, filename)\n}\n\nmain()\n"
  },
  {
    "path": "src/masterToPDF.js",
    "content": "const pug = require('pug')\nconst colors = require('colors/safe')\nconst cheerio = require('cheerio')\nconst fs = require('fs')\nconst { filesize } = require('filesize')\nconst path = require('path')\nconst { performance } = require('perf_hooks')\n\n// Returns undefined if successful or an error object on failure\nexports.masterToPDF = async function (masterPath, relaxedGlobals, tempHTMLPath, outputPath, locals) {\n  var t0 = performance.now()\n  var page = relaxedGlobals.puppeteerPage\n  /*\n   *            Generate HTML\n   */\n  var pluginHooks = relaxedGlobals.pluginHooks\n  var html\n  if (masterPath.endsWith('.pug')) {\n    var pluginPugHeaders = []\n    for (var pugHeader of pluginHooks.pugHeaders) {\n      pluginPugHeaders.push(pugHeader.instance)\n    }\n    pluginPugHeaders = pluginPugHeaders.join('\\n\\n')\n\n    var pugFilters = Object.assign(...pluginHooks.pugFilters.map(o => o.instance))\n    try {\n      var masterPug = fs.readFileSync(masterPath, 'utf8')\n\n      html = pug.render(pluginPugHeaders + '\\n' + masterPug, Object.assign({}, locals ? locals : {}, {\n        filename: masterPath,\n        fs: fs,\n        basedir: relaxedGlobals.basedir,\n        cheerio: cheerio,\n        __root__: path.dirname(masterPath),\n        path: path,\n        require: require,\n        performance: performance,\n        filters: pugFilters\n      }))\n    } catch (error) {\n      console.log(error.message)\n      console.error(colors.red('There was a Pug error (see above)'))\n      return error\n    }\n  } else if (masterPath.endsWith('.html')) {\n    html = fs.readFileSync(masterPath, 'utf8')\n  }\n\n  /*\n   *            MODIFY HTML\n   */\n  var head = pluginHooks.headElements.map(e => e.instance).join(`\\n\\n`)\n  html = `\n    <!DOCTYPE html>\n    <html>\n      <head>\n        <meta charset=\"UTF-8\">\n        ${head}\n      </head>\n      <body> ${html} </body>\n    </html>`\n\n  for (var htmlModifier of pluginHooks.htmlModifiers) {\n    html = await htmlModifier.instance(html)\n  }\n\n  fs.writeFileSync(tempHTMLPath, html)\n\n  var tHTML = performance.now()\n  console.log(colors.magenta(`... HTML generated in ${((tHTML - t0) / 1000).toFixed(1)}s`))\n\n  /*\n   *            LOAD HTML\n   */\n  try {\n    await page.goto('file:' + tempHTMLPath, {\n      waitUntil: ['load', 'domcontentloaded'],\n      timeout: 1000 * (relaxedGlobals.config.pageRenderingTimeout || 30)\n    })\n  } catch(error) {\n    console.log(error.message)\n    console.error(colors.red('There was a page loading error.'))\n    if (error.message.indexOf('Timeout') > 0) {\n      console.log('Hey this looks like a timeout. Your project must be big. ' +\n                  'Increase the timeout by writing \"pageRenderingTimeout: 60\" ' +\n                  'at the top of your config.yml. Default is 30 (seconds).')\n    }\n    return error\n  }\n  \n  var tLoad = performance.now()\n  console.log(colors.magenta(`... Document loaded in ${((tLoad - tHTML) / 1000).toFixed(1)}s`))\n\n  await waitForNetworkIdle(page, 200)\n  var tNetwork = performance.now()\n  console.log(colors.magenta(`... Network idled in ${((tNetwork - tLoad) / 1000).toFixed(1)}s`))\n\n  // Get header/footer template\n  var header = await page.$eval('#page-header', element => element.innerHTML)\n    .catch(error => '')\n  var footer = await page.$eval('#page-footer', element => element.innerHTML)\n    .catch(error => '')\n\n  if (header !== '' && footer === '') {\n    footer = '<span></span>'\n  }\n  if ((footer !== '') && (header === '')) {\n    header = '<span></span>'\n  }\n  /*\n   *            Create PDF options\n   */\n  var options = {\n    path: outputPath,\n    displayHeaderFooter: !!(header || footer),\n    headerTemplate: header,\n    footerTemplate: footer,\n    printBackground: true\n  }\n\n  function getMatch (string, query) {\n    var result = string.match(query)\n    if (result) {\n      result = result[1]\n    }\n    return result\n  }\n\n  var width = getMatch(html, /-relaxed-page-width: (\\S+);/m)\n  if (width) {\n    options.width = width\n  }\n  var height = getMatch(html, /-relaxed-page-height: (\\S+);/m)\n  if (height) {\n    options.height = height\n  }\n  var size = getMatch(html, /-relaxed-page-size: (\\S+);/m)\n  if (size) {\n    options.size = size\n  }\n\n  for (var pageModifier of pluginHooks.pageModifiers) {\n    await pageModifier.instance(page)\n  }\n\n  for (pageModifier of pluginHooks.page2ndModifiers) {\n    await pageModifier.instance(page)\n  }\n\n  // TODO: add option to output full html from page\n\n  /*\n   *            PRINT PAGE TO PDF\n   */\n  await page.pdf(options)\n\n  var tPDF = performance.now()\n  let duration = ((tPDF - tNetwork) / 1000).toFixed(1)\n  let pdfSize = filesize(fs.statSync(outputPath).size)\n  console.log(colors.magenta(`... PDF written in ${duration}s (${pdfSize})`))\n}\n\n// Wait for all the content on the page to finish loading\nfunction waitForNetworkIdle (page, timeout, maxInflightRequests = 0) {\n  page.on('request', onRequestStarted)\n  page.on('requestfinished', onRequestFinished)\n  page.on('requestfailed', onRequestFinished)\n\n  let inflight = 0\n  let fulfill\n  let promise = new Promise(x => fulfill = x)\n  let timeoutId = setTimeout(onTimeoutDone, timeout)\n  return promise\n\n  function onTimeoutDone () {\n    page.off('request', onRequestStarted)\n    page.off('requestfinished', onRequestFinished)\n    page.off('requestfailed', onRequestFinished)\n    fulfill()\n  }\n\n  function onRequestStarted () {\n    ++inflight\n    if (inflight > maxInflightRequests) {\n      clearTimeout(timeoutId)\n    }\n  }\n\n  function onRequestFinished () {\n    if (inflight === 0) {\n      return\n    }\n    --inflight\n    if (inflight === maxInflightRequests) {\n      timeoutId = setTimeout(onTimeoutDone, timeout)\n    }\n  }\n}\n"
  },
  {
    "path": "src/parseLocals.js",
    "content": "const fs = require('fs')\nconst path = require('path')\nconst colors = require('colors/safe')\n\nlet lastLocalJsonPath\n\nfunction logError(error, message) {\n  console.error(error)\n  console.error(colors.red(message))\n}\n\nfunction readJsonFileAsString(jsonPath) {\n  try {\n    return fs.readFileSync(jsonPath, { encoding: 'utf-8' })\n  } catch (error) {\n    logError(error, `ReLaXed error: Could not read .json file at: ${jsonPath}`)\n  }\n}\n\nfunction parseJson(str) {\n  try {\n    return JSON.parse(str)\n  } catch (error) {\n    logError(error, 'ReLaXed error: Could not parse locals JSON, see error above.')\n  }\n}\n\nfunction isPathToJsonFile(filePath) {\n  return path.extname(filePath) === \".json\"\n}\n\nfunction isLastLocalJsonPath(filePath) {\n  return lastLocalJsonPath === filePath\n}\n\nfunction parseLocals(locals, inputDir) {\n  if (!locals) {\n    return\n  }\n\n  jsonString = locals\n\n  if (isPathToJsonFile(locals)) {\n    lastLocalJsonPath = path.join(inputDir, locals)\n    jsonString = readJsonFileAsString(lastLocalJsonPath)\n  }\n\n  return parseJson(jsonString)\n}\n\nexports.isLastLocalJsonPath = isLastLocalJsonPath\nexports.parseLocals = parseLocals\n"
  },
  {
    "path": "src/plugins.js",
    "content": "const path = require('path')\nconst colors = require('colors/safe')\nconst builtinPlugins = require('./builtin_plugins')\nconst fs = require('fs')\nconst { execSync } = require('child_process')\n\nexports.builtinDefaultPlugins = builtinPlugins.defaultPlugins\n\n// Function to get the global npm root directory\nvar getGlobalNpmRoot = function () {\n  try {\n    return execSync('npm root -g', { encoding: 'utf8', stdio: 'pipe' }).trim()\n  } catch (error) {\n    // Fallback to common paths if npm command fails\n    return null\n  }\n}\n\nvar createConfigPlugin = async function (pluginName, parameters, localPath) {\n  // for each plugin, look for a local definition, a built-in definition, or\n  // a module-provided definition (module relaxed-pluginName)\n  var origin\n  var plugin = builtinPlugins.plugins[pluginName]\n\n  if (plugin) {\n    origin = `ReLaXed ${pluginName} built-in plugin`\n  } else {\n    var possiblePaths = [\n      {\n        location: path.join(localPath, `${pluginName}.plugin.js`),\n        origin: `local file ${pluginName}.plugin.js`\n      },\n      {\n        location: path.join(localPath, pluginName),\n        origin: `local plugin ${pluginName}`\n      },\n      {\n        location: path.join(localPath, `relaxed-${pluginName}`),\n        origin: `local relaxed-${pluginName}`\n      },\n      {\n        location: `relaxed-${pluginName}`,\n        origin: `relaxed-${pluginName}`\n      }\n    ]\n\n    // Add global npm root path if available\n    var globalNpmRoot = getGlobalNpmRoot()\n    if (globalNpmRoot) {\n      possiblePaths.push({\n        location: path.join(globalNpmRoot, `relaxed-${pluginName}`),\n        origin: `global relaxed-${pluginName}`\n      })\n    }\n\n    // Add common fallback paths\n    var fallbackPaths = [\n      {\n        // macOS homebrew (Apple Silicon)\n        location: `/opt/homebrew/lib/node_modules/relaxed-${pluginName}`,\n        origin: `relaxed-${pluginName}`\n      },\n      {\n        // linux & macOS homebrew (Intel)\n        location: `/usr/local/lib/node_modules/relaxed-${pluginName}`,\n        origin: `relaxed-${pluginName}`\n      },\n      {\n        // travis\n        location: `/home/travis/build/RelaxedJS/relaxed-${pluginName}`,\n        origin: `relaxed-${pluginName}`\n      }\n    ]\n    possiblePaths = possiblePaths.concat(fallbackPaths)\n    for (var possiblePath of possiblePaths) {\n      try {\n        plugin = require(possiblePath.location)\n        origin = possiblePath.origin\n        break\n      } catch (error) {\n        var expected = `Cannot find module '${possiblePath.location}'`\n        if (error.message.indexOf(expected) === -1) {\n          console.error(error)\n        }\n      }\n    }\n  }\n  if (!plugin) {\n    throw Error(`Plugin ${pluginName} not found !`)\n  }\n  var configuratedPlugin = await plugin.constructor(parameters)\n  configuratedPlugin.origin = origin\n  return configuratedPlugin\n}\n\nvar listPluginHooks = function (pluginList) {\n  var pluginHooks = {}\n  var hooks = [\n    'watchers',\n    'pugHeaders',\n    'pugFilters',\n    'headElements',\n    'htmlModifiers',\n    'pageModifiers',\n    'page2ndModifiers',\n    'postPDF'\n  ]\n  for (var hook of hooks) {\n    var hookInstances = []\n    for (var plugin of pluginList) {\n      try {\n        var thisPluginHooks = plugin[hook]\n        if (thisPluginHooks) {\n          if (!Array.isArray(thisPluginHooks)) {\n            thisPluginHooks = [thisPluginHooks]\n          }\n          for (var pluginHook of thisPluginHooks) {\n            hookInstances.push({\n              instance: pluginHook,\n              origin: plugin.origin\n            })\n          }\n        }\n      } catch (error) {\n        console.log(`In hook ${hook} of plugin [${plugin.origin}]:`)\n        console.log(error.message)\n        throw error\n      }\n    }\n    pluginHooks[hook] = hookInstances\n  }\n  // TODO: order watchers by watched extension inclusion.\n  return pluginHooks\n}\n\nvar updateRegisteredPlugins = async function (relaxedGlobals, inputDir) {\n  if (relaxedGlobals.config.plugins) {\n    console.log(colors.magenta('... Loading config plugins'))\n    var plugin, pluginName, params\n    for (var pluginDefinition of relaxedGlobals.config.plugins) {\n      try {\n        if (typeof (pluginDefinition) === 'string') {\n          [pluginName, params] = [pluginDefinition, {}]\n        } else {\n          [pluginName, params] = Object.entries(pluginDefinition)[0]\n        }\n        console.log(colors.magenta(`    - ${pluginName} plugin`))\n        plugin = await createConfigPlugin(pluginName, params, inputDir)\n        relaxedGlobals.configPlugins.push(plugin)\n      } catch (error) {\n        console.log(error.message)\n        console.error(colors.bold.red(`Could not load plugin ${pluginName}`))\n      }\n    }\n  }\n  var allPlugins = relaxedGlobals.configPlugins.concat(builtinPlugins.defaultPlugins)\n  relaxedGlobals.pluginHooks = listPluginHooks(allPlugins)\n\n  // TODO: remove some of these extensions as they get covered by plugins.\n  relaxedGlobals.watchedExtensions = [\n    '.pug',\n    '.md',\n    '.html',\n    '.css',\n    '.scss',\n    '.svg',\n    '.png',\n    '.jpeg',\n    '.jpg'\n  ]\n\n  for (var watcher of relaxedGlobals.pluginHooks.watchers) {\n    var exts = watcher.instance.extensions\n    relaxedGlobals.watchedExtensions = relaxedGlobals.watchedExtensions.concat(exts)\n  }\n}\n\nexports.updateRegisteredPlugins = updateRegisteredPlugins\nexports.listPluginHooks = listPluginHooks\nexports.createConfigPlugin = createConfigPlugin\n"
  },
  {
    "path": "test/samples/interactive_example/diagrams/diagram.mermaid",
    "content": "graph LR\n  mermaid --> svg\n  vega-lite --> svg\n  svg --> html\n  pug --> html\n  html -->|Chrome| pdf\n"
  },
  {
    "path": "test/samples/interactive_example/diagrams/plot.vegalite.json",
    "content": "{\n  \"$schema\": \"https://vega.github.io/schema/vega-lite/v2.0.json\",\n  \"data\": {\n    \"values\": [\n      {\"a\": \"A\",\"b\": 28}, {\"a\": \"B\",\"b\": 55}, {\"a\": \"C\",\"b\": 43},\n      {\"a\": \"D\",\"b\": 91}, {\"a\": \"E\",\"b\": 81}, {\"a\": \"F\",\"b\": 53},\n      {\"a\": \"G\",\"b\": 19}, {\"a\": \"H\",\"b\": 87}, {\"a\": \"I\",\"b\": 52},\n      {\"a\": \"J\",\"b\": 19}, {\"a\": \"K\",\"b\": 87}, {\"a\": \"L\",\"b\": 52},\n      {\"a\": \"M\",\"b\": 19}, {\"a\": \"N\",\"b\": 87}, {\"a\": \"O\",\"b\": 52}\n    ]\n   },\n  \"mark\": \"bar\",\n  \"encoding\": {\n    \"x\": {\"field\": \"a\", \"type\": \"ordinal\"},\n    \"y\": {\"field\": \"b\", \"type\": \"quantitative\"},\n    \"color\": {\"value\": \"#b4588c\"}\n  }\n}\n"
  },
  {
    "path": "test/samples/interactive_example/master.pug",
    "content": ".report-sidebar: p A sidebar will always make your document look nicer !\n\nh1#title Beautiful PDF documents with web technologies\n\n.summary.ui.piled.segment: span.\n\n  We tend to prefer mark-up languages (Markdown, LaTeX, etc.) to interfaced document editors like MS Office or Google Docs, because they make it easier to quickly write documents with consistent style. However, Markdown is limited to the title/sections/paragraphs structure, and LaTeX has obscure syntax and errors that also make it difficult to go off the beaten tracks.\n  This report introduces ReLaXed, a solution using Pug and SCSS for document definition and Google Chrome for rendering.\n\n:markdown-it\n\n  ## Web technologies have never looked so good\n  \n  Plenty of CSS frameworks will make sure your documents will look clean and modern. Javascript frameworks can plot schemas, highlight code, or render maths equations the same way LaTeX does. Millions of people (and growing) are now fluent in these technologies. Shorthand languages like Pug and SCSS are finally making it fun to write HTML and CSS. (Headless) web browsers can easily turn all these technologies into PDF, on any platform.\n  \n  As an illustration, it took just one line to import the Semantic UI framework and style this document. Now look at this gorgeous table (don't pay attention to the content, it's place-holder)\n\n\nfigure.block-center.width-15cm\n  table.ui.celled.table\n    thead\n      tr\n        th Feature\n        th Framework\n        th Notes\n    tbody\n      tr\n        td Mathematical equations\n        td MathJax\n        td.positive #[i.icon.checkmark] Totally working\n      tr\n        td Plots\n        td Vegalite\n        td Needs testing\n      tr.negative\n        td Simple installation\n        td NPM\n        td #[i.icon.close] Problematic\n      tr\n        td Flowcharts\n        td Mermaid.js\n        td.positive #[i.icon.checkmark] Beautifully working\n  figcaption\n    .reference Table 1\n    .caption.\n      There is not much to say about this table but hey this is a caption. Captions are cool.\n          \np Here is another cool component provided by Semantic UI:  \n.ui.container\n  .ui.icon.message.yellow.block-center\n    i.lightbulb.outline.icon\n    .content\n      .header Give it a try !\n      p\n      | The ReLaXed homepage is at \n      a(href='https://github.com/RelaxedJS/ReLaXed') github.com/RelaxedJS/ReLaXed\n\nfigure.float-left.width-8cm.ui.raised.segment\n  .panel\n    .label A\n    include diagrams/plot.svg\n  .panel\n    .label B\n    .top-5mm\n      include diagrams/diagram.svg\n\n  figcaption\n    .reference Figure 1\n    .title Examples of graphics generated by web frameworks.\n    .caption.\n      This also demonstrates figure composition into panels - suck it, markdown !\n      #[b A. ] Graph defined as a JSON and transformed to SVG using Vega-lite and Chrome.\n      #[b B. ] Graph generated using Mermaid and Chrome.\n\n:markdown-it(html=true)\n  Next we will have a look at some differences between ReLaXed and other frameworks.\n\n  ## ReLaXed vs other solutions\n\n  Here are a few highlights of what you may win, or lose, using ReLaXed instead\n  of another solution. This section is of course open to contributions.\n\n  Let us start with Markdown. This widely supported language (Github, NPM, etc.)\n  became very popular due to its simple and friendly syntax close to plain text.\n  Markdown also has cool editors and extensions. One example is\n  [``markdown-preview-enhanced``](https://atom.io/packages/markdown-preview-enhanced)\n  which can render plots, flowcharts, and equations.\n\n  ReLaXed has been specially extended so that it could support plot, flowchart,\n  and equations (using the same underlying libraries as\n  markdown-preview-enhanced), as illustrated in Figure 1. In addition, ReLaXed\n  allows you to write any kind of layout with boxes, sidebars, etc. HTML\n  elements are more fun to write with Pug (in markdown, HTML elements must be\n  written in plain HTML). You can define macros, use conditionals and loops, use\n  computed expressions using Javascript , and ReLaXed supports (S)CSS which is\n  pretty cool. Last but not least, you can write parts in markdown if you want\n  to <i class=\"em em-wink\"></i> . Yep, that was an emoji. Cost us one line of\n  code, to import [Emoji CSS](https://afeld.github.io/emoji-css/) as a\n  stylesheet.\n\n  Now what about LaTeX ? Sure, LaTeX is wide-spread in academic and publishing\n  communities, where it's typography and layout optimizations, and its\n  bibliography management tool are very appreciated. But LaTeX is also known for\n  its cryptic errors, its complex advanced syntax which not many make the effort\n  to learn, and as a consequence not many LaTeX venture on the creative side\n  with their own themes and layouts.\n\n  Certainly the letter and paragraph spacings won't be as nice in ReLaXed (but\n  Google Chrome is still doing a very good job), but the syntax, clear error\n  messages, etc. will certainly make  you happier. ReLaXed is also possibly\n  faster to render big documents (not entirely sure though <i class=\"em\n  em-thinking_face\"></i>).\n\ntemplate#page-footer\n  style(type='text/css').\n    .pdfheader {\n      font-size: 10px;\n      font-family: Helvetica;\n      font-weight: bold;\n      width: 1000px;\n      border-top: 1px solid black;\n      margin-left: 10%;\n      margin-right: 10%;\n      padding-top: 1mm;\n      margin-bottom: -1mm;\n      text-align: center;\n    }\n  \n  .pdfheader Page #[span.pageNumber] / #[span.totalPages]\n\nstyle\n  include:scss report.scss\n"
  },
  {
    "path": "test/samples/interactive_example/report.scss",
    "content": "@import 'https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.3.0/semantic.min.css';\n@import 'https://afeld.github.io/emoji-css/emoji.css';\n\n$primary_color: #b4588c;\n$page-width: 17cm;\n\n@page {\n  margin: 1cm 1cm 2cm 0cm;\n}\nhtml {\n  padding-left: 1cm;\n  padding-right: 1cm;\n  font-family: 'Lato';\n  font-size: 14px !important;\n}\nbody {\n  margin-left: 1cm;\n}\n#title {\n  font-weight: bold;\n  font-size: 2.5em;\n}\n\n.summary {\n  background-color: lighten($primary_color, 33%);\n}\n\nh2 {\n  font-size: 1.8em;\n}\n\np {\n  line-height: 1.5em;\n  font-size: 1.1em !important;\n  text-align: justify;\n }\n\na {\n  text-decoration: underline;\n  color: inherit;\n}\n\n$sidebar-width: 0.25in;\n$sidebar-height: 943px;\n\n.report-sidebar {\n  position: fixed;\n  left: -$sidebar-width;\n  top: 0;\n  width: $sidebar-width;\n  height: $sidebar-height;\n  writing-mode: vertical-rl;\n  border-top: $sidebar-width solid transparent;\n  border-left: $sidebar-width solid lighten($primary_color, 10%);\n  border-bottom: $sidebar-width solid transparent;\n  p {\n    text-align: center;\n    color: lighten($primary_color, 30%);\n    margin-top: -1.55em;\n  }\n}\n\n.block-center {\n  display: block;\n  margin: 0 auto;\n}\n\n.mermaid-svg {\n  max-width: 100%;\n  height: auto;\n  display: block;\n  .node {\n    rect {\n      fill: none;\n      rx: 5px;\n      ry: 5px;\n      stroke: black;\n\n    }\n  }\n  .edgelabel {\n    background: white;\n    color: #444;\n  }\n}\n\n.message, figure {\n  page-break-inside: avoid;\n}\n\n.message {\n  margin-bottom: 1em !important;\n}\n\n\nfigure {\n  margin-top: 0.5em;\n  margin-bottom: 1em;\n  padding: 0.5em;\n  &> .panel {\n    position: relative;\n    display: inline-block;\n    vertical-align: top;\n    width: 100%;\n    &> .label {\n      font-weight: bold;\n      position: absolute;\n      top: 0;\n      left: 0;\n    }\n  }\n  &.float-left {\n    margin-left: -1em !important;\n    margin-right: 2em !important;\n    margin-top: 1em;\n    margin-bottom: 1em;\n    float: left;\n  }\n  figcaption {\n    font-size: 0.75em;\n    text-align: justify;\n    width: 100%;\n    margin-top: 1em;\n    &> .reference, .title {\n      font-weight: bold;\n\n      display: inline;\n      margin-right: 0.5em;\n    }\n    &> .reference {\n      color: darken($primary_color, 50%);\n      &::after {\n        content: ': ';\n      }\n    }\n    &> .title {\n      color: darken($primary_color, 40%);\n    }\n    &> .caption {\n      display: inline;\n    }\n  }\n}\n\n\n@for $i from 1 to 20 {\n  .width-#{$i}cm { width: #{10 * $i}mm !important; }\n}\n@for $i from 1 to 20 {\n  .top-#{$i}mm { margin-top: #{$i}mm; }\n}\n"
  },
  {
    "path": "test/samples/pug/absolute_path/master.pug",
    "content": "h1 Hello !\n\nstyle\n  include:scss /tmp/absolute_path_test.scss\n"
  },
  {
    "path": "test/samples/pug/basic_example/master.pug",
    "content": "h1 Hello !\n\nstyle.\n  h1 {\n    font-size: 140px;\n    text-align:center;\n    font-family: \"Arial\";\n    margin-top: 5cm;\n  }\n"
  },
  {
    "path": "test/samples/pug/data_locals/master.pug",
    "content": "h1 You're a #{ occupation }, #{ name } !!!!\n\n\nstyle.\n  body {\n    font-family: \"Arial\";\n    font-size: 14px;\n  }\n  h1 {\n    font-size: 140px;\n    margin-top: 100px;\n    text-align:center;\n  }\n"
  },
  {
    "path": "test/samples/pug/data_locals_file/data.json",
    "content": "{\n  \"name\": \"Harry\",\n  \"occupation\": \"Wazzzard\"\n}\n"
  },
  {
    "path": "test/samples/pug/data_locals_file/master.pug",
    "content": "h1 You're a #{ occupation }, #{ name } !!!!\n\n\nstyle.\n  body {\n    font-family: \"Arial\";\n    font-size: 14px;\n  }\n  h1 {\n    font-size: 140px;\n    margin-top: 100px;\n    text-align:center;\n  }\n"
  },
  {
    "path": "test/samples/pug/data_require/data.json",
    "content": "{\n  \"name\": \"Harry\",\n  \"occupation\": \"Wazzzard\"\n}\n"
  },
  {
    "path": "test/samples/pug/data_require/master.pug",
    "content": "- var data = require(path.join(basedir, 'data.json'))\nh1 You're a #{ data.occupation }, #{ data.name } !!!!\n\n\nstyle.\n  body {\n    font-family: \"Arial\";\n    font-size: 14px;\n  }\n  h1 {\n    font-size: 140px;\n    margin-top: 100px;\n    text-align:center;\n  }\n"
  },
  {
    "path": "test/samples/pug/error/master.pug",
    "content": ": Pug syntax error\n"
  },
  {
    "path": "test/samples/pug/header_and_footer/master.pug",
    "content": "h1 bla\n\ntemplate#page-header\n  style(type='text/css').\n    .page-header {\n      font-size: 16px;\n      font-family: \"Arial\";\n      font-weight: bold;\n      width: 1000px;\n      border-top: 1px solid black;\n      margin-left: 10%;\n      margin-right: 10%;\n      padding-top: 1mm;\n      margin-bottom: -1mm;\n      text-align: center;\n    }\n  \n  .page-header Page #[span.pageNumber] / #[span.totalPages]\n  \ntemplate#page-footer\n  style(type='text/css').\n    .page-footer {\n      font-size: 16px;\n      color: green;\n      font-family: 'Arial';\n      font-weight: bold;\n      width: 1000px;\n      border-top: 1px solid black;\n      margin-left: 10%;\n      margin-right: 10%;\n      padding-top: 1mm;\n      margin-bottom: -1mm;\n      text-align: center;\n    }\n  \n  .page-footer Page #[span.pageNumber] / #[span.totalPages]\n\n\nstyle.\n  @page {\n    margin: 3cm 2cm 3cm 2cm;\n  }\n  body {\n    font-family: 'Arial';\n    font-size: 14px;\n  }\n"
  },
  {
    "path": "test/samples/pug/katex/config.yml",
    "content": "plugins:\n  - katex\n"
  },
  {
    "path": "test/samples/pug/katex/master.pug",
    "content": "h1 Hello Katex <br/><br/> #[:katex \\sum{ \\sqrt{x^2 + \\alpha}} ]\n\n\nstyle.\n  body {\n    font-family: \"Arial\";\n    font-size: 14px;\n  }\n  h1 {\n    font-size: 60px;\n    margin-top: 4cm;\n    text-align: center;\n  }\n"
  },
  {
    "path": "test/samples/pug/letter/letter.scss",
    "content": "html {\n  padding: 1cm 1.5cm 1cm 1.5cm;\n}\n\nbody {\n  padding-left: 1cm;\n  padding-right: 1cm;\n  font-family: 'Arial';\n}\n.header {\n  margin-left: -1cm;\n  height: 80px;\n  .logo {\n    height: 2cm;\n    width:auto;\n    position:absolute;\n  }\n}\n.signature {\n  height: 2cm;\n  margin-left: 1cm;\n  width: auto;\n}\n\n.recipient {\n  text-align: right;\n  margin: 1em auto 2em;\n}\n.main-text {\n  text-align: justify;\n}\n.opening, .closing {\n  margin: 50px 0 20px 0px;\n}\n"
  },
  {
    "path": "test/samples/pug/letter/master.pug",
    "content": ".header\n  img.logo(src='./relaxed-uni-logo.svg')\n\n.recipient\n  .name Dr. Eddie Thor\n  .institute Predatory Publishing Group\n  .address John Nicholson Street\n  .city EH1 RX2 Edinburgh\n\n.opening Dear editor,\n\n.main-text\n  :markdown-it\n    \n    We are submitting our manuscript entitled *\"On the creation of beautiful PDF\n    files using yet another HTML converter but this one has a cool name\"* to your\n    *Journal of Incremental Software*, which we believe is the best indicated to\n    publish this work (now that every bigger journal has said no).\n\n    In this paper we present ReLaXed, a new framework for the interactive\n    generation of PDFs using web technologies, in particular the *Pug* language.\n    While many other software exist to produce high quality PDF files, they\n    generally lack the cool packaging and novelty of Relaxed.JS (and by\n    \"novelty\", we really mean \"recentness\"). At this time, Relaxed.JS already\n    has one active user, and we expect this number to grow several-fold after\n    publication in your journal.\n\n    We believe that this new piece of software would find an important echo in\n    your audience, in particular among developers interested in the creation of\n    beautiful PDF files using web technologies, in particular the *Pug*\n    language. Please publish us pretty please.\n\n.closing Yours sincelerely,\n\nimg.signature(src='signature.svg')\n\n.sender\n  .name Axel Red\n  .title Head of Communications\n  .institute Relaxed University\n  .address United Kingdom\n\nstyle\n  include:scss letter.scss\n"
  },
  {
    "path": "test/samples/pug/local_plugin/config.yml",
    "content": "plugins:\n  - say-my-name:\n      name: Heisenberg\n"
  },
  {
    "path": "test/samples/pug/local_plugin/master.pug",
    "content": "div(style=\"text-align:center; font-size:58px; margin-top: 3cm\")\n  p Say my name with a mixin ! <br/> #{name}\n  p Again, with a HTML filter ! <br/> INSERT_NAME_HERE\n  \nstyle.\n  body {\n    font-family: \"Arial\";\n    font-size: 14px;\n  }\n  p {\n     text-align:center;\n     font-size:50px;\n     font-weight: bold;\n     margin-top: 3cm;\n  }\n"
  },
  {
    "path": "test/samples/pug/local_plugin/say-my-name.plugin.js",
    "content": "const fs = require('fs')\n\nexports.constructor = async function (pluginDefinition) {\n  return {\n    pugHeaders: [\n      `- var name = \"${pluginDefinition.name}\"`\n    ],\n    htmlModifiers: [\n      function (html) {\n        return html.replace('INSERT_NAME_HERE', pluginDefinition.name)\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "test/samples/pug/mathjax/config.yml",
    "content": "plugins:\n  - mathjax\n"
  },
  {
    "path": "test/samples/pug/mathjax/master.pug",
    "content": "h1 Hello Mathjax <br/> $$ \\sum{ \\sqrt{x^2 + \\alpha} } $$\n\nstyle.\n  body {\n    font-family: \"Arial\";\n    font-size: 14px;\n  }\n  h1 {\n     font-size: 60px;\n     margin-top: 4cm;\n     text-align: center\n  }\n"
  },
  {
    "path": "test/samples/pug/utf8-characters/master.pug",
    "content": "h1 ★ ▹ ☀\nh1 Ü ß\n//- h1 漢 字\n\nstyle.\n  body {\n    font-family: \"Arial\";\n    font-size: 14px;\n  }\n  h1 {\n     font-size: 150px;\n     margin-top: 3cm;\n     text-align: center\n  }\n"
  },
  {
    "path": "test/samples/special_renderings/chartjs/donut.chart.js",
    "content": "{\n  type: 'doughnut',\n  data: {\n    datasets: [{\n      data: [10, 20, 30],\n      backgroundColor:[\"#db7575\", \"#dbc575\", \"#75b0db\"]\n    }],\n    labels: ['Ms Word', 'Google Docs', 'ReLaXed']\n  },\n  options: {\n    width:350,\n    height:350,\n    devicePixelRatio: 0.8,\n    legend: {\n      position: 'bottom'\n    }\n  }\n};\n"
  },
  {
    "path": "test/samples/special_renderings/htable_csv/diff.txt",
    "content": "thead\n  th,<<\n>>, ,<<   p >>,H1\n  th,<<\n>>, ,<<   p >>,H2\n  th,<<\n>>, ,<<   p >>,H3\ntbody\n  tr\n    td,<<\n>>, ,<<     p >>,A1\n    td,<<\n>>, ,<<     p >>,A2\n    td,<<\n>>, ,<<     p >>,A3\n  tr\n    td,<<\n>>, ,<<     p >>,B1\n    td,<<\n>>, ,<<     p >>,B2\n    td,<<\n>>, ,<<     p >>,B3\n  tr\n    td,<<\n>>, ,<<     p >>,C1\n    td,<<\n>>, ,<<     p >>,C2\n    td,<<\n>>, ,<<     p >>,C3\n"
  },
  {
    "path": "test/samples/special_renderings/htable_csv/expected.pug",
    "content": "thead\n  th\n    p H1\n  th\n    p H2\n  th\n    p H3\ntbody\n  tr\n    td\n      p A1\n    td\n      p A2\n    td\n      p A3\n  tr\n    td\n      p B1\n    td\n      p B2\n    td\n      p B3\n  tr\n    td\n      p C1\n    td\n      p C2\n    td\n      p C3\n"
  },
  {
    "path": "test/samples/special_renderings/htable_csv/sample.md.htable.csv",
    "content": "H1,H2,H3\nA1,A2,A3\nB1,B2,B3\nC1,C2,C3\n"
  },
  {
    "path": "test/samples/special_renderings/table_csv/diff.txt",
    "content": "tbody\n  tr\n    td,<<\n>>, ,<<     p >>,A1\n    td,<<\n>>, ,<<     p >>,A2\n    td,<<\n>>, ,<<     p >>,A3\n  tr\n    td,<<\n>>, ,<<     p >>,B1\n    td,<<\n>>, ,<<     p >>,B2\n    td,<<\n>>, ,<<     p >>,B3\n  tr\n    td,<<\n>>, ,<<     p >>,C1\n    td,<<\n>>, ,<<     p >>,C2\n    td,<<\n>>, ,<<     p >>,C3\n"
  },
  {
    "path": "test/samples/special_renderings/table_csv/expected.pug",
    "content": "tbody\n  tr\n    td\n      p A1\n    td\n      p A2\n    td\n      p A3\n  tr\n    td\n      p B1\n    td\n      p B2\n    td\n      p B3\n  tr\n    td\n      p C1\n    td\n      p C2\n    td\n      p C3\n"
  },
  {
    "path": "test/samples/special_renderings/table_csv/sample.md.table.csv",
    "content": "A1,A2,A3\nB1,B2,B3\nC1,C2,C3\n"
  },
  {
    "path": "test/test.js",
    "content": "const { spawn } = require('child_process')\nconst path = require('path')\nconst fs = require('fs')\n// const { pdfToPngThumbnail } = require('./pdf2png.js')\nconst PDFImage = require('pdf-image').PDFImage\nconst PixelDiff = require('pixel-diff')\nconst JsDiff = require('diff')\nconst assert = require('assert')\n\nfs.writeFileSync(\"/tmp/absolute_path_test.scss\", \"h1 {color: red; font-size: 140px; text-align:center;}\")\n\nconst relaxed = path.join(__dirname, './../src/index.js')\n\ndescribe('Sample tests', function () {\n  var tests = [\n    {\n      sampleName: 'basic_example',\n      timeout: 10000\n    },\n    {\n      sampleName: 'local_plugin',\n      timeout: 10000\n    },\n    {\n      sampleName: 'data_locals',\n      timeout: 10000,\n      cmdOptions: ['--locals', '{ \"name\": \"Harry\", \"occupation\": \"Wazzzard\" }']\n    },\n    {\n      sampleName: 'data_locals_file',\n      timeout: 10000,\n      cmdOptions: ['--locals', './data.json']\n    },\n    {\n      sampleName: 'data_require',\n      timeout: 10000\n    },\n    {\n      sampleName: 'mathjax',\n      timeout: 10000\n    }, {\n      sampleName: 'katex',\n      timeout: 10000\n    },\n    {\n      sampleName: 'header_and_footer',\n      timeout: 10000\n    },\n    {\n      sampleName: 'utf8-characters',\n      timeout: 10000\n    },\n    {\n      sampleName: 'absolute_path',\n      timeout: 10000,\n      cmdOptions: ['--basedir', '/']\n    }\n  ]\n  tests.forEach(function (test) {\n    it('renders sample \"' + test.sampleName + '\" correctly', function (done) {\n      this.timeout(test.timeout)\n      var basedir = path.join(__dirname, 'samples', 'pug', test.sampleName)\n      var paths = {\n        master: path.join(basedir, 'master.pug'),\n        expected: path.join(basedir, 'expected.png'),\n        diff: path.join(basedir, 'diff.png'),\n        pdf: path.join(basedir, 'master.pdf'),\n        lastTestPNG: path.join(basedir, 'last_test_result.png'),\n        html: path.join(basedir, 'master_temp.htm')\n      }\n\n      var process = spawn(\n        relaxed,\n        [paths.master, '--build-once', '--no-sandbox'].concat(test.cmdOptions || [])\n      )\n      process.on('close', async function (code) {\n        assert.equal(code, 0)\n        var pdfImage = new PDFImage(paths.pdf, {\n          combinedImage: true,\n          graphicsMagick: true\n        })\n        try {\n          var imgPath = await pdfImage.convertFile()\n        } catch (error) {\n          done(error)\n        }\n\n\n        var diff = new PixelDiff({\n          imageAPath: paths.expected,\n          imageBPath: imgPath,\n          thresholdType: PixelDiff.THRESHOLD_PERCENT,\n          threshold: 0.01, // 1% threshold\n          imageOutputPath: paths.diff\n        })\n\n        diff.run((error, result) => {\n          fs.unlinkSync(paths.pdf)\n          fs.unlinkSync(paths.html)\n          fs.renameSync(imgPath, paths.lastTestPNG)\n          if (error) {\n            console.error(error)\n          } else {\n            assert(diff.hasPassed(result.code))\n          }\n          done()\n        })\n      })\n    })\n  })\n})\n\ndescribe('Error tests', function () {\n  var tests = [\n    {\n      sampleName: 'error',\n      timeout: 10000\n    },\n  ]\n  tests.forEach(function (test) {\n    it('fails to render sample \"' + test.sampleName + '\"', function (done) {\n      this.timeout(test.timeout)\n      var basedir = path.join(__dirname, 'samples', 'pug', test.sampleName)\n      var process = spawn(\n        relaxed,\n        [path.join(basedir, 'master.pug'), '--build-once', '--no-sandbox'].concat(test.cmdOptions || [])\n      )\n      process.on('close', function (code) {\n        assert.equal(code, 1)\n        done()\n      })\n    })\n  })\n})\n\ndescribe('Special rendering tests', function () {\n  var tests = [\n    {\n      sampleName: 'table_csv',\n      master: 'sample.md.table.csv',\n      output: 'sample.pug',\n      expected: 'expected.pug',\n      outputType: 'text',\n      timeout: 10000\n    },\n    {\n      sampleName: 'htable_csv',\n      master: 'sample.md.htable.csv',\n      output: 'sample.pug',\n      expected: 'expected.pug',\n      outputType: 'text',\n      timeout: 10000\n    },\n    {\n      sampleName: 'chartjs',\n      master: 'donut.chart.js',\n      output: 'donut.png',\n      expected: 'expected.png',\n      outputType: 'image',\n      timeout: 10000\n    }\n  ]\n  tests.forEach(function (test) {\n    it('renders sample \"' + test.sampleName + '\" correctly', function (done) {\n      this.timeout(test.timeout)\n      var basedir = path.join(__dirname, 'samples', 'special_renderings', test.sampleName)\n      var extensions = {\n        text: 'txt',\n        image: 'png'\n      }\n      var diffExtension = extensions[test.outputType]\n      var paths = {\n        master: path.join(basedir, test.master),\n        expected: path.join(basedir, test.expected),\n        output: path.join(basedir, test.output),\n        diff: path.join(basedir, 'diff.' + diffExtension),\n        lastOutput: path.join(basedir, 'last_test_' + test.output)\n      }\n      var process = spawn(relaxed, [ paths.master, '--build-once' ])\n      process.on('close', async function (code) {\n        assert.equal(code, 0)\n        if (test.outputType === 'text') {\n          var expected = fs.readFileSync(paths.expected, 'utf8')\n          var output = fs.readFileSync(paths.output, 'utf8')\n          var isDifferent = (output !== expected)\n          if (isDifferent) {\n            var diff = JsDiff.diffChars(expected, output)\n            var parts = []\n            diff.forEach(function (part) {\n              if (part.added) {\n                parts.push(`[[${part.value}]]`)\n              } else if (part.removed) {\n                parts.push(`<<${part.value}>>`)\n              } else {\n                parts.push(part.value)\n              }\n            })\n            fs.writeFileSync(paths.diff, parts.join())\n            fs.renameSync(paths.output, paths.lastOutput)\n            done(Error('Output differs from expectations'))\n          } else {\n            fs.renameSync(paths.output, paths.lastOutput)\n            done()\n          }\n        } else if (test.outputType === 'image') {\n          let diff = new PixelDiff({\n            imageAPath: paths.expected,\n            imageBPath: paths.output,\n            thresholdType: PixelDiff.THRESHOLD_PERCENT,\n            threshold: 0.01, // 1% threshold\n            imageOutputPath: paths.diff\n          })\n          diff.run((error, result) => {\n            fs.renameSync(paths.output, paths.lastOutput)\n            if (error) {\n              done(error)\n            } else {\n              assert(diff.hasPassed(result.code))\n              done()\n            }\n          })\n        }\n      })\n    })\n  })\n})\n\n\n\ndescribe('Interactive tests', function () {\n  var basedir = path.join(__dirname, 'samples', 'interactive_sample')\n  var paths = [\n    {\n      diagramData: 'diagram.mermaid',\n      output: ['diagram.svg'],\n      timeout: 10000\n    },\n    {\n      diagramData: 'plot.vegalite.json',\n      output: ['plot.svg'],\n      timeout: 10000\n    }\n  ]\n  // var process = spawn('relaxed', [ path.join(basedir, 'master.pug') ])\n  it('renders mermaid diagram interactively correctly (STUB)' , function (done) {\n    // TODO: Implement tests\n    done()\n  })\n})\n"
  }
]