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
git
testfiles
mdconfig.json

@ -10,5 +10,8 @@
- command to include a stylesheet `[!stylesheet]: file.css`
- option to export to pdf `--pdf`
- option to watch (and export to html) `--watch`
- option to bundle everything in one file `--bundle`
- 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.
-w, --watch Watch files for changes
--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
[!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:
@ -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-math | math
| markdown-it-div | div
| markdown-it-multimd-table | multimd-table
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
```
## 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
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",
"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": {
"version": "1.4.1",
"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-math": "^4.1.1",
"markdown-it-mathjax": "^2.0.0",
"markdown-it-multimd-table": "^3.2.2",
"markdown-it-plantuml": "^1.4.1",
"markdown-it-smartarrows": "^1.0.1",
"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 {extname} from 'path';
import {extname, dirname} from 'path';
import {ArgumentParser} from "argparse";
import {MarkdownConfig} from "./lib/MarkdownConfig";
import {logger} from "./lib/logger";
/**
* Returns the filename without the extension.
@ -12,6 +14,7 @@ function noExt(file: string): string {
}
async function main() {
logger.profile('processing');
const parser = new ArgumentParser({
addHelp: true
});
@ -34,26 +37,42 @@ async function main() {
action: 'storeTrue'
}
);
parser.addArgument(
['--bundle'],
{
help: 'bundles the html to a standalone file',
required: false,
action: 'storeTrue'
}
);
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) {
let outputFile = noExt(args.file) + '.html';
renderer.on('rendered', async (html) => {
await writeFile(outputFile, html);
console.log(`File stored as ${outputFile}`);
});
renderer.watch(args.file);
} else if (args.pdf) {
let outputFile = noExt(args.file) + '.pdf';
await renderer.renderPdf(args.file, outputFile);
console.log(`File stored as ${outputFile}`);
} else {
let outputFile = noExt(args.file) + '.html';
let html = await renderer.render(args.file);
await writeFile(outputFile, html);
console.log(`File stored as ${outputFile}`);
}
logger.profile('processing');
}
4
main();

@ -4,18 +4,18 @@ import {Renderer} from "./Renderer";
import {markdownPlugins} from './plugins';
import {pageFormats} from "./formats";
import {PDFFormat} from "puppeteer";
import {getMarkdownPlugin} from "./utils";
import {logger} from "./logger";
export class CommandParser {
public projectFiles: string[];
public pageFormat: PDFFormat;
public loadedPlugins: string[];
public stylesheets: string[];
private resolvePath: {path: string, lines: number}[];
private readonly resolvePath: {path: string, lines: number}[];
constructor() {
this.projectFiles = [];
this.loadedPlugins = [];
this.resolvePath = [];
this.stylesheets = [];
}
@ -50,9 +50,9 @@ export class CommandParser {
switch(match[1]) {
case 'use':
let plugins = match[2].split(',');
console.log(`Using plugins: ${match[2]}`);
logger.verbose(`Adding plugins: ${match[2]}`);
for (let mdPlugin of plugins) {
this.addMarkdownPlugin(mdPlugin.replace(/^ *| *$/g, ''), renderer);
renderer.addPlugin(getMarkdownPlugin(mdPlugin.replace(/^ *| *$/g, '')));
}
break;
case 'include':
@ -62,10 +62,10 @@ export class CommandParser {
inputLines.unshift(...included);
this.resolvePath.push({path: match[2], lines: included.length});
} else {
console.error(`Circular reference detected. Skipping include ${match[2]}`);
logger.warning(`Circular reference detected. Skipping include ${match[2]}`);
}
} catch (err) {
console.error(err.message);
logger.error(err.message);
outputLines.push(inputLine);
}
break;
@ -74,11 +74,10 @@ export class CommandParser {
// @ts-ignore
this.pageFormat = match[2];
else
console.log('Invalid page format or format already set: ' + match[2]);
logger.warning('Invalid page format or format already set: ' + match[2]);
break;
case 'newpage':
if (!this.loadedPlugins.includes(markdownPlugins.div))
this.addMarkdownPlugin('div', renderer);
renderer.addPlugin(getMarkdownPlugin(markdownPlugins.div));
outputLines.push('::: .newpage \n:::');
break;
case 'stylesheet':
@ -95,29 +94,6 @@ export class CommandParser {
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.
* @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 {EventEmitter} from "events";
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 {
private md: MarkdownIt;
private readonly beforeRendering: Function[];
private readonly afterRendering: Function[];
private commandParser: CommandParser;
private config: MarkdownConfig;
constructor() {
constructor(config?: MarkdownConfig) {
super();
this.config = config || new MarkdownConfig();
this.md = new MarkdownIt();
this.beforeRendering = [];
this.afterRendering = [];
@ -24,6 +28,13 @@ export class Renderer extends EventEmitter {
this.configure();
}
/**
* Returns the current config.
*/
public get mdConfig() {
return this.config;
}
/**
* Assign a function that should be used before rendering.
* @param func
@ -45,7 +56,16 @@ export class Renderer extends EventEmitter {
* @param mdPlugin
*/
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);
let document = await fsx.readFile(filename, 'utf-8');
document = document.replace(/\r\n/g, '\n');
logger.verbose(`Applying ${this.beforeRendering.length} beforeRendering functions...`);
for (let func of this.beforeRendering) {
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);
logger.verbose(`Applying ${this.afterRendering.length} afterRendering functions...`);
for (let func of this.afterRendering) {
result = await func(result, filename, this);
}
logger.verbose('HTML rendered');
this.emit('rendered', result);
return result;
}
@ -78,12 +103,25 @@ export class Renderer extends EventEmitter {
let html = await this.render(filename);
if (this.commandParser.pageFormat)
format = this.commandParser.pageFormat;
console.log('Launching puppeteer');
logger.info('Launching puppeteer');
const browser = await puppeteer.launch();
const page = await browser.newPage();
logger.info('Setting and evaluating content');
await page.setContent(html);
console.log(`Starting PDF export (format: ${format}) to ${output}`);
await page.pdf({path: output, format: format, margin: {top: '1.5cm', bottom: '1.5cm'}});
await page.waitForFunction('window.MathJax.isReady === true');
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();
}
@ -94,11 +132,11 @@ export class Renderer extends EventEmitter {
public watch(filename: string) {
const watcher = chokidar.watch(filename);
watcher.on('change', async () => {
console.log('Change detected. Rerendering');
logger.info('Change detected. Rerendering');
let start = Date.now();
this.md = new MarkdownIt();
await this.render(filename);
console.log(`Rendering took ${Date.now() - start} ms.`);
logger.info(`Rendering took ${Date.now() - start} ms.`);
});
this.on('rendered', () => {
watcher.add(this.commandParser.projectFiles);
@ -115,51 +153,51 @@ export class Renderer extends EventEmitter {
this.useAfter((doc: string) => new JSDOM(doc));
// include default style
this.useAfter(async (dom: JSDOM) => {
let styleTag = dom.window.document.createElement('style');
// append the default style
styleTag.innerHTML = await fsx.readFile(path.join(__dirname, 'styles/default.css'), 'utf-8');
dom.window.document.head.appendChild(styleTag);
logger.debug('Including default style');
let stylePath = path.join(__dirname, '../styles/default.css');
let document = dom.window.document;
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;
});
// include user defined styles
this.useAfter(async (dom: JSDOM) => {
logger.debug(`Including ${this.config.stylesheets.size} user styles.`);
let userStyles = dom.window.document.createElement('style');
userStyles.setAttribute('id', 'user-style');
// append all user defined stylesheets
for (let stylesheet of this.commandParser.stylesheets) {
for (let stylesheet of this.config.stylesheets) {
logger.debug(`Including ${stylesheet}`);
userStyles.innerHTML += await fsx.readFile(stylesheet, 'utf-8');
}
dom.window.document.head.appendChild(userStyles);
return dom;
});
this.useAfter(includeMathJax);
// include all images as base64
this.useAfter(async (dom: JSDOM, mainfile: string) => {
let document = dom.window.document;
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);
if (this.config.bundle)
this.useAfter(bundleImages);
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);
}
img.src = base64Url;
}
return dom;
});
this.useAfter((dom: JSDOM) => dom.serialize());
}
}

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

Loading…
Cancel
Save