新增调试信息

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
+1
View File
@@ -0,0 +1 @@
**/*
+17
View File
@@ -0,0 +1,17 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "jsdoc 4",
"skipFiles": [
"<node_internals>/**"
],
"program": "node jsdoc.js -c ./tmp/jsdoc-conf.js ./tmp/foo.js"
}
]
}
+202
View File
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+899
View File
@@ -0,0 +1,899 @@
# JSDoc change history
This file describes notable changes in each version of JSDoc, starting with version 3.0.0.
## 4.0.3 (May 2024)
Updates dependencies.
## 4.0.2 (February 2023)
Updates the Babel parser, and enables additional Babel plugins for ECMAScript proposals.
## 4.0.1 (February 2023)
If an inline link tag uses a pipe delimiter, and there are spaces around the pipe delimiter, then
the HTML link no longer contains `%20` at the end; also, the link text no longer contains extra
spaces. For example, `{@link https://example.com | link text}` is now rendered as
`<a href="https://example.com/">link text</a>` rather than
`<a href="https://example.com/%20"> link text</a>`.
## 4.0.0 (November 2022)
+ JSDoc releases now use [semantic versioning](https://semver.org/). If JSDoc makes
backwards-incompatible changes in the future, the major version will be incremented.
+ JSDoc no longer uses the [`taffydb`](https://taffydb.com/) package. If your JSDoc template or
plugin uses the `taffydb` package, see the
[instructions for replacing `taffydb` with `@jsdoc/salty`](https://github.com/jsdoc/jsdoc/tree/main/packages/jsdoc-salty#use-salty-in-a-jsdoc-template).
+ JSDoc now supports Node.js 12.0.0 and later.
## 3.6.11 (July 2022)
Updates dependency versions to make JSDoc compatible with Node.js 12.0.0 and later.
## 3.6.10 (January 2022)
Fixes an issue in JSDoc 3.6.9 that prevented JSDoc from being installed in some continuous
integration (CI) environments.
## 3.6.9 (January 2022)
Fixes an issue in JSDoc 3.6.8 that prevented `npm install jsdoc` from working.
## 3.6.8 (January 2022)
Updates dependencies.
## 3.6.7 (May 2021)
Updates dependencies.
## 3.6.6 (September 2020)
Fixes an issue that could cause members of an interface to be tracked incorrectly if the interface
was both defined as an ES2015 class and assigned to a variable. For example:
```js
/** @interface */
foo.Bar = class {
constructor() {
/** This member was missing from the generated docs. */
this.baz = null;
}
}
```
## 3.6.5 (July 2020)
Prevents circular references in doclets when two function parameters use the same type expression,
and the `--debug` flag is present.
## 3.6.4 (April 2020)
Updates dependencies.
## 3.6.3 (July 2019)
Updates dependencies.
## 3.6.2 (May 2019)
Fixes an issue that prevented ES 2015 classes from appearing in generated docs. ([#1644][1644])
## 3.6.1 (May 2019)
Prevents a crash when using type applications in Node.js 12. ([#1643][1643])
## 3.6.0 (May 2019)
### Major changes
+ JSDoc is now compatible with Node.js 12, and it requires Node.js 8.15.0 or later.
+ JSDoc now recognizes all documented Closure Compiler tags. ([#605][605])
### Enhancements
+ You can now use the `templates.useShortNamesInLinks` configuration setting to show the short name
of each symbol in link text (for example, `baz`), rather than the full longname (for example,
`foo.bar.baz`). ([#738][738])
+ When you enable the Markdown plugin, you can now specify a function that performs syntax
highlighting in code blocks. ([#1412][1412])
+ The default template now places namespaces near the top of the TOC. ([#1410][1410])
### Bug fixes
+ When you add a JSDoc comment to an ES2015 constructor, JSDoc now preserves all of the JSDoc tags,
not only the description and parameters. ([#1129][1129])
+ The `@exports` tag now works correctly when it is combined with the `@enum` tag. ([#970][970])
+ When you enable the Markdown plugin, and you use a code fence with the language set to `plain`,
JSDoc no longer pretty-prints the code block in the generated documentation. ([#1361][1361])
## 3.5.5 (September 2017)
Fixes a compatibility issue with Node.js 8.5.0. (#1438)
## 3.5.4 (August 2017)
+ When a class uses the `@hideconstructor` tag, the default template no longer displays the names of
parameters that the constructor accepts. (#1397)
+ When an arrow function expression returns a class, the class's methods and properties are now
named correctly. (#1409)
+ JSDoc no longer crashes when an anonymous class is passed as a function parameter. (#1416)
+ JSDoc now allows `import` and `export` declarations anywhere where a statement is allowed. (#1411)
+ JSDoc now allows `return` statements outside of functions. (#1411)
+ JSDoc now allows `super()` calls outside of a method definition. (#1411)
+ JSDoc no longer exits before the `STDOUT` pipe has been flushed. (#1408)
## 3.5.3 (July 2017)
+ Non-JSDoc comments (comments that do not begin with `/**`) are now ignored. (#1398)
+ JSDoc no longer crashes when it parses a class property with no value assigned to it. (#1400)
+ When there are JSDoc comments at the end of a source file that has a `'use strict';` directive,
the comments are no longer ignored. (#1396)
+ Namepaths that contain an `@` sign (for example, `module:@prefix/my-module~myCallback`) are now
parsed correctly. (#1302)
+ The default template now displays interfaces that belong to a namespace. (#1406)
+ When an ES2015 class inside a module uses an `@alias` tag, the class's constructor now gets the
correct longname. (#1395)
+ When there are no input files to process, JSDoc no longer prints help text to the console. (#1404)
## 3.5.2 (July 2017)
+ The default template now hides parameters and properties for class constructors that are hidden
with the `@hideconstructor` tag. (#1397)
+ JSDoc now uses an improved algorithm for locating plugins and template resources. (#1394)
+ When the `@alias` tag identifies an instance member (for example, `@alias Foo#bar`), the alias is
now applied correctly. (#1385)
+ When the `@alias` tag is applied to a class that is within a module and is aliased to the module
name, the class's instance members are now documented correctly. (#1134)
+ Fixed a crash when a `@param` tag uses the wrong delimiter to close the type expression (for
example, `@param {Object)`). (#1221)
+ The Markdown plugin now converts Markdown-formatted text in the `@summary` tag. (#1149)
## 3.5.1 (July 2017)
+ Fixed an issue that prevented JSDoc from working on versions of Node.js prior to 5.10.0. (#1393)
+ If the JSDoc configuration file does not have a file extension, JSDoc now assumes that the file
is in JSON format. (#1391)
## 3.5.0 (July 2017)
### Major changes
+ JSDoc now uses the [Babylon](https://github.com/babel/babylon) JavaScript parser, which means that
JSDoc can parse any JavaScript or JSX file that is supported by the [Babel](https://babeljs.io/)
compiler. For example, JSDoc can now parse JavaScript files that include all of the following
language features:
+ [Decorators](https://github.com/tc39/proposal-decorators)
+ [Public and private class fields](https://github.com/tc39/proposal-class-fields)
+ [Asynchronous iterators](https://github.com/tc39/proposal-async-iteration)
+ [Dynamic `import()`](https://github.com/tc39/proposal-dynamic-import)
+ [Optional chaining](https://github.com/tc39/proposal-optional-chaining)
+ You can now use a JavaScript file to configure JSDoc. The JavaScript file must be a CommonJS
module that exports a single configuration object. See the
[documentation](https://jsdoc.app/about-configuring-jsdoc.html) for details and examples.
+ Fixed multiple issues with documenting ES2015 classes and modules. See "Bug fixes" for details.
+ JSDoc now requires Node.js 4.2.0 or later.
### New tags
**Note**: Third-party templates may not support these new tags.
+ You can now use the new [`@async` tag](https://jsdoc.app/tags-async.html) to indicate that a
function is asynchronous (that is, that it was declared with the syntax `async function foo() {}`).
In general, you do not need to use this tag, because JSDoc autodetects asynchronous functions.
(#1188)
+ You can now use the new [`@generator` tag](https://jsdoc.app/tags-generator.html) to indicate
that a function is a generator function. In general, you do not need to use this tag, because JSDoc
autodetects generator functions. (#1158)
+ You can now use the new [`@hideconstructor` tag](https://jsdoc.app/tags-hideconstructor.html) to
tell JSDoc to hide a class's constructor from the documentation. (#952)
+ You can now use the new [`@package` tag](https://jsdoc.app/tags-package.html) to indicate that a
symbol is package-private. (#962)
+ You can now use the new [`@yields` tag](https://jsdoc.app/tags-yields.html) to document the
value that is yielded by a generator function. (#1388)
### Enhancements
+ JSDoc can now parse files that contain asynchronous functions (that is, functions declared as
`async function foo() {}`), and JSDoc autodetects when a function is asynchronous. (#1188)
+ JSDoc now autodetects generator functions. (#1158)
+ When JSDoc cannot parse a type expression, it now logs the line number on which the type
expression was found. (#1057)
+ When JSDoc fires `jsdocCommentFound` and `symbolFound` events, the event now includes a `columnno`
property indicating the column number on which the comment or symbol was found. (#1362)
+ You can now use the new `sourceType` configuration option to control how JavaScript files are
parsed. The default value is `module`. Set the value to `script` to suppress implied strict mode;
this setting will also prevent you from using ES2015 modules. (#1210)
+ You can now use the new `recurseDepth` configuration option to control how many levels deep JSDoc
will recursively search for files. The default value is 10. (#1340)
### Bug fixes
+ JSDoc now correctly documents the constructors and instance properties of ES2015 classes. (#1182)
+ JSDoc now correctly documents the constructor of an ES2015 class exported from an ES2015 module.
(#1272)
+ JSDoc now uses the correct scope for exported symbols, and their children, in ES2015 modules.
(#1293)
+ When JSDoc is run in a directory that has a `plugins/` or `templates/` directory, JSDoc can now
discover plugins and templates in other directories. (#1081, #1308)
+ JSDoc no longer crashes when it reads a UTF-8 JSON file with a leading BOM. (#1256, #1297)
+ When a function is assigned to a variable, JSDoc now autodetects the function's default and
repeatable parameters. (#1054)
+ JSDoc no longer crashes when the `@author` tag does not have a value. (#1289)
+ JSDoc now always calls `process.exit()` when exiting. (#1287)
### Default template
+ The default template now identifies asynchronous and generator functions. (#1158, #1188)
+ The default template now displays appropriate documentation for namespaces that are also
functions. (#955)
+ Images that are wider than the text area are now displayed correctly. (#1359)
## 3.4.3 (November 2016)
Updated JSDoc's `LICENSE.md` file.
## 3.4.2 (October 2016)
+ Classes exported from an ES2015 module are now documented correctly. (#1137)
+ Fixed an issue that prevented plugins and templates from being loaded correctly. (#1259)
+ Fixed a crash when using the experimental object spread operator in assignments. (#1258)
## 3.4.1 (September 2016)
### Enhancements
+ When installing JSDoc from NPM, all dependencies are now pulled from NPM. (#961)
+ The configuration setting `tags.allowUnknownTags` may now contain an array of tag names that
should be allowed. (#1159)
### Bug fixes
+ When an ES2015 module's default export is a class, JSDoc now documents the class correctly.
(#1113, #1120)
+ JSDoc no longer crashes when an ES2015 module exports an anonymous class. (#1113)
+ JSDoc no longer crashes when the experimental object spread operator is used. (#1141)
+ In ES2015 methods, JSDoc now autodetects whether a parameter is a default or repeatable parameter.
(#1144)
+ The Markdown plugin now works correctly with inline tags that contain special characters, such as
`{@link chat."#channel"}`. (#1035)
+ When JSDoc is run in a directory that has a `plugins/` or `templates/` directory, JSDoc can now
discover plugins and templates in other directories. (#1081)
### Templates
+ The default template now uses appropriate styles for displaying tables. (#1064)
+ The default template's CSS file no longer uses the same style for both `<h2>` and `<h3>` elements.
(#1108)
+ JSDoc now includes a `silent` template that generates no output. This template makes it easier to
use JSDoc as a linter to check for syntax errors and unrecognized tags in documentation comments.
(#1160)
## 3.4.0 (November 2015)
### Major changes
+ JSDoc is now compatible with Node.js 4.0.0 and later.
+ JSDoc no longer runs on Mozilla Rhino. Use Node.js to run JSDoc.
+ JSDoc can now parse ECMAScript 2015 code, including code that uses native classes and modules.
(#555)
+ JSDoc can now parse [JSX](https://facebook.github.io/jsx/) files. (#1001)
+ JSDoc's `app` and `env` global variables are now deprecated. Avoid using the `app` global. Use the
`jsdoc/env` module instead of the `env` global. (#812)
### Enhancements
+ `const` declarations are now automatically treated as constants. (#555)
+ Templates may now run asynchronously. To create an asynchronous template, simply return a promise
from your template's `publish` method. (#953)
### Bug fixes
+ Symbols now get the correct longname when they are defined as properties of a prototype and
include special characters, such as `#` and `.` (for example, `Foo.prototype['this#is#bar']`).
(#888)
+ Instance members that are defined as computed properties of `this` (for example, `this['bar']`)
now get the correct longname. (#890)
+ When an instance member (for example, `this.bar`) is documented within a member of a prototype
(for example, `Foo.prototype.setBar`), the instance member's longname is now set correctly. (#1011)
+ The `@borrows` tag now works with symbol names that contain whitespace. (#818)
### Plugins
+ For the Markdown plugin, you can now autogenerate a heading ID for each heading by setting the
configuration property `markdown.idInHeadings` to `true`. (#1032)
### Template improvements
+ In the default template, you can now show the full namepath of each object in the navigation
column by setting the configuration property `templates.default.useLongnameInNav` to `true`. (#986)
+ In the Haruki template, falsy default values now appear in the output. (#1063)
## 3.3.3 (September 2015)
+ Symbols named `prototype` are now handled correctly. (#891)
+ Fixed an issue that could cause JSDoc to go into an infinite loop when a module is documented
twice. (#975)
+ Fixed an issue that could cause parsing errors on valid regular expressions. (#1053)
## 3.3.2 (June 2015)
JSDoc no longer crashes when parsing a large number of files, or a single object that has a large
number of properties. (#976)
## 3.3.1 (June 2015)
+ Fixed a crash in the Haruki template. (#1005)
+ When a type expression includes a record type with numeric keys (for example, `{0: string}`), the
type expression is now parsed correctly. (#1016)
## 3.3.0 (May 2015)
### Major changes
+ You can now run JSDoc on Node.js. (#93)
+ You can now use the `@interface` and `@implements` tags to document interfaces and their
implementations. (#720, #828)
+ Closure Compiler's `@inheritDoc` and `@override` tags are now supported. (#53)
+ If the JSDoc comment for a symbol includes the `@mixes` tag, all of the mixins now appear in the
symbol's documentation. (#378)
+ JSDoc can now log information to the console as it runs (for example, the name of each file that
JSDoc parses). To log this information, run JSDoc with the `--verbose` flag. (#416)
+ You can now use any file as the package or README file for your documentation. Use the
`-P/--package/` and `-R/--readme` flags to specify the package and README file. (#708)
+ The default template's typography and color scheme have been significantly improved. (#550, #780,
#843)
### Enhancements
+ You can now use the `--pedantic` flag to treat all errors as fatal errors, and to treat warnings
as errors. This flag replaces the `--lenient` flag, which had roughly the opposite meaning and is no
longer available. (#416)
+ You can now use the `-a/--access` flag to control whether private, protected, and public symbols
appear in the documentation. (#860, #861)
+ You can now use the `--debug` flag to log detailed debugging information to the console. This
information can help you diagnose bugs in JSDoc itself. (#416)
+ JSDoc's configuration file can now contain JavaScript comments. (#660)
+ You can now include source files from a directory, but exclude one of its subdirectories, by
adding the subdirectory to the `source.exclude` option in the configuration file. (#484)
+ The `source.exclude` option now works correctly when JSDoc is run with the `-r/--recurse` flag.
(#616)
+ When JSDoc is run with the `-r/--recurse` flag, it now scans for tutorials recursively. (#712)
+ JSDoc's `-X/--explain` option now runs much more quickly. (#633)
+ If all of the text for an `@example` tag is indented, JSDoc now removes the extra indentation.
(#540)
+ The default value for a parameter or property can now include brackets (for example,
`@param {Array.<string>} [foo=['bar']]`). (#640)
+ You can now provide a default value for parameters and properties that are not optional (for
example, `@property {string} foo='bar'`). (#791)
+ If the `@type` tag includes a description (for example, `@type {string} some text`), JSDoc now
parses the type expression correctly and discards the description. (#615)
+ You can now add JSDoc comments to function parameters. (#473)
+ For Closure Compiler projects, you can now enable a Closure Compiler-specific tag dictionary that
more closely matches Closure Compiler's semantics. To enable Closure Compiler semantics, set the
configuration file's `tags.dictionaries` option to `['closure']`. This option can contain the values
`jsdoc`, `closure`, or both. If multiple dictionaries are enabled, and a tag is defined in more than
one dictionary, JSDoc uses the definition from the first dictionary that contains the tag. (#729,
#730, #731, #732)
+ If one symbol overrides another, JSDoc now adds an `overrides` property to the doclet that is
overriding another. The `overrides` property contains the longname of the overridden symbol. (#792)
+ When a JSDoc tag contains a type expression, the doclet's `type` object now includes a hidden
`parsedType` property. The `parsedType` property contains a syntax tree that represents the type
expression. The syntax tree is generated by [Catharsis](https://github.com/hegemonic/catharsis), and
its format may change in the future. (#576)
+ JSDoc now allows output filenames to contain non-ASCII characters. In addition, links to output
files are now URL-encoded when necessary. (#677)
+ JSDoc now ensures that output filenames do not have a leading underscore. (#758)
+ JSDoc now tries to ensure that `id` attributes in output files are unique within that file. (#539)
+ JSDoc now has an up-to-date JSON Schema file for parse results. The schema file is available in
`lib/jsdoc/schema.js`. (#327)
+ JSDoc now extracts more information from package files. (#710)
+ JSDoc now displays usage information if you run JSDoc without any input files, or with an
unrecognized command-line option. (#609, #840)
### Bug fixes
+ When the `allowUnknownTags` option is set to `false`, JSDoc no longer logs warnings about the
presence of `@also` and `@scope` tags. (#574)
+ Fixed several errors when parsing type expressions. (#619, #644, #652, #705, #767)
+ Properties added to the `module.exports` object can now be documented. (#500)
+ When a symbol's name starts with the same characters as its parent namespace (such as `Vector` in
`V.Vector`), JSDoc now assigns the correct longname to the symbol. (#608)
+ If a child class inherits from multiple parent classes, and the parent classes have instance
members with the same name, the child class no longer displays the documentation from both parent
classes. (#613)
+ If a source file contains an object literal, and one of the property names is a whitespace
character or a character that must be escaped in a regular expression, JSDoc now parses the file
successfully. (#549, #775)
+ Virtual comments now work correctly for overloaded functions. (#727)
+ When a virtual comment appears within a module, JSDoc now assigns the correct values to the
virtual comment doclet's `memberof`, `longname`, and `scope` properties. (#631)
+ JSDoc now sets the `scope` property to `global` for all global doclets. (#684)
+ Module doclets no longer have a `scope` property. (#782)
+ In Markdown tutorials, JSDoc no longer unescapes HTML entities. (#743)
+ If a longname includes a variation (for example, `Foo#bar(variation)`), the link text is now
preserved when generating HTML links. (#857)
+ When a single JSDoc comment includes `@class`, `@classdesc`, and `@constructor` tags, JSDoc no
longer ignores the value of the `@classdesc` tag. (#806)
+ For tags where the name and type are both optional (`@constant`, `@external`, `@member`,
`@module`, `@namespace`, and `@param`), JSDoc now parses the tag correctly when it includes a type
but not a name. (#351, #535)
+ The `@default` tag now works correctly when used with an array literal. (#604)
+ The `@enum` tag now works correctly when the enumeration is part of a chain of assignments (for
example, `var FOO = exports.FOO = {/* enumerated values */}`). (#702)
+ The `@exports` and `@module` tags now work correctly when their value includes a `module:`
namespace (for example, `@exports module:foo`). (#786)
+ The `@memberof` tag now works correctly when it refers to a module that is defined in a separate
file. (#880)
+ The `@variation` tag now works correctly when its value is enclosed in parentheses (for example,
`@variation (foo)`). (#850)
### Plugins
+ Tag definitions can now have a `mustNotHaveDescription` property. When this property is set to
`true`, JSDoc will warn the user if the tag text includes a description (such as `The description`
in `@param {string} foo - The description`). (#615)
+ Tag definitions can now call the method `dictionary.normalize`, which is a synonym for
`dictionary.normalise`. (#884)
+ The Markdown plugin no longer prevents inline `{@link}` tags from working. (#518)
+ The Markdown plugin now converts `@author` and `@throws` tag values to HTML by default. (#736,
#878)
+ JSDoc now includes a `summarize` plugin that automatically generates summaries based on the
description. (#485)
+ JSDoc now includes an `underscore` plugin that finds symbols whose names begin with an underscore
and automatically tags them as `@private`. (#471)
+ Plugins can now replace the `doclet` property of `newDoclet` events. (#584)
### Template improvements
+ You can now override the default template's main layout file, `layout.tmpl`, by setting the
`templates.default.layoutFile` option in JSDoc's configuration file. The property can contain a
relative or absolute path to the replacement for `layout.tmpl`. Relative paths are resolved against
the path to the configuration file; the current working directory; and the JSDoc directory, in that
order. (#480)
+ When the `templates.default.outputSourceFiles` option is set to `false`, the documentation no
longer shows the path to each source file. (#571)
+ You can now use the property `templates.default.staticFiles.include` to list files that will be
copied to the output directory. For backwards compatibility, the property
`templates.default.staticFiles.paths` is also supported but is deprecated. (#785)
+ The property `templates.default.staticFiles.include` now works correctly when an absolute path is
specified. (#939)
+ The `templates.default.staticFiles` options now work correctly on Windows. (#785)
+ In output files, you can now prevent the date from appearing in the footer by setting the property
`templates.default.includeDate` to `false`. (#910)
+ Output files no longer show the default value for members of an enumeration. (#689)
+ In certain types of AMD modules, the module-overview section is no longer duplicated. (#853)
+ If a constructor is assigned to `module.exports`, the value of the `@classdesc` tag now appears in
the documentation. (#740)
+ If a constructor is assigned to `module.exports`, and the constructor inherits from another class,
the parent class is now listed in the documentation. (#594)
+ Text within an `@example` tag, including HTML tags, is now properly escaped. (#511)
+ If a member has a `@fires` tag, the tag information now appears in the documentation. (#568)
+ If a symbol has members that use the `@mixin` tag, the mixins are now listed in the documentation.
(#379, #602)
+ When multiple `@param` tags are used to document properties of array values (for example,
`@param {Object[]} foo` and `@param {string} foo[].bar`), the properties are now grouped into the
appropriate row of the parameters table. (#870)
+ If a member has a `@requires` tag, the tag information now appears in the documentation. (#563)
+ Type expressions are now presented more clearly. (#618)
+ Pretty-printed source files now include line numbers. (#532)
+ When you run JSDoc with a single input file, the full path to the file no longer appears in the
documentation. (#553)
+ When an overloaded function is assigned to `module.exports`, the documentation now displays all of
the signatures for the overloaded function. (#727)
+ Resolved several issues that caused the default template to generate invalid HTML. (#843)
## 3.2.2 (November 2013)
### Bug fixes
+ Addressed a regression in JSDoc 3.2.1 that could prevent a function declaration from shadowing a
declaration with the same name in an outer scope. (#513)
+ If a child class overrides a method in a parent class without documenting the overridden method,
the method's documentation is now copied from the parent class. (#503)
+ You can now use inline HTML tags in Markdown-formatted text. In addition, JSDoc now uses only the
[marked Markdown parser](https://github.com/chjj/marked); the markdown-js parser has been removed.
(#510)
+ Type expressions can now include a much broader range of repeatable types. In addition, you can
now use Closure Compiler's nullable and non-nullable modifiers with repeatable types. For example,
the type expression `...!string` (a repeatable, non-nullable string) is now parsed correctly. (#502)
+ If a function accepts a parameter named `prototype`, the parameter is no longer renamed during
parsing. (#505)
+ If the list of input files includes relative paths, the paths are now resolved relative to the
user's working directory. (a3d33842)
## 3.2.1 (October 2013)
### Enhancements
+ JSDoc's parser now fires a `processingComplete` event after JSDoc has completed all
post-processing of the parse results. This event has a `doclets` property containing an array of
doclets. (#421)
+ When JSDoc's parser fires a `parseComplete` event, the event now includes a `doclets` property
containing an array of doclets. (#431)
+ You can now use relative paths in the JSDoc configuration file's `source.exclude` option. Relative
paths will be resolved relative to the current working directory. (#405)
+ If a symbol uses the `@default` tag, and its default value is an object literal, this value is now
stored as a string, and the doclet will have a `defaultvaluetype` property containing the string
`object`. This change enables templates to show the default value with appropriate syntax
highlighting. (#419)
+ Inline `{@link}` tags can now contain newlines. (#441)
### Bug fixes
+ Inherited symbols now indicate that they were inherited from the ancestor that defined the symbol,
rather than the direct parent. (#422)
+ If the first line of a JavaScript file contains a hashbang (for example, `#!/usr/bin/env node`),
the hashbang is now ignored when the file is parsed. (#499)
+ Resolved a crash when a JavaScript file contains a [JavaScript
1.8](https://developer.mozilla.org/en-US/docs/Web/JavaScript/New_in_JavaScript/1.8) keyword, such as
`let`. (#477)
+ The type expression `function[]` is now parsed correctly. (#493)
+ If a module is tagged incorrectly, the module's output file now has a valid filename. (#440, #458)
+ For tags that accept names, such as `@module` and `@param`, if a hyphen is used to separate the
name and description, the hyphen must appear on the same line as the name. This change prevents a
Markdown bullet on the followng line from being interpreted as a separator. (#459)
+ When lenient mode is enabled, a `@param` tag with an invalid type expression no longer causes a
crash. (#448)
+ The `@requires` tag can now contain an inline tag in its tag text. (#486)
+ The `@returns` tag can now contain inline tags even if a type is not specified. (#444)
+ When lenient mode is enabled, a `@returns` tag with no value no longer causes a crash. (#451)
+ The `@type` tag now works correctly with type expressions that span multiple lines. (#427)
+ If a string contains inline `{@link}` tags preceded by bracketed link text (for example,
`[test]{@link Test#test}`), HTML links are now generated correctly even if the string contains other
bracketed text. (#470)
+ On POSIX systems, if you run JSDoc using a symlink to the startup script, JSDoc now works
correctly. (#492)
### Default template
+ Pretty-printed source files are now generated by default. To disable this feature, add the
property `templates.default.outputSourceFiles: false` to your `conf.json` file. (#454)
+ Links to a specific line in a source file now work correctly. (#475)
+ Pretty-printed source files are now generated using the encoding specified in the `-e/--encoding`
option. (#496)
+ If a `@default` tag is added to a symbol whose default value is an object, the value is now
displayed in the output file. (#419)
+ Output files now identify symbols as "abstract" rather than "virtual." (#432)
## 3.2.0 (May 2013)
### Major changes
+ JSDoc can now parse any valid [Google Closure Compiler type
expression](https://developers.google.com/closure/compiler/docs/js-for-compiler#types). **Note**: As
a result of this change, JSDoc quits if a file contains an invalid type expression. To prevent JSDoc
from quitting, run JSDoc with the `--lenient` (`-l`) command-line option. (Multiple issues)
+ You can now use the new `@listens` tag to indicate that a symbol listens for an event. (#273)
### Enhancements
+ The parser now fires a `parseBegin` event before it starts parsing files, as well as a
`parseComplete` event after all files have been parsed. Plugins can define event handlers for these
events, and `parseBegin` handlers can modify the list of files to parse. (#299)
+ Event handlers for `jsdocCommentFound` events can now modify the JSDoc comment. (#228)
+ You can now exclude tags from Markdown processing using the new option `markdown.excludeTags` in
the configuration file. (#337)
+ You can now use the [marked](https://github.com/chjj/marked) Markdown parser by setting the
configuration property `markdown.parser` to `marked`. In addition, if `markdown.parser` is set to
`gfm`, JSDoc will now use the "marked" parser instead. (#385)
+ The `@typedef` tag no longer requires a name when used with a Closure Compiler-style type
definition. For example, the following type definition will automatically get the name `Foo.Bar`:
```javascript
/** @typedef {string} */
Foo.Bar;
```
(#391)
+ You can now use an inline `{@type}` tag in a parameter's description. If this tag is present,
JSDoc will assume that the parameter uses the type specified in the inline `{@type}` tag. For
example, the following `@param` tag would cause `myParam`'s type to be documented as `Foo`:
```
@param {(boolean|string)} myParam - My special parameter. {@type Foo}
```
(#152)
+ The `console.log` function now behaves the same way as on Node.js. In addition, the functions
`console.info`, `console.error`, `console.warn`, and `console.trace` have been implemented. (#298)
+ You can now use npm to install JSDoc globally by running `npm install -g`. **Note**: JSDoc will
still run under Mozilla Rhino, not Node.js. (#374)
+ The `jsVersion` configuration property has been removed. (#390)
### Bug fixes
+ JSDoc now quits if the configuration file cannot be loaded. (#407)
+ JSDoc's `--explain` (`-X`) option now runs much more quickly, and it outputs valid JSON to the
console. (#298)
+ JSDoc's `--lenient` (`-l`) option now prints warnings on STDERR rather than STDOUT. (#298)
+ The parser now assigns the correct scope to object properties whose names include single quotes.
(#386)
+ The parser now recognizes CommonJS modules that export a single function rather than an object.
(#384)
+ The inline `{@link}` tag now works correctly when `@link` is followed by a tab. (#359)
+ On POSIX systems, quoted command-line arguments are no longer split on spaces. (#397)
### Plugins
+ The new `overloadHelper` plugin makes it easier to link to overloaded methods. (#179)
+ The `markdown` plugin now converts Markdown links in the `@see` tag. (#297)
### Default template enhancements
+ You can now use the configuration property `templates.default.staticFiles` to copy additional
static files to the output directory. (#393)
+ All output files now use human-readable filenames. (#339)
+ The documentation for events now lists the symbols that listen to that event. (#273)
+ Links to source files now allow you to jump to the line where a symbol is defined. (#316)
+ The output files now link to individual types within a Closure Compiler type expression. (Multiple
issues)
+ CommonJS modules that export a single function, rather than an object, are now documented more
clearly. (#384)
+ Functions that can throw multiple types of errors are now documented more clearly. (#389)
+ If a `@property` tag does not identify the property's name, the template no longer throws an
error. (#373)
+ The type of each `@typedef` is now displayed. (#391)
+ If a `@see` tag contains a URL (for example, `@see http://example.com` or
`@see <http://example.com>`), the tag text is now converted to a link. (#371)
+ Repeatable parameters are now identified. (#381)
+ The "Classes" header is no longer repeated in the navigation bar. (#361)
+ When the only documented symbols in global scope are type definitions, you can now click the
"Global" header to view their documentation. (#261)
## 3.1.1 (February 2013)
+ Resolved a crash when no input files contain JSDoc comments. (#329)
+ Resolved a crash when JSDoc cannot identify the common prefix of several paths. (#330)
+ Resolved a crash when the full path to JSDoc contained at least one space. (#347)
+ Files named `README.md` or `package.json` will now be processed when they are specified on the
command line. (#350)
+ You can now use `@emits` as a synonym for `@fires`. (#324)
+ The module `jsdoc/util/templateHelper` now allows you to specify the CSS class for links that are
generated by the following methods: (#331)
+ `getAncestorLinks`
+ `getSignatureReturns`
+ `getSignatureTypes`
+ `linkto`
## 3.1.0 (January 2013)
### Major changes
+ You can now use the new `@callback` tag to provide information about a callback function's
signature. To document a callback function, create a standalone JSDoc comment, as shown in the
following example:
```javascript
/**
* @class
*/
function MyClass() {}
/**
* Send a request.
*
* @param {MyClass~responseCb} cb - Called after a response is received.
*/
MyClass.prototype.sendRequest = function(cb) {
// code
};
/**
* Callback for sending a request.
*
* @callback MyClass~responseCb
* @param {?string} error - Information about the error.
* @param {?string} response - Body of the response.
*/
```
+ The inline link tag, `{@link}`, has been improved:
+ You can now use a space as the delimiter between the link target and link text.
+ In your `conf.json` file, you can now enable the option `templates.cleverLinks` to display
code links in a monospace font and URL links in plain text. You can also enable the option
`templates.monospaceLinks` to display all links in a monospace font. **Note**: JSDoc templates
must be updated to respect these options.
+ You can now use the new inline tags `{@linkplain}`, which forces a plain-text link, and
`{@linkcode}`, which forces a monospace link. These tags always override the settings in your
`conf.json` file. (#250)
+ JSDoc now provides a `-l/--lenient` option that tells JSDoc to continue running if it encounters a
non-fatal error. (Multiple issues)
+ A template's `publish.js` file should now assign its `publish` function to `exports.publish`,
rather than defining a global `publish` function. The global `publish` function is deprecated and
may not be supported in future versions. JSDoc's built-in templates reflect this change. (#166)
+ The template helper (`templateHelper.js`) exports a variety of new functions for finding
information within a parse tree. These functions were previously contained within the default
template. (#186)
+ Updated the `fs` and `path` modules to make their behavior more consistent with Node.js. In
addition, created extended versions of these modules with additional functionality. (Multiple
commits)
+ Updated or replaced numerous third-party modules. (Multiple commits)
+ Reorganized the JSDoc codebase in preparation for future enhancements. (Multiple commits)
+ JSDoc now embeds a version of Mozilla Rhino that recognizes Node.js packages, including
`package.json` files. (Multiple commits)
+ Node.js' `npm` utility can now install JSDoc from its GitHub repository. **Note**: JSDoc is not
currently compatible with Node.js. However, this change allows JSDoc to be installed as a dependency
of a Node.js project. In this version, global installation with `npm` is not supported. (Multiple
commits)
### Enhancements
+ If a `README.md` file is passed to JSDoc, its contents will be included on the `index.html` page
of the generated documentation. (#128)
+ The `@augments` tag can now refer to an undocumented member, such as `window.XMLHTTPRequest`.
(#160)
+ The `@extends` tag can now refer to an undocumented member, such as `window.XMLHttpRequest`. In
addition, you can now use `@host` as a synonym for `@extends`. (#145)
+ The `@lends` tag is now supported in multiline JSDoc comments. (#163)
+ On Windows, `jsdoc.cmd` now provides the same options as the `jsdoc` shell script. (#127)
+ JSDoc now provides `setTimeout()`, `clearTimeout()`, `setInterval()`, and `clearInterval()`
functions. (Multiple commits)
+ JSDoc no longer provides a global `exit()` function. Use `process.exit()` instead. (1228a8f7)
+ JSDoc now includes additional shims for Node.js' built-in modules. **Note**: Many of these shims
implement only the functions that JSDoc uses, and they may not be consistent with Node.js' behavior
in edge cases. (Multiple commits)
+ JSDoc now provides a `-v/--version` option to display information about the current version.
(#303)
+ When running tests, you can now use the `--nocolor` option to disable colored output. On Windows,
colored output is always disabled. (e17601fe, 8bc33541)
### Bug fixes
+ When using the `@event` tag to define an event within a class or namespace, the event's longname
is now set correctly regardless of tag order. (#280)
+ The `@property` tag no longer results in malformed parse trees. (20f87094)
+ The `jsdoc` and `jsdoc.cmd` scripts now work correctly with paths that include spaces. (#127,
#130)
+ The `jsdoc` script now works correctly on Cygwin and MinGW, and with the `dash` shell. (#182,
#184, #187)
+ The `-d/--destination` option is no longer treated as a path relative to the JSDoc directory.
Instead, it can contain an absolute path, or a path relative to the current working directory.
(f5e3f0f3)
+ JSDoc now provides default options for the values in `conf.json`. (#129)
+ If the `conf.json` file does not exist, JSDoc no longer tries to create it, which prevents errors
if the current user does not have write access to the JSDoc directory. (d2d05fcb)
+ Doclets for getters and setters are now parsed appropriately. (#150)
+ Only the first asterisk is removed from each line of a JSDoc comment. (#172)
+ If a child member overrides an ancestor member, the ancestor member is no longer documented.
(#158)
+ If a member of a namespace has the same name as a namespace, the member is now documented
correctly. (#214)
+ The parse tree now uses a single set of properties to track both JSDoc-style type information and
Closure Compiler-style type information. (#118)
+ If a type has a leading `!`, indicating that it is non-nullable, the leading `!` is now removed
from the type name. (#226)
+ When Markdown formatting is enabled, underscores in inline `{@link}` tags are no longer treated as
Markdown formatting characters. (#259)
+ Markdown links now work correctly when a JavaScript reserved word, such as `constructor`, is used
as the link text. (#249)
+ Markdown files for tutorials are now parsed based on the settings in `conf.json`, rather than
using the "evilstreak" Markdown parser in all cases. (#220)
+ If a folder contains both tutorial source files and `.js` files, JSDoc no longer attempts to parse
the `.js` files as JSON files. (#222)
+ The "evilstreak" Markdown parser now works correctly with files that use Windows-style line
endings. (#223)
+ JSDoc no longer fails unit tests when the `conf.json` file is not present. (#206)
+ On Windows, JSDoc now passes all unit tests. (Multiple commits)
### Plugins
+ The new `partial` plugin adds support for a `@partial` tag, which links to an external file that
contains JSDoc comments. (#156)
+ The new `commentsOnly` plugin removes everything in a file except JSDoc-style comments. You can
use this plugin to document source files that are not valid JavaScript, including source files for
other languages. (#304)
+ The new `eventDumper` plugin logs information about parser events to the console. (#242)
+ The new `verbose` plugin logs the name of each input file to the console. (#157)
### Template enhancements
#### Default template
+ The template output now includes pretty-printed versions of source files. This feature is enabled
by default. To disable this feature, add the property `templates.default.outputSourceFiles: false`
to your `conf.json` file. (#208)
+ You can now use the template if it is placed outside of the JSDoc directory. (#198)
+ The template no longer throws an error when a parameter does not have a name. (#175)
+ The navigation bar now includes an "Events" section if any events are documented. (#280)
+ Pages no longer include a "Classes" header when no classes are documented. (eb0186b9)
+ Member details now include "Inherited From" section when a member is inherited from another
member. (#154)
+ If an `@author` tag contains text in the format "Jane Doe <jdoe@example.com>", the value is now
converted to an HTML `mailto:` link. (#326)
+ Headings for functions now include the function's signature. (#253)
+ Type information is now displayed for events. (#192)
+ Functions now link to their return type when appropriate. (#192)
+ Type definitions that contain functions are now displayed correctly. (#292)
+ Tutorial output is now generated correctly. (#188)
+ Output files now use Google Code Prettify with the Tomorrow theme as a syntax highlighter. (#193)
+ The `index.html` output file is no longer overwritten if a namespace called `index` has been
documented. (#244)
+ The current JSDoc version number is now displayed in the footer. (#321)
#### Haruki template
+ Members are now contained in arrays rather than objects, allowing overloaded members to be
documented. (#153)
+ A clearer error message is now provided when the output destination is not specified correctly.
(#174)
## 3.0.1 (June 2012)
### Enhancements
+ The `conf.json` file may now contain `source.include` and `source.exclude` properties. (#56)
+ `source.include` specifies files or directories that JSDoc should _always_ check for
documentation.
+ `source.exclude` specifies files or directories that JSDoc should _never_ check for
documentation.
These settings take precedence over the `source.includePattern` and `source.excludePattern`
properties, which contain regular expressions that JSDoc uses to search for source files.
+ The `-t/--template` option may now specify the absolute path to a template. (#122)
### Bug fixes
+ JSDoc no longer throws exceptions when a symbol has a special name, such as `hasOwnProperty`.
(1ef37251)
+ The `@alias` tag now works correctly when documenting inner classes as globals. (810dd7f7)
### Template improvements
+ The default template now sorts classes by name correctly when the classes come from several
modules. (4ce17195)
+ The Haruki template now correctly supports `@example`, `@members`, and `@returns` tags. (6580e176,
59655252, 31c8554d)
## 3.0.0 (May 2012)
Initial release.
[605]: https://github.com/jsdoc/jsdoc/issues/605
[738]: https://github.com/jsdoc/jsdoc/issues/738
[970]: https://github.com/jsdoc/jsdoc/issues/970
[1129]: https://github.com/jsdoc/jsdoc/issues/1129
[1361]: https://github.com/jsdoc/jsdoc/issues/1361
[1410]: https://github.com/jsdoc/jsdoc/issues/1410
[1412]: https://github.com/jsdoc/jsdoc/issues/1412
[1643]: https://github.com/jsdoc/jsdoc/issues/1643
[1644]: https://github.com/jsdoc/jsdoc/issues/1644
+102
View File
@@ -0,0 +1,102 @@
# License
JSDoc is free software, licensed under the Apache License, Version 2.0 (the
"License"). Commercial and non-commercial use are permitted in compliance with
the License.
Copyright (c) 2011-present Michael Mathews <micmath@gmail.com> and the
[contributors to JSDoc](https://github.com/jsdoc/jsdoc/graphs/contributors). All
rights reserved.
You can obtain a copy of the License at:
https://www.apache.org/licenses/LICENSE-2.0
In addition, a copy of the License is included with this distribution.
As stated in Section 7, "Disclaimer of Warranty," of the License:
> Licensor provides the Work (and each Contributor provides its Contributions)
> on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
> express or implied, including, without limitation, any warranties or
> conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
> PARTICULAR PURPOSE. You are solely responsible for determining the
> appropriateness of using or redistributing the Work and assume any risks
> associated with Your exercise of permissions under this License.
The source code for JSDoc is available at: https://github.com/jsdoc/jsdoc
# Third-party software
JSDoc includes the following third-party software, either in whole or in part.
Each third-party software package is provided under its own license.
## MIT License
Several of the following software packages are distributed under the MIT
license, which is reproduced below:
> 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.
## Google Code Prettify
Google Code Prettify is distributed under the Apache License 2.0, which is
included with this package.
Copyright (c) 2006 Google Inc.
The source code for Google Code Prettify is available at:
https://code.google.com/p/google-code-prettify/
## Jasmine
Jasmine is distributed under the MIT license, which is reproduced above.
Copyright (c) 2008-2011 Pivotal Labs.
The source code for Jasmine is available at:
https://github.com/pivotal/jasmine
## jasmine-node
jasmine-node is distributed under the MIT license, which is reproduced above.
Copyright (c) 2010 Adam Abrons and Misko Hevery (http://getangular.com).
The source code for jasmine-node is available at:
https://github.com/mhevery/jasmine-node
## Open Sans
Open Sans is distributed under the Apache License 2.0, which is
included with this package.
Copyright (c) 2010-2011, Google Inc.
This typeface, including the complete set of variations, are available at:
https://fonts.google.com/specimen/Open+Sans
## Tomorrow theme for Google Code Prettify
The Tomorrow theme for Google Code Prettify is distributed under the MIT
license, which is reproduced above.
Copyright (c) 2016 Yoshihide Jimbo.
The source code for the Tomorrow theme is available at:
https://github.com/jmblog/color-themes-for-google-code-prettify
+93
View File
@@ -0,0 +1,93 @@
# JSDoc
[![Build Status](https://travis-ci.org/jsdoc/jsdoc.svg?branch=master)](http://travis-ci.org/jsdoc/jsdoc)
An API documentation generator for JavaScript.
Want to contribute to JSDoc? Please read `CONTRIBUTING.md`.
Installation and Usage
----------------------
JSDoc supports stable versions of Node.js 12.0.0 and later. You can install
JSDoc globally or in your project's `node_modules` folder.
To install the latest version on npm globally (might require `sudo`;
[learn how to fix this](https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally)):
npm install -g jsdoc
To install the latest version on npm locally and save it in your package's
`package.json` file:
npm install --save-dev jsdoc
To install the latest development version locally, without updating your
project's `package.json` file:
npm install git+https://github.com/jsdoc/jsdoc.git
If you installed JSDoc locally, the JSDoc command-line tool is available in
`./node_modules/.bin`. To generate documentation for the file
`yourJavaScriptFile.js`:
./node_modules/.bin/jsdoc yourJavaScriptFile.js
If you installed JSDoc globally, run the `jsdoc` command:
jsdoc yourJavaScriptFile.js
By default, the generated documentation is saved in a directory named `out`. You
can use the `--destination` (`-d`) option to specify another directory.
Run `jsdoc --help` for a complete list of command-line options.
## Templates and tools
The JSDoc community has created templates and other tools to help you generate
and customize your documentation. Here are a few of them:
### Templates
+ [jaguarjs-jsdoc](https://github.com/davidshimjs/jaguarjs-jsdoc)
+ [DocStrap](https://github.com/docstrap/docstrap)
([example](https://docstrap.github.io/docstrap))
+ [jsdoc3Template](https://github.com/DBCDK/jsdoc3Template)
([example](https://github.com/danyg/jsdoc3Template/wiki#wiki-screenshots))
+ [minami](https://github.com/Nijikokun/minami)
+ [docdash](https://github.com/clenemt/docdash)
([example](http://clenemt.github.io/docdash/))
+ [tui-jsdoc-template](https://github.com/nhnent/tui.jsdoc-template)
([example](https://nhnent.github.io/tui.jsdoc-template/latest/))
+ [better-docs](https://github.com/SoftwareBrothers/better-docs)
([example](https://softwarebrothers.github.io/admin-bro-dev/index.html))
### Build tools
+ [JSDoc Grunt plugin](https://github.com/krampstudio/grunt-jsdoc)
+ [JSDoc Gulp plugin](https://github.com/mlucool/gulp-jsdoc3)
### Other tools
+ [jsdoc-to-markdown](https://github.com/jsdoc2md/jsdoc-to-markdown)
+ [Integrating GitBook with
JSDoc](https://medium.com/@kevinast/integrate-gitbook-jsdoc-974be8df6fb3)
## For more information
+ Documentation is available at [jsdoc.app](https://jsdoc.app/).
+ Contribute to the docs at
[jsdoc/jsdoc.github.io](https://github.com/jsdoc/jsdoc.github.io).
+ [Join JSDoc's Slack channel](https://jsdoc-slack.appspot.com/).
+ Ask for help on the
[JSDoc Users mailing list](http://groups.google.com/group/jsdoc-users).
+ Post questions tagged `jsdoc` to
[Stack Overflow](http://stackoverflow.com/questions/tagged/jsdoc).
## License
JSDoc is copyright (c) 2011-present Michael Mathews <micmath@gmail.com> and the
[contributors to JSDoc](https://github.com/jsdoc/jsdoc/graphs/contributors).
JSDoc is free software, licensed under the Apache License, Version 2.0. See the
file `LICENSE.md` in this distribution for more details.
+465
View File
@@ -0,0 +1,465 @@
/* eslint-disable indent, no-process-exit */
/**
* Helper methods for running JSDoc on the command line.
*
* A few critical notes for anyone who works on this module:
*
* + The module should really export an instance of `cli`, and `props` should be properties of a
* `cli` instance.
*
* @private
*/
module.exports = (() => {
const app = require('jsdoc/app');
const env = require('jsdoc/env');
const logger = require('jsdoc/util/logger');
const stripBom = require('jsdoc/util/stripbom');
const stripJsonComments = require('strip-json-comments');
const Promise = require('bluebird');
const props = {
docs: [],
packageJson: null,
shouldExitWithError: false,
tmpdir: null
};
const FATAL_ERROR_MESSAGE = 'Exiting JSDoc because an error occurred. See the previous log ' +
'messages for details.';
const cli = {};
// TODO: docs
cli.setVersionInfo = () => {
const fs = require('fs');
const path = require('path');
// allow this to throw--something is really wrong if we can't read our own package file
const info = JSON.parse( stripBom.strip(fs.readFileSync(path.join(env.dirname, 'package.json'),
'utf8')) );
env.version = {
number: info.version,
revision: new Date( parseInt(info.revision, 10) ).toUTCString()
};
return cli;
};
// TODO: docs
cli.loadConfig = () => {
const _ = require('underscore');
const args = require('jsdoc/opts/args');
const Config = require('jsdoc/config');
let config;
const fs = require('jsdoc/fs');
const path = require('jsdoc/path');
let confPath;
let isFile;
const defaultOpts = {
destination: './out/',
encoding: 'utf8'
};
try {
env.opts = args.parse(env.args);
}
catch (e) {
console.error(`${e.message}\n`);
cli.printHelp().then(() => {
cli.exit(1);
});
}
confPath = env.opts.configure || path.join(env.dirname, 'conf.json');
try {
isFile = fs.statSync(confPath).isFile();
}
catch (e) {
isFile = false;
}
if ( !isFile && !env.opts.configure ) {
confPath = path.join(env.dirname, 'conf.json.EXAMPLE');
}
try {
switch ( path.extname(confPath) ) {
case '.js':
config = require( path.resolve(confPath) ) || {};
break;
case '.json':
case '.EXAMPLE':
default:
config = fs.readFileSync(confPath, 'utf8');
break;
}
env.conf = new Config(config).get();
}
catch (e) {
cli.exit(1, `Cannot parse the config file ${confPath}: ${e}\n${FATAL_ERROR_MESSAGE}`);
}
// look for options on the command line, in the config file, and in the defaults, in that order
env.opts = _.defaults(env.opts, env.conf.opts, defaultOpts);
return cli;
};
// TODO: docs
cli.configureLogger = () => {
function recoverableError() {
props.shouldExitWithError = true;
}
function fatalError() {
cli.exit(1);
}
if (env.opts.debug) {
logger.setLevel(logger.LEVELS.DEBUG);
}
else if (env.opts.verbose) {
logger.setLevel(logger.LEVELS.INFO);
}
if (env.opts.pedantic) {
logger.once('logger:warn', recoverableError);
logger.once('logger:error', fatalError);
}
else {
logger.once('logger:error', recoverableError);
}
logger.once('logger:fatal', fatalError);
return cli;
};
// TODO: docs
cli.logStart = () => {
logger.debug( cli.getVersion() );
logger.debug('Environment info: %j', {
env: {
conf: env.conf,
opts: env.opts
}
});
};
// TODO: docs
cli.logFinish = () => {
let delta;
let deltaSeconds;
if (env.run.finish && env.run.start) {
delta = env.run.finish.getTime() - env.run.start.getTime();
}
if (delta !== undefined) {
deltaSeconds = (delta / 1000).toFixed(2);
logger.info('Finished running in %s seconds.', deltaSeconds);
}
};
// TODO: docs
cli.runCommand = cb => {
let cmd;
const opts = env.opts;
if (opts.help) {
cmd = cli.printHelp;
}
else if (opts.test) {
cmd = cli.runTests;
}
else if (opts.version) {
cmd = cli.printVersion;
}
else {
cmd = cli.main;
}
cmd().then(errorCode => {
if (!errorCode && props.shouldExitWithError) {
errorCode = 1;
}
cb(errorCode);
});
};
// TODO: docs
cli.printHelp = () => {
cli.printVersion();
console.log( `\n${require('jsdoc/opts/args').help()}\n` );
console.log('Visit https://jsdoc.app/ for more information.');
return Promise.resolve(0);
};
// TODO: docs
cli.runTests = () => {
const path = require('jsdoc/path');
const runner = Promise.promisify(require( path.join(env.dirname, 'test/runner') ));
console.log('Running tests...');
return runner();
};
// TODO: docs
cli.getVersion = () => `JSDoc ${env.version.number} (${env.version.revision})`;
// TODO: docs
cli.printVersion = () => {
console.log( cli.getVersion() );
return Promise.resolve(0);
};
// TODO: docs
cli.main = () => {
cli.scanFiles();
if (env.sourceFiles.length === 0) {
console.log('There are no input files to process.');
return Promise.resolve(0);
} else {
return cli.createParser()
.parseFiles()
.processParseResults()
.then(() => {
env.run.finish = new Date();
return 0;
});
}
};
function readPackageJson(filepath) {
const fs = require('jsdoc/fs');
try {
return stripJsonComments( fs.readFileSync(filepath, 'utf8') );
}
catch (e) {
logger.error('Unable to read the package file "%s"', filepath);
return null;
}
}
function buildSourceList() {
const Readme = require('jsdoc/readme');
let packageJson;
let readmeHtml;
let sourceFile;
let sourceFiles = env.opts._ ? env.opts._.slice(0) : [];
if (env.conf.source && env.conf.source.include) {
sourceFiles = sourceFiles.concat(env.conf.source.include);
}
// load the user-specified package/README files, if any
if (env.opts.package) {
packageJson = readPackageJson(env.opts.package);
}
if (env.opts.readme) {
readmeHtml = new Readme(env.opts.readme).html;
}
// source files named `package.json` or `README.md` get special treatment, unless the user
// explicitly specified a package and/or README file
for (let i = 0, l = sourceFiles.length; i < l; i++) {
sourceFile = sourceFiles[i];
if ( !env.opts.package && /\bpackage\.json$/i.test(sourceFile) ) {
packageJson = readPackageJson(sourceFile);
sourceFiles.splice(i--, 1);
}
if ( !env.opts.readme && /(\bREADME|\.md)$/i.test(sourceFile) ) {
readmeHtml = new Readme(sourceFile).html;
sourceFiles.splice(i--, 1);
}
}
props.packageJson = packageJson;
env.opts.readme = readmeHtml;
return sourceFiles;
}
// TODO: docs
cli.scanFiles = () => {
const Filter = require('jsdoc/src/filter').Filter;
let filter;
env.opts._ = buildSourceList();
// are there any files to scan and parse?
if (env.conf.source && env.opts._.length) {
filter = new Filter(env.conf.source);
env.sourceFiles = app.jsdoc.scanner.scan(env.opts._,
(env.opts.recurse ? env.conf.recurseDepth : undefined), filter);
}
return cli;
};
function resolvePluginPaths(paths) {
const path = require('jsdoc/path');
const pluginPaths = [];
paths.forEach(plugin => {
const basename = path.basename(plugin);
const dirname = path.dirname(plugin);
const pluginPath = path.getResourcePath(dirname, basename);
if (!pluginPath) {
logger.error('Unable to find the plugin "%s"', plugin);
return;
}
pluginPaths.push( pluginPath );
});
return pluginPaths;
}
cli.createParser = () => {
const handlers = require('jsdoc/src/handlers');
const parser = require('jsdoc/src/parser');
const plugins = require('jsdoc/plugins');
app.jsdoc.parser = parser.createParser(env.conf.parser);
if (env.conf.plugins) {
env.conf.plugins = resolvePluginPaths(env.conf.plugins);
plugins.installPlugins(env.conf.plugins, app.jsdoc.parser);
}
handlers.attachTo(app.jsdoc.parser);
return cli;
};
cli.parseFiles = () => {
const augment = require('jsdoc/augment');
const borrow = require('jsdoc/borrow');
const Package = require('jsdoc/package').Package;
let docs;
let packageDocs;
props.docs = docs = app.jsdoc.parser.parse(env.sourceFiles, env.opts.encoding);
// If there is no package.json, just create an empty package
packageDocs = new Package(props.packageJson);
packageDocs.files = env.sourceFiles || [];
docs.push(packageDocs);
logger.debug('Adding inherited symbols, mixins, and interface implementations...');
augment.augmentAll(docs);
logger.debug('Adding borrowed doclets...');
borrow.resolveBorrows(docs);
logger.debug('Post-processing complete.');
app.jsdoc.parser.fireProcessingComplete(docs);
return cli;
};
cli.processParseResults = () => {
if (env.opts.explain) {
cli.dumpParseResults();
return Promise.resolve();
}
else {
cli.resolveTutorials();
return cli.generateDocs();
}
};
cli.dumpParseResults = () => {
console.log(require('jsdoc/util/dumper').dump(props.docs));
return cli;
};
cli.resolveTutorials = () => {
const resolver = require('jsdoc/tutorial/resolver');
if (env.opts.tutorials) {
resolver.load(env.opts.tutorials);
resolver.resolve();
}
return cli;
};
cli.generateDocs = () => {
const path = require('jsdoc/path');
const resolver = require('jsdoc/tutorial/resolver');
const { taffy } = require('@jsdoc/salty');
let template;
env.opts.template = (() => {
const publish = env.opts.template || 'templates/default';
const templatePath = path.getResourcePath(publish);
// if we didn't find the template, keep the user-specified value so the error message is
// useful
return templatePath || env.opts.template;
})();
try {
template = require(`${env.opts.template}/publish`);
}
catch (e) {
logger.fatal(`Unable to load template: ${e.message}` || e);
}
// templates should include a publish.js file that exports a "publish" function
if (template.publish && typeof template.publish === 'function') {
let publishPromise;
logger.info('Generating output files...');
publishPromise = template.publish(
taffy(props.docs),
env.opts,
resolver.root
);
return Promise.resolve(publishPromise);
}
else {
logger.fatal(`${env.opts.template} does not export a "publish" function. Global "publish" functions are no longer supported.`);
}
return Promise.resolve();
};
// TODO: docs
cli.exit = (exitCode, message) => {
if (exitCode > 0 && message) {
console.error(message);
}
process.on('exit', () => { process.exit(exitCode); });
};
return cli;
})();
+17
View File
@@ -0,0 +1,17 @@
{
"tags": {
"allowUnknownTags": true
},
"source": {
"includePattern": ".+\\.js(doc|x)?$",
"excludePattern": "(^|\\/|\\\\)_"
},
"plugins": [],
"templates": {
"cleverLinks": false,
"monospaceLinks": false,
"default": {
"outputSourceFiles": true
}
}
}
Generated Vendored Executable
+94
View File
@@ -0,0 +1,94 @@
#!/usr/bin/env node
// initialize the environment for Node.js
(() => {
const fs = require('fs');
const path = require('path');
let env;
let jsdocPath = __dirname;
const pwd = process.cwd();
// Create a custom require method that adds `lib/jsdoc` and `node_modules` to the module
// lookup path. This makes it possible to `require('jsdoc/foo')` from external templates and
// plugins, and within JSDoc itself. It also allows external templates and plugins to
// require JSDoc's module dependencies without installing them locally.
/* eslint-disable no-global-assign, no-redeclare */
require = require('requizzle')({
requirePaths: {
before: [path.join(__dirname, 'lib')],
after: [path.join(__dirname, 'node_modules')]
},
infect: true
});
/* eslint-enable no-global-assign, no-redeclare */
// resolve the path if it's a symlink
if ( fs.statSync(jsdocPath).isSymbolicLink() ) {
jsdocPath = path.resolve( path.dirname(jsdocPath), fs.readlinkSync(jsdocPath) );
}
env = require('./lib/jsdoc/env');
env.dirname = jsdocPath;
env.pwd = pwd;
env.args = process.argv.slice(2);
})();
/**
* Data about the environment in which JSDoc is running, including the configuration settings that
* were used to run JSDoc.
*
* @deprecated As of JSDoc 3.4.0. Use `require('jsdoc/env')` to access the `env` object. The global
* `env` object will be removed in a future release.
* @namespace
* @name env
*/
global.env = (() => require('./lib/jsdoc/env'))();
/**
* Data that must be shared across the entire application.
*
* @deprecated As of JSDoc 3.4.0. Avoid using the `app` object. The global `app` object and the
* `jsdoc/app` module will be removed in a future release.
* @namespace
* @name app
*/
global.app = (() => require('./lib/jsdoc/app'))();
(() => {
const env = global.env;
const cli = require('./cli');
function cb(errorCode) {
cli.logFinish();
cli.exit(errorCode || 0);
}
cli.setVersionInfo()
.loadConfig();
if (!env.opts.test) {
cli.configureLogger();
}
cli.logStart();
if (env.opts.debug) {
/**
* Recursively print an object's properties to stdout. This method is safe to use with
* objects that contain circular references.
*
* This method is available only when JSDoc is run with the `--debug` option.
*
* @global
* @name dump
* @private
* @param {...*} obj - Object(s) to print to stdout.
*/
global.dump = (...args) => {
console.log(require('./lib/jsdoc/util/dumper').dump(args));
};
}
cli.runCommand(cb);
})();
+19
View File
@@ -0,0 +1,19 @@
/**
* Objects that are shared across the entire application.
*
* @deprecated As of JSDoc 3.4.0. Do not use this module. It will be removed in a future release.
* @module jsdoc/app
*/
module.exports = {
/**
* Namespace for shared objects.
*
* @namespace
* @type {Object}
*/
jsdoc: {
name: require('jsdoc/name'),
parser: null,
scanner: new (require('jsdoc/src/scanner').Scanner)()
}
};
+608
View File
@@ -0,0 +1,608 @@
/**
* Provides methods for augmenting the parse results based on their content.
* @module jsdoc/augment
*/
const doop = require('jsdoc/util/doop');
const jsdoc = {
doclet: require('jsdoc/doclet')
};
const name = require('jsdoc/name');
const hasOwnProp = Object.prototype.hasOwnProperty;
function mapDependencies(index, propertyName) {
const dependencies = {};
let doc;
let doclets;
const kinds = ['class', 'external', 'interface', 'mixin'];
let len = 0;
Object.keys(index).forEach(indexName => {
doclets = index[indexName];
for (let i = 0, ii = doclets.length; i < ii; i++) {
doc = doclets[i];
if (kinds.includes(doc.kind)) {
dependencies[indexName] = {};
if (hasOwnProp.call(doc, propertyName)) {
len = doc[propertyName].length;
for (let j = 0; j < len; j++) {
dependencies[indexName][doc[propertyName][j]] = true;
}
}
}
}
});
return dependencies;
}
class Sorter {
constructor(dependencies) {
this.dependencies = dependencies;
this.visited = {};
this.sorted = [];
}
visit(key) {
if (!(key in this.visited)) {
this.visited[key] = true;
if (this.dependencies[key]) {
Object.keys(this.dependencies[key]).forEach(path => {
this.visit(path);
});
}
this.sorted.push(key);
}
}
sort() {
Object.keys(this.dependencies).forEach(key => {
this.visit(key);
});
return this.sorted;
}
}
function sort(dependencies) {
const sorter = new Sorter(dependencies);
return sorter.sort();
}
function getMembers(longname, {index}, scopes) {
const memberof = index.memberof[longname] || [];
const members = [];
memberof.forEach(candidate => {
if (scopes.includes(candidate.scope)) {
members.push(candidate);
}
});
return members;
}
function getDocumentedLongname(longname, {index}) {
const doclets = index.documented[longname] || [];
return doclets[doclets.length - 1];
}
function addDocletProperty(doclets, propName, value) {
for (let i = 0, l = doclets.length; i < l; i++) {
doclets[i][propName] = value;
}
}
function reparentDoclet({longname}, child) {
const parts = name.shorten(child.longname);
parts.memberof = longname;
child.memberof = longname;
child.longname = name.combine(parts);
}
function parentIsClass({kind}) {
return kind === 'class';
}
function staticToInstance(doclet) {
const parts = name.shorten(doclet.longname);
parts.scope = name.SCOPE.PUNC.INSTANCE;
doclet.longname = name.combine(parts);
doclet.scope = name.SCOPE.NAMES.INSTANCE;
}
/**
* Update the list of doclets to be added to another symbol.
*
* We add only one doclet per longname. For example: If `ClassA` inherits from two classes that both
* use the same method name, `ClassA` gets docs for one method rather than two.
*
* Also, the last symbol wins for any given longname. For example: If you write `@extends Class1
* @extends Class2`, and both classes have an instance method called `myMethod`, you get the docs
* from `Class2#myMethod`.
*
* @private
* @param {module:jsdoc/doclet.Doclet} doclet - The doclet to be added.
* @param {Array.<module:jsdoc/doclet.Doclet>} additions - An array of doclets that will be added to
* another symbol.
* @param {Object.<string, number>} indexes - A dictionary of indexes into the `additions` array.
* Each key is a longname, and each value is the index of the longname's doclet.
* @return {void}
*/
function updateAddedDoclets(doclet, additions, indexes) {
if (typeof indexes[doclet.longname] !== 'undefined') {
// replace the existing doclet
additions[indexes[doclet.longname]] = doclet;
}
else {
// add the doclet to the array, and track its index
additions.push(doclet);
indexes[doclet.longname] = additions.length - 1;
}
}
/**
* Update the index of doclets whose `undocumented` property is not `true`.
*
* @private
* @param {module:jsdoc/doclet.Doclet} doclet - The doclet to be added to the index.
* @param {Object.<string, Array.<module:jsdoc/doclet.Doclet>>} documented - The index of doclets
* whose `undocumented` property is not `true`.
* @return {void}
*/
function updateDocumentedDoclets(doclet, documented) {
if ( !hasOwnProp.call(documented, doclet.longname) ) {
documented[doclet.longname] = [];
}
documented[doclet.longname].push(doclet);
}
/**
* Update the index of doclets with a `memberof` value.
*
* @private
* @param {module:jsdoc/doclet.Doclet} doclet - The doclet to be added to the index.
* @param {Object.<string, Array.<module:jsdoc/doclet.Doclet>>} memberof - The index of doclets
* with a `memberof` value.
* @return {void}
*/
function updateMemberofDoclets(doclet, memberof) {
if (doclet.memberof) {
if ( !hasOwnProp.call(memberof, doclet.memberof) ) {
memberof[doclet.memberof] = [];
}
memberof[doclet.memberof].push(doclet);
}
}
function explicitlyInherits(doclets) {
let doclet;
let inherits = false;
for (let i = 0, l = doclets.length; i < l; i++) {
doclet = doclets[i];
if (typeof doclet.inheritdoc !== 'undefined' || typeof doclet.override !== 'undefined') {
inherits = true;
break;
}
}
return inherits;
}
function changeMemberof(longname, newMemberof) {
const atoms = name.shorten(longname);
atoms.memberof = newMemberof;
return name.combine(atoms);
}
// TODO: try to reduce overlap with similar methods
function getInheritedAdditions(doclets, docs, {documented, memberof}) {
let additionIndexes;
const additions = [];
let childDoclet;
let childLongname;
let doc;
let parentDoclet;
let parentMembers;
let parents;
let member;
let parts;
// doclets will be undefined if the inherited symbol isn't documented
doclets = doclets || [];
for (let i = 0, ii = doclets.length; i < ii; i++) {
doc = doclets[i];
parents = doc.augments;
if ( parents && (doc.kind === 'class' || doc.kind === 'interface') ) {
// reset the lookup table of added doclet indexes by longname
additionIndexes = {};
for (let j = 0, jj = parents.length; j < jj; j++) {
parentMembers = getMembers(parents[j], docs, ['instance']);
for (let k = 0, kk = parentMembers.length; k < kk; k++) {
parentDoclet = parentMembers[k];
// We only care about symbols that are documented.
if (parentDoclet.undocumented) {
continue;
}
childLongname = changeMemberof(parentDoclet.longname, doc.longname);
childDoclet = getDocumentedLongname(childLongname, docs) || {};
// We don't want to fold in properties from the child doclet if it had an
// `@inheritdoc` tag.
if (hasOwnProp.call(childDoclet, 'inheritdoc')) {
childDoclet = {};
}
member = jsdoc.doclet.combine(childDoclet, parentDoclet);
if (!member.inherited) {
member.inherits = member.longname;
}
member.inherited = true;
member.memberof = doc.longname;
parts = name.shorten(member.longname);
parts.memberof = doc.longname;
member.longname = name.combine(parts);
// Indicate what the descendant is overriding. (We only care about the closest
// ancestor. For classes A > B > C, if B#a overrides A#a, and C#a inherits B#a,
// we don't want the doclet for C#a to say that it overrides A#a.)
if ( hasOwnProp.call(docs.index.longname, member.longname) ) {
member.overrides = parentDoclet.longname;
}
else {
delete member.overrides;
}
// Add the ancestor's docs unless the descendant overrides the ancestor AND
// documents the override.
if ( !hasOwnProp.call(documented, member.longname) ) {
updateAddedDoclets(member, additions, additionIndexes);
updateDocumentedDoclets(member, documented);
updateMemberofDoclets(member, memberof);
}
// If the descendant used an @inheritdoc or @override tag, add the ancestor's
// docs, and ignore the existing doclets.
else if ( explicitlyInherits(documented[member.longname]) ) {
// Ignore any existing doclets. (This is safe because we only get here if
// `member.longname` is an own property of `documented`.)
addDocletProperty(documented[member.longname], 'ignore', true);
updateAddedDoclets(member, additions, additionIndexes);
updateDocumentedDoclets(member, documented);
updateMemberofDoclets(member, memberof);
// Remove property that's no longer accurate.
if (member.virtual) {
delete member.virtual;
}
// Remove properties that we no longer need.
if (member.inheritdoc) {
delete member.inheritdoc;
}
if (member.override) {
delete member.override;
}
}
// If the descendant overrides the ancestor and documents the override,
// update the doclets to indicate what the descendant is overriding.
else {
addDocletProperty(documented[member.longname], 'overrides',
parentDoclet.longname);
}
}
}
}
}
return additions;
}
function updateMixes(mixedDoclet, mixedLongname) {
let idx;
let mixedName;
let names;
// take the fast path if there's no array of mixed-in longnames
if (!mixedDoclet.mixes) {
mixedDoclet.mixes = [mixedLongname];
}
else {
// find the short name of the longname we're mixing in
mixedName = name.shorten(mixedLongname).name;
// find the short name of each previously mixed-in symbol
// TODO: why do we run a map if we always shorten the same value? this looks like a bug...
names = mixedDoclet.mixes.map(() => name.shorten(mixedDoclet.longname).name);
// if we're mixing `myMethod` into `MixinC` from `MixinB`, and `MixinB` had the method mixed
// in from `MixinA`, don't show `MixinA.myMethod` in the `mixes` list
idx = names.indexOf(mixedName);
if (idx !== -1) {
mixedDoclet.mixes.splice(idx, 1);
}
mixedDoclet.mixes.push(mixedLongname);
}
}
// TODO: try to reduce overlap with similar methods
function getMixedInAdditions(mixinDoclets, allDoclets, {documented, memberof}) {
let additionIndexes;
const additions = [];
const commentedDoclets = documented;
let doclet;
let mixedDoclet;
let mixedDoclets;
let mixes;
// mixinDoclets will be undefined if the mixed-in symbol isn't documented
mixinDoclets = mixinDoclets || [];
for (let i = 0, ii = mixinDoclets.length; i < ii; i++) {
doclet = mixinDoclets[i];
mixes = doclet.mixes;
if (mixes) {
// reset the lookup table of added doclet indexes by longname
additionIndexes = {};
for (let j = 0, jj = mixes.length; j < jj; j++) {
mixedDoclets = getMembers(mixes[j], allDoclets, ['static']);
for (let k = 0, kk = mixedDoclets.length; k < kk; k++) {
// We only care about symbols that are documented.
if (mixedDoclets[k].undocumented) {
continue;
}
mixedDoclet = doop(mixedDoclets[k]);
updateMixes(mixedDoclet, mixedDoclet.longname);
mixedDoclet.mixed = true;
reparentDoclet(doclet, mixedDoclet);
// if we're mixing into a class, treat the mixed-in symbol as an instance member
if (parentIsClass(doclet)) {
staticToInstance(mixedDoclet);
}
updateAddedDoclets(mixedDoclet, additions, additionIndexes);
updateDocumentedDoclets(mixedDoclet, commentedDoclets);
updateMemberofDoclets(mixedDoclet, memberof);
}
}
}
}
return additions;
}
function updateImplements(implDoclets, implementedLongname) {
if ( !Array.isArray(implDoclets) ) {
implDoclets = [implDoclets];
}
implDoclets.forEach(implDoclet => {
if ( !hasOwnProp.call(implDoclet, 'implements') ) {
implDoclet.implements = [];
}
if (!implDoclet.implements.includes(implementedLongname)) {
implDoclet.implements.push(implementedLongname);
}
});
}
// TODO: try to reduce overlap with similar methods
function getImplementedAdditions(implDoclets, allDoclets, {documented, memberof}) {
let additionIndexes;
const additions = [];
let childDoclet;
let childLongname;
const commentedDoclets = documented;
let doclet;
let implementations;
let implExists;
let implementationDoclet;
let interfaceDoclets;
let parentDoclet;
// interfaceDoclets will be undefined if the implemented symbol isn't documented
implDoclets = implDoclets || [];
for (let i = 0, ii = implDoclets.length; i < ii; i++) {
doclet = implDoclets[i];
implementations = doclet.implements;
if (implementations) {
// reset the lookup table of added doclet indexes by longname
additionIndexes = {};
for (let j = 0, jj = implementations.length; j < jj; j++) {
interfaceDoclets = getMembers(implementations[j], allDoclets, ['instance']);
for (let k = 0, kk = interfaceDoclets.length; k < kk; k++) {
parentDoclet = interfaceDoclets[k];
// We only care about symbols that are documented.
if (parentDoclet.undocumented) {
continue;
}
childLongname = changeMemberof(parentDoclet.longname, doclet.longname);
childDoclet = getDocumentedLongname(childLongname, allDoclets) || {};
// We don't want to fold in properties from the child doclet if it had an
// `@inheritdoc` tag.
if (hasOwnProp.call(childDoclet, 'inheritdoc')) {
childDoclet = {};
}
implementationDoclet = jsdoc.doclet.combine(childDoclet, parentDoclet);
reparentDoclet(doclet, implementationDoclet);
updateImplements(implementationDoclet, parentDoclet.longname);
// If there's no implementation, move along.
implExists = hasOwnProp.call(allDoclets.index.longname,
implementationDoclet.longname);
if (!implExists) {
continue;
}
// Add the interface's docs unless the implementation is already documented.
if ( !hasOwnProp.call(commentedDoclets, implementationDoclet.longname) ) {
updateAddedDoclets(implementationDoclet, additions, additionIndexes);
updateDocumentedDoclets(implementationDoclet, commentedDoclets);
updateMemberofDoclets(implementationDoclet, memberof);
}
// If the implementation used an @inheritdoc or @override tag, add the
// interface's docs, and ignore the existing doclets.
else if ( explicitlyInherits(commentedDoclets[implementationDoclet.longname]) ) {
// Ignore any existing doclets. (This is safe because we only get here if
// `implementationDoclet.longname` is an own property of
// `commentedDoclets`.)
addDocletProperty(commentedDoclets[implementationDoclet.longname], 'ignore',
true);
updateAddedDoclets(implementationDoclet, additions, additionIndexes);
updateDocumentedDoclets(implementationDoclet, commentedDoclets);
updateMemberofDoclets(implementationDoclet, memberof);
// Remove property that's no longer accurate.
if (implementationDoclet.virtual) {
delete implementationDoclet.virtual;
}
// Remove properties that we no longer need.
if (implementationDoclet.inheritdoc) {
delete implementationDoclet.inheritdoc;
}
if (implementationDoclet.override) {
delete implementationDoclet.override;
}
}
// If there's an implementation, and it's documented, update the doclets to
// indicate what the implementation is implementing.
else {
updateImplements(commentedDoclets[implementationDoclet.longname],
parentDoclet.longname);
}
}
}
}
}
return additions;
}
function augment(doclets, propertyName, docletFinder) {
const index = doclets.index.longname;
const dependencies = sort( mapDependencies(index, propertyName) );
dependencies.forEach(depName => {
const additions = docletFinder(index[depName], doclets, doclets.index);
additions.forEach(addition => {
const longname = addition.longname;
if ( !hasOwnProp.call(index, longname) ) {
index[longname] = [];
}
index[longname].push(addition);
doclets.push(addition);
});
});
}
/**
* Add doclets to reflect class inheritance.
*
* For example, if `ClassA` has the instance method `myMethod`, and `ClassB` inherits from `ClassA`,
* calling this method creates a new doclet for `ClassB#myMethod`.
*
* @param {!Array.<module:jsdoc/doclet.Doclet>} doclets - The doclets generated by JSDoc.
* @param {!Object} doclets.index - The doclet index.
* @return {void}
*/
exports.addInherited = doclets => {
augment(doclets, 'augments', getInheritedAdditions);
};
/**
* Add doclets to reflect mixins. When a symbol is mixed into a class, the class' version of the
* mixed-in symbol is treated as an instance member.
*
* For example:
*
* + If `MixinA` has the static method `myMethod`, and `MixinB` mixes `MixinA`, calling this method
* creates a new doclet for the static method `MixinB.myMethod`.
* + If `MixinA` has the static method `myMethod`, and `ClassA` mixes `MixinA`, calling this method
* creates a new doclet for the instance method `ClassA#myMethod`.
*
* @param {!Array.<module:jsdoc/doclet.Doclet>} doclets - The doclets generated by JSDoc.
* @param {!Object} doclets.index - The doclet index.
* @return {void}
*/
exports.addMixedIn = doclets => {
augment(doclets, 'mixes', getMixedInAdditions);
};
/**
* Add and update doclets to reflect implementations of interfaces.
*
* For example, if `InterfaceA` has the instance method `myMethod`, and `ClassA` implements
* `InterfaceA`, calling this method does the following:
*
* + Updates `InterfaceA` to indicate that it is implemented by `ClassA`
* + Updates `InterfaceA#myMethod` to indicate that it is implemented by `ClassA#myMethod`
* + Updates `ClassA#myMethod` to indicate that it implements `InterfaceA#myMethod`
*
* If `ClassA#myMethod` used the `@override` or `@inheritdoc` tag, calling this method would also
* generate a new doclet that reflects the interface's documentation for `InterfaceA#myMethod`.
*
* @param {!Array.<module:jsdoc/doclet.Doclet>} docs - The doclets generated by JSDoc.
* @param {!Object} doclets.index - The doclet index.
* @return {void}
*/
exports.addImplemented = doclets => {
augment(doclets, 'implements', getImplementedAdditions);
};
/**
* Add and update doclets to reflect all of the following:
*
* + Inherited classes
* + Mixins
* + Interface implementations
*
* Calling this method is equivalent to calling all other methods exported by this module.
*
* @return {void}
*/
exports.augmentAll = doclets => {
exports.addMixedIn(doclets);
exports.addImplemented(doclets);
exports.addInherited(doclets);
// look for implemented doclets again, in case we inherited an interface
exports.addImplemented(doclets);
};
+52
View File
@@ -0,0 +1,52 @@
/**
* A collection of functions relating to resolving @borrows tags in JSDoc symbols.
* @module jsdoc/borrow
*/
const doop = require('jsdoc/util/doop');
const SCOPE = require('jsdoc/name').SCOPE;
function cloneBorrowedDoclets({borrowed, longname}, doclets) {
borrowed.forEach(({from, as}) => {
const borrowedDoclets = doclets.index.longname[from];
let borrowedAs = as || from;
let parts;
let scopePunc;
if (borrowedDoclets) {
borrowedAs = borrowedAs.replace(/^prototype\./, SCOPE.PUNC.INSTANCE);
doop(borrowedDoclets).forEach(clone => {
// TODO: this will fail on longnames like '"Foo#bar".baz'
parts = borrowedAs.split(SCOPE.PUNC.INSTANCE);
if (parts.length === 2) {
clone.scope = SCOPE.NAMES.INSTANCE;
scopePunc = SCOPE.PUNC.INSTANCE;
}
else {
clone.scope = SCOPE.NAMES.STATIC;
scopePunc = SCOPE.PUNC.STATIC;
}
clone.name = parts.pop();
clone.memberof = longname;
clone.longname = clone.memberof + scopePunc + clone.name;
doclets.push(clone);
});
}
});
}
/**
Take a copy of the docs for borrowed symbols and attach them to the
docs for the borrowing symbol. This process changes the symbols involved,
moving docs from the "borrowed" array and into the general docs, then
deleting the "borrowed" array.
*/
exports.resolveBorrows = doclets => {
for (let doclet of doclets.index.borrowed) {
cloneBorrowedDoclets(doclet, doclets);
delete doclet.borrowed;
}
doclets.index.borrowed = [];
};
+74
View File
@@ -0,0 +1,74 @@
/**
* @module jsdoc/config
*/
const stripBom = require('jsdoc/util/stripbom');
const stripJsonComments = require('strip-json-comments');
function mergeRecurse(target, source) {
Object.keys(source).forEach(p => {
if ( source[p].constructor === Object ) {
if ( !target[p] ) {
target[p] = {};
}
mergeRecurse(target[p], source[p]);
}
else {
target[p] = source[p];
}
});
return target;
}
// required config values, override these defaults in your config.json if necessary
const defaults = {
plugins: [],
recurseDepth: 10,
source: {
includePattern: '.+\\.js(doc|x)?$',
excludePattern: ''
},
sourceType: 'module',
tags: {
allowUnknownTags: true,
dictionaries: ['jsdoc', 'closure']
},
templates: {
monospaceLinks: false,
cleverLinks: false
}
};
/**
* Represents a JSDoc application configuration.
*/
class Config {
/**
* @param {(string|object)} [jsonOrObject] - The contents of config.json, or a JavaScript object
* exported from a .js config file.
*/
constructor(jsonOrObject) {
if (typeof jsonOrObject === 'undefined') {
jsonOrObject = {};
}
if (typeof jsonOrObject === 'string') {
jsonOrObject = JSON.parse( (stripJsonComments(stripBom.strip(jsonOrObject)) || '{}') );
}
if (typeof jsonOrObject !== 'object') {
jsonOrObject = {};
}
this._config = mergeRecurse(defaults, jsonOrObject);
}
/**
* Get the merged configuration values.
*/
get() {
return this._config;
}
}
module.exports = Config;
+562
View File
@@ -0,0 +1,562 @@
/**
* @module jsdoc/doclet
*/
const _ = require('underscore');
const jsdoc = {
env: require('jsdoc/env'),
name: require('jsdoc/name'),
src: {
astnode: require('jsdoc/src/astnode'),
Syntax: require('jsdoc/src/syntax').Syntax
},
tag: {
Tag: require('jsdoc/tag').Tag,
dictionary: require('jsdoc/tag/dictionary')
},
util: {
doop: require('jsdoc/util/doop')
}
};
const path = require('jsdoc/path');
const Syntax = jsdoc.src.Syntax;
const util = require('util');
function applyTag(doclet, {title, value}) {
if (title === 'name') {
doclet.name = value;
}
if (title === 'kind') {
doclet.kind = value;
}
if (title === 'description') {
doclet.description = value;
}
}
function fakeMeta(node) {
return {
type: node ? node.type : null,
node: node
};
}
// use the meta info about the source code to guess what the doclet kind should be
// TODO: set this elsewhere (maybe jsdoc/src/astnode.getInfo)
function codeToKind(code) {
const isFunction = jsdoc.src.astnode.isFunction;
let kind = 'member';
const node = code.node;
if ( isFunction(code.type) && code.type !== Syntax.MethodDefinition ) {
kind = 'function';
}
else if (code.type === Syntax.MethodDefinition) {
if (code.node.kind === 'constructor') {
kind = 'class';
}
else if (code.node.kind !== 'get' && code.node.kind !== 'set') {
kind = 'function';
}
}
else if (code.type === Syntax.ClassDeclaration || code.type === Syntax.ClassExpression) {
kind = 'class';
}
else if (code.type === Syntax.ExportAllDeclaration) {
// this value will often be an Identifier for a variable, which isn't very useful
kind = codeToKind(fakeMeta(node.source));
}
else if (code.type === Syntax.ExportDefaultDeclaration ||
code.type === Syntax.ExportNamedDeclaration) {
kind = codeToKind(fakeMeta(node.declaration));
}
else if (code.type === Syntax.ExportSpecifier) {
// this value will often be an Identifier for a variable, which isn't very useful
kind = codeToKind(fakeMeta(node.local));
}
else if ( code.node && code.node.parent && isFunction(code.node.parent) ) {
kind = 'param';
}
return kind;
}
function unwrap(docletSrc) {
if (!docletSrc) { return ''; }
// note: keep trailing whitespace for @examples
// extra opening/closing stars are ignored
// left margin is considered a star and a space
// use the /m flag on regex to avoid having to guess what this platform's newline is
docletSrc =
// remove opening slash+stars
docletSrc.replace(/^\/\*\*+/, '')
// replace closing star slash with end-marker
.replace(/\**\*\/$/, '\\Z')
// remove left margin like: spaces+star or spaces+end-marker
.replace(/^\s*(\* ?|\\Z)/gm, '')
// remove end-marker
.replace(/\s*\\Z$/g, '');
return docletSrc;
}
/**
* Convert the raw source of the doclet comment into an array of pseudo-Tag objects.
* @private
*/
function toTags(docletSrc) {
let parsedTag;
const tagData = [];
let tagText;
let tagTitle;
// split out the basic tags, keep surrounding whitespace
// like: @tagTitle tagBody
docletSrc
// replace splitter ats with an arbitrary sequence
.replace(/^(\s*)@(\S)/gm, '$1\\@$2')
// then split on that arbitrary sequence
.split('\\@')
.forEach($ => {
if ($) {
parsedTag = $.match(/^(\S+)(?:\s+(\S[\s\S]*))?/);
if (parsedTag) {
tagTitle = parsedTag[1];
tagText = parsedTag[2];
if (tagTitle) {
tagData.push({
title: tagTitle,
text: tagText
});
}
}
}
});
return tagData;
}
function fixDescription(docletSrc, {code}) {
let isClass;
if (!/^\s*@/.test(docletSrc) && docletSrc.replace(/\s/g, '').length) {
isClass = code &&
(code.type === Syntax.ClassDeclaration ||
code.type === Syntax.ClassExpression);
docletSrc = `${isClass ? '@classdesc' : '@description'} ${docletSrc}`;
}
return docletSrc;
}
/**
* Replace the existing tag dictionary with a new tag dictionary.
*
* Used for testing only.
*
* @private
* @param {module:jsdoc/tag/dictionary.Dictionary} dict - The new tag dictionary.
*/
exports._replaceDictionary = function _replaceDictionary(dict) {
jsdoc.tag.dictionary = dict;
require('jsdoc/tag')._replaceDictionary(dict);
require('jsdoc/util/templateHelper')._replaceDictionary(dict);
};
function removeGlobal(longname) {
const globalRegexp = new RegExp(`^${jsdoc.name.LONGNAMES.GLOBAL}\\.?`);
return longname.replace(globalRegexp, '');
}
/**
* Get the full path to the source file that is associated with a doclet.
*
* @private
* @param {module:jsdoc/doclet.Doclet} The doclet to check for a filepath.
* @return {string} The path to the doclet's source file, or an empty string if the path is not
* available.
*/
function getFilepath(doclet) {
if (!doclet || !doclet.meta || !doclet.meta.filename) {
return '';
}
return path.join(doclet.meta.path || '', doclet.meta.filename);
}
function dooper(source, target, properties) {
properties.forEach(property => {
switch (typeof source[property]) {
case 'function':
// do nothing
break;
case 'object':
target[property] = jsdoc.util.doop(source[property]);
break;
default:
target[property] = source[property];
}
});
}
/**
* Copy all but a list of excluded properties from one of two doclets onto a target doclet. Prefers
* the primary doclet over the secondary doclet.
*
* @private
* @param {module:jsdoc/doclet.Doclet} primary - The primary doclet.
* @param {module:jsdoc/doclet.Doclet} secondary - The secondary doclet.
* @param {module:jsdoc/doclet.Doclet} target - The doclet to which properties will be copied.
* @param {Array.<string>} exclude - The names of properties to exclude from copying.
*/
function copyMostProperties(primary, secondary, target, exclude) {
const primaryProperties = _.difference(Object.getOwnPropertyNames(primary), exclude);
const secondaryProperties = _.difference(Object.getOwnPropertyNames(secondary),
exclude.concat(primaryProperties));
dooper(primary, target, primaryProperties);
dooper(secondary, target, secondaryProperties);
}
/**
* Copy specific properties from one of two doclets onto a target doclet, as long as the property
* has a non-falsy value and a length greater than 0. Prefers the primary doclet over the secondary
* doclet.
*
* @private
* @param {module:jsdoc/doclet.Doclet} primary - The primary doclet.
* @param {module:jsdoc/doclet.Doclet} secondary - The secondary doclet.
* @param {module:jsdoc/doclet.Doclet} target - The doclet to which properties will be copied.
* @param {Array.<string>} include - The names of properties to copy.
*/
function copySpecificProperties(primary, secondary, target, include) {
include.forEach(property => {
if ({}.hasOwnProperty.call(primary, property) && primary[property] &&
primary[property].length) {
target[property] = jsdoc.util.doop(primary[property]);
}
else if ({}.hasOwnProperty.call(secondary, property) && secondary[property] &&
secondary[property].length) {
target[property] = jsdoc.util.doop(secondary[property]);
}
});
}
/**
* Represents a single JSDoc comment.
*
* @alias module:jsdoc/doclet.Doclet
*/
class Doclet {
/**
* Create a doclet.
*
* @param {string} docletSrc - The raw source code of the jsdoc comment.
* @param {object=} meta - Properties describing the code related to this comment.
*/
constructor(docletSrc, meta = {}) {
let newTags = [];
/** The original text of the comment from the source code. */
this.comment = docletSrc;
this.setMeta(meta);
docletSrc = unwrap(docletSrc);
docletSrc = fixDescription(docletSrc, meta);
newTags = toTags.call(this, docletSrc);
for (let i = 0, l = newTags.length; i < l; i++) {
this.addTag(newTags[i].title, newTags[i].text);
}
this.postProcess();
}
/** Called once after all tags have been added. */
postProcess() {
let i;
let l;
if (!this.preserveName) {
jsdoc.name.resolve(this);
}
if (this.name && !this.longname) {
this.setLongname(this.name);
}
if (this.memberof === '') {
delete this.memberof;
}
if (!this.kind && this.meta && this.meta.code) {
this.addTag( 'kind', codeToKind(this.meta.code) );
}
if (this.variation && this.longname && !/\)$/.test(this.longname) ) {
this.longname += `(${this.variation})`;
}
// add in any missing param names
if (this.params && this.meta && this.meta.code && this.meta.code.paramnames) {
for (i = 0, l = this.params.length; i < l; i++) {
if (!this.params[i].name) {
this.params[i].name = this.meta.code.paramnames[i] || '';
}
}
}
}
/**
* Add a tag to the doclet.
*
* @param {string} title - The title of the tag being added.
* @param {string} [text] - The text of the tag being added.
*/
addTag(title, text) {
const tagDef = jsdoc.tag.dictionary.lookUp(title);
const newTag = new jsdoc.tag.Tag(title, text, this.meta);
if (tagDef && tagDef.onTagged) {
tagDef.onTagged(this, newTag);
}
if (!tagDef) {
this.tags = this.tags || [];
this.tags.push(newTag);
}
applyTag(this, newTag);
}
/**
* Set the doclet's `memberof` property.
*
* @param {string} sid - The longname of the doclet's parent symbol.
*/
setMemberof(sid) {
/**
* The longname of the symbol that contains this one, if any.
* @type {string}
*/
this.memberof = removeGlobal(sid)
.replace(/\.prototype/g, jsdoc.name.SCOPE.PUNC.INSTANCE);
}
/**
* Set the doclet's `longname` property.
*
* @param {string} name - The longname for the doclet.
*/
setLongname(name) {
/**
* The fully resolved symbol name.
* @type {string}
*/
this.longname = removeGlobal(name);
if (jsdoc.tag.dictionary.isNamespace(this.kind)) {
this.longname = jsdoc.name.applyNamespace(this.longname, this.kind);
}
}
/**
* Set the doclet's `scope` property. Must correspond to a scope name that is defined in
* {@link module:jsdoc/name.SCOPE.NAMES}.
*
* @param {module:jsdoc/name.SCOPE.NAMES} scope - The scope for the doclet relative to the
* symbol's parent.
* @throws {Error} If the scope name is not recognized.
*/
setScope(scope) {
let errorMessage;
let filepath;
const scopeNames = _.values(jsdoc.name.SCOPE.NAMES);
if (!scopeNames.includes(scope)) {
filepath = getFilepath(this);
errorMessage = util.format('The scope name "%s" is not recognized. Use one of the ' +
'following values: %j', scope, scopeNames);
if (filepath) {
errorMessage += util.format(' (Source file: %s)', filepath);
}
throw new Error(errorMessage);
}
this.scope = scope;
}
/**
* Add a symbol to this doclet's `borrowed` array.
*
* @param {string} source - The longname of the symbol that is the source.
* @param {string} target - The name the symbol is being assigned to.
*/
borrow(source, target) {
const about = { from: source };
if (target) {
about.as = target;
}
if (!this.borrowed) {
/**
* A list of symbols that are borrowed by this one, if any.
* @type {Array.<string>}
*/
this.borrowed = [];
}
this.borrowed.push(about);
}
mix(source) {
/**
* A list of symbols that are mixed into this one, if any.
* @type Array.<string>
*/
this.mixes = this.mixes || [];
this.mixes.push(source);
}
/**
* Add a symbol to the doclet's `augments` array.
*
* @param {string} base - The longname of the base symbol.
*/
augment(base) {
/**
* A list of symbols that are augmented by this one, if any.
* @type Array.<string>
*/
this.augments = this.augments || [];
this.augments.push(base);
}
/**
* Set the `meta` property of this doclet.
*
* @param {object} meta
*/
setMeta(meta) {
let pathname;
/**
* Information about the source code associated with this doclet.
* @namespace
*/
this.meta = this.meta || {};
if (meta.range) {
/**
* The positions of the first and last characters of the code associated with this doclet.
* @type Array.<number>
*/
this.meta.range = meta.range.slice(0);
}
if (meta.lineno) {
/**
* The name of the file containing the code associated with this doclet.
* @type string
*/
this.meta.filename = path.basename(meta.filename);
/**
* The line number of the code associated with this doclet.
* @type number
*/
this.meta.lineno = meta.lineno;
/**
* The column number of the code associated with this doclet.
* @type number
*/
this.meta.columnno = meta.columnno;
pathname = path.dirname(meta.filename);
if (pathname && pathname !== '.') {
this.meta.path = pathname;
}
}
/**
* Information about the code symbol.
* @namespace
*/
this.meta.code = this.meta.code || {};
if (meta.id) { this.meta.code.id = meta.id; }
if (meta.code) {
if (meta.code.name) {
/**
* The name of the symbol in the source code.
* @type {string}
*/
this.meta.code.name = meta.code.name;
}
if (meta.code.type) {
/**
* The type of the symbol in the source code.
* @type {string}
*/
this.meta.code.type = meta.code.type;
}
if (meta.code.node) {
Object.defineProperty(this.meta.code, 'node', {
value: meta.code.node,
enumerable: false
});
}
if (meta.code.funcscope) {
this.meta.code.funcscope = meta.code.funcscope;
}
if (typeof meta.code.value !== 'undefined') {
/**
* The value of the symbol in the source code.
* @type {*}
*/
this.meta.code.value = meta.code.value;
}
if (meta.code.paramnames) {
this.meta.code.paramnames = meta.code.paramnames.slice(0);
}
}
}
}
exports.Doclet = Doclet;
/**
* Combine two doclets into a new doclet.
*
* @param {module:jsdoc/doclet.Doclet} primary - The doclet whose properties will be used.
* @param {module:jsdoc/doclet.Doclet} secondary - The doclet to use as a fallback for properties
* that the primary doclet does not have.
* @returns {module:jsdoc/doclet.Doclet} A new doclet that combines the primary and secondary
* doclets.
*/
exports.combine = (primary, secondary) => {
const copyMostPropertiesExclude = [
'params',
'properties',
'undocumented'
];
const copySpecificPropertiesInclude = [
'params',
'properties'
];
const target = new Doclet('');
// First, copy most properties to the target doclet.
copyMostProperties(primary, secondary, target, copyMostPropertiesExclude);
// Then copy a few specific properties to the target doclet, as long as they're not falsy and
// have a length greater than 0.
copySpecificProperties(primary, secondary, target, copySpecificPropertiesInclude);
return target;
};
+77
View File
@@ -0,0 +1,77 @@
/**
* Data about the environment in which JSDoc is running, including the configuration settings that
* were used to run JSDoc.
*
* @module jsdoc/env
*/
module.exports = {
/**
* The times at which JSDoc started and finished.
*
* @type {Object}
* @property {Date} start - The time at which JSDoc started running.
* @property {Date} finish - The time at which JSDoc finished running.
*/
run: {
start: new Date(),
finish: null
},
/**
* The command-line arguments passed to JSDoc.
*
* @type {Array<*>}
*/
args: [],
/**
* The data parsed from JSDoc's configuration file.
*
* @type Object<string, *>
*/
conf: {},
/**
* The absolute path to the base directory in which JSDoc is located. Set at startup.
*
* @private
* @type {string}
*/
dirname: null,
/**
* The user's working directory at the time when JSDoc started running.
*
* @private
* @type {string}
*/
pwd: null,
/**
* The command-line arguments, parsed into a key/value hash.
*
* @type {Object}
* @example if (global.env.opts.help) { console.log('Helpful message.'); }
*/
opts: {},
/**
* The source files that JSDoc will parse.
*
* @type {Array<string>}
* @memberof env
*/
sourceFiles: [],
/**
* The JSDoc version number and revision date.
*
* @type {Object<string, string>}
* @property {string} number - The JSDoc version number.
* @property {string} revision - The JSDoc revision number, expressed as a UTC date string.
*/
version: {
number: null,
revision: null
}
};
+109
View File
@@ -0,0 +1,109 @@
/**
* Extended version of the standard `fs` module.
* @module jsdoc/fs
*/
const fs = require('fs');
const path = require('path');
const mkdirp = require('mkdirp');
const ls = exports.ls = (dir, recurse, _allFiles, _path) => {
let file;
let files;
let isFile;
// first pass
if (_path === undefined) {
_allFiles = [];
_path = [dir];
}
if (!_path.length) {
return _allFiles;
}
if (recurse === undefined) {
recurse = 1;
}
try {
isFile = fs.statSync(dir).isFile();
}
catch (e) {
isFile = false;
}
if (isFile) {
files = [dir];
}
else {
files = fs.readdirSync(dir);
}
for (let i = 0, l = files.length; i < l; i++) {
file = String(files[i]);
// skip dot files
if (file.match(/^\.[^./\\]/)) {
continue;
}
if ( fs.statSync(path.join(_path.join('/'), file)).isDirectory() ) {
// it's a directory
_path.push(file);
if (_path.length - 1 < recurse) {
ls(_path.join('/'), recurse, _allFiles, _path);
}
_path.pop();
}
else {
// it's a file
_allFiles.push( path.normalize(path.join(_path.join('/'), file)) );
}
}
return _allFiles;
};
exports.toDir = _path => {
let isDirectory;
_path = path.normalize(_path);
try {
isDirectory = fs.statSync(_path).isDirectory();
}
catch (e) {
isDirectory = false;
}
if (isDirectory) {
return _path;
} else {
return path.dirname(_path);
}
};
exports.mkPath = _path => {
if ( Array.isArray(_path) ) {
_path = _path.join('');
}
mkdirp.sync(_path);
};
exports.copyFileSync = (inFile, outDir = '', fileName) => {
fileName = fileName || path.basename(inFile);
fs.copyFileSync(inFile, path.join(outDir, fileName));
};
const alwaysOverride = {
'copyFileSync': true
};
Object.keys(fs).forEach(member => {
if (!alwaysOverride[member]) {
exports[member] = fs[member];
}
});
+595
View File
@@ -0,0 +1,595 @@
/**
* A collection of functions relating to JSDoc symbol name manipulation.
* @module jsdoc/name
*/
const _ = require('underscore');
const escape = require('escape-string-regexp');
const hasOwnProp = Object.prototype.hasOwnProperty;
/**
* Longnames that have a special meaning in JSDoc.
*
* @enum {string}
* @static
* @memberof module:jsdoc/name
*/
const LONGNAMES = exports.LONGNAMES = {
/** Longname used for doclets that do not have a longname, such as anonymous functions. */
ANONYMOUS: '<anonymous>',
/** Longname that represents global scope. */
GLOBAL: '<global>'
};
// Module namespace prefix.
const MODULE_NAMESPACE = 'module:';
/**
* Names and punctuation marks that identify doclet scopes.
*
* @enum {string}
* @static
* @memberof module:jsdoc/name
*/
const SCOPE = exports.SCOPE = {
NAMES: {
GLOBAL: 'global',
INNER: 'inner',
INSTANCE: 'instance',
STATIC: 'static'
},
PUNC: {
INNER: '~',
INSTANCE: '#',
STATIC: '.'
}
};
// For backwards compatibility, this enum must use lower-case keys
const scopeToPunc = exports.scopeToPunc = {
'inner': SCOPE.PUNC.INNER,
'instance': SCOPE.PUNC.INSTANCE,
'static': SCOPE.PUNC.STATIC
};
const puncToScope = exports.puncToScope = _.invert(scopeToPunc);
const DEFAULT_SCOPE = SCOPE.NAMES.STATIC;
const SCOPE_PUNC = _.values(SCOPE.PUNC);
const SCOPE_PUNC_STRING = `[${SCOPE_PUNC.join()}]`;
const REGEXP_LEADING_SCOPE = new RegExp(`^(${SCOPE_PUNC_STRING})`);
const REGEXP_TRAILING_SCOPE = new RegExp(`(${SCOPE_PUNC_STRING})$`);
const DESCRIPTION = '(?:(?:[ \\t]*\\-\\s*|\\s+)(\\S[\\s\\S]*))?$';
const REGEXP_DESCRIPTION = new RegExp(DESCRIPTION);
const REGEXP_NAME_DESCRIPTION = new RegExp(`^(\\[[^\\]]+\\]|\\S+)${DESCRIPTION}`);
function nameIsLongname(name, memberof) {
const regexp = new RegExp(`^${escape(memberof)}${SCOPE_PUNC_STRING}`);
return regexp.test(name);
}
function prototypeToPunc(name) {
// don't mangle symbols named "prototype"
if (name === 'prototype') {
return name;
}
return name.replace(/(?:^|\.)prototype\.?/g, SCOPE.PUNC.INSTANCE);
}
// TODO: docs
/**
* @param {string} name - The symbol's longname.
* @return {string} The symbol's basename.
*/
exports.getBasename = name => {
if (name !== undefined) {
return name.replace(/^([$a-z_][$a-z_0-9]*).*?$/i, '$1');
}
return undefined;
};
// TODO: deprecate exports.resolve in favor of a better name
/**
* Resolves the longname, memberof, variation and name values of the given doclet.
* @param {module:jsdoc/doclet.Doclet} doclet
*/
exports.resolve = doclet => {
let about = {};
let memberof = doclet.memberof || '';
let metaName;
let name = doclet.name ? String(doclet.name) : '';
let puncAndName;
let puncAndNameIndex;
// change MyClass.prototype.instanceMethod to MyClass#instanceMethod
// (but not in function params, which lack doclet.kind)
// TODO: check for specific doclet.kind values (probably function, class, and module)
if (name && doclet.kind) {
name = prototypeToPunc(name);
}
doclet.name = name;
// does the doclet have an alias that identifies the memberof? if so, use it
if (doclet.alias) {
about = exports.shorten(name);
if (about.memberof) {
memberof = about.memberof;
}
}
// member of a var in an outer scope?
else if (name && !memberof && doclet.meta.code && doclet.meta.code.funcscope) {
name = doclet.longname = doclet.meta.code.funcscope + SCOPE.PUNC.INNER + name;
}
if (memberof || doclet.forceMemberof) { // @memberof tag given
memberof = prototypeToPunc(memberof);
// the name is a complete longname, like @name foo.bar, @memberof foo
if (name && nameIsLongname(name, memberof) && name !== memberof) {
about = exports.shorten(name, (doclet.forceMemberof ? memberof : undefined));
}
// the name and memberof are identical and refer to a module,
// like @name module:foo, @memberof module:foo (probably a member like 'var exports')
else if (name && name === memberof && name.indexOf(MODULE_NAMESPACE) === 0) {
about = exports.shorten(name, (doclet.forceMemberof ? memberof : undefined));
}
// the name and memberof are identical, like @name foo, @memberof foo
else if (name && name === memberof) {
doclet.scope = doclet.scope || DEFAULT_SCOPE;
name = memberof + scopeToPunc[doclet.scope] + name;
about = exports.shorten(name, (doclet.forceMemberof ? memberof : undefined));
}
// like @memberof foo# or @memberof foo~
else if (name && REGEXP_TRAILING_SCOPE.test(memberof) ) {
about = exports.shorten(memberof + name, (doclet.forceMemberof ? memberof : undefined));
}
else if (name && doclet.scope) {
about = exports.shorten(memberof + (scopeToPunc[doclet.scope] || '') + name,
(doclet.forceMemberof ? memberof : undefined));
}
}
else { // no @memberof
about = exports.shorten(name);
}
if (about.name) {
doclet.name = about.name;
}
if (about.memberof) {
doclet.setMemberof(about.memberof);
}
if (about.longname && (!doclet.longname || doclet.longname === doclet.name)) {
doclet.setLongname(about.longname);
}
if (doclet.scope === SCOPE.NAMES.GLOBAL) { // via @global tag?
doclet.setLongname(doclet.name);
delete doclet.memberof;
}
else if (about.scope) {
if (about.memberof === LONGNAMES.GLOBAL) { // via @memberof <global> ?
doclet.scope = SCOPE.NAMES.GLOBAL;
}
else {
doclet.scope = puncToScope[about.scope];
}
}
else if (doclet.name && doclet.memberof && !doclet.longname) {
if ( REGEXP_LEADING_SCOPE.test(doclet.name) ) {
doclet.scope = puncToScope[RegExp.$1];
doclet.name = doclet.name.substr(1);
}
else if (doclet.meta.code && doclet.meta.code.name) {
// HACK: Handle cases where an ES 2015 class is a static memberof something else, and
// the class has instance members. In these cases, we have to detect the instance
// members' scope by looking at the meta info. There's almost certainly a better way to
// do this...
metaName = String(doclet.meta.code.name);
puncAndName = SCOPE.PUNC.INSTANCE + doclet.name;
puncAndNameIndex = metaName.indexOf(puncAndName);
if ( puncAndNameIndex !== -1 &&
(puncAndNameIndex === metaName.length - puncAndName.length) ) {
doclet.scope = SCOPE.NAMES.INSTANCE;
}
}
doclet.scope = doclet.scope || DEFAULT_SCOPE;
doclet.setLongname(doclet.memberof + scopeToPunc[doclet.scope] + doclet.name);
}
if (about.variation) {
doclet.variation = about.variation;
}
// if we never found a longname, just use an empty string
if (!doclet.longname) {
doclet.longname = '';
}
};
/**
* @param {string} longname The full longname of the symbol.
* @param {string} ns The namespace to be applied.
* @returns {string} The longname with the namespace applied.
*/
exports.applyNamespace = (longname, ns) => {
const nameParts = exports.shorten(longname);
const name = nameParts.name;
longname = nameParts.longname;
if ( !/^[a-zA-Z]+?:.+$/i.test(name) ) {
longname = longname.replace( new RegExp(`${escape(name)}$`), `${ns}:${name}` );
}
return longname;
};
// TODO: docs
exports.stripNamespace = longname => longname.replace(/^[a-zA-Z]+:/, '');
/**
* Check whether a parent longname is an ancestor of a child longname.
*
* @param {string} parent - The parent longname.
* @param {string} child - The child longname.
* @return {boolean} `true` if the parent is an ancestor of the child; otherwise, `false`.
*/
exports.hasAncestor = (parent, child) => {
let hasAncestor = false;
let memberof = child;
if (!parent || !child) {
return hasAncestor;
}
// fast path for obvious non-ancestors
if (child.indexOf(parent) !== 0) {
return hasAncestor;
}
do {
memberof = exports.shorten(memberof).memberof;
if (memberof === parent) {
hasAncestor = true;
}
} while (!hasAncestor && memberof);
return hasAncestor;
};
// TODO: docs
function atomize(longname, sliceChars, forcedMemberof) {
let i;
let memberof = '';
let name = '';
let parts;
let partsRegExp;
let scopePunc = '';
let token;
const tokens = [];
let variation;
// quoted strings in a longname are atomic, so we convert them to tokens:
// foo["bar"] => foo.@{1}@
// Foo.prototype["bar"] => Foo#@{1}
longname = longname.replace(/(prototype|#)?(\[?["'].+?["']\]?)/g, ($, p1, p2) => {
let punc = '';
// is there a leading bracket?
if ( /^\[/.test(p2) ) {
// is it a static or instance member?
punc = p1 ? SCOPE.PUNC.INSTANCE : SCOPE.PUNC.STATIC;
p2 = p2.replace(/^\[/g, '')
.replace(/\]$/g, '');
}
token = `@{${tokens.length}}@`;
tokens.push(p2);
return punc + token;
});
longname = prototypeToPunc(longname);
if (typeof forcedMemberof !== 'undefined') {
partsRegExp = new RegExp(`^(.*?)([${sliceChars.join()}]?)$`);
name = longname.substr(forcedMemberof.length);
parts = forcedMemberof.match(partsRegExp);
if (parts[1]) {
memberof = parts[1] || forcedMemberof;
}
if (parts[2]) {
scopePunc = parts[2];
}
}
else if (longname) {
parts = (longname.match(new RegExp(`^(:?(.+)([${sliceChars.join()}]))?(.+?)$`)) || [])
.reverse();
name = parts[0] || '';
scopePunc = parts[1] || '';
memberof = parts[2] || '';
}
// like /** @name foo.bar(2) */
if ( /(.+)\(([^)]+)\)$/.test(name) ) {
name = RegExp.$1;
variation = RegExp.$2;
}
// restore quoted strings
i = tokens.length;
while (i--) {
longname = longname.replace(`@{${i}}@`, tokens[i]);
memberof = memberof.replace(`@{${i}}@`, tokens[i]);
scopePunc = scopePunc.replace(`@{${i}}@`, tokens[i]);
name = name.replace(`@{${i}}@`, tokens[i]);
}
return {
longname: longname,
memberof: memberof,
scope: scopePunc,
name: name,
variation: variation
};
}
// TODO: deprecate exports.shorten in favor of a better name
/**
* Given a longname like "a.b#c(2)", slice it up into an object containing the memberof, the scope,
* the name, and variation.
* @param {string} longname
* @param {string} forcedMemberof
* @returns {object} Representing the properties of the given name.
*/
exports.shorten = (longname, forcedMemberof) => atomize(longname, SCOPE_PUNC, forcedMemberof);
// TODO: docs
exports.combine = ({memberof, scope, name, variation}) => [
(memberof || ''),
(scope || ''),
(name || ''),
(variation || '')
].join('');
// TODO: docs
exports.stripVariation = name => {
const parts = exports.shorten(name);
parts.variation = '';
return exports.combine(parts);
};
function splitLongname(longname, options) {
const chunks = [];
let currentNameInfo;
const nameInfo = {};
let previousName = longname;
const splitters = SCOPE_PUNC.concat('/');
options = _.defaults(options || {}, {
includeVariation: true
});
do {
if (!options.includeVariation) {
previousName = exports.stripVariation(previousName);
}
currentNameInfo = nameInfo[previousName] = atomize(previousName, splitters);
previousName = currentNameInfo.memberof;
chunks.push(currentNameInfo.scope + currentNameInfo.name);
} while (previousName);
return {
chunks: chunks.reverse(),
nameInfo: nameInfo
};
}
/**
* Convert an array of doclet longnames into a tree structure, optionally attaching doclets to the
* tree.
*
* Each level of the tree is an object with the following properties:
*
* + `longname {string}`: The longname.
* + `memberof {string?}`: The memberof.
* + `scope {string?}`: The longname's scope, represented as a punctuation mark (for example, `#`
* for instance and `.` for static).
* + `name {string}`: The short name.
* + `doclet {Object?}`: The doclet associated with the longname, or `null` if the doclet was not
* provided.
* + `children {Object?}`: The children of the current longname. Not present if there are no
* children.
*
* For example, suppose you have the following array of doclet longnames:
*
* ```js
* [
* "module:a",
* "module:a/b",
* "myNamespace",
* "myNamespace.Foo",
* "myNamespace.Foo#bar"
* ]
* ```
*
* This method converts these longnames to the following tree:
*
* ```js
* {
* "module:a": {
* "longname": "module:a",
* "memberof": "",
* "scope": "",
* "name": "module:a",
* "doclet": null,
* "children": {
* "/b": {
* "longname": "module:a/b",
* "memberof": "module:a",
* "scope": "/",
* "name": "b",
* "doclet": null
* }
* }
* },
* "myNamespace": {
* "longname": "myNamespace",
* "memberof": "",
* "scope": "",
* "name": "myNamespace",
* "doclet": null,
* "children": {
* ".Foo": {
* "longname": "myNamespace.Foo",
* "memberof": "myNamespace",
* "scope": ".",
* "name": "Foo",
* "doclet": null,
* "children": {
* "#bar": {
* "longname": "myNamespace.Foo#bar",
* "memberof": "myNamespace.Foo",
* "scope": "#",
* "name": "bar",
* "doclet": null
* }
* }
* }
* }
* }
* }
* ```
*
* @param {Array<string>} longnames - The longnames to convert into a tree.
* @param {Object<string, module:jsdoc/doclet.Doclet>} doclets - The doclets to attach to a tree.
* Each property should be the longname of a doclet, and each value should be the doclet for that
* longname.
* @return {Object} A tree with information about each longname in the format shown above.
*/
exports.longnamesToTree = (longnames, doclets) => {
const splitOptions = { includeVariation: false };
const tree = {};
longnames.forEach(longname => {
let currentLongname = '';
let currentParent = tree;
let nameInfo;
let processed;
// don't try to add empty longnames to the tree
if (!longname) {
return;
}
processed = splitLongname(longname, splitOptions);
nameInfo = processed.nameInfo;
processed.chunks.forEach(chunk => {
currentLongname += chunk;
if (currentParent !== tree) {
currentParent.children = currentParent.children || {};
currentParent = currentParent.children;
}
if (!hasOwnProp.call(currentParent, chunk)) {
currentParent[chunk] = nameInfo[currentLongname];
}
if (currentParent[chunk]) {
currentParent[chunk].doclet = doclets ? doclets[currentLongname] : null;
currentParent = currentParent[chunk];
}
});
});
return tree;
};
/**
* Split a string that starts with a name and ends with a description into its parts. Allows the
* defaultvalue (if present) to contain brackets. If the name is found to have mismatched brackets,
* null is returned.
* @param {string} nameDesc
* @returns {object} Hash with "name" and "description" properties.
*/
function splitNameMatchingBrackets(nameDesc) {
const buffer = [];
let c;
let stack = 0;
let stringEnd = null;
for (var i = 0; i < nameDesc.length; ++i) {
c = nameDesc[i];
buffer.push(c);
if (stringEnd) {
if (c === '\\' && i + 1 < nameDesc.length) {
buffer.push(nameDesc[++i]);
} else if (c === stringEnd) {
stringEnd = null;
}
} else if (c === '"' || c === "'") {
stringEnd = c;
} else if (c === '[') {
++stack;
} else if (c === ']') {
if (--stack === 0) {
break;
}
}
}
if (stack || stringEnd) {
return null;
}
nameDesc.substr(i).match(REGEXP_DESCRIPTION);
return {
name: buffer.join(''),
description: RegExp.$1
};
}
// TODO: deprecate exports.splitName in favor of a better name
/**
* Split a string that starts with a name and ends with a description into its parts.
* @param {string} nameDesc
* @returns {object} Hash with "name" and "description" properties.
*/
exports.splitName = nameDesc => {
// like: name, [name], name text, [name] text, name - text, or [name] - text
// the hyphen must be on the same line as the name; this prevents us from treating a Markdown
// dash as a separator
// optional values get special treatment
let result = null;
if (nameDesc[0] === '[') {
result = splitNameMatchingBrackets(nameDesc);
if (result !== null) {
return result;
}
}
nameDesc.match(REGEXP_NAME_DESCRIPTION);
return {
name: RegExp.$1,
description: RegExp.$2
};
};
+306
View File
@@ -0,0 +1,306 @@
/**
* Parse the command line arguments.
* @module jsdoc/opts/argparser
*/
const _ = require('underscore');
const util = require('util');
const hasOwnProp = Object.prototype.hasOwnProperty;
function padding(length) {
return new Array(length + 1).join(' ');
}
function padLeft(str, length) {
return padding(length) + str;
}
function padRight(str, length) {
return str + padding(length);
}
function findMaxLength(arr) {
let max = 0;
arr.forEach(({length}) => {
if (length > max) {
max = length;
}
});
return max;
}
function concatWithMaxLength(items, maxLength) {
let result = '';
// to prevent endless loops, always use the first item, regardless of length
result += items.shift();
while ( items.length && (result.length + items[0].length < maxLength) ) {
result += ` ${items.shift()}`;
}
return result;
}
// we want to format names and descriptions like this:
// | -f, --foo Very long description very long description very long |
// | description very long description. |
function formatHelpInfo({names, descriptions}) {
const MARGIN_LENGTH = 4;
const results = [];
const maxLength = process.stdout.columns;
const maxNameLength = findMaxLength(names);
const wrapDescriptionAt = maxLength - (MARGIN_LENGTH * 3) - maxNameLength;
// build the string for each option
names.forEach((name, i) => {
let result;
let partialDescription;
let words;
// add a left margin to the name
result = padLeft(names[i], MARGIN_LENGTH);
// and a right margin, with extra padding so the descriptions line up with one another
result = padRight(result, maxNameLength - names[i].length + MARGIN_LENGTH);
// split the description on spaces
words = descriptions[i].split(' ');
// add as much of the description as we can fit on the first line
result += concatWithMaxLength(words, wrapDescriptionAt);
// if there's anything left, keep going until we've consumed the description
while (words.length) {
partialDescription = padding( maxNameLength + (MARGIN_LENGTH * 2) );
partialDescription += concatWithMaxLength(words, wrapDescriptionAt);
result += `\n${partialDescription}`;
}
results.push(result);
});
return results;
}
/**
* A parser to interpret the key-value pairs entered on the command line.
*
* @alias module:jsdoc/opts/argparser
*/
class ArgParser {
/**
* Create an instance of the parser.
*/
constructor() {
this._options = [];
this._shortNameIndex = {};
this._longNameIndex = {};
}
_getOptionByShortName(name) {
if (hasOwnProp.call(this._shortNameIndex, name)) {
return this._options[this._shortNameIndex[name]];
}
return null;
}
_getOptionByLongName(name) {
if (hasOwnProp.call(this._longNameIndex, name)) {
return this._options[this._longNameIndex[name]];
}
return null;
}
_addOption(option) {
let currentIndex;
const longName = option.longName;
const shortName = option.shortName;
this._options.push(option);
currentIndex = this._options.length - 1;
if (shortName) {
this._shortNameIndex[shortName] = currentIndex;
}
if (longName) {
this._longNameIndex[longName] = currentIndex;
}
return this;
}
/**
* Provide information about a legal option.
*
* @param {character} shortName - The short name of the option, entered like: -T.
* @param {string} longName - The equivalent long name of the option, entered like: --test.
* @param {boolean} hasValue - Does this option require a value? Like: -t templatename
* @param {string} helpText - A brief description of the option.
* @param {boolean} [canHaveMultiple=false] - Set to `true` if the option can be provided more
* than once.
* @param {function} [coercer] - A function to coerce the given value to a specific type.
* @return {this}
* @example
* myParser.addOption('t', 'template', true, 'The path to the template.');
* myParser.addOption('h', 'help', false, 'Show the help message.');
*/
addOption(shortName, longName, hasValue, helpText, canHaveMultiple = false, coercer) {
return this._addOption({
shortName: shortName,
longName: longName,
hasValue: hasValue,
helpText: helpText,
canHaveMultiple: canHaveMultiple,
coercer: coercer
});
}
// TODO: refactor addOption to accept objects, then get rid of this method
/**
* Provide information about an option that should not cause an error if present, but that is always
* ignored (for example, an option that was used in previous versions but is no longer supported).
*
* @private
* @param {string} shortName - The short name of the option with a leading hyphen (for example,
* `-v`).
* @param {string} longName - The long name of the option with two leading hyphens (for example,
* `--version`).
*/
addIgnoredOption(shortName, longName) {
return this._addOption({
shortName: shortName,
longName: longName,
ignore: true
});
}
/**
* Generate a summary of all the options with corresponding help text.
* @returns {string}
*/
help() {
const options = {
names: [],
descriptions: []
};
this._options.forEach(option => {
let name = '';
// don't show ignored options
if (option.ignore) {
return;
}
if (option.shortName) {
name += `-${option.shortName}${option.longName ? ', ' : ''}`;
}
if (option.longName) {
name += `--${option.longName}`;
}
if (option.hasValue) {
name += ' <value>';
}
options.names.push(name);
options.descriptions.push(option.helpText);
});
return `Options:\n${formatHelpInfo(options).join('\n')}`;
}
/**
* Get the options.
* @param {Array.<string>} args An array, like ['-x', 'hello']
* @param {Object} [defaults={}] An optional collection of default values.
* @returns {Object} The keys will be the longNames, or the shortName if no longName is defined for
* that option. The values will be the values provided, or `true` if the option accepts no value.
*/
parse(args, defaults) {
let arg;
let next;
let option;
const result = ( defaults && _.defaults({}, defaults) ) || {};
let shortName;
let longName;
let name;
let value;
result._ = [];
for (let i = 0, l = args.length; i < l; i++) {
arg = String(args[i]);
next = (i < l - 1) ? String(args[i + 1]) : null;
shortName = null;
value = null;
// like -t
if (arg.charAt(0) === '-') {
// like --template
if (arg.charAt(1) === '-') {
name = longName = arg.slice(2);
option = this._getOptionByLongName(longName);
}
else {
name = shortName = arg.slice(1);
option = this._getOptionByShortName(shortName);
}
if (option === null) {
throw new Error( util.format('Unknown command-line option "%s".', name) );
}
if (option.hasValue) {
value = next;
i++;
if (value === null || value.charAt(0) === '-') {
throw new Error( util.format('The command-line option "%s" requires a value.', name) );
}
}
else {
value = true;
}
// skip ignored options now that we've consumed the option text
if (option.ignore) {
continue;
}
if (option.longName && shortName) {
name = option.longName;
}
if (typeof option.coercer === 'function') {
value = option.coercer(value);
}
// Allow for multiple options of the same type to be present
if (option.canHaveMultiple && hasOwnProp.call(result, name)) {
const val = result[name];
if (val instanceof Array) {
val.push(value);
} else {
result[name] = [val, value];
}
}
else {
result[name] = value;
}
}
else {
result._.push(arg);
}
}
return result;
}
}
module.exports = ArgParser;
+82
View File
@@ -0,0 +1,82 @@
/**
* @module jsdoc/opts/args
* @requires jsdoc/opts/argparser
*/
const ArgParser = require('jsdoc/opts/argparser');
const cast = require('jsdoc/util/cast').cast;
const querystring = require('querystring');
let ourOptions;
const argParser = new ArgParser();
const hasOwnProp = Object.prototype.hasOwnProperty;
function parseQuery(str) {
return cast( querystring.parse(str) );
}
/* eslint-disable no-multi-spaces */
argParser.addOption('a', 'access', true, 'Only display symbols with the given access: "package", public", "protected", "private" or "undefined", or "all" for all access levels. Default: all except "private"', true);
argParser.addOption('c', 'configure', true, 'The path to the configuration file. Default: path/to/jsdoc/conf.json');
argParser.addOption('d', 'destination', true, 'The path to the output folder. Default: ./out/');
argParser.addOption('', 'debug', false, 'Log information for debugging JSDoc.');
argParser.addOption('e', 'encoding', true, 'Assume this encoding when reading all source files. Default: utf8');
argParser.addOption('h', 'help', false, 'Print this message and quit.');
argParser.addOption('', 'match', true, 'When running tests, only use specs whose names contain <value>.', true);
argParser.addOption('', 'nocolor', false, 'When running tests, do not use color in console output.');
argParser.addOption('p', 'private', false, 'Display symbols marked with the @private tag. Equivalent to "--access all". Default: false');
argParser.addOption('P', 'package', true, 'The path to the project\'s package file. Default: path/to/sourcefiles/package.json');
argParser.addOption('', 'pedantic', false, 'Treat errors as fatal errors, and treat warnings as errors. Default: false');
argParser.addOption('q', 'query', true, 'A query string to parse and store in jsdoc.env.opts.query. Example: foo=bar&baz=true', false, parseQuery);
argParser.addOption('r', 'recurse', false, 'Recurse into subdirectories when scanning for source files and tutorials.');
argParser.addOption('R', 'readme', true, 'The path to the project\'s README file. Default: path/to/sourcefiles/README.md');
argParser.addOption('t', 'template', true, 'The path to the template to use. Default: path/to/jsdoc/templates/default');
argParser.addOption('T', 'test', false, 'Run all tests and quit.');
argParser.addOption('u', 'tutorials', true, 'Directory in which JSDoc should search for tutorials.');
argParser.addOption('v', 'version', false, 'Display the version number and quit.');
argParser.addOption('', 'verbose', false, 'Log detailed information to the console as JSDoc runs.');
argParser.addOption('X', 'explain', false, 'Dump all found doclet internals to console and quit.');
/* eslint-enable no-multi-spaces */
// Options that are no longer supported and should be ignored
argParser.addIgnoredOption('l', 'lenient'); // removed in JSDoc 3.3.0
/**
* Set the options for this app.
* @throws {Error} Illegal arguments will throw errors.
* @param {string|String[]} args The command line arguments for this app.
*/
exports.parse = (args = []) => {
if (typeof args === 'string' || args.constructor === String) {
args = String(args).split(/\s+/g);
}
ourOptions = argParser.parse(args);
return ourOptions;
};
/**
* Retrieve help message for options.
*/
exports.help = () => argParser.help();
/**
* Get a named option.
* @variation name
* @param {string} name The name of the option.
* @return {string} The value associated with the given name.
*//**
* Get all the options for this app.
* @return {Object} A collection of key/values representing all the options.
*/
exports.get = name => {
if (typeof name === 'undefined') {
return ourOptions;
}
else if ( hasOwnProp.call(ourOptions, name) ) {
return ourOptions[name];
}
return undefined;
};
+257
View File
@@ -0,0 +1,257 @@
const logger = require('jsdoc/util/logger');
const stripBom = require('jsdoc/util/stripbom');
/**
* Provides access to information about a JavaScript package.
*
* @module jsdoc/package
* @see https://www.npmjs.org/doc/files/package.json.html
*/
// Collect all of the license information from a `package.json` file.
function getLicenses(packageInfo) {
const licenses = packageInfo.licenses ? packageInfo.licenses.slice(0) : [];
if (packageInfo.license) {
licenses.push({ type: packageInfo.license });
}
return licenses;
}
/**
* Information about where to report bugs in the package.
*
* @typedef {Object} module:jsdoc/package.Package~BugInfo
* @property {string} email - The email address for reporting bugs.
* @property {string} url - The URL for reporting bugs.
*/
/**
* Information about a package's software license.
*
* @typedef {Object} module:jsdoc/package.Package~LicenseInfo
* @property {string} type - An identifier for the type of license.
* @property {string} url - The URL for the complete text of the license.
*/
/**
* Information about a package author or contributor.
*
* @typedef {Object} module:jsdoc/package.Package~PersonInfo
* @property {string} name - The person's full name.
* @property {string} email - The person's email address.
* @property {string} url - The URL of the person's website.
*/
/**
* Information about a package's version-control repository.
*
* @typedef {Object} module:jsdoc/package.Package~RepositoryInfo
* @property {string} type - The type of version-control system that the repository uses (for
* example, `git` or `svn`).
* @property {string} url - The URL for the repository.
*/
/**
* Information about a JavaScript package. JSDoc can extract package information from
* `package.json` files that follow the
* [npm specification](https://www.npmjs.org/doc/files/package.json.html).
*
* **Note**: JSDoc does not validate or normalize the contents of `package.json` files. If your
* `package.json` file does not follow the npm specification, some properties of the `Package`
* object may not use the format documented here.
*/
class Package {
/**
* @param {string} json - The contents of the `package.json` file.
*/
constructor(json) {
let packageInfo;
/**
* The string identifier that is shared by all `Package` objects.
*
* @readonly
* @default
* @type {string}
*/
this.kind = 'package';
try {
packageInfo = JSON.parse(stripBom.strip(json) || '{}');
}
catch (e) {
logger.error('Unable to parse the package file: %s', e.message);
packageInfo = {};
}
if (packageInfo.name) {
/**
* The package name.
*
* @type {string}
*/
this.name = packageInfo.name;
}
/**
* The unique longname for this `Package` object.
*
* @type {string}
*/
this.longname = `${this.kind}:${this.name}`;
if (packageInfo.author) {
/**
* The author of this package. Contains either a
* {@link module:jsdoc/package.Package~PersonInfo PersonInfo} object or a string with
* information about the author.
*
* @type {(module:jsdoc/package.Package~PersonInfo|string)}
* @since 3.3.0
*/
this.author = packageInfo.author;
}
if (packageInfo.bugs) {
/**
* Information about where to report bugs in the project. May contain a URL, a string, or an
* object with more detailed information.
*
* @type {(string|module:jsdoc/package.Package~BugInfo)}
* @since 3.3.0
*/
this.bugs = packageInfo.bugs;
}
if (packageInfo.contributors) {
/**
* The contributors to this package.
*
* @type {Array.<(module:jsdoc/package.Package~PersonInfo|string)>}
* @since 3.3.0
*/
this.contributors = packageInfo.contributors;
}
if (packageInfo.dependencies) {
/**
* The dependencies for this package.
*
* @type {Object}
* @since 3.3.0
*/
this.dependencies = packageInfo.dependencies;
}
if (packageInfo.description) {
/**
* A brief description of the package.
*
* @type {string}
*/
this.description = packageInfo.description;
}
if (packageInfo.devDependencies) {
/**
* The development dependencies for this package.
*
* @type {Object}
* @since 3.3.0
*/
this.devDependencies = packageInfo.devDependencies;
}
if (packageInfo.engines) {
/**
* The JavaScript engines that this package supports. Each key is a string that identifies
* the engine (for example, `node`). Each value is a
* [semver](https://www.npmjs.org/doc/misc/semver.html)-compliant version number for the
* engine.
*
* @type {Object}
* @since 3.3.0
*/
this.engines = packageInfo.engines;
}
/**
* The source files associated with the package.
*
* New `Package` objects always contain an empty array, regardless of whether the `package.json`
* file includes a `files` property.
*
* After JSDoc parses your input files, it sets this property to a list of paths to your input
* files.
*
* @type {Array.<string>}
*/
this.files = [];
if (packageInfo.homepage) {
/**
* The URL for the package's homepage.
*
* @type {string}
* @since 3.3.0
*/
this.homepage = packageInfo.homepage;
}
if (packageInfo.keywords) {
/**
* Keywords to help users find the package.
*
* @type {Array.<string>}
* @since 3.3.0
*/
this.keywords = packageInfo.keywords;
}
if (packageInfo.license || packageInfo.licenses) {
/**
* The licenses used by this package. Combines information from the `package.json` file's
* `license` property and the deprecated `licenses` property.
*
* @type {Array.<module:jsdoc/package.Package~LicenseInfo>}
*/
this.licenses = getLicenses(packageInfo);
}
if (packageInfo.main) {
/**
* The module ID that provides the primary entry point to the package. For example, if your
* package is a CommonJS module, and the value of this property is `foo`, users should be
* able to load your module with `require('foo')`.
*
* @type {string}
* @since 3.3.0
*/
this.main = packageInfo.main;
}
if (packageInfo.repository) {
/**
* The version-control repository for the package.
*
* @type {module:jsdoc/package.Package~RepositoryInfo}
* @since 3.3.0
*/
this.repository = packageInfo.repository;
}
if (packageInfo.version) {
/**
* The [semver](https://www.npmjs.org/doc/misc/semver.html)-compliant version number of the
* package.
*
* @type {string}
* @since 3.2.0
*/
this.version = packageInfo.version;
}
}
}
exports.Package = Package;
+169
View File
@@ -0,0 +1,169 @@
/**
* Extended version of the standard `path` module.
* @module jsdoc/path
*/
const env = require('jsdoc/env');
const fs = require('fs');
const path = require('path');
function prefixReducer(previousPath, current) {
let currentPath = [];
// if previousPath is defined, but has zero length, there's no common prefix; move along
if (previousPath && !previousPath.length) {
return currentPath;
}
currentPath = path.resolve(env.pwd, current).split(path.sep) || [];
if (previousPath && currentPath.length) {
// remove chunks that exceed the previous path's length
currentPath = currentPath.slice(0, previousPath.length);
// if a chunk doesn't match the previous path, remove everything from that chunk on
for (let i = 0, l = currentPath.length; i < l; i++) {
if (currentPath[i] !== previousPath[i]) {
currentPath.splice(i, currentPath.length - i);
break;
}
}
}
return currentPath;
}
/**
* Find the common prefix for an array of paths. If there is a common prefix, a trailing separator
* is appended to the prefix. Relative paths are resolved relative to the current working directory.
*
* For example, assuming that the current working directory is `/Users/jsdoc`:
*
* + For the single path `foo/bar/baz/qux.js`, the common prefix is `foo/bar/baz/`.
* + For paths `foo/bar/baz/qux.js`, `foo/bar/baz/quux.js`, and `foo/bar/baz.js`, the common prefix
* is `/Users/jsdoc/foo/bar/`.
* + For paths `../jsdoc/foo/bar/baz/qux/quux/test.js`, `/Users/jsdoc/foo/bar/bazzy.js`, and
* `../../Users/jsdoc/foo/bar/foobar.js`, the common prefix is `/Users/jsdoc/foo/bar/`.
* + For paths `foo/bar/baz/qux.js` and `../../Library/foo/bar/baz.js`, there is no common prefix,
* and an empty string is returned.
*
* @param {Array.<string>} paths - The paths to search for a common prefix.
* @return {string} The common prefix, or an empty string if there is no common prefix.
*/
exports.commonPrefix = (paths = []) => {
let prefix = '';
let segments;
// if there's only one path, its resolved dirname (plus a trailing slash) is the common prefix
if (paths.length === 1) {
prefix = path.resolve(env.pwd, paths[0]);
if ( path.extname(prefix) ) {
prefix = path.dirname(prefix);
}
prefix += path.sep;
}
else {
segments = paths.reduce(prefixReducer, undefined) || [];
// if there's anything left (other than a placeholder for a leading slash), add a
// placeholder for a trailing slash
if ( segments.length && (segments.length > 1 || segments[0] !== '') ) {
segments.push('');
}
prefix = segments.join(path.sep);
}
return prefix;
};
/**
* Retrieve the fully qualified path to the requested resource.
*
* If the resource path is specified as a relative path, JSDoc searches for the resource in the
* following locations, in this order:
*
* 1. The current working directory
* 2. The directory where the JSDoc configuration file is located
* 3. The JSDoc directory
* 4. Anyplace where `require()` can find the resource (for example, in your project's
* `node_modules` directory)
*
* If the resource path is specified as a fully qualified path, JSDoc searches for the resource in
* the following locations, in this order:
*
* 1. The resource path
* 2. Anyplace where `require()` can find the resource (for example, in your project's
* `node_modules` directory)
*
* @param {string} filepath - The path to the requested resource. May be an absolute path; a path
* relative to the JSDoc directory; or a path relative to the current working directory.
* @param {string} [filename] - The filename of the requested resource.
* @return {string} The fully qualified path to the requested resource. Includes the filename if one
* was provided.
*/
exports.getResourcePath = (filepath, filename) => {
let result = null;
const searchDirs = [env.pwd, path.dirname(env.opts.configure || ''), env.dirname];
function exists(p) {
try {
fs.statSync(p);
return true;
}
catch (e) {
return false;
}
}
function resolve(p) {
try {
return require.resolve(p);
}
catch (e) {
return null;
}
}
function find(p) {
// does the requested path exist?
if ( exists(p) ) {
result = p;
}
else {
// can `require()` find the requested path?
result = resolve(p);
}
return Boolean(result);
}
filepath = path.join(filepath, filename || '');
// is the filepath absolute? if so, just use it
if ( path.isAbsolute(filepath) ) {
find(filepath);
}
else {
searchDirs.some(searchDir => {
if (searchDir) {
return find( path.resolve(path.join(searchDir, filepath)) );
}
else {
return false;
}
});
}
// if we still haven't found the resource, maybe it's an installed module
if (!result) {
result = resolve(filepath);
}
return result;
};
Object.keys(path).forEach(member => {
exports[member] = path[member];
});
+35
View File
@@ -0,0 +1,35 @@
/**
* Utility functions to support the JSDoc plugin framework.
* @module jsdoc/plugins
*/
const dictionary = require('jsdoc/tag/dictionary');
function addHandlers(handlers, parser) {
Object.keys(handlers).forEach(eventName => {
parser.on(eventName, handlers[eventName]);
});
}
exports.installPlugins = (plugins, parser) => {
let plugin;
for (let pluginModule of plugins) {
plugin = require(pluginModule);
// allow user-defined plugins to...
// ...register event handlers
if (plugin.handlers) {
addHandlers(plugin.handlers, parser);
}
// ...define tags
if (plugin.defineTags) {
plugin.defineTags(dictionary);
}
// ...add a Mozilla Parser API node visitor
if (plugin.astNodeVisitor) {
parser.addAstNodeVisitor(plugin.astNodeVisitor);
}
}
};
+24
View File
@@ -0,0 +1,24 @@
/**
* Make the contents of a README file available to include in the output.
* @module jsdoc/readme
*/
const env = require('jsdoc/env');
const fs = require('jsdoc/fs');
const markdown = require('jsdoc/util/markdown');
/**
* Represents a README file.
*/
class ReadMe {
/**
* @param {string} path - The filepath to the README.
*/
constructor(path) {
const content = fs.readFileSync(path, env.opts.encoding);
const parse = markdown.getParser();
this.html = parse(content);
}
}
module.exports = ReadMe;
+664
View File
@@ -0,0 +1,664 @@
/**
* Schema for validating JSDoc doclets.
* @module jsdoc/schema
* @see <https://trac.tools.ietf.org/html/draft-wright-json-schema-validation-01>
*/
// JSON schema types
const ARRAY = 'array';
const BOOLEAN = 'boolean';
const NULL = 'null';
const NUMBER = 'number';
const OBJECT = 'object';
const STRING = 'string';
const BOOLEAN_OPTIONAL = [BOOLEAN, NULL];
const STRING_OPTIONAL = [STRING, NULL];
const EVENT_REGEXP = 'event:[\\S]+';
const PACKAGE_REGEXP = 'package:[\\S]+';
const STRING_SCHEMA = {
type: STRING
};
// information about the code associated with a doclet
const META_SCHEMA = exports.META_SCHEMA = {
type: OBJECT,
additionalProperties: false,
properties: {
code: {
type: OBJECT,
additionalProperties: false,
properties: {
funcscope: {
type: STRING
},
id: {
type: STRING
},
name: {},
node: {
type: OBJECT
},
paramnames: {
type: ARRAY,
uniqueItems: true,
items: {
type: STRING
}
},
type: {
type: STRING
},
value: {}
}
},
columnno: {
title: 'The column number of the code associated with this doclet.',
type: NUMBER
},
filename: {
title: 'The name of the file that contains the code associated with this doclet.',
type: STRING
},
lineno: {
title: 'The line number of the code associated with this doclet.',
type: NUMBER
},
path: {
title: 'The path in which the code associated with this doclet is located.',
type: STRING
},
range: {
title: 'The positions of the first and last characters of the code associated with ' +
'this doclet.',
type: ARRAY,
minItems: 2,
maxItems: 2,
items: {
type: NUMBER
}
},
vars: {
type: OBJECT
}
}
};
// type property containing type names
const TYPE_PROPERTY_SCHEMA = exports.TYPE_PROPERTY_SCHEMA = {
type: OBJECT,
additionalProperties: false,
properties: {
names: {
type: ARRAY,
minItems: 1,
items: {
type: STRING
}
},
// type parser output
parsedType: {
type: OBJECT,
additionalProperties: true
}
}
};
// enumeration properties
const ENUM_PROPERTY_SCHEMA = exports.ENUM_PROPERTY_SCHEMA = {
type: OBJECT,
additionalProperties: false,
properties: {
comment: {
type: STRING
},
defaultvalue: {},
description: {
type: STRING_OPTIONAL
},
kind: {
type: STRING,
enum: ['member']
},
longname: {
type: STRING
},
memberof: {
type: STRING
},
meta: META_SCHEMA,
name: {
type: STRING
},
// is this member nullable? (derived from the type expression)
nullable: {
type: BOOLEAN_OPTIONAL
},
// is this member optional? (derived from the type expression)
optional: {
type: BOOLEAN_OPTIONAL
},
scope: {
type: STRING,
enum: ['static']
},
type: TYPE_PROPERTY_SCHEMA,
// can this member be provided more than once? (derived from the type expression)
variable: {
type: BOOLEAN_OPTIONAL
}
}
};
// function parameter, or object property defined with @property tag
const PARAM_SCHEMA = exports.PARAM_SCHEMA = {
type: OBJECT,
additionalProperties: false,
properties: {
// what is the default value for this parameter?
defaultvalue: {},
// a description of the parameter
description: {
type: STRING_OPTIONAL
},
// what name does this parameter have within the function?
name: {
type: STRING
},
// can the value for this parameter be null?
nullable: {
type: BOOLEAN_OPTIONAL
},
// is a value for this parameter optional?
optional: {
type: BOOLEAN_OPTIONAL
},
// what are the types of value expected for this parameter?
type: TYPE_PROPERTY_SCHEMA,
// can this parameter be repeated?
variable: {
type: BOOLEAN_OPTIONAL
}
}
};
const DOCLET_SCHEMA = exports.DOCLET_SCHEMA = {
type: OBJECT,
additionalProperties: false,
properties: {
// what access privileges are allowed
access: {
type: STRING,
enum: [
'package',
'private',
'protected',
'public'
]
},
alias: {
type: STRING
},
async: {
type: BOOLEAN
},
augments: {
type: ARRAY,
uniqueItems: true,
items: {
type: STRING
}
},
author: {
type: ARRAY,
items: {
type: STRING
}
},
borrowed: {
type: ARRAY,
uniqueItems: true,
items: {
type: OBJECT,
additionalProperties: false,
properties: {
// name of the target
as: {
type: STRING
},
// name of the source
from: {
type: STRING
}
}
}
},
// a description of the class that this constructor belongs to
classdesc: {
type: STRING
},
comment: {
type: STRING
},
copyright: {
type: STRING
},
defaultvalue: {},
defaultvaluetype: {
type: STRING,
enum: [OBJECT, ARRAY]
},
// is usage of this symbol deprecated?
deprecated: {
type: [STRING, BOOLEAN]
},
// a description
description: {
type: STRING_OPTIONAL
},
// something else to consider
examples: {
type: ARRAY,
items: {
type: STRING
}
},
exceptions: {
type: ARRAY,
items: PARAM_SCHEMA
},
// the path to another constructor
extends: {
type: ARRAY,
uniqueItems: true,
items: {
type: STRING
}
},
// the path to another doc object
fires: {
type: ARRAY,
uniqueItems: true,
items: {
type: STRING,
pattern: EVENT_REGEXP
}
},
forceMemberof: {
type: BOOLEAN_OPTIONAL
},
generator: {
type: BOOLEAN
},
hideconstructor: {
type: BOOLEAN
},
ignore: {
type: BOOLEAN
},
implementations: {
type: ARRAY,
items: {
type: STRING
}
},
implements: {
type: ARRAY,
items: {
type: STRING
}
},
inheritdoc: {
type: STRING
},
inherited: {
type: BOOLEAN
},
inherits: {
type: STRING,
dependency: {
inherited: true
}
},
isEnum: {
type: BOOLEAN
},
// what kind of symbol is this?
kind: {
type: STRING,
enum: [
'class',
'constant',
'event',
'external',
'file',
'function',
'interface',
'member',
'mixin',
'module',
'namespace',
'package',
'param',
'typedef'
]
},
license: {
type: STRING
},
listens: {
type: ARRAY,
uniqueItems: true,
items: {
type: STRING,
pattern: EVENT_REGEXP
}
},
longname: {
type: STRING
},
// probably a leading substring of the path
memberof: {
type: STRING
},
// information about this doc
meta: META_SCHEMA,
// was this doclet mixed in?
mixed: {
type: BOOLEAN
},
mixes: {
type: ARRAY,
uniqueItems: true,
items: {
type: STRING
}
},
modifies: {
type: ARRAY,
uniqueItems: true,
items: PARAM_SCHEMA
},
// probably a trailing substring of the path
name: {
type: STRING
},
// is this member nullable? (derived from the type expression)
nullable: {
type: BOOLEAN_OPTIONAL
},
// is this member optional? (derived from the type expression)
optional: {
type: BOOLEAN_OPTIONAL
},
// does this member explicitly override the parent?
override: {
type: BOOLEAN
},
overrides: {
type: STRING
},
// are there function parameters associated with this doc?
params: {
type: ARRAY,
uniqueItems: true,
items: PARAM_SCHEMA
},
preserveName: {
type: BOOLEAN
},
properties: {
type: ARRAY,
uniqueItems: true,
minItems: 1,
items: {
anyOf: [ENUM_PROPERTY_SCHEMA, PARAM_SCHEMA]
}
},
readonly: {
type: BOOLEAN
},
// the symbol being documented requires another symbol
requires: {
type: ARRAY,
uniqueItems: true,
minItems: 1,
items: {
type: STRING
}
},
returns: {
type: ARRAY,
minItems: 1,
items: PARAM_SCHEMA
},
// what sort of parent scope does this symbol have?
scope: {
type: STRING,
enum: [
'global',
'inner',
'instance',
'static'
]
},
// something else to consider
see: {
type: ARRAY,
minItems: 1,
items: {
type: STRING
}
},
// at what previous version was this doc added?
since: {
type: STRING
},
summary: {
type: STRING
},
// arbitrary tags associated with this doc
tags: {
type: ARRAY,
minItems: 1,
items: {
type: OBJECT,
additionalProperties: false,
properties: {
originalTitle: {
type: STRING
},
text: {
type: STRING
},
title: {
type: STRING
},
value: {
oneOf: [STRING_SCHEMA, PARAM_SCHEMA]
}
}
}
},
'this': {
type: STRING
},
todo: {
type: ARRAY,
minItems: 1,
items: {
type: STRING
}
},
// extended tutorials
tutorials: {
type: ARRAY,
minItems: 1,
items: {
type: STRING
}
},
// what type is the value that this doc is associated with, like `number`
type: TYPE_PROPERTY_SCHEMA,
undocumented: {
type: BOOLEAN
},
// can this member be provided more than once? (derived from the type expression)
variable: {
type: BOOLEAN_OPTIONAL
},
variation: {
type: STRING
},
// what is the version of this doc
version: {
type: STRING
},
// is a member left to be implemented during inheritance?
virtual: {
type: BOOLEAN
},
yields: {
type: ARRAY,
minItems: 1,
items: PARAM_SCHEMA
}
}
};
const CONTACT_INFO_SCHEMA = exports.CONTACT_INFO_SCHEMA = {
type: OBJECT,
additionalProperties: false,
properties: {
email: {
type: STRING
},
name: {
type: STRING
},
url: {
type: STRING,
format: 'uri'
}
}
};
const BUGS_SCHEMA = exports.BUGS_SCHEMA = {
type: OBJECT,
additionalProperties: false,
properties: {
email: {
type: STRING
},
url: {
type: STRING,
format: 'uri'
}
}
};
const PACKAGE_SCHEMA = exports.PACKAGE_SCHEMA = {
type: OBJECT,
additionalProperties: false,
properties: {
author: {
anyOf: [STRING_SCHEMA, CONTACT_INFO_SCHEMA]
},
bugs: {
anyOf: [STRING_SCHEMA, BUGS_SCHEMA]
},
contributors: {
type: ARRAY,
minItems: 0,
items: {
anyOf: [STRING_SCHEMA, CONTACT_INFO_SCHEMA]
}
},
dependencies: {
type: OBJECT
},
description: {
type: STRING
},
devDependencies: {
type: OBJECT
},
engines: {
type: OBJECT
},
files: {
type: ARRAY,
uniqueItems: true,
minItems: 0,
items: {
type: STRING
}
},
homepage: {
type: STRING,
format: 'uri'
},
keywords: {
type: ARRAY,
minItems: 0,
items: {
type: STRING
}
},
kind: {
type: STRING,
enum: ['package']
},
licenses: {
type: ARRAY,
minItems: 1,
items: {
type: OBJECT,
additionalProperties: false,
properties: {
type: {
type: STRING
},
url: {
type: STRING,
format: 'uri'
}
}
}
},
longname: {
type: STRING,
pattern: PACKAGE_REGEXP
},
main: {
type: STRING
},
name: {
type: STRING
},
repository: {
type: OBJECT,
additionalProperties: false,
properties: {
type: {
type: STRING
},
// we don't use `format: 'uri'` here because repo URLs are atypical
url: {
type: STRING
}
}
},
version: {
type: STRING
}
}
};
exports.DOCLETS_SCHEMA = {
type: ARRAY,
items: {
anyOf: [DOCLET_SCHEMA, PACKAGE_SCHEMA]
}
};
+72
View File
@@ -0,0 +1,72 @@
const babelParser = require('@babel/parser');
const env = require('jsdoc/env');
const logger = require('jsdoc/util/logger');
// exported so we can use them in tests
const parserOptions = exports.parserOptions = {
allowAwaitOutsideFunction: true,
allowImportExportEverywhere: true,
allowReturnOutsideFunction: true,
allowSuperOutsideMethod: true,
allowUndeclaredExports: true,
plugins: [
'asyncDoExpressions',
'decoratorAutoAccessors',
['decorators', {
version: '2022-03'
}],
'decimal',
'destructuringPrivate',
'doExpressions',
'estree',
'explicitResourceManagement',
'exportDefaultFrom',
'functionBind',
'functionSent',
'importAssertions',
'importMeta',
'importReflection',
'jsx',
'moduleBlocks',
'partialApplication',
['pipelineOperator', {
proposal: 'hack',
topicToken: '^^'
}],
'recordAndTuple',
'regexpUnicodeSets',
'throwExpressions'
],
ranges: true,
sourceType: env.conf.sourceType
};
function parse(source, filename) {
let ast;
try {
ast = babelParser.parse(source, parserOptions);
// console.log(JSON.stringify(ast, null, 2));
}
catch (e) {
logger.error('Unable to parse %s: %s', filename, e.message);
}
return ast;
}
// TODO: docs
class AstBuilder {
// TODO: docs
/* eslint-disable no-empty-function */
constructor() {}
/* eslint-enable no-empty-function */
// TODO: docs
/* eslint-disable class-methods-use-this */
build(source, filename) {
return parse(source, filename);
}
/* eslint-enable class-methods-use-this */
}
exports.AstBuilder = AstBuilder;
+555
View File
@@ -0,0 +1,555 @@
// TODO: docs
/** @module jsdoc/src/astnode */
const cast = require('jsdoc/util/cast').cast;
const env = require('jsdoc/env');
const name = require('jsdoc/name');
const Syntax = require('jsdoc/src/syntax').Syntax;
const util = require('util');
// Counter for generating unique node IDs.
let uid = 100000000;
/**
* Check whether an AST node represents a function.
*
* @alias module:jsdoc/src/astnode.isFunction
* @param {(Object|string)} node - The AST node to check, or the `type` property of a node.
* @return {boolean} Set to `true` if the node is a function or `false` in all other cases.
*/
const isFunction = exports.isFunction = node => {
let type;
if (!node) {
return false;
}
if (typeof node === 'string') {
type = node;
}
else {
type = node.type;
}
return type === Syntax.FunctionDeclaration || type === Syntax.FunctionExpression ||
type === Syntax.MethodDefinition || type === Syntax.ArrowFunctionExpression;
};
/**
* Check whether an AST node creates a new scope.
*
* @alias module:jsdoc/src/astnode.isScope
* @param {Object} node - The AST node to check.
* @return {Boolean} Set to `true` if the node creates a new scope, or `false` in all other cases.
*/
exports.isScope = node => // TODO: handle blocks with "let" declarations
Boolean(node) && typeof node === 'object' && (node.type === Syntax.CatchClause ||
node.type === Syntax.ClassDeclaration || node.type === Syntax.ClassExpression || isFunction(node));
// TODO: docs
exports.addNodeProperties = node => {
const debugEnabled = Boolean(env.opts.debug);
const newProperties = {};
if (!node || typeof node !== 'object') {
return null;
}
if (!node.nodeId) {
newProperties.nodeId = {
value: `astnode${uid++}`,
enumerable: debugEnabled
};
}
if (!node.parent && node.parent !== null) {
newProperties.parent = {
// `null` means 'no parent', so use `undefined` for now
value: undefined,
writable: true
};
}
if (!node.enclosingScope && node.enclosingScope !== null) {
newProperties.enclosingScope = {
// `null` means 'no enclosing scope', so use `undefined` for now
value: undefined,
writable: true
};
}
if (debugEnabled && typeof node.parentId === 'undefined') {
newProperties.parentId = {
enumerable: true,
get() {
return this.parent ? this.parent.nodeId : null;
}
};
}
if (debugEnabled && typeof node.enclosingScopeId === 'undefined') {
newProperties.enclosingScopeId = {
enumerable: true,
get() {
return this.enclosingScope ? this.enclosingScope.nodeId : null;
}
};
}
Object.defineProperties(node, newProperties);
return node;
};
// TODO: docs
const nodeToValue = exports.nodeToValue = node => {
let key;
let parent;
let str;
let tempObject;
switch (node.type) {
case Syntax.ArrayExpression:
tempObject = [];
node.elements.forEach((el, i) => {
// handle sparse arrays. use `null` to represent missing values, consistent with
// JSON.stringify([,]).
if (!el) {
tempObject[i] = null;
}
else {
tempObject[i] = nodeToValue(el);
}
});
str = JSON.stringify(tempObject);
break;
case Syntax.AssignmentExpression:
// falls through
case Syntax.AssignmentPattern:
str = nodeToValue(node.left);
break;
case Syntax.BigIntLiteral:
str = node.value;
break;
case Syntax.ClassDeclaration:
str = nodeToValue(node.id);
break;
case Syntax.ClassPrivateProperty:
// TODO: Strictly speaking, the name should be '#' plus node.key, but because we
// already use '#' as scope punctuation, that causes JSDoc to get extremely confused.
// The solution probably involves quoting part or all of the name, but JSDoc doesn't
// deal with quoted names very nicely right now, and most people probably won't want to
// document class private properties anyhow. So for now, we'll just cheat and omit the
// leading '#'.
str = nodeToValue(node.key.id);
break;
case Syntax.ClassProperty:
str = nodeToValue(node.key);
break;
case Syntax.ExportAllDeclaration:
// falls through
case Syntax.ExportDefaultDeclaration:
str = 'module.exports';
break;
case Syntax.ExportNamedDeclaration:
if (node.declaration) {
// like `var` in: export var foo = 'bar';
// we need a single value, so we use the first variable name
if (node.declaration.declarations) {
str = `exports.${nodeToValue(node.declaration.declarations[0])}`;
}
else {
str = `exports.${nodeToValue(node.declaration)}`;
}
}
// otherwise we'll use the ExportSpecifier nodes
break;
case Syntax.ExportSpecifier:
str = `exports.${nodeToValue(node.exported)}`;
break;
case Syntax.ArrowFunctionExpression:
// falls through
case Syntax.FunctionDeclaration:
// falls through
case Syntax.FunctionExpression:
if (node.id && node.id.name) {
str = node.id.name;
}
break;
case Syntax.Identifier:
str = node.name;
break;
case Syntax.Literal:
str = node.value;
break;
case Syntax.MemberExpression:
// could be computed (like foo['bar']) or not (like foo.bar)
str = nodeToValue(node.object);
if (node.computed) {
str += util.format('[%s]', node.property.raw);
}
else {
str += `.${nodeToValue(node.property)}`;
}
break;
case Syntax.MethodDefinition:
parent = node.parent.parent;
// for class expressions, we want the name of the variable the class is assigned to
// (but there won't be a name if the class is returned by an arrow function expression)
// TODO: we should use `name.LONGNAMES.ANONYMOUS` instead of an empty string, but that
// causes problems downstream if the parent class has an `@alias` tag
if (parent.type === Syntax.ClassExpression) {
str = nodeToValue(parent.parent) || '';
}
// for the constructor of a module's default export, use a special name
else if (node.kind === 'constructor' && parent.parent &&
parent.parent.type === Syntax.ExportDefaultDeclaration) {
str = 'module.exports';
}
// for the constructor of a module's named export, use the name of the export
// declaration
else if (node.kind === 'constructor' && parent.parent &&
parent.parent.type === Syntax.ExportNamedDeclaration) {
str = nodeToValue(parent.parent);
}
// for other constructors, use the name of the parent class
else if (node.kind === 'constructor') {
str = nodeToValue(parent);
}
// if the method is a member of a module's default export, ignore the name, because it's
// irrelevant
else if (parent.parent && parent.parent.type === Syntax.ExportDefaultDeclaration) {
str = '';
}
// otherwise, use the class's name
else {
str = parent.id ? nodeToValue(parent.id) : '';
}
if (node.kind !== 'constructor') {
if (str) {
str += node.static ? name.SCOPE.PUNC.STATIC : name.SCOPE.PUNC.INSTANCE;
}
str += nodeToValue(node.key);
}
break;
case Syntax.ObjectExpression:
tempObject = {};
node.properties.forEach(prop => {
// ExperimentalSpreadProperty have no key
// like var hello = {...hi};
if (!prop.key) {
return;
}
key = prop.key.name;
// preserve literal values so that the JSON form shows the correct type
if (prop.value.type === Syntax.Literal) {
tempObject[key] = prop.value.value;
}
else {
tempObject[key] = nodeToValue(prop);
}
});
str = JSON.stringify(tempObject);
break;
case Syntax.RestElement:
str = nodeToValue(node.argument);
break;
case Syntax.ThisExpression:
str = 'this';
break;
case Syntax.UnaryExpression:
// like -1. in theory, operator can be prefix or postfix. in practice, any value with a
// valid postfix operator (such as -- or ++) is not a UnaryExpression.
str = nodeToValue(node.argument);
if (node.prefix === true) {
str = cast(node.operator + str);
}
else {
// this shouldn't happen
throw new Error( util.format('Found a UnaryExpression with a postfix operator: %j',
node) );
}
break;
case Syntax.VariableDeclarator:
str = nodeToValue(node.id);
break;
default:
str = '';
}
return str;
};
// backwards compatibility
exports.nodeToString = nodeToValue;
// TODO: docs
const getParamNames = exports.getParamNames = node => {
let params;
if (!node || !node.params) {
return [];
}
params = node.params.slice(0);
return params.map(param => nodeToValue(param));
};
// TODO: docs
const isAccessor = exports.isAccessor = node => Boolean(node) && typeof node === 'object' &&
(node.type === Syntax.Property || node.type === Syntax.MethodDefinition) &&
(node.kind === 'get' || node.kind === 'set');
// TODO: docs
exports.isAssignment = node => Boolean(node) && typeof node === 'object' &&
(node.type === Syntax.AssignmentExpression || node.type === Syntax.VariableDeclarator);
// TODO: docs
/**
* Retrieve information about the node, including its name and type.
*/
exports.getInfo = node => {
const info = {};
switch (node.type) {
// like the function in: "var foo = () => {}"
case Syntax.ArrowFunctionExpression:
info.node = node;
info.name = '';
info.type = info.node.type;
info.paramnames = getParamNames(node);
break;
// like: "foo = 'bar'" (after declaring foo)
// like: "MyClass.prototype.myMethod = function() {}" (after declaring MyClass)
case Syntax.AssignmentExpression:
info.node = node.right;
info.name = nodeToValue(node.left);
info.type = info.node.type;
info.value = nodeToValue(info.node);
// if the assigned value is a function, we need to capture the parameter names here
info.paramnames = getParamNames(node.right);
break;
// like "bar='baz'" in: function foo(bar='baz') {}
case Syntax.AssignmentPattern:
info.node = node;
info.name = nodeToValue(node.left);
info.type = info.node.type;
info.value = nodeToValue(info.node);
break;
// like: "class Foo {}"
// or "class" in: "export default class {}"
case Syntax.ClassDeclaration:
info.node = node;
// if this class is the default export, we need to use a special name
if (node.parent && node.parent.type === Syntax.ExportDefaultDeclaration) {
info.name = 'module.exports';
}
else {
info.name = node.id ? nodeToValue(node.id) : '';
}
info.type = info.node.type;
info.paramnames = [];
node.body.body.some(({kind, value}) => {
if (kind === 'constructor') {
info.paramnames = getParamNames(value);
return true;
}
return false;
});
break;
// like "#b = 1;" in: "class A { #b = 1; }"
case Syntax.ClassPrivateProperty:
info.node = node;
info.name = nodeToValue(info.node);
info.type = info.node.type;
break;
// like "b = 1;" in: "class A { b = 1; }"
case Syntax.ClassProperty:
info.node = node;
info.name = nodeToValue(info.node);
info.type = info.node.type;
break;
// like: "export * from 'foo'"
case Syntax.ExportAllDeclaration:
info.node = node;
info.name = nodeToValue(info.node);
info.type = info.node.type;
break;
// like: "export default 'foo'"
case Syntax.ExportDefaultDeclaration:
info.node = node.declaration;
info.name = nodeToValue(node);
info.type = info.node.type;
if ( isFunction(info.node) ) {
info.paramnames = getParamNames(info.node);
}
break;
// like: "export var foo;" (has declaration)
// or: "export {foo}" (no declaration)
case Syntax.ExportNamedDeclaration:
info.node = node;
info.name = nodeToValue(info.node);
info.type = info.node.declaration ? info.node.declaration.type :
Syntax.ObjectExpression;
if (info.node.declaration) {
if ( isFunction(info.node.declaration) ) {
info.paramnames = getParamNames(info.node.declaration);
}
// TODO: This duplicates logic for another node type in `jsdoc/src/visitor` in
// `makeSymbolFoundEvent()`. Is there a way to combine the logic for both node types
// into a single module?
if (info.node.declaration.kind === 'const') {
info.kind = 'constant';
}
}
break;
// like "foo as bar" in: "export {foo as bar}"
case Syntax.ExportSpecifier:
info.node = node;
info.name = nodeToValue(info.node);
info.type = info.node.local.type;
if ( isFunction(info.node.local) ) {
info.paramnames = getParamNames(info.node.local);
}
break;
// like: "function foo() {}"
// or the function in: "export default function() {}"
case Syntax.FunctionDeclaration:
info.node = node;
info.name = node.id ? nodeToValue(node.id) : '';
info.type = info.node.type;
info.paramnames = getParamNames(node);
break;
// like the function in: "var foo = function() {}"
case Syntax.FunctionExpression:
info.node = node;
// TODO: should we add a name for, e.g., "var foo = function bar() {}"?
info.name = '';
info.type = info.node.type;
info.paramnames = getParamNames(node);
break;
// like the param "bar" in: "function foo(bar) {}"
case Syntax.Identifier:
info.node = node;
info.name = nodeToValue(info.node);
info.type = info.node.type;
break;
// like "a.b.c"
case Syntax.MemberExpression:
info.node = node;
info.name = nodeToValue(info.node);
info.type = info.node.type;
break;
// like: "foo() {}"
case Syntax.MethodDefinition:
info.node = node;
info.name = nodeToValue(info.node);
info.type = info.node.type;
info.paramnames = getParamNames(node.value);
break;
// like "a: 0" in "var foo = {a: 0}"
case Syntax.Property:
info.node = node.value;
info.name = nodeToValue(node.key);
info.value = nodeToValue(info.node);
// property names with unsafe characters must be quoted
if ( !/^[$_a-zA-Z0-9]*$/.test(info.name) ) {
info.name = `"${String(info.name).replace(/"/g, '\\"')}"`;
}
if ( isAccessor(node) ) {
info.type = nodeToValue(info.node);
info.paramnames = getParamNames(info.node);
}
else {
info.type = info.node.type;
}
break;
// like "...bar" in: function foo(...bar) {}
case Syntax.RestElement:
info.node = node;
info.name = nodeToValue(info.node.argument);
info.type = info.node.type;
break;
// like: "var i = 0" (has init property)
// like: "var i" (no init property)
case Syntax.VariableDeclarator:
info.node = node.init || node.id;
info.name = node.id.name;
if (node.init) {
info.type = info.node.type;
info.value = nodeToValue(info.node);
}
break;
default:
info.node = node;
info.type = info.node.type;
}
return info;
};
+63
View File
@@ -0,0 +1,63 @@
/**
* @module jsdoc/src/filter
*/
const env = require('jsdoc/env');
const path = require('jsdoc/path');
function makeRegExp(config) {
let regExp = null;
if (config) {
regExp = (typeof config === 'string') ? new RegExp(config) : config;
}
return regExp;
}
/**
* @alias module:jsdoc/src/filter.Filter
*/
class Filter {
/**
* @param {Object} opts
* @param {string[]} opts.exclude - Specific files to exclude.
* @param {(string|RegExp)} opts.includePattern
* @param {(string|RegExp)} opts.excludePattern
*/
constructor({exclude, includePattern, excludePattern}) {
this.exclude = exclude && Array.isArray(exclude) ?
exclude.map($ => path.resolve(env.pwd, $)) :
null;
this.includePattern = makeRegExp(includePattern);
this.excludePattern = makeRegExp(excludePattern);
}
/**
* @param {string} filepath - The filepath to check.
* @returns {boolean} Should the given file be included?
*/
isIncluded(filepath) {
let included = true;
filepath = path.resolve(env.pwd, filepath);
if ( this.includePattern && !this.includePattern.test(filepath) ) {
included = false;
}
if ( this.excludePattern && this.excludePattern.test(filepath) ) {
included = false;
}
if (this.exclude) {
this.exclude.forEach(exclude => {
if ( filepath.indexOf(exclude) === 0 ) {
included = false;
}
});
}
return included;
}
}
exports.Filter = Filter;
+372
View File
@@ -0,0 +1,372 @@
/**
* @module jsdoc/src/handlers
*/
const escape = require('escape-string-regexp');
const jsdoc = {
doclet: require('jsdoc/doclet'),
name: require('jsdoc/name'),
src: {
syntax: require('jsdoc/src/syntax')
},
util: {
logger: require('jsdoc/util/logger')
}
};
let currentModule = null;
const SCOPE_NAMES = jsdoc.name.SCOPE.NAMES;
const SCOPE_PUNC = jsdoc.name.SCOPE.PUNC;
const Syntax = jsdoc.src.syntax.Syntax;
class CurrentModule {
constructor(doclet) {
this.doclet = doclet;
this.longname = doclet.longname;
this.originalName = doclet.meta.code.name || '';
}
}
function filterByLongname({longname}) {
// you can't document prototypes
if ( /#$/.test(longname) ) {
return true;
}
return false;
}
function createDoclet(comment, e) {
let doclet;
let flatComment;
let msg;
try {
doclet = new jsdoc.doclet.Doclet(comment, e);
}
catch (error) {
flatComment = comment.replace(/[\r\n]/g, '');
msg = `cannot create a doclet for the comment "${flatComment}": ${error.message}`;
jsdoc.util.logger.error(msg);
doclet = new jsdoc.doclet.Doclet('', e);
}
return doclet;
}
/**
* Create a doclet for a `symbolFound` event. The doclet represents an actual symbol that is defined
* in the code.
*
* Here's why this function is useful. A JSDoc comment can define a symbol name by including:
*
* + A `@name` tag
* + Another tag that accepts a name, such as `@function`
*
* When the JSDoc comment defines a symbol name, we treat it as a "virtual comment" for a symbol
* that isn't actually present in the code. And if a virtual comment is attached to a symbol, it's
* possible that the comment and symbol have nothing to do with one another.
*
* To handle this case, this function checks the new doclet to see if we've already added a name
* property by parsing the JSDoc comment. If so, this method creates a replacement doclet that
* ignores the attached JSDoc comment and only looks at the code.
*
* @private
*/
function createSymbolDoclet(comment, e) {
let doclet = createDoclet(comment, e);
if (doclet.name) {
// try again, without the comment
e.comment = '@undocumented';
doclet = createDoclet(e.comment, e);
}
return doclet;
}
function setCurrentModule(doclet) {
if (doclet.kind === 'module') {
currentModule = new CurrentModule(doclet);
}
}
function setModuleScopeMemberOf(parser, doclet) {
let parentDoclet;
let skipMemberof;
// handle module symbols that are _not_ assigned to module.exports
if (currentModule && currentModule.longname !== doclet.name) {
if (!doclet.scope) {
// is this a method definition? if so, we usually get the scope from the node directly
if (doclet.meta && doclet.meta.code && doclet.meta.code.node &&
doclet.meta.code.node.type === Syntax.MethodDefinition) {
// special case for constructors of classes that have @alias tags
if (doclet.meta.code.node.kind === 'constructor') {
parentDoclet = parser._getDocletById(
doclet.meta.code.node.parent.parent.nodeId
);
if (parentDoclet && parentDoclet.alias) {
// the constructor should use the same name as the class
doclet.addTag('alias', parentDoclet.alias);
doclet.addTag('name', parentDoclet.alias);
// and we shouldn't try to set a memberof value
skipMemberof = true;
}
}
else if (doclet.meta.code.node.static) {
doclet.addTag('static');
}
else {
doclet.addTag('instance');
}
}
// is this something that the module exports? if so, it's a static member
else if (doclet.meta && doclet.meta.code && doclet.meta.code.node &&
doclet.meta.code.node.parent &&
doclet.meta.code.node.parent.type === Syntax.ExportNamedDeclaration) {
doclet.addTag('static');
}
// otherwise, it must be an inner member
else {
doclet.addTag('inner');
}
}
// if the doclet isn't a memberof anything yet, and it's not a global, it must be a memberof
// the current module (unless we were told to skip adding memberof)
if (!doclet.memberof && doclet.scope !== SCOPE_NAMES.GLOBAL && !skipMemberof) {
doclet.addTag('memberof', currentModule.longname);
}
}
}
function setDefaultScope(doclet) {
// module doclets don't get a default scope
if (!doclet.scope && doclet.kind !== 'module') {
doclet.setScope(SCOPE_NAMES.GLOBAL);
}
}
function addDoclet(parser, newDoclet) {
let e;
if (newDoclet) {
setCurrentModule(newDoclet);
e = { doclet: newDoclet };
parser.emit('newDoclet', e);
if ( !e.defaultPrevented && !filterByLongname(e.doclet) ) {
parser.addResult(e.doclet);
}
}
}
function processAlias(parser, doclet, astNode) {
let memberofName;
if (doclet.alias === '{@thisClass}') {
memberofName = parser.resolveThis(astNode);
// "class" refers to the owner of the prototype, not the prototype itself
if ( /^(.+?)(\.prototype|#)$/.test(memberofName) ) {
memberofName = RegExp.$1;
}
doclet.alias = memberofName;
}
doclet.addTag('name', doclet.alias);
doclet.postProcess();
}
// TODO: separate code that resolves `this` from code that resolves the module object
function findSymbolMemberof(parser, doclet, astNode, nameStartsWith, trailingPunc) {
let memberof = '';
let nameAndPunc;
let scopePunc = '';
// handle computed properties like foo['bar']
if (trailingPunc === '[') {
// we don't know yet whether the symbol is a static or instance member
trailingPunc = null;
}
nameAndPunc = nameStartsWith + (trailingPunc || '');
// remove stuff that indicates module membership (but don't touch the name `module.exports`,
// which identifies the module object itself)
if (doclet.name !== 'module.exports') {
doclet.name = doclet.name.replace(nameAndPunc, '');
}
// like `bar` in:
// exports.bar = 1;
// module.exports.bar = 1;
// module.exports = MyModuleObject; MyModuleObject.bar = 1;
if (nameStartsWith !== 'this' && currentModule && doclet.name !== 'module.exports') {
memberof = currentModule.longname;
scopePunc = SCOPE_PUNC.STATIC;
}
// like: module.exports = 1;
else if (doclet.name === 'module.exports' && currentModule) {
doclet.addTag('name', currentModule.longname);
doclet.postProcess();
}
else {
memberof = parser.resolveThis(astNode);
// like the following at the top level of a module:
// this.foo = 1;
if (nameStartsWith === 'this' && currentModule && !memberof) {
memberof = currentModule.longname;
scopePunc = SCOPE_PUNC.STATIC;
}
else {
scopePunc = SCOPE_PUNC.INSTANCE;
}
}
return {
memberof: memberof,
scopePunc: scopePunc
};
}
function addSymbolMemberof(parser, doclet, astNode) {
let basename;
let memberof;
let memberofInfo;
let moduleOriginalName = '';
let resolveTargetRegExp;
let scopePunc;
let unresolved;
if (!astNode) {
return;
}
// check to see if the doclet name is an unresolved reference to the module object, or to `this`
// TODO: handle cases where the module object is shadowed in the current scope
if (currentModule) {
moduleOriginalName = `|${currentModule.originalName}`;
}
resolveTargetRegExp = new RegExp(`^((?:module.)?exports|this${moduleOriginalName})(\\.|\\[|$)`);
unresolved = resolveTargetRegExp.exec(doclet.name);
if (unresolved) {
memberofInfo = findSymbolMemberof(parser, doclet, astNode, unresolved[1], unresolved[2]);
memberof = memberofInfo.memberof;
scopePunc = memberofInfo.scopePunc;
if (memberof) {
doclet.name = doclet.name ?
memberof + scopePunc + doclet.name :
memberof;
}
}
else {
memberofInfo = parser.astnodeToMemberof(astNode);
basename = memberofInfo.basename;
memberof = memberofInfo.memberof;
}
// if we found a memberof name, apply it to the doclet
if (memberof) {
doclet.addTag('memberof', memberof);
if (basename) {
doclet.name = (doclet.name || '')
.replace(new RegExp(`^${escape(basename)}.`), '');
}
}
// otherwise, add the defaults for a module (if we're currently in a module)
else {
setModuleScopeMemberOf(parser, doclet);
}
}
function newSymbolDoclet(parser, docletSrc, e) {
const newDoclet = createSymbolDoclet(docletSrc, e);
// if there's an alias, use that as the symbol name
if (newDoclet.alias) {
processAlias(parser, newDoclet, e.astnode);
}
// otherwise, get the symbol name from the code
else if (e.code && typeof e.code.name !== 'undefined' && e.code.name !== '') {
newDoclet.addTag('name', e.code.name);
if (!newDoclet.memberof) {
addSymbolMemberof(parser, newDoclet, e.astnode);
}
newDoclet.postProcess();
}
else {
return false;
}
// set the scope to global unless any of the following are true:
// a) the doclet is a memberof something
// b) the doclet represents a module
// c) we're in a module that exports only this symbol
if ( !newDoclet.memberof && newDoclet.kind !== 'module' &&
(!currentModule || currentModule.longname !== newDoclet.name) ) {
newDoclet.scope = SCOPE_NAMES.GLOBAL;
}
// handle cases where the doclet kind is auto-detected from the node type
if (e.code.kind && newDoclet.kind === 'member') {
newDoclet.kind = e.code.kind;
}
addDoclet(parser, newDoclet);
e.doclet = newDoclet;
return true;
}
/**
* Attach these event handlers to a particular instance of a parser.
* @param parser
*/
exports.attachTo = parser => {
// Handle JSDoc "virtual comments" that include one of the following:
// + A `@name` tag
// + Another tag that accepts a name, such as `@function`
parser.on('jsdocCommentFound', e => {
const comments = e.comment.split(/@also\b/g);
let newDoclet;
for (let i = 0, l = comments.length; i < l; i++) {
newDoclet = createDoclet(comments[i], e);
// we're only interested in virtual comments here
if (!newDoclet.name) {
continue;
}
// add the default scope/memberof for a module (if we're in a module)
setModuleScopeMemberOf(parser, newDoclet);
newDoclet.postProcess();
// if we _still_ don't have a scope, use the default
setDefaultScope(newDoclet);
addDoclet(parser, newDoclet);
e.doclet = newDoclet;
}
});
// Handle named symbols in the code. May or may not have a JSDoc comment attached.
parser.on('symbolFound', e => {
const comments = e.comment.split(/@also\b/g);
for (let i = 0, l = comments.length; i < l; i++) {
newSymbolDoclet(parser, comments[i], e);
}
});
parser.on('fileComplete', () => {
currentModule = null;
});
};
+673
View File
@@ -0,0 +1,673 @@
/**
* @module jsdoc/src/parser
*/
const EventEmitter = require('events').EventEmitter;
const fs = require('jsdoc/fs');
const jsdoc = {
doclet: require('jsdoc/doclet'),
env: require('jsdoc/env'),
name: require('jsdoc/name'),
src: {
astnode: require('jsdoc/src/astnode'),
syntax: require('jsdoc/src/syntax')
},
util: {
doop: require('jsdoc/util/doop')
}
};
const logger = require('jsdoc/util/logger');
const hasOwnProp = Object.prototype.hasOwnProperty;
const Syntax = jsdoc.src.syntax.Syntax;
// TODO: docs
const PARSERS = exports.PARSERS = {
js: 'jsdoc/src/parser'
};
/* eslint-disable no-script-url */
// Prefix for JavaScript strings that were provided in lieu of a filename.
const SCHEMA = 'javascript:';
/* eslint-enable no-script-url */
class DocletCache {
constructor() {
this._doclets = {};
}
get(name) {
if ( !hasOwnProp.call(this._doclets, name) ) {
return null;
}
// always return the most recent doclet
return this._doclets[name][this._doclets[name].length - 1];
}
put(name, value) {
if ( !hasOwnProp.call(this._doclets, name) ) {
this._doclets[name] = [];
}
this._doclets[name].push(value);
}
}
// TODO: docs
exports.createParser = type => {
let modulePath;
if (!type) {
/* istanbul ignore next */
type = 'js';
}
if (hasOwnProp.call(PARSERS, type)) {
modulePath = PARSERS[type];
}
else {
logger.fatal('The parser type "%s" is not recognized.', type);
return null;
}
return new (require(modulePath).Parser)();
};
// TODO: docs
function pretreat(code) {
return code
// comment out hashbang at the top of the file, like: #!/usr/bin/env node
.replace(/^(#![\S \t]+\r?\n)/, '// $1')
// to support code minifiers that preserve /*! comments, treat /*!* as equivalent to /**
.replace(/\/\*!\*/g, '/**')
// merge adjacent doclets
.replace(/\*\/\/\*\*+/g, '@also');
}
// TODO: docs
function definedInScope(doclet, basename) {
return Boolean(doclet) && Boolean(doclet.meta) && Boolean(doclet.meta.vars) &&
Boolean(basename) && hasOwnProp.call(doclet.meta.vars, basename);
}
// TODO: docs
/**
* @alias module:jsdoc/src/parser.Parser
* @extends module:events.EventEmitter
*/
class Parser extends EventEmitter {
// TODO: docs
constructor(builderInstance, visitorInstance, walkerInstance) {
super();
this.clear();
this._astBuilder = builderInstance || new (require('jsdoc/src/astbuilder').AstBuilder)();
this._visitor = visitorInstance || new (require('jsdoc/src/visitor').Visitor)();
this._walker = walkerInstance || new (require('jsdoc/src/walker').Walker)();
this._visitor.setParser(this);
Object.defineProperties(this, {
astBuilder: {
get() {
return this._astBuilder;
}
},
visitor: {
get() {
return this._visitor;
}
},
walker: {
get() {
return this._walker;
}
}
});
}
// TODO: docs
clear() {
this._resultBuffer = [];
this._resultBuffer.index = {
borrowed: [],
documented: {},
longname: {},
memberof: {}
};
this._byNodeId = new DocletCache();
this._byLongname = new DocletCache();
this._byLongname.put(jsdoc.name.LONGNAMES.GLOBAL, {
meta: {}
});
}
// TODO: update docs
/**
* Parse the given source files for JSDoc comments.
* @param {Array.<string>} sourceFiles An array of filepaths to the JavaScript sources.
* @param {string} [encoding]
*
* @fires module:jsdoc/src/parser.Parser.parseBegin
* @fires module:jsdoc/src/parser.Parser.fileBegin
* @fires module:jsdoc/src/parser.Parser.jsdocCommentFound
* @fires module:jsdoc/src/parser.Parser.symbolFound
* @fires module:jsdoc/src/parser.Parser.newDoclet
* @fires module:jsdoc/src/parser.Parser.fileComplete
* @fires module:jsdoc/src/parser.Parser.parseComplete
*
* @example <caption>Parse two source files.</caption>
* var myFiles = ['file1.js', 'file2.js'];
* var docs = jsdocParser.parse(myFiles);
*/
parse(sourceFiles, encoding) {
encoding = encoding || jsdoc.env.conf.encoding || 'utf8';
let filename = '';
let sourceCode = '';
let sourceFile;
const parsedFiles = [];
const e = {};
if (typeof sourceFiles === 'string') {
sourceFiles = [sourceFiles];
}
e.sourcefiles = sourceFiles;
logger.debug('Parsing source files: %j', sourceFiles);
this.emit('parseBegin', e);
for (let i = 0, l = sourceFiles.length; i < l; i++) {
sourceCode = '';
sourceFile = sourceFiles[i];
if (sourceFile.indexOf(SCHEMA) === 0) {
sourceCode = sourceFile.substr(SCHEMA.length);
filename = `[[string${i}]]`;
}
else {
filename = sourceFile;
try {
sourceCode = fs.readFileSync(filename, encoding);
}
catch (err) {
logger.error('Unable to read and parse the source file %s: %s', filename, err);
}
}
if (sourceCode.length) {
this._parseSourceCode(sourceCode, filename);
parsedFiles.push(filename);
}
}
this.emit('parseComplete', {
sourcefiles: parsedFiles,
doclets: this._resultBuffer
});
logger.debug('Finished parsing source files.');
return this._resultBuffer;
}
// TODO: docs
fireProcessingComplete(doclets) {
this.emit('processingComplete', { doclets: doclets });
}
// TODO: docs
results() {
return this._resultBuffer;
}
// TODO: update docs
/**
* @param {module:jsdoc/doclet.Doclet} doclet The parse result to add to the result buffer.
*/
addResult(doclet) {
const index = this._resultBuffer.index;
this._resultBuffer.push(doclet);
// track all doclets by longname
if ( !hasOwnProp.call(index.longname, doclet.longname) ) {
index.longname[doclet.longname] = [];
}
index.longname[doclet.longname].push(doclet);
// track all doclets that have a memberof by memberof
if (doclet.memberof) {
if ( !hasOwnProp.call(index.memberof, doclet.memberof) ) {
index.memberof[doclet.memberof] = [];
}
index.memberof[doclet.memberof].push(doclet);
}
// track longnames of documented symbols
if (!doclet.undocumented) {
if ( !hasOwnProp.call(index.documented, doclet.longname) ) {
index.documented[doclet.longname] = [];
}
index.documented[doclet.longname].push(doclet);
}
// track doclets with a `borrowed` property
if ( hasOwnProp.call(doclet, 'borrowed') ) {
index.borrowed.push(doclet);
}
}
// TODO: docs
addAstNodeVisitor(visitor) {
this._visitor.addAstNodeVisitor(visitor);
}
// TODO: docs
getAstNodeVisitors() {
return this._visitor.getAstNodeVisitors();
}
/** @private */
_parseSourceCode(sourceCode, sourceName) {
let ast;
let e = {
filename: sourceName
};
this.emit('fileBegin', e);
logger.info('Parsing %s ...', sourceName);
if (!e.defaultPrevented) {
e = {
filename: sourceName,
source: sourceCode
};
this.emit('beforeParse', e);
sourceCode = e.source;
sourceName = e.filename;
sourceCode = pretreat(e.source);
ast = this._astBuilder.build(sourceCode, sourceName);
if (ast) {
this._walkAst(ast, this._visitor, sourceName);
}
}
this.emit('fileComplete', e);
}
/** @private */
_walkAst(ast, visitor, sourceName) {
this._walker.recurse(ast, visitor, sourceName);
}
// TODO: docs
addDocletRef(e) {
let fakeDoclet;
let node;
if (e && e.code && e.code.node) {
node = e.code.node;
if (e.doclet) {
// allow lookup from node ID => doclet
this._byNodeId.put(node.nodeId, e.doclet);
this._byLongname.put(e.doclet.longname, e.doclet);
}
// keep references to undocumented anonymous functions, too, as they might have scoped vars
else if (
(node.type === Syntax.FunctionDeclaration || node.type === Syntax.FunctionExpression ||
node.type === Syntax.ArrowFunctionExpression) &&
!this._getDocletById(node.nodeId) ) {
fakeDoclet = {
longname: jsdoc.name.LONGNAMES.ANONYMOUS,
meta: {
code: e.code
}
};
this._byNodeId.put(node.nodeId, fakeDoclet);
this._byLongname.put(fakeDoclet.longname, fakeDoclet);
}
}
}
// TODO: docs
_getDocletById(id) {
return this._byNodeId.get(id);
}
/**
* Retrieve the most recently seen doclet that has the given longname.
*
* @param {string} longname - The longname to search for.
* @return {module:jsdoc/doclet.Doclet?} The most recent doclet for the longname.
*/
_getDocletByLongname(longname) {
return this._byLongname.get(longname);
}
// TODO: docs
/**
* Given a node, determine what the node is a member of.
* @param {node} node
* @returns {string} The long name of the node that this is a member of.
*/
astnodeToMemberof(node) {
let basename;
let doclet;
let scope;
const result = {};
const type = node.type;
if ( (type === Syntax.FunctionDeclaration || type === Syntax.FunctionExpression ||
type === Syntax.ArrowFunctionExpression || type === Syntax.VariableDeclarator) &&
node.enclosingScope ) {
doclet = this._getDocletById(node.enclosingScope.nodeId);
if (!doclet) {
result.memberof = jsdoc.name.LONGNAMES.ANONYMOUS + jsdoc.name.SCOPE.PUNC.INNER;
}
else {
result.memberof = doclet.longname + jsdoc.name.SCOPE.PUNC.INNER;
}
}
else if (type === Syntax.ClassPrivateProperty || type === Syntax.ClassProperty) {
doclet = this._getDocletById(node.enclosingScope.nodeId);
if (!doclet) {
result.memberof = jsdoc.name.LONGNAMES.ANONYMOUS + jsdoc.name.SCOPE.PUNC.INSTANCE;
}
else {
result.memberof = doclet.longname + jsdoc.name.SCOPE.PUNC.INSTANCE;
}
}
else if (type === Syntax.MethodDefinition && node.kind === 'constructor') {
doclet = this._getDocletById(node.enclosingScope.nodeId);
// global classes aren't a member of anything
if (doclet.memberof) {
result.memberof = doclet.memberof + jsdoc.name.SCOPE.PUNC.INNER;
}
}
// special case for methods in classes that are returned by arrow function expressions; for
// other method definitions, we get the memberof from the node name elsewhere. yes, this is
// confusing...
else if (type === Syntax.MethodDefinition && node.parent.parent.parent &&
node.parent.parent.parent.type === Syntax.ArrowFunctionExpression) {
doclet = this._getDocletById(node.enclosingScope.nodeId);
if (doclet) {
result.memberof = doclet.longname +
(node.static === true ?
jsdoc.name.SCOPE.PUNC.STATIC :
jsdoc.name.SCOPE.PUNC.INSTANCE);
}
}
else {
// check local references for aliases
scope = node;
basename = jsdoc.name.getBasename( jsdoc.src.astnode.nodeToValue(node) );
// walk up the scope chain until we find the scope in which the node is defined
while (scope.enclosingScope) {
doclet = this._getDocletById(scope.enclosingScope.nodeId);
if ( doclet && definedInScope(doclet, basename) ) {
result.memberof = doclet.meta.vars[basename];
result.basename = basename;
break;
}
else {
// move up
scope = scope.enclosingScope;
}
}
// do we know that it's a global?
doclet = this._getDocletByLongname(jsdoc.name.LONGNAMES.GLOBAL);
if ( doclet && definedInScope(doclet, basename) ) {
result.memberof = doclet.meta.vars[basename];
result.basename = basename;
}
else {
doclet = this._getDocletById(node.parent.nodeId);
// set the result if we found a doclet. (if we didn't, the AST node may describe a
// global symbol.)
if (doclet) {
result.memberof = doclet.longname || doclet.name;
}
}
}
return result;
}
/**
* Get the doclet for the lowest-level class, if any, that is in the scope chain for a given node.
*
* @param {Object} node - The node whose scope chain will be searched.
* @return {module:jsdoc/doclet.Doclet?} The doclet for the lowest-level class in the node's scope
* chain.
*/
_getParentClass({enclosingScope}) {
let doclet;
let nameAtoms;
let scope = enclosingScope;
function isClass(d) {
return d && d.kind === 'class';
}
while (scope) {
// get the doclet, if any, for the parent scope
doclet = this._getDocletById(scope.nodeId);
if (doclet) {
// is the doclet for a class? if so, we're done
if ( isClass(doclet) ) {
break;
}
// is the doclet for an instance member of a class? if so, try to get the doclet for the
// owning class
nameAtoms = jsdoc.name.shorten(doclet.longname);
if (nameAtoms.scope === jsdoc.name.SCOPE.PUNC.INSTANCE) {
doclet = this._getDocletByLongname(nameAtoms.memberof);
if ( isClass(doclet) ) {
break;
}
}
}
// move up to the next parent scope
scope = scope.enclosingScope;
}
return (isClass(doclet) ? doclet : null);
}
// TODO: docs
/**
* Resolve what "this" refers to relative to a node.
* @param {node} node - The "this" node
* @returns {string} The longname of the enclosing node.
*/
resolveThis(node) {
let doclet;
let parentClass;
let result;
// Properties are handled below.
if (node.type !== Syntax.Property && node.enclosingScope) {
// For ES2015 constructor functions, we use the class declaration to resolve `this`.
if (node.parent && node.parent.type === Syntax.MethodDefinition &&
node.parent.kind === 'constructor') {
doclet = this._getDocletById(node.parent.parent.parent.nodeId);
}
// Otherwise, if there's an enclosing scope, we use the enclosing scope to resolve `this`.
else {
doclet = this._getDocletById(node.enclosingScope.nodeId);
}
if (!doclet) {
result = jsdoc.name.LONGNAMES.ANONYMOUS; // TODO handle global this?
}
else if (doclet.this) {
result = doclet.this;
}
else if (doclet.kind === 'function' && doclet.memberof) {
parentClass = this._getParentClass(node);
// like: function Foo() { this.bar = function(n) { /** blah */ this.name = n; };
// or: Foo.prototype.bar = function(n) { /** blah */ this.name = n; };
// or: var Foo = exports.Foo = function(n) { /** blah */ this.name = n; };
// or: Foo.constructor = function(n) { /** blah */ this.name = n; }
if ( parentClass || /\.constructor$/.test(doclet.longname) ) {
result = doclet.memberof;
}
// like: function notAClass(n) { /** global this */ this.name = n; }
else {
result = doclet.longname;
}
}
// like: var foo = function(n) { /** blah */ this.bar = n; }
else if ( doclet.kind === 'member' && jsdoc.src.astnode.isAssignment(node) ) {
result = doclet.longname;
}
// walk up to the closest class we can find
else if (doclet.kind === 'class' || doclet.kind === 'interface' || doclet.kind === 'module') {
result = doclet.longname;
}
else if (node.enclosingScope) {
result = this.resolveThis(node.enclosingScope);
}
}
// For object properties, we use the node's parent (the object) instead.
else {
doclet = this._getDocletById(node.parent.nodeId);
if (!doclet) {
// The object wasn't documented, so we don't know what name to use.
result = '';
}
else {
result = doclet.longname;
}
}
return result;
}
/**
* Given an AST node representing an object property, find the doclets for the parent object or
* objects.
*
* If the object is part of a simple assignment (for example, `var foo = { x: 1 }`), this method
* returns a single doclet (in this case, the doclet for `foo`).
*
* If the object is part of a chained assignment (for example, `var foo = exports.FOO = { x: 1 }`,
* this method returns multiple doclets (in this case, the doclets for `foo` and `exports.FOO`).
*
* @param {Object} node - An AST node representing an object property.
* @return {Array.<module:jsdoc/doclet.Doclet>} An array of doclets for the parent object or objects, or
* an empty array if no doclets are found.
*/
resolvePropertyParents({parent}) {
let currentAncestor = parent;
let nextAncestor = currentAncestor.parent;
let doclet;
const doclets = [];
while (currentAncestor) {
doclet = this._getDocletById(currentAncestor.nodeId);
if (doclet) {
doclets.push(doclet);
}
// if the next ancestor is an assignment expression (for example, `exports.FOO` in
// `var foo = exports.FOO = { x: 1 }`, keep walking upwards
if (nextAncestor && nextAncestor.type === Syntax.AssignmentExpression) {
nextAncestor = nextAncestor.parent;
currentAncestor = currentAncestor.parent;
}
// otherwise, we're done
else {
currentAncestor = null;
}
}
return doclets;
}
// TODO: docs
/**
* Resolve what function a var is limited to.
* @param {astnode} node
* @param {string} basename The leftmost name in the long name: in foo.bar.zip the basename is foo.
*/
resolveVar({enclosingScope, type}, basename) {
let doclet;
let result;
const scope = enclosingScope;
// HACK: return an empty string for function declarations so they don't end up in anonymous
// scope (see #685 and #693)
if (type === Syntax.FunctionDeclaration) {
result = '';
}
else if (!scope) {
result = ''; // global
}
else {
doclet = this._getDocletById(scope.nodeId);
if ( definedInScope(doclet, basename) ) {
result = doclet.longname;
}
else {
result = this.resolveVar(scope, basename);
}
}
return result;
}
// TODO: docs
resolveEnum(e) {
const doclets = this.resolvePropertyParents(e.code.node.parent);
doclets.forEach(doclet => {
if (doclet && doclet.isEnum) {
doclet.properties = doclet.properties || [];
// members of an enum inherit the enum's type
if (doclet.type && !e.doclet.type) {
// clone the type to prevent circular refs
e.doclet.type = jsdoc.util.doop(doclet.type);
}
delete e.doclet.undocumented;
e.doclet.defaultvalue = e.doclet.meta.code.value;
// add the doclet to the parent's properties
doclet.properties.push(e.doclet);
}
});
}
}
exports.Parser = Parser;
// TODO: document other events
/**
* Fired once for each JSDoc comment in the current source code.
* @event jsdocCommentFound
* @memberof module:jsdoc/src/parser.Parser
* @type {Object}
* @property {string} comment The text content of the JSDoc comment
* @property {number} lineno The line number associated with the found comment.
* @property {number} columnno The column number associated with the found comment.
* @property {string} filename The file name associated with the found comment.
*/
+62
View File
@@ -0,0 +1,62 @@
/**
* @module jsdoc/src/scanner
* @requires module:jsdoc/fs
*/
const EventEmitter = require('events').EventEmitter;
const env = require('jsdoc/env');
const fs = require('jsdoc/fs');
const logger = require('jsdoc/util/logger');
const path = require('jsdoc/path');
/**
* @extends module:events.EventEmitter
*/
class Scanner extends EventEmitter {
constructor() {
super();
}
/**
* Recursively searches the given searchPaths for js files.
* @param {Array.<string>} searchPaths
* @param {number} [depth]
* @fires sourceFileFound
*/
scan(searchPaths = [], depth = 1, filter) {
let currentFile;
let filePaths = [];
searchPaths.forEach($ => {
const filepath = path.resolve( env.pwd, decodeURIComponent($) );
try {
currentFile = fs.statSync(filepath);
}
catch (e) {
logger.error('Unable to find the source file or directory %s', filepath);
return;
}
if ( currentFile.isFile() ) {
filePaths.push(filepath);
}
else {
filePaths = filePaths.concat( fs.ls(filepath, depth) );
}
});
filePaths = filePaths.filter($ => filter.isIncluded($));
filePaths = filePaths.filter($ => {
const e = { fileName: $ };
this.emit('sourceFileFound', e);
return !e.defaultPrevented;
});
return filePaths;
}
}
exports.Scanner = Scanner;
+96
View File
@@ -0,0 +1,96 @@
// TODO: docs
exports.Syntax = {
ArrayExpression: 'ArrayExpression',
ArrayPattern: 'ArrayPattern',
ArrowFunctionExpression: 'ArrowFunctionExpression',
AssignmentExpression: 'AssignmentExpression',
AssignmentPattern: 'AssignmentPattern',
AwaitExpression: 'AwaitExpression',
BigIntLiteral: 'BigIntLiteral',
BinaryExpression: 'BinaryExpression',
BindExpression: 'BindExpression',
BlockStatement: 'BlockStatement',
BreakStatement: 'BreakStatement',
CallExpression: 'CallExpression',
CatchClause: 'CatchClause',
ClassBody: 'ClassBody',
ClassDeclaration: 'ClassDeclaration',
ClassExpression: 'ClassExpression',
ClassPrivateProperty: 'ClassPrivateProperty',
ClassProperty: 'ClassProperty',
ComprehensionBlock: 'ComprehensionBlock',
ComprehensionExpression: 'ComprehensionExpression',
ConditionalExpression: 'ConditionalExpression',
ContinueStatement: 'ContinueStatement',
DebuggerStatement: 'DebuggerStatement',
Decorator: 'Decorator',
DoExpression: 'DoExpression',
DoWhileStatement: 'DoWhileStatement',
EmptyStatement: 'EmptyStatement',
ExperimentalRestProperty: 'ExperimentalRestProperty',
ExperimentalSpreadProperty: 'ExperimentalSpreadProperty',
ExportAllDeclaration: 'ExportAllDeclaration',
ExportDefaultDeclaration: 'ExportDefaultDeclaration',
ExportDefaultSpecifier: 'ExportDefaultSpecifier',
ExportNamedDeclaration: 'ExportNamedDeclaration',
ExportNamespaceSpecifier: 'ExportNamespaceSpecifier',
ExportSpecifier: 'ExportSpecifier',
ExpressionStatement: 'ExpressionStatement',
File: 'File',
ForInStatement: 'ForInStatement',
ForOfStatement: 'ForOfStatement',
ForStatement: 'ForStatement',
FunctionDeclaration: 'FunctionDeclaration',
FunctionExpression: 'FunctionExpression',
Identifier: 'Identifier',
IfStatement: 'IfStatement',
Import: 'Import',
ImportDeclaration: 'ImportDeclaration',
ImportDefaultSpecifier: 'ImportDefaultSpecifier',
ImportNamespaceSpecifier: 'ImportNamespaceSpecifier',
ImportSpecifier: 'ImportSpecifier',
JSXAttribute: 'JSXAttribute',
JSXClosingElement: 'JSXClosingElement',
JSXElement: 'JSXElement',
JSXEmptyExpression: 'JSXEmptyExpression',
JSXExpressionContainer: 'JSXExpressionContainer',
JSXIdentifier: 'JSXIdentifier',
JSXMemberExpression: 'JSXMemberExpression',
JSXNamespacedName: 'JSXNamespacedName',
JSXOpeningElement: 'JSXOpeningElement',
JSXSpreadAttribute: 'JSXSpreadAttribute',
JSXText: 'JSXText',
LabeledStatement: 'LabeledStatement',
LetStatement: 'LetStatement',
Literal: 'Literal',
LogicalExpression: 'LogicalExpression',
MemberExpression: 'MemberExpression',
MetaProperty: 'MetaProperty',
MethodDefinition: 'MethodDefinition',
NewExpression: 'NewExpression',
ObjectExpression: 'ObjectExpression',
ObjectPattern: 'ObjectPattern',
PrivateName: 'PrivateName',
Program: 'Program',
Property: 'Property',
RestElement: 'RestElement',
ReturnStatement: 'ReturnStatement',
SequenceExpression: 'SequenceExpression',
SpreadElement: 'SpreadElement',
Super: 'Super',
SwitchCase: 'SwitchCase',
SwitchStatement: 'SwitchStatement',
TaggedTemplateExpression: 'TaggedTemplateExpression',
TemplateElement: 'TemplateElement',
TemplateLiteral: 'TemplateLiteral',
ThisExpression: 'ThisExpression',
ThrowStatement: 'ThrowStatement',
TryStatement: 'TryStatement',
UnaryExpression: 'UnaryExpression',
UpdateExpression: 'UpdateExpression',
VariableDeclaration: 'VariableDeclaration',
VariableDeclarator: 'VariableDeclarator',
WhileStatement: 'WhileStatement',
WithStatement: 'WithStatement',
YieldExpression: 'YieldExpression'
};
+859
View File
@@ -0,0 +1,859 @@
/**
* @module jsdoc/src/visitor
*/
// TODO: consider exporting more stuff so users can override it
const jsdoc = {
doclet: require('jsdoc/doclet'),
name: require('jsdoc/name'),
src: {
astnode: require('jsdoc/src/astnode'),
syntax: require('jsdoc/src/syntax')
},
util: {
logger: require('jsdoc/util/logger')
}
};
const Syntax = jsdoc.src.syntax.Syntax;
/**
* Get the raw comment string for a block comment node.
*
* @private
* @param {!Object} comment - A comment node with `type` and `value` properties.
*/
function getRawComment({value}) {
return `/*${value}*/`;
}
/**
* Check whether a comment node represents a block comment.
*
* @param {!Object} comment - A comment node with `type` and `value` properties.
* @return {boolean} `true` if the comment is a block comment, `false` otherwise.
*/
function isBlockComment({type}) {
return type === 'CommentBlock';
}
/**
* Verify that a block comment exists; that it is a JSDoc comment; and that its leading delimiter
* does not contain three or more asterisks.
*
* @private
* @memberof module:jsdoc/src/parser.Parser
*/
function isValidJsdoc(commentSrc) {
return commentSrc && commentSrc.length > 4 && commentSrc.indexOf('/**') === 0 &&
commentSrc.indexOf('/***') !== 0;
}
// TODO: docs
function getLeadingJsdocComment(node) {
let comment = null;
let leadingComments = node.leadingComments;
if (Array.isArray(leadingComments) && leadingComments.length) {
// the attached comments may include line comments, which we don't want
leadingComments = leadingComments.filter(isBlockComment);
if (leadingComments.length) {
// treat the comment closest to the node as the leading comment
comment = getRawComment(leadingComments[leadingComments.length - 1]);
if ( !isValidJsdoc(comment) ) {
comment = null;
}
}
}
return comment;
}
// TODO: docs
function makeVarsFinisher(scopeDoclet) {
return ({doclet, code}) => {
// no need to evaluate all things related to scopeDoclet again, just use it
if ( scopeDoclet && doclet && (doclet.alias || doclet.memberof) ) {
scopeDoclet.meta.vars[code.name] = doclet.longname;
}
};
}
// Given an event, get the parent node's doclet.
function getParentDocletFromEvent(parser, {doclet}) {
if (doclet && doclet.meta && doclet.meta.code && doclet.meta.code.node &&
doclet.meta.code.node.parent) {
return parser._getDocletById(doclet.meta.code.node.parent.nodeId);
}
return null;
}
/**
* For function parameters that have inline documentation, create a function that will merge the
* inline documentation into the function's doclet. If the parameter is already documented in the
* function's doclet, the inline documentation will be ignored.
*
* @private
* @param {module:jsdoc/src/parser.Parser} parser - The JSDoc parser.
* @return {function} A function that merges a parameter's inline documentation into the function's
* doclet.
*/
function makeInlineParamsFinisher(parser) {
return e => {
let documentedParams;
let knownParams;
let param;
let parentDoclet;
let i = 0;
parentDoclet = getParentDocletFromEvent(parser, e);
if (!parentDoclet) {
return;
}
// we only want to use the doclet if it's param-specific (but not, for example, if it's
// a param tagged with `@exports` in an AMD module)
if (e.doclet.kind !== 'param') {
return;
}
parentDoclet.params = parentDoclet.params || [];
documentedParams = parentDoclet.params;
knownParams = parentDoclet.meta.code.paramnames || [];
while (true) {
param = documentedParams[i];
// is the param already documented? if so, we don't need to use the doclet
if (param && param.name === e.doclet.name) {
e.doclet.undocumented = true;
break;
}
// if we ran out of documented params, or we're at the parameter's actual position,
// splice in the param at the current index
if ( !param || i === knownParams.indexOf(e.doclet.name) ) {
documentedParams.splice(i, 0, {
type: e.doclet.type || {},
description: '',
name: e.doclet.name
});
// the doclet is no longer needed
e.doclet.undocumented = true;
break;
}
i++;
}
};
}
/**
* Given an array of nodes that represent function parameters, find the node for the rest parameter,
* if any.
*
* @private
* @param {Array.<Object>} params - An array of nodes that represent function parameters.
* @return {Object?} The node for the rest parameter.
*/
function findRestParam(params) {
let restParam = null;
params.some(param => {
if (param.type === Syntax.RestElement) {
restParam = param;
return true;
}
return false;
});
return restParam;
}
/**
* For functions that may include a rest parameter, create a function that will automatically update
* the rest parameter's documentation to indicate that the parameter is repeatable. If the parameter
* is not documented, the function's doclet will remain unchanged.
*
* @private
* @return {function} A function that updates the rest parameter's documentation to indicate that
* the parameter is repeatable.
*/
function makeRestParamFinisher() {
return e => {
const doclet = e.doclet;
let documentedParams;
let restNode;
if (!doclet) {
return;
}
documentedParams = doclet.params = doclet.params || [];
restNode = findRestParam(e.code.node.params ||
(e.code.node.value && e.code.node.value.params) ||
(e.code.node.init && e.code.node.init.params) ||
[]);
if (restNode) {
for (let i = documentedParams.length - 1; i >= 0; i--) {
if (documentedParams[i].name === restNode.argument.name) {
documentedParams[i].variable = true;
break;
}
}
}
};
}
/**
* Given an array of nodes that represent function parameters, find the nodes for the default
* parameters, if any.
*
* @private
* @param {Array.<Object>} params - An array of nodes that represent function parameters.
* @return {Array.<Object>} The nodes for the default parameters.
*/
function findDefaultParams(params) {
const defaultParams = [];
params.forEach(param => {
if (param.type === Syntax.AssignmentPattern) {
defaultParams.push(param);
}
else {
defaultParams.push(null);
}
});
return defaultParams;
}
/**
* For functions that may have at least one parameter with default values, create a function that
* will automatically add the parameters' default values to the function's documentation. If any
* default value is already documented, the function's doclet will remain unchanged.
*
* This function is only intended to handle default parameters whose node type is `Syntax.Literal`
* (string, numeric, and boolean literals). This is because more complex default values may include,
* for example, references to internal variables, which it may not make sense to include in
* documentation.
*
* @private
* @return {function} A function that updates the function doclet to include the default values of
* parameters.
*/
function makeDefaultParamFinisher() {
return e => {
let defaultValues;
const doclet = e.doclet;
let documentedParams;
let paramName;
let params;
if (!doclet) {
return;
}
documentedParams = doclet.params = doclet.params || [];
params = e.code.node.params || (e.code.node.value && e.code.node.value.params) || [];
defaultValues = findDefaultParams(params);
for (let i = 0, j = 0, l = params.length; i < l; i++) {
// bail out if we ran out of documented params
if (!documentedParams[j]) {
break;
}
// if the current parameter doesn't appear to be documented, move to the next one
paramName = params[i].type === Syntax.AssignmentPattern ?
params[i].left.name :
params[i].name;
if (paramName !== documentedParams[j].name) {
continue;
}
// add the default value iff a) a literal default value is defined in the code,
// b) no default value is documented, and c) the default value is not an empty string
if (defaultValues[i] &&
defaultValues[i].right &&
defaultValues[i].right.type === Syntax.Literal &&
typeof documentedParams[j].defaultvalue === 'undefined' &&
defaultValues[i].right.value !== '') {
documentedParams[j].defaultvalue =
jsdoc.src.astnode.nodeToValue(defaultValues[i].right);
}
// move to the next documented param
j++;
}
};
}
/**
* For method definitions that are constructors, create a function that will merge portions of the
* constructor's doclet into the class's doclet, provided that a doclet exists for the class.
* Merging the constructor's documentation allows ES 2015 classes to be documented in a natural way,
* with separate JSDoc comments for the class and its constructor.
*
* @private
* @param {module:jsdoc/src/parser.Parser} parser - The JSDoc parser.
* @return {function} A function that merges the constructor's doclet into the class's doclet.
*/
function makeConstructorFinisher(parser) {
return e => {
let combined;
const doclet = e.doclet;
let parentDoclet;
// for class declarations that are named module exports, the node that's documented is the
// ExportNamedDeclaration, not the ClassDeclaration
if (e.code.node.parent.parent.parent &&
e.code.node.parent.parent.parent.type === Syntax.ExportNamedDeclaration) {
parentDoclet = parser._getDocletById(e.code.node.parent.parent.parent.nodeId);
}
// otherwise, we want the ClassDeclaration
else {
parentDoclet = parser._getDocletById(e.code.node.parent.parent.nodeId);
}
if (!doclet || !parentDoclet || parentDoclet.undocumented) {
return;
}
// We prefer the parent doclet because it has the correct kind, longname, and memberof.
// The child doclet might or might not have the correct kind, longname, and memberof.
combined = jsdoc.doclet.combine(parentDoclet, doclet);
parser.addResult(combined);
parentDoclet.undocumented = doclet.undocumented = true;
};
}
/**
* Create a function that will add an `async` property to the doclet for async functions.
*
* @private
* @return {function} A function that adds an `async` property to the doclet of async functions.
*/
function makeAsyncFunctionFinisher() {
return e => {
const doclet = e.doclet;
if (!doclet) {
return;
}
if ( e.code.node.async || (e.code.node.value && e.code.node.value.async) ||
(e.code.node.init && e.code.node.init.async) ) {
doclet.async = true;
}
};
}
/**
* Create a function that will mark a doclet as private.
*
* @private
* @return {function} A function that marks a doclet as private.
*/
function makePrivatePropertyFinisher() {
return ({doclet}) => {
doclet.access = 'private';
};
}
/**
* Create a function that will mark a doclet as a generator function.
*
* @private
* @return {function} A function that marks a doclet as a generator function.
*/
function makeGeneratorFinisher() {
return e => {
const doclet = e.doclet;
if (!doclet) {
return;
}
if ( e.code.node.generator || (e.code.node.init && e.code.node.init.generator) ||
(e.code.node.value && e.code.node.value.generator) ) {
doclet.generator = true;
}
};
}
// TODO: docs
class SymbolFound {
// TODO: docs
constructor(node, filename, extras = {}) {
this.id = extras.id || node.nodeId;
this.comment = extras.comment || getLeadingJsdocComment(node) || '@undocumented';
this.lineno = extras.lineno || node.loc.start.line;
this.columnno = extras.columnno || node.loc.start.column;
this.range = extras.range || node.range;
this.filename = extras.filename || filename;
this.astnode = extras.astnode || node;
this.code = extras.code;
this.event = extras.event || 'symbolFound';
this.finishers = extras.finishers || [];
// make sure the event includes properties that don't have default values
Object.keys(extras).forEach(key => {
this[key] = extras[key];
});
}
}
// TODO: docs
class JsdocCommentFound {
// TODO: docs
constructor({loc, range}, rawComment, filename) {
this.comment = rawComment;
this.lineno = loc.start.line;
this.columnno = loc.start.column;
this.filename = filename;
this.range = range;
Object.defineProperty(this, 'event', {
value: 'jsdocCommentFound'
});
}
}
// TODO: docs
function hasComments(node) {
return (node && node.leadingComments && node.leadingComments.length) ||
(node && node.trailingComments && node.trailingComments.length) ||
(node && node.innerComments && node.innerComments.length);
}
// TODO: docs
function removeCommentDelimiters(comment) {
return comment.substring(2, comment.length - 2);
}
// TODO: docs
function updateCommentNode(commentNode, comment) {
commentNode.value = removeCommentDelimiters(comment);
}
// TODO: docs
// TODO: note that it's essential to call this function before you try to resolve names!
function trackVars(parser, {enclosingScope}, {code, finishers}) {
let doclet;
const enclosingScopeId = enclosingScope ? enclosingScope.nodeId : null;
if (enclosingScopeId) {
doclet = parser._getDocletById(enclosingScopeId);
}
else {
doclet = parser._getDocletByLongname(jsdoc.name.LONGNAMES.GLOBAL);
}
if (doclet) {
doclet.meta.vars = doclet.meta.vars || {};
doclet.meta.vars[code.name] = null;
finishers.push( makeVarsFinisher(doclet) );
}
}
// TODO: docs
function makeSymbolFoundEvent(node, parser, filename) {
let e;
let basename;
let parent;
const extras = {
code: jsdoc.src.astnode.getInfo(node)
};
switch (node.type) {
// like: i = 0;
case Syntax.AssignmentExpression:
e = new SymbolFound(node, filename, extras);
trackVars(parser, node, e);
basename = jsdoc.name.getBasename(e.code.name);
if (basename !== 'this') {
e.code.funcscope = parser.resolveVar(node, basename);
}
break;
// like `bar='baz'` in: function foo(bar='baz') {}
case Syntax.AssignmentPattern:
parent = node.parent;
if ( node.leadingComments && parent && jsdoc.src.astnode.isFunction(parent) ) {
extras.finishers = [makeInlineParamsFinisher(parser)];
e = new SymbolFound(node, filename, extras);
trackVars(parser, node, e);
}
break;
// like: class foo {}
case Syntax.ClassDeclaration:
// falls through
// like: let MyClass = class {}
case Syntax.ClassExpression:
e = new SymbolFound(node, filename, extras);
trackVars(parser, node, e);
basename = jsdoc.name.getBasename(e.code.name);
break;
// like `#b = 1` in: class A { #b = 1; }
case Syntax.ClassPrivateProperty:
extras.finishers = [
parser.resolveEnum,
makePrivatePropertyFinisher()
];
e = new SymbolFound(node, filename, extras);
break;
// like `b = 1` in: class A { b = 1; }
case Syntax.ClassProperty:
extras.finishers = [parser.resolveEnum];
e = new SymbolFound(node, filename, extras);
break;
// like: export * from 'foo'
case Syntax.ExportAllDeclaration:
e = new SymbolFound(node, filename, extras);
break;
// like: export default 'foo'
case Syntax.ExportDefaultDeclaration:
// falls through
// like: export var foo;
// or: export {foo}
case Syntax.ExportNamedDeclaration:
// falls through
// like `foo as bar` in: export {foo as bar}
case Syntax.ExportSpecifier:
e = new SymbolFound(node, filename, extras);
trackVars(parser, node, e);
break;
// like: var foo = () => {};
case Syntax.ArrowFunctionExpression:
// falls through
// like: function foo() {}
case Syntax.FunctionDeclaration:
// falls through
// like: var foo = function() {};
case Syntax.FunctionExpression:
extras.finishers = [
// handle cases where at least one parameter has a default value
makeDefaultParamFinisher(),
// handle rest parameters
makeRestParamFinisher(),
// handle async functions
makeAsyncFunctionFinisher(),
// handle generator functions
makeGeneratorFinisher()
];
e = new SymbolFound(node, filename, extras);
trackVars(parser, node, e);
basename = jsdoc.name.getBasename(e.code.name);
e.code.funcscope = parser.resolveVar(node, basename);
break;
// like `bar` in: function foo(/** @type {string} */ bar) {}
// or `module` in: define("MyModule", function(/** @exports MyModule */ module) {}
// This is an extremely common type of node; we only care about function parameters with
// inline comments. No need to fire an event in other cases.
case Syntax.Identifier:
parent = node.parent;
// function parameters with inline comments
if ( node.leadingComments && parent && jsdoc.src.astnode.isFunction(parent) ) {
extras.finishers = [makeInlineParamsFinisher(parser)];
e = new SymbolFound(node, filename, extras);
trackVars(parser, node, e);
}
break;
// like `obj.prop` in: /** @typedef {string} */ obj.prop;
// Closure Compiler uses this pattern extensively for enums.
// No need to fire an event unless the node is already commented.
case Syntax.MemberExpression:
if (node.leadingComments) {
e = new SymbolFound(node, filename, extras);
}
break;
// like: foo() {}
// or: constructor() {}
case Syntax.MethodDefinition:
extras.finishers = [
// handle cases where at least one parameter has a default value
makeDefaultParamFinisher(),
// handle rest parameters
makeRestParamFinisher(),
// handle async functions
makeAsyncFunctionFinisher(),
// handle generator functions
makeGeneratorFinisher()
];
// for constructors, we attempt to merge the constructor's docs into the class's docs
if (node.kind === 'constructor') {
extras.finishers.push( makeConstructorFinisher(parser) );
}
e = new SymbolFound(node, filename, extras);
break;
// like `{}` in: function Foo = Class.create(/** @lends Foo */ {});
case Syntax.ObjectExpression:
e = new SymbolFound(node, filename, extras);
break;
// like `bar: true` in: var foo = { bar: true };
// like `get bar() {}` in: var foo = { get bar() {} };
case Syntax.Property:
if (node.kind !== 'get' && node.kind !== 'set') {
extras.finishers = [parser.resolveEnum];
}
e = new SymbolFound(node, filename, extras);
break;
// like `...bar` in: function foo(...bar) {}
case Syntax.RestElement:
parent = node.parent;
if ( node.leadingComments && parent && jsdoc.src.astnode.isFunction(parent) ) {
extras.finishers = [makeInlineParamsFinisher(parser)];
e = new SymbolFound(node, filename, extras);
trackVars(parser, node, e);
}
break;
// like: var i = 0;
case Syntax.VariableDeclarator:
extras.finishers = [
// handle cases where at least one parameter has a default value
makeDefaultParamFinisher(),
// handle rest parameters
makeRestParamFinisher(),
// handle async functions
makeAsyncFunctionFinisher(),
// handle generator functions
makeGeneratorFinisher()
];
e = new SymbolFound(node, filename, extras);
trackVars(parser, node, e);
basename = jsdoc.name.getBasename(e.code.name);
// auto-detect constants
if (node.parent.kind === 'const') {
e.code.kind = 'constant';
}
break;
default:
// ignore
}
if (!e) {
e = {
finishers: []
};
}
return e;
}
// TODO: docs
class Visitor {
// TODO: docs
constructor() {
this._parser = null;
// Mozilla Parser API node visitors added by plugins
this._nodeVisitors = [];
// built-in visitors
this._visitors = [
this.visitNodeComments,
this.visitNode
];
}
/**
* Set the parser instance that visitors can use.
*
* @param {module:jsdoc/src/parser.Parser} parser - The parser instance.
*/
setParser(parser) {
this._parser = parser;
}
// TODO: docs
addAstNodeVisitor(visitor) {
this._nodeVisitors.push(visitor);
}
// TODO: docs
removeAstNodeVisitor(visitor) {
const idx = this._nodeVisitors.indexOf(visitor);
if (idx !== -1) {
this._nodeVisitors.splice(idx, 1);
}
}
// TODO: docs
getAstNodeVisitors() {
return this._nodeVisitors;
}
// TODO: docs; visitor signature is (node, parser, filename)
visit(node, filename) {
for (let visitor of this._visitors) {
visitor.call(this, node, this._parser, filename);
}
return true;
}
/* eslint-disable class-methods-use-this */
// TODO: docs
visitNodeComments(node, parser, filename) {
let comments;
let e;
const isBlock = isBlockComment(node);
let lastTrailingComment;
let nextProgramNode;
let nextProgramNodeIndex;
let rawComment;
function addComments(source) {
comments = comments.concat( source.slice(0) );
}
if ( !hasComments(node) && (!node.type || !isBlock) ) {
return true;
}
comments = isBlock ? [node] : [];
if (node.leadingComments && node.leadingComments.length) {
addComments(node.leadingComments);
}
// trailing comments are always duplicates of leading comments unless they're attached to the
// Program node...
if (node.type === Syntax.Program && node.trailingComments && node.trailingComments.length) {
addComments(node.trailingComments);
}
// ...or if they were comments from the end of the file that were erroneously attached to a
// `'use strict';` declaration (https://github.com/babel/babel/issues/6688).
if (node.type === Syntax.ExpressionStatement && node.directive === 'use strict' &&
node.trailingComments && node.trailingComments.length) {
// to be safe, we verify that the trailing comments came after the next node in the Program
// body, which means the comments were attached to the wrong node
if (node.parent.body.length > 1) {
nextProgramNodeIndex = node.parent.body.indexOf(node) + 1;
nextProgramNode = node.parent.body[nextProgramNodeIndex];
lastTrailingComment = node.trailingComments[node.trailingComments.length - 1];
if (lastTrailingComment.start > nextProgramNode.end) {
addComments(node.trailingComments);
}
}
}
if (node.innerComments && node.innerComments.length) {
addComments(node.innerComments);
}
for (let comment of comments) {
rawComment = getRawComment(comment);
if ( isValidJsdoc(rawComment) ) {
e = new JsdocCommentFound(comment, rawComment, filename);
parser.emit(e.event, e, parser);
if (e.comment !== rawComment) {
updateCommentNode(comment, e.comment);
}
}
}
return true;
}
/* eslint-enable class-methods-use-this */
// TODO: docs
visitNode(node, parser, filename) {
const e = makeSymbolFoundEvent(node, parser, filename);
if (this._nodeVisitors && this._nodeVisitors.length) {
for (let visitor of this._nodeVisitors) {
visitor.visitNode(node, e, parser, filename);
if (e.stopPropagation) {
break;
}
}
}
if (!e.preventDefault) {
parser.emit(e.event, e, parser);
}
// add the node to the parser's lookup table
parser.addDocletRef(e);
for (let finisher of e.finishers) {
finisher.call(parser, e);
}
return true;
}
}
exports.Visitor = Visitor;
+704
View File
@@ -0,0 +1,704 @@
/**
* Traversal utilities for ASTs that are compatible with the ESTree API.
*
* @module jsdoc/src/walker
*/
const astnode = require('jsdoc/src/astnode');
const logger = require('jsdoc/util/logger');
const Syntax = require('jsdoc/src/syntax').Syntax;
// TODO: docs
function getCurrentScope(scopes) {
return scopes[scopes.length - 1] || null;
}
// TODO: docs
function moveLeadingComments(source, target, count) {
if (source.leadingComments) {
if (count === undefined) {
count = source.leadingComments.length;
}
target.leadingComments = source.leadingComments.slice(0, count);
source.leadingComments = source.leadingComments.slice(count);
}
}
// TODO: docs
function moveTrailingComments(source, target, count) {
if (source.trailingComments) {
if (count === undefined) {
count = source.trailingComments.length;
}
target.trailingComments = source.trailingComments.slice(
source.trailingComments.length - count, count
);
source.trailingComments = source.trailingComments.slice(0);
}
}
/* eslint-disable no-empty-function, no-unused-vars */
function leafNode(node, parent, state, cb) {}
/* eslint-enable no-empty-function, no-unused-vars */
// TODO: docs
const walkers = exports.walkers = {};
walkers[Syntax.ArrayExpression] = (node, parent, state, cb) => {
for (let element of node.elements) {
if (element) {
cb(element, node, state);
}
}
};
// TODO: verify correctness
walkers[Syntax.ArrayPattern] = (node, parent, state, cb) => {
for (let element of node.elements) {
// must be an identifier or an expression
if (element && element.type !== Syntax.Identifier) {
cb(element, node, state);
}
}
};
walkers[Syntax.ArrowFunctionExpression] = (node, parent, state, cb) => {
if (node.id) {
cb(node.id, node, state);
}
for (let param of node.params) {
cb(param, node, state);
}
cb(node.body, node, state);
};
walkers[Syntax.AssignmentExpression] = (node, parent, state, cb) => {
cb(node.left, node, state);
cb(node.right, node, state);
};
walkers[Syntax.AssignmentPattern] = walkers[Syntax.AssignmentExpression];
walkers[Syntax.AwaitExpression] = (node, parent, state, cb) => {
cb(node.argument, node, state);
};
walkers[Syntax.BigIntLiteral] = leafNode;
walkers[Syntax.BinaryExpression] = (node, parent, state, cb) => {
cb(node.left, node, state);
cb(node.right, node, state);
};
walkers[Syntax.BindExpression] = (node, parent, state, cb) => {
if (node.object) {
cb(node.object, node, state);
}
cb(node.callee, node, state);
};
walkers[Syntax.BlockStatement] = (node, parent, state, cb) => {
for (let bodyItem of node.body) {
cb(bodyItem, node, state);
}
};
walkers[Syntax.BreakStatement] = leafNode;
walkers[Syntax.CallExpression] = function(node, parent, state, cb) {
cb(node.callee, node, state);
if (node.arguments) {
for (let arg of node.arguments) {
cb(arg, node, state);
}
}
};
walkers[Syntax.CatchClause] = leafNode;
walkers[Syntax.ClassBody] = walkers[Syntax.BlockStatement];
walkers[Syntax.ClassDeclaration] = (node, parent, state, cb) => {
if (node.id) {
cb(node.id, node, state);
}
if (node.superClass) {
cb(node.superClass, node, state);
}
if (node.body) {
cb(node.body, node, state);
}
if (node.decorators) {
for (let decorator of node.decorators) {
cb(decorator, node, state);
}
}
};
walkers[Syntax.ClassExpression] = walkers[Syntax.ClassDeclaration];
// walkers[Syntax.ClassPrivateProperty] is defined later
// walkers[Syntax.ClassProperty] is defined later
// TODO: verify correctness
walkers[Syntax.ComprehensionBlock] = walkers[Syntax.AssignmentExpression];
// TODO: verify correctness
walkers[Syntax.ComprehensionExpression] = (node, parent, state, cb) => {
cb(node.body, node, state);
if (node.filter) {
cb(node.filter, node, state);
}
for (let block of node.blocks) {
cb(block, node, state);
}
};
walkers[Syntax.ConditionalExpression] = (node, parent, state, cb) => {
cb(node.test, node, state);
cb(node.consequent, node, state);
cb(node.alternate, node, state);
};
walkers[Syntax.ContinueStatement] = leafNode;
walkers[Syntax.DebuggerStatement] = leafNode;
walkers[Syntax.Decorator] = (node, parent, state, cb) => {
cb(node.expression, node, state);
};
walkers[Syntax.DoExpression] = (node, parent, state, cb) => {
cb(node.body, node, state);
};
walkers[Syntax.DoWhileStatement] = (node, parent, state, cb) => {
cb(node.test, node, state);
cb(node.body, node, state);
};
walkers[Syntax.EmptyStatement] = leafNode;
walkers[Syntax.ExperimentalRestProperty] = (node, parent, state, cb) => {
cb(node.argument, node, state);
};
walkers[Syntax.ExperimentalSpreadProperty] = walkers[Syntax.ExperimentalRestProperty];
walkers[Syntax.ExportAllDeclaration] = (node, parent, state, cb) => {
if (node.source) {
cb(node.source, node, state);
}
};
walkers[Syntax.ExportDefaultDeclaration] = (node, parent, state, cb) => {
// if the declaration target is a class, move leading comments to the declaration target
if (node.declaration && node.declaration.type === Syntax.ClassDeclaration) {
moveLeadingComments(node, node.declaration);
}
if (node.declaration) {
cb(node.declaration, node, state);
}
};
walkers[Syntax.ExportDefaultSpecifier] = (node, parent, state, cb) => {
cb(node.exported, node, state);
};
walkers[Syntax.ExportNamedDeclaration] = (node, parent, state, cb) => {
if (node.declaration) {
cb(node.declaration, node, state);
}
for (let specifier of node.specifiers) {
cb(specifier, node, state);
}
if (node.source) {
cb(node.source, node, state);
}
};
walkers[Syntax.ExportNamespaceSpecifier] = (node, parent, state, cb) => {
cb(node.exported, node, state);
};
walkers[Syntax.ExportSpecifier] = (node, parent, state, cb) => {
if (node.exported) {
cb(node.exported, node, state);
}
if (node.local) {
cb(node.local, node, state);
}
};
walkers[Syntax.ExpressionStatement] = (node, parent, state, cb) => {
moveLeadingComments(node, node.expression);
cb(node.expression, node, state);
};
walkers[Syntax.File] = (node, parent, state, cb) => {
cb(node.program, node, state);
};
walkers[Syntax.ForInStatement] = (node, parent, state, cb) => {
cb(node.left, node, state);
cb(node.right, node, state);
cb(node.body, node, state);
};
walkers[Syntax.ForOfStatement] = walkers[Syntax.ForInStatement];
walkers[Syntax.ForStatement] = (node, parent, state, cb) => {
if (node.init) {
cb(node.init, node, state);
}
if (node.test) {
cb(node.test, node, state);
}
if (node.update) {
cb(node.update, node, state);
}
cb(node.body, node, state);
};
walkers[Syntax.FunctionDeclaration] = walkers[Syntax.ArrowFunctionExpression];
walkers[Syntax.FunctionExpression] = walkers[Syntax.ArrowFunctionExpression];
walkers[Syntax.Identifier] = leafNode;
walkers[Syntax.IfStatement] = (node, parent, state, cb) => {
cb(node.test, node, state);
cb(node.consequent, node, state);
if (node.alternate) {
cb(node.alternate, node, state);
}
};
walkers[Syntax.Import] = leafNode;
walkers[Syntax.ImportDeclaration] = (node, parent, state, cb) => {
if (node.specifiers) {
for (let specifier of node.specifiers) {
cb(specifier, node, state);
}
}
if (node.source) {
cb(node.source, node, state);
}
};
walkers[Syntax.ImportDefaultSpecifier] = (node, parent, state, cb) => {
if (node.local) {
cb(node.local, node, state);
}
};
walkers[Syntax.ImportNamespaceSpecifier] = walkers[Syntax.ImportDefaultSpecifier];
walkers[Syntax.ImportSpecifier] = walkers[Syntax.ExportSpecifier];
walkers[Syntax.JSXAttribute] = (node, parent, state, cb) => {
cb(node.name, node, state);
if (node.value) {
cb(node.value, node, state);
}
};
walkers[Syntax.JSXClosingElement] = (node, parent, state, cb) => {
cb(node.name, node, state);
};
walkers[Syntax.JSXElement] = (node, parent, state, cb) => {
cb(node.openingElement, node, state);
if (node.closingElement) {
cb(node.closingElement, node, state);
}
for (let child of node.children) {
cb(child, node, state);
}
};
walkers[Syntax.JSXEmptyExpression] = leafNode;
walkers[Syntax.JSXExpressionContainer] = (node, parent, state, cb) => {
cb(node.expression, node, state);
};
walkers[Syntax.JSXIdentifier] = leafNode;
walkers[Syntax.JSXMemberExpression] = (node, parent, state, cb) => {
cb(node.object, node, state);
cb(node.property, node, state);
};
walkers[Syntax.JSXNamespacedName] = (node, parent, state, cb) => {
cb(node.namespace, node, state);
cb(node.name, node, state);
};
walkers[Syntax.JSXOpeningElement] = (node, parent, state, cb) => {
cb(node.name, node, state);
for (let attribute of node.attributes) {
cb(attribute, node, state);
}
};
walkers[Syntax.JSXSpreadAttribute] = (node, parent, state, cb) => {
cb(node.argument, node, state);
};
walkers[Syntax.JSXText] = leafNode;
walkers[Syntax.LabeledStatement] = (node, parent, state, cb) => {
cb(node.body, node, state);
};
// TODO: add scope info??
walkers[Syntax.LetStatement] = (node, parent, state, cb) => {
for (let headItem of node.head) {
cb(headItem.id, node, state);
if (headItem.init) {
cb(headItem.init, node, state);
}
}
cb(node.body, node, state);
};
walkers[Syntax.Literal] = leafNode;
walkers[Syntax.LogicalExpression] = walkers[Syntax.BinaryExpression];
walkers[Syntax.MemberExpression] = (node, parent, state, cb) => {
cb(node.object, node, state);
if (node.property) {
cb(node.property, node, state);
}
};
walkers[Syntax.MetaProperty] = leafNode;
walkers[Syntax.MethodDefinition] = (node, parent, state, cb) => {
if (node.key) {
cb(node.key, node, state);
}
if (node.value) {
cb(node.value, node, state);
}
if (node.decorators) {
for (let decorator of node.decorators) {
cb(decorator, node, state);
}
}
};
walkers[Syntax.ModuleDeclaration] = (node, parent, state, cb) => {
if (node.id) {
cb(node.id, node, state);
}
if (node.source) {
cb(node.source, node, state);
}
if (node.body) {
cb(node.body, node, state);
}
};
walkers[Syntax.NewExpression] = walkers[Syntax.CallExpression];
walkers[Syntax.ObjectExpression] = (node, parent, state, cb) => {
for (let property of node.properties) {
cb(property, node, state);
}
};
walkers[Syntax.ObjectPattern] = walkers[Syntax.ObjectExpression];
walkers[Syntax.PrivateName] = (node, parent, state, cb) => {
cb(node.id, node, state);
};
walkers[Syntax.Program] = (node, parent, state, cb) => {
// if the first item in the body has multiple leading comments, move all but the last one to
// this node. this happens, for example, when a file has a /** @module */ standalone comment
// followed by one or more other comments.
if (node.body[0] && node.body[0].leadingComments && node.body[0].leadingComments.length > 1) {
moveLeadingComments(node.body[0], node, node.body[0].leadingComments.length - 1);
}
// if the last item in the body has trailing comments, move them to this node
if (node.body.length && node.body[node.body.length - 1].trailingComments) {
moveTrailingComments(node.body[node.body.length - 1], node);
}
for (let bodyItem of node.body) {
cb(bodyItem, node, state);
}
};
walkers[Syntax.Property] = (node, parent, state, cb) => {
// move leading comments from key to property node
moveLeadingComments(node.key, node);
if (node.value) {
cb(node.value, node, state);
}
if (node.decorators) {
for (let decorator of node.decorators) {
cb(decorator, node, state);
}
}
};
walkers[Syntax.ClassPrivateProperty] = (node, parent, state, cb) => {
// move leading comments from key to property node
moveLeadingComments(node.key, node);
// add `name` property to key, so we don't have to give this type of node special treatment
// when we resolve its name
node.key.name = node.key.id.name;
if (node.value) {
cb(node.value, node, state);
}
if (node.decorators) {
for (let decorator of node.decorators) {
cb(decorator, node, state);
}
}
};
walkers[Syntax.ClassProperty] = walkers[Syntax.Property];
walkers[Syntax.RestElement] = (node, parent, state, cb) => {
if (node.argument) {
cb(node.argument, node, state);
}
};
walkers[Syntax.ReturnStatement] = (node, parent, state, cb) => {
if (node.argument) {
cb(node.argument, node, state);
}
};
walkers[Syntax.SequenceExpression] = (node, parent, state, cb) => {
for (let expression of node.expressions) {
cb(expression, node, state);
}
};
walkers[Syntax.SpreadElement] = (node, parent, state, cb) => {
if (node.argument) {
cb(node.argument, node, state);
}
};
walkers[Syntax.Super] = leafNode;
walkers[Syntax.SwitchCase] = (node, parent, state, cb) => {
if (node.test) {
cb(node.test, node, state);
}
for (let consequentItem of node.consequent) {
cb(consequentItem, node, state);
}
};
walkers[Syntax.SwitchStatement] = (node, parent, state, cb) => {
cb(node.discriminant, node, state);
for (let caseItem of node.cases) {
cb(caseItem, node, state);
}
};
walkers[Syntax.TaggedTemplateExpression] = (node, parent, state, cb) => {
if (node.tag) {
cb(node.tag, node, state);
}
if (node.quasi) {
cb(node.quasi, node, state);
}
};
walkers[Syntax.TemplateElement] = leafNode;
walkers[Syntax.TemplateLiteral] = (node, parent, state, cb) => {
if (node.quasis && node.quasis.length) {
for (let quasi of node.quasis) {
cb(quasi, node, state);
}
}
if (node.expressions && node.expressions.length) {
for (let expression of node.expressions) {
cb(expression, node, state);
}
}
};
walkers[Syntax.ThisExpression] = leafNode;
walkers[Syntax.ThrowStatement] = (node, parent, state, cb) => {
cb(node.argument, node, state);
};
walkers[Syntax.TryStatement] = (node, parent, state, cb) => {
cb(node.block, node, state);
if (node.handler) {
cb(node.handler.body, node, state);
}
if (node.finalizer) {
cb(node.finalizer, node, state);
}
};
walkers[Syntax.UnaryExpression] = (node, parent, state, cb) => {
cb(node.argument, node, state);
};
walkers[Syntax.UpdateExpression] = walkers[Syntax.UnaryExpression];
walkers[Syntax.VariableDeclaration] = (node, parent, state, cb) => {
// move leading comments to first declarator
moveLeadingComments(node, node.declarations[0]);
for (let declaration of node.declarations) {
cb(declaration, node, state);
}
};
walkers[Syntax.VariableDeclarator] = (node, parent, state, cb) => {
cb(node.id, node, state);
if (node.init) {
cb(node.init, node, state);
}
};
walkers[Syntax.WhileStatement] = walkers[Syntax.DoWhileStatement];
walkers[Syntax.WithStatement] = (node, parent, state, cb) => {
cb(node.object, node, state);
cb(node.body, node, state);
};
walkers[Syntax.YieldExpression] = (node, parent, state, cb) => {
if (node.argument) {
cb(node.argument, node, state);
}
};
/**
* Create a walker that can traverse an AST that is consistent with the Mozilla Parser API.
*
* @memberof module:jsdoc/src/walker
*/
class Walker {
// TODO: docs
constructor(walkerFuncs = walkers) {
this._walkers = walkerFuncs;
}
// TODO: docs
_recurse(filename, ast) {
const self = this;
const state = {
filename: filename,
nodes: [],
scopes: []
};
function logUnknownNodeType({type}) {
logger.debug('Found a node with unrecognized type %s. Ignoring the node and its ' +
'descendants.', type);
}
function cb(node, parent, cbState) {
let currentScope;
const isScope = astnode.isScope(node);
astnode.addNodeProperties(node);
node.parent = parent || null;
currentScope = getCurrentScope(cbState.scopes);
if (currentScope) {
node.enclosingScope = currentScope;
}
if (isScope) {
cbState.scopes.push(node);
}
cbState.nodes.push(node);
if (!self._walkers[node.type]) {
logUnknownNodeType(node);
} else {
self._walkers[node.type](node, parent, cbState, cb);
}
if (isScope) {
cbState.scopes.pop();
}
}
cb(ast, null, state);
return state;
}
// TODO: docs
recurse(ast, visitor, filename) {
let shouldContinue;
const state = this._recurse(filename, ast);
if (visitor) {
for (let node of state.nodes) {
shouldContinue = visitor.visit(node, filename);
if (!shouldContinue) {
break;
}
}
}
return ast;
}
}
exports.Walker = Walker;
+198
View File
@@ -0,0 +1,198 @@
/**
* Functionality related to JSDoc tags.
* @module jsdoc/tag
* @requires module:jsdoc/env
* @requires module:jsdoc/path
* @requires module:jsdoc/tag/dictionary
* @requires module:jsdoc/tag/validator
* @requires module:jsdoc/tag/type
* @requires module:jsdoc/util/logger
* @requires module:util
*/
const jsdoc = {
env: require('jsdoc/env'),
tag: {
dictionary: require('jsdoc/tag/dictionary'),
validator: require('jsdoc/tag/validator'),
type: require('jsdoc/tag/type')
},
util: {
logger: require('jsdoc/util/logger')
}
};
const path = require('jsdoc/path');
const util = require('util');
// Check whether the text is the same as a symbol name with leading or trailing whitespace. If so,
// the whitespace must be preserved, and the text cannot be trimmed.
function mustPreserveWhitespace(text, meta) {
return meta && meta.code && meta.code.name === text && text.match(/(?:^\s+)|(?:\s+$)/);
}
function trim(text, opts, meta) {
let indentMatcher;
let match;
opts = opts || {};
text = String(typeof text === 'undefined' ? '' : text);
if ( mustPreserveWhitespace(text, meta) ) {
text = util.format('"%s"', text);
}
else if (opts.keepsWhitespace) {
text = text.replace(/^[\n\r\f]+|[\n\r\f]+$/g, '');
if (opts.removesIndent) {
match = text.match(/^([ \t]+)/);
if (match && match[1]) {
indentMatcher = new RegExp(`^${match[1]}`, 'gm');
text = text.replace(indentMatcher, '');
}
}
}
else {
text = text.replace(/^\s+|\s+$/g, '');
}
return text;
}
function addHiddenProperty(obj, propName, propValue) {
Object.defineProperty(obj, propName, {
value: propValue,
writable: true,
enumerable: Boolean(jsdoc.env.opts.debug),
configurable: true
});
}
function parseType({text, originalTitle}, {canHaveName, canHaveType}, meta) {
try {
return jsdoc.tag.type.parse(text, canHaveName, canHaveType);
}
catch (e) {
jsdoc.util.logger.error(
'Unable to parse a tag\'s type expression%s with tag title "%s" and text "%s": %s',
meta.filename ? ( ` for source file ${path.join(meta.path, meta.filename)}${meta.lineno ? (` in line ${meta.lineno}`) : ''}` ) : '',
originalTitle,
text,
e.message
);
return {};
}
}
function processTagText(tag, tagDef, meta) {
let tagType;
if (tagDef.onTagText) {
tag.text = tagDef.onTagText(tag.text);
}
if (tagDef.canHaveType || tagDef.canHaveName) {
/** The value property represents the result of parsing the tag text. */
tag.value = {};
tagType = parseType(tag, tagDef, meta);
// It is possible for a tag to *not* have a type but still have
// optional or defaultvalue, e.g. '@param [foo]'.
// Although tagType.type.length == 0 we should still copy the other properties.
if (tagType.type) {
if (tagType.type.length) {
tag.value.type = {
names: tagType.type
};
addHiddenProperty(tag.value.type, 'parsedType', tagType.parsedType);
}
['optional', 'nullable', 'variable', 'defaultvalue'].forEach(prop => {
if (typeof tagType[prop] !== 'undefined') {
tag.value[prop] = tagType[prop];
}
});
}
if (tagType.text && tagType.text.length) {
tag.value.description = tagType.text;
}
if (tagDef.canHaveName) {
// note the dash is a special case: as a param name it means "no name"
if (tagType.name && tagType.name !== '-') { tag.value.name = tagType.name; }
}
}
else {
tag.value = tag.text;
}
}
/**
* Replace the existing tag dictionary with a new tag dictionary.
*
* Used for testing only. Do not call this method directly. Instead, call
* {@link module:jsdoc/doclet._replaceDictionary}, which also updates this module's tag dictionary.
*
* @private
* @param {module:jsdoc/tag/dictionary.Dictionary} dict - The new tag dictionary.
*/
exports._replaceDictionary = function _replaceDictionary(dict) {
jsdoc.tag.dictionary = dict;
};
/**
* Represents a single doclet tag.
*/
class Tag {
/**
* Constructs a new tag object. Calls the tag validator.
*
* @param {string} tagTitle
* @param {string=} tagBody
* @param {object=} meta
*/
constructor(tagTitle, tagBody, meta) {
let tagDef;
let trimOpts;
meta = meta || {};
this.originalTitle = trim(tagTitle);
/** The title of the tag (for example, `title` in `@title text`). */
this.title = jsdoc.tag.dictionary.normalise(this.originalTitle);
tagDef = jsdoc.tag.dictionary.lookUp(this.title);
trimOpts = {
keepsWhitespace: tagDef.keepsWhitespace,
removesIndent: tagDef.removesIndent
};
/**
* The text following the tag (for example, `text` in `@title text`).
*
* Whitespace is trimmed from the tag text as follows:
*
* + If the tag's `keepsWhitespace` option is falsy, all leading and trailing whitespace are
* removed.
* + If the tag's `keepsWhitespace` option is set to `true`, leading and trailing whitespace are
* not trimmed, unless the `removesIndent` option is also enabled.
* + If the tag's `removesIndent` option is set to `true`, any indentation that is shared by
* every line in the string is removed. This option is ignored unless `keepsWhitespace` is set
* to `true`.
*
* **Note**: If the tag text is the name of a symbol, and the symbol's name includes leading or
* trailing whitespace (for example, the property names in `{ ' ': true, ' foo ': false }`),
* the tag text is not trimmed. Instead, the tag text is wrapped in double quotes to prevent the
* whitespace from being trimmed.
*/
this.text = trim(tagBody, trimOpts, meta);
if (this.text) {
processTagText(this, tagDef, meta);
}
jsdoc.tag.validator.validate(this, tagDef, meta);
}
}
exports.Tag = Tag;
+120
View File
@@ -0,0 +1,120 @@
/** @module jsdoc/tag/dictionary */
const definitions = require('jsdoc/tag/dictionary/definitions');
const hasOwnProp = Object.prototype.hasOwnProperty;
let dictionary;
/** @private */
class TagDefinition {
constructor(dict, title, etc) {
const self = this;
etc = etc || {};
this.title = dict.normalise(title);
Object.defineProperty(this, '_dictionary', {
value: dict
});
Object.keys(etc).forEach(p => {
self[p] = etc[p];
});
}
/** @private */
synonym(synonymName) {
this._dictionary.defineSynonym(this.title, synonymName);
return this;
}
}
/**
* @alias module:jsdoc/tag/dictionary.Dictionary
*/
class Dictionary {
constructor() {
this._tags = {};
this._tagSynonyms = {};
// The longnames for `Package` objects include a `package` namespace. There's no `package` tag,
// though, so we declare the namespace here.
this._namespaces = ['package'];
}
_defineNamespace(title) {
title = this.normalise(title || '');
if (title && !this._namespaces.includes(title)) {
this._namespaces.push(title);
}
return this;
}
defineTag(title, opts) {
const tagDef = new TagDefinition(this, title, opts);
this._tags[tagDef.title] = tagDef;
if (opts && opts.isNamespace) {
this._defineNamespace(tagDef.title);
}
return this._tags[tagDef.title];
}
defineSynonym(title, synonym) {
this._tagSynonyms[synonym.toLowerCase()] = this.normalise(title);
}
getNamespaces() {
return this._namespaces.slice(0);
}
lookUp(title) {
title = this.normalise(title);
if ( hasOwnProp.call(this._tags, title) ) {
return this._tags[title];
}
return false;
}
isNamespace(kind) {
if (kind) {
kind = this.normalise(kind);
if (this._namespaces.includes(kind)) {
return true;
}
}
return false;
}
normalise(title) {
const canonicalName = title.toLowerCase();
if ( hasOwnProp.call(this._tagSynonyms, canonicalName) ) {
return this._tagSynonyms[canonicalName];
}
return canonicalName;
}
normalize(title) {
return this.normalise(title);
}
}
// initialize the default dictionary
dictionary = new Dictionary();
definitions.defineTags(dictionary);
// make the constructor available for unit-testing purposes
dictionary.Dictionary = Dictionary;
/** @type {module:jsdoc/tag/dictionary.Dictionary} */
module.exports = dictionary;
File diff suppressed because it is too large Load Diff
+135
View File
@@ -0,0 +1,135 @@
/**
* @module jsdoc/tag/inline
*/
/**
* Information about an inline tag that was found within a string.
*
* @typedef {Object} InlineTagInfo
* @memberof module:jsdoc/tag/inline
* @property {?string} completeTag - The entire inline tag, including its enclosing braces.
* @property {?string} tag - The tag whose text was found.
* @property {?string} text - The tag text that was found.
*/
/**
* Information about the results of replacing inline tags within a string.
*
* @typedef {Object} InlineTagResult
* @memberof module:jsdoc/tag/inline
* @property {Array.<module:jsdoc/tag/inline.InlineTagInfo>} tags - The inline tags that were found.
* @property {string} newString - The updated text string after extracting or replacing the inline
* tags.
*/
/**
* Text-replacing function for strings that contain an inline tag.
*
* @callback InlineTagReplacer
* @memberof module:jsdoc/tag/inline
* @param {string} string - The complete string containing the inline tag.
* @param {module:jsdoc/tag/inline.InlineTagInfo} tagInfo - Information about the inline tag.
* @return {string} An updated version of the complete string.
*/
/**
* Create a regexp that matches a specific inline tag, or all inline tags.
*
* @private
* @memberof module:jsdoc/tag/inline
* @param {?string} tagName - The inline tag that the regexp will match. May contain regexp
* characters. If omitted, matches any string.
* @param {?string} prefix - A prefix for the regexp. Defaults to an empty string.
* @param {?string} suffix - A suffix for the regexp. Defaults to an empty string.
* @returns {RegExp} A regular expression that matches the requested inline tag.
*/
function regExpFactory(tagName = '\\S+', prefix = '', suffix = '') {
return new RegExp(`${prefix}\\{@${tagName}\\s+((?:.|\n)+?)\\}${suffix}`, 'i');
}
/**
* Check whether a string is an inline tag. You can check for a specific inline tag or for any valid
* inline tag.
*
* @param {string} string - The string to check.
* @param {?string} tagName - The inline tag to match. May contain regexp characters. If this
* parameter is omitted, this method returns `true` for any valid inline tag.
* @returns {boolean} Set to `true` if the string is a valid inline tag or `false` in all other
* cases.
*/
exports.isInlineTag = (string, tagName) => regExpFactory(tagName, '^', '$').test(string);
/**
* Replace all instances of multiple inline tags with other text.
*
* @param {string} string - The string in which to replace the inline tags.
* @param {Object} replacers - The functions that are used to replace text in the string. The keys
* must contain tag names (for example, `link`), and the values must contain functions with the
* type {@link module:jsdoc/tag/inline.InlineTagReplacer}.
* @return {module:jsdoc/tag/inline.InlineTagResult} The updated string, as well as information
* about the inline tags that were found.
*/
exports.replaceInlineTags = (string, replacers) => {
const tagInfo = [];
function replaceMatch(replacer, tag, match, text) {
const matchedTag = {
completeTag: match,
tag: tag,
text: text
};
tagInfo.push(matchedTag);
return replacer(string, matchedTag);
}
string = string || '';
Object.keys(replacers).forEach(replacer => {
const tagRegExp = regExpFactory(replacer);
let matches;
let previousString;
// call the replacer once for each match
do {
matches = tagRegExp.exec(string);
if (matches) {
previousString = string;
string = replaceMatch(replacers[replacer], replacer, matches[0], matches[1]);
}
} while (matches && previousString !== string);
});
return {
tags: tagInfo,
newString: string.trim()
};
};
/**
* Replace all instances of an inline tag with other text.
*
* @param {string} string - The string in which to replace the inline tag.
* @param {string} tag - The name of the inline tag to replace.
* @param {module:jsdoc/tag/inline.InlineTagReplacer} replacer - The function that is used to
* replace text in the string.
* @return {module:jsdoc/tag/inline.InlineTagResult} The updated string, as well as information
* about the inline tags that were found.
*/
exports.replaceInlineTag = (string, tag, replacer) => {
const replacers = {};
replacers[tag] = replacer;
return exports.replaceInlineTags(string, replacers);
};
/**
* Extract inline tags from a string, replacing them with an empty string.
*
* @param {string} string - The string from which to extract text.
* @param {?string} tag - The inline tag to extract.
* @return {module:jsdoc/tag/inline.InlineTagResult} The updated string, as well as information
* about the inline tags that were found.
*/
exports.extractInlineTag = (string, tag) => exports.replaceInlineTag(string, tag, (str, {completeTag}) => str.replace(completeTag, ''));
+306
View File
@@ -0,0 +1,306 @@
/**
* @module jsdoc/tag/type
*/
const catharsis = require('catharsis');
const jsdoc = {
name: require('jsdoc/name'),
tag: {
inline: require('jsdoc/tag/inline')
},
util: {
cast: require('jsdoc/util/cast')
}
};
/**
* Information about a type expression extracted from tag text.
*
* @typedef TypeExpressionInfo
* @memberof module:jsdoc/tag/type
* @property {string} expression - The type expression.
* @property {string} text - The updated tag text.
*/
/** @private */
function unescapeBraces(text) {
return text.replace(/\\\{/g, '{')
.replace(/\\\}/g, '}');
}
/**
* Extract a type expression from the tag text.
*
* @private
* @param {string} string - The tag text.
* @return {module:jsdoc/tag/type.TypeExpressionInfo} The type expression and updated tag text.
*/
function extractTypeExpression(string) {
let completeExpression;
let count = 0;
let position = 0;
let expression = '';
const startIndex = string.search(/\{[^@]/);
let textStartIndex;
if (startIndex !== -1) {
// advance to the first character in the type expression
position = textStartIndex = startIndex + 1;
count++;
while (position < string.length) {
switch (string[position]) {
case '\\':
// backslash is an escape character, so skip the next character
position++;
break;
case '{':
count++;
break;
case '}':
count--;
break;
default:
// do nothing
}
if (count === 0) {
completeExpression = string.slice(startIndex, position + 1);
expression = string.slice(textStartIndex, position).trim();
break;
}
position++;
}
}
string = completeExpression ? string.replace(completeExpression, '') : string;
return {
expression: unescapeBraces(expression),
newString: string.trim()
};
}
/** @private */
function getTagInfo(tagValue, canHaveName, canHaveType) {
let name = '';
let typeExpression = '';
let text = tagValue;
let expressionAndText;
let nameAndDescription;
let typeOverride;
if (canHaveType) {
expressionAndText = extractTypeExpression(text);
typeExpression = expressionAndText.expression;
text = expressionAndText.newString;
}
if (canHaveName) {
nameAndDescription = jsdoc.name.splitName(text);
name = nameAndDescription.name;
text = nameAndDescription.description;
}
// an inline @type tag, like {@type Foo}, overrides the type expression
if (canHaveType) {
typeOverride = jsdoc.tag.inline.extractInlineTag(text, 'type');
if (typeOverride.tags && typeOverride.tags[0]) {
typeExpression = typeOverride.tags[0].text;
}
text = typeOverride.newString;
}
return {
name: name,
typeExpression: typeExpression,
text: text
};
}
/**
* Information provided in a JSDoc tag.
*
* @typedef {Object} TagInfo
* @memberof module:jsdoc/tag/type
* @property {string} TagInfo.defaultvalue - The default value of the member.
* @property {string} TagInfo.name - The name of the member (for example, `myParamName`).
* @property {boolean} TagInfo.nullable - Indicates whether the member can be set to `null` or
* `undefined`.
* @property {boolean} TagInfo.optional - Indicates whether the member is optional.
* @property {string} TagInfo.text - Descriptive text for the member (for example, `The user's email
* address.`).
* @property {Array.<string>} TagInfo.type - The type or types that the member can contain (for
* example, `string` or `MyNamespace.MyClass`).
* @property {string} TagInfo.typeExpression - The type expression that was parsed to identify the
* types.
* @property {boolean} TagInfo.variable - Indicates whether the number of members that are provided
* can vary (for example, in a function that accepts any number of parameters).
*/
// TODO: move to module:jsdoc/name?
/**
* Extract JSDoc-style type information from the name specified in the tag info, including the
* member name; whether the member is optional; and the default value of the member.
*
* @private
* @param {module:jsdoc/tag/type.TagInfo} tagInfo - Information contained in the tag.
* @return {module:jsdoc/tag/type.TagInfo} Updated information from the tag.
*/
function parseName(tagInfo) {
// like '[foo]' or '[ foo ]' or '[foo=bar]' or '[ foo=bar ]' or '[ foo = bar ]'
// or 'foo=bar' or 'foo = bar'
if ( /^(\[)?\s*(.+?)\s*(\])?$/.test(tagInfo.name) ) {
tagInfo.name = RegExp.$2;
// were the "optional" brackets present?
if (RegExp.$1 && RegExp.$3) {
tagInfo.optional = true;
}
// like 'foo=bar' or 'foo = bar'
if ( /^(.+?)\s*=\s*(.+)$/.test(tagInfo.name) ) {
tagInfo.name = RegExp.$1;
tagInfo.defaultvalue = jsdoc.util.cast.cast(RegExp.$2);
}
}
return tagInfo;
}
/** @private */
function getTypeStrings(parsedType, isOutermostType) {
let applications;
let typeString;
let types = [];
const TYPES = catharsis.Types;
switch (parsedType.type) {
case TYPES.AllLiteral:
types.push('*');
break;
case TYPES.FunctionType:
types.push('function');
break;
case TYPES.NameExpression:
types.push(parsedType.name);
break;
case TYPES.NullLiteral:
types.push('null');
break;
case TYPES.RecordType:
types.push('Object');
break;
case TYPES.TypeApplication:
// if this is the outermost type, we strip the modifiers; otherwise, we keep them
if (isOutermostType) {
applications = parsedType.applications.map(application =>
catharsis.stringify(application)).join(', ');
typeString = `${getTypeStrings(parsedType.expression)[0]}.<${applications}>`;
types.push(typeString);
}
else {
types.push( catharsis.stringify(parsedType) );
}
break;
case TYPES.TypeUnion:
parsedType.elements.forEach(element => {
types = types.concat( getTypeStrings(element) );
});
break;
case TYPES.UndefinedLiteral:
types.push('undefined');
break;
case TYPES.UnknownLiteral:
types.push('?');
break;
default:
// this shouldn't happen
throw new Error(`unrecognized type ${parsedType.type} in parsed type: ${parsedType}`);
}
return types;
}
/**
* Extract JSDoc-style and Closure Compiler-style type information from the type expression
* specified in the tag info.
*
* @private
* @param {module:jsdoc/tag/type.TagInfo} tagInfo - Information contained in the tag.
* @return {module:jsdoc/tag/type.TagInfo} Updated information from the tag.
*/
function parseTypeExpression(tagInfo) {
let parsedType;
// don't try to parse empty type expressions
if (!tagInfo.typeExpression) {
return tagInfo;
}
try {
parsedType = catharsis.parse(tagInfo.typeExpression, {
jsdoc: true,
useCache: false
});
}
catch (e) {
// always re-throw so the caller has a chance to report which file was bad
throw new Error(`Invalid type expression "${tagInfo.typeExpression}": ${e.message}`);
}
tagInfo.type = tagInfo.type.concat( getTypeStrings(parsedType, true) );
tagInfo.parsedType = parsedType;
// Catharsis and JSDoc use the same names for 'optional' and 'nullable'...
['optional', 'nullable'].forEach(key => {
if (parsedType[key] !== null && parsedType[key] !== undefined) {
tagInfo[key] = parsedType[key];
}
});
// ...but not 'variable'.
if (parsedType.repeatable !== null && parsedType.repeatable !== undefined) {
tagInfo.variable = parsedType.repeatable;
}
return tagInfo;
}
// TODO: allow users to add/remove type parsers (perhaps via plugins)
const typeParsers = [parseName, parseTypeExpression];
/**
* Parse the value of a JSDoc tag.
*
* @param {string} tagValue - The value of the tag. For example, the tag `@param {string} name` has
* a value of `{string} name`.
* @param {boolean} canHaveName - Indicates whether the value can include a symbol name.
* @param {boolean} canHaveType - Indicates whether the value can include a type expression that
* describes the symbol.
* @return {module:jsdoc/tag/type.TagInfo} Information obtained from the tag.
* @throws {Error} Thrown if a type expression cannot be parsed.
*/
exports.parse = (tagValue, canHaveName, canHaveType) => {
let tagInfo;
if (typeof tagValue !== 'string') {
tagValue = '';
}
tagInfo = getTagInfo(tagValue, canHaveName, canHaveType);
tagInfo.type = tagInfo.type || [];
typeParsers.forEach(parser => {
tagInfo = parser(tagInfo);
});
// if we wanted a type, but the parsers didn't add any type names, use the type expression
if (canHaveType && !tagInfo.type.length && tagInfo.typeExpression) {
tagInfo.type = [tagInfo.typeExpression];
}
return tagInfo;
};
+51
View File
@@ -0,0 +1,51 @@
/**
* @module jsdoc/tag/validator
* @requires jsdoc/tag/dictionary
*/
const env = require('jsdoc/env');
const logger = require('jsdoc/util/logger');
function buildMessage(tagName, {filename, lineno, comment}, desc) {
let result = `The @${tagName} tag ${desc}. File: ${filename}, line: ${lineno}`;
if (comment) {
result += `\n${comment}`;
}
return result;
}
/**
* Validate the given tag.
*/
exports.validate = ({title, text, value}, tagDef, meta) => {
const allowUnknownTags = env.conf.tags.allowUnknownTags;
// handle cases where the tag definition does not exist
if (!tagDef) {
// log an error if unknown tags are not allowed
if (!allowUnknownTags ||
(Array.isArray(allowUnknownTags) &&
!allowUnknownTags.includes(title))) {
logger.error( buildMessage(title, meta, 'is not a known tag') );
}
// stop validation, since there's nothing to validate against
return;
}
// check for errors that make the tag useless
if (!text && tagDef.mustHaveValue) {
logger.error( buildMessage(title, meta, 'requires a value') );
}
// check for minor issues that are usually harmless
else if (text && tagDef.mustNotHaveValue) {
logger.warn( buildMessage(title, meta,
'does not permit a value; the value will be ignored') );
}
else if (value && value.description && tagDef.mustNotHaveDescription) {
logger.warn( buildMessage(title, meta,
'does not permit a description; the description will be ignored') );
}
};
+80
View File
@@ -0,0 +1,80 @@
/**
* Wrapper for underscore's template utility to allow loading templates from files.
* @module jsdoc/template
*/
const _ = require('underscore');
const fs = require('jsdoc/fs');
const path = require('path');
/**
* Underscore template helper.
*/
class Template {
/**
* @param {string} filepath - Templates directory.
*/
constructor(filepath) {
this.path = filepath;
this.layout = null;
this.cache = {};
// override default template tag settings
this.settings = {
evaluate: /<\?js([\s\S]+?)\?>/g,
interpolate: /<\?js=([\s\S]+?)\?>/g,
escape: /<\?js~([\s\S]+?)\?>/g
};
}
/**
* Loads template from given file.
* @param {string} file - Template filename.
* @return {function} Returns template closure.
*/
load(file) {
return _.template(fs.readFileSync(file, 'utf8'), null, this.settings);
}
/**
* Renders template using given data.
*
* This is low-level function, for rendering full templates use {@link Template.render()}.
*
* @param {string} file - Template filename.
* @param {object} data - Template variables (doesn't have to be object, but passing variables dictionary is best way and most common use).
* @return {string} Rendered template.
*/
partial(file, data) {
file = path.resolve(this.path, file);
// load template into cache
if (!(file in this.cache)) {
this.cache[file] = this.load(file);
}
// keep template helper context
return this.cache[file].call(this, data);
}
/**
* Renders template with given data.
*
* This method automaticaly applies layout if set.
*
* @param {string} file - Template filename.
* @param {object} data - Template variables (doesn't have to be object, but passing variables dictionary is best way and most common use).
* @return {string} Rendered template.
*/
render(file, data) {
// main content
let content = this.partial(file, data);
// apply layout
if (this.layout) {
data.content = content;
content = this.partial(this.layout, data);
}
return content;
}
}
exports.Template = Template;
+153
View File
@@ -0,0 +1,153 @@
/**
* @module jsdoc/tutorial
*/
const markdown = require('jsdoc/util/markdown');
const hasOwnProp = Object.prototype.hasOwnProperty;
/**
* Removes child tutorial from the parent. Does *not* unset child.parent though.
*
* @param {Tutorial} parent - parent tutorial.
* @param {Tutorial} child - Old child.
* @private
*/
function removeChild({children}, child) {
const index = children.indexOf(child);
if (index !== -1) {
children.splice(index, 1);
}
}
/**
* Adds a child to the parent tutorial. Does *not* set child.parent though.
*
* @param {Tutorial} parent - parent tutorial.
* @param {Tutorial} child - New child.
* @private
*/
function addChild({children}, child) {
children.push(child);
}
/**
* Represents a single JSDoc tutorial.
*/
class Tutorial {
/**
* @param {string} name - Tutorial name.
* @param {string} content - Text content.
* @param {number} type - Source formating.
*/
constructor(name, content, type) {
this.title = this.name = this.longname = name;
this.content = content;
this.type = type;
// default values
this.parent = null;
this.children = [];
}
/**
* Moves children from current parent to different one.
*
* @param {?Tutorial} parent - New parent. If null, the tutorial has no parent.
*/
setParent(parent) {
// removes node from old parent
if (this.parent) {
removeChild(this.parent, this);
}
this.parent = parent;
if (parent) {
addChild(parent, this);
}
}
/* eslint-disable class-methods-use-this */
/**
* Removes children from current node.
*
* @param {Tutorial} child - Old child.
*/
removeChild(child) {
child.setParent(null);
}
/* eslint-enable class-methods-use-this */
/**
* Adds new children to current node.
*
* @param {Tutorial} child - New child.
*/
addChild(child) {
child.setParent(this);
}
/**
* Prepares source.
*
* @return {string} HTML source.
*/
parse() {
switch (this.type) {
// nothing to do
case exports.TYPES.HTML:
return this.content;
// markdown
case exports.TYPES.MARKDOWN:
return markdown.getParser()(this.content);
// uhm... should we react somehow?
// if not then this case can be merged with TYPES.HTML
default:
return this.content;
}
}
}
exports.Tutorial = Tutorial;
/**
* Represents the root tutorial.
* @extends {module:jsdoc/tutorial.Tutorial}
*/
class RootTutorial extends Tutorial {
constructor() {
super('', '', null);
this._tutorials = {};
}
/**
* Retrieve a tutorial by name.
* @param {string} name - Tutorial name.
* @return {module:jsdoc/tutorial.Tutorial} Tutorial instance.
*/
getByName(name) {
return hasOwnProp.call(this._tutorials, name) && this._tutorials[name];
}
/**
* Add a child tutorial to the root.
* @param {module:jsdoc/tutorial.Tutorial} child - Child tutorial.
*/
_addTutorial(child) {
this._tutorials[child.name] = child;
}
}
exports.RootTutorial = RootTutorial;
/**
* Tutorial source types.
*
* @enum {number}
*/
exports.TYPES = {
HTML: 1,
MARKDOWN: 2
};
+183
View File
@@ -0,0 +1,183 @@
/**
* @module jsdoc/tutorial/resolver
*/
const env = require('jsdoc/env');
const fs = require('jsdoc/fs');
const logger = require('jsdoc/util/logger');
const path = require('path');
const stripBom = require('jsdoc/util/stripbom');
const tutorial = require('jsdoc/tutorial');
const hasOwnProp = Object.prototype.hasOwnProperty;
// TODO: make this an instance member of `RootTutorial`?
const conf = {};
const finder = /^(.*)\.(x(?:ht)?ml|html?|md|markdown|json)$/i;
/** checks if `conf` is the metadata for a single tutorial.
* A tutorial's metadata has a property 'title' and/or a property 'children'.
* @param {object} json - the object we want to test (typically from JSON.parse)
* @returns {boolean} whether `json` could be the metadata for a tutorial.
*/
function isTutorialJSON(json) {
// if conf.title exists or conf.children exists, it is metadata for a tutorial
return (hasOwnProp.call(json, 'title') || hasOwnProp.call(json, 'children'));
}
/**
* Root tutorial.
* @type {module:jsdoc/tutorial.Root}
*/
exports.root = new tutorial.RootTutorial();
/**
* Helper function that adds tutorial configuration to the `conf` variable. This helps when multiple
* tutorial configurations are specified in one object, or when a tutorial's children are specified
* as tutorial configurations as opposed to an array of tutorial names.
*
* Recurses as necessary to ensure all tutorials are added.
*
* @param {string} name - if `meta` is a configuration for a single tutorial, this is that
* tutorial's name.
* @param {object} meta - object that contains tutorial information. Can either be for a single
* tutorial, or for multiple (where each key in `meta` is the tutorial name and each value is the
* information for a single tutorial). Additionally, a tutorial's 'children' property may either be
* an array of strings (names of the child tutorials), OR an object giving the configuration for the
* child tutorials.
*/
function addTutorialConf(name, meta) {
let names;
if (isTutorialJSON(meta)) {
// if the children are themselves tutorial defintions as opposed to an
// array of strings, add each child.
if (hasOwnProp.call(meta, 'children') && !Array.isArray(meta.children)) {
names = Object.keys(meta.children);
for (let childName of names) {
addTutorialConf(childName, meta.children[childName]);
}
// replace with an array of names.
meta.children = names;
}
// check if the tutorial has already been defined...
if (hasOwnProp.call(conf, name)) {
logger.warn(`Metadata for the tutorial ${name} is defined more than once. Only the first definition will be used.`);
} else {
conf[name] = meta;
}
} else {
// keys are tutorial names, values are `Tutorial` instances
names = Object.keys(meta);
for (let tutorialName of names) {
addTutorialConf(tutorialName, meta[tutorialName]);
}
}
}
/**
* Add a tutorial.
* @param {module:jsdoc/tutorial.Tutorial} current - Tutorial to add.
*/
exports.addTutorial = current => {
if (exports.root.getByName(current.name)) {
logger.warn('The tutorial %s is defined more than once. Only the first definition will be used.', current.name);
} else {
// by default, the root tutorial is the parent
current.setParent(exports.root);
exports.root._addTutorial(current);
}
};
/**
* Load tutorials from the given path.
* @param {string} filepath - Tutorials directory.
*/
exports.load = filepath => {
let content;
let current;
const files = fs.ls(filepath, env.opts.recurse ? env.conf.recurseDepth : undefined);
let name;
let match;
let type;
// tutorials handling
files.forEach(file => {
match = file.match(finder);
// any filetype that can apply to tutorials
if (match) {
name = path.basename(match[1]);
content = fs.readFileSync(file, env.opts.encoding);
switch (match[2].toLowerCase()) {
// HTML type
case 'xml':
case 'xhtml':
case 'html':
case 'htm':
type = tutorial.TYPES.HTML;
break;
// Markdown typs
case 'md':
case 'markdown':
type = tutorial.TYPES.MARKDOWN;
break;
// configuration file
case 'json':
addTutorialConf(name, JSON.parse(stripBom.strip(content)));
// don't add this as a tutorial
return;
// how can it be? check `finder' regexp
// not a file we want to work with
default:
return;
}
current = new tutorial.Tutorial(name, content, type);
exports.addTutorial(current);
}
});
};
/**
* Resolves hierarchical structure.
*/
exports.resolve = () => {
let item;
let current;
Object.keys(conf).forEach(name => {
current = exports.root.getByName(name);
// TODO: should we complain about this?
if (!current) {
return;
}
item = conf[name];
// set title
if (item.title) {
current.title = item.title;
}
// add children
if (item.children) {
item.children.forEach(child => {
const childTutorial = exports.root.getByName(child);
if (!childTutorial) {
logger.error('Missing child tutorial: %s', child);
}
else {
childTutorial.setParent(current);
}
});
}
});
};
+95
View File
@@ -0,0 +1,95 @@
/**
* Module to convert values between various JavaScript types.
* @module
* @private
*/
/**
* Check whether a string contains a boolean or numeric value, and convert the string to the
* appropriate type if necessary.
*
* @private
* @param {string} str - The string to convert.
* @return {(string|number|boolean)} The converted value.
*/
function castString(str) {
let number;
let result;
switch (str) {
case 'true':
result = true;
break;
case 'false':
result = false;
break;
case 'NaN':
result = NaN;
break;
case 'null':
result = null;
break;
case 'undefined':
result = undefined;
break;
default:
if (typeof str === 'string') {
if (str.includes('.')) {
number = parseFloat(str);
}
else {
number = parseInt(str, 10);
}
if ( String(number) === str && !isNaN(number) ) {
result = number;
}
else {
result = str;
}
}
}
return result;
}
/**
* Check whether a string contains a boolean or numeric value, and convert the string to the
* appropriate type if necessary.
*
* If an object or array is passed to this method, the object or array's values will be recursively
* converted to the appropriate types. The original object or array is not modified.
*
* @private
* @param {(string|Object|Array)} item - The item whose type will be converted.
* @return {(string|number|boolean|Object|Array)} The converted value.
*/
exports.cast = function cast(item) {
let result;
if ( Array.isArray(item) ) {
result = [];
for (let i = 0, l = item.length; i < l; i++) {
result[i] = cast(item[i]);
}
}
else if (typeof item === 'object' && item !== null) {
result = {};
Object.keys(item).forEach(prop => {
result[prop] = cast(item[prop]);
});
}
else if (typeof item === 'string') {
result = castString(item);
}
else {
result = item;
}
return result;
};
+79
View File
@@ -0,0 +1,79 @@
/**
* Deep clone a simple object. Ignores non-enumerable properties.
* @private
*/
const setDefined = typeof Set !== 'undefined';
function addItem(set, item) {
if (setDefined) {
set.add(item);
}
else if (!set.includes(item)) {
set.push(item);
}
}
function hasItem(set, item) {
if (setDefined) {
return set.has(item);
}
else {
return set.includes(item);
}
}
// TODO: can we remove the circular-ref checking? pretty sure it's not needed anymore...
// if we still need this for some reason, we should share code with jsdoc/util/dumper
function doop(o, seen) {
let clone;
let descriptor;
let props;
let i;
let l;
if (!seen) {
seen = setDefined ? new Set() : [];
}
if (o instanceof Object && o.constructor !== Function) {
if ( hasItem(seen, o) ) {
clone = '<CircularRef>';
}
else {
addItem(seen, o);
if ( Array.isArray(o) ) {
clone = [];
for (i = 0, l = o.length; i < l; i++) {
clone[i] = (o[i] instanceof Object) ? doop(o[i], seen) : o[i];
}
}
else {
clone = Object.create( Object.getPrototypeOf(o) );
props = Object.keys(o);
for (i = 0, l = props.length; i < l; i++) {
descriptor = Object.getOwnPropertyDescriptor(o, props[i]);
if (descriptor.value) {
descriptor.value = doop(descriptor.value, seen);
}
Object.defineProperty(clone, props[i], descriptor);
}
}
}
return clone;
}
return o;
}
// Wrapper to avoid exposing the 'seen' parameter outside of this module.
function doopWrapper(o) {
return doop(o);
}
// for backwards compatibility
doopWrapper.doop = doopWrapper;
module.exports = doopWrapper;
+146
View File
@@ -0,0 +1,146 @@
/**
* Recursively print out all names and values in a data structure.
* @module jsdoc/util/dumper
*/
const util = require('util');
const OBJECT_WALKER_KEY = 'hasBeenSeenByWalkerDumper';
const SET_DEFINED = (typeof Set !== 'undefined');
class ObjectWalker {
constructor() {
this.seenItems = SET_DEFINED ? new Set() : [];
}
seen(object) {
let result;
if (SET_DEFINED) {
result = this.seenItems.has(object);
}
else {
result = object[OBJECT_WALKER_KEY];
}
return result;
}
markAsSeen(object) {
if (SET_DEFINED) {
this.seenItems.add(object);
}
else {
object[OBJECT_WALKER_KEY] = true;
this.seenItems.push(object);
}
}
removeSeenFlag(obj) {
if (SET_DEFINED) {
this.seenItems.delete(obj);
}
else {
delete obj[OBJECT_WALKER_KEY];
}
}
/* eslint-disable class-methods-use-this */
// some objects are unwalkable, like Java native objects
isUnwalkable(o) {
return o && typeof o === 'object' && typeof o.constructor === 'undefined';
}
isFunction(o) {
return (o && typeof o === 'function') || o instanceof Function;
}
isObject(o) {
return (o && o instanceof Object) ||
(o && typeof o.constructor !== 'undefined' && o.constructor.name === 'Object');
}
/* eslint-enable class-methods-use-this */
checkCircularRefs(o, func) {
if ( this.seen(o) ) {
return '<CircularRef>';
}
else {
this.markAsSeen(o);
return func(o);
}
}
walk(o) {
let result;
const self = this;
if ( this.isUnwalkable(o) ) {
result = '<Object>';
}
else if ( o === undefined ) {
result = null;
}
else if ( Array.isArray(o) ) {
result = this.checkCircularRefs(o, arr => {
const newArray = [];
arr.forEach(item => {
newArray.push( self.walk(item) );
});
self.removeSeenFlag(arr);
return newArray;
});
}
else if ( util.isRegExp(o) ) {
result = `<RegExp ${o}>`;
}
else if ( util.isDate(o) ) {
result = `<Date ${o.toUTCString()}>`;
}
else if ( util.isError(o) ) {
result = { message: o.message };
}
else if ( this.isFunction(o) ) {
result = `<Function${o.name ? ` ${o.name}` : ''}>`;
}
else if ( this.isObject(o) && o !== null ) {
result = this.checkCircularRefs(o, obj => {
const newObj = {};
Object.keys(obj).forEach(key => {
if (!SET_DEFINED && key === OBJECT_WALKER_KEY) { return; }
newObj[key] = self.walk(obj[key]);
});
self.removeSeenFlag(obj);
return newObj;
});
}
// should be safe to JSON.stringify() everything else
else {
result = o;
}
return result;
}
}
/**
* @param {*} object
*/
exports.dump = function(...args) {
const result = [];
let walker;
for (let arg of args) {
walker = new ObjectWalker();
result.push( JSON.stringify(walker.walk(arg), null, 4) );
}
return result.join('\n');
};
+32
View File
@@ -0,0 +1,32 @@
/**
* Helper functions for handling errors.
*
* @deprecated As of JSDoc 3.3.0. This module may be removed in a future release. Use the module
* {@link module:jsdoc/util/logger} to log warnings and errors.
* @module jsdoc/util/error
*/
/**
* Log an exception as an error.
*
* Prior to JSDoc 3.3.0, this method would either log the exception (if lenient mode was enabled) or
* re-throw the exception (default).
*
* In JSDoc 3.3.0 and later, lenient mode has been replaced with strict mode, which is disabled by
* default. If strict mode is enabled, calling the `handle` method causes JSDoc to exit immediately,
* just as if the exception had been re-thrown.
*
* @deprecated As of JSDoc 3.3.0. This module may be removed in a future release.
* @param {Error} e - The exception to log.
* @memberof module:jsdoc/util/error
*/
exports.handle = e => {
const logger = require('jsdoc/util/logger');
let msg = e ? ( e.message || JSON.stringify(e) ) : '';
// include the error type if it's an Error object
if (e instanceof Error) {
msg = `${e.name}: ${msg}`;
}
logger.error(msg);
};
+254
View File
@@ -0,0 +1,254 @@
/**
* Logging tools for JSDoc.
*
* Log messages are printed to the console based on the current logging level. By default, messages
* at level `{@link module:jsdoc/util/logger.LEVELS.ERROR}` or above are logged; all other messages
* are ignored.
*
* In addition, the module object emits an event whenever a logger method is called, regardless of
* the current logging level. The event's name is the string `logger:` followed by the logger's name
* (for example, `logger:error`). The event handler receives an array of arguments that were passed
* to the logger method.
*
* Each logger method accepts a `message` parameter that may contain zero or more placeholders. Each
* placeholder is replaced by the corresponding argument following the message. If the placeholder
* does not have a corresponding argument, the placeholder is not replaced.
*
* The following placeholders are supported:
*
* + `%s`: String.
* + `%d`: Number.
* + `%j`: JSON.
*
* @module jsdoc/util/logger
* @extends module:events.EventEmitter
* @example
* var logger = require('jsdoc/util/logger');
*
* var data = {
* foo: 'bar'
* };
* var name = 'baz';
*
* logger.warn('%j %s', data, name); // prints '{"foo":"bar"} baz'
* @see http://nodejs.org/api/util.html#util_util_format_format
*/
const EventEmitter = require('events').EventEmitter;
const util = require('util');
/* eslint-disable no-empty-function */
class Logger extends EventEmitter {}
/* eslint-enable no-empty-function */
const logger = module.exports = new Logger();
/**
* Logging levels for the JSDoc logger. The default logging level is
* {@link module:jsdoc/util/logger.LEVELS.ERROR}.
*
* @alias module:jsdoc/util/logger.LEVELS
* @enum
* @type {number}
*/
const LEVELS = logger.LEVELS = {
/**
* Do not log any messages.
*
* @alias module:jsdoc/util/logger.LEVELS.SILENT
*/
SILENT: 0,
/**
* Log fatal errors that prevent JSDoc from running.
*
* @alias module:jsdoc/util/logger.LEVELS.FATAL
*/
FATAL: 10,
/**
* Log all errors, including errors from which JSDoc can recover.
*
* @alias module:jsdoc/util/logger.LEVELS.ERROR
*/
ERROR: 20,
/**
* Log the following messages:
*
* + Warnings
* + Errors
*
* @alias module:jsdoc/util/logger.LEVELS.WARN
*/
WARN: 30,
/**
* Log the following messages:
*
* + Informational messages
* + Warnings
* + Errors
*
* @alias module:jsdoc/util/logger.LEVELS.INFO
*/
INFO: 40,
/**
* Log the following messages:
*
* + Debugging messages
* + Informational messages
* + Warnings
* + Errors
*
* @alias module:jsdoc/util/logger.LEVELS.DEBUG
*/
DEBUG: 50,
/**
* Log all messages.
*
* @alias module:jsdoc/util/logger.LEVELS.VERBOSE
*/
VERBOSE: 1000
};
const DEFAULT_LEVEL = LEVELS.WARN;
let logLevel = DEFAULT_LEVEL;
const PREFIXES = {
DEBUG: 'DEBUG: ',
ERROR: 'ERROR: ',
FATAL: 'FATAL: ',
WARN: 'WARNING: '
};
// Add a prefix to a log message if necessary.
function addPrefix(args, prefix) {
let updatedArgs;
if (prefix && typeof args[0] === 'string') {
updatedArgs = args.slice(0);
updatedArgs[0] = prefix + updatedArgs[0];
}
return updatedArgs || args;
}
// TODO: document events
function wrapLogFunction(name, func) {
const eventName = `logger:${name}`;
const upperCaseName = name.toUpperCase();
const level = LEVELS[upperCaseName];
const prefix = PREFIXES[upperCaseName];
return function() {
let loggerArgs;
const args = Array.prototype.slice.call(arguments, 0);
if (logLevel >= level) {
loggerArgs = addPrefix(args, prefix);
func(...loggerArgs);
}
args.unshift(eventName);
logger.emit(...args);
};
}
// Print a message to STDOUT without a terminating newline.
function printToStdout(...args) {
process.stdout.write( util.format(...args) );
}
/**
* Log a message at log level {@link module:jsdoc/util/logger.LEVELS.DEBUG}.
*
* @alias module:jsdoc/util/logger.debug
* @param {string} message - The message to log.
* @param {...*=} values - The values that will replace the message's placeholders.
*/
logger.debug = wrapLogFunction('debug', console.info);
/**
* Print a string at log level {@link module:jsdoc/util/logger.LEVELS.DEBUG}. The string is not
* terminated by a newline.
*
* @alias module:jsdoc/util/logger.printDebug
* @param {string} message - The message to log.
* @param {...*=} values - The values that will replace the message's placeholders.
*/
logger.printDebug = wrapLogFunction('debug', printToStdout);
/**
* Log a message at log level {@link module:jsdoc/util/logger.LEVELS.ERROR}.
*
* @alias module:jsdoc/util/logger.error
* @param {string} message - The message to log.
* @param {...*=} values - The values that will replace the message's placeholders.
*/
logger.error = wrapLogFunction('error', console.error);
/**
* Log a message at log level {@link module:jsdoc/util/logger.LEVELS.FATAL}.
*
* @alias module:jsdoc/util/logger.fatal
* @param {string} message - The message to log.
* @param {...*=} values - The values that will replace the message's placeholders.
*/
logger.fatal = wrapLogFunction('fatal', console.error);
/**
* Log a message at log level {@link module:jsdoc/util/logger.LEVELS.INFO}.
*
* @alias module:jsdoc/util/logger.info
* @param {string} message - The message to log.
* @param {...*=} values - The values that will replace the message's placeholders.
*/
logger.info = wrapLogFunction('info', console.info);
/**
* Print a string at log level {@link module:jsdoc/util/logger.LEVELS.INFO}. The string is not
* terminated by a newline.
*
* @alias module:jsdoc/util/logger.printInfo
* @param {string} message - The message to log.
* @param {...*=} values - The values that will replace the message's placeholders.
*/
logger.printInfo = wrapLogFunction('info', printToStdout);
/**
* Log a message at log level {@link module:jsdoc/util/logger.LEVELS.VERBOSE}.
*
* @alias module:jsdoc/util/logger.verbose
* @param {string} message - The message to log.
* @param {...*=} values - The values that will replace the message's placeholders.
*/
logger.verbose = wrapLogFunction('verbose', console.info);
/**
* Print a string at log level {@link module:jsdoc/util/logger.LEVELS.VERBOSE}. The string is not
* terminated by a newline.
*
* @alias module:jsdoc/util/logger.printVerbose
* @param {string} message - The message to log.
* @param {...*=} values - The values that will replace the message's placeholders.
*/
logger.printVerbose = wrapLogFunction('verbose', printToStdout);
/**
* Log a message at log level {@link module:jsdoc/util/logger.LEVELS.WARN}.
*
* @alias module:jsdoc/util/logger.warn
* @param {string} message - The message to log.
* @param {...*=} values - The values that will replace the message's placeholders.
*/
logger.warn = wrapLogFunction('warn', console.warn);
/**
* Set the log level.
*
* @alias module:jsdoc/util/logger.setLevel
* @param {module:jsdoc/util/logger.LEVELS} level - The log level to use.
*/
logger.setLevel = function(level) {
logLevel = (level !== undefined) ? level : DEFAULT_LEVEL;
};
/**
* Get the current log level.
*
* @alias module:jsdoc/util/logger.getLevel
* @return {module:jsdoc/util/logger.LEVELS} The current log level.
*/
logger.getLevel = function() {
return logLevel;
};
+282
View File
@@ -0,0 +1,282 @@
/**
* Provides access to Markdown-related functions.
* @module jsdoc/util/markdown
*/
const env = require('jsdoc/env');
const logger = require('jsdoc/util/logger');
const MarkdownIt = require('markdown-it');
const { marked } = require('marked');
const mda = require('markdown-it-anchor');
const path = require('jsdoc/path');
const util = require('util');
/**
* Enumeration of Markdown parsers that are available.
* @enum {String}
*/
const parserNames = {
/**
* The [`markdown-js`](https://github.com/evilstreak/markdown-js) (aka "evilstreak") parser.
*
* @deprecated Replaced by `markdown-it`.
*/
evilstreak: 'markdownit',
/**
* The "GitHub-flavored Markdown" parser.
*
* @deprecated Replaced by `markdown-it`.
*/
gfm: 'markdownit',
/**
* The `markdown-it` parser.
*/
markdownit: 'markdownit',
/**
* The [Marked](https://github.com/chjj/marked) parser.
*
* @deprecated Will be replaced by `markdown-it` in JSDoc 3.7.0.
*/
marked: 'marked'
};
/**
* Escape underscores that occur within an inline tag in order to protect them from the `marked`
* parser.
*
* @param {string} source - The source text to sanitize.
* @return {string} The source text, where underscores within inline tags have been protected with a
* preceding backslash (e.g., `\_`). The `marked` parser will strip the backslash and protect the
* underscore.
*/
function escapeUnderscores(source) {
return source.replace(/\{@[^}\r\n]+\}/g, wholeMatch => wholeMatch.replace(/(^|[^\\])_/g, '$1\\_'));
}
/**
* Escape HTTP/HTTPS URLs so that they are not automatically converted to HTML links.
*
* @param {string} source - The source text to escape.
* @return {string} The source text with escape characters added to HTTP/HTTPS URLs.
*/
function escapeUrls(source) {
return source.replace(/(https?):\/\//g, '$1:\\/\\/');
}
/**
* Unescape HTTP/HTTPS URLs after Markdown parsing is complete.
*
* @param {string} source - The source text to unescape.
* @return {string} The source text with escape characters removed from HTTP/HTTPS URLs.
*/
function unescapeUrls(source) {
return source.replace(/(https?):\\\/\\\//g, '$1://');
}
/**
* Escape backslashes within inline tags so that they are not stripped.
*
* @param {string} source - The source text to escape.
* @return {string} The source text with backslashes escaped within inline tags.
*/
function escapeInlineTagBackslashes(source) {
return source.replace(/\{@[^}\r\n]+\}/g, wholeMatch => wholeMatch.replace(/\\/g, '\\\\'));
}
/**
* Escape characters in text within a code block.
*
* @param {string} source - The source text to escape.
* @return {string} The escaped source text.
*/
function escapeCode(source) {
return source.replace(/</g, '&lt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
/**
* Wrap a code snippet in HTML tags that enable syntax highlighting.
*
* @param {string} code - The code snippet.
* @param {string?} language - The language of the code snippet.
* @return {string} The wrapped code snippet.
*/
function highlight(code, language) {
let classString;
let langClass = '';
if (language && (language !== 'plain')) {
langClass = ` lang-${language}`;
}
if (language !== 'plain') {
classString = util.format(' class="prettyprint source%s"', langClass);
}
else {
classString = ' class="source"';
}
return util.format('<pre%s><code>%s</code></pre>', classString, escapeCode(code));
}
/**
* Unencode quotes that occur within {@ ... } after the Markdown parser has turned them into HTML
* entities.
*
* @param {string} source - The source text to unencode.
* @return {string} The source text with HTML entity `&quot;` converted back to standard quotes.
*/
function unencodeQuotes(source) {
return source.replace(/\{@[^}\r\n]+\}/g, wholeMatch => wholeMatch.replace(/&quot;/g, '"'));
}
/**
* Get the appropriate function for applying syntax highlighting to text, based on the user's
* Markdown configuration settings.
*
* @param {Object} conf - The user's Markdown configuration settings.
* @return {function} The highlighter function.
*/
function getHighlighter(conf) {
let highlighter;
let highlighterPath;
switch (typeof conf.highlight) {
case 'string':
highlighterPath = path.getResourcePath(conf.highlight);
if (highlighterPath) {
highlighter = require(highlighterPath).highlight;
if (typeof highlighter !== 'function') {
logger.error('The syntax highlighting module "%s" does not assign a method ' +
'to exports.highlight. Using the default syntax highlighter.',
conf.highlight);
highlighter = highlight;
}
}
else {
logger.error('Unable to find the syntax highlighting module "%s". Using the ' +
'default syntax highlighter.', conf.highlight);
highlighter = highlight;
}
break;
case 'function':
highlighter = conf.highlight;
break;
default:
highlighter = highlight;
}
return highlighter;
}
/**
* Retrieve a function that accepts a single parameter containing Markdown source. The function uses
* the specified parser to transform the Markdown source to HTML, then returns the HTML as a string.
*
* @private
* @param {String} parserName The name of the selected parser.
* @param {Object} [conf] Configuration for the selected parser, if any.
* @returns {Function} A function that accepts Markdown source, feeds it to the selected parser, and
* returns the resulting HTML.
*/
function getParseFunction(parserName, conf) {
let highlighter;
let parserFunction;
let renderer;
conf = conf || {};
highlighter = getHighlighter(conf);
switch (parserName) {
case parserNames.marked:
if (conf.hardwrap) {
marked.setOptions({breaks: true});
}
// Marked generates an "id" attribute for headers; this custom renderer suppresses it
renderer = new marked.Renderer();
if (!conf.idInHeadings) {
renderer.heading = (text, level) => util.format('<h%s>%s</h%s>', level, text, level);
}
renderer.code = highlighter;
parserFunction = source => {
let result;
source = escapeUnderscores(source);
source = escapeUrls(source);
result = marked(source, { renderer: renderer })
.replace(/\s+$/, '')
.replace(/&#39;/g, "'");
result = unescapeUrls(result);
result = unencodeQuotes(result);
return result;
};
parserFunction._parser = parserNames.marked;
return parserFunction;
case parserNames.markdownit:
renderer = new MarkdownIt({
breaks: Boolean(conf.hardwrap),
highlight: highlighter,
html: true
});
if (conf.idInHeadings) {
renderer.use(mda, { tabIndex: false });
}
parserFunction = source => {
let result;
source = escapeUrls(source);
source = escapeInlineTagBackslashes(source);
result = renderer.render(source)
.replace(/\s+$/, '')
.replace(/&#39;/g, "'");
result = unescapeUrls(result);
result = unencodeQuotes(result);
return result;
};
parserFunction._parser = parserNames.markdownit;
return parserFunction;
default:
logger.error('Unrecognized Markdown parser "%s". Markdown support is disabled.',
parserName);
return undefined;
}
}
/**
* Retrieve a Markdown parsing function based on the value of the `conf.json` file's
* `env.conf.markdown` property. The parsing function accepts a single parameter containing Markdown
* source. The function uses the parser specified in `conf.json` to transform the Markdown source to
* HTML, then returns the HTML as a string.
*
* @returns {function} A function that accepts Markdown source, feeds it to the selected parser, and
* returns the resulting HTML.
*/
exports.getParser = () => {
const conf = env.conf.markdown;
const parser = (conf && conf.parser) ? parserNames[conf.parser] : parserNames.markdownit;
return getParseFunction(parser, conf);
};
+14
View File
@@ -0,0 +1,14 @@
/**
* Module to strip the leading BOM, if present, from UTF-8 files.
* @module
* @private
*/
/**
* Strip the leading BOM, if present, from a string.
*
* @private
* @param {string} text - The string to strip.
* @return {string} The stripped string.
*/
exports.strip = (text = '') => text.replace(/^\uFEFF/, '');
File diff suppressed because it is too large Load Diff
+65
View File
@@ -0,0 +1,65 @@
{
"name": "jsdoc",
"version": "4.0.3",
"revision": "1714619656778",
"description": "An API documentation generator for JavaScript.",
"keywords": [
"documentation",
"javascript"
],
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/jsdoc/jsdoc"
},
"dependencies": {
"@babel/parser": "^7.20.15",
"@jsdoc/salty": "^0.2.1",
"@types/markdown-it": "^14.1.1",
"bluebird": "^3.7.2",
"catharsis": "^0.9.0",
"escape-string-regexp": "^2.0.0",
"js2xmlparser": "^4.0.2",
"klaw": "^3.0.0",
"markdown-it": "^14.1.0",
"markdown-it-anchor": "^8.6.7",
"marked": "^4.0.10",
"mkdirp": "^1.0.4",
"requizzle": "^0.2.3",
"strip-json-comments": "^3.1.0",
"underscore": "~1.13.2"
},
"devDependencies": {
"ajv": "^6.12.0",
"gulp": "^5.0.0",
"gulp-eslint": "^6.0.0",
"gulp-json-editor": "^2.5.6",
"nyc": "^15.1.0"
},
"engines": {
"node": ">=12.0.0"
},
"scripts": {
"test": "gulp lint; gulp test"
},
"bin": {
"jsdoc": "./jsdoc.js"
},
"bugs": "https://github.com/jsdoc/jsdoc/issues",
"author": {
"name": "Michael Mathews",
"email": "micmath@gmail.com"
},
"contributors": [
{
"name": "The JSDoc Contributors",
"url": "https://github.com/jsdoc/jsdoc/graphs/contributors"
}
],
"maintainers": [
{
"name": "Jeff Williams",
"email": "jeffrey.l.williams@gmail.com"
}
]
}
+21
View File
@@ -0,0 +1,21 @@
/* eslint-disable spaced-comment */
/**
* Demonstrate how to modify the source code before the parser sees it.
*
* @module plugins/commentConvert
*/
exports.handlers = {
///
/// Convert ///-style comments into jsdoc comments.
/// @param e
/// @param e.filename
/// @param e.source
///
beforeParse(e) {
e.source = e.source.replace(/(\n[ \t]*\/\/\/[^\n]*)+/g, $ => {
const replacement = `\n/**${$.replace(/^[ \t]*\/\/\//mg, '').replace(/(\n$|$)/, '*/$1')}`;
return replacement;
});
}
};
+17
View File
@@ -0,0 +1,17 @@
/**
* Remove everything in a file except JSDoc-style comments. By enabling this plugin, you can
* document source files that are not valid JavaScript (including source files for other languages).
* @module plugins/commentsOnly
*/
exports.handlers = {
beforeParse(e) {
// a JSDoc comment looks like: /**[one or more chars]*/
const comments = e.source.match(/\/\*\*[\s\S]+?\*\//g);
if (comments) {
e.source = comments.join('\n\n');
} else {
e.source = ''; // If file has no comments, parser should still receive no code
}
}
};
+18
View File
@@ -0,0 +1,18 @@
/**
* Escape HTML tags in descriptions.
*
* @module plugins/escapeHtml
*/
exports.handlers = {
/**
* Translate HTML tags in descriptions into safe entities. Replaces <, & and newlines
*/
newDoclet({doclet}) {
if (doclet.description) {
doclet.description = doclet.description
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/\r\n|\n|\r/g, '<br>');
}
}
};
+98
View File
@@ -0,0 +1,98 @@
/**
* Dump information about parser events to the console.
*
* @module plugins/eventDumper
*/
const _ = require('underscore');
const doop = require('jsdoc/util/doop');
const dump = require('jsdoc/util/dumper').dump;
const env = require('jsdoc/env');
const util = require('util');
const conf = env.conf.eventDumper || {};
// Dump the included parser events (defaults to all events)
let events = conf.include || [
'parseBegin',
'fileBegin',
'beforeParse',
'jsdocCommentFound',
'symbolFound',
'newDoclet',
'fileComplete',
'parseComplete',
'processingComplete'
];
// Don't dump the excluded parser events
if (conf.exclude) {
events = _.difference(events, conf.exclude);
}
/**
* Replace AST node objects in events with a placeholder.
*
* @param {Object} o - An object whose properties may contain AST node objects.
* @return {Object} The modified object.
*/
function replaceNodeObjects(o) {
const OBJECT_PLACEHOLDER = '<Object>';
if (o.code && o.code.node) {
// don't break the original object!
o.code = doop(o.code);
o.code.node = OBJECT_PLACEHOLDER;
}
if (o.doclet && o.doclet.meta && o.doclet.meta.code && o.doclet.meta.code.node) {
// don't break the original object!
o.doclet.meta.code = doop(o.doclet.meta.code);
o.doclet.meta.code.node = OBJECT_PLACEHOLDER;
}
if (o.astnode) {
o.astnode = OBJECT_PLACEHOLDER;
}
return o;
}
/**
* Get rid of unwanted crud in an event object.
*
* @param {object} e The event object.
* @return {object} The fixed-up object.
*/
function cleanse(e) {
let result = {};
Object.keys(e).forEach(prop => {
// by default, don't stringify properties that contain an array of functions
if (!conf.includeFunctions && util.isArray(e[prop]) && e[prop][0] &&
String(typeof e[prop][0]) === 'function') {
result[prop] = `function[${e[prop].length}]`;
}
// never include functions that belong to the object
else if (typeof e[prop] !== 'function') {
result[prop] = e[prop];
}
});
// allow users to omit node objects, which can be enormous
if (conf.omitNodes) {
result = replaceNodeObjects(result);
}
return result;
}
exports.handlers = {};
events.forEach(eventType => {
exports.handlers[eventType] = e => {
console.log( dump({
type: eventType,
content: cleanse(e)
}) );
};
});
+88
View File
@@ -0,0 +1,88 @@
/**
* Translate doclet descriptions from Markdown into HTML.
*
* @module plugins/markdown
*/
const env = require('jsdoc/env');
const config = env.conf.markdown || {};
const defaultTags = [
'author',
'classdesc',
'description',
'exceptions',
'params',
'properties',
'returns',
'see',
'summary'
];
const hasOwnProp = Object.prototype.hasOwnProperty;
const parse = require('jsdoc/util/markdown').getParser();
let tags = [];
let excludeTags = [];
function shouldProcessString(tagName, text) {
let shouldProcess = true;
// we only want to process `@author` and `@see` tags that contain Markdown links
if ( (tagName === 'author' || tagName === 'see') && !text.includes('[') ) {
shouldProcess = false;
}
return shouldProcess;
}
/**
* Process the markdown source in a doclet. The properties that should be processed are
* configurable, but always include "author", "classdesc", "description", "exceptions", "params",
* "properties", "returns", and "see". Handled properties can be bare strings, objects, or arrays
* of objects.
*/
function process(doclet) {
tags.forEach(tag => {
if ( !hasOwnProp.call(doclet, tag) ) {
return;
}
if (typeof doclet[tag] === 'string' && shouldProcessString(tag, doclet[tag]) ) {
doclet[tag] = parse(doclet[tag]);
}
else if ( Array.isArray(doclet[tag]) ) {
doclet[tag].forEach((value, index, original) => {
const inner = {};
inner[tag] = value;
process(inner);
original[index] = inner[tag];
});
}
else if (doclet[tag]) {
process(doclet[tag]);
}
});
}
// set up the list of "tags" (properties) to process
if (config.tags) {
tags = config.tags.slice();
}
// set up the list of default tags to exclude from processing
if (config.excludeTags) {
excludeTags = config.excludeTags.slice();
}
defaultTags.forEach(tag => {
if (!excludeTags.includes(tag) && !tags.includes(tag)) {
tags.push(tag);
}
});
exports.handlers = {
/**
* Translate Markdown syntax in a new doclet's description into HTML. Is run
* by JSDoc 3 whenever a "newDoclet" event fires.
*/
newDoclet({doclet}) {
process(doclet);
}
};
+181
View File
@@ -0,0 +1,181 @@
/**
* The Overload Helper plugin automatically adds a signature-like string to the longnames of
* overloaded functions and methods. In JSDoc, this string is known as a _variation_. (The longnames
* of overloaded constructor functions are _not_ updated, so that JSDoc can identify the class'
* members correctly.)
*
* Using this plugin allows you to link to overloaded functions without manually adding `@variation`
* tags to your documentation.
*
* For example, suppose your code includes a function named `foo` that you can call in the
* following ways:
*
* + `foo()`
* + `foo(bar)`
* + `foo(bar, baz)` (where `baz` is repeatable)
*
* This plugin assigns the following variations and longnames to each version of `foo`:
*
* + `foo()` gets the variation `()` and the longname `foo()`.
* + `foo(bar)` gets the variation `(bar)` and the longname `foo(bar)`.
* + `foo(bar, baz)` (where `baz` is repeatable) gets the variation `(bar, ...baz)` and the longname
* `foo(bar, ...baz)`.
*
* You can then link to these functions with `{@link foo()}`, `{@link foo(bar)}`, and
* `{@link foo(bar, ...baz)`. Note that the variation is based on the names of the function
* parameters, _not_ their types.
*
* If you prefer to manually assign variations to certain functions, you can still do so with the
* `@variation` tag. This plugin will not change these variations or add more variations for that
* function, as long as the variations you've defined result in unique longnames.
*
* If an overloaded function includes multiple signatures with the same parameter names, the plugin
* will assign numeric variations instead, starting at `(1)` and counting upwards.
*
* @module plugins/overloadHelper
*/
// lookup table of function doclets by longname
let functionDoclets;
function hasUniqueValues(obj) {
let isUnique = true;
const seen = [];
Object.keys(obj).forEach(key => {
if (seen.includes(obj[key])) {
isUnique = false;
}
seen.push(obj[key]);
});
return isUnique;
}
function getParamNames(params) {
const names = [];
params.forEach(param => {
let name = param.name || '';
if (param.variable) {
name = `...${name}`;
}
if (name !== '') {
names.push(name);
}
});
return names.length ? names.join(', ') : '';
}
function getParamVariation({params}) {
return getParamNames(params || []);
}
function getUniqueVariations(doclets) {
let counter = 0;
const variations = {};
const docletKeys = Object.keys(doclets);
function getUniqueNumbers() {
docletKeys.forEach(doclet => {
let newLongname;
while (true) {
counter++;
variations[doclet] = String(counter);
// is this longname + variation unique?
newLongname = `${doclets[doclet].longname}(${variations[doclet]})`;
if ( !functionDoclets[newLongname] ) {
break;
}
}
});
}
function getUniqueNames() {
// start by trying to preserve existing variations
docletKeys.forEach(doclet => {
variations[doclet] = doclets[doclet].variation || getParamVariation(doclets[doclet]);
});
// if they're identical, try again, without preserving existing variations
if ( !hasUniqueValues(variations) ) {
docletKeys.forEach(doclet => {
variations[doclet] = getParamVariation(doclets[doclet]);
});
// if they're STILL identical, switch to numeric variations
if ( !hasUniqueValues(variations) ) {
getUniqueNumbers();
}
}
}
// are we already using numeric variations? if so, keep doing that
if (functionDoclets[`${doclets.newDoclet.longname}(1)`]) {
getUniqueNumbers();
}
else {
getUniqueNames();
}
return variations;
}
function ensureUniqueLongname(newDoclet) {
const doclets = {
oldDoclet: functionDoclets[newDoclet.longname],
newDoclet: newDoclet
};
const docletKeys = Object.keys(doclets);
let oldDocletLongname;
let variations = {};
if (doclets.oldDoclet) {
oldDocletLongname = doclets.oldDoclet.longname;
// if the shared longname has a variation, like MyClass#myLongname(variation),
// remove the variation
if (doclets.oldDoclet.variation || doclets.oldDoclet.variation === '') {
docletKeys.forEach(doclet => {
doclets[doclet].longname = doclets[doclet].longname.replace(/\([\s\S]*\)$/, '');
doclets[doclet].variation = null;
});
}
variations = getUniqueVariations(doclets);
// update the longnames/variations
docletKeys.forEach(doclet => {
doclets[doclet].longname += `(${variations[doclet]})`;
doclets[doclet].variation = variations[doclet];
});
// update the old doclet in the lookup table
functionDoclets[oldDocletLongname] = null;
functionDoclets[doclets.oldDoclet.longname] = doclets.oldDoclet;
}
// always store the new doclet in the lookup table
functionDoclets[doclets.newDoclet.longname] = doclets.newDoclet;
return doclets.newDoclet;
}
exports.handlers = {
parseBegin() {
functionDoclets = {};
},
newDoclet(e) {
if (e.doclet.kind === 'function') {
e.doclet = ensureUniqueLongname(e.doclet);
}
},
parseComplete() {
functionDoclets = null;
}
};
+30
View File
@@ -0,0 +1,30 @@
/**
* Adds support for reusable partial jsdoc files.
*
* @module plugins/partial
*/
const env = require('jsdoc/env');
const fs = require('jsdoc/fs');
const path = require('path');
exports.handlers = {
/**
* Include a partial jsdoc
*
* @param e
* @param e.filename
* @param e.source
* @example
* @partial "partial_doc.jsdoc"
*/
beforeParse(e) {
e.source = e.source.replace(/(@partial ".*")+/g, $ => {
const pathArg = $.match(/".*"/)[0].replace(/"/g, '');
const fullPath = path.join(e.filename, '..', pathArg);
const partialData = fs.readFileSync(fullPath, env.opts.encoding);
return partialData;
});
}
};
+19
View File
@@ -0,0 +1,19 @@
/**
* Strips the rails template tags from a js.erb file
*
* @module plugins/railsTemplate
*/
exports.handlers = {
/**
* Remove rails tags from the source input (e.g. <% foo bar %>)
*
* @param e
* @param e.filename
* @param e.source
*/
beforeParse(e) {
if (e.filename.match(/\.erb$/)) {
e.source = e.source.replace(/<%.*%>/g, '');
}
}
};
+15
View File
@@ -0,0 +1,15 @@
/**
* This is just an example.
*
* @module plugins/shout
*/
exports.handlers = {
/**
* Make your descriptions more shoutier.
*/
newDoclet({doclet}) {
if (typeof doclet.description === 'string') {
doclet.description = doclet.description.toUpperCase();
}
}
};
+49
View File
@@ -0,0 +1,49 @@
/**
* @module plugins/sourcetag
*/
const logger = require('jsdoc/util/logger');
exports.handlers = {
/**
* Support @source tag. Expected value like:
*
* { "filename": "myfile.js", "lineno": 123 }
*
* Modifies the corresponding meta values on the given doclet.
*
* WARNING: If you are using a JSDoc template that generates pretty-printed source files,
* such as JSDoc's default template, this plugin can cause JSDoc to crash. To fix this issue,
* update your template settings to disable pretty-printed source files.
*
* @source { "filename": "sourcetag.js", "lineno": 9 }
*/
newDoclet({doclet}) {
let tags = doclet.tags;
let tag;
let value;
// any user-defined tags in this doclet?
if (typeof tags !== 'undefined') {
// only interested in the @source tags
tags = tags.filter(({title}) => title === 'source');
if (tags.length) {
// take the first one
tag = tags[0];
try {
value = JSON.parse(tag.value);
}
catch (ex) {
logger.error('@source tag expects a valid JSON value, like { "filename": "myfile.js", "lineno": 123 }.');
return;
}
doclet.meta = doclet.meta || {};
doclet.meta.filename = value.filename || '';
doclet.meta.lineno = value.lineno || '';
}
}
}
};
+58
View File
@@ -0,0 +1,58 @@
/**
* This plugin creates a summary tag, if missing, from the first sentence in the description.
*
* @module plugins/summarize
*/
exports.handlers = {
/**
* Autogenerate summaries, if missing, from the description, if present.
*/
newDoclet({doclet}) {
let endTag;
let tags;
let stack;
// If the summary is missing, grab the first sentence from the description
// and use that.
if (doclet && !doclet.summary && doclet.description) {
// The summary may end with `.$`, `. `, or `.<` (a period followed by an HTML tag).
doclet.summary = doclet.description.split(/\.$|\.\s|\.</)[0];
// Append `.` as it was removed in both cases, or is possibly missing.
doclet.summary += '.';
// This is an excerpt of something that is possibly HTML.
// Balance it using a stack. Assume it was initially balanced.
tags = doclet.summary.match(/<[^>]+>/g) || [];
stack = [];
tags.forEach(tag => {
const idx = tag.indexOf('/');
if (idx === -1) {
// start tag -- push onto the stack
stack.push(tag);
} else if (idx === 1) {
// end tag -- pop off of the stack
stack.pop();
}
// otherwise, it's a self-closing tag; don't modify the stack
});
// stack should now contain only the start tags that lack end tags,
// with the most deeply nested start tag at the top
while (stack.length > 0) {
// pop the unmatched tag off the stack
endTag = stack.pop();
// get just the tag name
endTag = endTag.substring(1, endTag.search(/[ >]/));
// append the end tag
doclet.summary += `</${endTag}>`;
}
// and, finally, if the summary starts and ends with a <p> tag, remove it; let the
// template decide whether to wrap the summary in a <p> tag
doclet.summary = doclet.summary.replace(/^<p>(.*)<\/p>$/i, '$1');
}
}
};
+31
View File
@@ -0,0 +1,31 @@
'use strict';
/**
* @see [Nowhere](http://nowhere.com)
*/
function foo() {}
/**
* @see AnObject#myProperty
*/
function bar() {}
/**
* @author [Mr. Macintosh](http://www.folklore.org/StoryView.py?story=Mister_Macintosh.txt)
* @classdesc My class.
* @description My class.
* @exception {Error} Some error.
* @param {string} myParam - My parameter.
* @property {string} value - Value of myParam.
* @return {MyClass} Class instance.
* @see [Example Inc.](http://example.com)
* @summary My class.
*/
function MyClass(myParam) {
this.value = myParam;
}
/**
* "See" {@link chat."#channel"."say-\"hello\""}.
*/
function MyOtherClass() {}
+50
View File
@@ -0,0 +1,50 @@
/**
* A bowl of non-spicy soup.
* @class
*//**
* A bowl of spicy soup.
* @class
* @param {number} spiciness - The spiciness of the soup, in Scoville heat units (SHU).
*/
function Soup(spiciness) {}
/**
* Slurp the soup.
*//**
* Slurp the soup loudly.
* @param {number} dBA - The slurping volume, in A-weighted decibels.
*/
Soup.prototype.slurp = function(dBA) {};
/**
* Salt the soup as needed, using a highly optimized soup-salting heuristic.
*//**
* Salt the soup, specifying the amount of salt to add.
* @variation mg
* @param {number} amount - The amount of salt to add, in milligrams.
*/
Soup.prototype.salt = function(amount) {};
/**
* Heat the soup by the specified number of degrees.
* @param {number} degrees - The number of degrees, in Fahrenheit, by which to heat the soup.
*//**
* Heat the soup by the specified number of degrees.
* @variation 1
* @param {string} degrees - The number of degrees, in Fahrenheit, by which to heat the soup, but
* as a string for some reason.
*//**
* Heat the soup by the specified number of degrees.
* @param {boolean} degrees - The number of degrees, as a boolean. Wait, what?
*/
Soup.prototype.heat = function(degrees) {};
/**
* Discard the soup.
* @variation discardSoup
*//**
* Discard the soup by pouring it into the specified container.
* @variation discardSoup
* @param {Object} container - The container in which to discard the soup.
*/
Soup.prototype.discard = function(container) {};
+19
View File
@@ -0,0 +1,19 @@
/**
* Strips the rails template tags from a js.erb file
*
* @module plugins/railsTemplate
*/
exports.handlers = {
/**
* Remove rails tags from the source input (e.g. <% foo bar %>)
* @param e
* @param e.filename
* @param e.source
*/
beforeParse: function(e) {
if (e.filename.match(/\.erb$/)) {
e.source = e.source.replace(/<%.*%> /g, "");
}
}
};
+22
View File
@@ -0,0 +1,22 @@
'use strict';
/** This doclet will be shown by default, just like normal. */
function normal() {}
/** This doclet will be hidden by default because it begins with an underscore. */
function _hidden() {}
/**
* Klass class
* @class
*/
function Klass() {
/** This is a private property of the class, and should not. */
this._privateProp = null;
/**
* This is a property explicitly marked as private.
* @private
*/
this.privateProp = null;
}
+20
View File
@@ -0,0 +1,20 @@
'use strict';
describe('commentConvert plugin', function() {
var env = require('jsdoc/env');
var path = require('jsdoc/path');
var docSet;
var parser = jasmine.createParser();
var pluginPath = 'plugins/commentConvert';
var pluginPathResolved = path.join(env.dirname, pluginPath);
var plugin = require(pluginPathResolved);
require('jsdoc/plugins').installPlugins([pluginPathResolved], parser);
docSet = jasmine.getDocSetFromFile(pluginPath + '.js', parser);
it('should convert ///-style comments into jsdoc comments', function() {
var doclet = docSet.getByLongname('module:plugins/commentConvert.handlers.beforeParse');
expect(doclet.length).toEqual(1);
});
});
+20
View File
@@ -0,0 +1,20 @@
'use strict';
describe('escapeHtml plugin', function() {
var env = require('jsdoc/env');
var path = require('jsdoc/path');
var docSet;
var parser = jasmine.createParser();
var pluginPath = 'plugins/escapeHtml';
var pluginPathResolved = path.join(env.dirname, pluginPath);
require('jsdoc/plugins').installPlugins([pluginPathResolved], parser);
docSet = jasmine.getDocSetFromFile(pluginPath + '.js', parser);
it("should escape '&', '<' and newlines in doclet descriptions", function() {
var doclet = docSet.getByLongname('module:plugins/escapeHtml.handlers.newDoclet');
expect(doclet[0].description).toEqual('Translate HTML tags in descriptions into safe entities. Replaces &lt;, &amp; and newlines');
});
});
+60
View File
@@ -0,0 +1,60 @@
'use strict';
var env = require('jsdoc/env');
var path = require('jsdoc/path');
describe('markdown plugin', function() {
var pluginPath = 'plugins/markdown';
var pluginPathResolved = path.join(env.dirname, pluginPath);
var plugin = require(pluginPathResolved);
var docSet = jasmine.getDocSetFromFile('plugins/test/fixtures/markdown.js');
// TODO: more tests; refactor the plugin so multiple settings can be tested
it('should process the correct tags by default', function() {
var myClass = docSet.getByLongname('MyClass')[0];
plugin.handlers.newDoclet({ doclet: myClass });
[
myClass.author[0],
myClass.classdesc,
myClass.description,
myClass.exceptions[0].description,
myClass.params[0].description,
myClass.properties[0].description,
myClass.returns[0].description,
myClass.see,
myClass.summary
].forEach(function(value) {
// if we processed the value, it should be wrapped in a <p> tag
expect( /^<p>(?:.+)<\/p>$/.test(value) ).toBe(true);
});
});
it('should unescape &quot; entities in inline tags, but not elsewhere', function() {
var myOtherClass = docSet.getByLongname('MyOtherClass')[0];
plugin.handlers.newDoclet({ doclet: myOtherClass });
expect(myOtherClass.description).toContain('chat."#channel"."say-\\"hello\\""');
expect(myOtherClass.description).toContain('&quot;See&quot;');
});
describe('@see tag support', function() {
var foo = docSet.getByLongname('foo')[0];
var bar = docSet.getByLongname('bar')[0];
it('should parse @see tags containing links', function() {
plugin.handlers.newDoclet({ doclet: foo });
expect(typeof foo).toEqual('object');
expect(foo.see[0]).toEqual('<p><a href="http://nowhere.com">Nowhere</a></p>');
});
it('should not parse @see tags that do not contain links', function() {
plugin.handlers.newDoclet({ doclet: bar });
expect(typeof bar).toEqual('object');
expect(bar.see[0]).toEqual('AnObject#myProperty');
});
});
});
+102
View File
@@ -0,0 +1,102 @@
'use strict';
describe('plugins/overloadHelper', function() {
var env = require('jsdoc/env');
var path = require('jsdoc/path');
var docSet;
var parser = jasmine.createParser();
var pluginPath = 'plugins/overloadHelper';
var pluginPathResolved = path.resolve(env.dirname, pluginPath);
var plugin = require(pluginPathResolved);
require('jsdoc/plugins').installPlugins([pluginPathResolved], parser);
docSet = jasmine.getDocSetFromFile('plugins/test/fixtures/overloadHelper.js', parser);
it('should exist', function() {
expect(plugin).toBeDefined();
expect(typeof plugin).toBe('object');
});
it('should export handlers', function() {
expect(plugin.handlers).toBeDefined();
expect(typeof plugin.handlers).toBe('object');
});
it('should export a "newDoclet" handler', function() {
expect(plugin.handlers.newDoclet).toBeDefined();
expect(typeof plugin.handlers.newDoclet).toBe('function');
});
it('should export a "parseComplete" handler', function() {
expect(plugin.handlers.parseComplete).toBeDefined();
expect(typeof plugin.handlers.parseComplete).toBe('function');
});
describe('newDoclet handler', function() {
it('should not add unique longnames to constructors', function() {
var soup = docSet.getByLongname('Soup');
var soup1 = docSet.getByLongname('Soup()');
var soup2 = docSet.getByLongname('Soup(spiciness)');
expect(soup.length).toBe(2);
expect(soup1.length).toBe(0);
expect(soup2.length).toBe(0);
});
it('should add unique longnames to methods', function() {
var slurp = docSet.getByLongname('Soup#slurp');
var slurp1 = docSet.getByLongname('Soup#slurp()');
var slurp2 = docSet.getByLongname('Soup#slurp(dBA)');
expect(slurp.length).toBe(0);
expect(slurp1.length).toBe(1);
expect(slurp2.length).toBe(1);
});
it('should update the "variation" property of the method', function() {
var slurp1 = docSet.getByLongname('Soup#slurp()')[0];
var slurp2 = docSet.getByLongname('Soup#slurp(dBA)')[0];
expect(slurp1.variation).toBe('');
expect(slurp2.variation).toBe('dBA');
});
it('should not add to or change existing variations that are unique', function() {
var salt1 = docSet.getByLongname('Soup#salt');
var salt2 = docSet.getByLongname('Soup#salt(mg)');
expect(salt1.length).toBe(1);
expect(salt2.length).toBe(1);
});
it('should not duplicate the names of existing numeric variations', function() {
var heat1 = docSet.getByLongname('Soup#heat(1)');
var heat2 = docSet.getByLongname('Soup#heat(2)');
var heat3 = docSet.getByLongname('Soup#heat(3)');
expect(heat1.length).toBe(1);
expect(heat2.length).toBe(1);
expect(heat3.length).toBe(1);
});
it('should replace identical variations with new, unique variations', function() {
var discard1 = docSet.getByLongname('Soup#discard()');
var discard2 = docSet.getByLongname('Soup#discard(container)');
expect(discard1.length).toBe(1);
expect(discard2.length).toBe(1);
});
});
describe('parseComplete handler', function() {
// disabled because on the second run, each comment is being parsed twice; who knows why...
xit('should not retain parse results between parser runs', function() {
parser.clear();
docSet = jasmine.getDocSetFromFile('plugins/test/fixtures/overloadHelper.js', parser);
var heat = docSet.getByLongname('Soup#heat(4)');
expect(heat.length).toBe(0);
});
});
});
+19
View File
@@ -0,0 +1,19 @@
'use strict';
describe('railsTemplate plugin', function() {
var env = require('jsdoc/env');
var path = require('jsdoc/path');
var parser = jasmine.createParser();
var pluginPath = path.join(env.dirname, 'plugins/railsTemplate');
var plugin = require(pluginPath);
require('jsdoc/plugins').installPlugins([pluginPath], parser);
require('jsdoc/src/handlers').attachTo(parser);
it('should remove <% %> rails template tags from the source of *.erb files', function() {
var docSet = parser.parse([path.join(env.dirname, 'plugins/test/fixtures/railsTemplate.js.erb')]);
expect(docSet[2].description).toEqual('Remove rails tags from the source input (e.g. )');
});
});
+20
View File
@@ -0,0 +1,20 @@
'use strict';
describe('shout plugin', function() {
var env = require('jsdoc/env');
var path = require('jsdoc/path');
var docSet;
var parser = jasmine.createParser();
var pluginPath = 'plugins/shout';
var pluginPathResolved = path.join(env.dirname, pluginPath);
var plugin = require(pluginPathResolved);
require('jsdoc/plugins').installPlugins([pluginPathResolved], parser);
docSet = jasmine.getDocSetFromFile(pluginPath + '.js', parser);
it('should make the description uppercase', function() {
var doclet = docSet.getByLongname('module:plugins/shout.handlers.newDoclet');
expect(doclet[0].description).toEqual('MAKE YOUR DESCRIPTIONS MORE SHOUTIER.');
});
});
+22
View File
@@ -0,0 +1,22 @@
'use strict';
describe('sourcetag plugin', function() {
var env = require('jsdoc/env');
var path = require('jsdoc/path');
var docSet;
var parser = jasmine.createParser();
var pluginPath = 'plugins/sourcetag';
var pluginPathResolved = path.join(env.dirname, pluginPath);
require('jsdoc/plugins').installPlugins([pluginPathResolved], parser);
docSet = jasmine.getDocSetFromFile(pluginPath + '.js', parser);
it("should set the lineno and filename of the doclet's meta property", function() {
var doclet = docSet.getByLongname('module:plugins/sourcetag.handlers.newDoclet');
expect(doclet[0].meta).toBeDefined();
expect(doclet[0].meta.filename).toEqual('sourcetag.js');
expect(doclet[0].meta.lineno).toEqual(9);
});
});
+112
View File
@@ -0,0 +1,112 @@
/*global describe, expect, it */
'use strict';
var summarize = require('../../summarize');
describe('summarize', function() {
it('should export handlers', function() {
expect(summarize.handlers).toBeDefined();
expect(typeof summarize.handlers).toBe('object');
});
it('should export a newDoclet handler', function() {
expect(summarize.handlers.newDoclet).toBeDefined();
expect(typeof summarize.handlers.newDoclet).toBe('function');
});
describe('newDoclet handler', function() {
var handler = summarize.handlers.newDoclet;
it('should not blow up if the doclet is missing', function() {
function noDoclet() {
return handler({});
}
expect(noDoclet).not.toThrow();
});
it('should not change the summary if it is already defined', function() {
var doclet = {
summary: 'This is a summary.',
description: 'Descriptions are good.'
};
handler({ doclet: doclet });
expect(doclet.summary).not.toBe(doclet.description);
});
it('should not do anything if the description is missing', function() {
var doclet = {};
handler({ doclet: doclet });
expect(doclet.summary).not.toBeDefined();
});
it('should use the first sentence as the summary', function() {
var doclet = {
description: 'This sentence is the summary. This sentence is not.'
};
handler({ doclet: doclet });
expect(doclet.summary).toBe('This sentence is the summary.');
});
it('should not add an extra period if there is only one sentence in the description',
function() {
var doclet = {
description: 'This description has only one sentence.'
};
handler({ doclet: doclet });
expect(doclet.summary).toBe('This description has only one sentence.');
});
it('should use the entire description, plus a period, as the summary if the description ' +
'does not contain a period', function() {
var doclet = {
description: 'This is a description'
};
handler({ doclet: doclet });
expect(doclet.summary).toBe('This is a description.');
});
it('should use the entire description as the summary if the description contains only ' +
'one sentence', function() {
var doclet = {
description: 'This is a description.'
};
handler({ doclet: doclet });
expect(doclet.description).toBe('This is a description.');
});
it('should work when an HTML tag immediately follows the first sentence', function() {
var doclet = {
description: 'This sentence is the summary.<small>This sentence is small.</small>'
};
handler({ doclet: doclet });
expect(doclet.summary).toBe('This sentence is the summary.');
});
it('should generate valid HTML if a tag is opened, but not closed, in the summary',
function() {
var doclet = {
description: 'This description has <em>a tag. The tag straddles</em> sentences.'
};
handler({ doclet: doclet });
expect(doclet.summary).toBe('This description has <em>a tag.</em>');
});
it('should not include a <p> tag in the summary', function() {
var doclet = {
description: '<p>This description contains HTML.</p><p>And plenty of it!</p>'
};
handler({ doclet: doclet });
expect(doclet.summary).toBe('This description contains HTML.');
});
});
});
+35
View File
@@ -0,0 +1,35 @@
'use strict';
describe('underscore plugin', function () {
var env = require('jsdoc/env');
var path = require('jsdoc/path');
var docSet;
var parser = jasmine.createParser();
var pluginPath = 'plugins/underscore';
var fixturePath = 'plugins/test/fixtures/underscore';
var pluginPathResolved = path.join(env.dirname, pluginPath);
var plugin = require(pluginPathResolved);
require('jsdoc/plugins').installPlugins([pluginPathResolved], parser);
docSet = jasmine.getDocSetFromFile(fixturePath + '.js', parser);
it('should not mark normal, public properties as private', function() {
// Base line tests
var normal = docSet.getByLongname('normal');
expect(normal[0].access).toBeUndefined();
var realPrivate = docSet.getByLongname('Klass#privateProp');
expect(realPrivate[0].access).toEqual('private');
});
it('should hide doclet for symbols beginning with an underscore under normal circumstances', function () {
var hidden = docSet.getByLongname('_hidden');
expect(hidden[0].access).toEqual('private');
});
it('picks up "this"', function() {
var privateUnderscore = docSet.getByLongname('Klass#_privateProp');
expect(privateUnderscore[0].access).toEqual('private');
});
});
+16
View File
@@ -0,0 +1,16 @@
/**
* Removes all symbols that begin with an underscore from the doc output. If
* you're using underscores to denote private variables in modules, this
* automatically hides them.
*
* @module plugins/underscore
*/
exports.handlers = {
newDoclet({doclet}) {
// Ignore comment blocks for all symbols that begin with underscore
if (doclet.name.charAt(0) === '_' || doclet.name.substr(0, 6) === 'this._') {
doclet.access = 'private';
}
}
};
+27
View File
@@ -0,0 +1,27 @@
To create or use your own template:
1. Create a folder with the same name as your template (for example, `mycooltemplate`).
2. Within the template folder, create a file named `publish.js`. This file must be a CommonJS module that exports a method named `publish`.
For example:
````javascript
/** @module publish */
/**
* Generate documentation output.
*
* @param {TAFFY} data - A TaffyDB collection representing
* all the symbols documented in your code.
* @param {object} opts - An object with options information.
*/
exports.publish = function(data, opts) {
// do stuff here to generate your output files
};
````
To invoke JSDoc 3 with your own template, use the `-t` command line option, and specify the path to your template folder:
````
./jsdoc mycode.js -t /path/to/mycooltemplate
````
+9
View File
@@ -0,0 +1,9 @@
## Generating Typeface Fonts
The default template uses the [OpenSans](https://www.google.com/fonts/specimen/Open+Sans) typeface. The font files can be regenerated as follows:
1. Open the [OpenSans page at Font Squirrel](<http://www.fontsquirrel.com/fonts/open-sans>).
2. Click on the 'Webfont Kit' tab.
3. Either leave the subset drop-down as 'Western Latin (Default)', or, if we decide we need more glyphs, than change it to 'No Subsetting'.
4. Click the 'DOWNLOAD @FONT-FACE KIT' button.
5. For each typeface variant we plan to use, copy the 'eot', 'svg' and 'woff' files into the 'templates/default/static/fonts' directory.
+692
View File
@@ -0,0 +1,692 @@
const doop = require('jsdoc/util/doop');
const env = require('jsdoc/env');
const fs = require('jsdoc/fs');
const helper = require('jsdoc/util/templateHelper');
const logger = require('jsdoc/util/logger');
const path = require('jsdoc/path');
const { taffy } = require('@jsdoc/salty');
const template = require('jsdoc/template');
const util = require('util');
const htmlsafe = helper.htmlsafe;
const linkto = helper.linkto;
const resolveAuthorLinks = helper.resolveAuthorLinks;
const hasOwnProp = Object.prototype.hasOwnProperty;
let data;
let view;
let outdir = path.normalize(env.opts.destination);
function find(spec) {
return helper.find(data, spec);
}
function tutoriallink(tutorial) {
return helper.toTutorial(tutorial, null, {
tag: 'em',
classname: 'disabled',
prefix: 'Tutorial: '
});
}
function getAncestorLinks(doclet) {
return helper.getAncestorLinks(data, doclet);
}
function hashToLink(doclet, hash) {
let url;
if ( !/^(#.+)/.test(hash) ) {
return hash;
}
url = helper.createLink(doclet);
url = url.replace(/(#.+|$)/, hash);
return `<a href="${url}">${hash}</a>`;
}
function needsSignature({kind, type, meta}) {
let needsSig = false;
// function and class definitions always get a signature
if (kind === 'function' || kind === 'class') {
needsSig = true;
}
// typedefs that contain functions get a signature, too
else if (kind === 'typedef' && type && type.names &&
type.names.length) {
for (let i = 0, l = type.names.length; i < l; i++) {
if (type.names[i].toLowerCase() === 'function') {
needsSig = true;
break;
}
}
}
// and namespaces that are functions get a signature (but finding them is a
// bit messy)
else if (kind === 'namespace' && meta && meta.code &&
meta.code.type && meta.code.type.match(/[Ff]unction/)) {
needsSig = true;
}
return needsSig;
}
function getSignatureAttributes({optional, nullable}) {
const attributes = [];
if (optional) {
attributes.push('opt');
}
if (nullable === true) {
attributes.push('nullable');
}
else if (nullable === false) {
attributes.push('non-null');
}
return attributes;
}
function updateItemName(item) {
const attributes = getSignatureAttributes(item);
let itemName = item.name || '';
if (item.variable) {
itemName = `&hellip;${itemName}`;
}
if (attributes && attributes.length) {
itemName = util.format( '%s<span class="signature-attributes">%s</span>', itemName,
attributes.join(', ') );
}
return itemName;
}
function addParamAttributes(params) {
return params.filter(({name}) => name && !name.includes('.')).map(updateItemName);
}
function buildItemTypeStrings(item) {
const types = [];
if (item && item.type && item.type.names) {
item.type.names.forEach(name => {
types.push( linkto(name, htmlsafe(name)) );
});
}
return types;
}
function buildAttribsString(attribs) {
let attribsString = '';
if (attribs && attribs.length) {
attribsString = htmlsafe( util.format('(%s) ', attribs.join(', ')) );
}
return attribsString;
}
function addNonParamAttributes(items) {
let types = [];
items.forEach(item => {
types = types.concat( buildItemTypeStrings(item) );
});
return types;
}
function addSignatureParams(f) {
const params = f.params ? addParamAttributes(f.params) : [];
f.signature = util.format( '%s(%s)', (f.signature || ''), params.join(', ') );
}
function addSignatureReturns(f) {
const attribs = [];
let attribsString = '';
let returnTypes = [];
let returnTypesString = '';
const source = f.yields || f.returns;
// jam all the return-type attributes into an array. this could create odd results (for example,
// if there are both nullable and non-nullable return types), but let's assume that most people
// who use multiple @return tags aren't using Closure Compiler type annotations, and vice-versa.
if (source) {
source.forEach(item => {
helper.getAttribs(item).forEach(attrib => {
if (!attribs.includes(attrib)) {
attribs.push(attrib);
}
});
});
attribsString = buildAttribsString(attribs);
}
if (source) {
returnTypes = addNonParamAttributes(source);
}
if (returnTypes.length) {
returnTypesString = util.format( ' &rarr; %s{%s}', attribsString, returnTypes.join('|') );
}
f.signature = `<span class="signature">${f.signature || ''}</span><span class="type-signature">${returnTypesString}</span>`;
}
function addSignatureTypes(f) {
const types = f.type ? buildItemTypeStrings(f) : [];
f.signature = `${f.signature || ''}<span class="type-signature">${types.length ? ` :${types.join('|')}` : ''}</span>`;
}
function addAttribs(f) {
const attribs = helper.getAttribs(f);
const attribsString = buildAttribsString(attribs);
f.attribs = util.format('<span class="type-signature">%s</span>', attribsString);
}
function shortenPaths(files, commonPrefix) {
Object.keys(files).forEach(file => {
files[file].shortened = files[file].resolved.replace(commonPrefix, '')
// always use forward slashes
.replace(/\\/g, '/');
});
return files;
}
function getPathFromDoclet({meta}) {
if (!meta) {
return null;
}
return meta.path && meta.path !== 'null' ?
path.join(meta.path, meta.filename) :
meta.filename;
}
function generate(title, docs, filename, resolveLinks) {
let docData;
let html;
let outpath;
resolveLinks = resolveLinks !== false;
docData = {
env: env,
title: title,
docs: docs
};
outpath = path.join(outdir, filename);
html = view.render('container.tmpl', docData);
if (resolveLinks) {
html = helper.resolveLinks(html); // turn {@link foo} into <a href="foodoc.html">foo</a>
}
fs.writeFileSync(outpath, html, 'utf8');
}
function generateSourceFiles(sourceFiles, encoding = 'utf8') {
Object.keys(sourceFiles).forEach(file => {
let source;
// links are keyed to the shortened path in each doclet's `meta.shortpath` property
const sourceOutfile = helper.getUniqueFilename(sourceFiles[file].shortened);
helper.registerLink(sourceFiles[file].shortened, sourceOutfile);
try {
source = {
kind: 'source',
code: helper.htmlsafe( fs.readFileSync(sourceFiles[file].resolved, encoding) )
};
}
catch (e) {
logger.error('Error while generating source file %s: %s', file, e.message);
}
generate(`Source: ${sourceFiles[file].shortened}`, [source], sourceOutfile,
false);
});
}
/**
* Look for classes or functions with the same name as modules (which indicates that the module
* exports only that class or function), then attach the classes or functions to the `module`
* property of the appropriate module doclets. The name of each class or function is also updated
* for display purposes. This function mutates the original arrays.
*
* @private
* @param {Array.<module:jsdoc/doclet.Doclet>} doclets - The array of classes and functions to
* check.
* @param {Array.<module:jsdoc/doclet.Doclet>} modules - The array of module doclets to search.
*/
function attachModuleSymbols(doclets, modules) {
const symbols = {};
// build a lookup table
doclets.forEach(symbol => {
symbols[symbol.longname] = symbols[symbol.longname] || [];
symbols[symbol.longname].push(symbol);
});
modules.forEach(module => {
if (symbols[module.longname]) {
module.modules = symbols[module.longname]
// Only show symbols that have a description. Make an exception for classes, because
// we want to show the constructor-signature heading no matter what.
.filter(({description, kind}) => description || kind === 'class')
.map(symbol => {
symbol = doop(symbol);
if (symbol.kind === 'class' || symbol.kind === 'function') {
symbol.name = `${symbol.name.replace('module:', '(require("')}"))`;
}
return symbol;
});
}
});
}
function buildMemberNav(items, itemHeading, itemsSeen, linktoFn) {
let nav = '';
if (items.length) {
let itemsNav = '';
items.forEach(item => {
let displayName;
if ( !hasOwnProp.call(item, 'longname') ) {
itemsNav += `<li>${linktoFn('', item.name)}</li>`;
}
else if ( !hasOwnProp.call(itemsSeen, item.longname) ) {
if (env.conf.templates.default.useLongnameInNav) {
displayName = item.longname;
} else {
displayName = item.name;
}
itemsNav += `<li>${linktoFn(item.longname, displayName.replace(/\b(module|event):/g, ''))}</li>`;
itemsSeen[item.longname] = true;
}
});
if (itemsNav !== '') {
nav += `<h3>${itemHeading}</h3><ul>${itemsNav}</ul>`;
}
}
return nav;
}
function linktoTutorial(longName, name) {
return tutoriallink(name);
}
function linktoExternal(longName, name) {
return linkto(longName, name.replace(/(^"|"$)/g, ''));
}
/**
* Create the navigation sidebar.
* @param {object} members The members that will be used to create the sidebar.
* @param {array<object>} members.classes
* @param {array<object>} members.externals
* @param {array<object>} members.globals
* @param {array<object>} members.mixins
* @param {array<object>} members.modules
* @param {array<object>} members.namespaces
* @param {array<object>} members.tutorials
* @param {array<object>} members.events
* @param {array<object>} members.interfaces
* @return {string} The HTML for the navigation sidebar.
*/
function buildNav(members) {
let globalNav;
let nav = '<h2><a href="index.html">Home</a></h2>';
const seen = {};
const seenTutorials = {};
nav += buildMemberNav(members.modules, 'Modules', {}, linkto);
nav += buildMemberNav(members.externals, 'Externals', seen, linktoExternal);
nav += buildMemberNav(members.namespaces, 'Namespaces', seen, linkto);
nav += buildMemberNav(members.classes, 'Classes', seen, linkto);
nav += buildMemberNav(members.interfaces, 'Interfaces', seen, linkto);
nav += buildMemberNav(members.events, 'Events', seen, linkto);
nav += buildMemberNav(members.mixins, 'Mixins', seen, linkto);
nav += buildMemberNav(members.tutorials, 'Tutorials', seenTutorials, linktoTutorial);
if (members.globals.length) {
globalNav = '';
members.globals.forEach(({kind, longname, name}) => {
if ( kind !== 'typedef' && !hasOwnProp.call(seen, longname) ) {
globalNav += `<li>${linkto(longname, name)}</li>`;
}
seen[longname] = true;
});
if (!globalNav) {
// turn the heading into a link so you can actually get to the global page
nav += `<h3>${linkto('global', 'Global')}</h3>`;
}
else {
nav += `<h3>Global</h3><ul>${globalNav}</ul>`;
}
}
return nav;
}
/**
@param {TAFFY} taffyData See <http://taffydb.com/>.
@param {object} opts
@param {Tutorial} tutorials
*/
exports.publish = (taffyData, opts, tutorials) => {
let classes;
let conf;
let externals;
let files;
let fromDir;
let globalUrl;
let indexUrl;
let interfaces;
let members;
let mixins;
let modules;
let namespaces;
let outputSourceFiles;
let packageInfo;
let packages;
const sourceFilePaths = [];
let sourceFiles = {};
let staticFileFilter;
let staticFilePaths;
let staticFiles;
let staticFileScanner;
let templatePath;
data = taffyData;
conf = env.conf.templates || {};
conf.default = conf.default || {};
templatePath = path.normalize(opts.template);
view = new template.Template( path.join(templatePath, 'tmpl') );
// claim some special filenames in advance, so the All-Powerful Overseer of Filename Uniqueness
// doesn't try to hand them out later
indexUrl = helper.getUniqueFilename('index');
// don't call registerLink() on this one! 'index' is also a valid longname
globalUrl = helper.getUniqueFilename('global');
helper.registerLink('global', globalUrl);
// set up templating
view.layout = conf.default.layoutFile ?
path.getResourcePath(path.dirname(conf.default.layoutFile),
path.basename(conf.default.layoutFile) ) :
'layout.tmpl';
// set up tutorials for helper
helper.setTutorials(tutorials);
data = helper.prune(data);
data.sort('longname, version, since');
helper.addEventListeners(data);
data().each(doclet => {
let sourcePath;
doclet.attribs = '';
if (doclet.examples) {
doclet.examples = doclet.examples.map(example => {
let caption;
let code;
if (example.match(/^\s*<caption>([\s\S]+?)<\/caption>(\s*[\n\r])([\s\S]+)$/i)) {
caption = RegExp.$1;
code = RegExp.$3;
}
return {
caption: caption || '',
code: code || example
};
});
}
if (doclet.see) {
doclet.see.forEach((seeItem, i) => {
doclet.see[i] = hashToLink(doclet, seeItem);
});
}
// build a list of source files
if (doclet.meta) {
sourcePath = getPathFromDoclet(doclet);
sourceFiles[sourcePath] = {
resolved: sourcePath,
shortened: null
};
if (!sourceFilePaths.includes(sourcePath)) {
sourceFilePaths.push(sourcePath);
}
}
});
// update outdir if necessary, then create outdir
packageInfo = ( find({kind: 'package'}) || [] )[0];
if (packageInfo && packageInfo.name) {
outdir = path.join( outdir, packageInfo.name, (packageInfo.version || '') );
}
fs.mkPath(outdir);
// copy the template's static files to outdir
fromDir = path.join(templatePath, 'static');
staticFiles = fs.ls(fromDir, 3);
staticFiles.forEach(fileName => {
const toDir = fs.toDir( fileName.replace(fromDir, outdir) );
fs.mkPath(toDir);
fs.copyFileSync(fileName, toDir);
});
// copy user-specified static files to outdir
if (conf.default.staticFiles) {
// The canonical property name is `include`. We accept `paths` for backwards compatibility
// with a bug in JSDoc 3.2.x.
staticFilePaths = conf.default.staticFiles.include ||
conf.default.staticFiles.paths ||
[];
staticFileFilter = new (require('jsdoc/src/filter').Filter)(conf.default.staticFiles);
staticFileScanner = new (require('jsdoc/src/scanner').Scanner)();
staticFilePaths.forEach(filePath => {
let extraStaticFiles;
filePath = path.resolve(env.pwd, filePath);
extraStaticFiles = staticFileScanner.scan([filePath], 10, staticFileFilter);
extraStaticFiles.forEach(fileName => {
const sourcePath = fs.toDir(filePath);
const toDir = fs.toDir( fileName.replace(sourcePath, outdir) );
fs.mkPath(toDir);
fs.copyFileSync(fileName, toDir);
});
});
}
if (sourceFilePaths.length) {
sourceFiles = shortenPaths( sourceFiles, path.commonPrefix(sourceFilePaths) );
}
data().each(doclet => {
let docletPath;
const url = helper.createLink(doclet);
helper.registerLink(doclet.longname, url);
// add a shortened version of the full path
if (doclet.meta) {
docletPath = getPathFromDoclet(doclet);
docletPath = sourceFiles[docletPath].shortened;
if (docletPath) {
doclet.meta.shortpath = docletPath;
}
}
});
data().each(doclet => {
const url = helper.longnameToUrl[doclet.longname];
if (url.includes('#')) {
doclet.id = helper.longnameToUrl[doclet.longname].split(/#/).pop();
}
else {
doclet.id = doclet.name;
}
if ( needsSignature(doclet) ) {
addSignatureParams(doclet);
addSignatureReturns(doclet);
addAttribs(doclet);
}
});
// do this after the urls have all been generated
data().each(doclet => {
doclet.ancestors = getAncestorLinks(doclet);
if (doclet.kind === 'member') {
addSignatureTypes(doclet);
addAttribs(doclet);
}
if (doclet.kind === 'constant') {
addSignatureTypes(doclet);
addAttribs(doclet);
doclet.kind = 'member';
}
});
members = helper.getMembers(data);
members.tutorials = tutorials.children;
// output pretty-printed source files by default
outputSourceFiles = conf.default && conf.default.outputSourceFiles !== false;
// add template helpers
view.find = find;
view.linkto = linkto;
view.resolveAuthorLinks = resolveAuthorLinks;
view.tutoriallink = tutoriallink;
view.htmlsafe = htmlsafe;
view.outputSourceFiles = outputSourceFiles;
// once for all
view.nav = buildNav(members);
attachModuleSymbols( find({ longname: {left: 'module:'} }), members.modules );
// generate the pretty-printed source files first so other pages can link to them
if (outputSourceFiles) {
generateSourceFiles(sourceFiles, opts.encoding);
}
if (members.globals.length) { generate('Global', [{kind: 'globalobj'}], globalUrl); }
// index page displays information from package.json and lists files
files = find({kind: 'file'});
packages = find({kind: 'package'});
generate('Home',
packages.concat(
[{
kind: 'mainpage',
readme: opts.readme,
longname: (opts.mainpagetitle) ? opts.mainpagetitle : 'Main Page'
}]
).concat(files), indexUrl);
// set up the lists that we'll use to generate pages
classes = taffy(members.classes);
modules = taffy(members.modules);
namespaces = taffy(members.namespaces);
mixins = taffy(members.mixins);
externals = taffy(members.externals);
interfaces = taffy(members.interfaces);
Object.keys(helper.longnameToUrl).forEach(longname => {
const myClasses = helper.find(classes, {longname: longname});
const myExternals = helper.find(externals, {longname: longname});
const myInterfaces = helper.find(interfaces, {longname: longname});
const myMixins = helper.find(mixins, {longname: longname});
const myModules = helper.find(modules, {longname: longname});
const myNamespaces = helper.find(namespaces, {longname: longname});
if (myModules.length) {
generate(`Module: ${myModules[0].name}`, myModules, helper.longnameToUrl[longname]);
}
if (myClasses.length) {
generate(`Class: ${myClasses[0].name}`, myClasses, helper.longnameToUrl[longname]);
}
if (myNamespaces.length) {
generate(`Namespace: ${myNamespaces[0].name}`, myNamespaces, helper.longnameToUrl[longname]);
}
if (myMixins.length) {
generate(`Mixin: ${myMixins[0].name}`, myMixins, helper.longnameToUrl[longname]);
}
if (myExternals.length) {
generate(`External: ${myExternals[0].name}`, myExternals, helper.longnameToUrl[longname]);
}
if (myInterfaces.length) {
generate(`Interface: ${myInterfaces[0].name}`, myInterfaces, helper.longnameToUrl[longname]);
}
});
// TODO: move the tutorial functions to templateHelper.js
function generateTutorial(title, tutorial, filename) {
const tutorialData = {
title: title,
header: tutorial.title,
content: tutorial.parse(),
children: tutorial.children
};
const tutorialPath = path.join(outdir, filename);
let html = view.render('tutorial.tmpl', tutorialData);
// yes, you can use {@link} in tutorials too!
html = helper.resolveLinks(html); // turn {@link foo} into <a href="foodoc.html">foo</a>
fs.writeFileSync(tutorialPath, html, 'utf8');
}
// tutorials can have only one parent so there is no risk for loops
function saveChildren({children}) {
children.forEach(child => {
generateTutorial(`Tutorial: ${child.title}`, child, helper.tutorialToUrl(child.name));
saveChildren(child);
});
}
saveChildren(tutorials);
};
File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 116 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 118 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 120 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 114 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 120 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 117 KiB

@@ -0,0 +1,25 @@
/*global document */
(() => {
const source = document.getElementsByClassName('prettyprint source linenums');
let i = 0;
let lineNumber = 0;
let lineId;
let lines;
let totalLines;
let anchorHash;
if (source && source[0]) {
anchorHash = document.location.hash.substring(1);
lines = source[0].getElementsByTagName('li');
totalLines = lines.length;
for (; i < totalLines; i++) {
lineNumber++;
lineId = `line${lineNumber}`;
lines[i].id = lineId;
if (lineId === anchorHash) {
lines[i].className += ' selected';
}
}
}
})();
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
@@ -0,0 +1,2 @@
PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n "]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]*)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["com",
/^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]);

Some files were not shown because too many files have changed in this diff Show More