diff options
| author | ST-DDT <[email protected]> | 2024-04-01 10:21:18 +0200 |
|---|---|---|
| committer | GitHub <[email protected]> | 2024-04-01 10:21:18 +0200 |
| commit | 6191a5d883048b694404dbf42527caba395828ea (patch) | |
| tree | d0f18f17789cb0bbdb5d6087f1a95772438dfe27 /scripts/apidocs/utils | |
| parent | 7dae52bfcd93c41ec9d2c4dd4d96a07f31c3dfc1 (diff) | |
| download | faker-6191a5d883048b694404dbf42527caba395828ea.tar.xz faker-6191a5d883048b694404dbf42527caba395828ea.zip | |
docs: rewrite api-docs generation using ts-morph (#2628)
Diffstat (limited to 'scripts/apidocs/utils')
| -rw-r--r-- | scripts/apidocs/utils/format.ts | 31 | ||||
| -rw-r--r-- | scripts/apidocs/utils/markdown.ts | 111 | ||||
| -rw-r--r-- | scripts/apidocs/utils/paths.ts | 24 | ||||
| -rw-r--r-- | scripts/apidocs/utils/value-checks.ts | 69 |
4 files changed, 235 insertions, 0 deletions
diff --git a/scripts/apidocs/utils/format.ts b/scripts/apidocs/utils/format.ts new file mode 100644 index 00000000..ab9f524d --- /dev/null +++ b/scripts/apidocs/utils/format.ts @@ -0,0 +1,31 @@ +import type { Options } from 'prettier'; +import { format } from 'prettier'; +import prettierConfig from '../../../.prettierrc.js'; + +/** + * Formats markdown contents. + * + * @param text The text to format. + */ +export async function formatMarkdown(text: string): Promise<string> { + return format(text, prettierMarkdown); +} + +/** + * Formats typedoc contents. + * + * @param text The text to format. + */ +export async function formatTypescript(text: string): Promise<string> { + return format(text, prettierTypescript); +} + +const prettierMarkdown: Options = { + ...prettierConfig, + parser: 'markdown', +}; + +const prettierTypescript: Options = { + ...prettierConfig, + parser: 'typescript', +}; diff --git a/scripts/apidocs/utils/markdown.ts b/scripts/apidocs/utils/markdown.ts new file mode 100644 index 00000000..b0cc68be --- /dev/null +++ b/scripts/apidocs/utils/markdown.ts @@ -0,0 +1,111 @@ +import sanitizeHtml from 'sanitize-html'; +import type { MarkdownRenderer } from 'vitepress'; +import { createMarkdownRenderer } from 'vitepress'; +import vitepressConfig from '../../../docs/.vitepress/config'; +import { FILE_PATH_API_DOCS } from './paths'; + +let markdown: MarkdownRenderer; + +export async function initMarkdownRenderer(): Promise<void> { + markdown = await createMarkdownRenderer( + FILE_PATH_API_DOCS, + vitepressConfig.markdown, + '/' + ); +} + +const htmlSanitizeOptions: sanitizeHtml.IOptions = { + allowedTags: [ + 'a', + 'button', + 'code', + 'div', + 'li', + 'p', + 'pre', + 'span', + 'strong', + 'ul', + ], + allowedAttributes: { + a: ['href', 'target', 'rel'], + button: ['class', 'title'], + div: ['class'], + pre: ['class', 'v-pre'], + span: ['class', 'style'], + }, + selfClosing: [], +}; + +function comparableSanitizedHtml(html: string): string { + return html + .replaceAll(/&#x[0-9A-F]{2};/g, (x) => + String.fromCodePoint(Number.parseInt(x.slice(3, -1), 16)) + ) + .replaceAll('>', '>') + .replaceAll('<', '<') + .replaceAll('&', '&') + .replaceAll('=""', '') + .replaceAll(' ', ''); +} + +/** + * Converts a Typescript code block to an HTML string and sanitizes it. + * + * @param code The code to convert. + * + * @returns The converted HTML string. + */ +export function codeToHtml(code: string): string { + const delimiter = '```'; + return mdToHtml(`${delimiter}ts\n${code}\n${delimiter}`); +} + +/** + * Converts Markdown to an HTML string and sanitizes it. + * + * @param md The markdown to convert. + * @param inline Whether to render the markdown as inline, without a wrapping `<p>` tag. Defaults to `false`. + * + * @returns The converted HTML string. + */ +export function mdToHtml(md: string, inline?: boolean): string; +/** + * Converts Markdown to an HTML string and sanitizes it. + * + * @param md The markdown to convert. + * @param inline Whether to render the markdown as inline, without a wrapping `<p>` tag. Defaults to `false`. + * + * @returns The converted HTML string. + */ +export function mdToHtml( + md: string | undefined, + inline?: boolean +): string | undefined; +export function mdToHtml( + md: string | undefined, + inline: boolean = false +): string | undefined { + if (md == null) { + return undefined; + } + + const rawHtml = inline ? markdown.renderInline(md) : markdown.render(md); + + const safeHtml: string = sanitizeHtml(rawHtml, htmlSanitizeOptions); + // Revert some escaped characters for comparison. + if (comparableSanitizedHtml(rawHtml) === comparableSanitizedHtml(safeHtml)) { + return adjustUrls(safeHtml); + } + + console.debug('Rejected unsafe md:\n', md); + console.error('Rejected unsafe html:\n', rawHtml); + console.error('Clean unsafe html:\n', comparableSanitizedHtml(rawHtml)); + console.error('Clean safe html:\n', comparableSanitizedHtml(safeHtml)); + console.log('-'.repeat(80)); + throw new Error('Found unsafe html'); +} + +export function adjustUrls(description: string): string { + return description.replaceAll(/https:\/\/(next.)?fakerjs.dev\//g, '/'); +} diff --git a/scripts/apidocs/utils/paths.ts b/scripts/apidocs/utils/paths.ts new file mode 100644 index 00000000..8abca1ea --- /dev/null +++ b/scripts/apidocs/utils/paths.ts @@ -0,0 +1,24 @@ +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const FILE_PATH_THIS = dirname(fileURLToPath(import.meta.url)); +/** + * The path to the project directory. + */ +// Required for converting the source file paths to relative paths +export const FILE_PATH_PROJECT = resolve(FILE_PATH_THIS, '..', '..', '..'); +/** + * The path to the docs directory. + */ +// Required for writing the api page vitepress config +export const FILE_PATH_DOCS = resolve(FILE_PATH_PROJECT, 'docs'); +/** + * The path to the website's public directory. + */ +// Required for publishing the diff index +export const FILE_PATH_PUBLIC = resolve(FILE_PATH_DOCS, 'public'); +/** + * The path to the api docs directory. + */ +// Required for writing various api docs files +export const FILE_PATH_API_DOCS = resolve(FILE_PATH_DOCS, 'api'); diff --git a/scripts/apidocs/utils/value-checks.ts b/scripts/apidocs/utils/value-checks.ts new file mode 100644 index 00000000..f8578ccc --- /dev/null +++ b/scripts/apidocs/utils/value-checks.ts @@ -0,0 +1,69 @@ +export function exactlyOne<T>(input: ReadonlyArray<T>, property: string): T { + if (input.length !== 1) { + throw new Error( + `Expected exactly one element for ${property}, got ${input.length}` + ); + } + + return input[0]; +} + +export function optionalOne<T>( + input: ReadonlyArray<T>, + property: string +): T | undefined { + if (input.length > 1) { + throw new Error( + `Expected one optional element for ${property}, got ${input.length}` + ); + } + + return input[0]; +} + +export function required<T>( + input: T | undefined, + property: string +): NonNullable<T> { + if (input == null) { + throw new Error(`Expected a value for ${property}, got undefined`); + } + + return input; +} + +export function allRequired<T>( + input: ReadonlyArray<T | undefined>, + property: string +): Array<NonNullable<T>> { + return input.map((v, i) => required(v, `${property}[${i}]`)); +} + +export function atLeastOne<T>( + input: ReadonlyArray<T>, + property: string +): ReadonlyArray<T> { + if (input.length === 0) { + throw new Error(`Expected at least one element for ${property}`); + } + + return input; +} + +export function atLeastOneAndAllRequired<T>( + input: ReadonlyArray<T | undefined>, + property: string +): ReadonlyArray<NonNullable<T>> { + return atLeastOne(allRequired(input, property), property); +} + +export function valueForKey<T>(input: Record<string, T>, key: string): T { + return required(input[key], key); +} + +export function valuesForKeys<T>( + input: Record<string, T>, + keys: string[] +): T[] { + return keys.map((key) => valueForKey(input, key)); +} |
