aboutsummaryrefslogtreecommitdiff
path: root/scripts/apidocs/utils
diff options
context:
space:
mode:
authorST-DDT <[email protected]>2024-04-01 10:21:18 +0200
committerGitHub <[email protected]>2024-04-01 10:21:18 +0200
commit6191a5d883048b694404dbf42527caba395828ea (patch)
treed0f18f17789cb0bbdb5d6087f1a95772438dfe27 /scripts/apidocs/utils
parent7dae52bfcd93c41ec9d2c4dd4d96a07f31c3dfc1 (diff)
downloadfaker-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.ts31
-rw-r--r--scripts/apidocs/utils/markdown.ts111
-rw-r--r--scripts/apidocs/utils/paths.ts24
-rw-r--r--scripts/apidocs/utils/value-checks.ts69
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('&gt;', '>')
+ .replaceAll('&lt;', '<')
+ .replaceAll('&amp;', '&')
+ .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));
+}