Spaces:
Paused
Paused
| 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 | |
| }; | |
| }; | |