新增调试信息
This commit is contained in:
+17
@@ -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
@@ -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 (`<`).
|
||||
**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).
|
||||
+55
@@ -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
@@ -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
@@ -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
File diff suppressed because one or more lines are too long
+72
@@ -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
@@ -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 = '.<';
|
||||
} 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
@@ -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
@@ -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
@@ -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"
|
||||
}
|
||||
Reference in New Issue
Block a user