Skip to content

Commit

Permalink
docs(api): add refresh button to examples
Browse files Browse the repository at this point in the history
  • Loading branch information
ST-DDT committed Dec 1, 2024
1 parent 01e20e9 commit c12385b
Show file tree
Hide file tree
Showing 8 changed files with 246 additions and 17 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ versions.json
/dist
/docs/.vitepress/cache
/docs/.vitepress/dist
/docs/api/*.ts
!/docs/api/api-types.ts
/docs/api/*.md
!/docs/api/index.md
/docs/api/api-search-index.json
/docs/public/api-diff-index.json

# Faker
Expand Down
1 change: 1 addition & 0 deletions docs/.vitepress/components/api-docs/method.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface ApiDocsMethod {
readonly throws: string | undefined; // HTML
readonly signature: string; // HTML
readonly examples: string; // HTML
readonly refresh: (() => Promise<unknown[]>) | undefined;
readonly seeAlsos: string[];
readonly sourcePath: string; // URL-Suffix
}
Expand Down
117 changes: 115 additions & 2 deletions docs/.vitepress/components/api-docs/method.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import { sourceBaseUrl } from '../../../api/source-base-url';
import { slugify } from '../../shared/utils/slugify';
import type { ApiDocsMethod } from './method';
import MethodParameters from './method-parameters.vue';
import RefreshButton from './refresh-button.vue';
const { method } = defineProps<{ method: ApiDocsMethod }>();
const {
Expand All @@ -14,10 +16,112 @@ const {
throws,
signature,
examples,
refresh,
seeAlsos,
sourcePath,
} = method;
const code = ref<HTMLDivElement | null>(null);
const codeBlock = computed(() => code.value?.querySelector('div pre code'));
let codeLines: Element[] | undefined;
function initRefresh(): Element[] {
if (codeBlock.value == null) {
return [];
}
const domLines = codeBlock.value.querySelectorAll('.line');
let lineIndex = 0;
const result: Element[] = [];
while (lineIndex < domLines.length) {
// Skip empty and preparatory lines (no '^faker.' invocation)
if (
domLines[lineIndex]?.children.length === 0 ||
!/^\w*faker\w*\./i.test(domLines[lineIndex]?.textContent ?? '')
) {
lineIndex++;
continue;
}
// Skip to end of the invocation (if multiline)
while (
domLines[lineIndex] != null &&
!/^([^ ].*)?\);? ?(\/\/|$)/.test(domLines[lineIndex]?.textContent ?? '')
) {
lineIndex++;
}
const domLine = domLines[lineIndex];
result.push(domLine);
lineIndex++;
// Purge old results
if (domLine.lastElementChild?.textContent?.startsWith('//')) {
// Inline comments
domLine.lastElementChild.remove();
} else {
// Multiline comments
while (domLines[lineIndex]?.children[0]?.textContent?.startsWith('//')) {
domLines[lineIndex].previousSibling?.remove(); // newline
domLines[lineIndex].remove(); // comment
lineIndex++;
}
}
// Add space between invocation and comment (if missing)
const lastElementChild = domLine.lastElementChild;
if (
lastElementChild != null &&
!lastElementChild.textContent?.endsWith(' ')
) {
lastElementChild.textContent += ' ';
}
}
return result;
}
async function onRefresh() {
if (refresh != null && codeBlock.value != null) {
codeLines ??= initRefresh();
// Remove old comments
codeBlock.value
.querySelectorAll('.comment-delete-marker')
.forEach((el) => el.remove());
const results = await refresh();
// Insert new comments
for (let i = 0; i < results.length; i++) {
const result = results[i];
const domLine = codeLines[i];
const resultLines =
result === undefined
? ['undefined']
: JSON.stringify(result)
.replaceAll(/\\r/g, '')
.replaceAll(/</g, '&lt;')
.split('\\n');
if (resultLines.length === 1) {
domLine.insertAdjacentHTML('beforeend', newCommentSpan(resultLines[0]));
} else {
for (const line of resultLines.reverse()) {
domLine.insertAdjacentHTML('afterend', newCommentLine(line));
}
}
}
}
}
function newCommentLine(content: string): string {
return `<span class="line comment-delete-marker">\n${newCommentSpan(content)}</span>`;
}
function newCommentSpan(content: string): string {
return `<span class="comment-delete-marker" style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ${content}</span>`;
}
function seeAlsoToUrl(see: string): string {
const [, module, methodName] = see.replace(/\(.*/, '').split('\.');
Expand Down Expand Up @@ -51,8 +155,13 @@ function seeAlsoToUrl(see: string): string {

<div v-html="signature" />

<h3>Examples</h3>
<div v-html="examples" />
<h3 class="inline">Examples</h3>
<RefreshButton
v-if="refresh != null"
style="margin-left: 0.5em"
:refresh="onRefresh"
/>
<div ref="code" v-html="examples" />

<div v-if="seeAlsos.length > 0">
<h3>See Also</h3>
Expand Down Expand Up @@ -107,4 +216,8 @@ svg.source-link-icon {
display: inline;
margin-left: 0.3em;
}
h3.inline {
display: inline-block;
}
</style>
48 changes: 48 additions & 0 deletions docs/.vitepress/components/api-docs/refresh-button.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<script setup lang="ts">
import { ref } from 'vue';
const { refresh } = defineProps<{ refresh: () => Promise<void> }>();
const spinning = ref(false);
async function onRefresh() {
spinning.value = true;
await refresh();
spinning.value = false;
}
</script>

<template>
<button class="refresh" @click="onRefresh">
<div :class="{ spinning: spinning }">⟳</div>
</button>
</template>

<style scoped>
button.refresh {
border: 1px solid var(--vp-code-copy-code-border-color);
border-radius: 4px;
width: 40px;
height: 40px;
font-size: 25px;
vertical-align: middle;
}
button.refresh:hover {
background-color: var(--vp-code-copy-code-bg);
opacity: 1;
}
.spinning {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style>
1 change: 1 addition & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ async function enableFaker() {
e.g. 'faker.food.description()' or 'fakerZH_CN.person.firstName()'
For other languages please refer to https://fakerjs.dev/guide/localization.html#available-locales
For a full list of all methods please refer to https://fakerjs.dev/api/\`, logStyle);
enableFaker = () => imported; // Init only once
return imported;
}
`,
Expand Down
10 changes: 0 additions & 10 deletions docs/api/.gitignore

This file was deleted.

79 changes: 74 additions & 5 deletions scripts/apidocs/output/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export async function writePages(pages: RawApiDocsPage[]): Promise<void> {
async function writePage(page: RawApiDocsPage): Promise<void> {
try {
await writePageMarkdown(page);
await writePageJsonData(page);
await writePageData(page);
} catch (error) {
throw new Error(`Error writing page ${page.title}`, { cause: error });
}
Expand All @@ -51,7 +51,7 @@ async function writePageMarkdown(page: RawApiDocsPage): Promise<void> {
let content = `
<script setup>
import ApiDocsMethod from '../.vitepress/components/api-docs/method.vue';
import ${camelTitle} from './${camelTitle}.json';
import ${camelTitle} from './${camelTitle}.ts';
</script>
<!-- This file is automatically generated. -->
Expand Down Expand Up @@ -98,16 +98,42 @@ async function writePageMarkdown(page: RawApiDocsPage): Promise<void> {
*
* @param page The page to write.
*/
async function writePageJsonData(page: RawApiDocsPage): Promise<void> {
async function writePageData(page: RawApiDocsPage): Promise<void> {
const { camelTitle, methods } = page;
const pageData: Record<string, ApiDocsMethod> = Object.fromEntries(
await Promise.all(
methods.map(async (method) => [method.name, await toMethodData(method)])
)
);
const content = JSON.stringify(pageData, null, 2);

writeFileSync(resolve(FILE_PATH_API_DOCS, `${camelTitle}.json`), content);
const refreshFunctions: Record<string, string> = Object.fromEntries(
await Promise.all(
methods.map(async (method) => [
method.name,
await toRefreshFunction(method),
])
)
);

const content = `export default ${JSON.stringify(
pageData,
(_, value: unknown) => {
if (typeof value === 'function') {
return value.toString(); // Insert placeholder
}
return value;
},
2
)}`.replaceAll(
/"refresh-([^"-]+)-placeholder"/g,
(_, name) => refreshFunctions[name]
);

writeFileSync(
resolve(FILE_PATH_API_DOCS, `${camelTitle}.ts`),
await formatTypescript(content)
);
}

const defaultCommentRegex = /\s+Defaults to `([^`]+)`\..*/;
Expand All @@ -130,6 +156,10 @@ async function toMethodData(method: RawApiDocsMethod): Promise<ApiDocsMethod> {
let formattedSignature = await formatTypescript(signature);
formattedSignature = formattedSignature.trim();

// eslint-disable-next-line @typescript-eslint/require-await
const refresh = async () => ['refresh', name, 'placeholder'];
refresh.toString = () => `refresh-${name}-placeholder`;

/* Target order, omitted to improve diff to old files
return {
name,
Expand Down Expand Up @@ -167,6 +197,7 @@ async function toMethodData(method: RawApiDocsMethod): Promise<ApiDocsMethod> {
returns: returns.text,
signature: codeToHtml(formattedSignature),
examples: codeToHtml(examples.join('\n')),
refresh,
deprecated: mdToHtml(deprecated),
seeAlsos: seeAlsos.map((seeAlso) => mdToHtml(seeAlso, true)),
};
Expand All @@ -175,3 +206,41 @@ async function toMethodData(method: RawApiDocsMethod): Promise<ApiDocsMethod> {
export function extractSummaryDefault(description: string): string | undefined {
return defaultCommentRegex.exec(description)?.[1];
}

async function toRefreshFunction(method: RawApiDocsMethod): Promise<string> {
const { name, signatures } = method;
const signatureData = required(signatures.at(-1), 'method signature');
const { examples } = signatureData;

const exampleLines = examples
.join('\n')
.replaceAll(/ ?\/\/.*$/gm, '') // Remove comments
.replaceAll(/^import .*$/gm, '') // Remove imports
.replaceAll(
/^(\w*faker\w*\..+(?:(?:.|\n..)*\n[^ ])?\));?$/gim,
`try { result.push($1); } catch (error: unknown) { result.push(error instanceof Error ? error.name : 'Error'); }\n`
); // collect results of faker calls

const fullMethod = `async (): Promise<unknown[]> => {
await enableFaker();
faker.seed();
faker.setDefaultRefDate();
const result: unknown[] = [];
${exampleLines}
return result;
}`;
try {
const formattedMethod = await formatTypescript(fullMethod);
return formattedMethod.replace(/;\s+$/, ''); // Remove trailing semicolon
} catch (error: unknown) {
console.error(
'Failed to format refresh function for',
name,
fullMethod,
error
);
return 'undefined';
}
}
2 changes: 2 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
"exclude": [
"node_modules",
"dist",
// Ignore the generated API documentation
"docs/api",
// required for the signature related tests on macOS #2280
"test/scripts/apidocs/temp"
]
Expand Down

0 comments on commit c12385b

Please sign in to comment.