diff --git a/sync-api-docs/extractDocsFromRN.js b/sync-api-docs/extractDocsFromRN.js index f55543ba44e..6a1b7e20baf 100644 --- a/sync-api-docs/extractDocsFromRN.js +++ b/sync-api-docs/extractDocsFromRN.js @@ -41,16 +41,20 @@ async function extractDocsFromRN(rnRoot) { const result = reactDocs.parse( contents, - reactDocs.resolver.findExportedComponentDefinition, + reactDocs.resolver.findAllComponentDefinitions, reactDocs.defaultHandlers.filter( handler => handler !== reactDocs.handlers.propTypeCompositionHandler ), {filename: file} ); + const filteredResult = result.filter(item => { + if (item.description) return item; + }); + docs.push({ file, - component: cleanComponentResult(result), + component: cleanComponentResult(...filteredResult), }); } diff --git a/sync-api-docs/generateMarkdown.js b/sync-api-docs/generateMarkdown.js index e62948e27bf..8a572582519 100644 --- a/sync-api-docs/generateMarkdown.js +++ b/sync-api-docs/generateMarkdown.js @@ -5,13 +5,20 @@ * LICENSE file in the root directory of this source tree. */ const tokenizeComment = require('tokenize-comment'); +const {formatTypeColumn, formatDefaultColumn} = require('./propFormatter'); +const { + formatMethodType, + formatMethodName, + formatMethodDescription, +} = require('./methodFormatter'); + const { formatMultiplePlatform, + stringToInlineCodeForTable, maybeLinkifyType, maybeLinkifyTypeName, - formatTypeColumn, - formatDefaultColumn, -} = require('./propFormatter'); + formatType, +} = require('./utils'); // Formats an array of rows as a Markdown table function generateTable(rows) { @@ -72,24 +79,66 @@ function generateProp(propName, prop) { // Formats information about a prop function generateMethod(method, component) { - const infoTable = generateTable([ - { - ...(method.rnTags && method.rnTags.platform - ? {Platform: formatPlatformName(method.rnTags.platform)} - : {}), - }, - ]); + let descriptionTokenized = ''; + let header = 'Valid `params` keys are:'; + let mdPoints = ''; + if (method?.params[0]?.type?.raw) { + let desc = method?.params[0]?.type?.raw; + let len = method?.params[0]?.type?.signature?.properties?.length; + descriptionTokenized = tokenizeComment(desc); + + if ( + descriptionTokenized?.examples && + descriptionTokenized?.examples.length === len + ) { + let obj = []; + for (let i = 0; i < len; i++) { + let newObj = method?.params[0]?.type?.signature?.properties[i]; + newObj['description'] = descriptionTokenized?.examples[i]?.value; + obj.push(newObj); + } + + obj.map(item => { + if (item.description.trim() !== 'missing') + mdPoints += `- '${item.key}' (${item.value.name}) - ${ + item.description + }`; + else mdPoints += `- '${item.key}' (${item.value.name})`; + }); + } + } + + if (method?.docblock) { + let dblock = method.docblock + .split('\n') + .map(line => { + return line.replace(/ /, ''); + }) + .join('\n'); + const docblockTokenized = tokenizeComment(dblock); + dblock = dblock.replace(/@platform .*/g, ''); + method.rnTags = {}; + const platformTag = docblockTokenized.tags.find( + ({key}) => key === 'platform' + ); + + if (platformTag) { + method.rnTags.platform = platformTag.value.split(','); + } + } return ( '### `' + method.name + '()`' + + (method.rnTags && method.rnTags.platform + ? formatMultiplePlatform(method.rnTags.platform) + : '') + '\n' + '\n' + - generateMethodSignatureBlock(method, component) + (method.description ? method.description + '\n\n' : '') + generateMethodSignatureTable(method, component) + - infoTable + (mdPoints && header + '\n' + mdPoints) ).trim(); } @@ -118,15 +167,20 @@ function generateMethodSignatureTable(method, component) { if (!method.params.length) { return ''; } + return ( '**Parameters:**\n\n' + generateTable( - method.params.map(param => ({ - Name: param.name, - Type: param.type ? maybeLinkifyType(param.type) : '', - Required: param.optional ? 'No' : 'Yes', - Description: param.description, - })) + method.params.map(param => { + return { + Name: formatMethodName(param), + Type: formatMethodType(param), + Required: param.optional ? 'No' : 'Yes', + ...(param.description && { + Description: formatMethodDescription(param), + }), + }; + }) ) ); } @@ -229,26 +283,35 @@ function preprocessDescription(desc) { }); if (tabs === 2) { - const wrapper = `${playgroundTab}\n\n${functionalBlock}\n\n${ - descriptionTokenized.examples[0].raw - }\n\n${classBlock}\n\n${ - descriptionTokenized.examples[1].raw - }\n\n${endBlock}`; - return ( - descriptionTokenized.description + - `\n## Example\n` + - wrapper + - '\n' + - descriptionTokenized?.footer + const firstExample = desc.substr(desc.search('```SnackPlayer') + 1); + const secondExample = firstExample.substr( + firstExample.search('```SnackPlayer') + 1 ); - } else { + return ( desc.substr(0, desc.search('```SnackPlayer')) + - '\n' + - '\n## Example\n' + - '\n' + - desc.substr(desc.search('```SnackPlayer')) + `\n## Example\n` + + `${playgroundTab}\n\n${functionalBlock}\n\n${'`' + + firstExample.substr( + 0, + firstExample.search('```') + 3 + )}\n\n${classBlock}\n\n${'`' + + secondExample.substr( + 0, + secondExample.search('```') + 3 + )}\n\n${endBlock}` + + secondExample.substr(secondExample.search('```') + 3) ); + } else { + if (desc.search('```SnackPlayer') !== -1) { + return ( + desc.substr(0, desc.search('```SnackPlayer')) + + '\n' + + '\n## Example\n' + + '\n' + + desc.substr(desc.search('```SnackPlayer')) + ); + } else return desc; } } diff --git a/sync-api-docs/magic.js b/sync-api-docs/magic.js index e40137628e1..8a9fec95dc2 100644 --- a/sync-api-docs/magic.js +++ b/sync-api-docs/magic.js @@ -29,5 +29,33 @@ module.exports = { text: 'RefreshControl.SIZE', url: 'refreshcontrol.md#refreshlayoutconstssize', }, + StatusBarAnimation: { + text: 'StatusBarAnimation', + url: 'statusbar#statusbaranimation', + }, + StatusBarStyle: { + text: 'StatusBarStyle', + url: 'statusbar#statusbarstyle', + }, + ReactNode: { + text: 'React.Node', + url: 'react-node.md', + }, + TextStyleProps: { + text: 'Text Style Props', + url: 'text-style-props', + }, + SectionT: { + text: 'Section', + url: 'sectionlist#section', + }, + ViewStyleProps: { + text: 'View Style Props', + url: 'view-style-props', + }, + Text: { + text: 'Text', + url: 'text#style', + }, }, }; diff --git a/sync-api-docs/methodFormatter.js b/sync-api-docs/methodFormatter.js new file mode 100644 index 00000000000..54c62d9db41 --- /dev/null +++ b/sync-api-docs/methodFormatter.js @@ -0,0 +1,63 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +'use strict'; + +const {typeOf} = require('tokenize-comment/lib/utils'); +const magic = require('./magic'); +const {formatMultiplePlatform} = require('./utils'); + +function formatMethodType(param) { + let text, url; + if (param?.type?.name === 'union') { + if (param?.type?.alias) { + const {alias} = param.type; + if (Object.hasOwnProperty.call(magic.linkableTypeAliases, alias)) { + ({url, text} = magic.linkableTypeAliases[alias]); + } + if (url) return `[${text}](${url})`; + else return param.type.alias; + } + return param.type.name; + } else { + if (param?.type?.type) return param.type.type; + else return param.type.name; + } +} + +function formatMethodName(param) { + let tag = param.description; + if (tag) { + const isMatch = tag.match(/{@platform [a-z ,]*}/); + if (isMatch) { + const platform = isMatch[0].match(/ [a-z ,]*/); + tag = tag.replace(/{@platform [a-z ,]*}/g, ''); + tag = formatMultiplePlatform(platform[0].split(',')); + return param.name + tag; + } + } + return param.name; +} + +function formatMethodDescription(param) { + let tag = param.description; + const isMatch = tag.match(/{@platform [a-z ,]*}/); + if (isMatch) { + const platform = isMatch[0].match(/ [a-z ,]*/); + + // Replaces @platform strings with empty string + // and appends type with formatted platform + tag = tag.replace(/{@platform [a-z ,]*}/g, ''); + } + return tag; +} + +module.exports = { + formatMethodType, + formatMethodName, + formatMethodDescription, +}; diff --git a/sync-api-docs/propFormatter.js b/sync-api-docs/propFormatter.js index 42de69ce4e5..407e091b320 100644 --- a/sync-api-docs/propFormatter.js +++ b/sync-api-docs/propFormatter.js @@ -8,78 +8,15 @@ 'use strict'; const {typeOf} = require('tokenize-comment/lib/utils'); -const he = require('he'); const magic = require('./magic'); +const { + formatMultiplePlatform, + stringToInlineCodeForTable, + maybeLinkifyType, + maybeLinkifyTypeName, + formatType, +} = require('./utils'); -// Adds multiple platform tags for prop name -function formatMultiplePlatform(platforms) { - let platformString = ''; - platforms.forEach(platform => { - switch (platform.trim()) { - case 'ios': - platformString += '
if necessary.
-function stringToInlineCodeForTable(str) {
- let useHtml = /[`|]/.test(str);
- str = str.replace(/\n/g, ' ');
- if (useHtml) {
- return '' + he.encode(str).replace(/\|/g, '|') + '';
- }
- return '`' + str + '`';
-}
-
-function maybeLinkifyType(flowType) {
- let url, text;
- flowType.elements?.forEach(elem => {
- if (Object.hasOwnProperty.call(magic.linkableTypeAliases, elem.name)) {
- ({url, text} = magic.linkableTypeAliases[elem.name]);
- }
- });
- if (!text) {
- text = stringToInlineCodeForTable(
- flowType.raw || formatType(flowType.name)
- );
- }
- if (url) {
- return `[${text}](${url})`;
- }
- return text;
-}
-
-function formatType(name) {
- if (name.toLowerCase() === 'boolean') return 'bool';
- if (name.toLowerCase() === 'stringish') return 'string';
- if (name === '$ReadOnlyArray') return 'array';
- return name;
-}
-
-function maybeLinkifyTypeName(name) {
- let url, text;
- if (Object.hasOwnProperty.call(magic.linkableTypeAliases, name)) {
- ({url, text} = magic.linkableTypeAliases[name]);
- }
- if (!text) {
- text = stringToInlineCodeForTable(name);
- }
- if (url) {
- return `[${text}](${url})`;
- }
- return text;
-}
-
-// Adds proper markdown formatting to component's prop type.
function formatTypeColumn(prop) {
// Checks for @type pragma comment
if (prop.rnTags && prop.rnTags.type) {
@@ -87,6 +24,7 @@ function formatTypeColumn(prop) {
const typeTags = prop.rnTags.type;
typeTags.forEach(tag => {
+ let url, text;
// Checks for @platform pragma in @type string
const isMatch = tag.match(/{@platform [a-z ,]*}/);
if (isMatch) {
@@ -96,7 +34,30 @@ function formatTypeColumn(prop) {
// Replaces @platform strings with empty string
// and appends type with formatted platform
tag = tag.replace(/{@platform [a-z ,]*}/g, '');
+ if (Object.hasOwnProperty.call(magic.linkableTypeAliases, tag)) {
+ ({url, text} = magic.linkableTypeAliases[tag]);
+ if (url) tag = `[${text}](${url})`;
+ }
tag = tag + formatMultiplePlatform(platform[0].split(','));
+ } else {
+ // Check if there are multiple comma separated types in a single line
+ if (tag.match(/, /)) {
+ let newTag = '';
+ const tags = tag.split(', ');
+ tags.forEach(item => {
+ if (Object.hasOwnProperty.call(magic.linkableTypeAliases, item)) {
+ ({url, text} = magic.linkableTypeAliases[item]);
+ if (url) newTag += ', ' + `[${text}](${url})`;
+ } else newTag += ', ' + item;
+ });
+ //Trim comma from beginning
+ tag = newTag.replace(/^, /, '');
+ }
+ // If there is no comma separated types in rnTags
+ else if (Object.hasOwnProperty.call(magic.linkableTypeAliases, tag)) {
+ ({url, text} = magic.linkableTypeAliases[tag]);
+ if (url) tag = `[${text}](${url})`;
+ }
}
tableRows = tableRows + tag + '
';
});
@@ -122,13 +83,17 @@ function formatTypeColumn(prop) {
Object.hasOwnProperty.call(magic.linkableTypeAliases, eventType)
) {
({url, text} = magic.linkableTypeAliases[eventType]);
- return `${prop.flowType.type}([${text}](${url}))`;
+ if (url) {
+ return `${prop.flowType.type}([${text}](${url}))`;
+ }
}
// TODO: Handling unknown function params
return `${prop.flowType.type}`;
} else {
return prop.flowType.type;
}
+ } else if (prop.flowType.type === 'object') {
+ return prop.flowType.type;
}
} else if (prop.flowType.name.includes('$ReadOnlyArray')) {
prop?.flowType?.elements[0]?.elements &&
@@ -140,6 +105,28 @@ function formatTypeColumn(prop) {
}
});
if (url) return `array of [${text}](${url})`;
+ else if (prop?.flowType?.elements[0].name === 'union') {
+ const unionTypes = prop?.flowType?.elements[0]?.elements.reduce(
+ (acc, curr) => {
+ acc.push(curr.value);
+ return acc;
+ },
+ []
+ );
+ return `array of enum(${unionTypes.join(', ')})`;
+ } else if (prop?.flowType?.elements[0]?.name) {
+ const typeName = prop.flowType.elements[0].name;
+ //array of number
+ if (typeName === 'number') return `array of ${typeName}`;
+ else if (
+ Object.hasOwnProperty.call(magic.linkableTypeAliases, typeName)
+ ) {
+ ({url, text} = magic.linkableTypeAliases[typeName]);
+ if (url) return `array of [${text}](${url})`;
+ }
+ //default array for all other types
+ else return 'array';
+ }
} else if (prop.flowType.name === '$ReadOnly') {
// Special Case: switch#trackcolor
let markdown = '';
@@ -154,11 +141,36 @@ function formatTypeColumn(prop) {
markdown += `${key}: [${text}](${url})` + ', ';
}
});
+ if (!url) markdown += `${key}: ${value.name}` + ', ';
}
);
if (markdown.match(/, $/)) markdown = markdown.replace(/, $/, '');
return `${prop.flowType.elements[0]?.type}: {${markdown}}`;
}
+ } else if (prop.flowType.name === 'union') {
+ let unionTypes = prop.flowType.raw.split('|');
+
+ // Trim whitespaces and remove any leftover `|` (to avoid table split)
+ unionTypes = unionTypes
+ .map(elem => {
+ return elem.trim().replace(/|/g, '');
+ })
+ .filter(item => {
+ if (item) return item;
+ });
+
+ // Get text and url from magic aliases
+ prop?.flowType?.elements?.forEach(elem => {
+ if (Object.hasOwnProperty.call(magic.linkableTypeAliases, elem.name)) {
+ ({url, text} = magic.linkableTypeAliases[elem.name]);
+ }
+ });
+
+ if (url) return `[${text}](${url})`;
+
+ return `enum(${unionTypes.join(', ')})`;
+ } else if (prop.flowType.name === 'ReactElement') {
+ return 'element';
} else {
// Get text and url from magic aliases
prop?.flowType?.elements?.forEach(elem => {
@@ -202,18 +214,16 @@ function formatDefaultColumn(prop) {
prop?.flowType?.elements.some(elem => {
if (elem.name === 'NativeColorValue' && !tag.includes('null')) {
colorBlock =
- '';
+ ``;
return true;
}
});
tag =
- (!tag.includes('null') ? '`' + tag + '`' : tag) +
colorBlock +
+ (!tag.includes('null') ? '`' + tag + '`' : tag) +
formatMultiplePlatform(platform[0].split(','));
- } else {
+ } else if (!tag.includes('`')) {
tag = '`' + tag + '`';
}
tableRows = tableRows + tag + '
';
@@ -229,9 +239,6 @@ function formatDefaultColumn(prop) {
}
module.exports = {
- formatMultiplePlatform,
- maybeLinkifyType,
- maybeLinkifyTypeName,
formatTypeColumn,
formatDefaultColumn,
};
diff --git a/sync-api-docs/sync-api-docs.js b/sync-api-docs/sync-api-docs.js
index ec3a4dae50c..e5d6db8e8f3 100644
--- a/sync-api-docs/sync-api-docs.js
+++ b/sync-api-docs/sync-api-docs.js
@@ -17,9 +17,9 @@ const path = require('path');
const extractDocsFromRN = require('./extractDocsFromRN');
const preprocessGeneratedApiDocs = require('./preprocessGeneratedApiDocs');
const generateMarkdown = require('./generateMarkdown');
-const titleToId = require('./titleToId');
+const {titleToId} = require('./utils');
-const DOCS_ROOT_DIR = path.resolve(__dirname, '..', '..', '..', 'docs');
+const DOCS_ROOT_DIR = path.resolve(__dirname, '..', 'docs');
async function generateApiDocs(rnPath) {
const apiDocs = await extractDocsFromRN(rnPath);
diff --git a/sync-api-docs/utils.js b/sync-api-docs/utils.js
new file mode 100644
index 00000000000..ea4433a7873
--- /dev/null
+++ b/sync-api-docs/utils.js
@@ -0,0 +1,95 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+'use strict';
+const he = require('he');
+const magic = require('./magic');
+
+// Adds multiple platform tags for prop name
+function formatMultiplePlatform(platforms) {
+ let platformString = '';
+ platforms.forEach(platform => {
+ switch (platform.trim().toLowerCase()) {
+ case 'ios':
+ platformString += '' + 'iOS' + ' ';
+ break;
+ case 'android':
+ platformString += '' + 'Android' + '';
+ break;
+ case 'tv':
+ platformString += '' + 'TV' + '';
+ break;
+ //TODO: Add a new CSS class for VR
+ case 'vr':
+ platformString += '' + 'VR' + '';
+ }
+ });
+ return platformString;
+}
+
+// Wraps a string in an inline code block in a way that is safe to include in a
+// table cell, by wrapping it as HTML if necessary.
+function stringToInlineCodeForTable(str) {
+ let useHtml = /[`|]/.test(str);
+ str = str.replace(/\n/g, ' ');
+ if (useHtml) {
+ return '' + he.encode(str).replace(/\|/g, '|') + '';
+ }
+ return '`' + str + '`';
+}
+
+function maybeLinkifyType(flowType) {
+ let url, text;
+ flowType.elements?.forEach(elem => {
+ if (Object.hasOwnProperty.call(magic.linkableTypeAliases, elem.name)) {
+ ({url, text} = magic.linkableTypeAliases[elem.name]);
+ }
+ });
+ if (!text) {
+ text = stringToInlineCodeForTable(
+ flowType.raw || formatType(flowType.name)
+ );
+ }
+ if (url) {
+ return `[${text}](${url})`;
+ }
+ return text;
+}
+
+function formatType(name) {
+ if (name.toLowerCase() === 'boolean') return 'bool';
+ if (name.toLowerCase() === 'stringish') return 'string';
+ if (name === '$ReadOnlyArray') return 'array';
+ return name;
+}
+
+function maybeLinkifyTypeName(name) {
+ let url, text;
+ if (Object.hasOwnProperty.call(magic.linkableTypeAliases, name)) {
+ ({url, text} = magic.linkableTypeAliases[name]);
+ }
+ if (!text) {
+ text = stringToInlineCodeForTable(name);
+ }
+ if (url) {
+ return `[${text}](${url})`;
+ }
+ return text;
+}
+
+function titleToId(title) {
+ return title.toLowerCase().replace(/[^a-z]+/g, '-');
+}
+
+module.exports = {
+ formatMultiplePlatform,
+ stringToInlineCodeForTable,
+ maybeLinkifyType,
+ maybeLinkifyTypeName,
+ formatType,
+ titleToId,
+};