Skip to content

Commit 0acd718

Browse files
committed
docs: update API documentation
1 parent 8ff1096 commit 0acd718

File tree

17 files changed

+746
-222
lines changed

17 files changed

+746
-222
lines changed

packages/docs/build/api.mjs

+259-67
Original file line numberDiff line numberDiff line change
@@ -3,129 +3,321 @@
33
'use strict'
44

55
import { globby } from 'globby'
6-
import { writeFile } from 'node:fs/promises'
6+
import { writeFile, mkdir } from 'node:fs/promises'
77
import path from 'node:path'
88
import { fileURLToPath } from 'node:url'
99
import { parse } from 'react-docgen-typescript'
1010

11-
const __dirname = path.dirname(fileURLToPath(import.meta.url))
11+
/**
12+
* Derive __dirname in ESM
13+
*/
14+
const __filename = fileURLToPath(import.meta.url)
15+
const __dirname = path.dirname(__filename)
1216

13-
const GLOB = [
17+
/**
18+
* Glob patterns to locate .tsx files for documentation.
19+
* Adjust these patterns based on your project structure.
20+
*/
21+
const GLOB_PATTERNS = [
1422
'**/src/**/*.tsx',
1523
'../node_modules/@coreui/icons-react/src/**/*.tsx',
1624
'../node_modules/@coreui/react-chartjs/src/**/*.tsx',
1725
]
26+
27+
/**
28+
* Options for globby to control file matching behavior.
29+
*/
1830
const GLOBBY_OPTIONS = {
1931
absolute: true,
2032
cwd: path.join(__dirname, '..', '..'),
2133
gitignore: false,
2234
ignore: ['**/docs/**', '**/__tests__/**'],
2335
}
24-
const EXCLUDED_FILES = []
2536

26-
const options = {
37+
/**
38+
* Excluded files list (currently unused).
39+
* Can be utilized for additional exclusion patterns if needed.
40+
*/
41+
const EXCLUDED_FILES = [] // Currently unused, but can be utilized if needed
42+
43+
/**
44+
* Options for react-docgen-typescript parser.
45+
*/
46+
const DOCGEN_OPTIONS = {
2747
savePropValueAsString: true,
2848
shouldIncludePropTagMap: true,
2949
}
3050

31-
const PRO_COMPONENTS = []
51+
/**
52+
* List of pro components that require special handling.
53+
*/
54+
const PRO_COMPONENTS = [
55+
'CDatePicker',
56+
'CDateRangePicker',
57+
'CFormMask',
58+
'CLoadingButton',
59+
'CMultiSelect',
60+
'CRating',
61+
'CSmartPagination',
62+
'CSmartTable',
63+
'CTimePicker',
64+
'CVirtualScroller',
65+
]
66+
67+
/**
68+
* Escapes special characters in text to prevent Markdown rendering issues.
69+
*
70+
* @param {string} text - The text to escape.
71+
* @returns {string} - The escaped text.
72+
*/
73+
function escapeMarkdown(text) {
74+
if (typeof text !== 'string') return text
75+
return (
76+
text
77+
.replaceAll(/(<)/g, String.raw`\$1`)
78+
// .replaceAll(/<C(.*)\/>/g, '`<C$1/>`')
79+
.replaceAll('\n', '<br/>')
80+
.replaceAll(/`([^`]+)`/g, '<code>{`$1`}</code>')
81+
)
82+
}
3283

33-
const replace = (text) =>
34-
text
35-
.replaceAll('(<', '(\\<')
36-
.replace(/<C(.*)\/>/g, '`<C$1/>`')
37-
.replaceAll('\n', '<br/>')
84+
/**
85+
* Generates the relative filename based on the file path.
86+
*
87+
* @param {string} file - The absolute file path.
88+
* @returns {string} - The relative filename.
89+
*/
90+
function getRelativeFilename(file) {
91+
let relativePath
92+
relativePath = file.includes('node_modules')
93+
? path.relative(path.join(__dirname, '..', '..'), file).replace('coreui-', '')
94+
: path.relative(GLOBBY_OPTIONS.cwd, file).replace('coreui-', '')
3895

39-
async function createMdx(file, filename, name, props) {
40-
if (typeof props === 'undefined') {
41-
return
96+
// Remove '-pro' from the filename if not a pro component
97+
const isPro = PRO_COMPONENTS.some((component) => file.includes(component))
98+
if (!isPro) {
99+
relativePath = relativePath.replace('-pro', '')
42100
}
43101

44-
const pro = PRO_COMPONENTS.some((v) => file.includes(v))
45-
let relativeFilename
46-
if (file.includes('node_modules')) {
47-
relativeFilename = file.replace(path.join(file, '..', '..', '..'), '').replace('coreui-', '')
48-
} else {
49-
relativeFilename = file.replace(GLOBBY_OPTIONS.cwd, '').replace('coreui-', '')
102+
return relativePath
103+
}
104+
105+
/**
106+
* Splits the input string by the '|' character, but only when the '|' is outside of any curly braces {} and parentheses ().
107+
*
108+
* @param {string} input - The string to be split.
109+
* @returns {string[]} An array of split parts, trimmed of whitespace.
110+
* @throws {Error} Throws an error if there are unmatched braces or parentheses in the input.
111+
*/
112+
function splitOutsideBracesAndParentheses(input) {
113+
const parts = []
114+
let currentPart = ''
115+
let braceDepth = 0 // Tracks depth of curly braces {}
116+
let parenthesisDepth = 0 // Tracks depth of parentheses ()
117+
118+
for (const char of input) {
119+
switch (char) {
120+
case '{': {
121+
braceDepth++
122+
break
123+
}
124+
case '}': {
125+
braceDepth--
126+
if (braceDepth < 0) {
127+
throw new Error('Unmatched closing curly brace detected.')
128+
}
129+
break
130+
}
131+
case '(': {
132+
parenthesisDepth++
133+
break
134+
}
135+
case ')': {
136+
parenthesisDepth--
137+
if (parenthesisDepth < 0) {
138+
throw new Error('Unmatched closing parenthesis detected.')
139+
}
140+
break
141+
}
142+
case '|': {
143+
// Split only if not inside any braces or parentheses
144+
if (braceDepth === 0 && parenthesisDepth === 0 && currentPart.trim()) {
145+
parts.push(currentPart.trim())
146+
currentPart = ''
147+
continue // Skip adding the '|' to currentPart
148+
}
149+
break
150+
}
151+
default: {
152+
// No action needed for other characters
153+
break
154+
}
155+
}
156+
currentPart += char
157+
}
158+
159+
// After processing all characters, check for unmatched opening braces or parentheses
160+
if (braceDepth !== 0) {
161+
throw new Error('Unmatched opening curly brace detected.')
162+
}
163+
if (parenthesisDepth !== 0) {
164+
throw new Error('Unmatched opening parenthesis detected.')
50165
}
51166

52-
if (!pro) {
53-
relativeFilename = relativeFilename.replace('-pro', '')
167+
// Add the last accumulated part if it's not empty
168+
if (currentPart.trim()) {
169+
parts.push(currentPart.trim())
54170
}
55171

56-
let content = `\n`
57-
content += `\`\`\`jsx\n`
58-
content += `import { ${name} } from '@coreui/${relativeFilename.split('/')[1]}'\n`
172+
return parts
173+
}
174+
175+
/**
176+
* Creates an MDX file with the component's API documentation.
177+
*
178+
* @param {string} file - The absolute path to the component file.
179+
* @param {object} component - The component information extracted by react-docgen-typescript.
180+
*/
181+
async function createMdx(file, component) {
182+
if (!component) {
183+
return
184+
}
185+
186+
const filename = path.basename(file, '.tsx')
187+
const relativeFilename = getRelativeFilename(file)
188+
189+
// Construct import statements
190+
let content = `\n\`\`\`jsx\n`
191+
const importPathParts = relativeFilename.split('/')
192+
if (importPathParts.length > 1) {
193+
content += `import { ${component.displayName} } from '@coreui/${importPathParts[1]}'\n`
194+
}
59195
content += `// or\n`
60-
content += `import ${name} from '@coreui${relativeFilename.replace('.tsx', '')}'\n`
196+
content += `import ${component.displayName} from '@coreui${relativeFilename.replace('.tsx', '')}'\n`
61197
content += `\`\`\`\n\n`
62198

63-
let index = 0
64-
for (const [key, value] of Object.entries(props).sort()) {
199+
const sortedProps = Object.entries(component.props).sort(([a], [b]) => a.localeCompare(b))
200+
201+
// Initialize table headers
202+
for (const [index, [propName, propInfo]] of sortedProps.entries()) {
203+
const isLast = index === sortedProps.length - 1
204+
if (index === 0) {
205+
content += `<div className="table-responsive table-api border rounded mb-3">\n`
206+
content += ` <table className="table">\n`
207+
content += ` <thead>\n`
208+
content += ` <tr>\n`
209+
content += ` <th>Property</th>\n`
210+
content += ` <th>Default</th>\n`
211+
content += ` <th>Type</th>\n`
212+
content += ` </tr>\n`
213+
content += ` </thead>\n`
214+
content += ` <tbody>\n`
215+
}
216+
217+
// Skip props from React's type definitions
65218
if (
66-
value.parent.fileName.includes('@types/react/index.d.ts') ||
67-
value.parent.fileName.includes('@types/react/ts5.0/index.d.ts')
219+
propInfo.parent?.fileName?.includes('@types/react/index.d.ts') ||
220+
propInfo.parent?.fileName?.includes('@types/react/ts5.0/index.d.ts')
68221
) {
222+
if (isLast) {
223+
content += ` </tbody>\n`
224+
content += ` </table>\n`
225+
content += `</div>\n`
226+
}
227+
69228
continue
70229
}
71230

72-
if (value.tags.ignore === '') {
231+
// Skip props marked to be ignored
232+
if (propInfo.tags?.ignore === '') {
73233
continue
74234
}
75235

76-
if (index === 0) {
77-
content += `| Property | Description | Type | Default |\n`
78-
content += `| --- | --- | --- | --- |\n`
79-
}
80-
let name = value.name || ''
81-
const since = value.tags.since ? ` **_${value.tags.since}+_**` : ''
82-
const deprecated = value.tags.deprecated ? ` **_Deprecated ${value.tags.deprecated}+_**` : ''
83-
const description = value.description || '-'
84-
const type = value.type
85-
? value.type.name.includes('ReactElement')
236+
const displayName = propInfo.name || ''
237+
const since = propInfo.tags?.since
238+
? `<span className="badge bg-success">${propInfo.tags.since}+</span>`
239+
: ''
240+
const deprecated = propInfo.tags?.deprecated
241+
? `<span className="badge bg-success">Deprecated ${propInfo.tags.since}</span>`
242+
: ''
243+
const description = propInfo.description || '-'
244+
245+
const type = propInfo.type
246+
? propInfo.type.name.includes('ReactElement')
86247
? 'ReactElement'
87-
: value.type.name
248+
: propInfo.type.name
88249
: ''
89-
const defaultValue = value.defaultValue
90-
? value.defaultValue.value.replace('undefined', '-')
91-
: '-'
92-
const types = []
93-
type.split(' | ').map((element) => {
94-
types.push(`\`${element.replace(/"/g, "'")}\``)
95-
})
96-
97-
content += `| **${name}**${since}${deprecated} | ${replace(description)} | ${types.join(
98-
' \\| ',
99-
)} | ${replace(defaultValue)} |\n`
100-
index++
250+
const defaultValue = propInfo.defaultValue ? `\`${propInfo.defaultValue.value}\`` : `undefined`
251+
252+
// Format types as inline code
253+
const types = splitOutsideBracesAndParentheses(type)
254+
.map((_type) => `\`${_type.trim()}\``)
255+
.join(', ')
256+
257+
const id = `${component.displayName.toLowerCase()}-${propName}`
258+
const anchor = `<a href="#${id}" aria-label="${component.displayName} ${displayName} permalink" className="anchor-link after">#</a>`
259+
260+
content += ` <tr id="${id}">\n`
261+
content += ` <td className="text-primary fw-semibold">${displayName}${anchor}${since}${deprecated}</td>\n`
262+
content += ` <td>${escapeMarkdown(defaultValue)}</td>\n`
263+
content += ` <td>${escapeMarkdown(types)}</td>\n`
264+
content += ` </tr>\n`
265+
content += ` <tr>\n`
266+
content += ` <td colSpan="3">${escapeMarkdown(description)}${propInfo.tags?.example ? `<br /><JSXDocs code={\`${propInfo.tags.example}\`} />` : ''}</td>\n`
267+
content += ` </tr>\n`
268+
269+
if (isLast) {
270+
content += ` </tbody>\n`
271+
content += ` </table>\n`
272+
content += `</div>\n`
273+
}
101274
}
102275

103-
await writeFile(`content/api/${filename}.api.mdx`, content, {
104-
encoding: 'utf8',
105-
}).then(() => {
276+
// Define the output directory and ensure it exists
277+
const outputDir = path.join('content', 'api')
278+
const outputPath = path.join(outputDir, `${filename}.api.mdx`)
279+
280+
// Create the directory if it doesn't exist
281+
try {
282+
await mkdir(outputDir, { recursive: true })
283+
await writeFile(outputPath, content, { encoding: 'utf8' })
106284
console.log(`File created: ${filename}.api.mdx`)
107-
})
285+
} catch (error) {
286+
console.error(`Failed to write file ${outputPath}:`, error)
287+
}
108288
}
109289

290+
/**
291+
* Main function to execute the script.
292+
*/
110293
async function main() {
111294
try {
112-
const files = await globby(GLOB, GLOBBY_OPTIONS, EXCLUDED_FILES)
295+
// Retrieve all matching files based on the glob patterns
296+
const files = await globby(GLOB_PATTERNS, GLOBBY_OPTIONS)
113297

298+
// Process each file concurrently
114299
await Promise.all(
115-
files.map((file) => {
116-
console.log(file)
117-
// const props = docgen.parse(file, options)
118-
const props = parse(file, options)
119-
if (props && typeof props[0] !== 'undefined') {
120-
const filename = path.basename(file, '.tsx')
121-
createMdx(file, filename, props[0].displayName, props[0].props)
300+
files.map(async (file) => {
301+
console.log(`Processing file: ${file}`)
302+
let components
303+
304+
try {
305+
components = parse(file, DOCGEN_OPTIONS)
306+
} catch (parseError) {
307+
console.error(`Failed to parse ${file}:`, parseError)
308+
return
309+
}
310+
311+
if (components && components.length > 0) {
312+
await Promise.all(components.map((component) => createMdx(file, component)))
122313
}
123314
}),
124315
)
125316
} catch (error) {
126-
console.error(error)
317+
console.error('An error occurred:', error)
127318
process.exit(1)
128319
}
129320
}
130321

322+
// Execute the main function
131323
main()

0 commit comments

Comments
 (0)