Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 0acd718

Browse filesBrowse files
committed
docs: update API documentation
1 parent 8ff1096 commit 0acd718
Copy full SHA for 0acd718

File tree

17 files changed

+746
-222
lines changed
Filter options

17 files changed

+746
-222
lines changed

‎packages/docs/build/api.mjs

Copy file name to clipboardExpand all lines: packages/docs/build/api.mjs
+259-67Lines changed: 259 additions & 67 deletions
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.