| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563 | const _ = require('lodash');const fs = require('fs');const path = require('path');const stringify = require('./stringify');const Types = require('./types');const DEFAULT_OPTIONS = {    language: 'en',    resources: {        en: JSON.parse(fs.readFileSync(path.join(__dirname, '../res/en.json'), 'utf8'))    }};// order matters for these!const FUNCTION_DETAILS = ['new', 'this'];const FUNCTION_DETAILS_VARIABLES = ['functionNew', 'functionThis'];const MODIFIERS = ['optional', 'nullable', 'repeatable'];const TEMPLATE_VARIABLES = [    'application',    'codeTagClose',    'codeTagOpen',    'element',    'field',    'functionNew',    'functionParams',    'functionReturns',    'functionThis',    'keyApplication',    'name',    'nullable',    'optional',    'param',    'prefix',    'repeatable',    'suffix',    'type'];const FORMATS = {    EXTENDED: 'extended',    SIMPLE: 'simple'};function makeTagOpen(codeTag, codeClass) {    let tagOpen = '';    const tags = codeTag ? codeTag.split(' ') : [];    tags.forEach(tag => {        const tagClass = codeClass ? ` class="${codeClass}"` : '';        tagOpen += `<${tag}${tagClass}>`;    });    return tagOpen;}function makeTagClose(codeTag) {    let tagClose = '';    const tags = codeTag ? codeTag.split(' ') : [];    tags.reverse();    tags.forEach(tag => {        tagClose += `</${tag}>`;    });    return tagClose;}function reduceMultiple(context, keyName, contextName, translate, previous, current, index, items) {    let key;    switch (index) {        case 0:            key = '.first.many';            break;        case (items.length - 1):            key = '.last.many';            break;        default:            key = '.middle.many';    }    key = keyName + key;    context[contextName] = items[index];    return previous + translate(key, context);}function modifierKind(useLongFormat) {    return useLongFormat ? FORMATS.EXTENDED : FORMATS.SIMPLE;}function buildModifierStrings(describer, modifiers, type, useLongFormat) {    const result = {};    modifiers.forEach(modifier => {        const key = modifierKind(useLongFormat);        const modifierStrings = describer[modifier](type[modifier]);        result[modifier] = modifierStrings[key];    });    return result;}function addModifiers(describer, context, result, type, useLongFormat) {    const keyPrefix = `modifiers.${modifierKind(useLongFormat)}`;    const modifiers = buildModifierStrings(describer, MODIFIERS, type, useLongFormat);    MODIFIERS.forEach(modifier => {        const modifierText = modifiers[modifier] || '';        result.modifiers[modifier] = modifierText;        if (!useLongFormat) {            context[modifier] = modifierText;        }    });    context.prefix = describer._translate(`${keyPrefix}.prefix`, context);    context.suffix = describer._translate(`${keyPrefix}.suffix`, context);}function addFunctionModifiers(describer, context, {modifiers}, type, useLongFormat) {    const functionDetails = buildModifierStrings(describer, FUNCTION_DETAILS, type, useLongFormat);    FUNCTION_DETAILS.forEach((functionDetail, i) => {        const functionExtraInfo = functionDetails[functionDetail] || '';        const functionDetailsVariable = FUNCTION_DETAILS_VARIABLES[i];        modifiers[functionDetailsVariable] = functionExtraInfo;        if (!useLongFormat) {            context[functionDetailsVariable] += functionExtraInfo;        }    });}// Replace 2+ whitespace characters with a single whitespace character.function collapseSpaces(string) {    return string.replace(/(\s)+/g, '$1');}function getApplicationKey({expression}, applications) {    if (applications.length === 1) {        if (/[Aa]rray/.test(expression.name)) {            return 'array';        } else {            return 'other';        }    } else if (/[Ss]tring/.test(applications[0].name)) {        // object with string keys        return 'object';    } else {        // object with non-string keys        return 'objectNonString';    }}class Result {    constructor() {        this.description = '';        this.modifiers = {            functionNew: '',            functionThis: '',            optional: '',            nullable: '',            repeatable: ''        };        this.returns = '';    }}class Context {    constructor(props) {        props = props || {};        TEMPLATE_VARIABLES.forEach(variable => {            this[variable] = props[variable] || '';        });    }}class Describer {    constructor(opts) {        let options;        this._useLongFormat = true;        options = this._options = _.defaults(opts || {}, DEFAULT_OPTIONS);        this._stringifyOptions = _.defaults(options, { _ignoreModifiers: true });        // use a dictionary, not a Context object, so we can more easily merge this into Context objects        this._i18nContext = {            codeTagClose: makeTagClose(options.codeTag),            codeTagOpen: makeTagOpen(options.codeTag, options.codeClass)        };        // templates start out as strings; we lazily replace them with template functions        this._templates = options.resources[options.language];        if (!this._templates) {            throw new Error(`I18N resources are not available for the language ${options.language}`);        }    }    _stringify(type, typeString, useLongFormat) {        const context = new Context({            type: typeString || stringify(type, this._stringifyOptions)        });        const result = new Result();        addModifiers(this, context, result, type, useLongFormat);        result.description = this._translate('type', context).trim();        return result;    }    _translate(key, context) {        let result;        let templateFunction = _.get(this._templates, key);        context = context || new Context();        if (templateFunction === undefined) {            throw new Error(`The template ${key} does not exist for the ` +                `language ${this._options.language}`);        }        // compile and cache the template function if necessary        if (typeof templateFunction === 'string') {            // force the templates to use the `context` object            templateFunction = templateFunction.replace(/<%= /g, '<%= context.');            templateFunction = _.template(templateFunction, {variable: 'context'});            _.set(this._templates, key, templateFunction);        }        result = (templateFunction(_.extend(context, this._i18nContext)) || '')            // strip leading spaces            .replace(/^\s+/, '');        result = collapseSpaces(result);        return result;    }    _modifierHelper(key, modifierPrefix = '', context) {        return {            extended: key ?                this._translate(`${modifierPrefix}.${FORMATS.EXTENDED}.${key}`, context) :                '',            simple: key ?                this._translate(`${modifierPrefix}.${FORMATS.SIMPLE}.${key}`, context) :                ''        };    }    _translateModifier(key, context) {        return this._modifierHelper(key, 'modifiers', context);    }    _translateFunctionModifier(key, context) {        return this._modifierHelper(key, 'function', context);    }    application(type, useLongFormat) {        const applications = type.applications.slice(0);        const context = new Context();        const key = `application.${getApplicationKey(type, applications)}`;        const result = new Result();        addModifiers(this, context, result, type, useLongFormat);        context.type = this.type(type.expression).description;        context.application = this.type(applications.pop()).description;        context.keyApplication = applications.length ? this.type(applications.pop()).description : '';        result.description = this._translate(key, context).trim();        return result;    }    elements(type, useLongFormat) {        const context = new Context();        const items = type.elements.slice(0);        const result = new Result();        addModifiers(this, context, result, type, useLongFormat);        result.description = this._combineMultiple(items, context, 'union', 'element');        return result;    }    new(funcNew) {        const context = new Context({'functionNew': this.type(funcNew).description});        const key = funcNew ? 'new' : '';        return this._translateFunctionModifier(key, context);    }    nullable(nullable) {        let key;        switch (nullable) {            case true:                key = 'nullable';                break;            case false:                key = 'nonNullable';                break;            default:                key = '';        }        return this._translateModifier(key);    }    optional(optional) {        const key = (optional === true) ? 'optional' : '';        return this._translateModifier(key);    }    repeatable(repeatable) {        const key = (repeatable === true) ? 'repeatable' : '';        return this._translateModifier(key);    }    _combineMultiple(items, context, keyName, contextName) {        const result = new Result();        const self = this;        let strings;        strings = typeof items[0] === 'string' ?            items.slice(0) :            items.map(item => self.type(item).description);        switch (strings.length) {            case 0:                // falls through            case 1:                context[contextName] = strings[0] || '';                result.description = this._translate(`${keyName}.first.one`, context);                break;            case 2:                strings.forEach((item, idx) => {                    const key = `${keyName + (idx === 0 ? '.first' : '.last' )}.two`;                    context[contextName] = item;                    result.description += self._translate(key, context);                });                break;            default:                result.description = strings.reduce(reduceMultiple.bind(null, context, keyName,                    contextName, this._translate.bind(this)), '');        }        return result.description.trim();    }    /* eslint-enable no-unused-vars */    params(params, functionContext) {        const context = new Context();        const result = new Result();        const self = this;        let strings;        // TODO: this hardcodes the order and placement of functionNew and functionThis; need to move        // this to the template (and also track whether to put a comma after the last modifier)        functionContext = functionContext || {};        params = params || [];        strings = params.map(param => self.type(param).description);        if (functionContext.functionThis) {            strings.unshift(functionContext.functionThis);        }        if (functionContext.functionNew) {            strings.unshift(functionContext.functionNew);        }        result.description = this._combineMultiple(strings, context, 'params', 'param');        return result;    }    this(funcThis) {        const context = new Context({'functionThis': this.type(funcThis).description});        const key = funcThis ? 'this' : '';        return this._translateFunctionModifier(key, context);    }    type(type, useLongFormat) {        let result = new Result();        if (useLongFormat === undefined) {            useLongFormat = this._useLongFormat;        }        // ensure we don't use the long format for inner types        this._useLongFormat = false;        if (!type) {            return result;        }        switch (type.type) {            case Types.AllLiteral:                result = this._stringify(type, this._translate('all'), useLongFormat);                break;            case Types.FunctionType:                result = this._signature(type, useLongFormat);                break;            case Types.NameExpression:                result = this._stringify(type, null, useLongFormat);                break;            case Types.NullLiteral:                result = this._stringify(type, this._translate('null'), useLongFormat);                break;            case Types.RecordType:                result = this._record(type, useLongFormat);                break;            case Types.TypeApplication:                result = this.application(type, useLongFormat);                break;            case Types.TypeUnion:                result = this.elements(type, useLongFormat);                break;            case Types.UndefinedLiteral:                result = this._stringify(type, this._translate('undefined'), useLongFormat);                break;            case Types.UnknownLiteral:                result = this._stringify(type, this._translate('unknown'), useLongFormat);                break;            default:                throw new Error(`Unknown type: ${JSON.stringify(type)}`);        }        return result;    }    _record(type, useLongFormat) {        const context = new Context();        let items;        const result = new Result();        items = this._recordFields(type.fields);        addModifiers(this, context, result, type, useLongFormat);        result.description = this._combineMultiple(items, context, 'record', 'field');        return result;    }    _recordFields(fields) {        const context = new Context();        let result = [];        const self = this;        if (!fields.length) {            return result;        }        result = fields.map(field => {            const key = `field.${field.value ? 'typed' : 'untyped'}`;            context.name = self.type(field.key).description;            if (field.value) {                context.type = self.type(field.value).description;            }            return self._translate(key, context);        });        return result;    }    _getHrefForString(nameString) {        let href = '';        const links = this._options.links;        if (!links) {            return href;        }        // accept a map or an object        if (links instanceof Map) {            href = links.get(nameString);        } else if ({}.hasOwnProperty.call(links, nameString)) {            href = links[nameString];        }        return href;    }    _addLinks(nameString) {        const href = this._getHrefForString(nameString);        let link = nameString;        let linkClass = this._options.linkClass || '';        if (href) {            if (linkClass) {                linkClass = ` class="${linkClass}"`;            }            link = `<a href="${href}"${linkClass}>${nameString}</a>`;        }        return link;    }    result(type, useLongFormat) {        const context = new Context();        const key = `function.${modifierKind(useLongFormat)}.returns`;        const result = new Result();        context.type = this.type(type).description;        addModifiers(this, context, result, type, useLongFormat);        result.description = this._translate(key, context);        return result;    }    _signature(type, useLongFormat) {        const context = new Context();        const kind = modifierKind(useLongFormat);        const result = new Result();        let returns;        addModifiers(this, context, result, type, useLongFormat);        addFunctionModifiers(this, context, result, type, useLongFormat);        context.functionParams = this.params(type.params || [], context).description;        if (type.result) {            returns = this.result(type.result, useLongFormat);            if (useLongFormat) {                result.returns = returns.description;            } else {                context.functionReturns = returns.description;            }        }        result.description += this._translate(`function.${kind}.signature`, context).trim();        return result;    }}module.exports = (type, options) => {    const simple = new Describer(options).type(type, false);    const extended = new Describer(options).type(type);    [simple, extended].forEach(result => {        result.description = collapseSpaces(result.description.trim());    });    return {        simple: simple.description,        extended    };};
 |