Skip to main content
Glama
yun8711
by yun8711
extract-docs.ts17.8 kB
import { dirname, resolve, join } from 'path' import { fileURLToPath } from 'url' import fs from 'fs' import { ComponentModel, ComponentProp, ComponentEvent, ComponentSlot, ComponentMethod, } from '../src/types/index.ts' import { marked } from 'marked' import * as cheerio from 'cheerio' // ============================================= // 配置路径:你的 Element UI 源码路径 // ============================================= // 本项目绝对路径 const projectPath = resolve(dirname(fileURLToPath(import.meta.url)), '../') // 本地element-ui源码路径 const ELEMENT_SRC = resolve(projectPath, '../../opensource/element') // element-ui组件目录 const COMPONENTS_DIR = join(ELEMENT_SRC, 'packages') // element-ui中文文档目录 const DOCS_DIR_ZH = join(ELEMENT_SRC, 'examples/docs/zh-CN') // element-ui web-types.json路径 const WEB_TYPES_PATH = join(ELEMENT_SRC, 'web-types.json') // 输出组件数据文件路径 const OUTPUT = resolve(projectPath, 'src/metadata/components.ts') // 输出文档文件路径 const DOCS_OUTPUT_DIR = resolve(projectPath, 'src/examples') // ========================================================= // 读取 web-types.json // ========================================================= function loadWebTypes() { if (!fs.existsSync(WEB_TYPES_PATH)) { console.warn('⚠️ web-types.json not found:', WEB_TYPES_PATH) return null } return JSON.parse(fs.readFileSync(WEB_TYPES_PATH, 'utf-8')) } // ========================================================= // 读取所有中文 markdown 文档 // ========================================================= function readMarkdownDocs() { const files = fs.readdirSync(DOCS_DIR_ZH).filter(f => f.endsWith('.md')) const docs: Record<string, string> = {} files.forEach(file => { const content = fs.readFileSync(join(DOCS_DIR_ZH, file), 'utf-8') const name = file.replace('.md', '') docs[name] = content }) return docs } // ========================================================= // 从 markdown 提取描述、demo 区块、methods(使用 marked + cheerio) // ========================================================= function parseMarkdown(md: string, componentName?: string) { // 1. Markdown 转 HTML(同步模式) const html = marked.parse(md, { async: false }) as string const $ = cheerio.load(html) let description = '' const examples: any[] = [] const methodsMap: Record<string, ComponentMethod[]> = {} const eventsMap: Record<string, ComponentEvent[]> = {} // 2. 提取描述(从第一个段落获取) const firstParagraph = $('p').first().text().trim() if (firstParagraph) { description = firstParagraph } // 3. 提取示例代码(从 :::demo 块) $('pre code').each((index, element) => { const code = $(element).text().trim() if (code) { examples.push({ title: `示例 ${index + 1}`, code: code, }) } }) // 4. 提取方法信息 let currentComponentType = componentName ? componentName.replace('el-', '') : 'default' $('h3').each((index, heading) => { const headingText = $(heading).text().trim() // 检测 Methods 或 Events 标题(支持多种格式) const isMethodsSection = headingText.includes('方法') || headingText.includes('Methods') const isEventsSection = headingText.includes('Events') || headingText.includes('事件') if (isMethodsSection || isEventsSection) { const isMethods = isMethodsSection // 确定组件类型(从标题中提取,或使用当前组件类型) if (headingText.includes('Table') || headingText.includes('表格')) { currentComponentType = 'table' } else if (headingText.includes('Input') || headingText.includes('输入框')) { currentComponentType = 'input' } else if (headingText.includes('Form') || headingText.includes('表单')) { currentComponentType = 'form' } else if (headingText.includes('Menu') || headingText.includes('菜单')) { currentComponentType = 'menu' } else if (headingText.includes('Statistic') || headingText.includes('统计')) { currentComponentType = 'statistic' } else if (headingText.includes('Carousel') || headingText.includes('轮播')) { currentComponentType = 'carousel' } else if (headingText.includes('Cascader') && headingText.includes('Panel')) { currentComponentType = 'cascader-panel' } else if (headingText.includes('Cascader')) { currentComponentType = 'cascader' } else if (headingText.includes('Tree') || headingText.includes('树')) { currentComponentType = 'tree' } // 对于通用标题如 "### 方法" 或 "### Events",保持当前的 currentComponentType // 查找后面的表格(可能不直接相邻) let tableElement = $(heading).next('table') if (tableElement.length === 0) { // 如果直接下一个不是表格,查找后面的兄弟元素 let sibling = $(heading).next() while (sibling.length > 0 && !sibling.is('table')) { sibling = sibling.next() } if (sibling.is('table')) { tableElement = sibling } } if (tableElement.length > 0) { if (isMethods) { const methods = parseMethodsTableFromHTML(tableElement, $) if (!methodsMap[currentComponentType]) { methodsMap[currentComponentType] = [] } methodsMap[currentComponentType].push(...methods) } else { const events = parseEventsTableFromHTML(tableElement, $) if (!eventsMap[currentComponentType]) { eventsMap[currentComponentType] = [] } eventsMap[currentComponentType].push(...events) } } } }) return { description, examples, methodsMap, eventsMap } } // 从 HTML 表格中解析方法信息 function parseMethodsTableFromHTML(tableElement: cheerio.Cheerio<any>, $: cheerio.CheerioAPI): ComponentMethod[] { const methods: ComponentMethod[] = [] // 检查表头是否是方法表格(支持多种格式) const headers = tableElement.find('thead th').map((i, th) => $(th).text().trim()).get() const isMethodsTable = headers.length >= 3 && (headers[0] === '方法名' || headers[0] === 'Method') && (headers[1] === '说明' || headers[1] === 'Description') && (headers[2] === '参数' || headers[2] === 'Parameters') if (isMethodsTable) { // 解析表格行 tableElement.find('tbody tr').each((rowIndex, row) => { const cells = $(row).find('td').map((i, td) => $(td).text().trim()).get() if (cells.length >= 3) { const methodName = cells[0] const methodDescription = cells[1] const methodParameters = cells[2] // 跳过空行或无效行 if (methodName && methodName !== '方法名' && methodName !== 'Method' && methodName !== '----' && methodName !== '---') { const method: ComponentMethod = { name: methodName, description: methodDescription || '', parameters: methodParameters && methodParameters !== '—' && methodParameters !== '-' && methodParameters !== 'N/A' ? [{ raw: methodParameters }] : [], } methods.push(method) } } }) } return methods } // 从 HTML 表格中解析事件信息 function parseEventsTableFromHTML(tableElement: cheerio.Cheerio<any>, $: cheerio.CheerioAPI): ComponentEvent[] { const events: ComponentEvent[] = [] // 检查表头是否是事件表格(支持多种格式) const headers = tableElement.find('thead th').map((i, th) => $(th).text().trim()).get() const isEventsTable = headers.length >= 3 && (headers[0] === '事件名' || headers[0] === '事件名称' || headers[0] === 'Event') && (headers[1] === '说明' || headers[1] === 'Description') && (headers[2] === '参数' || headers[2] === '回调参数' || headers[2] === 'Parameters') if (isEventsTable) { // 解析表格行 tableElement.find('tbody tr').each((rowIndex, row) => { const cells = $(row).find('td').map((i, td) => $(td).text().trim()).get() if (cells.length >= 3) { const eventName = cells[0] const eventDescription = cells[1] const eventParameters = cells[2] // 跳过空行或无效行 if (eventName && eventName !== '事件名' && eventName !== '事件名称' && eventName !== 'Event' && eventName !== '----' && eventName !== '---') { const event: ComponentEvent = { name: eventName, description: eventDescription || '', parameters: eventParameters && eventParameters !== '—' && eventParameters !== '-' && eventParameters !== 'N/A' ? [{ raw: eventParameters }] : [], } events.push(event) } } }) } return events } // 过滤 markdown 内容,移除 API 表格部分,只保留代码示例 function filterMarkdownContent(md: string): string { const lines = md.split('\n') const filteredLines: string[] = [] let inApiSection = false let inCodeBlock = false let skipNextTable = false for (let i = 0; i < lines.length; i++) { const line = lines[i] const trimmedLine = line.trim() // 检测代码块开始/结束 if (trimmedLine.startsWith('```')) { inCodeBlock = !inCodeBlock filteredLines.push(line) continue } // 在代码块内,保留所有内容 if (inCodeBlock) { filteredLines.push(line) continue } // 检测 API 相关标题 if (trimmedLine.match(/^###\s+(Attributes|Props|属性|Events|事件|Methods|方法|Slots|插槽|Slot)/i) || trimmedLine.match(/^###\s+(Table|Input|Form|Menu|Statistic|Carousel|Cascader|Tree|Upload)?\s*(Attributes|Props|属性|Events|事件|Methods|方法|Slots|插槽|Slot)/i)) { inApiSection = true skipNextTable = true // 不添加这个标题行 continue } // 如果在 API 部分 if (inApiSection) { // 检测表格开始 if (trimmedLine.includes('|') && skipNextTable) { // 跳过表格相关行,直到下一个标题或代码块 continue } // 检测下一个主要标题(跳出 API 部分) if (trimmedLine.match(/^###\s/) && !trimmedLine.match(/^###\s+(Attributes|Props|属性|Events|事件|Methods|方法|Slots|插槽|Slot)/i)) { inApiSection = false skipNextTable = false } // 检测 :::demo 块(示例代码),即使在 API 部分也要保留 if (trimmedLine.includes(':::demo')) { inApiSection = false // 临时跳出 API 部分以保留示例 filteredLines.push(line) continue } // 检测空行或分隔行,可能是表格结束 if (trimmedLine === '' || trimmedLine.match(/^[-+=*_]{3,}$/)) { skipNextTable = false continue } // 跳过 API 部分的普通文本 continue } // 检测 :::demo 块开始 if (trimmedLine.includes(':::demo')) { filteredLines.push(line) continue } // 保留其他内容(描述、示例等) filteredLines.push(line) } return filteredLines.join('\n') } // ========================================================= // 解析 d.ts 文件:仅提取 props, events // ========================================================= function parseDTS(dtsPath: string) { if (!fs.existsSync(dtsPath)) { return { props: [], events: [], slots: [] } } const text = fs.readFileSync(dtsPath, 'utf-8') const props: ComponentProp[] = [] const events: ComponentEvent[] = [] // 改进的属性解析:只匹配简单类型,不包含复杂对象或函数 const propRegex = /^\s+(\w+)\??:\s*([^;{}=>\n]+)/gm let m while ((m = propRegex.exec(text))) { const propName = m[1] let typeStr = m[2].trim() // 跳过构造函数、方法等 if (typeStr.includes('(') || typeStr.includes('new ')) { continue } // 清理类型字符串,移除注释和多余空格 typeStr = typeStr.replace(/\s*\*.*$/gm, '').trim() props.push({ name: propName, type: { raw: typeStr }, required: !text.includes(`${propName}?:`), }) } // 事件解析保持不变 const eventRegex = /on(\w+)\??:\s*\(([^)]+)\)/g while ((m = eventRegex.exec(text))) { const eventName = m[1].toLowerCase() events.push({ name: eventName, parameters: [{ raw: m[2] }], ts: m[0], }) } return { props, events, slots: [] } } // ========================================================= // 合并 props(优先级:web-types > d.ts) // ========================================================= function mergeProps( webProps: any[], dtsProps: ComponentProp[] ): ComponentProp[] { const merged: ComponentProp[] = [] for (const wp of webProps) { const dts = dtsProps.find(p => p.name === wp.name) merged.push({ name: wp.name, description: wp.description ?? dts?.description, required: wp.required ?? dts?.required, default: wp.default, type: { raw: dts?.type.raw ?? wp.type, }, }) } return merged } // ========================================================= // 合并 events(优先级:markdown > web-types > d.ts) // ========================================================= function mergeEvents( webEvents: any[], dtsEvents: ComponentEvent[], mdEvents: ComponentEvent[] = [] ): ComponentEvent[] { const merged: ComponentEvent[] = [] // 首先添加 markdown 中的事件 for (const mdEvent of mdEvents) { merged.push(mdEvent) } // 然后添加 web-types 的事件(如果没有重复) for (const we of webEvents) { const existing = merged.find(e => e.name === we.name) if (!existing) { const dts = dtsEvents.find(e => e.name === we.name) merged.push({ name: we.name, description: we.description, parameters: dts?.parameters ?? [{ raw: we.type }], ts: dts?.ts, }) } } // 最后添加 d.ts 中的事件(如果没有重复) for (const dtsEvent of dtsEvents) { const existing = merged.find(e => e.name === dtsEvent.name) if (!existing) { merged.push(dtsEvent) } } return merged } // ========================================================= // 主流程 // ========================================================= function generate() { console.log('📚 Loading web-types...') const webTypes = loadWebTypes() console.log('📄 Loading markdown...') const markdowns = readMarkdownDocs() console.log('📦 Reading component dirs...') const componentNames = fs .readdirSync(COMPONENTS_DIR) .filter(name => fs.statSync(join(COMPONENTS_DIR, name)).isDirectory()) const components: Record<string, ComponentModel> = {} for (const comp of componentNames) { const compDir = join(COMPONENTS_DIR, comp) // --- d.ts --- const dtsPath = join(ELEMENT_SRC, 'types', `${comp}.d.ts`) // --- web-types --- const wtEntry = webTypes.contributions.html['vue-components'].find( (el: any) => el.name.toLowerCase() === `el-${comp}` || el.name === `El${comp.charAt(0).toUpperCase() + comp.slice(1)}` ) // --- 组件名称 --- const componentName = `el-${comp}` // --- markdown --- const md = markdowns[comp] ?? '' const mdParsed = parseMarkdown(md, componentName) const dts = parseDTS(dtsPath) const props = wtEntry ? mergeProps(wtEntry.props ?? [], dts.props) : dts.props // 从 markdown 中解析的事件 const mdEvents = mdParsed.eventsMap[comp] || mdParsed.eventsMap['default'] || [] const events = wtEntry ? mergeEvents(wtEntry.js?.events ?? wtEntry.events ?? [], dts.events, mdEvents) : mergeEvents([], dts.events, mdEvents) const slots: ComponentSlot[] = wtEntry?.slots?.map((s: any) => ({ name: s.name, description: s.description, })) ?? [] const docFileName = `${componentName}.md` const docFilePath = join(DOCS_OUTPUT_DIR, docFileName) fs.mkdirSync(DOCS_OUTPUT_DIR, { recursive: true }) // 过滤 markdown 内容,移除 API 表格,只保留代码示例 const filteredMd = filterMarkdownContent(md) fs.writeFileSync(docFilePath, filteredMd, 'utf-8') // --- 复制 d.ts 文件 --- if (fs.existsSync(dtsPath)) { const dtsFileName = `${componentName}.d.ts` const dtsOutputPath = join(DOCS_OUTPUT_DIR, dtsFileName) fs.copyFileSync(dtsPath, dtsOutputPath) } // --- 构建 ComponentModel --- components[componentName] = { tagName: componentName, description: wtEntry?.description ?? mdParsed.description, // https://element.eleme.cn/#/zh-CN/component/border docUrl: `https://element.eleme.cn/#/zh-CN/component/${componentName.replace( 'el-', '' )}`, props, events, slots, methods: mdParsed.methodsMap[comp] || mdParsed.methodsMap['default'] || [], // 从 markdown 文档中解析的方法信息 } } fs.mkdirSync(dirname(OUTPUT), { recursive: true }) // 输出为 ES 模块,直接导出 components 对象 const jsContent = `// Auto-generated by extract-docs.ts // Do not edit this file manually export default ${JSON.stringify(components, null, 2)} ` fs.writeFileSync(OUTPUT, jsContent, 'utf-8') console.log('🎉 Done! Output generated:', OUTPUT) } generate()

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/yun8711/element-ui-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server