|
| 1 | +import * as fs from 'fs'; |
| 2 | + |
| 3 | +interface ApiProp { |
| 4 | + type: { |
| 5 | + name: string; |
| 6 | + description?: string; |
| 7 | + }; |
| 8 | + required?: boolean; |
| 9 | + default?: string; |
| 10 | + deprecated?: boolean; |
| 11 | + deprecationInfo?: string; |
| 12 | + signature?: { |
| 13 | + type: string; |
| 14 | + describedArgs?: string[]; |
| 15 | + }; |
| 16 | + additionalInfo?: { |
| 17 | + cssApi?: boolean; |
| 18 | + sx?: boolean; |
| 19 | + }; |
| 20 | +} |
| 21 | + |
| 22 | +interface ApiSlot { |
| 23 | + name: string; |
| 24 | + description: string; |
| 25 | + default: string; |
| 26 | + class: string | null; |
| 27 | +} |
| 28 | + |
| 29 | +interface ApiClass { |
| 30 | + key: string; |
| 31 | + className: string; |
| 32 | + description: string; |
| 33 | + isGlobal: boolean; |
| 34 | +} |
| 35 | + |
| 36 | +interface ApiInheritance { |
| 37 | + component: string; |
| 38 | + pathname: string; |
| 39 | +} |
| 40 | + |
| 41 | +interface ApiJson { |
| 42 | + props: Record<string, ApiProp>; |
| 43 | + name: string; |
| 44 | + imports: string[]; |
| 45 | + slots?: ApiSlot[]; |
| 46 | + classes?: ApiClass[]; |
| 47 | + spread?: boolean; |
| 48 | + themeDefaultProps?: boolean; |
| 49 | + muiName?: string; |
| 50 | + forwardsRefTo?: string | null; |
| 51 | + filename?: string; |
| 52 | + inheritance?: ApiInheritance; |
| 53 | + demos?: string; |
| 54 | + cssComponent?: boolean; |
| 55 | + deprecated?: boolean; |
| 56 | + deprecationInfo?: string; |
| 57 | +} |
| 58 | + |
| 59 | +/** |
| 60 | + * Convert prop type description from HTML format |
| 61 | + */ |
| 62 | +function formatPropTypeDescription(html: string): string { |
| 63 | + // Decode HTML entities |
| 64 | + const result = html |
| 65 | + .replace(/</g, '<') |
| 66 | + .replace(/>/g, '>') |
| 67 | + .replace(/"/g, '"') |
| 68 | + .replace(/|/g, '|') |
| 69 | + .replace(/ /g, ' ') |
| 70 | + .replace(/&/g, '&') |
| 71 | + // Replace <br> tags with space to maintain readability |
| 72 | + .replace(/<br\s*\/?>/gi, ' ') |
| 73 | + // Clean up excessive whitespace |
| 74 | + .replace(/\s+/g, ' ') |
| 75 | + .trim(); |
| 76 | + |
| 77 | + return result; |
| 78 | +} |
| 79 | + |
| 80 | +/** |
| 81 | + * Convert HTML to markdown |
| 82 | + */ |
| 83 | +function htmlToMarkdown(html: string): string { |
| 84 | + // First pass: decode entities and handle inline elements |
| 85 | + let markdown = html |
| 86 | + // Decode HTML entities first |
| 87 | + .replace(/</g, '<') |
| 88 | + .replace(/>/g, '>') |
| 89 | + .replace(/"/g, '"') |
| 90 | + .replace(/|/g, '|') |
| 91 | + .replace(/ /g, ' ') |
| 92 | + .replace(/&/g, '&') |
| 93 | + // Convert <code> to backticks |
| 94 | + .replace(/<code>([^<]+)<\/code>/gi, '`$1`') |
| 95 | + // Convert <a> to markdown links |
| 96 | + .replace(/<a\s+href="([^"]+)">([^<]+)<\/a>/gi, '[$2]($1)'); |
| 97 | + |
| 98 | + // Handle lists - process them as complete units to avoid extra line breaks |
| 99 | + markdown = markdown.replace(/<ul[^>]*>(.*?)<\/ul>/gis, (match, listContent: string) => { |
| 100 | + // Process each list item |
| 101 | + const items = listContent |
| 102 | + .split(/<\/li>/) |
| 103 | + .map((item) => item.replace(/<li[^>]*>/, '').trim()) |
| 104 | + .filter((item) => item.length > 0) |
| 105 | + .map((item) => `- ${item}`) |
| 106 | + .join('\n'); |
| 107 | + |
| 108 | + return `\n${items}\n`; |
| 109 | + }); |
| 110 | + |
| 111 | + // Handle other block elements |
| 112 | + markdown = markdown |
| 113 | + // Convert <br> to newline |
| 114 | + .replace(/<br\s*\/?>/gi, '\n') |
| 115 | + // Convert <p> to double newline |
| 116 | + .replace(/<p[^>]*>/gi, '\n\n') |
| 117 | + .replace(/<\/p>/gi, '') |
| 118 | + // Remove any remaining HTML tags |
| 119 | + .replace(/<[^>]+>/g, '') |
| 120 | + // Clean up excessive whitespace (but preserve intentional line breaks) |
| 121 | + .replace(/[ \t]+/g, ' ') |
| 122 | + .replace(/ *\n */g, '\n') |
| 123 | + .replace(/\n{3,}/g, '\n\n') |
| 124 | + .trim(); |
| 125 | + |
| 126 | + return markdown; |
| 127 | +} |
| 128 | + |
| 129 | +/** |
| 130 | + * Format prop type for markdown |
| 131 | + */ |
| 132 | +function formatPropType(prop: ApiProp): string { |
| 133 | + let type = prop.type.name; |
| 134 | + |
| 135 | + if (prop.type.description) { |
| 136 | + // Use specialized function for prop type descriptions |
| 137 | + type = formatPropTypeDescription(prop.type.description); |
| 138 | + } |
| 139 | + |
| 140 | + if (prop.signature) { |
| 141 | + type = prop.signature.type; |
| 142 | + } |
| 143 | + |
| 144 | + // Escape pipes in union types for better markdown readability |
| 145 | + type = type.replace(/\s*\|\s*/g, ' \\| '); |
| 146 | + |
| 147 | + // Wrap all prop types in backticks to prevent markdown table issues with pipes |
| 148 | + return `\`${type}\``; |
| 149 | +} |
| 150 | + |
| 151 | +/** |
| 152 | + * Generate props table |
| 153 | + */ |
| 154 | +function generatePropsTable(props: Record<string, ApiProp>): string { |
| 155 | + const propEntries = Object.entries(props); |
| 156 | + if (propEntries.length === 0) { |
| 157 | + return ''; |
| 158 | + } |
| 159 | + |
| 160 | + let table = '## Props\n\n'; |
| 161 | + table += '| Name | Type | Default | Required | Description |\n'; |
| 162 | + table += '|------|------|---------|----------|-------------|\n'; |
| 163 | + |
| 164 | + for (const [propName, prop] of propEntries) { |
| 165 | + const name = prop.deprecated ? `${propName} (deprecated)` : propName; |
| 166 | + const type = formatPropType(prop); |
| 167 | + const defaultValue = prop.default ? `\`${prop.default}\`` : '-'; |
| 168 | + const required = prop.required ? 'Yes' : 'No'; |
| 169 | + |
| 170 | + let description = ''; |
| 171 | + if (prop.deprecated && prop.deprecationInfo) { |
| 172 | + description = `⚠️ ${htmlToMarkdown(prop.deprecationInfo)}`; |
| 173 | + } else if (prop.additionalInfo?.cssApi) { |
| 174 | + description = 'Override or extend the styles applied to the component.'; |
| 175 | + } else if (prop.additionalInfo?.sx) { |
| 176 | + description = |
| 177 | + 'The system prop that allows defining system overrides as well as additional CSS styles.'; |
| 178 | + } |
| 179 | + |
| 180 | + table += `| ${name} | ${type} | ${defaultValue} | ${required} | ${description} |\n`; |
| 181 | + } |
| 182 | + |
| 183 | + return table; |
| 184 | +} |
| 185 | + |
| 186 | +/** |
| 187 | + * Generate slots table |
| 188 | + */ |
| 189 | +function generateSlotsTable(slots: ApiSlot[]): string { |
| 190 | + if (!slots || slots.length === 0) { |
| 191 | + return ''; |
| 192 | + } |
| 193 | + |
| 194 | + let table = '## Slots\n\n'; |
| 195 | + table += '| Name | Default | Class | Description |\n'; |
| 196 | + table += '|------|---------|-------|-------------|\n'; |
| 197 | + |
| 198 | + for (const slot of slots) { |
| 199 | + const className = slot.class ? `\`.${slot.class}\`` : '-'; |
| 200 | + const description = htmlToMarkdown(slot.description); |
| 201 | + table += `| ${slot.name} | \`${slot.default}\` | ${className} | ${description} |\n`; |
| 202 | + } |
| 203 | + |
| 204 | + return table; |
| 205 | +} |
| 206 | + |
| 207 | +/** |
| 208 | + * Generate classes table |
| 209 | + */ |
| 210 | +function generateClassesTable(classes: ApiClass[]): string { |
| 211 | + if (!classes || classes.length === 0) { |
| 212 | + return ''; |
| 213 | + } |
| 214 | + |
| 215 | + let table = '## CSS\n\n'; |
| 216 | + table += '### Rule name\n\n'; |
| 217 | + table += '| Global class | Rule name | Description |\n'; |
| 218 | + table += '|--------------|-----------|-------------|\n'; |
| 219 | + |
| 220 | + for (const cls of classes) { |
| 221 | + const globalClass = cls.isGlobal ? `\`.${cls.className}\`` : '-'; |
| 222 | + const ruleName = cls.isGlobal ? '-' : cls.key; |
| 223 | + const description = htmlToMarkdown(cls.description); |
| 224 | + table += `| ${globalClass} | ${ruleName} | ${description} |\n`; |
| 225 | + } |
| 226 | + |
| 227 | + return table; |
| 228 | +} |
| 229 | + |
| 230 | +/** |
| 231 | + * Process API JSON and convert to markdown |
| 232 | + */ |
| 233 | +export function processApiJson(apiJson: ApiJson | string): string { |
| 234 | + const api: ApiJson = typeof apiJson === 'string' ? JSON.parse(apiJson) : apiJson; |
| 235 | + |
| 236 | + let markdown = `# ${api.name} API\n\n`; |
| 237 | + |
| 238 | + // Add deprecation warning if applicable |
| 239 | + if (api.deprecated) { |
| 240 | + const warningText = api.deprecationInfo |
| 241 | + ? htmlToMarkdown(api.deprecationInfo) |
| 242 | + : 'This component is deprecated. Consider using an alternative component.'; |
| 243 | + markdown += `> ⚠️ **Warning**: ${warningText}\n\n`; |
| 244 | + } |
| 245 | + |
| 246 | + // Add demos section |
| 247 | + if (api.demos) { |
| 248 | + markdown += '## Demos\n\n'; |
| 249 | + markdown += |
| 250 | + 'For examples and details on the usage of this React component, visit the component demo pages:\n\n'; |
| 251 | + markdown += `${htmlToMarkdown(api.demos)}\n\n`; |
| 252 | + } |
| 253 | + |
| 254 | + // Add import section |
| 255 | + markdown += '## Import\n\n'; |
| 256 | + markdown += '```jsx\n'; |
| 257 | + markdown += api.imports.join('\n// or\n'); |
| 258 | + markdown += '\n```\n\n'; |
| 259 | + |
| 260 | + // Add props section |
| 261 | + const propsTable = generatePropsTable(api.props); |
| 262 | + if (propsTable) { |
| 263 | + markdown += `${propsTable}\n`; |
| 264 | + } |
| 265 | + |
| 266 | + // Add ref information |
| 267 | + if (api.forwardsRefTo === null) { |
| 268 | + markdown += '> **Note**: This component cannot hold a ref.\n\n'; |
| 269 | + } else { |
| 270 | + markdown += `> **Note**: The \`ref\` is forwarded to the root element${api.forwardsRefTo ? ` (${api.forwardsRefTo})` : ''}.\n\n`; |
| 271 | + } |
| 272 | + |
| 273 | + // Add spread information |
| 274 | + if (api.spread) { |
| 275 | + const spreadElement = api.inheritance |
| 276 | + ? `[${api.inheritance.component}](${api.inheritance.pathname})` |
| 277 | + : 'native element'; |
| 278 | + markdown += `> Any other props supplied will be provided to the root element (${spreadElement}).\n\n`; |
| 279 | + } |
| 280 | + |
| 281 | + // Add inheritance section |
| 282 | + if (api.inheritance) { |
| 283 | + markdown += '## Inheritance\n\n'; |
| 284 | + markdown += `While not explicitly documented above, the props of the [${api.inheritance.component}](${api.inheritance.pathname}) component are also available on ${api.name}.`; |
| 285 | + if (api.inheritance.component === 'Transition') { |
| 286 | + markdown += |
| 287 | + ' A subset of components support [react-transition-group](https://reactcommunity.org/react-transition-group/transition/) out of the box.'; |
| 288 | + } |
| 289 | + markdown += '\n\n'; |
| 290 | + } |
| 291 | + |
| 292 | + // Add theme default props section |
| 293 | + if (api.themeDefaultProps && api.muiName) { |
| 294 | + markdown += '## Theme default props\n\n'; |
| 295 | + markdown += `You can use \`${api.muiName}\` to change the default props of this component with the theme.\n\n`; |
| 296 | + } |
| 297 | + |
| 298 | + // Add slots section |
| 299 | + const slotsTable = generateSlotsTable(api.slots || []); |
| 300 | + if (slotsTable) { |
| 301 | + markdown += `${slotsTable}\n`; |
| 302 | + } |
| 303 | + |
| 304 | + // Add classes section |
| 305 | + const classesTable = generateClassesTable(api.classes || []); |
| 306 | + if (classesTable) { |
| 307 | + markdown += `${classesTable}\n`; |
| 308 | + } |
| 309 | + |
| 310 | + // Add CSS component note |
| 311 | + if (api.cssComponent) { |
| 312 | + markdown += `> **Note**: As a CSS utility, the \`${api.name}\` component also supports all system properties. You can use them as props directly on the component.\n\n`; |
| 313 | + } |
| 314 | + |
| 315 | + // Add source code section |
| 316 | + if (api.filename) { |
| 317 | + markdown += '## Source code\n\n'; |
| 318 | + markdown += `If you did not find the information on this page, consider having a look at the implementation of the component for more detail.\n\n`; |
| 319 | + markdown += `- [${api.filename}](https://github.com/mui/material-ui/tree/HEAD${api.filename})\n\n`; |
| 320 | + } |
| 321 | + |
| 322 | + return markdown.trim(); |
| 323 | +} |
| 324 | + |
| 325 | +/** |
| 326 | + * Process API JSON file and return markdown |
| 327 | + */ |
| 328 | +export function processApiFile(filePath: string): string { |
| 329 | + const content = fs.readFileSync(filePath, 'utf-8'); |
| 330 | + return processApiJson(content); |
| 331 | +} |
0 commit comments