Greatly simplify and document the usage of JSDoc

This commit simplifies the exports.js plugin so it only relies
on the stability notes to generate the documentation, which
completely decouples it from the exportable API.

As a rule of thumb, whenever something has an 'api' annotation,
it should also have a 'stability' annotation. A more verbose
documentation of ol3 specific annotation usage is available in
the new 'apidoc/readme.md' file.

This commit also modifies all source files to implement these
usage suggestions.
This commit is contained in:
Andreas Hocevar
2014-04-13 10:43:05 +02:00
committed by Tim Schaub
parent aaf6101d0f
commit c17ac0cae3
84 changed files with 403 additions and 195 deletions

View File

@@ -22,7 +22,8 @@
"node_modules/jsdoc/plugins/markdown",
"apidoc/plugins/inheritdoc",
"apidoc/plugins/interface",
"apidoc/plugins/exports",
"apidoc/plugins/api",
"apidoc/plugins/olx-typedefs",
"apidoc/plugins/todo",
"apidoc/plugins/observable",
"apidoc/plugins/stability"

51
apidoc/plugins/api.js Normal file
View File

@@ -0,0 +1,51 @@
/*
* Based on @stability annotations, and assuming that items with no @stability
* annotation should not be documented, this plugin removes undocumented symbols
* from the documentation. Undocumented classes with documented members get a
* 'hideConstructur' property, which is read by the template so it can hide the
* constructor.
*/
function hasApiMembers(doclet) {
return doclet.longname.split('#')[0] == this.longname;
}
var api = [];
exports.handlers = {
newDoclet: function(e) {
var doclet = e.doclet;
// Keep track of api items - needed in parseComplete to determine classes
// with api members.
if (doclet.stability) {
api.push(doclet);
}
// Mark explicity defined namespaces - needed in parseComplete to keep
// namespaces that we need as containers for api items.
if (/.*\.jsdoc$/.test(doclet.meta.filename) && doclet.kind == 'namespace') {
doclet.namespace_ = true;
}
},
parseComplete: function(e) {
var doclets = e.doclets;
for (var i = doclets.length - 1; i >= 0; --i) {
var doclet = doclets[i];
// Always document namespaces and items with stability annotation
if (doclet.stability || doclet.namespace_) {
continue;
}
if (doclet.kind == 'class' && api.some(hasApiMembers, doclet)) {
// Mark undocumented classes with documented members as unexported.
// This is used in ../template/tmpl/container.tmpl to hide the
// constructor from the docs.
doclet.hideConstructor = true;
} else {
// Remove all other undocumented symbols
doclets.splice(i, 1);
}
}
}
};

View File

@@ -1,109 +0,0 @@
/*
* This plugin removes unexported symbols from the documentation.
* Unexported modules linked from @param or @fires will be marked unexported,
* and the documentation will not contain the constructor. Everything else is
* marked undocumented, which will remove it from the docs.
*/
var api = [];
var unexported = [];
var observablesByClass = {};
function collectExports(source) {
var symbols = JSON.parse(source).symbols;
for (var i = 0, ii = symbols.length; i < ii; ++i) {
api.push(symbols[i].name);
}
}
var encoding = env.conf.encoding || 'utf8';
var fs = require('jsdoc/fs');
collectExports(fs.readFileSync('build/symbols.json', encoding));
exports.handlers = {
newDoclet: function(e) {
var i, ii, j, jj;
if (e.doclet.meta.filename == "olx.js" && e.doclet.longname != 'olx') {
api.push(e.doclet.longname);
}
if (e.doclet.longname.indexOf('oli.') === 0) {
unexported.push(e.doclet.longname.replace(/^oli\./, 'ol.'));
}
if (api.indexOf(e.doclet.longname) > -1) {
var names, name;
var params = e.doclet.params;
if (params) {
for (i = 0, ii = params.length; i < ii; ++i) {
names = params[i].type.names;
if (names) {
for (j = 0, jj=names.length; j < jj; ++j) {
name = names[j];
if (unexported.indexOf(name) === -1) {
unexported.push(name);
}
}
}
}
}
var links = e.doclet.comment.match(/\{@link ([^\}]*)\}/g);
if (links) {
for (i=0, ii=links.length; i < ii; ++i) {
var link = links[i].match(/\{@link (.*)\}/)[1];
if (unexported.indexOf(link) === -1) {
unexported.push(link);
}
}
}
}
if (e.doclet.observables) {
var observables = observablesByClass[e.doclet.longname] = [];
for (i = e.doclet.observables.length - 1; i >= 0; --i) {
observables.push(e.doclet.observables[i].name);
}
}
},
parseComplete: function(e) {
for (var j = e.doclets.length - 1; j >= 0; --j) {
var doclet = e.doclets[j];
if (doclet.meta.filename == 'olx.js' && doclet.kind == 'typedef') {
for (var i = e.doclets.length - 1; i >= 0; --i) {
var propertyDoclet = e.doclets[i];
if (propertyDoclet.memberof == doclet.longname) {
if (!doclet.properties) {
doclet.properties = [];
}
doclet.properties.unshift(propertyDoclet);
e.doclets.splice(i, 1);
}
}
}
if (doclet.kind == 'namespace' || doclet.kind == 'event' || doclet.fires) {
continue;
}
var fqn = doclet.longname;
if (fqn) {
var getterOrSetter = fqn.match(/([^#]*)#[gs]et(.*)/);
if (getterOrSetter) {
var observables = observablesByClass[getterOrSetter[1]];
if (observables && observables.indexOf(getterOrSetter[2].toLowerCase()) > -1) {
// Always document getters/setters of observables
continue;
}
}
if (doclet.memberof && doclet.memberof.indexOf('oli.') === 0 &&
unexported.indexOf(doclet.memberof) > -1) {
// Always document members of referenced oli interfaces
continue;
}
doclet.unexported = (api.indexOf(fqn) === -1 && unexported.indexOf(fqn) !== -1);
if (api.indexOf(fqn) === -1 && unexported.indexOf(fqn) === -1) {
e.doclets.splice(j, 1);
}
}
}
}
};

View File

@@ -10,13 +10,16 @@ exports.defineTags = function(dictionary) {
}
});
var augmentsTag = dictionary.lookUp('augments');
dictionary.defineTag('implements', {
mustHaveValue: true,
onTagged: function(doclet, tag) {
tag.value = tag.value.match(/^\{?([^\}]*)\}?$/)[1];
augmentsTag.onTagged.apply(this, arguments);
if (!doclet.implements) {
doclet.implements = [];
}
doclet.implements.push(tag.value.match(/^{(.*)}$/)[1]);
doclet.implements.push(tag.value);
}
});

View File

@@ -0,0 +1,23 @@
/*
* Converts olx.js @type annotations into properties of the previous @typedef.
*/
var olxTypedef = null;
exports.handlers = {
newDoclet: function(e) {
var doclet = e.doclet;
if (doclet.meta.filename == 'olx.js') {
if (doclet.kind == 'typedef') {
olxTypedef = doclet;
doclet.properties = [];
} else if (olxTypedef && doclet.memberof == olxTypedef.longname) {
olxTypedef.properties.push(doclet);
} else {
olxTypedef = null;
}
}
}
};

136
apidoc/readme.md Normal file
View File

@@ -0,0 +1,136 @@
# API Documentation
This directory contains configuration (`conf.json`), static content (`index.md`), template (`template/`) and plugins (`plugins/`) for the [JSDoc3](http://usejsdoc.org/) API generator.
## Documenting the source code
JSDoc annotations are used for metadata used by the compiler, for defining the user facing API, and for user documentation.
In the simplest case, a JSDoc block can look like this:
```js
/**
* Add the given control to the map.
* @param {ol.control.Control} control Control.
* @todo stability experimental
* @todo api
*/
ol.Map.prototype.addControl = function(control) {
// ...
};
```
The first line is text for the user documentation. This can be long, and it can
contain Markdown.
The second line tells the Closure compiler the type of the argument.
The third line marks the API stability. Once the documentation story is fully settled, we will remove the `todo ` and just write `@stability experimental`. Without such a stability note, the method will not be documented in the generated API documentation.
The last line marks the method as exportable so it can be made available to the user facing API. This will also change to just `@api` eventually.
### Observable properties
For classes that inherit from `ol.Object`, there is a special documentation case for getters and setters:
```js
/**
* Get the size of this map.
* @return {ol.Size|undefined} Size.
* @todo stability experimental
*/
ol.Map.prototype.getSize = function() {
// ...
};
goog.exportProperty(
ol.Map.prototype,
'getSize',
ol.Map.prototype.getSize);
```
Because `ol.Object` needs to rely on these getter and setter names, these methods are not marked `@api` as exportable. Instead, `goog.exportProperty()` is used after the method definition to make sure that this method is always part of the API and not renamed in build configurations that do not need it.
To document observable properties with the `ol.ObjectEvent` types they are associated with, the `@observable` property is used (currently still `@todo observable`):
```js
* @constructor
* @todo observable layergroup {ol.layer.Group} a layer group containing the
* layers in this map.
* @todo observable size {ol.Size} the size in pixels of the map in the DOM
* @todo observable target {string|Element} the Element or id of the Element
* that the map is rendered in.
* @todo observable view {ol.IView} the view that controls this map
*/
ol.Map = function(options) {
```
The first argument to that annotation is the name of the property, then the type(s) in curly braces, and then a description. NOTE/TODO: The `apidoc/plugins/observable.js` plugin does currently not handle inherited observable properties.
### Events
Events are documented using `@fires` and `@event` annotations:
```js
/**
* Constants for event names.
* @enum {string}
*/
ol.MapBrowserEvent.EventType = {
/**
* A true single click with no dragging and no double click. Note that this
* event is delayed by 250 ms to ensure that it is not a double click.
* @event ol.MapBrowserEvent#singleclick
* @todo stability experimental
*/
SINGLECLICK: 'singleclick',
// ...
};
```
Note the value of the `@event` annotation. The text before the hash refers to the event class that the event belongs to, and the text after the hash is the type of the event. To export these properties, they need to be defined in `externs/oli.js` (also see `readme.md` in `externs/`). In addition, a stability note is required in the source code (`src/ol/MapBrowserEvent.js`) to make sure that documentation gets generated:
```js
ol.MapBrowserEvent = function(type, map, browserEvent, opt_frameState) {
// ...
/**
* @type {ol.Coordinate}
* @todo stability experimental
*/
this.coordinate = map.getEventCoordinate(this.originalEvent);
// ...
};
```
To document which events are fired by a class or method, the `@fires` annotation is used:
```js
* @fires {@link ol.MapBrowserEvent} ol.MapBrowserEvent
* @fires {@link ol.MapEvent} ol.MapEvent
* @fires {@link ol.render.Event} ol.render.Event
*/
ol.Map = function(options) {
// ...
};
```
Again, note the syntax of the `@fires` annotation. The link is necessary to provide a link to the documentation of the event, and the name of the event class is necessary for JSDoc3 to know which event we are talking about.
### Special cases with inheritance
When an item is marked `@api` in a subclass and not the base class, the documentation needs to be provided in the class where the item is exported. If the item is a (member) function, the `@function` annotation needs to be used:
```js
/**
* Read a feature from a GeoJSON Feature source. This method will throw
* an error if used with a FeatureCollection source.
* @function
* @param {ArrayBuffer|Document|Node|Object|string} source Source.
* @return {ol.Feature} Feature.
* @todo stability experimental
* @todo api
*/
ol.format.GeoJSON.prototype.readFeature;
```
The `@function` annotation is also needed when the function assignment is a
constant function from a `goog` namespace (e.g. `goog.AbstractMethod`).
For an abstract method, if it exported by every subclass, the documentation can be provided in the abstract class, with a `@stability` note. Implementing classes can use `@inheritDoc` and export the item:
```js
/**
* @inheritDoc
* @todo api
*/
```
When only a subset of the subclasses exports the item, @inheritDoc cannot
be used, and every exporting class needs to provide the documentation.

View File

@@ -27,7 +27,7 @@
<?js if (doc.kind === 'module' && doc.module) { ?>
<?js= self.partial('method.tmpl', doc.module) ?>
<?js } ?>
<?js if (!doc.unexported && doc.kind === 'class') { ?>
<?js if (doc.kind === 'class' && !doc.hideConstructor && !doc.interface) { ?>
<?js= self.partial('method.tmpl', doc) ?>
<?js } else { ?>
<?js if (doc.description) { ?>
@@ -47,7 +47,8 @@
<h3 class="subsection-title">Extends</h3>
<ul><?js doc.augments.forEach(function(a) { ?>
<li><?js= self.linkto(a, a) ?></li>
<li><?js= self.linkto(a, a) ?>
<?js= (doc.implements&&doc.implements.indexOf(a)>-1?'(Interface)':'') ?></li>
<?js }); ?></ul>
<?js } ?>