sync-api-docs/generateMarkdown.js (282 lines of code) (raw):
/**
* 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.
*/
const tokenizeComment = require('tokenize-comment');
const {formatTypeColumn, formatDefaultColumn} = require('./propFormatter');
const {
formatMethodType,
formatMethodName,
formatMethodDescription,
} = require('./methodFormatter');
const {
formatMultiplePlatform,
stringToInlineCodeForTable,
maybeLinkifyType,
maybeLinkifyTypeName,
formatType,
} = require('./utils');
// Formats an array of rows as a Markdown table
function generateTable(rows) {
const colWidths = new Map();
for (const row of rows) {
for (const col of Object.keys(row)) {
colWidths.set(
col,
Math.max(colWidths.get(col) || col.length, String(row[col]).length)
);
}
}
if (!colWidths.size) {
return '';
}
let header = '|',
divider = '|';
for (const [col, width] of colWidths) {
header += ' ' + col.padEnd(width + 1) + '|';
divider += ' ' + '-'.repeat(width) + ' ' + '|';
}
let result = header + '\n' + divider + '\n';
for (const row of rows) {
result += '|';
for (const [col, width] of colWidths) {
result += ' ' + String(row[col] || '').padEnd(width + 1) + '|';
}
result += '\n';
}
return result;
}
// Formats information about a prop
function generateProp(propName, prop) {
const infoTable = generateTable([
{
Type: formatTypeColumn(prop),
...formatDefaultColumn(prop),
},
]);
return (
'### ' +
(prop.required ? '<div class="label required basic">Required</div>' : '') +
'`' +
propName +
'`' +
(prop.rnTags && prop.rnTags.platform
? formatMultiplePlatform(prop.rnTags.platform)
: '') +
'\n' +
'\n' +
(prop.description ? prop.description + '\n\n' : '') +
infoTable
);
}
// Formats information about a prop
function generateMethod(method, component) {
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' +
(method.description ? method.description + '\n\n' : '') +
generateMethodSignatureTable(method, component) +
(mdPoints && header + '\n' + mdPoints)
).trim();
}
function lowerFirst(s) {
return s[0].toLowerCase() + s.slice(1);
}
function generateMethodSignatureBlock(method, component) {
return (
'```jsx\n' +
(method.modifiers.includes('static')
? component.displayName + '.'
: lowerFirst(component.displayName + '.')) +
method.name +
'(' +
method.params
.map(param => (param.optional ? `[${param.name}]` : param.name))
.join(', ') +
');' +
'\n' +
'```\n\n'
);
}
function generateMethodSignatureTable(method, component) {
if (!method.params.length) {
return '';
}
return (
'**Parameters:**\n\n' +
generateTable(
method.params.map(param => {
return {
Name: formatMethodName(param),
Type: formatMethodType(param),
Required: param.optional ? 'No' : 'Yes',
...(param.description && {
Description: formatMethodDescription(param),
}),
};
})
)
);
}
// Formats information about props
function generateProps({props, composes}) {
if (!props || !Object.keys(props).length) {
return '';
}
return (
'## Props' +
'\n' +
'\n' +
(composes && composes.length
? composes
.map(parent => 'Inherits ' + maybeLinkifyTypeName(parent) + '.')
.join('\n\n') + '\n\n'
: '') +
Object.keys(props)
.sort((a, b) => a.localeCompare(b))
.sort((a, b) => props[b].required - props[a].required)
.map(function (propName) {
return generateProp(propName, props[propName]);
})
.join('\n\n---\n\n')
);
}
function generateMethods(component) {
const {methods} = component;
if (!methods || !methods.length) {
return '';
}
return (
'## Methods' +
'\n' +
'\n' +
[...methods]
.sort((a, b) =>
a.name.localeCompare(
b.name /* TODO @nocommit what's a neutral locale */
)
)
.map(function (method) {
return generateMethod(method, component);
})
.join('\n\n---\n\n')
);
}
// Generates a Docusaurus header for a component page
function generateHeader({id, title}) {
return (
'---' + '\n' + 'id: ' + id + '\n' + 'title: ' + title + '\n' + '---' + '\n'
);
}
// Function to process example contained description
function preprocessDescription(desc) {
// Playground tabs for the class and functional components
const playgroundTab = `<div class="toggler">
<ul role="tablist" class="toggle-syntax">
<li id="functional" class="button-functional" aria-selected="false" role="tab" tabindex="0" aria-controls="functionaltab" onclick="displayTabs('syntax', 'functional')">
Function Component Example
</li>
<li id="classical" class="button-classical" aria-selected="false" role="tab" tabindex="0" aria-controls="classicaltab" onclick="displayTabs('syntax', 'classical')">
Class Component Example
</li>
</ul>
</div>`;
//Blocks for different syntax sections
const functionalBlock = `<block class='functional syntax' />`;
const classBlock = `<block class='classical syntax' />`;
const endBlock = `<block class='endBlock syntax' />`;
desc = desc
.split('\n')
.map(line => {
return line.replace(/ /, '');
})
.join('\n');
const descriptionTokenized = tokenizeComment(desc);
// Tabs counter for examples
let tabs = 0;
descriptionTokenized.examples.map(item => {
const matchSnackPlayer = item.language.match(/(SnackPlayer name=).*/g);
if (matchSnackPlayer) {
const matchClassComp = matchSnackPlayer[0].match(
/Class%20Component%20Example/
);
const matchFuncComp = matchSnackPlayer[0].match(
/Function%20Component%20Example/
);
if (matchClassComp || matchFuncComp) tabs++;
}
});
if (tabs === 2) {
const firstExample = desc.slice(desc.search('```SnackPlayer') + 1);
const secondExample = firstExample.slice(
firstExample.search('```SnackPlayer') + 1
);
return (
desc.substring(0, desc.search('```SnackPlayer')) +
`\n## Example\n` +
`${playgroundTab}\n\n${functionalBlock}\n\n${
'`' + firstExample.slice(0, firstExample.search('```') + 3)
}\n\n${classBlock}\n\n${
'`' + secondExample.slice(0, secondExample.search('```') + 3)
}\n\n${endBlock}` +
secondExample.slice(secondExample.search('```') + 3)
);
} else {
if (desc.search('```SnackPlayer') !== -1) {
return (
desc.slice(0, desc.search('```SnackPlayer')) +
'\n' +
'\n## Example\n' +
'\n' +
desc.slice(desc.search('```SnackPlayer'))
);
} else return desc;
}
}
function generateMarkdown({id, title}, component) {
const markdownString =
generateHeader({id, title}) +
'\n' +
preprocessDescription(component.description) +
'\n\n' +
'---\n\n' +
'# Reference\n\n' +
generateProps(component) +
generateMethods(component);
return markdownString.replace(/\n{3,}/g, '\n\n');
}
module.exports = generateMarkdown;