Skip to content

Commit 1119e25

Browse files
authored
[docs-infra] Add a script to generate Material UI llms.txt and docs markdown. (#46308)
1 parent d90089a commit 1119e25

22 files changed

+1673
-15
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
/docs/export
1717
/docs/pages/playground/
1818
/docs/public/feed/
19+
/docs/public/material-ui/
1920
/examples/**/.cache
2021
/packages/mui-codemod/lib
2122
/packages/mui-envinfo/*.tgz

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
"release:pack": "tsx scripts/releasePack.mts",
2323
"docs:api": "rimraf --glob ./docs/pages/**/api-docs ./docs/pages/**/api && pnpm docs:api:build",
2424
"docs:api:build": "tsx ./scripts/buidApiDocs/index.ts",
25-
"docs:build": "pnpm --filter docs build",
25+
"docs:llms:build": "rimraf --glob ./docs/public/material-ui/ && tsx ./scripts/buildLlmsDocs/index.ts --projectSettings ./packages/api-docs-builder-core/materialUi/projectSettings.ts",
26+
"docs:build": "pnpm docs:llms:build && pnpm --filter docs build",
2627
"docs:build-sw": "pnpm --filter docs build-sw",
2728
"docs:build-color-preview": "babel-node scripts/buildColorTypes",
2829
"docs:deploy": "pnpm --filter docs run deploy",
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Export all functions from both modules
2+
export * from './processComponent';
3+
export * from './processApi';
Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
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(/&lt;/g, '<')
66+
.replace(/&gt;/g, '>')
67+
.replace(/&quot;/g, '"')
68+
.replace(/&#124;/g, '|')
69+
.replace(/&nbsp;/g, ' ')
70+
.replace(/&amp;/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(/&lt;/g, '<')
88+
.replace(/&gt;/g, '>')
89+
.replace(/&quot;/g, '"')
90+
.replace(/&#124;/g, '|')
91+
.replace(/&nbsp;/g, ' ')
92+
.replace(/&amp;/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

Comments
 (0)