Config files

- added support for `mdconf.json`config file
- added markdown-it plugins
- added mathjax evaluation script
- deactivated math styling
develop
Trivernis 5 years ago
parent 62d75c3a27
commit 21fd8356c3

1
.gitignore vendored

@ -6,3 +6,4 @@ test.html
testchapter.md testchapter.md
git git
testfiles testfiles
mdconfig.json

@ -10,5 +10,8 @@
- command to include a stylesheet `[!stylesheet]: file.css` - command to include a stylesheet `[!stylesheet]: file.css`
- option to export to pdf `--pdf` - option to export to pdf `--pdf`
- option to watch (and export to html) `--watch` - option to watch (and export to html) `--watch`
- option to bundle everything in one file `--bundle`
- stylesheets supporting math and code highlighting (with highlightjs) - stylesheets supporting math and code highlighting (with highlightjs)
- auto base64 converting for images for a standalone html - auto base64 converting for images for a standalone html when bundling flag is set
- support for `mdconf.json`config file
- MathJax script to html

@ -14,7 +14,7 @@ Optional arguments:
-h, --help Show this help message and exit. -h, --help Show this help message and exit.
-w, --watch Watch files for changes -w, --watch Watch files for changes
--pdf Output as pdf --pdf Output as pdf
--bundle Bundle all images and script in one html
``` ```
@ -35,6 +35,8 @@ The usage of a markdown-it plugin inside a document can be decleared by using
```markdown ```markdown
[!use]: plugin1, plugin2, plugin3 [!use]: plugin1, plugin2, plugin3
or
[!use]: # (plugin1, plugin2, plugin3)
``` ```
The plugin names are listed in the following table. Basically it is just the package name with the markdown-it removed: The plugin names are listed in the following table. Basically it is just the package name with the markdown-it removed:
@ -56,6 +58,7 @@ The plugin names are listed in the following table. Basically it is just the pac
| markdown-it-mathjax | mathjax | markdown-it-mathjax | mathjax
| markdown-it-math | math | markdown-it-math | math
| markdown-it-div | div | markdown-it-div | div
| markdown-it-multimd-table | multimd-table
For example you can declare the use of `markdown-it-emoji` the following: For example you can declare the use of `markdown-it-emoji` the following:
@ -90,6 +93,24 @@ You can include your own stylesheet. It is applied after the default style. The
[!stylesheet]: path/to/style.css [!stylesheet]: path/to/style.css
``` ```
## Configuration file
You can also define plugins, stylesheets and other stuff by using a `mdconf.json` file in the same directory as the main markdown file. Example config:
```json
{
"format": "A4",
"plugins": [
"emoji",
"footnote",
"markdown-it-multimd-table"
],
"stylesheets": [
"customstyle.css"
]
}
```
## Other stuff ## Other stuff
Images and stylesheets are included within the html file. All image urls are converted into base64 urls. Images and stylesheets are included within the html file. All image urls are converted into base64 urls.

22
package-lock.json generated

@ -3456,6 +3456,28 @@
"resolved": "https://registry.npmjs.org/markdown-it-mathjax/-/markdown-it-mathjax-2.0.0.tgz", "resolved": "https://registry.npmjs.org/markdown-it-mathjax/-/markdown-it-mathjax-2.0.0.tgz",
"integrity": "sha1-ritPTFxxmgP55HXGZPeyaFIx2ek=" "integrity": "sha1-ritPTFxxmgP55HXGZPeyaFIx2ek="
}, },
"markdown-it-multimd-table": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/markdown-it-multimd-table/-/markdown-it-multimd-table-3.2.2.tgz",
"integrity": "sha512-mqv+/5XE4D75yT6CQP/eyezWIrBJWu6pY6LLL8qDs/Gnh6PxfdxO+T4A0oYoHx8RZLvUZZWWNrqplLPw4euuGQ==",
"requires": {
"markdown-it": "^8.4.2"
},
"dependencies": {
"markdown-it": {
"version": "8.4.2",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.4.2.tgz",
"integrity": "sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==",
"requires": {
"argparse": "^1.0.7",
"entities": "~1.1.1",
"linkify-it": "^2.0.0",
"mdurl": "^1.0.1",
"uc.micro": "^1.0.5"
}
}
}
},
"markdown-it-plantuml": { "markdown-it-plantuml": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/markdown-it-plantuml/-/markdown-it-plantuml-1.4.1.tgz", "resolved": "https://registry.npmjs.org/markdown-it-plantuml/-/markdown-it-plantuml-1.4.1.tgz",

@ -47,6 +47,7 @@
"markdown-it-mark": "^2.0.0", "markdown-it-mark": "^2.0.0",
"markdown-it-math": "^4.1.1", "markdown-it-math": "^4.1.1",
"markdown-it-mathjax": "^2.0.0", "markdown-it-mathjax": "^2.0.0",
"markdown-it-multimd-table": "^3.2.2",
"markdown-it-plantuml": "^1.4.1", "markdown-it-plantuml": "^1.4.1",
"markdown-it-smartarrows": "^1.0.1", "markdown-it-smartarrows": "^1.0.1",
"markdown-it-sub": "^1.0.0", "markdown-it-sub": "^1.0.0",

@ -1,7 +1,9 @@
import {Renderer} from "./Renderer"; import {Renderer} from "./lib/Renderer";
import {writeFile} from 'fs-extra'; import {writeFile} from 'fs-extra';
import {extname} from 'path'; import {extname, dirname} from 'path';
import {ArgumentParser} from "argparse"; import {ArgumentParser} from "argparse";
import {MarkdownConfig} from "./lib/MarkdownConfig";
import {logger} from "./lib/logger";
/** /**
* Returns the filename without the extension. * Returns the filename without the extension.
@ -12,6 +14,7 @@ function noExt(file: string): string {
} }
async function main() { async function main() {
logger.profile('processing');
const parser = new ArgumentParser({ const parser = new ArgumentParser({
addHelp: true addHelp: true
}); });
@ -34,26 +37,42 @@ async function main() {
action: 'storeTrue' action: 'storeTrue'
} }
); );
parser.addArgument(
['--bundle'],
{
help: 'bundles the html to a standalone file',
required: false,
action: 'storeTrue'
}
);
let args = parser.parseArgs(); let args = parser.parseArgs();
let renderer = new Renderer(); let config = new MarkdownConfig(dirname(args.file));
config.bundle = args.bundle || args.pdf;
let renderer = new Renderer(config);
if (args.watch) { if (args.watch) {
let outputFile = noExt(args.file) + '.html'; let outputFile = noExt(args.file) + '.html';
renderer.on('rendered', async (html) => { renderer.on('rendered', async (html) => {
await writeFile(outputFile, html); await writeFile(outputFile, html);
console.log(`File stored as ${outputFile}`); console.log(`File stored as ${outputFile}`);
}); });
renderer.watch(args.file); renderer.watch(args.file);
} else if (args.pdf) { } else if (args.pdf) {
let outputFile = noExt(args.file) + '.pdf'; let outputFile = noExt(args.file) + '.pdf';
await renderer.renderPdf(args.file, outputFile); await renderer.renderPdf(args.file, outputFile);
console.log(`File stored as ${outputFile}`); console.log(`File stored as ${outputFile}`);
} else { } else {
let outputFile = noExt(args.file) + '.html'; let outputFile = noExt(args.file) + '.html';
let html = await renderer.render(args.file); let html = await renderer.render(args.file);
await writeFile(outputFile, html); await writeFile(outputFile, html);
console.log(`File stored as ${outputFile}`); console.log(`File stored as ${outputFile}`);
} }
logger.profile('processing');
} }
4
main(); main();

@ -4,18 +4,18 @@ import {Renderer} from "./Renderer";
import {markdownPlugins} from './plugins'; import {markdownPlugins} from './plugins';
import {pageFormats} from "./formats"; import {pageFormats} from "./formats";
import {PDFFormat} from "puppeteer"; import {PDFFormat} from "puppeteer";
import {getMarkdownPlugin} from "./utils";
import {logger} from "./logger";
export class CommandParser { export class CommandParser {
public projectFiles: string[]; public projectFiles: string[];
public pageFormat: PDFFormat; public pageFormat: PDFFormat;
public loadedPlugins: string[];
public stylesheets: string[]; public stylesheets: string[];
private resolvePath: {path: string, lines: number}[]; private readonly resolvePath: {path: string, lines: number}[];
constructor() { constructor() {
this.projectFiles = []; this.projectFiles = [];
this.loadedPlugins = [];
this.resolvePath = []; this.resolvePath = [];
this.stylesheets = []; this.stylesheets = [];
} }
@ -50,9 +50,9 @@ export class CommandParser {
switch(match[1]) { switch(match[1]) {
case 'use': case 'use':
let plugins = match[2].split(','); let plugins = match[2].split(',');
console.log(`Using plugins: ${match[2]}`); logger.verbose(`Adding plugins: ${match[2]}`);
for (let mdPlugin of plugins) { for (let mdPlugin of plugins) {
this.addMarkdownPlugin(mdPlugin.replace(/^ *| *$/g, ''), renderer); renderer.addPlugin(getMarkdownPlugin(mdPlugin.replace(/^ *| *$/g, '')));
} }
break; break;
case 'include': case 'include':
@ -62,10 +62,10 @@ export class CommandParser {
inputLines.unshift(...included); inputLines.unshift(...included);
this.resolvePath.push({path: match[2], lines: included.length}); this.resolvePath.push({path: match[2], lines: included.length});
} else { } else {
console.error(`Circular reference detected. Skipping include ${match[2]}`); logger.warning(`Circular reference detected. Skipping include ${match[2]}`);
} }
} catch (err) { } catch (err) {
console.error(err.message); logger.error(err.message);
outputLines.push(inputLine); outputLines.push(inputLine);
} }
break; break;
@ -74,11 +74,10 @@ export class CommandParser {
// @ts-ignore // @ts-ignore
this.pageFormat = match[2]; this.pageFormat = match[2];
else else
console.log('Invalid page format or format already set: ' + match[2]); logger.warning('Invalid page format or format already set: ' + match[2]);
break; break;
case 'newpage': case 'newpage':
if (!this.loadedPlugins.includes(markdownPlugins.div)) renderer.addPlugin(getMarkdownPlugin(markdownPlugins.div));
this.addMarkdownPlugin('div', renderer);
outputLines.push('::: .newpage \n:::'); outputLines.push('::: .newpage \n:::');
break; break;
case 'stylesheet': case 'stylesheet':
@ -95,29 +94,6 @@ export class CommandParser {
return outputLines.join('\n'); return outputLines.join('\n');
} }
/**
* Adds a markdown-it plugin to the renderer
* @param pluginName
* @param renderer
*/
private addMarkdownPlugin(pluginName: string, renderer: Renderer): void {
try {
// @ts-ignore
let moduleName = markdownPlugins[pluginName];
if (moduleName) {
let plugin: any = require(moduleName);
if (plugin && !this.loadedPlugins.includes(plugin)) {
renderer.addPlugin(plugin);
this.loadedPlugins.push(plugin);
}
} else {
console.error(`Plugin "${pluginName}" not found.`);
}
} catch (err) {
console.error(err);
}
}
/** /**
* Imports a file into the markdown. * Imports a file into the markdown.
* @param filepath * @param filepath

@ -0,0 +1,52 @@
import * as fsx from 'fs-extra';
import * as path from 'path';
import {getMarkdownPlugin} from "./utils";
const confName = 'mdconfig.json';
/**
* Markdown config class for easyer access on the configuration.
*/
export class MarkdownConfig {
public plugins: Set<any> = new Set<any>();
public stylesheets: Set<string> = new Set<string>();
public format: string = 'A4';
public bundle: boolean = false;
private readonly filename: string;
/**
* Creates a new config with the given directory or the processes work directory.
* @param dirname
*/
constructor(dirname?: string) {
dirname = dirname || process.cwd();
let confFile: string = path.join(dirname, confName);
if (fsx.pathExistsSync(confFile)) {
this.filename = confFile;
this.loadData();
}
}
/**
* Loads the data from a config file.
*/
private loadData() {
let configData = fsx.readJsonSync(this.filename);
this.format = configData.format || this.format;
this.bundle = configData.bundle;
if (configData.plugins)
this.plugins = new Set<string>(configData.plugins.map(getMarkdownPlugin));
if (configData.stylesheets)
this.stylesheets = new Set<string>(configData.stylesheets);
}
/**
* Saves the config file to a config file.
* @param filename
*/
public save(filename?: string) {
let confFile = filename || this.filename;
fsx.writeJsonSync(confFile, this);
}
}

@ -7,16 +7,20 @@ import {JSDOM} from 'jsdom';
import {CommandParser} from "./CommandParser"; import {CommandParser} from "./CommandParser";
import {EventEmitter} from "events"; import {EventEmitter} from "events";
import {PDFFormat} from "puppeteer"; import {PDFFormat} from "puppeteer";
import fetch from 'node-fetch'; import {MarkdownConfig} from "./MarkdownConfig";
import {bundleImages, delay, includeMathJax} from "./utils";
import {logger} from "./logger";
export class Renderer extends EventEmitter { export class Renderer extends EventEmitter {
private md: MarkdownIt; private md: MarkdownIt;
private readonly beforeRendering: Function[]; private readonly beforeRendering: Function[];
private readonly afterRendering: Function[]; private readonly afterRendering: Function[];
private commandParser: CommandParser; private commandParser: CommandParser;
private config: MarkdownConfig;
constructor() { constructor(config?: MarkdownConfig) {
super(); super();
this.config = config || new MarkdownConfig();
this.md = new MarkdownIt(); this.md = new MarkdownIt();
this.beforeRendering = []; this.beforeRendering = [];
this.afterRendering = []; this.afterRendering = [];
@ -24,6 +28,13 @@ export class Renderer extends EventEmitter {
this.configure(); this.configure();
} }
/**
* Returns the current config.
*/
public get mdConfig() {
return this.config;
}
/** /**
* Assign a function that should be used before rendering. * Assign a function that should be used before rendering.
* @param func * @param func
@ -45,7 +56,16 @@ export class Renderer extends EventEmitter {
* @param mdPlugin * @param mdPlugin
*/ */
public addPlugin(mdPlugin: any) { public addPlugin(mdPlugin: any) {
this.md.use(mdPlugin); this.config.plugins.add(mdPlugin);
}
/**
* Adds a stylesheet to the config.
* The stylesheets will be included after the md-it render.
* @param filepath
*/
public addStylesheet(filepath: string) {
this.config.stylesheets.add(filepath)
} }
/** /**
@ -56,14 +76,19 @@ export class Renderer extends EventEmitter {
filename = path.resolve(filename); filename = path.resolve(filename);
let document = await fsx.readFile(filename, 'utf-8'); let document = await fsx.readFile(filename, 'utf-8');
document = document.replace(/\r\n/g, '\n'); document = document.replace(/\r\n/g, '\n');
logger.verbose(`Applying ${this.beforeRendering.length} beforeRendering functions...`);
for (let func of this.beforeRendering) { for (let func of this.beforeRendering) {
document = await func(document, filename, this); document = await func(document, filename, this);
} }
this.addConfigPlugins();
logger.verbose(`Rendering with markdown-it and ${this.config.plugins.size} plugins`);
let result: string = this.md.render(document); let result: string = this.md.render(document);
logger.verbose(`Applying ${this.afterRendering.length} afterRendering functions...`);
for (let func of this.afterRendering) { for (let func of this.afterRendering) {
result = await func(result, filename, this); result = await func(result, filename, this);
} }
logger.verbose('HTML rendered');
this.emit('rendered', result); this.emit('rendered', result);
return result; return result;
} }
@ -78,12 +103,25 @@ export class Renderer extends EventEmitter {
let html = await this.render(filename); let html = await this.render(filename);
if (this.commandParser.pageFormat) if (this.commandParser.pageFormat)
format = this.commandParser.pageFormat; format = this.commandParser.pageFormat;
console.log('Launching puppeteer'); logger.info('Launching puppeteer');
const browser = await puppeteer.launch(); const browser = await puppeteer.launch();
const page = await browser.newPage(); const page = await browser.newPage();
logger.info('Setting and evaluating content');
await page.setContent(html); await page.setContent(html);
console.log(`Starting PDF export (format: ${format}) to ${output}`); await page.waitForFunction('window.MathJax.isReady === true');
await page.pdf({path: output, format: format, margin: {top: '1.5cm', bottom: '1.5cm'}}); await delay(1000);
logger.info(`Starting PDF export (format: ${format}) to ${output}`);
await page.pdf({
path: output,
format: format,
printBackground: true,
margin: {
top: '1.5cm',
bottom: '1.5cm',
left: '1.5cm',
right: '1.5cm'
}
});
await browser.close(); await browser.close();
} }
@ -94,11 +132,11 @@ export class Renderer extends EventEmitter {
public watch(filename: string) { public watch(filename: string) {
const watcher = chokidar.watch(filename); const watcher = chokidar.watch(filename);
watcher.on('change', async () => { watcher.on('change', async () => {
console.log('Change detected. Rerendering'); logger.info('Change detected. Rerendering');
let start = Date.now(); let start = Date.now();
this.md = new MarkdownIt(); this.md = new MarkdownIt();
await this.render(filename); await this.render(filename);
console.log(`Rendering took ${Date.now() - start} ms.`); logger.info(`Rendering took ${Date.now() - start} ms.`);
}); });
this.on('rendered', () => { this.on('rendered', () => {
watcher.add(this.commandParser.projectFiles); watcher.add(this.commandParser.projectFiles);
@ -115,51 +153,51 @@ export class Renderer extends EventEmitter {
this.useAfter((doc: string) => new JSDOM(doc)); this.useAfter((doc: string) => new JSDOM(doc));
// include default style // include default style
this.useAfter(async (dom: JSDOM) => { this.useAfter(async (dom: JSDOM) => {
let styleTag = dom.window.document.createElement('style'); logger.debug('Including default style');
// append the default style let stylePath = path.join(__dirname, '../styles/default.css');
styleTag.innerHTML = await fsx.readFile(path.join(__dirname, 'styles/default.css'), 'utf-8'); let document = dom.window.document;
dom.window.document.head.appendChild(styleTag);
if (this.config.bundle) {
let styleTag = document.createElement('style');
styleTag.innerHTML = await fsx.readFile(stylePath, 'utf-8');
document.head.appendChild(styleTag);
} else {
let linkTag = document.createElement('link');
linkTag.rel = 'stylesheet';
linkTag.href = stylePath;
document.head.appendChild(linkTag);
}
return dom; return dom;
}); });
// include user defined styles // include user defined styles
this.useAfter(async (dom: JSDOM) => { this.useAfter(async (dom: JSDOM) => {
logger.debug(`Including ${this.config.stylesheets.size} user styles.`);
let userStyles = dom.window.document.createElement('style'); let userStyles = dom.window.document.createElement('style');
userStyles.setAttribute('id', 'user-style'); userStyles.setAttribute('id', 'user-style');
// append all user defined stylesheets for (let stylesheet of this.config.stylesheets) {
for (let stylesheet of this.commandParser.stylesheets) { logger.debug(`Including ${stylesheet}`);
userStyles.innerHTML += await fsx.readFile(stylesheet, 'utf-8'); userStyles.innerHTML += await fsx.readFile(stylesheet, 'utf-8');
} }
dom.window.document.head.appendChild(userStyles); dom.window.document.head.appendChild(userStyles);
return dom; return dom;
}); });
this.useAfter(includeMathJax);
// include all images as base64 // include all images as base64
this.useAfter(async (dom: JSDOM, mainfile: string) => { if (this.config.bundle)
let document = dom.window.document; this.useAfter(bundleImages);
let mainFolder = path.dirname(mainfile);
let imgs = document.querySelectorAll('img');
for (let img of imgs) {
let source = img.src;
let filepath = source;
let base64Url = source;
if (!path.isAbsolute(filepath))
filepath = path.join(mainFolder, filepath);
if (await fsx.pathExists(filepath)) {
let type = path.extname(source).slice(1);
base64Url = `data:image/${type};base64,`;
base64Url += (await fsx.readFile(filepath)).toString('base64');
} else {
try {
let response = await fetch(source);
base64Url = `data:${response.headers.get('content-type')};base64,`;
base64Url += (await response.buffer()).toString('base64');
} catch (error) {
console.error(error);
}
}
img.src = base64Url;
}
return dom;
});
this.useAfter((dom: JSDOM) => dom.serialize()); this.useAfter((dom: JSDOM) => dom.serialize());
} }
/**
* Adds all plugins from the config to markdown-it.
*/
private addConfigPlugins() {
for (let plugin of this.config.plugins) {
try {
this.md.use(plugin);
} catch (err) {
logger.error(err);
}
}
}
} }

@ -0,0 +1,15 @@
import * as winston from "winston";
export const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
),
level: process.env.NODE_ENV === 'production'? 'info' : 'silly'
})
]
});

@ -0,0 +1,83 @@
import {JSDOM} from "jsdom";
import * as path from "path";
import * as fsx from "fs-extra";
import fetch from "node-fetch";
import {logger} from "./logger";
import {markdownPlugins} from "./plugins";
const mathJaxUrl = 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML';
/**
* Bundles all images in the image tags.
* @param dom
* @param mainfile
*/
export async function bundleImages(dom: JSDOM, mainfile: string): Promise<JSDOM> {
let document = dom.window.document;
let mainFolder = path.dirname(mainfile);
let imgs = document.querySelectorAll('img');
logger.debug(`Bundling ${imgs.length} images.`);
for (let img of imgs) {
let source = img.src;
let filepath = source;
let base64Url = source;
if (!path.isAbsolute(filepath))
filepath = path.join(mainFolder, filepath);
if (await fsx.pathExists(filepath)) {
let type = path.extname(source).slice(1);
base64Url = `data:image/${type};base64,`;
base64Url += (await fsx.readFile(filepath)).toString('base64');
} else {
try {
let response = await fetch(source);
base64Url = `data:${response.headers.get('content-type')};base64,`;
base64Url += (await response.buffer()).toString('base64');
} catch (error) {
logger.error(error);
}
}
img.src = base64Url;
}
return dom;
}
/**
* Includes mathjax in the dom.
* @param dom
*/
export function includeMathJax(dom: JSDOM): JSDOM {
let document = dom.window.document;
let scriptTag = document.createElement('script');
scriptTag.src = mathJaxUrl;
document.head.appendChild(scriptTag);
return dom;
}
/**
* Returns the markdown plugin associated with the pluginName
* @param pluginName
*/
export function getMarkdownPlugin(pluginName: string) {
logger.debug(`Trying to resolve plugin ${pluginName}`);
if (markdownPlugins[pluginName]) {
return require(markdownPlugins[pluginName]);
} else {
try {
let plugin = require(pluginName);
if (plugin)
return plugin;
} catch (err) {
console.error(`Module ${pluginName} not found.`);
}
}
}
/**
* Asynchronous elay function
* @param milliseconds
*/
export function delay(milliseconds: number) {
return new Promise(function(resolve) {
setTimeout(resolve, milliseconds)
});
}

@ -1,11 +1,9 @@
@import includes/vars @import includes/vars
@import includes/w3-math //@import includes/w3-math
@import includes/highlighjs @import includes/highlighjs
body body
font-family: Arial, sans-serif font-family: Arial, sans-serif
width: 80%
margin: auto
pre pre
background-color: $backgroundSecondary background-color: $backgroundSecondary
@ -23,7 +21,7 @@ nav
page-break-after: always page-break-after: always
img img
width: 100% max-width: 100%
height: auto height: auto
.page .page

Loading…
Cancel
Save