From 22f59a88a615d869f1b49f35e75e6e680fe9b3f1 Mon Sep 17 00:00:00 2001 From: Christine Abernathy Date: Tue, 21 Jun 2016 14:22:44 -0700 Subject: [PATCH] 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 --- Libraries/Components/StatusBar/StatusBar.js | 46 +++ Libraries/Utilities/AlertIOS.js | 146 +++++--- website/jsdocs/jsdoc-conf.json | 15 + website/jsdocs/jsdoc-plugin-values.js | 11 + website/layout/AutodocsLayout.js | 335 ++++++++++++++++-- website/package.json | 12 +- website/server/docgenHelpers.js | 141 +++++++- website/server/extractDocs.js | 248 +++++++++++-- website/src/react-native/css/react-native.css | 50 +++ 9 files changed, 906 insertions(+), 98 deletions(-) create mode 100644 website/jsdocs/jsdoc-conf.json create mode 100644 website/jsdocs/jsdoc-plugin-values.js diff --git a/Libraries/Components/StatusBar/StatusBar.js b/Libraries/Components/StatusBar/StatusBar.js index e49f7c930..8c0b65a0b 100644 --- a/Libraries/Components/StatusBar/StatusBar.js +++ b/Libraries/Components/StatusBar/StatusBar.js @@ -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'); diff --git a/Libraries/Utilities/AlertIOS.js b/Libraries/Utilities/AlertIOS.js index 1da237b0c..50a5accc4 100644 --- a/Libraries/Utilities/AlertIOS.js +++ b/Libraries/Utilities/AlertIOS.js @@ -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 Example with custom buttons * - * ``` * 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 Example with custom buttons * - * 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 Example with the default button and a custom callback + * * AlertIOS.prompt( * 'Update username', * null, * text => console.log("Your username is "+text), * null, * 'default' - * ) - * ``` + * ); */ static prompt( title: ?string, diff --git a/website/jsdocs/jsdoc-conf.json b/website/jsdocs/jsdoc-conf.json new file mode 100644 index 000000000..c755bca38 --- /dev/null +++ b/website/jsdocs/jsdoc-conf.json @@ -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 + } +} diff --git a/website/jsdocs/jsdoc-plugin-values.js b/website/jsdocs/jsdoc-plugin-values.js new file mode 100644 index 000000000..3d71c7f71 --- /dev/null +++ b/website/jsdocs/jsdoc-plugin-values.js @@ -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); + } + }); +}; diff --git a/website/layout/AutodocsLayout.js b/website/layout/AutodocsLayout.js index f990bc5fe..6353bbfad 100644 --- a/website/layout/AutodocsLayout.js +++ b/website/layout/AutodocsLayout.js @@ -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 {typeName}; +} + +function renderTypeWithLinks(type, docTitle, namedTypes) { + if (!type || !type.names) { + return null; + } + + const docPath = docTitle ? 'docs/' + docTitle.toLowerCase() + '.html' : 'docs/'; + return ( +
+ { + type.names.map((typeName, index, array) => { + let separator = index < array.length - 1 && ' | '; + return ( + + {renderTypeNameLink(typeName, docPath, namedTypes)} + {separator} + + ); + }) + } +
+ ); +} + 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 ( ); }, - renderMethods: function(methods) { + renderMethods: function(methods, namedTypes) { if (!methods || !methods.length) { return null; } @@ -277,7 +331,38 @@ var ComponentDoc = React.createClass({
{methods.filter((method) => { return method.name[0] !== '_'; - }).map(this.renderMethod)} + }).map(method => this.renderMethod(method, namedTypes))} +
+ + ); + }, + + renderTypeDef: function(typedef, namedTypes) { + return ( + + ); + }, + + renderTypeDefs: function(typedefs, namedTypes) { + if (!typedefs || !typedefs.length) { + return null; + } + return ( + + Type Definitions +
+ {typedefs.map((typedef) => { + return this.renderTypeDef(typedef, namedTypes); + })}
); @@ -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 (
@@ -293,7 +379,8 @@ var ComponentDoc = React.createClass({ Props {this.renderProps(content.props, content.composes)} - {this.renderMethods(content.methods)} + {this.renderMethods(content.methods, namedTypes)} + {this.renderTypeDefs(content.typedef, namedTypes)}
); } @@ -301,19 +388,22 @@ var ComponentDoc = React.createClass({ var APIDoc = React.createClass({ - renderMethod: function(method) { + renderMethod: function(method, namedTypes) { return ( ); }, - renderMethods: function(methods) { + renderMethods: function(methods, namedTypes) { if (!methods.length) { return null; } @@ -323,7 +413,7 @@ var APIDoc = React.createClass({
{methods.filter((method) => { return method.name[0] !== '_'; - }).map(this.renderMethod)} + }).map(method => this.renderMethod(method, namedTypes))}
); @@ -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 && {removeCommentsFromDocblock(cls.docblock)} } - {this.renderMethods(cls.methods)} + {this.renderMethods(cls.methods, namedTypes)} {this.renderProperties(cls.properties)} @@ -393,6 +483,55 @@ var APIDoc = React.createClass({ ); }, + renderTypeDef: function(typedef, namedTypes) { + return ( + + ); + }, + + renderTypeDefs: function(typedefs, namedTypes) { + if (!typedefs || !typedefs.length) { + return null; + } + return ( + + Type Definitions +
+ {typedefs.map((typedef) => { + return this.renderTypeDef(typedef, namedTypes); + })} +
+
+ ); + }, + + renderMainDescription: function(content) { + if (content.docblock) { + return ( + + {removeCommentsFromDocblock(content.docblock)} + + ); + } + if (content.class && content.class.length && content.class[0].description) { + return ( + + {content.class[0].description} + + ); + } + 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 (
- - {removeCommentsFromDocblock(content.docblock)} - - {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)}
); } @@ -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>/ig; + const result = re.exec(example); + const caption = result ? result[1] + ':' : 'Example:'; + const code = example.replace(/.*?<\/caption>/ig, '') + .replace(/^\n\n/, ''); + return ( +
+
+ {caption} + + {code} + +
+ ); + }); + }, + + 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 ( +
+ Parameters: + + + + + + + + + {params.map((param) => { + return ( + + + + + ); + })} + +
Name and TypeDescription
+ {param.optional ? '[' + param.name + ']' : param.name} +

+ {renderTypeWithLinks(param.type, this.props.apiName, this.props.namedTypes)} +
{param.description}
+
+ ); + }, + render: function() { return (
- {this.props.modifiers.length && + {this.props.modifiers && this.props.modifiers.length && {this.props.modifiers.join(' ') + ' '} || ''} {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 && {this.props.description} } + {this.renderMethodParameters(this.props.params)} + {this.renderMethodExamples(this.props.examples)} +
+ ); + }, +}); + +var TypeDef = React.createClass({ + renderProperties: function(properties) { + if (!properties || !properties.length) { + return null; + } + if (!properties[0].type || !properties[0].type.names) { + return null; + } + return ( +
+
+ Properties: + + + + + + + + + {properties.map((property) => { + return ( + + + + + ); + })} + +
Name and TypeDescription
+ {property.optional ? '[' + property.name + ']' : property.name} +

+ {renderTypeWithLinks(property.type, this.props.apiName, this.props.namedTypes)} +
{property.description}
+
+ ); + }, + + renderValues: function(values) { + if (!values || !values.length) { + return null; + } + if (!values[0].type || !values[0].type.names) { + return null; + } + return ( +
+
+ Constants: + + + + + + + + + {values.map((value) => { + return ( + + + + + ); + })} + +
ValueDescription
+ {value.name} + {value.description}
+
+ ); + }, + + render: function() { + return ( +
+
+ {this.props.name} +
+ {this.props.description && + {this.props.description} + } + Type: +
+ {this.props.type.names.join(' | ')} + {this.renderProperties(this.props.properties)} + {this.renderValues(this.props.values)}
); }, diff --git a/website/package.json b/website/package.json index 270037f3c..985b9bcf4 100644 --- a/website/package.json +++ b/website/package.json @@ -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" } } diff --git a/website/server/docgenHelpers.js b/website/server/docgenHelpers.js index 36873fdee..e648ef660 100644 --- a/website/server/docgenHelpers.js +++ b/website/server/docgenHelpers.js @@ -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; diff --git a/website/server/extractDocs.js b/website/server/extractDocs.js index e16dad530..e9cdc2055 100644 --- a/website/server/extractDocs.js +++ b/website/server/extractDocs.js @@ -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'); + }) ); }; diff --git a/website/src/react-native/css/react-native.css b/website/src/react-native/css/react-native.css index d51ddd575..2e2bd9ee1 100644 --- a/website/src/react-native/css/react-native.css +++ b/website/src/react-native/css/react-native.css @@ -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; +}