新增调试信息

This commit is contained in:
2025-10-27 22:15:25 +08:00
parent ae62457d8c
commit 04642cb2f0
5479 changed files with 683397 additions and 3450 deletions
+17
View File
@@ -0,0 +1,17 @@
Copyright (c) 2014 Google Inc.
Copyright (c) 2012-2014 Jeff Williams
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute,
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+393
View File
@@ -0,0 +1,393 @@
# Catharsis
[![Build Status][travis-img]][travis-url]
[travis-img]: https://travis-ci.com/hegemonic/catharsis.svg?branch=master
[travis-url]: https://travis-ci.com/hegemonic/catharsis
A JavaScript parser for
[Google Closure Compiler](https://github.com/google/closure-compiler/wiki/Annotating-JavaScript-for-the-Closure-Compiler#type-expressions)
and [JSDoc](https://github.com/jsdoc/jsdoc) type expressions.
Catharsis is designed to be:
+ **Accurate**. Catharsis is based on a [PEG.js](https://pegjs.org/) grammar
that's designed to handle any valid type expression. It uses a thorough test
suite to verify the parser's accuracy.
+ **Fast**. Parse results are cached, so the parser is invoked only when
necessary.
+ **Flexible**. Catharsis can convert a parse result back into a type
expression, or into a description of the type expression. In addition, Catharsis
can parse [JSDoc](https://github.com/jsdoc/jsdoc)-style type expressions.
## Example
```js
const catharsis = require('catharsis');
// Closure Compiler parsing
const type = '!Object';
let parsedType;
try {
parsedType = catharsis.parse(type); // {"type":"NameExpression,"name":"Object","nullable":false}
} catch(e) {
console.error('unable to parse %s: %s', type, e);
}
// JSDoc-style type expressions enabled
const jsdocType = 'string[]'; // Closure Compiler expects Array.<string>
let parsedJsdocType;
try {
parsedJsdocType = catharsis.parse(jsdocType, {jsdoc: true});
} catch (e) {
console.error('unable to parse %s: %s', jsdocType, e);
}
// Converting parse results back to type expressions
catharsis.stringify(parsedType); // !Object
catharsis.stringify(parsedJsdocType); // string[]
catharsis.stringify(parsedJsdocType, {restringify: true}); // Array.<string>
// Converting parse results to descriptions of the type expression
catharsis.describe(parsedType).simple; // non-null Object
catharsis.describe(parsedJsdocType).simple; // Array of string
```
See the
[`test/specs` directory](https://github.com/hegemonic/catharsis/tree/master/test/specs)
for more examples of Catharsis' parse results.
## Methods
### `parse(typeExpression, options)`
Parse a type expression, and return the parse results. Throws an error if the
type expression cannot be parsed.
When called without options, Catharsis attempts to parse type expressions in the
same way as Closure Compiler. When the `jsdoc` option is enabled, Catharsis can
also parse several kinds of type expressions that are permitted in
[JSDoc](https://github.com/jsdoc/jsdoc):
+ The string `function` is treated as a function type with no parameters.
+ You can omit the period from type applications. For example,
`Array.<string>` and `Array<string>` are parsed in the same way.
+ If can append `[]` to a name expression (for example, `string[]`), it is
interpreted as a type application with the expression `Array` (for example,
`Array.<string>`).
+ Name expressions can contain the characters `#`, `~`, `:`, and `/`.
+ Name expressions can contain a suffix that is similar to a function signature
(for example, `MyClass(foo, bar)`).
+ Name expressions can contain a reserved word.
+ Record types can use types other than name expressions for keys.
#### Parameters
+ `type`: A string containing a Closure Compiler type expression.
+ `options`: Options for parsing the type expression.
+ `options.jsdoc`: Specifies whether to enable parsing of JSDoc-style type
expressions. Defaults to `false`.
+ `options.useCache`: Specifies whether to use the cache of parsed types.
Defaults to `true`.
#### Returns
An object containing the parse results. See the
[`test/specs` directory](https://github.com/hegemonic/catharsis/tree/master/test/specs)
for examples of the parse results for different type expressions.
The object also includes two non-enumerable properties:
+ `jsdoc`: A boolean that indicates whether the type expression was parsed with
JSDoc support enabled.
+ `typeExpression`: A string that contains the type expression that was parsed.
### `stringify(parsedType, options)`
Stringify `parsedType`, and return the type expression. If validation is
enabled, throws an error if the stringified type expression cannot be parsed.
#### Parameters ####
+ `parsedType`: An object containing a parsed Closure Compiler type expression.
+ `options`: Options for stringifying the parse results.
+ `options.cssClass`: Synonym for `options.linkClass`. Deprecated in version
0.8.0; will be removed in a future version.
+ `options.htmlSafe`: Specifies whether to return an HTML-safe string that
replaces left angle brackets (`<`) with the corresponding entity (`&lt;`).
**Note**: Characters in name expressions are not escaped.
+ `options.linkClass`: A CSS class to add to HTML links. Used only if
`options.links` is provided. By default, no CSS class is added.
+ `options.links`: An object or map whose keys are name expressions and
whose values are URIs. If a name expression matches a key in
`options.links`, the name expression will be wrapped in an HTML `<a>` tag
that links to the URI. If you also specify `options.linkClass`, the `<a>`
tag includes a `class` attribute. **Note**: When using this option, parsed
types are always restringified, and the resulting string is not cached.
+ `options.restringify`: Forces Catharsis to restringify the parsed type. If
this option is not specified, and the parsed type object includes a
`typeExpression` property, Catharsis returns the `typeExpression` property
without modification when possible. Defaults to `false`.
+ `options.useCache`: Specifies whether to use the cache of stringified type
expressions. Defaults to `true`.
+ `options.validate`: Specifies whether to validate the stringified parse
results by attempting to parse them as a type expression. If the stringified
results are not parsable with the default options, you must also provide the
appropriate options to pass to the `parse()` method. Defaults to `false`.
#### Returns
A string containing the type expression.
### `describe(parsedType, options)`
Convert a parsed type to a description of the type expression. This method is
especially useful if your users are not familiar with the syntax for type
expressions.
The `describe()` method returns the description in two formats:
+ **Simple format**. A string that provides a complete description of the type
expression.
+ **Extended format**. An object that separates out some of the details about
the outermost type expression, such as whether the type is optional, nullable,
or repeatable.
For example, when you call `describe('?function(new:MyObject, string)=')`, the
method returns the following data:
```js
{
simple: 'optional nullable function(constructs MyObject, string)',
extended: {
description: 'function(string)',
modifiers: {
functionNew: 'Returns MyObject when called with new.',
functionThis: '',
optional: 'Optional.',
nullable: 'May be null.',
repeatable: ''
},
returns: ''
}
}
```
#### Parameters
+ `parsedType`: An object containing a parsed Closure Compiler type expression.
+ `options`: Options for creating the description.
+ `options.codeClass`: A CSS class to add to the tag that is wrapped around
type names. Used only if you specify `options.codeTag`. By default, no CSS
class is added.
+ `options.codeTag`: The name of an HTML tag (for example, `code`) to wrap
around type names. For example, if this option is set to `code`, the type
expression `Array.<string>` would have the simple description
`<code>Array</code> of <code>string</code>`.
+ `options.language`: A string identifying the language in which to generate
the description. The identifier should be an
[ISO 639-1 language code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes)
(for example, `en`). It can optionally be followed by a hyphen and an
[ISO 3166-1 alpha-2 country code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)
(for example, `en-US`). If you use values other than `en`, you must provide
translation resources in `options.resources`. Defaults to `en`.
+ `options.linkClass`: A CSS class to add to HTML links. Used only if
`options.links` is provided. By default, no CSS class is added.
+ `options.links`: An object or map whose keys are name expressions and
whose values are URIs. If a name expression matches a key in
`options.links`, the name expression will be wrapped in an HTML `<a>` tag
that links to the URI. If you also specify `options.linkClass`, the `<a>`
tag includes a `class` attribute. **Note**: When you use this option, the
description is not cached.
+ `options.resources`: An object that specifies how to describe type
expressions for a given language. The object's property names should use the
same format as `options.language`. Each property should contain an object in
the same format as the translation resources in
[`res/en.json`](https://github.com/hegemonic/catharsis/blob/master/res/en.json).
If you specify a value for `options.resources.en`, that value overrides the
defaults in `res/en.json`.
+ `options.useCache`: Specifies whether to use the cache of descriptions.
Defaults to `true`.
### Returns
An object with the following properties:
+ `simple`: A string that provides a complete description of the type
expression.
+ `extended`: An object containing details about the outermost type expression.
+ `extended.description`: A string that provides a basic description of the
type expression, excluding the information contained in other properties.
+ `extended.modifiers`: Information about modifiers that apply to the type
expression.
+ `extended.modifiers.functionNew`: A string that describes what a
function returns when called with `new`. Returned only for function
types.
+ `extended.modifiers.functionThis`: A string that describes what the
keyword `this` refers to within a function. Returned only for function
types.
+ `extended.modifiers.nullable`: A string that indicates whether the
type is nullable or non-nullable.
+ `extended.modifiers.optional`: A string that indicates whether the
type is optional.
+ `extended.modifiers.repeatable`: A string that indicates whether the
type can be repeated.
+ `extended.returns`: A string that describes the function's return value.
Returned only for function types.
## Changelog
+ 0.9.0 (June 2020):
+ For the `describe()` and `stringify()` methods, the `options.links`
parameter now accepts either a map or an object.
+ Catharsis now requires Node.js 10 or later.
+ 0.8.11 (July 2019): Updated dependencies.
+ 0.8.10 (May 2019): Updated dependencies.
+ 0.8.9 (July 2017): Type expressions that include an `@` sign (for example,
`module:@prefix/mymodule~myCallback`) are now supported.
+ 0.8.8 (April 2016): Corrected the description of type applications other than
arrays that contain a single type (for example, `Promise.<string>`).
+ 0.8.7 (June 2015):
+ Record types that use numeric literals as property names (for example,
`{0: string}`) are now parsed correctly.
+ Record types with a property that contains a function, with no space after
the preceding colon (for example, `{foo:function()}`), are now parsed
correctly.
+ Repeatable function parameters are no longer required to be enclosed in
brackets, regardless of whether JSDoc-style type expressions are enabled. In
addition, the brackets are omitted when stringifying a parsed type
expression.
+ 0.8.6 (December 2014): Improved the description of the unknown type.
+ 0.8.5 (December 2014): Added support for postfix nullable/non-nullable
operators combined with the optional operator (for example, `foo?=`).
+ 0.8.4 (December 2014): JSDoc-style nested arrays (for example, `number[][]`)
are now parsed correctly when JSDoc-style type expressions are enabled.
+ 0.8.3 (October 2014):
+ Type applications are no longer required to include a period (`.`) as a
separator, regardless of whether JSDoc-style type expressions are enabled.
+ Type unions that are not enclosed in parentheses can now include the
repeatable (`...`) modifier when JSDoc-style type expressions are enabled.
+ Name expressions may now be enclosed in single or double quotation marks
when JSDoc-style type expressions are enabled.
+ 0.8.2 (June 2014): Fixed a compatibility issue with the JSDoc fork of Mozilla
Rhino.
+ 0.8.1 (June 2014): Added support for type unions that are not enclosed in
parentheses, and that contain nullable or non-nullable modifiers (for example,
`!string|!number`).
+ 0.8.0 (May 2014):
+ Added a `describe()` method, which converts a parsed type to a description
of the type.
+ Added a `linkClass` option to the `stringify()` method, and deprecated the
existing `cssClass` option. The `cssClass` option will be removed in a
future release.
+ Clarified and corrected several sections in the `README`.
+ 0.7.1 (April 2014): In record types, property names that begin with a keyword
(for example, `undefinedHTML`) are now parsed correctly when JSDoc-style type
expressions are enabled.
+ 0.7.0 (October 2013):
+ Repeatable type expressions other than name expressions (for example,
`...function()`) are now parsed and stringified correctly.
+ Type expressions that are both repeatable and either nullable or
non-nullable (for example, `...!number`) are now parsed and stringified
correctly.
+ Name expressions are now parsed correctly when they match a property name
in an object instance (for example, `constructor`).
+ 0.6.0 (September 2013): Added support for the type expression `function[]`
when JSDoc-style type expressions are enabled.
+ 0.5.6 (April 2013):
+ For consistency with Closure Compiler, parentheses are no longer required
around type unions, regardless of whether JSDoc-style type expressions are
enabled.
+ For consistency with Closure Compiler, you can now use postfix notation
for the `?` (nullable) and `!` (non-nullable) modifiers. For example,
`?string` and `string?` are now treated as equivalent.
+ String literals and numeric literals are now allowed as property names
within name expressions. For example, the name expression `Foo."bar"` is now
parsed correctly.
+ 0.5.5 (April 2013): Corrected a parsing issue with name expressions that end
with a value enclosed in parentheses.
+ 0.5.4 (April 2013):
+ Repeatable literals (for example, `...*`) are now parsed correctly.
+ When JSDoc-style type expressions are enabled, a name expression can now
contain a value enclosed in parentheses at the end of the name expression
(for example, `MyClass(2)`).
+ 0.5.3 (March 2013): The `parse()` method now correctly parses name expressions
that contain hyphens.
+ 0.5.2 (March 2013): The `parse()` method now correctly parses function types
when JSDoc-style type expressions are enabled.
+ 0.5.1 (March 2013): Newlines and extra spaces are now removed from type
expressions before they are parsed.
+ 0.5.0 (March 2013):
+ The `parse()` method's `lenient` option has been renamed to `jsdoc`.
**Note**: This change is not backwards-compatible with previous versions.
+ The `stringify()` method now accepts `cssClass` and `links` options, which
you can use to add HTML links to a type expression.
+ 0.4.3 (March 2013):
+ The `stringify()` method no longer caches HTML-safe type expressions as if
they were normal type expressions.
+ The `stringify()` method's options parameter may now include an
`options.restringify` property, and the behavior of the `options.useCache`
property has changed.
+ 0.4.2 (March 2013):
+ When lenient parsing is enabled, name expressions can now contain the
characters `:` and `/`.
+ When lenient parsing is enabled, a name expression followed by `[]` (for
example, `string[]`) will be interpreted as a type application with the
expression `Array` (for example, `Array.<string>`).
+ 0.4.1 (March 2013):
+ The `parse()` and `stringify()` methods now honor all of the specified
options.
+ When lenient parsing is enabled, name expressions can now contain a
reserved word.
+ 0.4.0 (March 2013):
+ Catharsis now supports a lenient parsing option that can parse several
kinds of malformed type expressions. See the documentation for details.
+ The objects containing parse results are now frozen.
+ The objects containing parse results now have two non-enumerable
properties:
+ `lenient`: A boolean indicating whether the type expression was parsed
in lenient mode.
+ `typeExpression`: A string containing the original type expression.
+ The `stringify()` method now honors the `useCache` option. If a parsed
type includes a `typeExpression` property, and `useCache` is not set to
`false`, the stringified type will be identical to the original type
expression.
+ 0.3.1 (March 2013): Type expressions that begin with a reserved word, such as
`integer`, are now parsed correctly.
+ 0.3.0 (March 2013):
+ The `parse()` and `stringify()` methods are now synchronous, and the
`parseSync()` and `stringifySync()` methods have been removed. **Note**:
This change is not backwards-compatible with previous versions.
+ The parse results now use a significantly different format from previous
versions. The new format is more expressive and is similar, but not
identical, to the format used by the
[doctrine](https://github.com/eslint/doctrine) parser. **Note**: This change
is not backwards-compatible with previous versions.
+ Name expressions that contain a reserved word now include a
`reservedWord: true` property.
+ Union types that are optional or nullable, or that can be repeated, are
now parsed and stringified correctly.
+ Optional function types and record types are now parsed and stringified
correctly.
+ Function types now longer include `new` or `this` properties unless the
properties are defined in the type expression. In addition, the `new` and
`this` properties can now use any type expression.
+ In record types, the key for a field type can now use any type expression.
+ Standalone single-character literals, such as ALL (`*`), are now parsed
and stringified correctly.
+ `null` and `undefined` literals with additional properties, such as
`repeatable`, are now stringified correctly.
+ 0.2.0 (November 2012):
+ Added `stringify()` and `stringifySync()` methods, which convert a parsed
type to a type expression.
+ Simplified the parse results for function signatures. **Note**: This
change is not backwards-compatible with previous versions.
+ Corrected minor errors in README.md.
+ 0.1.1 (November 2012): Added `opts` argument to `parse()` and `parseSync()`
methods. **Note**: The change to `parse()` is not backwards-compatible with
previous versions.
+ 0.1.0 (November 2012): Initial release.
## License
[MIT license](https://github.com/hegemonic/catharsis/blob/master/LICENSE).
Generated Vendored Executable
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env node
// Command-line tool that parses a type expression and dumps a JSON version of the parse tree.
const catharsis = require('../catharsis');
const path = require('path');
const util = require('util');
const command = path.basename(process.argv[1]);
const typeExpression = process.argv[2];
const opts = {
describe: false,
jsdoc: false
};
let parsedType;
function usage() {
console.log(util.format('Usage:\n %s typeExpression [--jsdoc] [--describe]', command));
}
function done(err) {
/* eslint-disable no-process-exit */
process.exit(err === undefined ? 0 : err);
/* eslint-enable no-process-exit */
}
process.argv.slice(3).forEach(arg => {
const parsedArg = arg.replace(/^-{2}/, '');
if (opts[parsedArg] !== undefined) {
opts[parsedArg] = true;
} else {
console.error('Unknown option "%s"', arg);
usage();
done(1);
}
});
if (!typeExpression) {
usage();
done(1);
} else {
try {
parsedType = catharsis.parse(typeExpression, opts);
if (opts.describe) {
parsedType = catharsis.describe(parsedType);
}
} catch (e) {
console.error(util.format('Unable to parse "%s" (exception follows):', typeExpression));
console.error(e.stack || e.message);
done(1);
}
console.log(JSON.stringify(parsedType, null, 2));
done();
}
+169
View File
@@ -0,0 +1,169 @@
/**
* Catharsis
* A parser for Google Closure Compiler type expressions, powered by PEG.js.
*
* @author Jeff Williams <jeffrey.l.williams@gmail.com>
* @license MIT
*/
const describe = require('./lib/describe');
const { parse } = require('./lib/parser');
const stringify = require('./lib/stringify');
const typeExpressionCache = {
normal: new Map(),
jsdoc: new Map()
};
const parsedTypeCache = {
normal: new Map(),
htmlSafe: new Map()
};
const descriptionCache = {
normal: new Map()
};
function getTypeExpressionCache({useCache, jsdoc}) {
if (useCache === false) {
return null;
} else if (jsdoc === true) {
return typeExpressionCache.jsdoc;
} else {
return typeExpressionCache.normal;
}
}
function getParsedTypeCache({useCache, links, htmlSafe}) {
if (useCache === false || links !== null || links !== undefined) {
return null;
} else if (htmlSafe === true) {
return parsedTypeCache.htmlSafe;
} else {
return parsedTypeCache.normal;
}
}
function getDescriptionCache({useCache, links}) {
if (useCache === false || links !== null || links !== undefined) {
return null;
} else {
return descriptionCache.normal;
}
}
// can't return the original if any of the following are true:
// 1. restringification was requested
// 2. htmlSafe option was requested
// 3. links option was provided
// 4. typeExpression property is missing
function canReturnOriginalExpression(parsedType, {restringify, htmlSafe, links}) {
return restringify !== true && htmlSafe !== true &&
(links === null || links === undefined) &&
Object.prototype.hasOwnProperty.call(parsedType, 'typeExpression');
}
// Add non-enumerable properties to a result object, then freeze it.
function prepareFrozenObject(obj, expr, {jsdoc}) {
Object.defineProperty(obj, 'jsdoc', {
value: jsdoc === true ? jsdoc : false
});
if (expr) {
Object.defineProperty(obj, 'typeExpression', {
value: expr
});
}
return Object.freeze(obj);
}
function cachedParse(expr, options) {
const cache = getTypeExpressionCache(options);
let parsedType = cache ? cache.get(expr) : null;
if (parsedType) {
return parsedType;
} else {
parsedType = parse(expr, options);
parsedType = prepareFrozenObject(parsedType, expr, options);
if (cache) {
cache.set(expr, parsedType);
}
return parsedType;
}
}
function cachedStringify(parsedType, options) {
const cache = getParsedTypeCache(options);
let stringified;
if (canReturnOriginalExpression(parsedType, options)) {
return parsedType.typeExpression;
} else if (cache) {
stringified = cache.get(parsedType);
if (!stringified) {
stringified = stringify(parsedType, options);
cache.set(parsedType, stringified);
}
return stringified;
} else {
return stringify(parsedType, options);
}
}
function cachedDescribe(parsedType, options) {
const cache = getDescriptionCache(options);
let description = cache ? cache.get(parsedType) : null;
if (description) {
return description;
} else {
description = describe(parsedType, options);
description = prepareFrozenObject(description, null, options);
if (cache) {
cache.set(parsedType, description);
}
return description;
}
}
/* eslint-disable class-methods-use-this */
class Catharsis {
constructor() {
this.Types = require('./lib/types');
}
parse(typeExpr, options = {}) {
typeExpr = typeExpr.replace(/[\r\n]/g, '')
.replace(/\s+/g, ' ')
.trim();
return cachedParse(typeExpr, options);
}
stringify(parsedType, options) {
let result;
options = options || {};
result = cachedStringify(parsedType, options);
if (options.validate) {
this.parse(result, options);
}
return result;
}
describe(parsedType, options = {}) {
return cachedDescribe(parsedType, options);
}
}
/* eslint-enable class-methods-use-this */
module.exports = new Catharsis();
+563
View File
@@ -0,0 +1,563 @@
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
};
};
+5776
View File
File diff suppressed because one or more lines are too long
+72
View File
@@ -0,0 +1,72 @@
const _ = require('lodash');
// JSON schema types
const ARRAY = 'array';
const BOOLEAN = 'boolean';
const OBJECT = 'object';
const STRING = 'string';
const BOOLEAN_SCHEMA = {
type: BOOLEAN
};
const STRING_SCHEMA = {
type: STRING
};
const TYPES = require('./types');
const TYPE_NAMES = _.values(TYPES);
module.exports = {
type: OBJECT,
additionalProperties: false,
properties: {
type: {
type: STRING,
enum: TYPE_NAMES
},
// field type
key: { '$ref': '#' },
value: { '$ref': '#' },
// function type
params: {
type: ARRAY,
items: { '$ref': '#' }
},
'new': { '$ref': '#' },
'this': { '$ref': '#' },
result: {'$ref': '#' },
// name expression
name: STRING_SCHEMA,
// record type
fields: {
type: ARRAY,
items: { '$ref': '#' }
},
// type application
expression: { '$ref': '#' },
applications: {
type: ARRAY,
minItems: 1,
maxItems: 2,
items: { '$ref': '#' }
},
// type union
elements: {
type: ARRAY,
minItems: 1,
items: { '$ref': '#' }
},
optional: BOOLEAN_SCHEMA,
nullable: BOOLEAN_SCHEMA,
repeatable: BOOLEAN_SCHEMA,
reservedWord: BOOLEAN_SCHEMA
},
required: ['type']
};
+277
View File
@@ -0,0 +1,277 @@
/* eslint-disable class-methods-use-this */
const Types = require('./types');
function combineNameAndType(nameString, typeString) {
const separator = (nameString && typeString) ? ':' : '';
return nameString + separator + typeString;
}
class Stringifier {
constructor(options) {
this._options = options || {};
this._options.linkClass = this._options.linkClass || this._options.cssClass;
}
applications(applications) {
let result = '';
const strings = [];
if (!applications) {
return result;
}
for (let i = 0, l = applications.length; i < l; i++) {
strings.push(this.type(applications[i]));
}
if (this._options.htmlSafe) {
result = '.&lt;';
} else {
result = '.<';
}
result += `${strings.join(', ')}>`;
return result;
}
elements(elements) {
let result = '';
const strings = [];
if (!elements) {
return result;
}
for (let i = 0, l = elements.length; i < l; i++) {
strings.push(this.type(elements[i]));
}
result = `(${strings.join('|')})`;
return result;
}
key(type) {
return this.type(type);
}
name(name) {
return name || '';
}
new(funcNew) {
return funcNew ? `new:${this.type(funcNew)}` : '';
}
nullable(nullable) {
switch (nullable) {
case true:
return '?';
case false:
return '!';
default:
return '';
}
}
optional(optional) {
if (optional === true) {
return '=';
} else {
return '';
}
}
params(params) {
let result = '';
const strings = [];
if (!params || params.length === 0) {
return result;
}
for (let i = 0, l = params.length; i < l; i++) {
strings.push(this.type(params[i]));
}
result = strings.join(', ');
return result;
}
result(result) {
return result ? `: ${this.type(result)}` : '';
}
stringify(type) {
return this.type(type);
}
this(funcThis) {
return funcThis ? `this:${this.type(funcThis)}` : '';
}
type(type) {
let typeString = '';
if (!type) {
return typeString;
}
switch (type.type) {
case Types.AllLiteral:
typeString = this._formatNameAndType(type, '*');
break;
case Types.FunctionType:
typeString = this._signature(type);
break;
case Types.NullLiteral:
typeString = this._formatNameAndType(type, 'null');
break;
case Types.RecordType:
typeString = this._record(type);
break;
case Types.TypeApplication:
typeString = this.type(type.expression) + this.applications(type.applications);
break;
case Types.UndefinedLiteral:
typeString = this._formatNameAndType(type, 'undefined');
break;
case Types.TypeUnion:
typeString = this.elements(type.elements);
break;
case Types.UnknownLiteral:
typeString = this._formatNameAndType(type, '?');
break;
default:
typeString = this._formatNameAndType(type);
}
// add optional/nullable/repeatable modifiers
if (!this._options._ignoreModifiers) {
typeString = this._addModifiers(type, typeString);
}
return typeString;
}
_record(type) {
const fields = this._recordFields(type.fields);
return `{${fields.join(', ')}}`;
}
_recordFields(fields) {
let field;
let keyAndValue;
const result = [];
if (!fields) {
return result;
}
for (let i = 0, l = fields.length; i < l; i++) {
field = fields[i];
keyAndValue = this.key(field.key);
keyAndValue += field.value ? `: ${this.type(field.value)}` : '';
result.push(keyAndValue);
}
return result;
}
// Adds optional, nullable, and repeatable modifiers if necessary.
_addModifiers(type, typeString) {
let combined;
let optional = '';
let repeatable = '';
if (type.repeatable) {
repeatable = '...';
}
combined = this.nullable(type.nullable) + combineNameAndType('', typeString);
optional = this.optional(type.optional);
return repeatable + combined + optional;
}
_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;
}
_formatNameAndType(type, literal) {
let nameString = type.name || literal || '';
const typeString = type.type ? this.type(type.type) : '';
nameString = this._addLinks(nameString);
return combineNameAndType(nameString, typeString);
}
_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;
}
_signature(type) {
let param;
let prop;
let signature;
const params = [];
// these go within the signature's parens, in this order
const props = [
'new',
'this',
'params'
];
for (let i = 0, l = props.length; i < l; i++) {
prop = props[i];
param = this[prop](type[prop]);
if (param.length > 0) {
params.push(param);
}
}
signature = `function(${params.join(', ')})`;
signature += this.result(type.result);
return signature;
}
}
module.exports = (type, options) => new Stringifier(options).stringify(type);
+22
View File
@@ -0,0 +1,22 @@
module.exports = Object.freeze({
// `*`
AllLiteral: 'AllLiteral',
// like `blah` in `{blah: string}`
FieldType: 'FieldType',
// like `function(string): string`
FunctionType: 'FunctionType',
// any string literal, such as `string` or `My.Namespace`
NameExpression: 'NameExpression',
// null
NullLiteral: 'NullLiteral',
// like `{foo: string}`
RecordType: 'RecordType',
// like `Array.<string>`
TypeApplication: 'TypeApplication',
// like `(number|string)`
TypeUnion: 'TypeUnion',
// undefined
UndefinedLiteral: 'UndefinedLiteral',
// `?`
UnknownLiteral: 'UnknownLiteral'
});
+30
View File
@@ -0,0 +1,30 @@
{
"version": "0.9.0",
"name": "catharsis",
"description": "A JavaScript parser for Google Closure Compiler and JSDoc type expressions.",
"author": "Jeff Williams <jeffrey.l.williams@gmail.com>",
"repository": {
"type": "git",
"url": "https://github.com/hegemonic/catharsis"
},
"bugs": "https://github.com/hegemonic/catharsis/issues",
"main": "catharsis.js",
"dependencies": {
"lodash": "^4.17.15"
},
"devDependencies": {
"ajv": "^6.12.2",
"mocha": "^8.0.1",
"pegjs": "^0.10.0",
"should": "^13.2.3",
"should-equal": "^2.0.0"
},
"engines": {
"node": ">= 10"
},
"scripts": {
"prepare": "./node_modules/pegjs/bin/pegjs ./lib/parser.pegjs",
"test": "./node_modules/mocha/bin/mocha"
},
"license": "MIT"
}
+92
View File
@@ -0,0 +1,92 @@
{
"all": "any type",
"application": {
"array": "<%= prefix %> <%= codeTagOpen %>Array<%= codeTagClose %> of <%= application %> <%= suffix %>",
"object": "<%= prefix %> <%= codeTagOpen %>Object<%= codeTagClose %> with <%= application %> properties <%= suffix %>",
"objectNonString": "<%= prefix %> <%= codeTagOpen %>Object<%= codeTagClose %> with <%= keyApplication %> keys and <%= application %> properties <%= suffix %>",
"other": "<%= prefix %> <%= codeTagOpen %><%= type %> containing <%= application %> <%= suffix %>"
},
"function": {
"extended": {
"new": "Returns <%= functionNew %> when called with <%= codeTagOpen %>new<%= codeTagClose %>.",
"returns": "Returns <%= type %>.",
"signature": "function(<%= functionParams %>)",
"this": "Within the function, <%= codeTagOpen %>this<%= codeTagClose %> refers to <%= functionThis %>."
},
"simple": {
"new": "constructs <%= functionNew %>",
"returns": "returns <%= type %>",
"signature": "<%= prefix %> function(<%= functionParams %>) <%= functionReturns %>",
"this": "<%= codeTagOpen %>this<%= codeTagClose %> = <%= functionThis %>"
}
},
"modifiers": {
"extended": {
"nonNullable": "Must not be null.",
"nullable": "May be null.",
"optional": "Optional.",
"prefix": "",
"repeatable": "May be provided more than once.",
"suffix": ""
},
"simple": {
"nonNullable": "non-null",
"nullable": "nullable",
"optional": "optional",
"prefix": "<%= optional %> <%= nullable %> <%= repeatable %>",
"repeatable": "repeatable",
"suffix": ""
}
},
"name": "<%= codeTagOpen %>{{ name }}<%= codeTagClose %> <%= suffix %>",
"null": "null",
"params": {
"first": {
"one": "<%= param %>",
"two": "<%= param %>, ",
"many": "<%= param %>, "
},
"middle": {
"many": "<%= param %>, "
},
"last": {
"two": "<%= param %>",
"many": "<%= param %>"
}
},
"record": {
"first": {
"one": "<%= prefix %> {<%= field %>} <%= suffix %>",
"two": "<%= prefix %> {<%= field %>, ",
"many": "<%= prefix %> {<%= field %>, "
},
"middle": {
"many": "<%= field %>, "
},
"last": {
"two": "<%= field %>} <%= suffix %>",
"many": "<%= field %>} <%= suffix %>"
}
},
"field": {
"typed": "<%= name %>: <%= type %>",
"untyped": "<%= name %>"
},
"type": "<%= prefix %> <%= codeTagOpen %><%= type %><%= codeTagClose %> <%= suffix %>",
"undefined": "undefined",
"union": {
"first": {
"one": "<%= prefix %> <%= element %> <%= suffix %>",
"two": "<%= prefix %> (<%= element %> ",
"many": "<%= prefix %> (<%= element %>, "
},
"middle": {
"many": "<%= element %>, "
},
"last": {
"two": "or <%= element %>) <%= suffix %>",
"many": "or <%= element %>) <%= suffix %>"
}
},
"unknown": "unknown"
}