ember-cli/ember-cli

View on GitHub
lib/utilities/markdown-color.js

Summary

Maintainability
A
25 mins
Test Coverage
D
69%
'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)
*/