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
- };
- };
|