diff options
| author | ST-DDT <[email protected]> | 2022-03-01 19:55:53 +0100 |
|---|---|---|
| committer | GitHub <[email protected]> | 2022-03-01 19:55:53 +0100 |
| commit | 61fafe47547873fcde74e98bff2a70738df7d4f2 (patch) | |
| tree | 379432fee1ba309ba1a004bd37af04bba4628a79 /scripts/apidoc | |
| parent | 442812f66a5ad9a42e389ad1ae06dbbcddad4e99 (diff) | |
| download | faker-61fafe47547873fcde74e98bff2a70738df7d4f2.tar.xz faker-61fafe47547873fcde74e98bff2a70738df7d4f2.zip | |
docs: generate docs for fake() and unique() (#564)
Diffstat (limited to 'scripts/apidoc')
| -rw-r--r-- | scripts/apidoc/apiDocsWriter.ts | 135 | ||||
| -rw-r--r-- | scripts/apidoc/directMethods.ts | 56 | ||||
| -rw-r--r-- | scripts/apidoc/moduleMethods.ts | 67 | ||||
| -rw-r--r-- | scripts/apidoc/signature.ts | 166 | ||||
| -rw-r--r-- | scripts/apidoc/utils.ts | 8 |
5 files changed, 432 insertions, 0 deletions
diff --git a/scripts/apidoc/apiDocsWriter.ts b/scripts/apidoc/apiDocsWriter.ts new file mode 100644 index 00000000..e2b42816 --- /dev/null +++ b/scripts/apidoc/apiDocsWriter.ts @@ -0,0 +1,135 @@ +import { writeFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import type { Options } from 'prettier'; +import { format } from 'prettier'; +import prettierConfig from '../../.prettierrc.cjs'; +import type { Method } from '../../docs/.vitepress/components/api-docs/method'; +import type { PageIndex } from './utils'; +import { pathDocsDir, pathOutputDir } from './utils'; + +const pathDocsApiPages = resolve(pathDocsDir, '.vitepress', 'api-pages.mjs'); + +const scriptCommand = 'pnpm run generate:api-docs'; + +const prettierMarkdown: Options = { + ...prettierConfig, + parser: 'markdown', +}; + +const prettierTypescript: Options = { + ...prettierConfig, + parser: 'typescript', +}; + +const prettierBabel: Options = { + ...prettierConfig, + parser: 'babel', +}; + +/** + * Writes the api page for the given module to the correct location. + * + * @param moduleName The name of the module to write the docs for. + * @param lowerModuleName The lowercase name of the module. + * @param comment The module comments. + */ +export function writeApiDocsModulePage( + moduleName: string, + lowerModuleName: string, + comment: string +): void { + // Write api docs page + let content = ` + <script setup> + import ApiDocsMethod from '../.vitepress/components/api-docs/method.vue' + import { ${lowerModuleName} } from './${lowerModuleName}' + import { ref } from 'vue'; + + const methods = ref(${lowerModuleName}); + </script> + + # ${moduleName} + + <!-- This file is automatically generated. --> + <!-- Run '${scriptCommand}' to update --> + + ::: v-pre + + ${comment} + + ::: + + <ApiDocsMethod v-for="method of methods" :key="method.name" :method="method" v-once /> + `.replace(/\n +/g, '\n'); + + content = format(content, prettierMarkdown); + + writeFileSync(resolve(pathOutputDir, lowerModuleName + '.md'), content); +} + +/** + * Writes the api page for the given method to the correct location. + * + * @param methodName The name of the method to write the docs for. + */ +export function writeApiDocsDirectPage(methodName: string): void { + let content = ` + <script setup> + import ApiDocsMethod from '../.vitepress/components/api-docs/method.vue' + import { ${methodName} } from './${methodName}' + import { ref } from 'vue'; + + const methods = ref(${methodName}); + </script> + + <ApiDocsMethod v-for="method of methods" :key="method.name" :method="method" v-once /> + `.replace(/\n +/g, '\n'); + + content = format(content, prettierMarkdown); + + writeFileSync(resolve(pathOutputDir, methodName + '.md'), content); +} + +/** + * Writes the api docs data to correct location. + * + * @param lowerModuleName The lowercase name of the module. + * @param methods The methods data to save. + */ +export function writeApiDocsData( + lowerModuleName: string, + methods: Method[] +): void { + let contentTs = ` +import type { Method } from '../.vitepress/components/api-docs/method'; + +export const ${lowerModuleName}: Method[] = ${JSON.stringify( + methods, + null, + 2 + )}`; + + contentTs = format(contentTs, prettierTypescript); + + writeFileSync(resolve(pathOutputDir, lowerModuleName + '.ts'), contentTs); +} + +/** + * Writes the api docs index to correct location. + * + * @param pages The pages to write into the index. + */ +export function writeApiPagesIndex(pages: PageIndex): void { + // Write api-pages.mjs + console.log('Updating api-pages.mjs'); + pages.sort((a, b) => a.text.localeCompare(b.text)); + let apiPagesContent = ` + // This file is automatically generated. + // Run '${scriptCommand}' to update + export const apiPages = ${JSON.stringify(pages)}; + `.replace(/\n +/, '\n'); + + apiPagesContent = format(apiPagesContent, prettierBabel); + + writeFileSync(pathDocsApiPages, apiPagesContent); +} diff --git a/scripts/apidoc/directMethods.ts b/scripts/apidoc/directMethods.ts new file mode 100644 index 00000000..616226ee --- /dev/null +++ b/scripts/apidoc/directMethods.ts @@ -0,0 +1,56 @@ +import * as TypeDoc from 'typedoc'; +import { writeApiDocsData, writeApiDocsDirectPage } from './apiDocsWriter'; +import { analyzeSignature } from './signature'; +import type { Page, PageIndex } from './utils'; + +/** + * Analyzes and writes the documentation for direct methods such as `faker.fake()`. + * + * @param project The project used to extract the direct methods. + * @returns The generated pages. + */ +export function processDirectMethods( + project: TypeDoc.ProjectReflection +): PageIndex { + const pages: PageIndex = []; + + const directs = project + .getChildrenByKind(TypeDoc.ReflectionKind.Class) + .filter((ref) => ref.name === 'Faker')[0] + .getChildrenByKind(TypeDoc.ReflectionKind.Property) + .filter((ref) => ['fake', 'unique'].includes(ref.name)); + + for (const direct of directs) { + pages.push(processDirectMethod(direct)); + } + + return pages; +} + +/** + * Analyzes and writes the documentation for a direct method such as `faker.fake()`. + * + * @param direct The direct method to process. + * @returns The generated pages. + */ +export function processDirectMethod( + direct: TypeDoc.DeclarationReflection +): Page { + const methodName = direct.name; + const upperMethodName = + methodName.substring(0, 1).toUpperCase() + methodName.substring(1); + console.log(`Processing Direct: ${upperMethodName}`); + + const signature = (direct.type as TypeDoc.ReflectionType).declaration + .signatures[0]; + + writeApiDocsDirectPage(methodName); + writeApiDocsData(methodName, [ + analyzeSignature(signature, undefined, methodName), + ]); + + return { + text: upperMethodName, + link: `/api/${methodName}.html`, + }; +} diff --git a/scripts/apidoc/moduleMethods.ts b/scripts/apidoc/moduleMethods.ts new file mode 100644 index 00000000..89c42a33 --- /dev/null +++ b/scripts/apidoc/moduleMethods.ts @@ -0,0 +1,67 @@ +import * as TypeDoc from 'typedoc'; +import type { Method } from '../../docs/.vitepress/components/api-docs/method'; +import faker from '../../src'; +import { writeApiDocsData, writeApiDocsModulePage } from './apiDocsWriter'; +import { analyzeSignature, toBlock } from './signature'; +import type { PageIndex } from './utils'; + +/** + * Analyzes and writes the documentation for modules and their methods such as `faker.animal.cat()`. + * + * @param project The project used to extract the modules. + * @returns The generated pages. + */ +export function processModuleMethods( + project: TypeDoc.ProjectReflection +): PageIndex { + const modules = project + .getChildrenByKind(TypeDoc.ReflectionKind.Namespace)[0] + .getChildrenByKind(TypeDoc.ReflectionKind.Class); + + const pages: PageIndex = []; + // Generate module file + for (const module of modules) { + pages.push(...processModuleMethod(module)); + } + + return pages; +} + +/** + * Analyzes and writes the documentation for a module and its methods such as `faker.animal.cat()`. + * + * @param direct The module to process. + * @returns The generated pages. + */ +function processModuleMethod(module: TypeDoc.DeclarationReflection): PageIndex { + const moduleName = module.name.replace('_', ''); + const lowerModuleName = + moduleName.substring(0, 1).toLowerCase() + moduleName.substring(1); + if (faker[lowerModuleName] === undefined) { + return []; + } + console.log(`Processing Module ${moduleName}`); + + const methods: Method[] = []; + + // Generate method section + for (const method of module.getChildrenByKind( + TypeDoc.ReflectionKind.Method + )) { + const methodName = method.name; + console.debug(`- ${methodName}`); + const signature = method.signatures[0]; + + methods.push(analyzeSignature(signature, lowerModuleName, methodName)); + } + + writeApiDocsModulePage(moduleName, lowerModuleName, toBlock(module.comment)); + writeApiDocsData(lowerModuleName, methods); + + return [ + { + text: moduleName, + link: `/api/${lowerModuleName}.html`, + }, + ]; +} diff --git a/scripts/apidoc/signature.ts b/scripts/apidoc/signature.ts new file mode 100644 index 00000000..cd4e4e76 --- /dev/null +++ b/scripts/apidoc/signature.ts @@ -0,0 +1,166 @@ +import sanitizeHtml from 'sanitize-html'; +import type * as TypeDoc from 'typedoc'; +import { createMarkdownRenderer } from 'vitepress'; +import type { + Method, + MethodParameter, +} from '../../docs/.vitepress/components/api-docs/method'; +import faker from '../../src'; +import { pathOutputDir } from './utils'; +// TODO ST-DDT 2022-02-20: Actually import this/fix module import errors +// import vitepressConfig from '../../docs/.vitepress/config'; + +export function prettifyMethodName(method: string): string { + return ( + // Capitalize and insert space before upper case characters + method.substring(0, 1).toUpperCase() + + method.substring(1).replace(/([A-Z]+)/g, ' $1') + ); +} + +export function toBlock(comment?: TypeDoc.Comment): string { + return ( + (comment?.shortText.trim() || 'Missing') + + (comment?.text ? '\n\n' + comment.text : '') + ); +} + +const markdown = createMarkdownRenderer( + pathOutputDir, + undefined, + // TODO ST-DDT 2022-02-20: Actually import this/fix module import errors + // vitepressConfig.markdown, + '/' +); + +const htmlSanitizeOptions: sanitizeHtml.IOptions = { + allowedTags: ['a', 'code', 'div', 'li', 'span', 'p', 'pre', 'ul'], + allowedAttributes: { + a: ['href', 'target', 'rel'], + div: ['class'], + pre: ['v-pre'], + span: ['class'], + }, + selfClosing: [], +}; + +function mdToHtml(md: string): string { + const rawHtml = markdown.render(md); + const safeHtml: string = sanitizeHtml(rawHtml, htmlSanitizeOptions); + // Revert some escaped characters for comparison. + if (rawHtml.replace(/>/g, '>') === safeHtml.replace(/>/g, '>')) { + return safeHtml; + } else { + console.debug('Rejected unsafe md:', md); + console.error('Rejected unsafe html:', rawHtml.replace(/>/g, '>')); + console.error('Expected safe html:', safeHtml.replace(/>/g, '>')); + throw new Error('Found unsafe html'); + } +} + +export function analyzeSignature( + signature: TypeDoc.SignatureReflection, + moduleName: string, + methodName: string +): Method { + const parameters: MethodParameter[] = []; + + // Collect Type Parameters + const typeParameters = signature.typeParameters || []; + const signatureTypeParameters: string[] = []; + for (const parameter of typeParameters) { + signatureTypeParameters.push(parameter.name); + parameters.push({ + name: parameter.name, + description: mdToHtml(toBlock(parameter.comment)), + }); + } + + // Collect Parameters + const signatureParameters: string[] = []; + let requiresArgs = false; + for ( + let index = 0; + signature.parameters && index < signature.parameters.length; + index++ + ) { + const parameter = signature.parameters[index]; + + const parameterDefault = parameter.defaultValue; + const parameterRequired = typeof parameterDefault === 'undefined'; + if (index === 0) { + requiresArgs = parameterRequired; + } + const parameterName = parameter.name + (parameterRequired ? '?' : ''); + const parameterType = parameter.type.toString(); + + let parameterDefaultSignatureText = ''; + if (!parameterRequired) { + parameterDefaultSignatureText = ' = ' + parameterDefault; + } + + signatureParameters.push( + parameterName + ': ' + parameterType + parameterDefaultSignatureText + ); + parameters.push({ + name: parameter.name, + type: parameterType, + default: parameterDefault, + description: mdToHtml(toBlock(parameter.comment)), + }); + } + + // Generate usage section + + let signatureTypeParametersString = ''; + if (signatureTypeParameters.length !== 0) { + signatureTypeParametersString = `<${signatureTypeParameters.join(', ')}>`; + } + const signatureParametersString = signatureParameters.join(', '); + + let examples: string; + if (moduleName) { + examples = `faker.${moduleName}.${methodName}${signatureTypeParametersString}(${signatureParametersString}): ${signature.type.toString()}\n`; + } else { + examples = `faker.${methodName}${signatureTypeParametersString}(${signatureParametersString}): ${signature.type.toString()}\n`; + } + faker.seed(0); + if (!requiresArgs && moduleName) { + try { + let example = JSON.stringify(faker[moduleName][methodName]()); + if (example.length > 50) { + example = example.substring(0, 47) + '...'; + } + + examples += `faker.${moduleName}.${methodName}()`; + examples += (example ? ` // => ${example}` : '') + '\n'; + } catch (error) { + // Ignore the error => hide the example call + result. + } + } + const exampleTags = + signature?.comment?.tags + .filter((tag) => tag.tagName === 'example') + .map((tag) => tag.text.trimEnd()) || []; + + if (exampleTags.length > 0) { + examples += exampleTags.join('\n').trim() + '\n'; + } + + const seeAlsos = + signature.comment?.tags + .filter((t) => t.tagName === 'see') + .map((t) => t.text.trim()) ?? []; + + const prettyMethodName = prettifyMethodName(methodName); + return { + name: methodName, + title: prettyMethodName, + description: mdToHtml(toBlock(signature.comment)), + parameters: parameters, + returns: signature.type.toString(), + examples: mdToHtml('```ts\n' + examples + '```'), + deprecated: signature.comment?.hasTag('deprecated') ?? false, + seeAlsos, + }; +} diff --git a/scripts/apidoc/utils.ts b/scripts/apidoc/utils.ts new file mode 100644 index 00000000..ee38d58d --- /dev/null +++ b/scripts/apidoc/utils.ts @@ -0,0 +1,8 @@ +import { resolve } from 'node:path'; + +export type Page = { text: string; link: string }; +export type PageIndex = Array<Page>; + +const pathRoot = resolve(__dirname, '..', '..'); +export const pathDocsDir = resolve(pathRoot, 'docs'); +export const pathOutputDir = resolve(pathDocsDir, 'api'); |
