Home Manual Reference Source Test Repository

src/Publisher/Builder/ManualDocBuilder.js

import IceCap from 'ice-cap';
import fs from 'fs-extra';
import cheerio from 'cheerio';
import DocBuilder from './DocBuilder.js';
import {markdown} from './util.js';

/**
 * Manual Output Builder class.
 */
export default class ManualDocBuilder extends DocBuilder {
  /**
   * execute building output.
   * @param {function(html: string, filePath: string)} callback - is called each manual.
   * @param {function(src: string, dest: string)} callbackForCopy - is called asset.
   */
  exec(callback, callbackForCopy) {
    if (!this._config.manual) return;

    const manualConfig = this._getManualConfig();
    const ice = this._buildLayoutDoc();
    ice.autoDrop = false;
    ice.attr('rootContainer', 'class', ' manual-root');

    {
      const fileName = 'manual/index.html';
      const baseUrl = this._getBaseUrl(fileName);
      this._buildManualIndex(manualConfig);
      ice.load('content', this._buildManualIndex(manualConfig), IceCap.MODE_WRITE);
      ice.load('nav', this._buildManualNav(manualConfig), IceCap.MODE_WRITE);
      ice.text('title', 'Manual', IceCap.MODE_WRITE);
      ice.attr('baseUrl', 'href', baseUrl, IceCap.MODE_WRITE);
      callback(ice.html, fileName);
    }

    for (let item of manualConfig) {
      if (!item.paths) continue;
      const fileName = this._getManualOutputFileName(item);
      const baseUrl = this._getBaseUrl(fileName);
      ice.load('content', this._buildManual(item), IceCap.MODE_WRITE);
      ice.load('nav', this._buildManualNav(manualConfig), IceCap.MODE_WRITE);
      ice.text('title', item.label, IceCap.MODE_WRITE);
      ice.attr('baseUrl', 'href', baseUrl, IceCap.MODE_WRITE);
      callback(ice.html, fileName);
    }

    if (this._config.manual.asset) {
      callbackForCopy(this._config.manual.asset, 'manual/asset');
    }
  }

  /**
   * get manual config based on ``config.manual``.
   * @returns {ManualConfigItem[]} built manual config.
   * @private
   */
  _getManualConfig() {
    const m = this._config.manual;
    const manualConfig = [];
    if (m.overview) manualConfig.push({label: 'Overview', paths: m.overview});
    if (m.installation) manualConfig.push({label: 'Installation', paths: m.installation});
    if (m.tutorial) manualConfig.push({label: 'Tutorial', paths: m.tutorial});
    if (m.usage) manualConfig.push({label: 'Usage', paths: m.usage});
    if (m.configuration) manualConfig.push({label: 'Configuration', paths: m.configuration});
    if (m.example) manualConfig.push({label: 'Example', paths: m.example});
    manualConfig.push({label: 'Reference', fileName: 'identifiers.html', references: true});
    if (m.faq) manualConfig.push({label: 'FAQ', paths: m.faq});
    if (m.changelog) manualConfig.push({label: 'Changelog', paths: m.changelog});
    return manualConfig;
  }

  /**
   * build manual navigation.
   * @param {ManualConfigItem[]} manualConfig - target manual config.
   * @return {IceCap} built navigation
   * @private
   */
  _buildManualNav(manualConfig) {
    const ice = this._buildManualIndex(manualConfig);
    const $root = cheerio.load(ice.html).root();
    $root.find('.github-markdown').removeClass('github-markdown');
    return $root.html();
  }

  /**
   * build manual.
   * @param {ManualConfigItem} item - target manual config item.
   * @return {IceCap} built manual.
   * @private
   */
  _buildManual(item) {
    const html = this._convertMDToHTML(item);
    const ice = new IceCap(this._readTemplate('manual.html'));
    ice.text('title', item.label);
    ice.load('content', html);

    // convert relative src to base url relative src.
    const $root = cheerio.load(ice.html).root();
    $root.find('img').each((i, el)=>{
      const $el = cheerio(el);
      const src = $el.attr('src');
      if (!src) return;
      if (src.match(/^http[s]?:/)) return;
      if (src.charAt(0) === '/') return;
      $el.attr('src', './manual/' + src);
    });
    $root.find('a').each((i, el)=>{
      const $el = cheerio(el);
      const href = $el.attr('href');
      if (!href) return;
      if (href.match(/^http[s]?:/)) return;
      if (href.charAt(0) === '/') return;
      if (href.charAt(0) === '#') return;
      $el.attr('href', './manual/' + href);
    });

    return $root.html();
  }

  /**
   * built manual index.
   * @param {ManualConfigItem[]} manualConfig - target manual config.
   * @return {IceCap} built index.
   * @private
   */
  _buildManualIndex(manualConfig) {
    const ice = new IceCap(this._readTemplate('manualIndex.html'));

    ice.loop('manual', manualConfig, (i, item, ice)=>{
      const toc = [];
      if (item.references) {
        const identifiers = this._findAllIdentifiersKindGrouping();
        if (identifiers.class.length) toc.push({label: 'Class', link: 'identifiers.html#class', indent: 'indent-h1'});
        if (identifiers.interface.length) toc.push({label: 'Interface', link: 'identifiers.html#interface', indent: 'indent-h1'});
        if (identifiers.function.length) toc.push({label: 'Function', link: 'identifiers.html#function', indent: 'indent-h1'});
        if (identifiers.variable.length) toc.push({label: 'Variable', link: 'identifiers.html#variable', indent: 'indent-h1'});
        if (identifiers.typedef.length) toc.push({label: 'Typedef', link: 'identifiers.html#typedef', indent: 'indent-h1'});
        if (identifiers.external.length) toc.push({label: 'External', link: 'identifiers.html#external', indent: 'indent-h1'});
      } else {
        const fileName = this._getManualOutputFileName(item);
        const html = this._convertMDToHTML(item);
        const $root = cheerio.load(html).root();
        const isHRise = $root.find('h1').length === 0;
        $root.find('h1,h2,h3,h4,h5').each((i, el)=>{
          const $el = cheerio(el);
          const label = $el.text();
          const link = `${fileName}#${$el.attr('id')}`;
          let indent;
          if (isHRise) {
            const tagName = `h${parseInt(el.tagName.charAt(1), 10) - 1}`;
            indent = `indent-${tagName}`;
          } else {
            indent = `indent-${el.tagName.toLowerCase()}`;
          }
          toc.push({label, link, indent});
        });
      }

      ice.attr('manual', 'data-toc-name', item.label.toLowerCase());
      ice.text('title', item.label);
      ice.attr('title', 'href', this._getManualOutputFileName(item));
      ice.loop('manualNav', toc, (i, item, ice)=>{
        ice.attr('manualNav', 'class', item.indent);
        ice.text('link', item.label);
        ice.attr('link', 'href', item.link);
      });
    });

    return ice;
  }

  /**
   * get manual file name.
   * @param {ManualConfigItem} item - target manual config item.
   * @returns {string} file name.
   * @private
   */
  _getManualOutputFileName(item) {
    if (item.fileName) return item.fileName;
    return `manual/${item.label.toLowerCase()}.html`;
  }

  /**
   * convert markdown to html.
   * if markdown has only one ``h1`` and it's text is ``item.label``, remove the ``h1``.
   * because duplication ``h1`` in output html.
   * @param {ManualConfigItem} item - target.
   * @returns {string} converted html.
   * @private
   */
  _convertMDToHTML(item) {
    const contents = [];
    for (let path of item.paths) {
      contents.push(fs.readFileSync(path).toString());
    }
    const content = contents.join('\n\n');
    const html = markdown(content);
    const $root = cheerio.load(html).root();
    return $root.html();
  }

  /**
   * get label synonyms.
   * @param {string} label - target item label.
   * @returns {string[]} synonyms
   * @private
   */
  _getLabelSynonyms(label) {
    switch (label.toLowerCase()) {
      case 'overview':
        return ['overview'];
      case 'installation':
        return ['installation', 'install'];
      case 'tutorial':
        return ['tutorial'];
      case 'configuration':
        return ['configuration', 'config'];
      case 'usage':
        return ['usage'];
      case 'example':
        return ['example', 'examples'];
      case 'faq':
        return ['faq'];
      case 'changelog':
        return ['changelog', 'change log'];
    }
  }
}