Skip to content

Commit e6d27a3

Browse files
ST-DDTShinigami92
andauthored
docs(api): add refresh button to examples (#3301)
* docs(api): add refresh button to examples * chore: improve button behavior slightly * chore: improve output format * chore: ignore examples without recordable results * temp * chore: use svg button * chore: use json5 format for test * chore: simplify result formatting * test: add formatting tests * test: add e2e refresh test * test: use static test values * chore: fix regex * chore: simplify refresh placeholder * Update cypress/e2e/example-refresh.cy.ts * fix: handle property after function call * Apply suggestions from code review Co-authored-by: Shinigami <[email protected]> * Apply suggestions from code review Co-authored-by: Shinigami <[email protected]> * Apply suggestions from code review Co-authored-by: Shinigami <[email protected]> * chore: format * chore: add comment --------- Co-authored-by: Shinigami <[email protected]>
1 parent 817f8a0 commit e6d27a3

File tree

16 files changed

+618
-17
lines changed

16 files changed

+618
-17
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ versions.json
8080
/dist
8181
/docs/.vitepress/cache
8282
/docs/.vitepress/dist
83+
/docs/api/*.ts
84+
!/docs/api/api-types.ts
85+
/docs/api/*.md
86+
!/docs/api/index.md
87+
/docs/api/api-search-index.json
8388
/docs/public/api-diff-index.json
8489

8590
# Faker

cypress/e2e/example-refresh.cy.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
describe('example-refresh', () => {
2+
it('should refresh the example', () => {
3+
// given
4+
cy.visit('/api/faker.html#constructor');
5+
cy.get('.refresh').first().as('refresh');
6+
cy.get('@refresh').next().find('code').as('codeBlock');
7+
cy.get('@codeBlock').then(($el) => {
8+
const originalCodeText = $el.text();
9+
10+
cy.get('@refresh')
11+
.click()
12+
.should('not.be.disabled') // stays disabled on error
13+
.then(() => {
14+
cy.get('@codeBlock').then(($el) => {
15+
const newCodeText = $el.text();
16+
expect(newCodeText).not.to.equal(originalCodeText);
17+
18+
cy.get('@refresh')
19+
.click()
20+
.should('not.be.disabled') // stays disabled on error
21+
.then(() => {
22+
cy.get('@codeBlock').then(($el) => {
23+
const newCodeText2 = $el.text();
24+
expect(newCodeText2).not.to.equal(originalCodeText);
25+
expect(newCodeText2).not.to.equal(newCodeText);
26+
});
27+
});
28+
});
29+
});
30+
});
31+
});
32+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export function formatResult(result: unknown): string {
2+
return result === undefined
3+
? 'undefined'
4+
: typeof result === 'bigint'
5+
? `${result}n`
6+
: JSON.stringify(result, undefined, 2)
7+
.replaceAll('\\r', '')
8+
.replaceAll('<', '&lt;')
9+
.replaceAll(
10+
/(^ *|: )"([^'\n]*?)"(?=,?$|: )/gm,
11+
(_, p1, p2) => `${p1}'${p2.replace(/\\"/g, '"')}'`
12+
)
13+
.replaceAll(/\n */g, ' ');
14+
}

docs/.vitepress/components/api-docs/method.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export interface ApiDocsMethod {
88
readonly throws: string | undefined; // HTML
99
readonly signature: string; // HTML
1010
readonly examples: string; // HTML
11+
readonly refresh: (() => Promise<unknown[]>) | undefined;
1112
readonly seeAlsos: string[];
1213
readonly sourcePath: string; // URL-Suffix
1314
}

docs/.vitepress/components/api-docs/method.vue

Lines changed: 118 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
<script setup lang="ts">
2+
import { computed, ref, useTemplateRef } from 'vue';
23
import { sourceBaseUrl } from '../../../api/source-base-url';
34
import { slugify } from '../../shared/utils/slugify';
5+
import { formatResult } from './format';
46
import type { ApiDocsMethod } from './method';
57
import MethodParameters from './method-parameters.vue';
8+
import RefreshButton from './refresh-button.vue';
69
710
const { method } = defineProps<{ method: ApiDocsMethod }>();
811
const {
@@ -14,10 +17,113 @@ const {
1417
throws,
1518
signature,
1619
examples,
20+
refresh,
1721
seeAlsos,
1822
sourcePath,
1923
} = method;
2024
25+
const code = useTemplateRef('code');
26+
const codeBlock = computed(() => code.value?.querySelector('div pre code'));
27+
const codeLines = ref<Element[]>();
28+
29+
function initRefresh(): Element[] {
30+
if (codeBlock.value == null) {
31+
return [];
32+
}
33+
const domLines = codeBlock.value.querySelectorAll('.line');
34+
let lineIndex = 0;
35+
const result: Element[] = [];
36+
while (lineIndex < domLines.length) {
37+
// Skip empty and preparatory lines (no '^faker.' invocation)
38+
if (
39+
domLines[lineIndex]?.children.length === 0 ||
40+
!/^\w*faker\w*\./i.test(domLines[lineIndex]?.textContent ?? '')
41+
) {
42+
lineIndex++;
43+
continue;
44+
}
45+
46+
// Skip to end of the invocation (if multiline)
47+
while (
48+
domLines[lineIndex] != null &&
49+
!/^([^ ].*)?\)(\.\w+)?;? ?(\/\/|$)/.test(
50+
domLines[lineIndex]?.textContent ?? ''
51+
)
52+
) {
53+
lineIndex++;
54+
}
55+
56+
if (lineIndex >= domLines.length) {
57+
break;
58+
}
59+
60+
const domLine = domLines[lineIndex];
61+
result.push(domLine);
62+
lineIndex++;
63+
64+
// Purge old results
65+
if (domLine.lastElementChild?.textContent?.startsWith('//')) {
66+
// Inline comments
67+
domLine.lastElementChild.remove();
68+
} else {
69+
// Multiline comments
70+
while (domLines[lineIndex]?.children[0]?.textContent?.startsWith('//')) {
71+
domLines[lineIndex].previousSibling?.remove(); // newline
72+
domLines[lineIndex].remove(); // comment
73+
lineIndex++;
74+
}
75+
}
76+
77+
// Add space between invocation and comment (if missing)
78+
const lastElementChild = domLine.lastElementChild;
79+
if (
80+
lastElementChild != null &&
81+
!lastElementChild.textContent?.endsWith(' ')
82+
) {
83+
lastElementChild.textContent += ' ';
84+
}
85+
}
86+
87+
return result;
88+
}
89+
90+
async function onRefresh(): Promise<void> {
91+
if (refresh != null && codeBlock.value != null) {
92+
codeLines.value ??= initRefresh();
93+
94+
const results = await refresh();
95+
96+
// Remove old comments
97+
codeBlock.value
98+
.querySelectorAll('.comment-delete-marker')
99+
.forEach((el) => el.remove());
100+
101+
// Insert new comments
102+
for (let i = 0; i < results.length; i++) {
103+
const result = results[i];
104+
const domLine = codeLines.value[i];
105+
const prettyResult = formatResult(result);
106+
const resultLines = prettyResult.split('\\n');
107+
108+
if (resultLines.length === 1) {
109+
domLine.insertAdjacentHTML('beforeend', newCommentSpan(resultLines[0]));
110+
} else {
111+
for (const line of resultLines.reverse()) {
112+
domLine.insertAdjacentHTML('afterend', newCommentLine(line));
113+
}
114+
}
115+
}
116+
}
117+
}
118+
119+
function newCommentLine(content: string): string {
120+
return `<span class="line comment-delete-marker">\n${newCommentSpan(content)}</span>`;
121+
}
122+
123+
function newCommentSpan(content: string): string {
124+
return `<span class="comment-delete-marker" style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ${content}</span>`;
125+
}
126+
21127
function seeAlsoToUrl(see: string): string {
22128
const [, module, methodName] = see.replace(/\(.*/, '').split('\.');
23129
@@ -51,8 +157,14 @@ function seeAlsoToUrl(see: string): string {
51157

52158
<div v-html="signature" />
53159

54-
<h3>Examples</h3>
55-
<div v-html="examples" />
160+
<h3 class="inline">Examples</h3>
161+
<RefreshButton
162+
class="refresh"
163+
v-if="refresh != null"
164+
style="margin-left: 0.5em"
165+
:refresh="onRefresh"
166+
/>
167+
<div ref="code" v-html="examples" />
56168

57169
<div v-if="seeAlsos.length > 0">
58170
<h3>See Also</h3>
@@ -107,4 +219,8 @@ svg.source-link-icon {
107219
display: inline;
108220
margin-left: 0.3em;
109221
}
222+
223+
h3.inline {
224+
display: inline-block;
225+
}
110226
</style>
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<script setup lang="ts">
2+
import { ref } from 'vue';
3+
4+
// This should probably use emit instead, but emit cannot be awaited
5+
const { refresh } = defineProps<{ refresh: () => Promise<void> }>();
6+
7+
const spinning = ref(false);
8+
9+
async function onRefresh() {
10+
spinning.value = true;
11+
await Promise.all([refresh(), delay(100)]);
12+
spinning.value = false;
13+
}
14+
15+
// Extra delay to make the spinning effect more visible
16+
// Some examples barely/don't change, so the spinning is the only visible effect
17+
function delay(ms: number) {
18+
return new Promise((resolve) => setTimeout(resolve, ms));
19+
}
20+
</script>
21+
22+
<template>
23+
<button
24+
class="refresh"
25+
title="Refresh Examples"
26+
:disabled="spinning"
27+
@click="onRefresh"
28+
>
29+
<div :class="{ spinning: spinning }" />
30+
</button>
31+
</template>
32+
33+
<style scoped>
34+
button.refresh {
35+
border: 1px solid var(--vp-code-copy-code-border-color);
36+
border-radius: 4px;
37+
width: 40px;
38+
height: 40px;
39+
font-size: 25px;
40+
vertical-align: middle;
41+
}
42+
43+
button.refresh div {
44+
background-image: url('refresh.svg');
45+
background-position: 50%;
46+
background-size: 20px;
47+
background-repeat: no-repeat;
48+
width: 100%;
49+
height: 100%;
50+
}
51+
52+
button.refresh:hover {
53+
background-color: var(--vp-code-copy-code-bg);
54+
opacity: 1;
55+
}
56+
57+
div.spinning {
58+
animation: spin 1s linear infinite;
59+
}
60+
61+
@keyframes spin {
62+
from {
63+
transform: rotate(0deg);
64+
}
65+
to {
66+
transform: rotate(360deg);
67+
}
68+
}
69+
</style>
Lines changed: 1 addition & 0 deletions
Loading

docs/.vitepress/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ async function enableFaker() {
133133
e.g. 'faker.food.description()' or 'fakerZH_CN.person.firstName()'
134134
For other languages please refer to https://fakerjs.dev/guide/localization.html#available-locales
135135
For a full list of all methods please refer to https://fakerjs.dev/api/\`, logStyle);
136+
enableFaker = () => imported; // Init only once
136137
return imported;
137138
}
138139
`,

docs/api/.gitignore

Lines changed: 0 additions & 10 deletions
This file was deleted.

eslint.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const config: ReturnType<typeof tseslint.config> = tseslint.config(
2424
'.github/workflows/commentCodeGeneration.ts',
2525
'.prettierrc.js',
2626
'docs/.vitepress/components/shims.d.ts',
27+
'docs/.vitepress/components/api-docs/format.ts',
2728
'docs/.vitepress/shared/utils/slugify.ts',
2829
'docs/.vitepress/theme/index.ts',
2930
'eslint.config.js',

0 commit comments

Comments
 (0)