Home Manual Reference Source Test Repository

src/Publisher/Builder/DocResolver.js

import fs from 'fs';
import path from 'path';
import {markdown} from './util.js';

/**
 * Resolve various properties in doc object.
 */
export default class DocResolver {
  /**
   * create instance.
   * @param {DocBuilder} builder - target doc builder.
   */
  constructor(builder) {
    this._builder = builder;
    this._data = builder._data;
  }

  /**
   * resolve various properties.
   */
  resolve() {
    this._resolveExtendsChain();
    this._resolveNecessary();
    this._resolveAccess();
    this._resolveUnexportIdentifier();
    this._resolveUndocumentIdentifier();
    this._resolveDuplication();
    this._resolveIgnore();
    this._resolveLink();
    this._resolveMarkdown();
    this._resolveTestRelation();
  }

  /**
   * resolve ignore property.
   * remove docs that has ignore property.
   * @private
   */
  _resolveIgnore() {
    if (this._data.__RESOLVED_IGNORE__) return;

    let docs = this._builder._find({ignore: true});
    for (let doc of docs) {
      let longname = doc.longname.replace(/[$]/g, '\\$');
      let regex = new RegExp(`^${longname}[.~#]`);
      this._data({longname: {regex: regex}}).remove();
    }
    this._data({ignore: true}).remove();

    this._data.__RESOLVED_IGNORE__ = true;
  }

  /**
   * resolve access property.
   * if doc does not have access property, the doc is public.
   * but name is started with '-', the doc is private.
   * @private
   */
  _resolveAccess() {
    if (this._data.__RESOLVED_ACCESS__) return;

    let config = this._builder._config;
    let access = config.access || ['public', 'protected', 'private'];
    let autoPrivate = config.autoPrivate;

    this._data().update(function(){
      if (!this.access) {
        if (autoPrivate && this.name.charAt(0) === '_') {
          /** @ignore */
          this.access = 'private';
        } else {
          this.access = 'public';
        }
      }

      if (!access.includes(this.access)) /** @ignore */ this.ignore = true;

      return this;
    });

    this._data.__RESOLVED_ACCESS__ = true;
  }

  /**
   * resolve unexport identifier doc.
   * doc is added ignore property that is not exported.
   * @private
   */
  _resolveUnexportIdentifier() {
    if (this._data.__RESOLVED_UNEXPORT_IDENTIFIER__) return;

    let config = this._builder._config;
    if (!config.unexportIdentifier) {
      this._data({export: false}).update({ignore: true});
    }

    this._data.__RESOLVED_UNEXPORT_IDENTIFIER__ = true;
  }

  /**
   * resolve undocument identifier doc.
   * doc is added ignore property that does not have document tag.
   * @private
   */
  _resolveUndocumentIdentifier() {
    if (this._data.__RESOLVED_UNDOCUMENT_IDENTIFIER__) return;

    if (!this._builder._config.undocumentIdentifier) {
      this._builder._data({undocument: true}).update({ignore: true});
    }

    this._data.__RESOLVED_UNDOCUMENT_IDENTIFIER__ = true;
  }

  /**
   * resolve description as markdown.
   * @private
   */
  _resolveMarkdown() {
    if (this._data.__RESOLVED_MARKDOWN__) return;

    function convert(obj) {
      for (let key of Object.keys(obj)) {
        let value = obj[key];
        if (key === 'description' && typeof value === 'string') {
          obj[key + 'Raw'] = obj[key];
          obj[key] = markdown(value, false);
        } else if (typeof value === 'object' && value) {
          convert(value);
        }
      }
    }

    let docs = this._builder._find();
    for (let doc of docs) {
      convert(doc);
    }

    this._data.__RESOLVED_MARKDOWN__ = true;
  }

  /**
   * resolve @link as html link.
   * @private
   * @todo resolve all ``description`` property.
   */
  _resolveLink() {
    if(this._data.__RESOLVED_LINK__) return;

    let link = (str)=>{
      if (!str) return str;

      return str.replace(/\{@link ([\w\#_\-.:\~\/$]+)}/g, (str, longname)=>{
        return this._builder._buildDocLinkHTML(longname, longname);
      });
    };

    this._data().each((v)=>{
      v.description = link(v.description);

      if (v.params) {
        for (let param of v.params) {
          param.description = link(param.description);
        }
      }

      if (v.return) {
        v.return.description = link(v.return.description);
      }

      if (v.throws) {
        for (let _throw of v.throws) {
          _throw.description = link(_throw.description);
        }
      }

      if (v.see) {
        for (let i = 0; i < v.see.length; i++) {
          if (v.see[i].indexOf('{@link') === 0) {
            v.see[i] = link(v.see[i]);
          } else if(v.see[i].indexOf('<a href') === 0) {
            // ignore
          } else {
            v.see[i] = `<a href="${v.see[i]}">${v.see[i]}</a>`;
          }
        }
      }
    });

    this._data.__RESOLVED_LINK__ = true;
  }

  /**
   * resolve class extends chain.
   * add following special property.
   * - ``_custom_extends_chain``: ancestor class chain.
   * - ``_custom_direct_subclasses``: class list that direct extends target doc.
   * - ``_custom_indirect_subclasses``: class list that indirect extends target doc.
   * - ``_custom_indirect_implements``: class list that target doc indirect implements.
   * - ``_custom_direct_implemented``: class list that direct implements target doc.
   * - ``_custom_indirect_implemented``: class list that indirect implements target doc.
   *
   * @private
   */
  _resolveExtendsChain() {
    if (this._data.__RESOLVED_EXTENDS_CHAIN__) return;

    let extendsChain = (doc) => {
      if (!doc.extends) return;

      let selfDoc = doc;

      // traverse super class.
      let chains = [];
      while (1) {
        if (!doc.extends) break;

        let superClassDoc = this._builder._findByName(doc.extends[0])[0];
        if (superClassDoc) {
          chains.push(superClassDoc.longname);
          doc = superClassDoc;
        } else {
          chains.push(doc.extends[0]);
          break;
        }
      }

      if (chains.length) {
        // direct subclass
        let superClassDoc = this._builder._findByName(chains[0])[0];
        if (superClassDoc) {
          if (!superClassDoc._custom_direct_subclasses) superClassDoc._custom_direct_subclasses = [];
          superClassDoc._custom_direct_subclasses.push(selfDoc.longname);
        }

        // indirect subclass
        for (let superClassLongname of chains.slice(1)) {
          superClassDoc = this._builder._findByName(superClassLongname)[0];
          if (superClassDoc) {
            if (!superClassDoc._custom_indirect_subclasses) superClassDoc._custom_indirect_subclasses = [];
            superClassDoc._custom_indirect_subclasses.push(selfDoc.longname);
          }
        }

        // indirect implements and mixes
        for (let superClassLongname of chains) {
          superClassDoc = this._builder._findByName(superClassLongname)[0];
          if (!superClassDoc) continue;

          // indirect implements
          if (superClassDoc.implements) {
            if (!selfDoc._custom_indirect_implements) selfDoc._custom_indirect_implements = [];
            selfDoc._custom_indirect_implements.push(...superClassDoc.implements);
          }

          // indirect mixes
          //if (superClassDoc.mixes) {
          //  if (!selfDoc._custom_indirect_mixes) selfDoc._custom_indirect_mixes = [];
          //  selfDoc._custom_indirect_mixes.push(...superClassDoc.mixes);
          //}
        }

        // extends chains
        selfDoc._custom_extends_chains = chains.reverse();
      }
    };

    let implemented = (doc) =>{
      let selfDoc = doc;

      // direct implemented (like direct subclass)
      for (let superClassLongname of selfDoc.implements || []) {
        let superClassDoc = this._builder._findByName(superClassLongname)[0];
        if (!superClassDoc) continue;
        if(!superClassDoc._custom_direct_implemented) superClassDoc._custom_direct_implemented = [];
        superClassDoc._custom_direct_implemented.push(selfDoc.longname);
      }

      // indirect implemented (like indirect subclass)
      for (let superClassLongname of selfDoc._custom_indirect_implements || []) {
        let superClassDoc = this._builder._findByName(superClassLongname)[0];
        if (!superClassDoc) continue;
        if(!superClassDoc._custom_indirect_implemented) superClassDoc._custom_indirect_implemented = [];
        superClassDoc._custom_indirect_implemented.push(selfDoc.longname);
      }
    };

    //var mixed = (doc) =>{
    //  var selfDoc = doc;
    //
    //  // direct mixed (like direct subclass)
    //  for (var superClassLongname of selfDoc.mixes || []) {
    //    var superClassDoc = this._builder._find({longname: superClassLongname})[0];
    //    if (!superClassDoc) continue;
    //    if(!superClassDoc._custom_direct_mixed) superClassDoc._custom_direct_mixed = [];
    //    superClassDoc._custom_direct_mixed.push(selfDoc.longname);
    //  }
    //
    //  // indirect mixed (like indirect subclass)
    //  for (var superClassLongname of selfDoc._custom_indirect_mixes || []) {
    //    var superClassDoc = this._builder._find({longname: superClassLongname})[0];
    //    if (!superClassDoc) continue;
    //    if(!superClassDoc._custom_indirect_mixed) superClassDoc._custom_indirect_mixed = [];
    //    superClassDoc._custom_indirect_mixed.push(selfDoc.longname);
    //  }
    //};

    let docs = this._builder._find({kind: 'class'});
    for (let doc of docs) {
      extendsChain(doc);
      implemented(doc);
      //mixed(doc);
    }

    this._data.__RESOLVED_EXTENDS_CHAIN__ = true;
  }

  /**
   * resolve necessary identifier.
   *
   * ```javascript
   * class Foo {}
   *
   * export default Bar extends Foo {}
   * ```
   *
   * ``Foo`` is not exported, but ``Bar`` extends ``Foo``.
   * ``Foo`` is necessary.
   * So, ``Foo`` must be exported by force.
   *
   * @private
   */
  _resolveNecessary() {
    let builder = this._builder;
    this._data({export: false}).update(function() {
      let doc = this;
      let childNames = [];
      if (doc._custom_direct_subclasses) childNames.push(...doc._custom_direct_subclasses);
      if (doc._custom_indirect_subclasses) childNames.push(...doc._custom_indirect_subclasses);
      if (doc._custom_direct_implemented) childNames.push(...doc._custom_direct_implemented);
      if (doc._custom_indirect_implemented) childNames.push(...doc._custom_indirect_implemented);

      for (let childName of childNames) {
        let childDoc = builder._find({longname: childName})[0];
        if (!childDoc) continue;
        if (!childDoc.ignore && childDoc.export) {
          doc.export = true;
          return doc;
        }
      }
    });
  }

  /**
   * resolve test and identifier relation. add special property.
   * - ``_custom_tests``: longnames of test doc.
   * - ``_custom_test_targets``: longnames of identifier.
   *
   * @private
   */
  _resolveTestRelation() {
    if (this._data.__RESOLVED_TEST_RELATION__) return;

    let testDocs = this._builder._find({kind: ['testDescribe', 'testIt']});
    for (let testDoc of testDocs) {
      let testTargets = testDoc.testTargets;
      if (!testTargets) continue;

      for (let testTarget of testTargets) {
        let doc = this._builder._findByName(testTarget)[0];
        if (doc) {
          if (!doc._custom_tests) doc._custom_tests = [];
          doc._custom_tests.push(testDoc.longname);

          if (!testDoc._custom_test_targets) testDoc._custom_test_targets = [];
          testDoc._custom_test_targets.push([doc.longname, testTarget]);
        } else {
          if (!testDoc._custom_test_targets) testDoc._custom_test_targets = [];
          testDoc._custom_test_targets.push([testTarget, testTarget]);
        }
      }
    }

    // test full description
    for (let testDoc of testDocs) {
      let desc = [];
      let parents = (testDoc.memberof.split('~')[1] || '').split('.');
      for (let parent of parents) {
        let doc = this._builder._find({kind: ['testDescribe', 'testIt'], name: parent})[0];
        if (!doc) continue;
        desc.push(doc.descriptionRaw);
      }
      desc.push(testDoc.descriptionRaw);
      testDoc.testFullDescription = desc.join(' ');
    }

    this._data.__RESOLVED_TEST_RELATION__ = true;
  }

  /**
   * resolve duplication identifier.
   * member doc is possible duplication.
   * other doc is not duplication.
   * @private
   */
  _resolveDuplication() {
    if (this._data.__RESOLVED_DUPLICATION__) return;

    let docs = this._builder._find({kind: 'member'});
    let ignoreId = [];
    for (let doc of docs) {
      // member duplicate with getter/setter/method.
      // when it, remove member.
      // getter/setter/method are high priority.
      const nonMemberDup = this._builder._find({longname: doc.longname, kind: {'!is': 'member'}});
      if (nonMemberDup.length) {
        ignoreId.push(doc.___id);
        continue;
      }

      let dup = this._builder._find({longname: doc.longname, kind: 'member'});
      if (dup.length > 1) {
        let ids = dup.map(v => v.___id);
        ids.sort((a, b)=> {
          return a < b ? -1 : 1;
        });
        ids.shift();
        ignoreId.push(...ids)
      }
    }

    this._data({___id: ignoreId}).update(function(){
      this.ignore = true;
      return this;
    });

    this._data.__RESOLVED_DUPLICATION__ = true;
  }
}