Full Code of RelaxedJS/ReLaXed for AI

master 9e31cd446150 cached
61 files
71.1 KB
21.6k tokens
16 symbols
1 requests
Download .txt
Repository: RelaxedJS/ReLaXed
Branch: master
Commit: 9e31cd446150
Files: 61
Total size: 71.1 KB

Directory structure:
gitextract_qbfk4w8g/

├── .github/
│   └── workflows/
│       └── npmpublish.yml
├── .gitignore
├── .npmignore
├── .travis.yml
├── LICENCE.txt
├── README.md
├── changelog.md
├── package.json
├── src/
│   ├── builtin_plugins/
│   │   ├── chartjs/
│   │   │   ├── index.js
│   │   │   └── template.pug
│   │   ├── flowchart/
│   │   │   ├── index.js
│   │   │   └── template.pug
│   │   ├── index.js
│   │   ├── katex/
│   │   │   ├── head.html
│   │   │   └── index.js
│   │   ├── markdown/
│   │   │   └── index.js
│   │   ├── mathjax/
│   │   │   └── index.js
│   │   ├── mermaid/
│   │   │   ├── index.js
│   │   │   └── template.pug
│   │   ├── scss/
│   │   │   └── index.js
│   │   ├── table/
│   │   │   ├── index.js
│   │   │   └── template.pug
│   │   └── vegalite/
│   │       ├── index.js
│   │       └── template.pug
│   ├── cli-tools/
│   │   ├── pdf2gif.js
│   │   └── pretty-pdf-thumbnail.js
│   ├── index.js
│   ├── masterToPDF.js
│   ├── parseLocals.js
│   └── plugins.js
└── test/
    ├── samples/
    │   ├── interactive_example/
    │   │   ├── diagrams/
    │   │   │   ├── diagram.mermaid
    │   │   │   └── plot.vegalite.json
    │   │   ├── master.pug
    │   │   └── report.scss
    │   ├── pug/
    │   │   ├── absolute_path/
    │   │   │   └── master.pug
    │   │   ├── basic_example/
    │   │   │   └── master.pug
    │   │   ├── data_locals/
    │   │   │   └── master.pug
    │   │   ├── data_locals_file/
    │   │   │   ├── data.json
    │   │   │   └── master.pug
    │   │   ├── data_require/
    │   │   │   ├── data.json
    │   │   │   └── master.pug
    │   │   ├── error/
    │   │   │   └── master.pug
    │   │   ├── header_and_footer/
    │   │   │   └── master.pug
    │   │   ├── katex/
    │   │   │   ├── config.yml
    │   │   │   └── master.pug
    │   │   ├── letter/
    │   │   │   ├── letter.scss
    │   │   │   └── master.pug
    │   │   ├── local_plugin/
    │   │   │   ├── config.yml
    │   │   │   ├── master.pug
    │   │   │   └── say-my-name.plugin.js
    │   │   ├── mathjax/
    │   │   │   ├── config.yml
    │   │   │   └── master.pug
    │   │   └── utf8-characters/
    │   │       └── master.pug
    │   └── special_renderings/
    │       ├── chartjs/
    │       │   └── donut.chart.js
    │       ├── htable_csv/
    │       │   ├── diff.txt
    │       │   ├── expected.pug
    │       │   └── sample.md.htable.csv
    │       └── table_csv/
    │           ├── diff.txt
    │           ├── expected.pug
    │           └── sample.md.table.csv
    └── test.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/npmpublish.yml
================================================
name: Node.js Package

on:
  release:
    types: [created]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v1
      - uses: actions/setup-node@v1
        with:
          node-version: 12
      - run: npm ci
      - run: npm test

  publish-npm:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v1
      - uses: actions/setup-node@v1
        with:
          node-version: 12
          registry-url: https://registry.npmjs.org/
      - run: npm ci
      - run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{secrets.npm_token}}

  publish-gpr:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v1
      - uses: actions/setup-node@v1
        with:
          node-version: 12
          registry-url: https://npm.pkg.github.com/
          scope: '@your-github-username'
      - run: npm ci
      - run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}


================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# OS specific temp files
.DS_Store
Thumbs.db

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# Typescript v1 declaration files
typings/

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env

# next.js build output
.next

test/samples/*/*/master.pdf
test/samples/*/*/master.png
test/samples/*/*/last_test_*
test/samples/*/*/diff.png
test/samples/*/*/master_temp.htm
*.htm

package-lock.json

# linux .directory
.directory

# VS code
.vscode
.eslintrc.*

*.local.bak


================================================
FILE: .npmignore
================================================
# https://docs.npmjs.com/misc/developers#keeping-files-out-of-your-package

#tests
test
coverage

#build tools
.travis.yml


================================================
FILE: .travis.yml
================================================
language: node_js

node_js:
  - "lts/*"
  - "stable"

before_install:
  - sudo apt-get -qq update
  - sudo apt-get install imagemagick ghostscript poppler-utils graphicsmagick

before_script:
  - npm link


================================================
FILE: LICENCE.txt
================================================

ISC License (ISC)

Copyright 2018, Zulko

Permission 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.

THE 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.


================================================
FILE: README.md
================================================
<p align="center"><img width='270px' src="https://github.com/RelaxedJS/ReLaXed/raw/master/logo-blue.png" /></p>

# ReLaXed

[![Build Status](https://travis-ci.org/RelaxedJS/ReLaXed.svg?branch=master)](https://travis-ci.org/RelaxedJS/ReLaXed)

ReLaXed 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.

Here it is in action in the Atom editor:

<p align='center'><img src="https://i.imgur.com/4N4fSYY.gif" title="source: imgur.com" /></p>

And here are a few output examples:

<table>
  <tr align="center">
    <td width="25%">
      <a href="https://github.com/RelaxedJS/ReLaXed-examples/blob/master/examples/book/book.pdf">
        <img src="https://github.com/RelaxedJS/ReLaXed-examples/raw/master/examples/book/book_screenshot.png" />
      </a>
      Book -
      <a href="https://github.com/RelaxedJS/ReLaXed-examples/tree/master/examples/book/"> source </a> /
      <a href="https://github.com/RelaxedJS/ReLaXed-examples/blob/master/examples/book/book.pdf"> PDF </a>
    </td>
    <td width="25%">
      <a href="https://github.com/RelaxedJS/ReLaXed-examples/blob/master/examples/letter/letter.pdf">
        <img src="https://github.com/RelaxedJS/ReLaXed-examples/raw/master/examples/letter/letter_screenshot.png" />
      </a>
      Letter -
      <a href="https://github.com/RelaxedJS/ReLaXed-examples/tree/master/examples/letter/"> Source </a> /
      <a href="https://github.com/RelaxedJS/ReLaXed-examples/blob/master/examples/letter/letter.pdf"> PDF </a>
    </td>
    <td width="25%">
      <a href="https://github.com/RelaxedJS/ReLaXed-examples/blob/master/examples/resume/resume.pdf">
        <img src="https://github.com/RelaxedJS/ReLaXed-examples/raw/master/examples/resume/resume_screenshot.png" />
      </a>
      Resume -
      <a href="https://github.com/RelaxedJS/ReLaXed-examples/tree/master/examples/resume/"> Source </a> /
      <a href="https://github.com/RelaxedJS/ReLaXed-examples/blob/master/examples/resume/resume.pdf"> PDF </a>
    </td>
    <td width="25%">
      <a href="https://github.com/RelaxedJS/ReLaXed-examples/blob/master/examples/business-card/business-card.pdf">
      <img src="https://github.com/RelaxedJS/ReLaXed-examples/raw/master/examples/business-card/businesscard_screenshot.png" /></a>
      Visit card -
      <a href="https://github.com/RelaxedJS/ReLaXed-examples/tree/master/examples/business-card/"> Source </a> /
      <a href="https://github.com/RelaxedJS/ReLaXed-examples/blob/master/examples/business-card/business-card.pdf"> PDF </a>
    </td>
  </tr>
</table>
<table>
  <tr align="center">
    <td width="25%">
      <a href="https://github.com/RelaxedJS/ReLaXed-examples/blob/master/examples/slides/slides.pdf">
        <img src="https://github.com/RelaxedJS/ReLaXed-examples/raw/master/examples/slides/slides_screenshot.png" />
      </a>
      Slides -
      <a href="https://github.com/RelaxedJS/ReLaXed-examples/tree/master/examples/slides/"> Source </a> /
      <a href="https://github.com/RelaxedJS/ReLaXed-examples/blob/master/examples/slides/slides.pdf"> PDF </a>
    </td>
    <td width="25%">
      <a href="https://github.com/RelaxedJS/ReLaXed-examples/blob/master/examples/report/report.pdf">
        <img src="https://github.com/RelaxedJS/ReLaXed-examples/raw/master/examples/report/report_screenshot.png" />
      </a>
      Report -
      <a href="https://github.com/RelaxedJS/ReLaXed-examples/tree/master/examples/report/"> Source </a> /
      <a href="https://github.com/RelaxedJS/ReLaXed-examples/blob/master/examples/report/report.pdf"> PDF </a>
    </td>
    <td width="25%">
       <a href="https://github.com/RelaxedJS/ReLaXed-examples/blob/master/examples/paper/paper.pdf">
         <img src="https://github.com/RelaxedJS/ReLaXed-examples/raw/master/examples/paper/paper_screenshot.png" />
       </a>
       Paper -
       <a href="https://github.com/RelaxedJS/ReLaXed-examples/tree/master/examples/paper/"> Source </a> /
       <a href="https://github.com/RelaxedJS/ReLaXed-examples/blob/master/examples/paper/paper.pdf"> PDF </a>
     </td>
   <td width="25%">
     <a href="https://github.com/RelaxedJS/ReLaXed-examples/blob/master/examples/poster/poster.pdf">
       <img src="https://github.com/RelaxedJS/ReLaXed-examples/raw/master/examples/poster/poster_screenshot.png" />
     </a>
     Poster -
     <a href="https://github.com/RelaxedJS/ReLaXed-examples/tree/master/examples/poster/"> Source </a> /
     <a href="https://github.com/RelaxedJS/ReLaXed-examples/blob/master/examples/poster/poster.pdf"> PDF </a>
   </td>
  </tr>
</table>

ReLaXed 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.

## Installing ReLaXed

Install ReLaXed via [NPM](https://www.npmjs.com/) with this command (do not use ``sudo``):

```
npm i -g relaxedjs
```

This 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))


## Getting started

To start a project, create a new document ``my_document.pug`` with the following Pug content:

```pug
h1 My document's title
p A paragraph in my document
```

Then start ReLaXed from a terminal:

```
relaxed my_document.pug
```

ReLaXed 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.

It is also possible to generate the PDF file just once, with no sub-sequent file-watching, with this command:

```
relaxed my_document.pug --build-once
```

To go further:

- Read more about [usage and options](https://github.com/RelaxedJS/ReLaXed/wiki/Command-line-options) of the ``relaxed`` command.
- Learn more about the capabilities of the [Pug language](https://pugjs.org/api/getting-started.html)
- Learn how to use or write [ReLaXed plugins](https://github.com/RelaxedJS/ReLaXed/wiki/Plugins)
- Browse the [examples](https://github.com/RelaxedJS/ReLaXed-examples)
- Read about our [recommended setup](https://github.com/RelaxedJS/ReLaXed/wiki/Tips-and-recommendations) to use ReLaXed
- read about [special file rendering](https://github.com/RelaxedJS/ReLaXed/wiki/Special-file-renderings) in ReLaxed
- Read [these comparisons](https://github.com/RelaxedJS/ReLaXed/wiki/ReLaXed-vs-other-solutions) between ReLaXed and other document-editing systems

## Why yet another PDF document creator?

Many 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.

However, 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.

On the other hand, web technologies have never looked so good.

- Beautiful CSS frameworks make sure your documents look clean and modern.
- There are JavaScript libraries for pretty much anything: plotting, highlight code, rendering equations...
- Millions of people (and growing) know how to use these.
- Shorthand languages like Pug and SCSS are finally making it fun to write HTML and CSS.
- (Headless) web browsers can easily turn web documents into PDF, on any platform.

ReLaXed is an attempt at finding the most comfortable way to leverage this for desktop PDF creation.

## How ReLaXed works

ReLaXed 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)).

<p align="center"><img width='600px' src="https://github.com/RelaxedJS/ReLaXed/raw/master/docs/relaxed_stack.png" /></p>

## Using it as a Node Module
**MasterToPDF.js** is exposed by default as main package, which can be used directly.

An Example: 

```javascript
const { masterToPDF } = require('relaxedjs');
const puppeteer = require('puppeteer');
const plugins = require('relaxedjs/src/plugins');
const path = require('path');

class HTML2PDF {
  constructor() {
    this.puppeteerConfig = {
      headless: true,
      args: [
        '--no-sandbox',
        '--disable-translate',
        '--disable-extensions',
        '--disable-sync',
      ],
    };

    this.relaxedGlobals = {
      busy: false,
      config: {},
      configPlugins: [],
    };

    this._initializedPlugins = false;
  }

  async _initializePlugins() {
    if (this._initializedPlugins) return; // Do not initialize plugins twice
    for (const [i, plugin] of plugins.builtinDefaultPlugins.entries()) {
      plugins.builtinDefaultPlugins[i] = await plugin.constructor();
    }
    await plugins.updateRegisteredPlugins(this.relaxedGlobals, '/');

    const chrome = await puppeteer.launch(this.puppeteerConfig);
    this.relaxedGlobals.puppeteerPage = await chrome.newPage();
    this._initializedPlugins = true;
  }

  async pdf(templatePath, json_data, tempHtmlPath, outputPdfPath) {
    await this._initializePlugins();
    if (this._initializedPlugins) {
      // Paths must be absolute
      const defaultTempHtmlPath = tempHtmlPath || path.resolve('temp.html');
      const defaultOutputPdfPath =
        outputPdfPath || path.resolve('output.pdf');

      await masterToPDF(
        templatePath,
        this.relaxedGlobals,
        defaultTempHtmlPath,
        defaultOutputPdfPath,
        json_data
      );
    }
  }
}

module.exports = new HTML2PDF();
```
Usage:

```javascript
const HTML2PDF = require('./HTML2PDF.js');
(async () => {
    await HTML2PDF.pdf('./template.pug', {"a":"b", "c":"d"});
})();
```


## Contribute!

ReLaXed 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!

For 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.

**Projects members:**

- [@Zulko](https://github.com/Zulko) (Owner)
- [@Drew-S](https://github.com/Drew-S) (architecture, plugins)
- [@DanielRuf](https://github.com/DanielRuf)
- [@benperiton](https://github.com/benperiton)

## License

[ISC](https://github.com/RelaxedJS/ReLaXed/blob/master/LICENCE.txt)


================================================
FILE: changelog.md
================================================
# Changelog

## v0.1.6

Features:

- Now exposing ``require`` for use in in-Pug javascript
- Bibliography system, built-in.
- Shorthand ``--bo`` for ``--build-once``

Fixes:

- fixed typo breaking special converters (mermaid, etc.)
- faster PDF rendering by using chromium's DOM instead of cheerio.
- better (faster) stepSVG mixin

Internal:

- Lots of code reorganization, mostly by @Drew-S, and marks for future plugins.
- A test suite ! But not for interactive usage yet.
- Added project members to readme.

## v 0.1.5

- Some console output fixes
- Now avoiding new renderings when already busy.
- New "built-in" mixin ``stepsSVG`` for including progressive
  (i.e. animated) SVGs into slides
- New command-line utility ``pretty-pdf-thumbnail`` shipped with ReLaXed.

## v 0.1.4

Important release with speed and features improvements.

Breaking API changes:

- Now using ``template#page-footer``, ``template#page-header`` to define
  page header and footer.
- MathJax de-activated by default

New features:

- Command-line parameter ``--build-once`` for one-time builds
- New exposed javascript globals in templates:
  - Packages: ``fs``, ``cheerio``
  - Variables: ``basedir`` (indicating the base path of the master file)
- Experimental Katex filter now available
- Files with extension ``.o.svg`` are automatically converted to optimized
  svgs (``*_optimized.svg``).
- Console now shows a breakdown of rendering time.

Other changes in the code:

- First test suite !!
- Removed some jstransformer dependencies.
- Refactoring with utils.py


================================================
FILE: package.json
================================================
{
  "name": "relaxedjs",
  "version": "0.2.6",
  "description": "Create PDF documents using web technologies (PDF/SCSS)",
  "main": "src/masterToPDF.js",
  "bin": {
    "relaxed": "src/index.js",
    "pretty-pdf-thumbnail": "src/cli-tools/pretty-pdf-thumbnail.js",
    "pdf2gif": "src/cli-tools/pdf2gif.js"
  },
  "scripts": {
    "test": "mocha"
  },
  "author": "Zulko",
  "homepage": "https://github.com/RelaxedJS",
  "license": "ISC",
  "dependencies": {
    "@iktakahiro/markdown-it-katex": "^4.0.1",
    "cheerio": "^1.1.2",
    "chokidar": "^4.0.3",
    "colors": "^1.4.0",
    "commander": "^14.0.0",
    "csvtojson": "^2.0.10",
    "deptree": "^1.0.0",
    "filesize": "^11.0.2",
    "html2jade": "^0.8.6",
    "jimp": "^1.6.0",
    "js-yaml": "^4.1.0",
    "jstransformer-highlight": "^2.0.0",
    "jstransformer-markdown-it": "^3.0.0",
    "katex": "^0.16.22",
    "markdown-it": "^14.1.0",
    "markdown-it-footnote": "^4.0.0",
    "mathjax-node-page": "^3.2.0",
    "mermaid": "^11.9.0",
    "pug": "^3.0.3",
    "puppeteer": "^24.15.0",
    "sass": "^1.89.2"
  },
  "devDependencies": {
    "diff": "^8.0.2",
    "mocha": "^11.7.1",
    "pdf-image": "^2.0.0",
    "pixel-diff": "^1.0.1"
  }
}


================================================
FILE: src/builtin_plugins/chartjs/index.js
================================================
const pug = require('pug')
const fs = require('fs')
const path = require('path')

exports.constructor = async function (params) {
  return {
    watchers: [
      {
        extensions: ['.chart.js'],
        handler: chartjsHandler
      }
    ]
  }
}


var chartjsHandler = async function (chartjsPath, page) {
  var chartSpec = fs.readFileSync(chartjsPath, 'utf8')
  var html = pug.renderFile(path.join(__dirname, 'template.pug'), { chartSpec })
  var tempHTML = chartjsPath + '.htm'

  fs.writeFileSync(tempHTML, html)
  await page.setContent(html)
  await page.waitForFunction(() => window.pngData)

  const dataUrl = await page.evaluate(() => window.pngData)
  const { buffer } = parseDataUrl(dataUrl)
  var pngPath = chartjsPath.substr(0, chartjsPath.length - '.chart.js'.length) + '.png'
  fs.writeFileSync(pngPath, buffer, 'base64')
}
// Scrape (pull) images from the web
// from https://intoli.com/blog/saving-images/
var parseDataUrl = function (dataUrl) {
  const matches = dataUrl.match(/^data:(.+);base64,(.+)$/)
  if (matches.length !== 3) {
    throw new Error('Could not parse data URL.')
  }
  return {
    mime: matches[1],
    buffer: Buffer.from(matches[2], 'base64')
  }
}


================================================
FILE: src/builtin_plugins/chartjs/template.pug
================================================
script(src='https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.2/Chart.min.js')

#chartContainer
  canvas#myChart
script.
  window.pngData = false
  var canvas = document.getElementById("myChart")
  var container = document.getElementById("chartContainer")
  function onComplete () {
    window.pngData = canvas.toDataURL();
  }
  var config=!{chartSpec};
  config.options.animation = {duration: 0, onComplete: onComplete}
  if (config.options.width) {
    canvas.style.width = config.options.width
    container.style.width = config.options.width
  }
  if (config.options.height) {
    canvas.style.height = config.options.height
    container.style.height = config.options.height
  }
  
  var ctx = document.getElementById("myChart").getContext('2d');
  var myChart = new Chart(ctx, config)


================================================
FILE: src/builtin_plugins/flowchart/index.js
================================================
const pug = require('pug')
const fs = require('fs')
const path = require('path')

exports.constructor = async function (params) {
  return {
    watchers: [
      {
        extensions: ['.flowchart', '.flowchart.json'],
        handler: flowchartHandler
      }
    ]
  }
}

var flowchartHandler = async function (flowchartPath, page) {
  if (flowchartPath.endsWith('.flowchart.json')) {
    flowchartPath = flowchartPath.substr(0, flowchartPath.length - 5)
  }
  var flowchartSpec = fs.readFileSync(flowchartPath, 'utf8')
  var flowchartConf = '{}'

  var possibleConfs = [
    path.join(path.resolve(flowchartPath, '..'), 'flowchart.default.json'),
    flowchartPath + '.json'
  ]

  for (var myPath of possibleConfs) {
    if (fs.existsSync(myPath)) {
      flowchartConf = fs.readFileSync(myPath, 'utf8')
    }
  }

  var html = pug.renderFile(path.join(__dirname, 'template.pug'), {
    flowchartSpec,
    flowchartConf
  })

  await page.setContent(html)
  await page.waitForSelector('#chart svg')

  var svg = await page.evaluate(function () {
    var el = document.querySelector('#chart svg')
    el.removeAttribute('height')
    el.removeAttribute('width')
    el.classList.add('flowchart-svg')
    return el.outerHTML
  })
  var svgPath = flowchartPath.substr(0, flowchartPath.lastIndexOf('.')) + '.svg'
  fs.writeFileSync(svgPath, svg)
}


================================================
FILE: src/builtin_plugins/flowchart/template.pug
================================================
script(src='http://cdnjs.cloudflare.com/ajax/libs/raphael/2.2.0/raphael-min.js')
script(src='http://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.0/jquery.min.js')
script(src='http://flowchart.js.org/flowchart-latest.js')
script.
  $(document).ready(function() {
    var chart = flowchart.parse(`!{flowchartSpec}`)
    var conf = {'symbols': []}
    for (var c of ['start', 'end', 'operation', 'subroutine', 'condition', 'inputoutput']) {
      conf.symbols[c] = {'class': c + '-element'}  
    }
    conf = Object.assign({}, conf, !{flowchartConf})
    chart.drawSVG('chart', conf)
  });
body
  #chart


================================================
FILE: src/builtin_plugins/index.js
================================================
const chartjs = require('./chartjs')
const table = require('./table')
const mathjax = require('./mathjax')
const markdown = require('./markdown')
const vegalite = require('./vegalite')
const mermaid = require('./mermaid')
const flowchart = require('./flowchart')
const scss = require('./scss')
const katex = require('./katex')
// THESE ARE PLUGINS THAT CAN BE LOADED VIA CONFIG.PY
// WE WILL CERTAINLY TAKE OUT MOST OF THEM, AS SEPARATE PLUGINS

exports.plugins = {
  mathjax,
  katex,
  markdown
}

// THESE ARE PLUGINS ADDING NO OVERHEAD, SO SAFE TO BE USED BY DEFAULT
// WE WILL ALSO CERTAINLY TAKE OUT MOST OF THEM, AS SEPARATE PLUGINS,
// TO AVOID TOO MANY DEPENDENCIES

exports.defaultPlugins = [
  table,
  chartjs,
  vegalite,
  flowchart,
  mermaid,
  scss,
  markdown
]


================================================
FILE: src/builtin_plugins/katex/head.html
================================================
<link
  rel="stylesheet"
  href="https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/katex.min.css"
  integrity="sha384-zB1R0rpPzHqg7Kpt0Aljp8JPLqbXI3bhnPWROx27a9N0Ll6ZP/+DiW/UqRcLbRjq"
  crossorigin="anonymous" />


================================================
FILE: src/builtin_plugins/katex/index.js
================================================
const katex = require('katex')
const fs = require('fs')
const path = require('path')

exports.constructor = async function (params) {
  return {
    headElements: fs.readFileSync(path.join(__dirname, 'head.html')),
    pugFilters: {
      katex (text, options) { return katex.renderToString(text) }
    }
  }
}


================================================
FILE: src/builtin_plugins/markdown/index.js
================================================
var hljs = require('highlight.js') // https://highlightjs.org/
var markdown = require('markdown-it')
var mdFootnote = require('markdown-it-footnote')
var mdKatex = require('@iktakahiro/markdown-it-katex')

exports.constructor = async function (params) {
  var href = "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/katex.min.css"
  return {
    pugFilters: { markdown: MarkdownPugFilter },
    headElements: `<link rel="stylesheet" href="${href}" />`
  }
}
function MarkdownPugFilter (text, options) {
  var md = markdown({
    ...options,
    highlight: function (str, lang) {
      if (lang && hljs.getLanguage(lang)) {
        try {
          return '<pre class="hljs"><code>' +
                  hljs.highlight(lang, str, true).value +
                  '</code></pre>';
        } catch (__) {}
      }
      return '<pre class="hljs"><code>' + md.utils.escapeHtml(str) + '</code></pre>'
    }
  })
  md.use(mdFootnote)
  md.use(mdKatex)
  return md.render(text)
}


================================================
FILE: src/builtin_plugins/mathjax/index.js
================================================
const mjpage = require('mathjax-node-page')

// TODO: would this work better if applied to the page instead of the HTML ?

exports.constructor = async function (params) {
  return {
    htmlModifiers: [ asyncMathjax ]
  }
}

var asyncMathjax = async function (html) {
  return new Promise(resolve => {
    mjpage.mjpage(html, {
      format: ['TeX']
    }, {
      mml: true,
      css: true,
      html: true
    }, response => resolve(response))
  })
}


================================================
FILE: src/builtin_plugins/mermaid/index.js
================================================
const pug = require('pug')
const fs = require('fs')
const path = require('path')

exports.constructor = async function (params) {
  return {
    watchers: [
      {
        extensions: ['.mermaid'],
        handler: mermaidHandler
      }
    ]
  }
}

var mermaidHandler = async function (mermaidPath, page) {
  var mermaidSpec = fs.readFileSync(mermaidPath, 'utf8')
  var html = pug.renderFile(path.join(__dirname, 'template.pug'), {
    mermaidSpec
  })

  await page.setContent(html)
  await page.waitForSelector('#graph svg')

  var svg = await page.evaluate(function () {
    var el = document.querySelector('#graph svg')
    el.removeAttribute('height')
    el.classList.add('mermaid-svg')
    return el.outerHTML
  })

  var svgPath = mermaidPath.substr(0, mermaidPath.lastIndexOf('.')) + '.svg'

  fs.writeFileSync(svgPath, svg)
}


================================================
FILE: src/builtin_plugins/mermaid/template.pug
================================================
script(src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/7.1.2/mermaid.min.js")
#graph.mermaid #{mermaidSpec}


================================================
FILE: src/builtin_plugins/scss/index.js
================================================
const sass = require('sass')

exports.constructor = async function (params) {
  return {
    pugFilters: { scss: ScssPugFilter }
  }
}

function ScssPugFilter (text, options) {
  if (options.filename.endsWith('scss'))
    return sass.compile(options.filename).css
  return sass.compileString(text).css
}


================================================
FILE: src/builtin_plugins/table/index.js
================================================

const pug = require('pug')
const fs = require('fs')
const path = require('path')
const csv = require('csvtojson')
var markdown = require('markdown-it')
const html2jade = require('html2jade')

exports.constructor = async function (params) {
  return {
    watchers: [
      {
        extensions: ['.table.csv', '.htable.csv', '.htable.md.csv'],
        async handler (tablePath, page) {
          await csvTtableToPug(tablePath)
        }
      }
    ]
  }
}

var csvTtableToPug = async function (tablePath) {
  const rows = await csv({output: 'csv', noheader: true, delimiter: 'auto'}).fromFile(tablePath)
  var isMarkdown = tablePath.includes('.md.')
  if (isMarkdown) {
    console.log('ah')
    md = markdown({html: true})
    rows.forEach(row => {
      row.forEach((data, i) => {
        row[i] = md.render(data)
      })
    })
  }
  var extension, header
  if (tablePath.endsWith('.htable.csv')) {
    extension = '.htable.csv'
    header = rows.shift()
  } else {
    extension = '.table.csv'
    header = null
  }
  var html = await pug.renderFile(path.join(__dirname, 'template.pug'), {
    header: header,
    tbody: rows
  })
  var pugPath = tablePath.substr(0, tablePath.length - extension.length - 3*isMarkdown) + '.pug'
  var jade = await new Promise(resolve => {
    html2jade.convertHtml(html, { bodyless: true }, function (err, jade) {
      if (err) {
        console.log('Error:', err)
        resolve(false)
      } else {
        resolve(jade)
      }
    })
  })
  fs.writeFileSync(pugPath, jade)
}


================================================
FILE: src/builtin_plugins/table/template.pug
================================================
if header
  thead
    for th in header
      th!= th
tbody
  for tr in tbody
    tr
      for td in tr
        td!= td


================================================
FILE: src/builtin_plugins/vegalite/index.js
================================================
const pug = require('pug')
const fs = require('fs')
const path = require('path')

exports.constructor = async function (params) {
  return {
    watchers: [
      {
        extensions: ['.vegalite.json'],
        handler: vegaliteHandler
      }
    ]
  }
}

var vegaliteHandler = async function (vegalitePath, page) {
  var vegaliteSpec = fs.readFileSync(vegalitePath, 'utf8')
  var html = pug.renderFile(path.join(__dirname, 'template.pug'), { vegaliteSpec })

  await page.setContent(html)
  await page.waitForSelector('#vis svg')

  var svg = await page.evaluate(function () {
    var el = document.querySelector('#vis svg')

    el.removeAttribute('height')
    el.removeAttribute('width')

    return el.outerHTML
  })

  var svgPath = vegalitePath.substr(0, vegalitePath.length - '.vegalite.json'.length) + '.svg'
  fs.writeFileSync(svgPath, svg)
}


================================================
FILE: src/builtin_plugins/vegalite/template.pug
================================================
script(src='https://cdn.jsdelivr.net/npm/vega@5.21.0')
script(src='https://cdn.jsdelivr.net/npm/vega-lite@5.2.0')
script(src='https://cdn.jsdelivr.net/npm/vega-embed@6.20.2')
#vis
script(type='text/javascript').
  vegaEmbed('#vis', !{vegaliteSpec}, {'renderer': 'svg'});


================================================
FILE: src/cli-tools/pdf2gif.js
================================================
#!/usr/bin/env node

const colors = require('colors')
const program = require('commander')
const path = require('path')
const { spawn } = require('child_process')
const version = require('../../package.json').version

var input, output

program
  .version('From ReLaXed ' + version)
  .usage('<input> [output] [options]')
  .arguments('<input> [output] [options]')
  .option('--width, -w', 'width in pixels')
  .option('--delay, -d', 'delay between frames')
  .option('--colors, -c', 'number of colors')
  .action(function (inp, out) {
    input = inp
    output = out
  })
program.parse(process.argv)
output = output || (input.slice(0, input.length - 4) + '.gif')
var width = (program.width || 400).toString()
var delay = (100 * (program.delay || 1.0)).toString()
var ncolors = (program.colors || 256).toString()
var subprocess = spawn('convert', [
  '-delay', delay,
  '-resize', width,
  '-colors', ncolors,
  '-layers', 'optimize',
  input,
  output
])

subprocess
  .on('data', function (data) {
    console.log(data)
  })
  .on('close', async function (code) {
    if (code) {
      console.log(code)
    } else {
      console.log('...done.')
    }
  })


================================================
FILE: src/cli-tools/pretty-pdf-thumbnail.js
================================================
#!/usr/bin/env node

const colors = require('colors')
const program = require('commander')
const path = require('path')
const { spawn } = require('child_process')
const version = require('../../package.json').version

var input, output

program
  .version('From ReLaXed ' + version)
  .usage('<input> [output] [options]')
  .arguments('<input> [output] [options]')
  .option('--width, -w', 'width in pixels')
  .option('--shadow, -s', 'shadow size in pixels', [])
  .action(function (inp, out) {
    input = inp
    output = out
  })

program.parse(process.argv)

program.shadow = program.shadow || 15
var subprocess = spawn('convert', [
  '-density', '300',
  input + '[0]',
  '-resize', ((program.size || 600) - 4 * program.shadow).toString(),
  `(`, '+clone', '-background', 'black',
  '-shadow', `${program.shadow}x${program.shadow}+1+1`, `)`,
  '+swap',
  '-background', 'white',
  '-layers', 'merge',
  '+repage',
  output
])

subprocess
  .on('data', function (data) {
    console.log(data)
  })
  .on('close', async function (code) {
    if (code) {
      console.log(code)
    } else {
      console.log('...done.')
    }
  })


================================================
FILE: src/index.js
================================================
#!/usr/bin/env node

const colors = require('colors/safe')
const { program } = require('commander')
const chokidar = require('chokidar')
const puppeteer = require('puppeteer')
const yaml = require('js-yaml')
const { performance } = require('perf_hooks')
const path = require('path')
const fs = require('fs')
const plugins = require('./plugins')
const { masterToPDF } = require('./masterToPDF.js')
const { parseLocals, isLastLocalJsonPath } = require('./parseLocals');
const { exec } = require('child_process')
var input, output
const version = require('../package.json').version

program
  .version(version)
  .usage('<input> [output] [options]')
  .arguments('<input> [output] [options]')
  .option('--no-sandbox', 'disable puppeteer sandboxing')
  .option('-w, --watch <locations>', 'Watch other locations', [])
  .option('-t, --temp [location]', 'Directory for temp file')
  .option('--bo, --build-once', 'Build once only, do not watch')
  .option('-l, --locals <json>', 'Json locals for pug rendering, string or path to .json file')
  .option('--basedir <location>', 'Base directory for absolute paths, e.g. /')

  .action(function (inp, out) {
    input = inp
    output = out
  })

// ARGUMENTS PARSING AND SETUP

program.parse(process.argv)
const options = program.opts()

if (!input || fs.lstatSync(input).isDirectory()) {
  input = autodetectMasterFile(input)
}

const inputPath = path.resolve(input)
const inputDir = path.resolve(inputPath, '..')
const inputFilenameNoExt = path.basename(input, path.extname(input))

var configPath
for (var filename of ['config.yml', 'config.json']) {
  let possiblePath = path.join(inputDir, filename)
  if (fs.existsSync(possiblePath)) {
    configPath = possiblePath
  }
}


// Output file, path, and temp html path
if (!output) {
  output = path.join(inputDir, inputFilenameNoExt + '.pdf')
}
const outputPath = path.resolve(output)

var tempDir
if (options.temp) {
  var validTempPath = fs.existsSync(options.temp) && fs.statSync(options.temp).isDirectory()

  if (validTempPath) {
    tempDir = path.resolve(options.temp)
  } else {
    console.error(colors.red('ReLaXed error: Could not find specified --temp directory: ' +
      options.temp))
    process.exit(1)
  }
} else {
  tempDir = inputDir
}

const tempHTMLPath = path.join(tempDir, inputFilenameNoExt + '_temp.htm')

// Default and additional watch locations
let watchLocations = [inputDir]
if (options.watch) {
  watchLocations = watchLocations.concat(options.watch)
}

let locals = parseLocals(options.locals, inputDir)

// Google Chrome headless configuration
const puppeteerConfig = {
  headless: true,
  args: (!options.sandbox ? ['--no-sandbox'] : []).concat([
    '--disable-translate',
    '--disable-extensions',
    '--disable-sync'
  ])
}


/*
 * ==============================================================
 *                         MAIN
 * ==============================================================
 */

const relaxedGlobals = {
  busy: false,
  config: {},
  configPlugins: [],
  basedir: options.basedir || inputDir
}

var updateConfig = async function () {
  if (configPath) {
    console.log(colors.magenta('... Reading config file'))
    var data = fs.readFileSync(configPath, 'utf8')
    if (configPath.endsWith('.json')) {
      relaxedGlobals.config = JSON.parse(data)
    } else {
      relaxedGlobals.config = yaml.load(data)
    }
  }
  await plugins.updateRegisteredPlugins(relaxedGlobals, inputDir)
}



async function main () {
  console.log(colors.magenta.bold('Launching ReLaXed...'))

  // LOAD BUILT-IN "ALWAYS-ON" PLUGINS
  for (var [i, plugin] of plugins.builtinDefaultPlugins.entries()) {
    plugins.builtinDefaultPlugins[i] = await plugin.constructor()
  }
  await updateConfig()
  const browser = await puppeteer.launch(puppeteerConfig)
  relaxedGlobals.puppeteerPage = await browser.newPage()

  relaxedGlobals.puppeteerPage.on('pageerror', function (err) {
    console.log(colors.red('Page error: ' + err.toString()))
  }).on('error', function (err) {
    console.log(colors.red('Error: ' + err.toString()))
  })

  const buildError = await build(inputPath)

  if (program.buildOnce) {
    process.exit(buildError ? 1 : 0)
  } else {
    watch()
  }
}

/*
 * ==============================================================
 *                         BUILD
 * ==============================================================
 */

async function build (filepath) {
  var shortFileName = filepath.replace(inputDir, '')
  if ((path.basename(filepath) === 'config.yml') || (filepath.endsWith('.plugin.js'))) {
    await updateConfig()
    return
  }

  var updatedLocals = false
  if (isLastLocalJsonPath(filepath)) {
    locals = parseLocals(options.locals, inputDir)
    updatedLocals = true
  }

  var page = relaxedGlobals.puppeteerPage
  // Ignore the call if ReLaXed is already busy processing other files.

  if (!updatedLocals && !(relaxedGlobals.watchedExtensions.some(ext => filepath.endsWith(ext)))) {
    if (!(['.pdf', '.htm'].some(ext => filepath.endsWith(ext)))) {
      console.log(colors.grey(`No process defined for file ${shortFileName}.`))
    }
    return
  }

  if (relaxedGlobals.busy) {
    console.log(colors.grey(`File ${shortFileName}: ignoring trigger, too busy.`))
    return
  }

  console.log(colors.magenta.bold(`\nProcessing ${shortFileName}...`))
  relaxedGlobals.busy = true
  var t0 = performance.now()


  var taskPromise = null

  for (var watcher of relaxedGlobals.pluginHooks.watchers) {
    if (watcher.instance.extensions.some(ext => filepath.endsWith(ext))) {
      taskPromise = watcher.instance.handler(filepath, page)
      break
    }
  }
  var generatingPDF = !taskPromise
  if (generatingPDF) {
    taskPromise = masterToPDF(inputPath, relaxedGlobals, tempHTMLPath, outputPath, locals)
  }
  const generateError = await taskPromise
  var duration = ((performance.now() - t0) / 1000).toFixed(2)
  console.log(colors.magenta.bold(`... Done in ${duration}s`))
  if (generatingPDF && relaxedGlobals.config.after && !generateError) {
    console.log(colors.magenta.bold("Running 'after' command..."))
    var subprocess = exec(relaxedGlobals.config.after, cwd=relaxedGlobals.basedir)
    
    subprocess.stdout.on('data', (data) => {
      console.log(`after-stdout: ${data}`);
    });
    subprocess.stderr.on('data', (data) => {
      console.error(`after-stderr: ${data}`);
    });
    var promise = new Promise(resolve => {
      subprocess.on('close', async function (code) {
        resolve()
      })
    })
    await promise
    console.log(colors.magenta.bold("...done running 'after' command."))
  }
  relaxedGlobals.busy = false
  return generateError
}

/**
 * Watch `watchLocations` paths for changes and continuously rebuild
 *
 * @param {puppeteer.Page} page
 */

/*
 * ==============================================================
 *                         WATCH
 * ==============================================================
 */

function watch () {
  console.log(colors.magenta(`\nNow idle and waiting for file changes.`))
  chokidar.watch(watchLocations, {
    awaitWriteFinish: {
      stabilityThreshold: 50,
      pollInterval: 100
    }
  }).on('change', build)
}

function autodetectMasterFile (input) {
  var dir = input || '.'
  var files = fs.readdirSync(dir).filter((name) => name.endsWith('.pug'))
  var filename
  if (files.length === 1) {
    filename = files[0]
  } else if (files.indexOf('master.pug') >= 0) {
    filename = 'master.pug'
  } else {
    var error
    if (input) {
      error = `Could not find a master file in the provided directory ${input}`
    } else {
      error = `No input provided and could not find a master file in the current directory`
    }
    console.log(colors.red.bold(error))
    program.help()
    process.exit(1)
  }
  return path.join(dir, filename)
}

main()


================================================
FILE: src/masterToPDF.js
================================================
const pug = require('pug')
const colors = require('colors/safe')
const cheerio = require('cheerio')
const fs = require('fs')
const { filesize } = require('filesize')
const path = require('path')
const { performance } = require('perf_hooks')

// Returns undefined if successful or an error object on failure
exports.masterToPDF = async function (masterPath, relaxedGlobals, tempHTMLPath, outputPath, locals) {
  var t0 = performance.now()
  var page = relaxedGlobals.puppeteerPage
  /*
   *            Generate HTML
   */
  var pluginHooks = relaxedGlobals.pluginHooks
  var html
  if (masterPath.endsWith('.pug')) {
    var pluginPugHeaders = []
    for (var pugHeader of pluginHooks.pugHeaders) {
      pluginPugHeaders.push(pugHeader.instance)
    }
    pluginPugHeaders = pluginPugHeaders.join('\n\n')

    var pugFilters = Object.assign(...pluginHooks.pugFilters.map(o => o.instance))
    try {
      var masterPug = fs.readFileSync(masterPath, 'utf8')

      html = pug.render(pluginPugHeaders + '\n' + masterPug, Object.assign({}, locals ? locals : {}, {
        filename: masterPath,
        fs: fs,
        basedir: relaxedGlobals.basedir,
        cheerio: cheerio,
        __root__: path.dirname(masterPath),
        path: path,
        require: require,
        performance: performance,
        filters: pugFilters
      }))
    } catch (error) {
      console.log(error.message)
      console.error(colors.red('There was a Pug error (see above)'))
      return error
    }
  } else if (masterPath.endsWith('.html')) {
    html = fs.readFileSync(masterPath, 'utf8')
  }

  /*
   *            MODIFY HTML
   */
  var head = pluginHooks.headElements.map(e => e.instance).join(`\n\n`)
  html = `
    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="UTF-8">
        ${head}
      </head>
      <body> ${html} </body>
    </html>`

  for (var htmlModifier of pluginHooks.htmlModifiers) {
    html = await htmlModifier.instance(html)
  }

  fs.writeFileSync(tempHTMLPath, html)

  var tHTML = performance.now()
  console.log(colors.magenta(`... HTML generated in ${((tHTML - t0) / 1000).toFixed(1)}s`))

  /*
   *            LOAD HTML
   */
  try {
    await page.goto('file:' + tempHTMLPath, {
      waitUntil: ['load', 'domcontentloaded'],
      timeout: 1000 * (relaxedGlobals.config.pageRenderingTimeout || 30)
    })
  } catch(error) {
    console.log(error.message)
    console.error(colors.red('There was a page loading error.'))
    if (error.message.indexOf('Timeout') > 0) {
      console.log('Hey this looks like a timeout. Your project must be big. ' +
                  'Increase the timeout by writing "pageRenderingTimeout: 60" ' +
                  'at the top of your config.yml. Default is 30 (seconds).')
    }
    return error
  }
  
  var tLoad = performance.now()
  console.log(colors.magenta(`... Document loaded in ${((tLoad - tHTML) / 1000).toFixed(1)}s`))

  await waitForNetworkIdle(page, 200)
  var tNetwork = performance.now()
  console.log(colors.magenta(`... Network idled in ${((tNetwork - tLoad) / 1000).toFixed(1)}s`))

  // Get header/footer template
  var header = await page.$eval('#page-header', element => element.innerHTML)
    .catch(error => '')
  var footer = await page.$eval('#page-footer', element => element.innerHTML)
    .catch(error => '')

  if (header !== '' && footer === '') {
    footer = '<span></span>'
  }
  if ((footer !== '') && (header === '')) {
    header = '<span></span>'
  }
  /*
   *            Create PDF options
   */
  var options = {
    path: outputPath,
    displayHeaderFooter: !!(header || footer),
    headerTemplate: header,
    footerTemplate: footer,
    printBackground: true
  }

  function getMatch (string, query) {
    var result = string.match(query)
    if (result) {
      result = result[1]
    }
    return result
  }

  var width = getMatch(html, /-relaxed-page-width: (\S+);/m)
  if (width) {
    options.width = width
  }
  var height = getMatch(html, /-relaxed-page-height: (\S+);/m)
  if (height) {
    options.height = height
  }
  var size = getMatch(html, /-relaxed-page-size: (\S+);/m)
  if (size) {
    options.size = size
  }

  for (var pageModifier of pluginHooks.pageModifiers) {
    await pageModifier.instance(page)
  }

  for (pageModifier of pluginHooks.page2ndModifiers) {
    await pageModifier.instance(page)
  }

  // TODO: add option to output full html from page

  /*
   *            PRINT PAGE TO PDF
   */
  await page.pdf(options)

  var tPDF = performance.now()
  let duration = ((tPDF - tNetwork) / 1000).toFixed(1)
  let pdfSize = filesize(fs.statSync(outputPath).size)
  console.log(colors.magenta(`... PDF written in ${duration}s (${pdfSize})`))
}

// Wait for all the content on the page to finish loading
function waitForNetworkIdle (page, timeout, maxInflightRequests = 0) {
  page.on('request', onRequestStarted)
  page.on('requestfinished', onRequestFinished)
  page.on('requestfailed', onRequestFinished)

  let inflight = 0
  let fulfill
  let promise = new Promise(x => fulfill = x)
  let timeoutId = setTimeout(onTimeoutDone, timeout)
  return promise

  function onTimeoutDone () {
    page.off('request', onRequestStarted)
    page.off('requestfinished', onRequestFinished)
    page.off('requestfailed', onRequestFinished)
    fulfill()
  }

  function onRequestStarted () {
    ++inflight
    if (inflight > maxInflightRequests) {
      clearTimeout(timeoutId)
    }
  }

  function onRequestFinished () {
    if (inflight === 0) {
      return
    }
    --inflight
    if (inflight === maxInflightRequests) {
      timeoutId = setTimeout(onTimeoutDone, timeout)
    }
  }
}


================================================
FILE: src/parseLocals.js
================================================
const fs = require('fs')
const path = require('path')
const colors = require('colors/safe')

let lastLocalJsonPath

function logError(error, message) {
  console.error(error)
  console.error(colors.red(message))
}

function readJsonFileAsString(jsonPath) {
  try {
    return fs.readFileSync(jsonPath, { encoding: 'utf-8' })
  } catch (error) {
    logError(error, `ReLaXed error: Could not read .json file at: ${jsonPath}`)
  }
}

function parseJson(str) {
  try {
    return JSON.parse(str)
  } catch (error) {
    logError(error, 'ReLaXed error: Could not parse locals JSON, see error above.')
  }
}

function isPathToJsonFile(filePath) {
  return path.extname(filePath) === ".json"
}

function isLastLocalJsonPath(filePath) {
  return lastLocalJsonPath === filePath
}

function parseLocals(locals, inputDir) {
  if (!locals) {
    return
  }

  jsonString = locals

  if (isPathToJsonFile(locals)) {
    lastLocalJsonPath = path.join(inputDir, locals)
    jsonString = readJsonFileAsString(lastLocalJsonPath)
  }

  return parseJson(jsonString)
}

exports.isLastLocalJsonPath = isLastLocalJsonPath
exports.parseLocals = parseLocals


================================================
FILE: src/plugins.js
================================================
const path = require('path')
const colors = require('colors/safe')
const builtinPlugins = require('./builtin_plugins')
const fs = require('fs')
const { execSync } = require('child_process')

exports.builtinDefaultPlugins = builtinPlugins.defaultPlugins

// Function to get the global npm root directory
var getGlobalNpmRoot = function () {
  try {
    return execSync('npm root -g', { encoding: 'utf8', stdio: 'pipe' }).trim()
  } catch (error) {
    // Fallback to common paths if npm command fails
    return null
  }
}

var createConfigPlugin = async function (pluginName, parameters, localPath) {
  // for each plugin, look for a local definition, a built-in definition, or
  // a module-provided definition (module relaxed-pluginName)
  var origin
  var plugin = builtinPlugins.plugins[pluginName]

  if (plugin) {
    origin = `ReLaXed ${pluginName} built-in plugin`
  } else {
    var possiblePaths = [
      {
        location: path.join(localPath, `${pluginName}.plugin.js`),
        origin: `local file ${pluginName}.plugin.js`
      },
      {
        location: path.join(localPath, pluginName),
        origin: `local plugin ${pluginName}`
      },
      {
        location: path.join(localPath, `relaxed-${pluginName}`),
        origin: `local relaxed-${pluginName}`
      },
      {
        location: `relaxed-${pluginName}`,
        origin: `relaxed-${pluginName}`
      }
    ]

    // Add global npm root path if available
    var globalNpmRoot = getGlobalNpmRoot()
    if (globalNpmRoot) {
      possiblePaths.push({
        location: path.join(globalNpmRoot, `relaxed-${pluginName}`),
        origin: `global relaxed-${pluginName}`
      })
    }

    // Add common fallback paths
    var fallbackPaths = [
      {
        // macOS homebrew (Apple Silicon)
        location: `/opt/homebrew/lib/node_modules/relaxed-${pluginName}`,
        origin: `relaxed-${pluginName}`
      },
      {
        // linux & macOS homebrew (Intel)
        location: `/usr/local/lib/node_modules/relaxed-${pluginName}`,
        origin: `relaxed-${pluginName}`
      },
      {
        // travis
        location: `/home/travis/build/RelaxedJS/relaxed-${pluginName}`,
        origin: `relaxed-${pluginName}`
      }
    ]
    possiblePaths = possiblePaths.concat(fallbackPaths)
    for (var possiblePath of possiblePaths) {
      try {
        plugin = require(possiblePath.location)
        origin = possiblePath.origin
        break
      } catch (error) {
        var expected = `Cannot find module '${possiblePath.location}'`
        if (error.message.indexOf(expected) === -1) {
          console.error(error)
        }
      }
    }
  }
  if (!plugin) {
    throw Error(`Plugin ${pluginName} not found !`)
  }
  var configuratedPlugin = await plugin.constructor(parameters)
  configuratedPlugin.origin = origin
  return configuratedPlugin
}

var listPluginHooks = function (pluginList) {
  var pluginHooks = {}
  var hooks = [
    'watchers',
    'pugHeaders',
    'pugFilters',
    'headElements',
    'htmlModifiers',
    'pageModifiers',
    'page2ndModifiers',
    'postPDF'
  ]
  for (var hook of hooks) {
    var hookInstances = []
    for (var plugin of pluginList) {
      try {
        var thisPluginHooks = plugin[hook]
        if (thisPluginHooks) {
          if (!Array.isArray(thisPluginHooks)) {
            thisPluginHooks = [thisPluginHooks]
          }
          for (var pluginHook of thisPluginHooks) {
            hookInstances.push({
              instance: pluginHook,
              origin: plugin.origin
            })
          }
        }
      } catch (error) {
        console.log(`In hook ${hook} of plugin [${plugin.origin}]:`)
        console.log(error.message)
        throw error
      }
    }
    pluginHooks[hook] = hookInstances
  }
  // TODO: order watchers by watched extension inclusion.
  return pluginHooks
}

var updateRegisteredPlugins = async function (relaxedGlobals, inputDir) {
  if (relaxedGlobals.config.plugins) {
    console.log(colors.magenta('... Loading config plugins'))
    var plugin, pluginName, params
    for (var pluginDefinition of relaxedGlobals.config.plugins) {
      try {
        if (typeof (pluginDefinition) === 'string') {
          [pluginName, params] = [pluginDefinition, {}]
        } else {
          [pluginName, params] = Object.entries(pluginDefinition)[0]
        }
        console.log(colors.magenta(`    - ${pluginName} plugin`))
        plugin = await createConfigPlugin(pluginName, params, inputDir)
        relaxedGlobals.configPlugins.push(plugin)
      } catch (error) {
        console.log(error.message)
        console.error(colors.bold.red(`Could not load plugin ${pluginName}`))
      }
    }
  }
  var allPlugins = relaxedGlobals.configPlugins.concat(builtinPlugins.defaultPlugins)
  relaxedGlobals.pluginHooks = listPluginHooks(allPlugins)

  // TODO: remove some of these extensions as they get covered by plugins.
  relaxedGlobals.watchedExtensions = [
    '.pug',
    '.md',
    '.html',
    '.css',
    '.scss',
    '.svg',
    '.png',
    '.jpeg',
    '.jpg'
  ]

  for (var watcher of relaxedGlobals.pluginHooks.watchers) {
    var exts = watcher.instance.extensions
    relaxedGlobals.watchedExtensions = relaxedGlobals.watchedExtensions.concat(exts)
  }
}

exports.updateRegisteredPlugins = updateRegisteredPlugins
exports.listPluginHooks = listPluginHooks
exports.createConfigPlugin = createConfigPlugin


================================================
FILE: test/samples/interactive_example/diagrams/diagram.mermaid
================================================
graph LR
  mermaid --> svg
  vega-lite --> svg
  svg --> html
  pug --> html
  html -->|Chrome| pdf


================================================
FILE: test/samples/interactive_example/diagrams/plot.vegalite.json
================================================
{
  "$schema": "https://vega.github.io/schema/vega-lite/v2.0.json",
  "data": {
    "values": [
      {"a": "A","b": 28}, {"a": "B","b": 55}, {"a": "C","b": 43},
      {"a": "D","b": 91}, {"a": "E","b": 81}, {"a": "F","b": 53},
      {"a": "G","b": 19}, {"a": "H","b": 87}, {"a": "I","b": 52},
      {"a": "J","b": 19}, {"a": "K","b": 87}, {"a": "L","b": 52},
      {"a": "M","b": 19}, {"a": "N","b": 87}, {"a": "O","b": 52}
    ]
   },
  "mark": "bar",
  "encoding": {
    "x": {"field": "a", "type": "ordinal"},
    "y": {"field": "b", "type": "quantitative"},
    "color": {"value": "#b4588c"}
  }
}


================================================
FILE: test/samples/interactive_example/master.pug
================================================
.report-sidebar: p A sidebar will always make your document look nicer !

h1#title Beautiful PDF documents with web technologies

.summary.ui.piled.segment: span.

  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.
  This report introduces ReLaXed, a solution using Pug and SCSS for document definition and Google Chrome for rendering.

:markdown-it

  ## Web technologies have never looked so good
  
  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.
  
  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)


figure.block-center.width-15cm
  table.ui.celled.table
    thead
      tr
        th Feature
        th Framework
        th Notes
    tbody
      tr
        td Mathematical equations
        td MathJax
        td.positive #[i.icon.checkmark] Totally working
      tr
        td Plots
        td Vegalite
        td Needs testing
      tr.negative
        td Simple installation
        td NPM
        td #[i.icon.close] Problematic
      tr
        td Flowcharts
        td Mermaid.js
        td.positive #[i.icon.checkmark] Beautifully working
  figcaption
    .reference Table 1
    .caption.
      There is not much to say about this table but hey this is a caption. Captions are cool.
          
p Here is another cool component provided by Semantic UI:  
.ui.container
  .ui.icon.message.yellow.block-center
    i.lightbulb.outline.icon
    .content
      .header Give it a try !
      p
      | The ReLaXed homepage is at 
      a(href='https://github.com/RelaxedJS/ReLaXed') github.com/RelaxedJS/ReLaXed

figure.float-left.width-8cm.ui.raised.segment
  .panel
    .label A
    include diagrams/plot.svg
  .panel
    .label B
    .top-5mm
      include diagrams/diagram.svg

  figcaption
    .reference Figure 1
    .title Examples of graphics generated by web frameworks.
    .caption.
      This also demonstrates figure composition into panels - suck it, markdown !
      #[b A. ] Graph defined as a JSON and transformed to SVG using Vega-lite and Chrome.
      #[b B. ] Graph generated using Mermaid and Chrome.

:markdown-it(html=true)
  Next we will have a look at some differences between ReLaXed and other frameworks.

  ## ReLaXed vs other solutions

  Here are a few highlights of what you may win, or lose, using ReLaXed instead
  of another solution. This section is of course open to contributions.

  Let us start with Markdown. This widely supported language (Github, NPM, etc.)
  became very popular due to its simple and friendly syntax close to plain text.
  Markdown also has cool editors and extensions. One example is
  [``markdown-preview-enhanced``](https://atom.io/packages/markdown-preview-enhanced)
  which can render plots, flowcharts, and equations.

  ReLaXed has been specially extended so that it could support plot, flowchart,
  and equations (using the same underlying libraries as
  markdown-preview-enhanced), as illustrated in Figure 1. In addition, ReLaXed
  allows you to write any kind of layout with boxes, sidebars, etc. HTML
  elements are more fun to write with Pug (in markdown, HTML elements must be
  written in plain HTML). You can define macros, use conditionals and loops, use
  computed expressions using Javascript , and ReLaXed supports (S)CSS which is
  pretty cool. Last but not least, you can write parts in markdown if you want
  to <i class="em em-wink"></i> . Yep, that was an emoji. Cost us one line of
  code, to import [Emoji CSS](https://afeld.github.io/emoji-css/) as a
  stylesheet.

  Now what about LaTeX ? Sure, LaTeX is wide-spread in academic and publishing
  communities, where it's typography and layout optimizations, and its
  bibliography management tool are very appreciated. But LaTeX is also known for
  its cryptic errors, its complex advanced syntax which not many make the effort
  to learn, and as a consequence not many LaTeX venture on the creative side
  with their own themes and layouts.

  Certainly the letter and paragraph spacings won't be as nice in ReLaXed (but
  Google Chrome is still doing a very good job), but the syntax, clear error
  messages, etc. will certainly make  you happier. ReLaXed is also possibly
  faster to render big documents (not entirely sure though <i class="em
  em-thinking_face"></i>).

template#page-footer
  style(type='text/css').
    .pdfheader {
      font-size: 10px;
      font-family: Helvetica;
      font-weight: bold;
      width: 1000px;
      border-top: 1px solid black;
      margin-left: 10%;
      margin-right: 10%;
      padding-top: 1mm;
      margin-bottom: -1mm;
      text-align: center;
    }
  
  .pdfheader Page #[span.pageNumber] / #[span.totalPages]

style
  include:scss report.scss


================================================
FILE: test/samples/interactive_example/report.scss
================================================
@import 'https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.3.0/semantic.min.css';
@import 'https://afeld.github.io/emoji-css/emoji.css';

$primary_color: #b4588c;
$page-width: 17cm;

@page {
  margin: 1cm 1cm 2cm 0cm;
}
html {
  padding-left: 1cm;
  padding-right: 1cm;
  font-family: 'Lato';
  font-size: 14px !important;
}
body {
  margin-left: 1cm;
}
#title {
  font-weight: bold;
  font-size: 2.5em;
}

.summary {
  background-color: lighten($primary_color, 33%);
}

h2 {
  font-size: 1.8em;
}

p {
  line-height: 1.5em;
  font-size: 1.1em !important;
  text-align: justify;
 }

a {
  text-decoration: underline;
  color: inherit;
}

$sidebar-width: 0.25in;
$sidebar-height: 943px;

.report-sidebar {
  position: fixed;
  left: -$sidebar-width;
  top: 0;
  width: $sidebar-width;
  height: $sidebar-height;
  writing-mode: vertical-rl;
  border-top: $sidebar-width solid transparent;
  border-left: $sidebar-width solid lighten($primary_color, 10%);
  border-bottom: $sidebar-width solid transparent;
  p {
    text-align: center;
    color: lighten($primary_color, 30%);
    margin-top: -1.55em;
  }
}

.block-center {
  display: block;
  margin: 0 auto;
}

.mermaid-svg {
  max-width: 100%;
  height: auto;
  display: block;
  .node {
    rect {
      fill: none;
      rx: 5px;
      ry: 5px;
      stroke: black;

    }
  }
  .edgelabel {
    background: white;
    color: #444;
  }
}

.message, figure {
  page-break-inside: avoid;
}

.message {
  margin-bottom: 1em !important;
}


figure {
  margin-top: 0.5em;
  margin-bottom: 1em;
  padding: 0.5em;
  &> .panel {
    position: relative;
    display: inline-block;
    vertical-align: top;
    width: 100%;
    &> .label {
      font-weight: bold;
      position: absolute;
      top: 0;
      left: 0;
    }
  }
  &.float-left {
    margin-left: -1em !important;
    margin-right: 2em !important;
    margin-top: 1em;
    margin-bottom: 1em;
    float: left;
  }
  figcaption {
    font-size: 0.75em;
    text-align: justify;
    width: 100%;
    margin-top: 1em;
    &> .reference, .title {
      font-weight: bold;

      display: inline;
      margin-right: 0.5em;
    }
    &> .reference {
      color: darken($primary_color, 50%);
      &::after {
        content: ': ';
      }
    }
    &> .title {
      color: darken($primary_color, 40%);
    }
    &> .caption {
      display: inline;
    }
  }
}


@for $i from 1 to 20 {
  .width-#{$i}cm { width: #{10 * $i}mm !important; }
}
@for $i from 1 to 20 {
  .top-#{$i}mm { margin-top: #{$i}mm; }
}


================================================
FILE: test/samples/pug/absolute_path/master.pug
================================================
h1 Hello !

style
  include:scss /tmp/absolute_path_test.scss


================================================
FILE: test/samples/pug/basic_example/master.pug
================================================
h1 Hello !

style.
  h1 {
    font-size: 140px;
    text-align:center;
    font-family: "Arial";
    margin-top: 5cm;
  }


================================================
FILE: test/samples/pug/data_locals/master.pug
================================================
h1 You're a #{ occupation }, #{ name } !!!!


style.
  body {
    font-family: "Arial";
    font-size: 14px;
  }
  h1 {
    font-size: 140px;
    margin-top: 100px;
    text-align:center;
  }


================================================
FILE: test/samples/pug/data_locals_file/data.json
================================================
{
  "name": "Harry",
  "occupation": "Wazzzard"
}


================================================
FILE: test/samples/pug/data_locals_file/master.pug
================================================
h1 You're a #{ occupation }, #{ name } !!!!


style.
  body {
    font-family: "Arial";
    font-size: 14px;
  }
  h1 {
    font-size: 140px;
    margin-top: 100px;
    text-align:center;
  }


================================================
FILE: test/samples/pug/data_require/data.json
================================================
{
  "name": "Harry",
  "occupation": "Wazzzard"
}


================================================
FILE: test/samples/pug/data_require/master.pug
================================================
- var data = require(path.join(basedir, 'data.json'))
h1 You're a #{ data.occupation }, #{ data.name } !!!!


style.
  body {
    font-family: "Arial";
    font-size: 14px;
  }
  h1 {
    font-size: 140px;
    margin-top: 100px;
    text-align:center;
  }


================================================
FILE: test/samples/pug/error/master.pug
================================================
: Pug syntax error


================================================
FILE: test/samples/pug/header_and_footer/master.pug
================================================
h1 bla

template#page-header
  style(type='text/css').
    .page-header {
      font-size: 16px;
      font-family: "Arial";
      font-weight: bold;
      width: 1000px;
      border-top: 1px solid black;
      margin-left: 10%;
      margin-right: 10%;
      padding-top: 1mm;
      margin-bottom: -1mm;
      text-align: center;
    }
  
  .page-header Page #[span.pageNumber] / #[span.totalPages]
  
template#page-footer
  style(type='text/css').
    .page-footer {
      font-size: 16px;
      color: green;
      font-family: 'Arial';
      font-weight: bold;
      width: 1000px;
      border-top: 1px solid black;
      margin-left: 10%;
      margin-right: 10%;
      padding-top: 1mm;
      margin-bottom: -1mm;
      text-align: center;
    }
  
  .page-footer Page #[span.pageNumber] / #[span.totalPages]


style.
  @page {
    margin: 3cm 2cm 3cm 2cm;
  }
  body {
    font-family: 'Arial';
    font-size: 14px;
  }


================================================
FILE: test/samples/pug/katex/config.yml
================================================
plugins:
  - katex


================================================
FILE: test/samples/pug/katex/master.pug
================================================
h1 Hello Katex <br/><br/> #[:katex \sum{ \sqrt{x^2 + \alpha}} ]


style.
  body {
    font-family: "Arial";
    font-size: 14px;
  }
  h1 {
    font-size: 60px;
    margin-top: 4cm;
    text-align: center;
  }


================================================
FILE: test/samples/pug/letter/letter.scss
================================================
html {
  padding: 1cm 1.5cm 1cm 1.5cm;
}

body {
  padding-left: 1cm;
  padding-right: 1cm;
  font-family: 'Arial';
}
.header {
  margin-left: -1cm;
  height: 80px;
  .logo {
    height: 2cm;
    width:auto;
    position:absolute;
  }
}
.signature {
  height: 2cm;
  margin-left: 1cm;
  width: auto;
}

.recipient {
  text-align: right;
  margin: 1em auto 2em;
}
.main-text {
  text-align: justify;
}
.opening, .closing {
  margin: 50px 0 20px 0px;
}


================================================
FILE: test/samples/pug/letter/master.pug
================================================
.header
  img.logo(src='./relaxed-uni-logo.svg')

.recipient
  .name Dr. Eddie Thor
  .institute Predatory Publishing Group
  .address John Nicholson Street
  .city EH1 RX2 Edinburgh

.opening Dear editor,

.main-text
  :markdown-it
    
    We are submitting our manuscript entitled *"On the creation of beautiful PDF
    files using yet another HTML converter but this one has a cool name"* to your
    *Journal of Incremental Software*, which we believe is the best indicated to
    publish this work (now that every bigger journal has said no).

    In this paper we present ReLaXed, a new framework for the interactive
    generation of PDFs using web technologies, in particular the *Pug* language.
    While many other software exist to produce high quality PDF files, they
    generally lack the cool packaging and novelty of Relaxed.JS (and by
    "novelty", we really mean "recentness"). At this time, Relaxed.JS already
    has one active user, and we expect this number to grow several-fold after
    publication in your journal.

    We believe that this new piece of software would find an important echo in
    your audience, in particular among developers interested in the creation of
    beautiful PDF files using web technologies, in particular the *Pug*
    language. Please publish us pretty please.

.closing Yours sincelerely,

img.signature(src='signature.svg')

.sender
  .name Axel Red
  .title Head of Communications
  .institute Relaxed University
  .address United Kingdom

style
  include:scss letter.scss


================================================
FILE: test/samples/pug/local_plugin/config.yml
================================================
plugins:
  - say-my-name:
      name: Heisenberg


================================================
FILE: test/samples/pug/local_plugin/master.pug
================================================
div(style="text-align:center; font-size:58px; margin-top: 3cm")
  p Say my name with a mixin ! <br/> #{name}
  p Again, with a HTML filter ! <br/> INSERT_NAME_HERE
  
style.
  body {
    font-family: "Arial";
    font-size: 14px;
  }
  p {
     text-align:center;
     font-size:50px;
     font-weight: bold;
     margin-top: 3cm;
  }


================================================
FILE: test/samples/pug/local_plugin/say-my-name.plugin.js
================================================
const fs = require('fs')

exports.constructor = async function (pluginDefinition) {
  return {
    pugHeaders: [
      `- var name = "${pluginDefinition.name}"`
    ],
    htmlModifiers: [
      function (html) {
        return html.replace('INSERT_NAME_HERE', pluginDefinition.name)
      }
    ]
  }
}


================================================
FILE: test/samples/pug/mathjax/config.yml
================================================
plugins:
  - mathjax


================================================
FILE: test/samples/pug/mathjax/master.pug
================================================
h1 Hello Mathjax <br/> $$ \sum{ \sqrt{x^2 + \alpha} } $$

style.
  body {
    font-family: "Arial";
    font-size: 14px;
  }
  h1 {
     font-size: 60px;
     margin-top: 4cm;
     text-align: center
  }


================================================
FILE: test/samples/pug/utf8-characters/master.pug
================================================
h1 ★ ▹ ☀
h1 Ü ß
//- h1 漢 字

style.
  body {
    font-family: "Arial";
    font-size: 14px;
  }
  h1 {
     font-size: 150px;
     margin-top: 3cm;
     text-align: center
  }


================================================
FILE: test/samples/special_renderings/chartjs/donut.chart.js
================================================
{
  type: 'doughnut',
  data: {
    datasets: [{
      data: [10, 20, 30],
      backgroundColor:["#db7575", "#dbc575", "#75b0db"]
    }],
    labels: ['Ms Word', 'Google Docs', 'ReLaXed']
  },
  options: {
    width:350,
    height:350,
    devicePixelRatio: 0.8,
    legend: {
      position: 'bottom'
    }
  }
};


================================================
FILE: test/samples/special_renderings/htable_csv/diff.txt
================================================
thead
  th,<<
>>, ,<<   p >>,H1
  th,<<
>>, ,<<   p >>,H2
  th,<<
>>, ,<<   p >>,H3
tbody
  tr
    td,<<
>>, ,<<     p >>,A1
    td,<<
>>, ,<<     p >>,A2
    td,<<
>>, ,<<     p >>,A3
  tr
    td,<<
>>, ,<<     p >>,B1
    td,<<
>>, ,<<     p >>,B2
    td,<<
>>, ,<<     p >>,B3
  tr
    td,<<
>>, ,<<     p >>,C1
    td,<<
>>, ,<<     p >>,C2
    td,<<
>>, ,<<     p >>,C3


================================================
FILE: test/samples/special_renderings/htable_csv/expected.pug
================================================
thead
  th
    p H1
  th
    p H2
  th
    p H3
tbody
  tr
    td
      p A1
    td
      p A2
    td
      p A3
  tr
    td
      p B1
    td
      p B2
    td
      p B3
  tr
    td
      p C1
    td
      p C2
    td
      p C3


================================================
FILE: test/samples/special_renderings/htable_csv/sample.md.htable.csv
================================================
H1,H2,H3
A1,A2,A3
B1,B2,B3
C1,C2,C3


================================================
FILE: test/samples/special_renderings/table_csv/diff.txt
================================================
tbody
  tr
    td,<<
>>, ,<<     p >>,A1
    td,<<
>>, ,<<     p >>,A2
    td,<<
>>, ,<<     p >>,A3
  tr
    td,<<
>>, ,<<     p >>,B1
    td,<<
>>, ,<<     p >>,B2
    td,<<
>>, ,<<     p >>,B3
  tr
    td,<<
>>, ,<<     p >>,C1
    td,<<
>>, ,<<     p >>,C2
    td,<<
>>, ,<<     p >>,C3


================================================
FILE: test/samples/special_renderings/table_csv/expected.pug
================================================
tbody
  tr
    td
      p A1
    td
      p A2
    td
      p A3
  tr
    td
      p B1
    td
      p B2
    td
      p B3
  tr
    td
      p C1
    td
      p C2
    td
      p C3


================================================
FILE: test/samples/special_renderings/table_csv/sample.md.table.csv
================================================
A1,A2,A3
B1,B2,B3
C1,C2,C3


================================================
FILE: test/test.js
================================================
const { spawn } = require('child_process')
const path = require('path')
const fs = require('fs')
// const { pdfToPngThumbnail } = require('./pdf2png.js')
const PDFImage = require('pdf-image').PDFImage
const PixelDiff = require('pixel-diff')
const JsDiff = require('diff')
const assert = require('assert')

fs.writeFileSync("/tmp/absolute_path_test.scss", "h1 {color: red; font-size: 140px; text-align:center;}")

const relaxed = path.join(__dirname, './../src/index.js')

describe('Sample tests', function () {
  var tests = [
    {
      sampleName: 'basic_example',
      timeout: 10000
    },
    {
      sampleName: 'local_plugin',
      timeout: 10000
    },
    {
      sampleName: 'data_locals',
      timeout: 10000,
      cmdOptions: ['--locals', '{ "name": "Harry", "occupation": "Wazzzard" }']
    },
    {
      sampleName: 'data_locals_file',
      timeout: 10000,
      cmdOptions: ['--locals', './data.json']
    },
    {
      sampleName: 'data_require',
      timeout: 10000
    },
    {
      sampleName: 'mathjax',
      timeout: 10000
    }, {
      sampleName: 'katex',
      timeout: 10000
    },
    {
      sampleName: 'header_and_footer',
      timeout: 10000
    },
    {
      sampleName: 'utf8-characters',
      timeout: 10000
    },
    {
      sampleName: 'absolute_path',
      timeout: 10000,
      cmdOptions: ['--basedir', '/']
    }
  ]
  tests.forEach(function (test) {
    it('renders sample "' + test.sampleName + '" correctly', function (done) {
      this.timeout(test.timeout)
      var basedir = path.join(__dirname, 'samples', 'pug', test.sampleName)
      var paths = {
        master: path.join(basedir, 'master.pug'),
        expected: path.join(basedir, 'expected.png'),
        diff: path.join(basedir, 'diff.png'),
        pdf: path.join(basedir, 'master.pdf'),
        lastTestPNG: path.join(basedir, 'last_test_result.png'),
        html: path.join(basedir, 'master_temp.htm')
      }

      var process = spawn(
        relaxed,
        [paths.master, '--build-once', '--no-sandbox'].concat(test.cmdOptions || [])
      )
      process.on('close', async function (code) {
        assert.equal(code, 0)
        var pdfImage = new PDFImage(paths.pdf, {
          combinedImage: true,
          graphicsMagick: true
        })
        try {
          var imgPath = await pdfImage.convertFile()
        } catch (error) {
          done(error)
        }


        var diff = new PixelDiff({
          imageAPath: paths.expected,
          imageBPath: imgPath,
          thresholdType: PixelDiff.THRESHOLD_PERCENT,
          threshold: 0.01, // 1% threshold
          imageOutputPath: paths.diff
        })

        diff.run((error, result) => {
          fs.unlinkSync(paths.pdf)
          fs.unlinkSync(paths.html)
          fs.renameSync(imgPath, paths.lastTestPNG)
          if (error) {
            console.error(error)
          } else {
            assert(diff.hasPassed(result.code))
          }
          done()
        })
      })
    })
  })
})

describe('Error tests', function () {
  var tests = [
    {
      sampleName: 'error',
      timeout: 10000
    },
  ]
  tests.forEach(function (test) {
    it('fails to render sample "' + test.sampleName + '"', function (done) {
      this.timeout(test.timeout)
      var basedir = path.join(__dirname, 'samples', 'pug', test.sampleName)
      var process = spawn(
        relaxed,
        [path.join(basedir, 'master.pug'), '--build-once', '--no-sandbox'].concat(test.cmdOptions || [])
      )
      process.on('close', function (code) {
        assert.equal(code, 1)
        done()
      })
    })
  })
})

describe('Special rendering tests', function () {
  var tests = [
    {
      sampleName: 'table_csv',
      master: 'sample.md.table.csv',
      output: 'sample.pug',
      expected: 'expected.pug',
      outputType: 'text',
      timeout: 10000
    },
    {
      sampleName: 'htable_csv',
      master: 'sample.md.htable.csv',
      output: 'sample.pug',
      expected: 'expected.pug',
      outputType: 'text',
      timeout: 10000
    },
    {
      sampleName: 'chartjs',
      master: 'donut.chart.js',
      output: 'donut.png',
      expected: 'expected.png',
      outputType: 'image',
      timeout: 10000
    }
  ]
  tests.forEach(function (test) {
    it('renders sample "' + test.sampleName + '" correctly', function (done) {
      this.timeout(test.timeout)
      var basedir = path.join(__dirname, 'samples', 'special_renderings', test.sampleName)
      var extensions = {
        text: 'txt',
        image: 'png'
      }
      var diffExtension = extensions[test.outputType]
      var paths = {
        master: path.join(basedir, test.master),
        expected: path.join(basedir, test.expected),
        output: path.join(basedir, test.output),
        diff: path.join(basedir, 'diff.' + diffExtension),
        lastOutput: path.join(basedir, 'last_test_' + test.output)
      }
      var process = spawn(relaxed, [ paths.master, '--build-once' ])
      process.on('close', async function (code) {
        assert.equal(code, 0)
        if (test.outputType === 'text') {
          var expected = fs.readFileSync(paths.expected, 'utf8')
          var output = fs.readFileSync(paths.output, 'utf8')
          var isDifferent = (output !== expected)
          if (isDifferent) {
            var diff = JsDiff.diffChars(expected, output)
            var parts = []
            diff.forEach(function (part) {
              if (part.added) {
                parts.push(`[[${part.value}]]`)
              } else if (part.removed) {
                parts.push(`<<${part.value}>>`)
              } else {
                parts.push(part.value)
              }
            })
            fs.writeFileSync(paths.diff, parts.join())
            fs.renameSync(paths.output, paths.lastOutput)
            done(Error('Output differs from expectations'))
          } else {
            fs.renameSync(paths.output, paths.lastOutput)
            done()
          }
        } else if (test.outputType === 'image') {
          let diff = new PixelDiff({
            imageAPath: paths.expected,
            imageBPath: paths.output,
            thresholdType: PixelDiff.THRESHOLD_PERCENT,
            threshold: 0.01, // 1% threshold
            imageOutputPath: paths.diff
          })
          diff.run((error, result) => {
            fs.renameSync(paths.output, paths.lastOutput)
            if (error) {
              done(error)
            } else {
              assert(diff.hasPassed(result.code))
              done()
            }
          })
        }
      })
    })
  })
})



describe('Interactive tests', function () {
  var basedir = path.join(__dirname, 'samples', 'interactive_sample')
  var paths = [
    {
      diagramData: 'diagram.mermaid',
      output: ['diagram.svg'],
      timeout: 10000
    },
    {
      diagramData: 'plot.vegalite.json',
      output: ['plot.svg'],
      timeout: 10000
    }
  ]
  // var process = spawn('relaxed', [ path.join(basedir, 'master.pug') ])
  it('renders mermaid diagram interactively correctly (STUB)' , function (done) {
    // TODO: Implement tests
    done()
  })
})
Download .txt
gitextract_qbfk4w8g/

├── .github/
│   └── workflows/
│       └── npmpublish.yml
├── .gitignore
├── .npmignore
├── .travis.yml
├── LICENCE.txt
├── README.md
├── changelog.md
├── package.json
├── src/
│   ├── builtin_plugins/
│   │   ├── chartjs/
│   │   │   ├── index.js
│   │   │   └── template.pug
│   │   ├── flowchart/
│   │   │   ├── index.js
│   │   │   └── template.pug
│   │   ├── index.js
│   │   ├── katex/
│   │   │   ├── head.html
│   │   │   └── index.js
│   │   ├── markdown/
│   │   │   └── index.js
│   │   ├── mathjax/
│   │   │   └── index.js
│   │   ├── mermaid/
│   │   │   ├── index.js
│   │   │   └── template.pug
│   │   ├── scss/
│   │   │   └── index.js
│   │   ├── table/
│   │   │   ├── index.js
│   │   │   └── template.pug
│   │   └── vegalite/
│   │       ├── index.js
│   │       └── template.pug
│   ├── cli-tools/
│   │   ├── pdf2gif.js
│   │   └── pretty-pdf-thumbnail.js
│   ├── index.js
│   ├── masterToPDF.js
│   ├── parseLocals.js
│   └── plugins.js
└── test/
    ├── samples/
    │   ├── interactive_example/
    │   │   ├── diagrams/
    │   │   │   ├── diagram.mermaid
    │   │   │   └── plot.vegalite.json
    │   │   ├── master.pug
    │   │   └── report.scss
    │   ├── pug/
    │   │   ├── absolute_path/
    │   │   │   └── master.pug
    │   │   ├── basic_example/
    │   │   │   └── master.pug
    │   │   ├── data_locals/
    │   │   │   └── master.pug
    │   │   ├── data_locals_file/
    │   │   │   ├── data.json
    │   │   │   └── master.pug
    │   │   ├── data_require/
    │   │   │   ├── data.json
    │   │   │   └── master.pug
    │   │   ├── error/
    │   │   │   └── master.pug
    │   │   ├── header_and_footer/
    │   │   │   └── master.pug
    │   │   ├── katex/
    │   │   │   ├── config.yml
    │   │   │   └── master.pug
    │   │   ├── letter/
    │   │   │   ├── letter.scss
    │   │   │   └── master.pug
    │   │   ├── local_plugin/
    │   │   │   ├── config.yml
    │   │   │   ├── master.pug
    │   │   │   └── say-my-name.plugin.js
    │   │   ├── mathjax/
    │   │   │   ├── config.yml
    │   │   │   └── master.pug
    │   │   └── utf8-characters/
    │   │       └── master.pug
    │   └── special_renderings/
    │       ├── chartjs/
    │       │   └── donut.chart.js
    │       ├── htable_csv/
    │       │   ├── diff.txt
    │       │   ├── expected.pug
    │       │   └── sample.md.htable.csv
    │       └── table_csv/
    │           ├── diff.txt
    │           ├── expected.pug
    │           └── sample.md.table.csv
    └── test.js
Download .txt
SYMBOL INDEX (16 symbols across 7 files)

FILE: src/builtin_plugins/katex/index.js
  method katex (line 9) | katex (text, options) { return katex.renderToString(text) }

FILE: src/builtin_plugins/markdown/index.js
  function MarkdownPugFilter (line 13) | function MarkdownPugFilter (text, options) {

FILE: src/builtin_plugins/scss/index.js
  function ScssPugFilter (line 9) | function ScssPugFilter (text, options) {

FILE: src/builtin_plugins/table/index.js
  method handler (line 14) | async handler (tablePath, page) {

FILE: src/index.js
  function main (line 126) | async function main () {
  function build (line 158) | async function build (filepath) {
  function watch (line 240) | function watch () {
  function autodetectMasterFile (line 250) | function autodetectMasterFile (input) {

FILE: src/masterToPDF.js
  function getMatch (line 121) | function getMatch (string, query) {
  function waitForNetworkIdle (line 164) | function waitForNetworkIdle (page, timeout, maxInflightRequests = 0) {

FILE: src/parseLocals.js
  function logError (line 7) | function logError(error, message) {
  function readJsonFileAsString (line 12) | function readJsonFileAsString(jsonPath) {
  function parseJson (line 20) | function parseJson(str) {
  function isPathToJsonFile (line 28) | function isPathToJsonFile(filePath) {
  function isLastLocalJsonPath (line 32) | function isLastLocalJsonPath(filePath) {
  function parseLocals (line 36) | function parseLocals(locals, inputDir) {
Condensed preview — 61 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (80K chars).
[
  {
    "path": ".github/workflows/npmpublish.yml",
    "chars": 994,
    "preview": "name: Node.js Package\n\non:\n  release:\n    types: [created]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      -"
  },
  {
    "path": ".gitignore",
    "chars": 1205,
    "preview": "# 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# Directo"
  },
  {
    "path": ".npmignore",
    "chars": 123,
    "preview": "# https://docs.npmjs.com/misc/developers#keeping-files-out-of-your-package\n\n#tests\ntest\ncoverage\n\n#build tools\n.travis.y"
  },
  {
    "path": ".travis.yml",
    "chars": 205,
    "preview": "language: node_js\n\nnode_js:\n  - \"lts/*\"\n  - \"stable\"\n\nbefore_install:\n  - sudo apt-get -qq update\n  - sudo apt-get insta"
  },
  {
    "path": "LICENCE.txt",
    "chars": 740,
    "preview": "\nISC License (ISC)\n\nCopyright 2018, Zulko\n\nPermission to use, copy, modify, and/or distribute this software for any purp"
  },
  {
    "path": "README.md",
    "chars": 11306,
    "preview": "<p align=\"center\"><img width='270px' src=\"https://github.com/RelaxedJS/ReLaXed/raw/master/logo-blue.png\" /></p>\n\n# ReLaX"
  },
  {
    "path": "changelog.md",
    "chars": 1549,
    "preview": "# Changelog\n\n## v0.1.6\n\nFeatures:\n\n- Now exposing ``require`` for use in in-Pug javascript\n- Bibliography system, built-"
  },
  {
    "path": "package.json",
    "chars": 1207,
    "preview": "{\n  \"name\": \"relaxedjs\",\n  \"version\": \"0.2.6\",\n  \"description\": \"Create PDF documents using web technologies (PDF/SCSS)\""
  },
  {
    "path": "src/builtin_plugins/chartjs/index.js",
    "chars": 1194,
    "preview": "const pug = require('pug')\nconst fs = require('fs')\nconst path = require('path')\n\nexports.constructor = async function ("
  },
  {
    "path": "src/builtin_plugins/chartjs/template.pug",
    "chars": 794,
    "preview": "script(src='https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.2/Chart.min.js')\n\n#chartContainer\n  canvas#myChart\nscrip"
  },
  {
    "path": "src/builtin_plugins/flowchart/index.js",
    "chars": 1349,
    "preview": "const pug = require('pug')\nconst fs = require('fs')\nconst path = require('path')\n\nexports.constructor = async function ("
  },
  {
    "path": "src/builtin_plugins/flowchart/template.pug",
    "chars": 600,
    "preview": "script(src='http://cdnjs.cloudflare.com/ajax/libs/raphael/2.2.0/raphael-min.js')\nscript(src='http://cdnjs.cloudflare.com"
  },
  {
    "path": "src/builtin_plugins/index.js",
    "chars": 780,
    "preview": "const chartjs = require('./chartjs')\nconst table = require('./table')\nconst mathjax = require('./mathjax')\nconst markdow"
  },
  {
    "path": "src/builtin_plugins/katex/head.html",
    "chars": 210,
    "preview": "<link\n  rel=\"stylesheet\"\n  href=\"https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/katex.min.css\"\n  integrity=\"sha384-zB1R0"
  },
  {
    "path": "src/builtin_plugins/katex/index.js",
    "chars": 311,
    "preview": "const katex = require('katex')\nconst fs = require('fs')\nconst path = require('path')\n\nexports.constructor = async functi"
  },
  {
    "path": "src/builtin_plugins/markdown/index.js",
    "chars": 969,
    "preview": "var hljs = require('highlight.js') // https://highlightjs.org/\nvar markdown = require('markdown-it')\nvar mdFootnote = re"
  },
  {
    "path": "src/builtin_plugins/mathjax/index.js",
    "chars": 455,
    "preview": "const mjpage = require('mathjax-node-page')\n\n// TODO: would this work better if applied to the page instead of the HTML "
  },
  {
    "path": "src/builtin_plugins/mermaid/index.js",
    "chars": 839,
    "preview": "const pug = require('pug')\nconst fs = require('fs')\nconst path = require('path')\n\nexports.constructor = async function ("
  },
  {
    "path": "src/builtin_plugins/mermaid/template.pug",
    "chars": 112,
    "preview": "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",
    "chars": 304,
    "preview": "const sass = require('sass')\n\nexports.constructor = async function (params) {\n  return {\n    pugFilters: { scss: ScssPug"
  },
  {
    "path": "src/builtin_plugins/table/index.js",
    "chars": 1523,
    "preview": "\nconst pug = require('pug')\nconst fs = require('fs')\nconst path = require('path')\nconst csv = require('csvtojson')\nvar m"
  },
  {
    "path": "src/builtin_plugins/table/template.pug",
    "chars": 119,
    "preview": "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",
    "chars": 856,
    "preview": "const pug = require('pug')\nconst fs = require('fs')\nconst path = require('path')\n\nexports.constructor = async function ("
  },
  {
    "path": "src/builtin_plugins/vegalite/template.pug",
    "chars": 271,
    "preview": "script(src='https://cdn.jsdelivr.net/npm/vega@5.21.0')\nscript(src='https://cdn.jsdelivr.net/npm/vega-lite@5.2.0')\nscript"
  },
  {
    "path": "src/cli-tools/pdf2gif.js",
    "chars": 1161,
    "preview": "#!/usr/bin/env node\n\nconst colors = require('colors')\nconst program = require('commander')\nconst path = require('path')\n"
  },
  {
    "path": "src/cli-tools/pretty-pdf-thumbnail.js",
    "chars": 1136,
    "preview": "#!/usr/bin/env node\n\nconst colors = require('colors')\nconst program = require('commander')\nconst path = require('path')\n"
  },
  {
    "path": "src/index.js",
    "chars": 7828,
    "preview": "#!/usr/bin/env node\n\nconst colors = require('colors/safe')\nconst { program } = require('commander')\nconst chokidar = req"
  },
  {
    "path": "src/masterToPDF.js",
    "chars": 5628,
    "preview": "const pug = require('pug')\nconst colors = require('colors/safe')\nconst cheerio = require('cheerio')\nconst fs = require('"
  },
  {
    "path": "src/parseLocals.js",
    "chars": 1136,
    "preview": "const fs = require('fs')\nconst path = require('path')\nconst colors = require('colors/safe')\n\nlet lastLocalJsonPath\n\nfunc"
  },
  {
    "path": "src/plugins.js",
    "chars": 5410,
    "preview": "const path = require('path')\nconst colors = require('colors/safe')\nconst builtinPlugins = require('./builtin_plugins')\nc"
  },
  {
    "path": "test/samples/interactive_example/diagrams/diagram.mermaid",
    "chars": 100,
    "preview": "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",
    "chars": 603,
    "preview": "{\n  \"$schema\": \"https://vega.github.io/schema/vega-lite/v2.0.json\",\n  \"data\": {\n    \"values\": [\n      {\"a\": \"A\",\"b\": 28}"
  },
  {
    "path": "test/samples/interactive_example/master.pug",
    "chars": 5500,
    "preview": ".report-sidebar: p A sidebar will always make your document look nicer !\n\nh1#title Beautiful PDF documents with web tech"
  },
  {
    "path": "test/samples/interactive_example/report.scss",
    "chars": 2518,
    "preview": "@import 'https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.3.0/semantic.min.css';\n@import 'https://afeld.github.io/em"
  },
  {
    "path": "test/samples/pug/absolute_path/master.pug",
    "chars": 62,
    "preview": "h1 Hello !\n\nstyle\n  include:scss /tmp/absolute_path_test.scss\n"
  },
  {
    "path": "test/samples/pug/basic_example/master.pug",
    "chars": 122,
    "preview": "h1 Hello !\n\nstyle.\n  h1 {\n    font-size: 140px;\n    text-align:center;\n    font-family: \"Arial\";\n    margin-top: 5cm;\n  "
  },
  {
    "path": "test/samples/pug/data_locals/master.pug",
    "chars": 192,
    "preview": "h1 You're a #{ occupation }, #{ name } !!!!\n\n\nstyle.\n  body {\n    font-family: \"Arial\";\n    font-size: 14px;\n  }\n  h1 {\n"
  },
  {
    "path": "test/samples/pug/data_locals_file/data.json",
    "chars": 50,
    "preview": "{\n  \"name\": \"Harry\",\n  \"occupation\": \"Wazzzard\"\n}\n"
  },
  {
    "path": "test/samples/pug/data_locals_file/master.pug",
    "chars": 192,
    "preview": "h1 You're a #{ occupation }, #{ name } !!!!\n\n\nstyle.\n  body {\n    font-family: \"Arial\";\n    font-size: 14px;\n  }\n  h1 {\n"
  },
  {
    "path": "test/samples/pug/data_require/data.json",
    "chars": 50,
    "preview": "{\n  \"name\": \"Harry\",\n  \"occupation\": \"Wazzzard\"\n}\n"
  },
  {
    "path": "test/samples/pug/data_require/master.pug",
    "chars": 256,
    "preview": "- var data = require(path.join(basedir, 'data.json'))\nh1 You're a #{ data.occupation }, #{ data.name } !!!!\n\n\nstyle.\n  b"
  },
  {
    "path": "test/samples/pug/error/master.pug",
    "chars": 19,
    "preview": ": Pug syntax error\n"
  },
  {
    "path": "test/samples/pug/header_and_footer/master.pug",
    "chars": 929,
    "preview": "h1 bla\n\ntemplate#page-header\n  style(type='text/css').\n    .page-header {\n      font-size: 16px;\n      font-family: \"Ari"
  },
  {
    "path": "test/samples/pug/katex/config.yml",
    "chars": 19,
    "preview": "plugins:\n  - katex\n"
  },
  {
    "path": "test/samples/pug/katex/master.pug",
    "chars": 210,
    "preview": "h1 Hello Katex <br/><br/> #[:katex \\sum{ \\sqrt{x^2 + \\alpha}} ]\n\n\nstyle.\n  body {\n    font-family: \"Arial\";\n    font-siz"
  },
  {
    "path": "test/samples/pug/letter/letter.scss",
    "chars": 451,
    "preview": "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.h"
  },
  {
    "path": "test/samples/pug/letter/master.pug",
    "chars": 1536,
    "preview": ".header\n  img.logo(src='./relaxed-uni-logo.svg')\n\n.recipient\n  .name Dr. Eddie Thor\n  .institute Predatory Publishing Gr"
  },
  {
    "path": "test/samples/pug/local_plugin/config.yml",
    "chars": 49,
    "preview": "plugins:\n  - say-my-name:\n      name: Heisenberg\n"
  },
  {
    "path": "test/samples/pug/local_plugin/master.pug",
    "chars": 335,
    "preview": "div(style=\"text-align:center; font-size:58px; margin-top: 3cm\")\n  p Say my name with a mixin ! <br/> #{name}\n  p Again, "
  },
  {
    "path": "test/samples/pug/local_plugin/say-my-name.plugin.js",
    "chars": 304,
    "preview": "const fs = require('fs')\n\nexports.constructor = async function (pluginDefinition) {\n  return {\n    pugHeaders: [\n      `"
  },
  {
    "path": "test/samples/pug/mathjax/config.yml",
    "chars": 21,
    "preview": "plugins:\n  - mathjax\n"
  },
  {
    "path": "test/samples/pug/mathjax/master.pug",
    "chars": 204,
    "preview": "h1 Hello Mathjax <br/> $$ \\sum{ \\sqrt{x^2 + \\alpha} } $$\n\nstyle.\n  body {\n    font-family: \"Arial\";\n    font-size: 14px;"
  },
  {
    "path": "test/samples/pug/utf8-characters/master.pug",
    "chars": 175,
    "preview": "h1 ★ ▹ ☀\nh1 Ü ß\n//- h1 漢 字\n\nstyle.\n  body {\n    font-family: \"Arial\";\n    font-size: 14px;\n  }\n  h1 {\n     font-size: 15"
  },
  {
    "path": "test/samples/special_renderings/chartjs/donut.chart.js",
    "chars": 317,
    "preview": "{\n  type: 'doughnut',\n  data: {\n    datasets: [{\n      data: [10, 20, 30],\n      backgroundColor:[\"#db7575\", \"#dbc575\", "
  },
  {
    "path": "test/samples/special_renderings/htable_csv/diff.txt",
    "chars": 375,
    "preview": "thead\n  th,<<\n>>, ,<<   p >>,H1\n  th,<<\n>>, ,<<   p >>,H2\n  th,<<\n>>, ,<<   p >>,H3\ntbody\n  tr\n    td,<<\n>>, ,<<     p >"
  },
  {
    "path": "test/samples/special_renderings/htable_csv/expected.pug",
    "chars": 231,
    "preview": "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  "
  },
  {
    "path": "test/samples/special_renderings/htable_csv/sample.md.htable.csv",
    "chars": 36,
    "preview": "H1,H2,H3\nA1,A2,A3\nB1,B2,B3\nC1,C2,C3\n"
  },
  {
    "path": "test/samples/special_renderings/table_csv/diff.txt",
    "chars": 291,
    "preview": "tbody\n  tr\n    td,<<\n>>, ,<<     p >>,A1\n    td,<<\n>>, ,<<     p >>,A2\n    td,<<\n>>, ,<<     p >>,A3\n  tr\n    td,<<\n>>, "
  },
  {
    "path": "test/samples/special_renderings/table_csv/expected.pug",
    "chars": 183,
    "preview": "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"
  },
  {
    "path": "test/samples/special_renderings/table_csv/sample.md.table.csv",
    "chars": 27,
    "preview": "A1,A2,A3\nB1,B2,B3\nC1,C2,C3\n"
  },
  {
    "path": "test/test.js",
    "chars": 7147,
    "preview": "const { spawn } = require('child_process')\nconst path = require('path')\nconst fs = require('fs')\n// const { pdfToPngThum"
  }
]

About this extraction

This page contains the full source code of the RelaxedJS/ReLaXed GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 61 files (71.1 KB), approximately 21.6k tokens, and a symbol index with 16 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!