Improve autogen for reference docs including jsdoc support

Summary:
As part of improving the API and Component reference docs #8154 this pull request adds the following:

- jsdoc support for API docs. See the AlertIOS changes as an example.
- type definitions support and added to both API and Component docs. This is supported via react-docgen and jsdoc.
- better formatting of method properties (now shown in a table).

FYI, API and Component docs were previously generated in two different ways. Components were using react-docgen and that basically remains as-is. APIs were using custom parsing code and that's been switched to use a jsdoc parser + react-docgen as an option for typedefs (it could also use the jsdoc parser).

Two docs have been updated to showcase how we'd like the new docs to look:

- AlertIOS (API): showing method parameters, examples, typedefs, more details overall.
- Statusbar (Component): showing method parameters, typedefs, more details overall.

**Note**: To convert new API docs to use the new format, add `jsdoc` to the initial file comment. C
Closes https://github.com/facebook/react-native/pull/8196

Differential Revision: D3465037

Pulled By: lacker

fbshipit-source-id: 78415d44bc5be02db802f5b1f7a0b249689abdf7
This commit is contained in:
Christine Abernathy 2016-06-21 14:22:44 -07:00 committed by Facebook Github Bot 2
parent bdb8efdd66
commit 22f59a88a6
9 changed files with 906 additions and 98 deletions

View File

@ -19,14 +19,35 @@ const processColor = require('processColor');
const StatusBarManager = require('NativeModules').StatusBarManager;
/**
* Status bar style
*/
export type StatusBarStyle = $Enum<{
/**
* Default status bar style
*/
'default': string,
/**
* Dark background style
*/
'light-content': string,
}>;
/**
* Status bar animation
*/
export type StatusBarAnimation = $Enum<{
/**
* No animation
*/
'none': string,
/**
* Fade animation
*/
'fade': string,
/**
* Slide animation
*/
'slide': string,
}>;
@ -135,6 +156,13 @@ const StatusBar = React.createClass({
// Provide an imperative API as static functions of the component.
// See the corresponding prop for more detail.
/**
* Show or hide the status bar
* @param hidden The dialog's title.
* @param animation Optional animation when
* changing the status bar hidden property.
*/
setHidden(hidden: boolean, animation?: StatusBarAnimation) {
animation = animation || 'none';
StatusBar._defaultProps.hidden.value = hidden;
@ -145,6 +173,11 @@ const StatusBar = React.createClass({
}
},
/**
* Set the status bar style
* @param style Status bar style to set
* @param animated Animate the style change.
*/
setBarStyle(style: StatusBarStyle, animated?: boolean) {
if (Platform.OS !== 'ios') {
console.warn('`setBarStyle` is only available on iOS');
@ -155,6 +188,10 @@ const StatusBar = React.createClass({
StatusBarManager.setStyle(style, animated);
},
/**
* Control the visibility of the network activity indicator
* @param visible Show the indicator.
*/
setNetworkActivityIndicatorVisible(visible: boolean) {
if (Platform.OS !== 'ios') {
console.warn('`setNetworkActivityIndicatorVisible` is only available on iOS');
@ -164,6 +201,11 @@ const StatusBar = React.createClass({
StatusBarManager.setNetworkActivityIndicatorVisible(visible);
},
/**
* Set the background color for the status bar
* @param color Background color.
* @param animated Animate the style change.
*/
setBackgroundColor(color: string, animated?: boolean) {
if (Platform.OS !== 'android') {
console.warn('`setBackgroundColor` is only available on Android');
@ -174,6 +216,10 @@ const StatusBar = React.createClass({
StatusBarManager.setColor(processColor(color), animated);
},
/**
* Control the translucency of the status bar
* @param translucent Set as translucent.
*/
setTranslucent(translucent: boolean) {
if (Platform.OS !== 'android') {
console.warn('`setTranslucent` is only available on Android');

View File

@ -8,64 +8,129 @@
*
* @providesModule AlertIOS
* @flow
* @jsdoc
*/
'use strict';
var RCTAlertManager = require('NativeModules').AlertManager;
/**
* An Alert button type
*/
export type AlertType = $Enum<{
/**
* Default alert with no inputs
*/
'default': string;
/**
* Plain text input alert
*/
'plain-text': string;
/**
* Secure text input alert
*/
'secure-text': string;
/**
* Login and password alert
*/
'login-password': string;
}>;
/**
* An Alert button style
*/
export type AlertButtonStyle = $Enum<{
/**
* Default button style
*/
'default': string;
/**
* Cancel button style
*/
'cancel': string;
/**
* Destructive button style
*/
'destructive': string;
}>;
/**
* Array or buttons
* @typedef {Array} ButtonsArray
* @property {string=} text Button label
* @property {Function=} onPress Callback function when button pressed
* @property {AlertButtonStyle=} style Button style
*/
type ButtonsArray = Array<{
/**
* Button label
*/
text?: string;
/**
* Callback function when button pressed
*/
onPress?: ?Function;
/**
* Button style
*/
style?: AlertButtonStyle;
}>;
/**
* The AlertsIOS utility provides two functions: `alert` and `prompt`. All
* functionality available through `AlertIOS.alert` is also available in the
* cross-platform `Alert.alert`, which we recommend you use if you don't need
* iOS-specific functionality.
* @description
* `AlertIOS` provides functionality to create an iOS alert dialog with a
* message or create a prompt for user input.
*
* `AlertIOS.prompt` allows you to prompt the user for input inside of an
* alert popup.
* Creating an iOS alert:
*
* ```
* AlertIOS.alert(
* 'Sync Complete',
* 'All your data are belong to us.'
* );
* ```
*
* Creating an iOS prompt:
*
* ```
* AlertIOS.prompt(
* 'Enter a value',
* null,
* text => console.log("You entered "+text)
* );
* ```
*
* We recommend using the [`Alert.alert`](/docs/alert.html) method for
* cross-platform support if you don't need to create iOS-only prompts.
*
*/
class AlertIOS {
/**
* Creates a popup to alert the user. See
* [Alert](docs/alert.html).
*
* - title: string -- The dialog's title.
* - message: string -- An optional message that appears above the text input.
* - callbackOrButtons -- This optional argument should be either a
* single-argument function or an array of buttons. If passed a function,
* it will be called when the user taps 'OK'.
* Create and display a popup alert.
* @static
* @method alert
* @param title The dialog's title.
* @param message An optional message that appears below
* the dialog's title.
* @param callbackOrButtons This optional argument should
* be either a single-argument function or an array of buttons. If passed
* a function, it will be called when the user taps 'OK'.
*
* If passed an array of button configurations, each button should include
* a `text` key, as well as optional `onPress` and `style` keys.
* `style` should be one of 'default', 'cancel' or 'destructive'.
* - type -- *deprecated, do not use*
* a `text` key, as well as optional `onPress` and `style` keys. `style`
* should be one of 'default', 'cancel' or 'destructive'.
* @param type Deprecated, do not use.
*
* Example:
* @example <caption>Example with custom buttons</caption>
*
* ```
* AlertIOS.alert(
* 'Sync Complete',
* 'All your data are belong to us.'
* 'Update available',
* 'Keep your app up to date to enjoy the latest features',
* [
* {text: 'Cancel', onPress: () => console.log('Cancel Pressed'), style: 'cancel'},
* {text: 'Install', onPress: () => console.log('Install Pressed')},
* ],
* );
* ```
*/
static alert(
title: ?string,
@ -82,23 +147,26 @@ class AlertIOS {
}
/**
* Prompt the user to enter some text.
*
* - title: string -- The dialog's title.
* - message: string -- An optional message that appears above the text input.
* - callbackOrButtons -- This optional argument should be either a
* single-argument function or an array of buttons. If passed a function,
* it will be called with the prompt's value when the user taps 'OK'.
* Create and display a prompt to enter some text.
* @static
* @method prompt
* @param title The dialog's title.
* @param message An optional message that appears above the text
* input.
* @param callbackOrButtons This optional argument should
* be either a single-argument function or an array of buttons. If passed
* a function, it will be called with the prompt's value when the user
* taps 'OK'.
*
* If passed an array of button configurations, each button should include
* a `text` key, as well as optional `onPress` and `style` keys (see example).
* `style` should be one of 'default', 'cancel' or 'destructive'.
* - type: string -- This configures the text input. One of 'plain-text',
* a `text` key, as well as optional `onPress` and `style` keys (see
* example). `style` should be one of 'default', 'cancel' or 'destructive'.
* @param type This configures the text input. One of 'plain-text',
* 'secure-text' or 'login-password'.
* - defaultValue: string -- the default value for the text field.
* @param defaultValue The dialog's title.
*
* @example <caption>Example with custom buttons</caption>
*
* Example with custom buttons:
* ```
* AlertIOS.prompt(
* 'Enter password',
* 'Enter your password to claim your $1.5B in lottery winnings',
@ -108,18 +176,16 @@ class AlertIOS {
* ],
* 'secure-text'
* );
* ```
*
* Example with the default button and a custom callback:
* ```
* @example <caption>Example with the default button and a custom callback</caption>
*
* AlertIOS.prompt(
* 'Update username',
* null,
* text => console.log("Your username is "+text),
* null,
* 'default'
* )
* ```
* );
*/
static prompt(
title: ?string,

View File

@ -0,0 +1,15 @@
{
"tags": {
"allowUnknownTags": true,
"dictionaries": ["jsdoc","closure"]
},
"source": {
"includePattern": ".+\\.js(doc)?$",
"excludePattern": "(^|\\/|\\\\)_"
},
"plugins": ["./jsdocs/jsdoc-plugin-values"],
"templates": {
"cleverLinks": true,
"monospaceLinks": false
}
}

View File

@ -0,0 +1,11 @@
exports.defineTags = function(dictionary) {
dictionary.defineTag('value', {
mustHaveValue: true,
canHaveType: true,
canHaveName: true,
onTagged: function(doclet, tag) {
if (!doclet.values) { doclet.values = []; }
doclet.values.push(tag.value);
}
});
};

View File

@ -9,6 +9,8 @@
* @providesModule AutodocsLayout
*/
'use strict';
var DocsSidebar = require('DocsSidebar');
var H = require('Header');
var Header = require('Header');
@ -93,6 +95,45 @@ function renderType(type) {
return type.name;
}
function renderTypeNameLink(typeName, docPath, namedTypes) {
const ignoreTypes = [
'string',
'number',
'boolean',
'object',
'function',
'array',
];
const typeNameLower = typeName.toLowerCase();
if (ignoreTypes.indexOf(typeNameLower) !== -1 || !namedTypes[typeNameLower]) {
return typeName;
}
return <a href={docPath + '#' + typeNameLower}>{typeName}</a>;
}
function renderTypeWithLinks(type, docTitle, namedTypes) {
if (!type || !type.names) {
return null;
}
const docPath = docTitle ? 'docs/' + docTitle.toLowerCase() + '.html' : 'docs/';
return (
<div>
{
type.names.map((typeName, index, array) => {
let separator = index < array.length - 1 && ' | ';
return (
<span key={index}>
{renderTypeNameLink(typeName, docPath, namedTypes)}
{separator}
</span>
);
})
}
</div>
);
}
function sortByPlatform(props, nameA, nameB) {
var a = props[nameA];
var b = props[nameB];
@ -134,6 +175,17 @@ function removeCommentsFromDocblock(docblock) {
.join('\n');
}
function getNamedTypes(typedefs) {
let namedTypes = {};
typedefs && typedefs.forEach(typedef => {
if (typedef.name) {
const type = typedef.name.toLowerCase();
namedTypes[type] = 1;
}
});
return namedTypes;
}
var ComponentDoc = React.createClass({
renderProp: function(name, prop) {
return (
@ -254,20 +306,22 @@ var ComponentDoc = React.createClass({
}
},
renderMethod: function(method) {
renderMethod: function(method, namedTypes) {
return (
<Method
key={method.name}
name={method.name}
description={method.description}
params={method.params}
modifiers={method.modifiers}
modifiers={method.scope ? [method.scope] : method.modifiers}
examples={method.examples}
returns={method.returns}
namedTypes={namedTypes}
/>
);
},
renderMethods: function(methods) {
renderMethods: function(methods, namedTypes) {
if (!methods || !methods.length) {
return null;
}
@ -277,7 +331,38 @@ var ComponentDoc = React.createClass({
<div className="props">
{methods.filter((method) => {
return method.name[0] !== '_';
}).map(this.renderMethod)}
}).map(method => this.renderMethod(method, namedTypes))}
</div>
</span>
);
},
renderTypeDef: function(typedef, namedTypes) {
return (
<TypeDef
key={typedef.name}
name={typedef.name}
description={typedef.description}
type={typedef.type}
properties={typedef.properties}
values={typedef.values}
apiName={this.props.apiName}
namedTypes={namedTypes}
/>
);
},
renderTypeDefs: function(typedefs, namedTypes) {
if (!typedefs || !typedefs.length) {
return null;
}
return (
<span>
<H level={3}>Type Definitions</H>
<div className="props">
{typedefs.map((typedef) => {
return this.renderTypeDef(typedef, namedTypes);
})}
</div>
</span>
);
@ -286,6 +371,7 @@ var ComponentDoc = React.createClass({
render: function() {
var content = this.props.content;
this.extractPlatformFromProps(content.props);
const namedTypes = getNamedTypes(content.typedef);
return (
<div>
<Marked>
@ -293,7 +379,8 @@ var ComponentDoc = React.createClass({
</Marked>
<H level={3}>Props</H>
{this.renderProps(content.props, content.composes)}
{this.renderMethods(content.methods)}
{this.renderMethods(content.methods, namedTypes)}
{this.renderTypeDefs(content.typedef, namedTypes)}
</div>
);
}
@ -301,19 +388,22 @@ var ComponentDoc = React.createClass({
var APIDoc = React.createClass({
renderMethod: function(method) {
renderMethod: function(method, namedTypes) {
return (
<Method
key={method.name}
name={method.name}
description={method.docblock && removeCommentsFromDocblock(method.docblock)}
description={method.description || method.docblock && removeCommentsFromDocblock(method.docblock)}
params={method.params}
modifiers={method.modifiers}
modifiers={method.scope ? [method.scope] : method.modifiers}
examples={method.examples}
apiName={this.props.apiName}
namedTypes={namedTypes}
/>
);
},
renderMethods: function(methods) {
renderMethods: function(methods, namedTypes) {
if (!methods.length) {
return null;
}
@ -323,7 +413,7 @@ var APIDoc = React.createClass({
<div className="props">
{methods.filter((method) => {
return method.name[0] !== '_';
}).map(this.renderMethod)}
}).map(method => this.renderMethod(method, namedTypes))}
</div>
</span>
);
@ -363,7 +453,7 @@ var APIDoc = React.createClass({
);
},
renderClasses: function(classes) {
renderClasses: function(classes, namedTypes) {
if (!classes || !classes.length) {
return null;
}
@ -382,7 +472,7 @@ var APIDoc = React.createClass({
{cls.docblock && <Marked>
{removeCommentsFromDocblock(cls.docblock)}
</Marked>}
{this.renderMethods(cls.methods)}
{this.renderMethods(cls.methods, namedTypes)}
{this.renderProperties(cls.properties)}
</ul>
</span>
@ -393,6 +483,55 @@ var APIDoc = React.createClass({
);
},
renderTypeDef: function(typedef, namedTypes) {
return (
<TypeDef
key={typedef.name}
name={typedef.name}
description={typedef.description}
type={typedef.type}
properties={typedef.properties}
values={typedef.values}
apiName={this.props.apiName}
namedTypes={namedTypes}
/>
);
},
renderTypeDefs: function(typedefs, namedTypes) {
if (!typedefs || !typedefs.length) {
return null;
}
return (
<span>
<H level={3}>Type Definitions</H>
<div className="props">
{typedefs.map((typedef) => {
return this.renderTypeDef(typedef, namedTypes);
})}
</div>
</span>
);
},
renderMainDescription: function(content) {
if (content.docblock) {
return (
<Marked>
{removeCommentsFromDocblock(content.docblock)}
</Marked>
);
}
if (content.class && content.class.length && content.class[0].description) {
return (
<Marked>
{content.class[0].description}
</Marked>
);
}
return null;
},
render: function() {
var content = this.props.content;
if (!content.methods) {
@ -400,14 +539,14 @@ var APIDoc = React.createClass({
'No component methods found for ' + content.componentName
);
}
const namedTypes = getNamedTypes(content.typedef);
return (
<div>
<Marked>
{removeCommentsFromDocblock(content.docblock)}
</Marked>
{this.renderMethods(content.methods)}
{this.renderMainDescription(content)}
{this.renderMethods(content.methods, namedTypes)}
{this.renderProperties(content.properties)}
{this.renderClasses(content.classes)}
{this.renderClasses(content.classes, namedTypes)}
{this.renderTypeDefs(content.typedef, namedTypes)}
</div>
);
}
@ -440,11 +579,73 @@ var Method = React.createClass({
return this.renderTypehintRec(typehint);
},
renderMethodExamples: function(examples) {
if (!examples || !examples.length) {
return null;
}
return examples.map((example) => {
const re = /<caption>(.*?)<\/caption>/ig;
const result = re.exec(example);
const caption = result ? result[1] + ':' : 'Example:';
const code = example.replace(/<caption>.*?<\/caption>/ig, '')
.replace(/^\n\n/, '');
return (
<div>
<br/>
{caption}
<Prism>
{code}
</Prism>
</div>
);
});
},
renderMethodParameters: function(params) {
if (!params || !params.length) {
return null;
}
if (!params[0].type || !params[0].type.names) {
return null;
}
const foundDescription = params.find(p => p.description);
if (!foundDescription) {
return null;
}
return (
<div>
<strong>Parameters:</strong>
<table className="params">
<thead>
<tr>
<th>Name and Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
{params.map((param) => {
return (
<tr>
<td>
{param.optional ? '[' + param.name + ']' : param.name}
<br/><br/>
{renderTypeWithLinks(param.type, this.props.apiName, this.props.namedTypes)}
</td>
<td className="description"><Marked>{param.description}</Marked></td>
</tr>
);
})}
</tbody>
</table>
</div>
);
},
render: function() {
return (
<div className="prop">
<Header level={4} className="propTitle" toSlug={this.props.name}>
{this.props.modifiers.length && <span className="propType">
{this.props.modifiers && this.props.modifiers.length && <span className="propType">
{this.props.modifiers.join(' ') + ' '}
</span> || ''}
{this.props.name}
@ -452,9 +653,7 @@ var Method = React.createClass({
({this.props.params
.map((param) => {
var res = param.name;
if (param.type) {
res += ': ' + this.renderTypehint(param.type);
}
res += param.optional ? '?' : '';
return res;
})
.join(', ')})
@ -464,6 +663,100 @@ var Method = React.createClass({
{this.props.description && <Marked>
{this.props.description}
</Marked>}
{this.renderMethodParameters(this.props.params)}
{this.renderMethodExamples(this.props.examples)}
</div>
);
},
});
var TypeDef = React.createClass({
renderProperties: function(properties) {
if (!properties || !properties.length) {
return null;
}
if (!properties[0].type || !properties[0].type.names) {
return null;
}
return (
<div>
<br/>
<strong>Properties:</strong>
<table className="params">
<thead>
<tr>
<th>Name and Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
{properties.map((property) => {
return (
<tr>
<td>
{property.optional ? '[' + property.name + ']' : property.name}
<br/><br/>
{renderTypeWithLinks(property.type, this.props.apiName, this.props.namedTypes)}
</td>
<td className="description"><Marked>{property.description}</Marked></td>
</tr>
);
})}
</tbody>
</table>
</div>
);
},
renderValues: function(values) {
if (!values || !values.length) {
return null;
}
if (!values[0].type || !values[0].type.names) {
return null;
}
return (
<div>
<br/>
<strong>Constants:</strong>
<table className="params">
<thead>
<tr>
<th>Value</th>
<th>Description</th>
</tr>
</thead>
<tbody>
{values.map((value) => {
return (
<tr>
<td>
{value.name}
</td>
<td className="description"><Marked>{value.description}</Marked></td>
</tr>
);
})}
</tbody>
</table>
</div>
);
},
render: function() {
return (
<div className="prop">
<Header level={4} className="propTitle" toSlug={this.props.name}>
{this.props.name}
</Header>
{this.props.description && <Marked>
{this.props.description}
</Marked>}
<strong>Type:</strong>
<br/>
{this.props.type.names.join(' | ')}
{this.renderProperties(this.props.properties)}
{this.renderValues(this.props.values)}
</div>
);
},

View File

@ -13,9 +13,17 @@
"mkdirp": "^0.5.1",
"optimist": "0.6.0",
"react": "~0.13.0",
"react-docgen": "^2.8.0",
"react-docgen": "^2.9.0",
"react-page-middleware": "git://github.com/facebook/react-page-middleware.git",
"request": "^2.69.0",
"semver-compare": "^1.0.0"
"semver-compare": "^1.0.0",
"babel-core": "^6.6.4",
"babel-plugin-external-helpers": "^6.5.0",
"babel-polyfill": "^6.6.1",
"babel-preset-react-native": "~1.6.0",
"babel-register": "^6.6.0",
"babel-types": "^6.6.4",
"jsdoc-api": "^1.1.0",
"deep-assign": "^2.0.0"
}
}

View File

@ -1,8 +1,8 @@
"use strict";
var docgen = require('react-docgen');
'use strict';
const docgen = require('react-docgen');
function stylePropTypeHandler(documentation, path) {
var propTypesPath = docgen.utils.getMemberValuePath(path, 'propTypes');
let propTypesPath = docgen.utils.getMemberValuePath(path, 'propTypes');
if (!propTypesPath) {
return;
}
@ -18,25 +18,25 @@ function stylePropTypeHandler(documentation, path) {
docgen.utils.getPropertyName(propertyPath) !== 'style') {
return;
}
var valuePath = docgen.utils.resolveToValue(propertyPath.get('value'));
let valuePath = docgen.utils.resolveToValue(propertyPath.get('value'));
// If it's a call to StyleSheetPropType, do stuff
if (valuePath.node.type !== 'CallExpression' ||
valuePath.node.callee.name !== 'StyleSheetPropType') {
return;
}
// Get type of style sheet
var styleSheetModule = docgen.utils.resolveToModule(
let styleSheetModule = docgen.utils.resolveToModule(
valuePath.get('arguments', 0)
);
if (styleSheetModule) {
var propDescriptor = documentation.getPropDescriptor('style');
let propDescriptor = documentation.getPropDescriptor('style');
propDescriptor.type = {name: 'stylesheet', value: styleSheetModule};
}
});
}
function deprecatedPropTypeHandler(documentation, path) {
var propTypesPath = docgen.utils.getMemberValuePath(path, 'propTypes');
let propTypesPath = docgen.utils.getMemberValuePath(path, 'propTypes');
if (!propTypesPath) {
return;
}
@ -48,13 +48,13 @@ function deprecatedPropTypeHandler(documentation, path) {
// Checks for deprecatedPropType function and add deprecation info.
propTypesPath.get('properties').each(function(propertyPath) {
var valuePath = docgen.utils.resolveToValue(propertyPath.get('value'));
let valuePath = docgen.utils.resolveToValue(propertyPath.get('value'));
// If it's a call to deprecatedPropType, do stuff
if (valuePath.node.type !== 'CallExpression' ||
valuePath.node.callee.name !== 'deprecatedPropType') {
return;
}
var propDescriptor = documentation.getPropDescriptor(
let propDescriptor = documentation.getPropDescriptor(
docgen.utils.getPropertyName(propertyPath)
);
// The 2nd argument of deprecatedPropType is the deprecation message.
@ -72,13 +72,104 @@ function deprecatedPropTypeHandler(documentation, path) {
});
}
function typedefHandler(documentation, path) {
const declarationPath = path.get('declaration');
const typePath = declarationPath.get('right');
// Name, type, description of the typedef
const name = declarationPath.value.id.name;
const type = { names: [typePath.node.id.name] };
const description = docgen.utils.docblock.getDocblock(path);
// Get the properties for the typedef
let paramsDescriptions = [];
let paramsTypes;
if (typePath.node.typeParameters) {
const paramsPath = typePath.get('typeParameters').get('params', 0);
if (paramsPath) {
const properties = paramsPath.get('properties');
// Get the descriptions inside each property (are inline leading comments)
paramsDescriptions =
properties.map(property => docgen.utils.docblock.getDocblock(property));
// Get the property type info
paramsTypes = docgen.utils.getFlowType(paramsPath);
}
}
// Get the property type, description and value info
let values = [];
if (paramsTypes && paramsTypes.signature && paramsTypes.signature.properties &&
paramsTypes.signature.properties.length !== 0) {
values = paramsTypes.signature.properties.map((property, index) => {
return {
type: { names: [property.value.name] },
description: paramsDescriptions[index],
name: property.key,
};
});
}
let typedef = {
name: name,
description: description,
type: type,
values: values,
};
documentation.set('typedef', typedef);
}
function getTypeName(type) {
let typeName;
switch (type.name) {
case 'signature':
typeName = type.type;
break;
case 'union':
typeName = type.elements.map(getTypeName);
break;
default:
typeName = type.alias ? type.alias : type.name;
break;
}
return typeName;
}
function jsDocFormatType(entities) {
let modEntities = entities;
if (entities) {
if (typeof entities === 'object' && entities.length) {
entities.map((entity, entityIndex) => {
if (entity.type) {
const typeNames = [].concat(getTypeName(entity.type));
modEntities[entityIndex].type = { names: typeNames };
}
});
} else {
modEntities.type = [].concat(getTypeName(entities));
}
}
return modEntities;
}
function jsDocFormatHandler(documentation, path) {
const methods = documentation.get('methods');
if (!methods || methods.length === 0) {
return;
}
let modMethods = methods;
methods.map((method, methodIndex) => {
modMethods[methodIndex].params = jsDocFormatType(method.params);
modMethods[methodIndex].returns = jsDocFormatType(method.returns);
});
documentation.set('methods', modMethods);
}
function findExportedOrFirst(node, recast) {
return docgen.resolver.findExportedComponentDefinition(node, recast) ||
docgen.resolver.findAllComponentDefinitions(node, recast)[0];
}
function findExportedObject(ast, recast) {
var objPath;
let objPath;
recast.visit(ast, {
visitAssignmentExpression: function(path) {
if (!objPath && docgen.utils.isExportsOrModuleAssignment(path)) {
@ -93,9 +184,9 @@ function findExportedObject(ast, recast) {
// handler.
// This converts any expression, e.g. `foo` to an object expression of
// the form `{propTypes: foo}`
var b = recast.types.builders;
var nt = recast.types.namedTypes;
var obj = objPath.node;
let b = recast.types.builders;
let nt = recast.types.namedTypes;
let obj = objPath.node;
// Hack: This is converting calls like
//
@ -107,7 +198,7 @@ function findExportedObject(ast, recast) {
if (nt.CallExpression.check(obj) &&
recast.print(obj.callee).code === 'Object.assign') {
obj = objPath.node.arguments[1];
var firstArg = objPath.node.arguments[0];
let firstArg = objPath.node.arguments[0];
if (recast.print(firstArg.callee).code === 'Object.create') {
firstArg = firstArg.arguments[0];
}
@ -123,7 +214,29 @@ function findExportedObject(ast, recast) {
return objPath;
}
function findExportedType(ast, recast) {
let types = recast.types.namedTypes;
let definitions;
recast.visit(ast, {
visitExportNamedDeclaration: function(path) {
if (path.node.declaration) {
if (types.TypeAlias.check(path.node.declaration)) {
if (!definitions) {
definitions = [];
}
definitions.push(path);
}
}
return false;
}
});
return definitions;
}
exports.stylePropTypeHandler = stylePropTypeHandler;
exports.deprecatedPropTypeHandler = deprecatedPropTypeHandler;
exports.typedefHandler = typedefHandler;
exports.jsDocFormatHandler = jsDocFormatHandler;
exports.findExportedOrFirst = findExportedOrFirst;
exports.findExportedObject = findExportedObject;
exports.findExportedType = findExportedType;

View File

@ -15,6 +15,9 @@ const fs = require('fs');
const jsDocs = require('../jsdocs/jsdocs.js');
const path = require('path');
const slugify = require('../core/slugify');
const babel = require('babel-core');
const jsdocApi = require('jsdoc-api');
const deepAssign = require('deep-assign');
const ANDROID_SUFFIX = 'android';
const CROSS_SUFFIX = 'cross';
@ -58,8 +61,8 @@ function getPlatformFromPath(filepath) {
}
function getExamplePaths(componentName, componentPlatform) {
var componentExample = '../Examples/UIExplorer/' + componentName + 'Example.';
var pathsToCheck = [
const componentExample = '../Examples/UIExplorer/' + componentName + 'Example.';
let pathsToCheck = [
componentExample + 'js',
componentExample + componentPlatform + '.js',
];
@ -69,7 +72,7 @@ function getExamplePaths(componentName, componentPlatform) {
componentExample + ANDROID_SUFFIX + '.js'
);
}
var paths = [];
let paths = [];
pathsToCheck.map((p) => {
if (fs.existsSync(p)) {
paths.push(p);
@ -79,12 +82,12 @@ function getExamplePaths(componentName, componentPlatform) {
}
function getExamples(componentName, componentPlatform) {
var paths = getExamplePaths(componentName, componentPlatform);
const paths = getExamplePaths(componentName, componentPlatform);
if (paths) {
var examples = [];
let examples = [];
paths.map((p) => {
var platform = p.match(/Example\.(.*)\.js$/);
var title = '';
const platform = p.match(/Example\.(.*)\.js$/);
let title = '';
if ((componentPlatform === CROSS_SUFFIX) && (platform !== null)) {
title = platform[1].toUpperCase();
}
@ -128,7 +131,7 @@ function filterMethods(method) {
// Determines whether a component should have a link to a runnable example
function isRunnable(componentName, componentPlatform) {
var paths = getExamplePaths(componentName, componentPlatform);
const paths = getExamplePaths(componentName, componentPlatform);
if (paths && paths.length > 0) {
return true;
} else {
@ -206,30 +209,229 @@ function componentsToMarkdown(type, json, filepath, idx, styles) {
let componentCount;
function getTypedef(filepath, fileContent, json) {
let typedefDocgen;
try {
typedefDocgen = docgen.parse(
fileContent,
docgenHelpers.findExportedType,
[docgenHelpers.typedefHandler]
).map((type) => type.typedef);
} catch (e) {
// Ignore errors due to missing exported type definitions
if (e.message.indexOf(docgen.ERROR_MISSING_DEFINITION) !== -1) {
console.error('Cannot parse file', filepath, e);
}
}
if (!json) {
return typedefDocgen;
}
let typedef = typedefDocgen;
if (json.typedef && json.typedef.length !== 0) {
json.typedef.forEach(def => {
const typedefMatch = typedefDocgen.find(t => t.name === def.name);
if (typedefMatch) {
typedef.name = Object.assign(typedefMatch, def);
} else {
typedef.push(def);
}
});
}
return typedef;
}
function renderComponent(filepath) {
const fileContent = fs.readFileSync(filepath);
const json = docgen.parse(
fs.readFileSync(filepath),
fileContent,
docgenHelpers.findExportedOrFirst,
docgen.defaultHandlers.concat([
docgenHelpers.stylePropTypeHandler,
docgenHelpers.deprecatedPropTypeHandler,
docgenHelpers.jsDocFormatHandler,
])
);
json.typedef = getTypedef(filepath, fileContent);
return componentsToMarkdown('component', json, filepath, componentCount++, styleDocs);
}
function renderAPI(type) {
return function(filepath) {
let json;
try {
json = jsDocs(fs.readFileSync(filepath).toString());
} catch (e) {
console.error('Cannot parse file', filepath, e);
json = {};
}
return componentsToMarkdown(type, json, filepath, componentCount++);
function isJsDocFormat(fileContent) {
const reComment = /\/\*\*[\s\S]+?\*\//g;
const comments = fileContent.match(reComment);
if (!comments) {
return false;
}
return !!comments[0].match(/\s*\*\s+@jsdoc/);
}
function parseAPIJsDocFormat(filepath, fileContent) {
const fileName = path.basename(filepath);
const babelRC = {
'filename': fileName,
'sourceFileName': fileName,
'plugins': [
'transform-flow-strip-types',
'babel-plugin-syntax-trailing-function-commas',
]
};
// Babel transform
const code = babel.transform(fileContent, babelRC).code;
// Parse via jsdocs
let jsonParsed = jsdocApi.explainSync({
source: code,
configure: './jsdocs/jsdoc-conf.json'
});
// Cleanup jsdocs return
jsonParsed = jsonParsed.filter(i => {
return !i.undocumented && !/package|file/.test(i.kind);
});
jsonParsed = jsonParsed.map((identifier) => {
delete identifier.comment;
return identifier;
});
jsonParsed.forEach((identifier, index) => {
identifier.order = index;
});
// Group by "kind"
let json = {};
jsonParsed.forEach((identifier, index) => {
let kind = identifier.kind;
if (kind === 'function') {
kind = 'methods';
}
if (!json[kind]) {
json[kind] = [];
}
delete identifier.kind;
json[kind].push(identifier);
});
json.typedef = getTypedef(filepath, fileContent, json);
return json;
}
function parseAPIInferred(filepath, fileContent) {
let json;
try {
json = jsDocs(fileContent);
} catch (e) {
console.error('Cannot parse file', filepath, e);
json = {};
}
return json;
}
function getTypeName(type) {
let typeName;
switch (type.name) {
case 'signature':
typeName = type.type;
break;
case 'union':
typeName = type.value ?
type.value.map(getTypeName) :
type.elements.map(getTypeName);
break;
case 'enum':
if (typeof type.value === 'string') {
typeName = type.value;
} else {
typeName = 'enum';
}
break;
case '$Enum':
if (type.elements[0].signature.properties) {
typeName = type.elements[0].signature.properties.map(p => p.key);
}
break;
case 'arrayOf':
typeName = getTypeName(type.value);
break;
case 'instanceOf':
typeName = type.value;
break;
case 'func':
typeName = 'function';
break;
default:
typeName = type.alias ? type.alias : type.name;
break;
}
return typeName;
}
function getTypehintRec(typehint) {
if (typehint.type === 'simple') {
return typehint.value;
}
if (typehint.type === 'generic') {
return getTypehintRec(typehint.value[0]) +
'<' + getTypehintRec(typehint.value[1]) + '>';
}
return JSON.stringify(typehint);
}
function getTypehint(typehint) {
if (typeof typehint === 'object' && typehint.name) {
return getTypeName(typehint);
}
try {
var typehint = JSON.parse(typehint);
} catch (e) {
return typehint.split('|').map(type => type.trim());
}
return getTypehintRec(typehint);
}
function getJsDocFormatType(entities) {
let modEntities = entities;
if (entities) {
if (typeof entities === 'object' && entities.length) {
entities.map((entity, entityIndex) => {
if (entity.typehint) {
const typeNames = [].concat(getTypehint(entity.typehint));
modEntities[entityIndex].type = { names: typeNames };
delete modEntities[entityIndex].typehint;
}
if (entity.name) {
const regexOptionalType = /\?$/;
if (regexOptionalType.test(entity.name)) {
modEntities[entityIndex].optional = true;
modEntities[entityIndex].name =
entity.name.replace(regexOptionalType, '');
}
}
});
} else {
const typeNames = [].concat(getTypehint(entities));
return { type: { names : typeNames } };
}
}
return modEntities;
}
function renderAPI(filepath, type) {
const fileContent = fs.readFileSync(filepath).toString();
let json = parseAPIInferred(filepath, fileContent);
if (isJsDocFormat(fileContent)) {
let jsonJsDoc = parseAPIJsDocFormat(filepath, fileContent);
// Combine method info with jsdoc fomatted content
const methods = json.methods;
if (methods && methods.length) {
let modMethods = methods;
methods.map((method, methodIndex) => {
modMethods[methodIndex].params = getJsDocFormatType(method.params);
modMethods[methodIndex].returns =
getJsDocFormatType(method.returntypehint);
delete modMethods[methodIndex].returntypehint;
});
json.methods = modMethods;
// Use deep Object.assign so duplicate properties are overwritten.
deepAssign(jsonJsDoc.methods, json.methods);
}
json = jsonJsDoc;
}
return componentsToMarkdown(type, json, filepath, componentCount++);
}
function renderStyle(filepath) {
@ -355,8 +557,12 @@ module.exports = function() {
componentCount = 0;
return [].concat(
components.map(renderComponent),
apis.map(renderAPI('api')),
apis.map((filepath) => {
return renderAPI(filepath, 'api');
}),
stylesWithPermalink.map(renderStyle),
polyfills.map(renderAPI('Polyfill'))
polyfills.map((filepath) => {
return renderAPI(filepath, 'Polyfill');
})
);
};

View File

@ -1512,3 +1512,53 @@ input#algolia-doc-search:focus {
background-color: hsl(198, 100%, 96%);
color: #3B3738;
}
.params, .props
{
border-spacing: 0;
border: 0;
border-collapse: collapse;
}
.params .name, .props .name, .name code {
color: #4D4E53;
}
.params td, .params th, .props td, .props th
{
border: 1px solid #ddd;
margin: 0px;
text-align: left;
vertical-align: top;
padding: 4px 6px;
display: table-cell;
}
.params thead tr, .props thead tr
{
background-color: hsl(198, 75%, 88%);
font-weight: bold;
}
.params .params thead tr, .props .props thead tr
{
background-color: #fff;
font-weight: bold;
}
.params th, .props th { border-right: 1px solid #aaa; }
.params thead .last, .props thead .last { border-right: 1px solid #ddd; }
.params td.description > div > p:first-child,
.props td.description > div > p:first-child
{
margin-top: 0;
padding-top: 0;
}
.params td.description > p:last-child,
.props td.description > p:last-child
{
margin-bottom: 0;
padding-bottom: 0;
}