describe.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
  1. const _ = require('lodash');
  2. const fs = require('fs');
  3. const path = require('path');
  4. const stringify = require('./stringify');
  5. const Types = require('./types');
  6. const DEFAULT_OPTIONS = {
  7. language: 'en',
  8. resources: {
  9. en: JSON.parse(fs.readFileSync(path.join(__dirname, '../res/en.json'), 'utf8'))
  10. }
  11. };
  12. // order matters for these!
  13. const FUNCTION_DETAILS = ['new', 'this'];
  14. const FUNCTION_DETAILS_VARIABLES = ['functionNew', 'functionThis'];
  15. const MODIFIERS = ['optional', 'nullable', 'repeatable'];
  16. const TEMPLATE_VARIABLES = [
  17. 'application',
  18. 'codeTagClose',
  19. 'codeTagOpen',
  20. 'element',
  21. 'field',
  22. 'functionNew',
  23. 'functionParams',
  24. 'functionReturns',
  25. 'functionThis',
  26. 'keyApplication',
  27. 'name',
  28. 'nullable',
  29. 'optional',
  30. 'param',
  31. 'prefix',
  32. 'repeatable',
  33. 'suffix',
  34. 'type'
  35. ];
  36. const FORMATS = {
  37. EXTENDED: 'extended',
  38. SIMPLE: 'simple'
  39. };
  40. function makeTagOpen(codeTag, codeClass) {
  41. let tagOpen = '';
  42. const tags = codeTag ? codeTag.split(' ') : [];
  43. tags.forEach(tag => {
  44. const tagClass = codeClass ? ` class="${codeClass}"` : '';
  45. tagOpen += `<${tag}${tagClass}>`;
  46. });
  47. return tagOpen;
  48. }
  49. function makeTagClose(codeTag) {
  50. let tagClose = '';
  51. const tags = codeTag ? codeTag.split(' ') : [];
  52. tags.reverse();
  53. tags.forEach(tag => {
  54. tagClose += `</${tag}>`;
  55. });
  56. return tagClose;
  57. }
  58. function reduceMultiple(context, keyName, contextName, translate, previous, current, index, items) {
  59. let key;
  60. switch (index) {
  61. case 0:
  62. key = '.first.many';
  63. break;
  64. case (items.length - 1):
  65. key = '.last.many';
  66. break;
  67. default:
  68. key = '.middle.many';
  69. }
  70. key = keyName + key;
  71. context[contextName] = items[index];
  72. return previous + translate(key, context);
  73. }
  74. function modifierKind(useLongFormat) {
  75. return useLongFormat ? FORMATS.EXTENDED : FORMATS.SIMPLE;
  76. }
  77. function buildModifierStrings(describer, modifiers, type, useLongFormat) {
  78. const result = {};
  79. modifiers.forEach(modifier => {
  80. const key = modifierKind(useLongFormat);
  81. const modifierStrings = describer[modifier](type[modifier]);
  82. result[modifier] = modifierStrings[key];
  83. });
  84. return result;
  85. }
  86. function addModifiers(describer, context, result, type, useLongFormat) {
  87. const keyPrefix = `modifiers.${modifierKind(useLongFormat)}`;
  88. const modifiers = buildModifierStrings(describer, MODIFIERS, type, useLongFormat);
  89. MODIFIERS.forEach(modifier => {
  90. const modifierText = modifiers[modifier] || '';
  91. result.modifiers[modifier] = modifierText;
  92. if (!useLongFormat) {
  93. context[modifier] = modifierText;
  94. }
  95. });
  96. context.prefix = describer._translate(`${keyPrefix}.prefix`, context);
  97. context.suffix = describer._translate(`${keyPrefix}.suffix`, context);
  98. }
  99. function addFunctionModifiers(describer, context, {modifiers}, type, useLongFormat) {
  100. const functionDetails = buildModifierStrings(describer, FUNCTION_DETAILS, type, useLongFormat);
  101. FUNCTION_DETAILS.forEach((functionDetail, i) => {
  102. const functionExtraInfo = functionDetails[functionDetail] || '';
  103. const functionDetailsVariable = FUNCTION_DETAILS_VARIABLES[i];
  104. modifiers[functionDetailsVariable] = functionExtraInfo;
  105. if (!useLongFormat) {
  106. context[functionDetailsVariable] += functionExtraInfo;
  107. }
  108. });
  109. }
  110. // Replace 2+ whitespace characters with a single whitespace character.
  111. function collapseSpaces(string) {
  112. return string.replace(/(\s)+/g, '$1');
  113. }
  114. function getApplicationKey({expression}, applications) {
  115. if (applications.length === 1) {
  116. if (/[Aa]rray/.test(expression.name)) {
  117. return 'array';
  118. } else {
  119. return 'other';
  120. }
  121. } else if (/[Ss]tring/.test(applications[0].name)) {
  122. // object with string keys
  123. return 'object';
  124. } else {
  125. // object with non-string keys
  126. return 'objectNonString';
  127. }
  128. }
  129. class Result {
  130. constructor() {
  131. this.description = '';
  132. this.modifiers = {
  133. functionNew: '',
  134. functionThis: '',
  135. optional: '',
  136. nullable: '',
  137. repeatable: ''
  138. };
  139. this.returns = '';
  140. }
  141. }
  142. class Context {
  143. constructor(props) {
  144. props = props || {};
  145. TEMPLATE_VARIABLES.forEach(variable => {
  146. this[variable] = props[variable] || '';
  147. });
  148. }
  149. }
  150. class Describer {
  151. constructor(opts) {
  152. let options;
  153. this._useLongFormat = true;
  154. options = this._options = _.defaults(opts || {}, DEFAULT_OPTIONS);
  155. this._stringifyOptions = _.defaults(options, { _ignoreModifiers: true });
  156. // use a dictionary, not a Context object, so we can more easily merge this into Context objects
  157. this._i18nContext = {
  158. codeTagClose: makeTagClose(options.codeTag),
  159. codeTagOpen: makeTagOpen(options.codeTag, options.codeClass)
  160. };
  161. // templates start out as strings; we lazily replace them with template functions
  162. this._templates = options.resources[options.language];
  163. if (!this._templates) {
  164. throw new Error(`I18N resources are not available for the language ${options.language}`);
  165. }
  166. }
  167. _stringify(type, typeString, useLongFormat) {
  168. const context = new Context({
  169. type: typeString || stringify(type, this._stringifyOptions)
  170. });
  171. const result = new Result();
  172. addModifiers(this, context, result, type, useLongFormat);
  173. result.description = this._translate('type', context).trim();
  174. return result;
  175. }
  176. _translate(key, context) {
  177. let result;
  178. let templateFunction = _.get(this._templates, key);
  179. context = context || new Context();
  180. if (templateFunction === undefined) {
  181. throw new Error(`The template ${key} does not exist for the ` +
  182. `language ${this._options.language}`);
  183. }
  184. // compile and cache the template function if necessary
  185. if (typeof templateFunction === 'string') {
  186. // force the templates to use the `context` object
  187. templateFunction = templateFunction.replace(/<%= /g, '<%= context.');
  188. templateFunction = _.template(templateFunction, {variable: 'context'});
  189. _.set(this._templates, key, templateFunction);
  190. }
  191. result = (templateFunction(_.extend(context, this._i18nContext)) || '')
  192. // strip leading spaces
  193. .replace(/^\s+/, '');
  194. result = collapseSpaces(result);
  195. return result;
  196. }
  197. _modifierHelper(key, modifierPrefix = '', context) {
  198. return {
  199. extended: key ?
  200. this._translate(`${modifierPrefix}.${FORMATS.EXTENDED}.${key}`, context) :
  201. '',
  202. simple: key ?
  203. this._translate(`${modifierPrefix}.${FORMATS.SIMPLE}.${key}`, context) :
  204. ''
  205. };
  206. }
  207. _translateModifier(key, context) {
  208. return this._modifierHelper(key, 'modifiers', context);
  209. }
  210. _translateFunctionModifier(key, context) {
  211. return this._modifierHelper(key, 'function', context);
  212. }
  213. application(type, useLongFormat) {
  214. const applications = type.applications.slice(0);
  215. const context = new Context();
  216. const key = `application.${getApplicationKey(type, applications)}`;
  217. const result = new Result();
  218. addModifiers(this, context, result, type, useLongFormat);
  219. context.type = this.type(type.expression).description;
  220. context.application = this.type(applications.pop()).description;
  221. context.keyApplication = applications.length ? this.type(applications.pop()).description : '';
  222. result.description = this._translate(key, context).trim();
  223. return result;
  224. }
  225. elements(type, useLongFormat) {
  226. const context = new Context();
  227. const items = type.elements.slice(0);
  228. const result = new Result();
  229. addModifiers(this, context, result, type, useLongFormat);
  230. result.description = this._combineMultiple(items, context, 'union', 'element');
  231. return result;
  232. }
  233. new(funcNew) {
  234. const context = new Context({'functionNew': this.type(funcNew).description});
  235. const key = funcNew ? 'new' : '';
  236. return this._translateFunctionModifier(key, context);
  237. }
  238. nullable(nullable) {
  239. let key;
  240. switch (nullable) {
  241. case true:
  242. key = 'nullable';
  243. break;
  244. case false:
  245. key = 'nonNullable';
  246. break;
  247. default:
  248. key = '';
  249. }
  250. return this._translateModifier(key);
  251. }
  252. optional(optional) {
  253. const key = (optional === true) ? 'optional' : '';
  254. return this._translateModifier(key);
  255. }
  256. repeatable(repeatable) {
  257. const key = (repeatable === true) ? 'repeatable' : '';
  258. return this._translateModifier(key);
  259. }
  260. _combineMultiple(items, context, keyName, contextName) {
  261. const result = new Result();
  262. const self = this;
  263. let strings;
  264. strings = typeof items[0] === 'string' ?
  265. items.slice(0) :
  266. items.map(item => self.type(item).description);
  267. switch (strings.length) {
  268. case 0:
  269. // falls through
  270. case 1:
  271. context[contextName] = strings[0] || '';
  272. result.description = this._translate(`${keyName}.first.one`, context);
  273. break;
  274. case 2:
  275. strings.forEach((item, idx) => {
  276. const key = `${keyName + (idx === 0 ? '.first' : '.last' )}.two`;
  277. context[contextName] = item;
  278. result.description += self._translate(key, context);
  279. });
  280. break;
  281. default:
  282. result.description = strings.reduce(reduceMultiple.bind(null, context, keyName,
  283. contextName, this._translate.bind(this)), '');
  284. }
  285. return result.description.trim();
  286. }
  287. /* eslint-enable no-unused-vars */
  288. params(params, functionContext) {
  289. const context = new Context();
  290. const result = new Result();
  291. const self = this;
  292. let strings;
  293. // TODO: this hardcodes the order and placement of functionNew and functionThis; need to move
  294. // this to the template (and also track whether to put a comma after the last modifier)
  295. functionContext = functionContext || {};
  296. params = params || [];
  297. strings = params.map(param => self.type(param).description);
  298. if (functionContext.functionThis) {
  299. strings.unshift(functionContext.functionThis);
  300. }
  301. if (functionContext.functionNew) {
  302. strings.unshift(functionContext.functionNew);
  303. }
  304. result.description = this._combineMultiple(strings, context, 'params', 'param');
  305. return result;
  306. }
  307. this(funcThis) {
  308. const context = new Context({'functionThis': this.type(funcThis).description});
  309. const key = funcThis ? 'this' : '';
  310. return this._translateFunctionModifier(key, context);
  311. }
  312. type(type, useLongFormat) {
  313. let result = new Result();
  314. if (useLongFormat === undefined) {
  315. useLongFormat = this._useLongFormat;
  316. }
  317. // ensure we don't use the long format for inner types
  318. this._useLongFormat = false;
  319. if (!type) {
  320. return result;
  321. }
  322. switch (type.type) {
  323. case Types.AllLiteral:
  324. result = this._stringify(type, this._translate('all'), useLongFormat);
  325. break;
  326. case Types.FunctionType:
  327. result = this._signature(type, useLongFormat);
  328. break;
  329. case Types.NameExpression:
  330. result = this._stringify(type, null, useLongFormat);
  331. break;
  332. case Types.NullLiteral:
  333. result = this._stringify(type, this._translate('null'), useLongFormat);
  334. break;
  335. case Types.RecordType:
  336. result = this._record(type, useLongFormat);
  337. break;
  338. case Types.TypeApplication:
  339. result = this.application(type, useLongFormat);
  340. break;
  341. case Types.TypeUnion:
  342. result = this.elements(type, useLongFormat);
  343. break;
  344. case Types.UndefinedLiteral:
  345. result = this._stringify(type, this._translate('undefined'), useLongFormat);
  346. break;
  347. case Types.UnknownLiteral:
  348. result = this._stringify(type, this._translate('unknown'), useLongFormat);
  349. break;
  350. default:
  351. throw new Error(`Unknown type: ${JSON.stringify(type)}`);
  352. }
  353. return result;
  354. }
  355. _record(type, useLongFormat) {
  356. const context = new Context();
  357. let items;
  358. const result = new Result();
  359. items = this._recordFields(type.fields);
  360. addModifiers(this, context, result, type, useLongFormat);
  361. result.description = this._combineMultiple(items, context, 'record', 'field');
  362. return result;
  363. }
  364. _recordFields(fields) {
  365. const context = new Context();
  366. let result = [];
  367. const self = this;
  368. if (!fields.length) {
  369. return result;
  370. }
  371. result = fields.map(field => {
  372. const key = `field.${field.value ? 'typed' : 'untyped'}`;
  373. context.name = self.type(field.key).description;
  374. if (field.value) {
  375. context.type = self.type(field.value).description;
  376. }
  377. return self._translate(key, context);
  378. });
  379. return result;
  380. }
  381. _getHrefForString(nameString) {
  382. let href = '';
  383. const links = this._options.links;
  384. if (!links) {
  385. return href;
  386. }
  387. // accept a map or an object
  388. if (links instanceof Map) {
  389. href = links.get(nameString);
  390. } else if ({}.hasOwnProperty.call(links, nameString)) {
  391. href = links[nameString];
  392. }
  393. return href;
  394. }
  395. _addLinks(nameString) {
  396. const href = this._getHrefForString(nameString);
  397. let link = nameString;
  398. let linkClass = this._options.linkClass || '';
  399. if (href) {
  400. if (linkClass) {
  401. linkClass = ` class="${linkClass}"`;
  402. }
  403. link = `<a href="${href}"${linkClass}>${nameString}</a>`;
  404. }
  405. return link;
  406. }
  407. result(type, useLongFormat) {
  408. const context = new Context();
  409. const key = `function.${modifierKind(useLongFormat)}.returns`;
  410. const result = new Result();
  411. context.type = this.type(type).description;
  412. addModifiers(this, context, result, type, useLongFormat);
  413. result.description = this._translate(key, context);
  414. return result;
  415. }
  416. _signature(type, useLongFormat) {
  417. const context = new Context();
  418. const kind = modifierKind(useLongFormat);
  419. const result = new Result();
  420. let returns;
  421. addModifiers(this, context, result, type, useLongFormat);
  422. addFunctionModifiers(this, context, result, type, useLongFormat);
  423. context.functionParams = this.params(type.params || [], context).description;
  424. if (type.result) {
  425. returns = this.result(type.result, useLongFormat);
  426. if (useLongFormat) {
  427. result.returns = returns.description;
  428. } else {
  429. context.functionReturns = returns.description;
  430. }
  431. }
  432. result.description += this._translate(`function.${kind}.signature`, context).trim();
  433. return result;
  434. }
  435. }
  436. module.exports = (type, options) => {
  437. const simple = new Describer(options).type(type, false);
  438. const extended = new Describer(options).type(type);
  439. [simple, extended].forEach(result => {
  440. result.description = collapseSpaces(result.description.trim());
  441. });
  442. return {
  443. simple: simple.description,
  444. extended
  445. };
  446. };