lib/utilities/markdown-color.js
'use strict';
const fs = require('fs');
const chalk = require('chalk');
const SilentError = require('silent-error');
const { merge } = require('ember-cli-lodash-subset');
let colors = ['red', 'green', 'blue', 'cyan', 'magenta', 'yellow', 'black', 'white', 'grey', 'gray'];
let backgroundColors = ['bgRed', 'bgGreen', 'bgBlue', 'bgCyan', 'bgMagenta', 'bgYellow', 'bgWhite', 'bgBlack'];
/*
Parses markdown and applies coloring to output by matching defined tokens and
applying styling. Default outputs chalk.js coloring for terminal output.
Options:
tokens: pass additional tokens in the following format:
{
name:{
token: '(Reserved)', // example of token
pattern: /(Reserved)/g, // regexp pattern
render(string) { return string + '!'} // function that takes and returns a string
}
}
markdownOptions: object containing options to pass (or override) to markdown-it-terminal upon
instantiation. See https://github.com/trabus/markdown-it-terminal/blob/master/index.js#L8
renderStyles: pass an object to render color styles, default is chalk.js
@class MarkdownColor
@constructor
@param {Object} [options]
*/
class MarkdownColor {
constructor(options) {
let optionTokens = (options && options.tokens) || {};
let renderStyles = (options && options.renderStyles) || chalk;
let tokens = this.generateTokens(renderStyles);
let markdownOptions = (options && options.markdownOptions) || {};
const markdown = require('markdown-it');
const terminal = require('markdown-it-terminal');
this.options = options || {};
this.markdown = markdown().use(terminal, markdownOptions);
this.tokens = merge(tokens, optionTokens);
}
/*
Lookup markdown file and render contents
@method renderFile
@param {String} [filePath]
@param {Object} [options]
*/
renderFile(filePath, options) {
let file;
try {
file = fs.readFileSync(filePath, 'utf-8');
} catch (e) {
throw new SilentError(`The file '${filePath}' doesn't exist. Please check your filePath`);
}
return this.render(file, options);
}
/*
Parse markdown and output as string
@method render
@param {String} [string]
@param {Object} [options]
@return {String}
*/
render(string, options) {
let indent = (options && options.indent) || '';
let input = this.markdown.render(string);
let styles = Object.keys(this.tokens);
input = input.replace(/^/gm, indent);
styles.reverse().map((style) => {
input = input.replace(this.tokens[style].pattern, this.tokens[style].render);
});
input = input.replace(/~\^(.*)~\^/g, escapeToken);
return input;
}
/*
Generate default style tokens
@method generateTokens
@return {Object}
*/
generateTokens(renderer) {
let defaultTokens = {
// ember-cli styles
option: {
name: 'option',
token: '--option',
pattern: /((--\w*\b)|(<options>))/g,
render: this.renderStylesFactory(renderer, 'cyan'),
},
default: {
name: 'default',
token: '(Default: default)',
pattern: /(\(Default:\s.*\))/g,
render: this.renderStylesFactory(renderer, 'cyan'),
},
required: {
name: 'required',
token: '(Required)',
pattern: /(\(Required\))/g,
render: this.renderStylesFactory(renderer, 'cyan'),
},
};
let colorTokens = unshiftValue(colors.concat(backgroundColors).map(getToken.bind(this)), {}).reduce(setToken);
return merge(colorTokens, defaultTokens);
}
/*
Looks up multiple styles to apply to the rendered output
@method renderStylesFactory
@param {Object} [renderer] should match chalk.js format
@param {String, Array} [styleNames]
@return {Function} Function that will wrap the input string with styling
*/
renderStylesFactory(renderer, styleNames) {
let styles;
if (Array.isArray(styleNames)) {
styles = styleNames.map(checkStyleName.bind(null, renderer));
} else {
styles = [checkStyleName(renderer, styleNames)];
}
return function (match, pattern) {
return styles.reverse().reduce((previous, current) => renderer[current](previous), pattern);
};
}
}
module.exports = MarkdownColor;
/*
Check to see if style exists on the renderer
@param {Object} [renderer] should match chalk.js format
@param {String} [name]
*/
function checkStyleName(renderer, name) {
if (typeof renderer[name] === 'function') {
return name;
} else {
throw new SilentError(`The style '${name}' is not supported by the markdown renderer.`);
}
}
/*
@param {String} [name]
@param {Object} [options]
@return {RegExp} Returns lookup pattern
*/
function getColorTokenRegex(name, options) {
options = options || {};
let start = options.start || '(?:<';
let end = options.end || '>)';
let close = options.close || '(?:</';
let middle = options.middle || '(.*?)';
let tag = start + name + end;
let closeTag = close + name + end;
let pattern = tag + middle + closeTag;
return new RegExp(pattern, 'g');
}
/*
@param {String} [name]
@param {Object} [options]
@return {Object} Returns token object
*/
function getToken(name, options) {
let renderer = (options && options.renderer) || chalk;
return {
name,
token: `<${name}></${name}>`,
pattern: getColorTokenRegex(name),
render: this.renderStylesFactory(renderer, name),
};
}
function setToken(result, color) {
result[color.name] = color;
return result;
}
function escapeToken(match, pattern) {
let output = pattern.replace(/~/g, '');
return `<${output}>`;
}
function unshiftValue(array, value) {
array.unshift(value);
return array;
}
/*
Formatting colors for ember-cli help
white: ember serve
yellow: <arg-option, >
cyan: --port, --important-option
cyan: (Default: something), (Default: 4200)
white: Description 1, Description 2
cyan: (Required)
*/