commands/meta-formatter.js

const {DocParser} = require('./doc-parser');
const {CodeFormatter} = require('./code-formatter');

/////////////////////////////////////////////////////////////////////////////

/**
 * A formatter for a command metadata: its signature and description.
 * Inherit from this class and override the desired methods to adjust the formatting.
 * @memberof commands
 */
class MetaFormatter {

  /**
   * Creates a new instance of this class.
   * @param {commands.CodeFormatter} [codeFormatter] - Code formatter to use when formatting code samples.
   */
  constructor(codeFormatter = new CodeFormatter()) {
    this._parser = new DocParser();
    this._codeFormatter = codeFormatter;
  }

  /**
   * Formats a command's metadata.
   * @param {commands.Meta} meta - A command's metadata to format.
   * @returns {string} The formatted command's metadata: its signature and description.
   */
  format(meta) {
    return `${this.formatSignature(meta)}\n${this.formatDesc(meta.desc)}`;
  }

  /**
   * Formats a command's description.
   * @param {string} desc - A command's raw description.
   * @returns {string} The formatted command's description.
   */
  formatDesc(desc) {
    let nodes = this._parser.parse(desc);
    return this._recur(nodes);
  }

  _recur(nodes) {
    return nodes.map(node => this[`_${node.type}`](node)).join('');
  }

  _text(node) {
    return node.value;
  }

  _untagged(node) {
    let res = this._recur(node.parts);
    return this.formatTextBlock(res) + '\n';
  }

  _block(node) {
    let res = this._recur(node.parts);
    return this.formatBlockTag(res, node.tag) + '\n';
  }

  _inline(node) {
    return this.formatInlineTag(node.value, node.tag);
  }

  /**
   * Formats a block tag found in a command's description.
   * @param {string} text - The text of the tag.
   * @param {string} tag - The tag name.
   * @returns {string} The formatted block tag.
   */
  formatBlockTag(text, tag) {
    if (tag === 'example') return this.formatCode(text);
    return this.formatTextBlock(text);
  }

  /**
   * Formats an inline tag found in a command's description.
   * @param {string} text - The text of the tag.
   * @param {string} tag - The tag name.
   * @returns {string} The formatted inline tag.
   */
  formatInlineTag(text, tag) {
    if (tag === 'code') return this.formatInlineCode(text);
    return text.trim();
  }

  /**
   * Formats a description text block. Either untagged or in a block tag.
   * @param {string} text - The text block to format.
   * @returns {string} The formatted text block.
   */
  formatTextBlock(text) {
    return text
      .trim() // trim the whole text block
      .replace(/^[ \t]+/gm, '') // trim the start or each line
      .replace(/[ \t]*([\n\r]?)$/gm, '$1'); // trim the end of each line, but keep the line break
  }

  /**
   * Formats code typically found in an example tag in a command's description.
   * @param {string} code - The raw code string to format.
   * @returns {string} The formatted code.
   */
  formatCode(code) {
    code = code.replace(/^\s*$[\n\r]*/m, ''); // remove first empty lines
    let indentSize = code.search(/[^ \t]|$/); // get indent size of first line
    code = code
      .replace(new RegExp(`^[ \\t]{${indentSize}}`, 'gm'), '') // unindent
      .replace(/\s*$/, ''); // trim end
    return this._codeFormatter.format(code);
  }

  /**
   * Formats inline code in a command's description.
   * @param {string} code - The raw inline code string to format.
   * @returns {string} The formatted code.
   */
  formatInlineCode(code) {
    return `\`${code.trim()}\``;
  }

  /**
   * Formats the signature of a command.
   * @param {commands.Meta} meta - A command's metadata.
   * @returns {string} The formatted command's signature.
   */
  formatSignature(meta) {
    return `${this.formatName(meta.name)}${this.formatParams(meta.params)}${this.formatReturn(meta.returns)}`;
  }

  /**
   * Formats the name of a command.
   * @param {string} name - A command's name.
   * @returns {string} The formatted command's name.
   */
  formatName(name) {
    return name;
  }

  /**
   * Formats the parameters of a command.
   * @param {commands.Param[]} params - A command's parameters.
   * @returns {string} The formatted command's parameters.
   */
  formatParams(params) {
    if (params.length === 0) return '';
    return ' ' + params.map(param => this.formatParam(param)).join(' ');
  }

  /**
   * Formats a parameter of a command.
   * @param {commands.Param} param - A command's parameter.
   * @returns {string} The formatted command's parameter.
   */
  formatParam(param) {
    let res = `:${param.name}`;
    if (param.isRest) res += ' ...';
    return res;
  }

  /**
   * Formats the return of a command.
   * @param {commands.Validate} $return - A command's return specification.
   * @returns {string} The formatted command's return.
   */
  formatReturn($return) {
    if (!$return) return '';
    return ` -> ${$return.exp}`;
  }

}

/////////////////////////////////////////////////////////////////////////////

module.exports = {
  Formatter: MetaFormatter, // alias
  MetaFormatter
};