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 += '
' + 'iOS' + '
'; - break; - case 'android': - platformString += '
' + 'Android' + '
'; - break; - case 'tv': - platformString += '
' + 'TV' + '
'; - } - }); - 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; -} - -// 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, +};