diff --git a/docs/app/components/PageHeaderLinks.vue b/docs/app/components/PageHeaderLinks.vue new file mode 100644 index 00000000..ed790589 --- /dev/null +++ b/docs/app/components/PageHeaderLinks.vue @@ -0,0 +1,77 @@ + + + diff --git a/docs/app/pages/[...slug].vue b/docs/app/pages/[...slug].vue index 8492b53e..e7d0e022 100644 --- a/docs/app/pages/[...slug].vue +++ b/docs/app/pages/[...slug].vue @@ -141,7 +141,7 @@ const communityLinks = computed(() => [{ - diff --git a/docs/server/plugins/llms.ts b/docs/server/plugins/llms.ts index c5fbdec7..5e58bddf 100644 --- a/docs/server/plugins/llms.ts +++ b/docs/server/plugins/llms.ts @@ -1,412 +1,8 @@ -import json5 from 'json5' -import { camelCase, kebabCase } from 'scule' -import { visit } from '@nuxt/content/runtime' import type { H3Event } from 'h3' import type { PageCollectionItemBase } from '@nuxt/content' -import * as theme from '../../.nuxt/ui' -import * as themePro from '../../.nuxt/ui-pro' -import meta from '#nuxt-component-meta' -// @ts-expect-error - no types available -import components from '#component-example/nitro' - -type ComponentAttributes = { - ':pro'?: string - ':prose'?: string - ':props'?: string - ':external'?: string - ':externalTypes'?: string - ':ignore'?: string - ':hide'?: string - ':slots'?: string -} - -type ThemeConfig = { - pro: boolean - prose: boolean - componentName: string -} - -type CodeConfig = { - pro: boolean - props: Record - external: string[] - externalTypes: string[] - ignore: string[] - hide: string[] - componentName: string - slots?: Record -} - -type Document = { - title: string - body: any -} - -const parseBoolean = (value?: string): boolean => value === 'true' - -function getComponentMeta(componentName: string) { - const pascalCaseName = componentName.charAt(0).toUpperCase() + componentName.slice(1) - - const strategies = [ - `U${pascalCaseName}`, - `Prose${pascalCaseName}`, - pascalCaseName - ] - - let componentMeta: any - let finalMetaComponentName: string = pascalCaseName - - for (const nameToTry of strategies) { - finalMetaComponentName = nameToTry - const metaAttempt = (meta as Record)[nameToTry]?.meta - if (metaAttempt) { - componentMeta = metaAttempt - break - } - } - - if (!componentMeta) { - console.warn(`[getComponentMeta] Metadata not found for ${pascalCaseName} using strategies: U, Prose, or no prefix. Last tried: ${finalMetaComponentName}`) - } - - return { - pascalCaseName, - metaComponentName: finalMetaComponentName, - componentMeta - } -} - -function replaceNodeWithPre(node: any[], language: string, code: string, filename?: string) { - node[0] = 'pre' - node[1] = { language, code } - if (filename) node[1].filename = filename -} - -function visitAndReplace(doc: Document, type: string, handler: (node: any[]) => void) { - visit(doc.body, (node) => { - if (Array.isArray(node) && node[0] === type) { - handler(node) - } - return true - }, node => node) -} - -function generateTSInterface( - name: string, - items: any[], - itemHandler: (item: any) => string, - description: string -) { - let code = `/**\n * ${description}\n */\ninterface ${name} {\n` - for (const item of items) { - code += itemHandler(item) - } - code += `}` - return code -} - -function propItemHandler(propValue: any): string { - if (!propValue?.name) return '' - const propName = propValue.name - const propType = propValue.type - ? Array.isArray(propValue.type) - ? propValue.type.map((t: any) => t.name || t).join(' | ') - : propValue.type.name || propValue.type - : 'any' - const isRequired = propValue.required || false - const hasDescription = propValue.description && propValue.description.trim().length > 0 - const hasDefault = propValue.default !== undefined - let result = '' - if (hasDescription || hasDefault) { - result += ` /**\n` - if (hasDescription) { - const descLines = propValue.description.split(/\r?\n/) - descLines.forEach((line: string) => { - result += ` * ${line}\n` - }) - } - if (hasDefault) { - let defaultValue = propValue.default - if (typeof defaultValue === 'string') { - defaultValue = `"${defaultValue.replace(/"/g, '\\"')}"` - } else { - defaultValue = JSON.stringify(defaultValue) - } - result += ` * @default ${defaultValue}\n` - } - result += ` */\n` - } - result += ` ${propName}${isRequired ? '' : '?'}: ${propType};\n` - return result -} - -function slotItemHandler(slotValue: any): string { - if (!slotValue?.name) return '' - const slotName = slotValue.name - const hasDescription = slotValue.description && slotValue.description.trim().length > 0 - let result = '' - if (hasDescription) { - result += ` /**\n` - const descLines = slotValue.description.split(/\r?\n/) - descLines.forEach((line: string) => { - result += ` * ${line}\n` - }) - result += ` */\n` - } - if (slotValue.bindings && Object.keys(slotValue.bindings).length > 0) { - let bindingsType = '{\n' - Object.entries(slotValue.bindings).forEach(([bindingName, bindingValue]: [string, any]) => { - const bindingType = bindingValue.type || 'any' - bindingsType += ` ${bindingName}: ${bindingType};\n` - }) - bindingsType += ' }' - result += ` ${slotName}(bindings: ${bindingsType}): any;\n` - } else { - result += ` ${slotName}(): any;\n` - } - return result -} - -function emitItemHandler(event: any): string { - if (!event?.name) return '' - let payloadType = 'void' - if (event.type) { - payloadType = Array.isArray(event.type) - ? event.type.map((t: any) => t.name || t).join(' | ') - : event.type.name || event.type - } - let result = '' - if (event.description && event.description.trim().length > 0) { - result += ` /**\n` - event.description.split(/\r?\n/).forEach((line: string) => { - result += ` * ${line}\n` - }) - result += ` */\n` - } - result += ` ${event.name}: (payload: ${payloadType}) => void;\n` - return result -} - -const generateThemeConfig = ({ pro, prose, componentName }: ThemeConfig) => { - const computedTheme = pro ? (prose ? themePro.prose : themePro) : theme - const componentTheme = computedTheme[componentName as keyof typeof computedTheme] - - return { - [pro ? 'uiPro' : 'ui']: prose - ? { prose: { [componentName]: componentTheme } } - : { [componentName]: componentTheme } - } -} - -const generateComponentCode = ({ - pro, - props, - external, - externalTypes, - hide, - componentName, - slots -}: CodeConfig) => { - const filteredProps = Object.fromEntries( - Object.entries(props).filter(([key]) => !hide.includes(key)) - ) - - const imports = pro - ? '' - : external - .filter((_, index) => externalTypes[index] && externalTypes[index] !== 'undefined') - .map((ext, index) => { - const type = externalTypes[index]?.replace(/[[\]]/g, '') - return `import type { ${type} } from '@nuxt/${pro ? 'ui-pro' : 'ui'}'` - }) - .join('\n') - - let itemsCode = '' - if (props.items) { - itemsCode = pro - ? `const items = ref(${json5.stringify(props.items, null, 2)})` - : `const items = ref<${externalTypes[0]}>(${json5.stringify(props.items, null, 2)})` - delete filteredProps.items - } - - let calendarValueCode = '' - if (componentName === 'calendar' && props.modelValue && Array.isArray(props.modelValue)) { - calendarValueCode = `const value = ref(new CalendarDate(${props.modelValue.join(', ')}))` - } - - const propsString = Object.entries(filteredProps) - .map(([key, value]) => { - const formattedKey = kebabCase(key) - if (typeof value === 'string') { - return `${formattedKey}="${value}"` - } else if (typeof value === 'number') { - return `:${formattedKey}="${value}"` - } else if (typeof value === 'boolean') { - return value ? formattedKey : `:${formattedKey}="false"` - } - return '' - }) - .filter(Boolean) - .join(' ') - - const itemsProp = props.items ? ':items="items"' : '' - const vModelProp = componentName === 'calendar' && props.modelValue ? 'v-model="value"' : '' - const allProps = [propsString, itemsProp, vModelProp].filter(Boolean).join(' ') - const formattedProps = allProps ? ` ${allProps}` : '' - - let scriptSetup = '' - if (imports || itemsCode || calendarValueCode) { - scriptSetup = '\n\n' - } - - let componentContent = '' - let slotContent = '' - - if (slots && Object.keys(slots).length > 0) { - const defaultSlot = slots.default?.trim() - if (defaultSlot) { - const indentedContent = defaultSlot - .split('\n') - .map(line => line.trim() ? ` ${line}` : line) - .join('\n') - componentContent = `\n${indentedContent}\n ` - } - - Object.entries(slots).forEach(([slotName, content]) => { - if (slotName !== 'default' && content?.trim()) { - const indentedSlotContent = content.trim() - .split('\n') - .map(line => line.trim() ? ` ${line}` : line) - .join('\n') - slotContent += `\n ` - } - }) - } - - const pascalCaseName = componentName.charAt(0).toUpperCase() + componentName.slice(1) - - let componentTemplate = '' - if (componentContent || slotContent) { - componentTemplate = `${componentContent}${slotContent}` // Removed space before closing tag - } else { - componentTemplate = `` - } - - return `${scriptSetup}` -} export default defineNitroPlugin((nitroApp) => { nitroApp.hooks.hook('content:llms:generate:document', async (_: H3Event, doc: PageCollectionItemBase) => { - const componentName = camelCase(doc.title) - - visitAndReplace(doc, 'component-theme', (node) => { - const attributes = node[1] as Record - const mdcSpecificName = attributes?.slug - - const finalComponentName = mdcSpecificName ? camelCase(mdcSpecificName) : componentName - - const pro = parseBoolean(attributes[':pro']) - const prose = parseBoolean(attributes[':prose']) - const appConfig = generateThemeConfig({ pro, prose, componentName: finalComponentName }) - - replaceNodeWithPre( - node, - 'ts', - `export default defineAppConfig(${json5.stringify(appConfig, null, 2)?.replace(/,([ |\t\n]+[}|\])])/g, '$1')})`, - 'app.config.ts' - ) - }) - - visitAndReplace(doc, 'component-code', (node) => { - const attributes = node[1] as ComponentAttributes - const pro = parseBoolean(attributes[':pro']) - const props = attributes[':props'] ? json5.parse(attributes[':props']) : {} - const external = attributes[':external'] ? json5.parse(attributes[':external']) : [] - const externalTypes = attributes[':externalTypes'] ? json5.parse(attributes[':externalTypes']) : [] - const ignore = attributes[':ignore'] ? json5.parse(attributes[':ignore']) : [] - const hide = attributes[':hide'] ? json5.parse(attributes[':hide']) : [] - const slots = attributes[':slots'] ? json5.parse(attributes[':slots']) : {} - - const code = generateComponentCode({ - pro, - props, - external, - externalTypes, - ignore, - hide, - componentName, - slots - }) - - replaceNodeWithPre(node, 'vue', code) - }) - - visitAndReplace(doc, 'component-props', (node) => { - const attributes = node[1] as Record - const mdcSpecificName = attributes?.name - const isProse = parseBoolean(attributes[':prose']) - - const finalComponentName = mdcSpecificName ? camelCase(mdcSpecificName) : componentName - - const { pascalCaseName, componentMeta } = getComponentMeta(finalComponentName) - - if (!componentMeta?.props) return - - const interfaceName = isProse ? `Prose${pascalCaseName}Props` : `${pascalCaseName}Props` - - const interfaceCode = generateTSInterface( - interfaceName, - Object.values(componentMeta.props), - propItemHandler, - `Props for the ${isProse ? 'Prose' : ''}${pascalCaseName} component` - ) - replaceNodeWithPre(node, 'ts', interfaceCode) - }) - - visitAndReplace(doc, 'component-slots', (node) => { - const { pascalCaseName, componentMeta } = getComponentMeta(componentName) - if (!componentMeta?.slots) return - - const interfaceCode = generateTSInterface( - `${pascalCaseName}Slots`, - Object.values(componentMeta.slots), - slotItemHandler, - `Slots for the ${pascalCaseName} component` - ) - replaceNodeWithPre(node, 'ts', interfaceCode) - }) - - visitAndReplace(doc, 'component-emits', (node) => { - const { pascalCaseName, componentMeta } = getComponentMeta(componentName) - const hasEvents = componentMeta?.events && Object.keys(componentMeta.events).length > 0 - - if (hasEvents) { - const interfaceCode = generateTSInterface( - `${pascalCaseName}Emits`, - Object.values(componentMeta.events), - emitItemHandler, - `Emitted events for the ${pascalCaseName} component` - ) - replaceNodeWithPre(node, 'ts', interfaceCode) - } else { - node[0] = 'p' - node[1] = {} - node[2] = 'No events available for this component.' - } - }) - - visitAndReplace(doc, 'component-example', (node) => { - const camelName = camelCase(node[1]['name']) - const name = camelName.charAt(0).toUpperCase() + camelName.slice(1) - const code = components[name].code - replaceNodeWithPre(node, 'vue', code, `${name}.vue`) - }) + transformMDC(doc as any) }) }) diff --git a/docs/server/routes/raw/[...slug].md.get.ts b/docs/server/routes/raw/[...slug].md.get.ts new file mode 100644 index 00000000..7c98fbf8 --- /dev/null +++ b/docs/server/routes/raw/[...slug].md.get.ts @@ -0,0 +1,30 @@ +import { stringify } from 'minimark/stringify' +import { withLeadingSlash } from 'ufo' + +export default eventHandler(async (event) => { + const slug = getRouterParams(event)['slug.md'] + if (!slug?.endsWith('.md')) { + throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true }) + } + + const path = withLeadingSlash(slug.replace('.md', '')) + // @ts-expect-error TODO: fix this + const page = await queryCollection(event, 'content').path(path).first() + if (!page) { + throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true }) + } + + // Add title and description to the top of the page if missing + if (page.body.value[0]?.[0] !== 'h1') { + page.body.value.unshift(['blockquote', {}, page.description]) + page.body.value.unshift(['h1', {}, page.title]) + } + + const transformedPage = transformMDC({ + title: page.title, + body: page.body + }) + + setHeader(event, 'Content-Type', 'text/markdown; charset=utf-8') + return stringify({ ...transformedPage.body, type: 'minimark' }, { format: 'markdown/html' }) +}) diff --git a/docs/server/utils/transformMDC.ts b/docs/server/utils/transformMDC.ts new file mode 100644 index 00000000..a43baf2d --- /dev/null +++ b/docs/server/utils/transformMDC.ts @@ -0,0 +1,410 @@ +import json5 from 'json5' +import { camelCase, kebabCase } from 'scule' +import { visit } from '@nuxt/content/runtime' +import * as theme from '../../.nuxt/ui' +import * as themePro from '../../.nuxt/ui-pro' +import meta from '#nuxt-component-meta' +// @ts-expect-error - no types available +import components from '#component-example/nitro' + +type ComponentAttributes = { + ':pro'?: string + ':prose'?: string + ':props'?: string + ':external'?: string + ':externalTypes'?: string + ':ignore'?: string + ':hide'?: string + ':slots'?: string +} + +type ThemeConfig = { + pro: boolean + prose: boolean + componentName: string +} + +type CodeConfig = { + pro: boolean + props: Record + external: string[] + externalTypes: string[] + ignore: string[] + hide: string[] + componentName: string + slots?: Record +} + +type Document = { + title: string + body: any +} + +const parseBoolean = (value?: string): boolean => value === 'true' + +function getComponentMeta(componentName: string) { + const pascalCaseName = componentName.charAt(0).toUpperCase() + componentName.slice(1) + + const strategies = [ + `U${pascalCaseName}`, + `Prose${pascalCaseName}`, + pascalCaseName + ] + + let componentMeta: any + let finalMetaComponentName: string = pascalCaseName + + for (const nameToTry of strategies) { + finalMetaComponentName = nameToTry + const metaAttempt = (meta as Record)[nameToTry]?.meta + if (metaAttempt) { + componentMeta = metaAttempt + break + } + } + + if (!componentMeta) { + console.warn(`[getComponentMeta] Metadata not found for ${pascalCaseName} using strategies: U, Prose, or no prefix. Last tried: ${finalMetaComponentName}`) + } + + return { + pascalCaseName, + metaComponentName: finalMetaComponentName, + componentMeta + } +} + +function replaceNodeWithPre(node: any[], language: string, code: string, filename?: string) { + node[0] = 'pre' + node[1] = { language, code } + if (filename) node[1].filename = filename +} + +function visitAndReplace(doc: Document, type: string, handler: (node: any[]) => void) { + visit(doc.body, (node) => { + if (Array.isArray(node) && node[0] === type) { + handler(node) + } + return true + }, node => node) +} + +function generateTSInterface( + name: string, + items: any[], + itemHandler: (item: any) => string, + description: string +) { + let code = `/**\n * ${description}\n */\ninterface ${name} {\n` + for (const item of items) { + code += itemHandler(item) + } + code += `}` + return code +} + +function propItemHandler(propValue: any): string { + if (!propValue?.name) return '' + const propName = propValue.name + const propType = propValue.type + ? Array.isArray(propValue.type) + ? propValue.type.map((t: any) => t.name || t).join(' | ') + : propValue.type.name || propValue.type + : 'any' + const isRequired = propValue.required || false + const hasDescription = propValue.description && propValue.description.trim().length > 0 + const hasDefault = propValue.default !== undefined + let result = '' + if (hasDescription || hasDefault) { + result += ` /**\n` + if (hasDescription) { + const descLines = propValue.description.split(/\r?\n/) + descLines.forEach((line: string) => { + result += ` * ${line}\n` + }) + } + if (hasDefault) { + let defaultValue = propValue.default + if (typeof defaultValue === 'string') { + defaultValue = `"${defaultValue.replace(/"/g, '\\"')}"` + } else { + defaultValue = JSON.stringify(defaultValue) + } + result += ` * @default ${defaultValue}\n` + } + result += ` */\n` + } + result += ` ${propName}${isRequired ? '' : '?'}: ${propType};\n` + return result +} + +function slotItemHandler(slotValue: any): string { + if (!slotValue?.name) return '' + const slotName = slotValue.name + const hasDescription = slotValue.description && slotValue.description.trim().length > 0 + let result = '' + if (hasDescription) { + result += ` /**\n` + const descLines = slotValue.description.split(/\r?\n/) + descLines.forEach((line: string) => { + result += ` * ${line}\n` + }) + result += ` */\n` + } + if (slotValue.bindings && Object.keys(slotValue.bindings).length > 0) { + let bindingsType = '{\n' + Object.entries(slotValue.bindings).forEach(([bindingName, bindingValue]: [string, any]) => { + const bindingType = bindingValue.type || 'any' + bindingsType += ` ${bindingName}: ${bindingType};\n` + }) + bindingsType += ' }' + result += ` ${slotName}(bindings: ${bindingsType}): any;\n` + } else { + result += ` ${slotName}(): any;\n` + } + return result +} + +function emitItemHandler(event: any): string { + if (!event?.name) return '' + let payloadType = 'void' + if (event.type) { + payloadType = Array.isArray(event.type) + ? event.type.map((t: any) => t.name || t).join(' | ') + : event.type.name || event.type + } + let result = '' + if (event.description && event.description.trim().length > 0) { + result += ` /**\n` + event.description.split(/\r?\n/).forEach((line: string) => { + result += ` * ${line}\n` + }) + result += ` */\n` + } + result += ` ${event.name}: (payload: ${payloadType}) => void;\n` + return result +} + +const generateThemeConfig = ({ pro, prose, componentName }: ThemeConfig) => { + const computedTheme = pro ? (prose ? themePro.prose : themePro) : theme + const componentTheme = computedTheme[componentName as keyof typeof computedTheme] + + return { + [pro ? 'uiPro' : 'ui']: prose + ? { prose: { [componentName]: componentTheme } } + : { [componentName]: componentTheme } + } +} + +const generateComponentCode = ({ + pro, + props, + external, + externalTypes, + hide, + componentName, + slots +}: CodeConfig) => { + const filteredProps = Object.fromEntries( + Object.entries(props).filter(([key]) => !hide.includes(key)) + ) + + const imports = pro + ? '' + : external + .filter((_, index) => externalTypes[index] && externalTypes[index] !== 'undefined') + .map((ext, index) => { + const type = externalTypes[index]?.replace(/[[\]]/g, '') + return `import type { ${type} } from '@nuxt/${pro ? 'ui-pro' : 'ui'}'` + }) + .join('\n') + + let itemsCode = '' + if (props.items) { + itemsCode = pro + ? `const items = ref(${json5.stringify(props.items, null, 2)})` + : `const items = ref<${externalTypes[0]}>(${json5.stringify(props.items, null, 2)})` + delete filteredProps.items + } + + let calendarValueCode = '' + if (componentName === 'calendar' && props.modelValue && Array.isArray(props.modelValue)) { + calendarValueCode = `const value = ref(new CalendarDate(${props.modelValue.join(', ')}))` + } + + const propsString = Object.entries(filteredProps) + .map(([key, value]) => { + const formattedKey = kebabCase(key) + if (typeof value === 'string') { + return `${formattedKey}="${value}"` + } else if (typeof value === 'number') { + return `:${formattedKey}="${value}"` + } else if (typeof value === 'boolean') { + return value ? formattedKey : `:${formattedKey}="false"` + } + return '' + }) + .filter(Boolean) + .join(' ') + + const itemsProp = props.items ? ':items="items"' : '' + const vModelProp = componentName === 'calendar' && props.modelValue ? 'v-model="value"' : '' + const allProps = [propsString, itemsProp, vModelProp].filter(Boolean).join(' ') + const formattedProps = allProps ? ` ${allProps}` : '' + + let scriptSetup = '' + if (imports || itemsCode || calendarValueCode) { + scriptSetup = '\n\n' + } + + let componentContent = '' + let slotContent = '' + + if (slots && Object.keys(slots).length > 0) { + const defaultSlot = slots.default?.trim() + if (defaultSlot) { + const indentedContent = defaultSlot + .split('\n') + .map(line => line.trim() ? ` ${line}` : line) + .join('\n') + componentContent = `\n${indentedContent}\n ` + } + + Object.entries(slots).forEach(([slotName, content]) => { + if (slotName !== 'default' && content?.trim()) { + const indentedSlotContent = content.trim() + .split('\n') + .map(line => line.trim() ? ` ${line}` : line) + .join('\n') + slotContent += `\n ` + } + }) + } + + const pascalCaseName = componentName.charAt(0).toUpperCase() + componentName.slice(1) + + let componentTemplate = '' + if (componentContent || slotContent) { + componentTemplate = `${componentContent}${slotContent}` // Removed space before closing tag + } else { + componentTemplate = `` + } + + return `${scriptSetup}` +} + +export function transformMDC(doc: Document): Document { + const componentName = camelCase(doc.title) + + visitAndReplace(doc, 'component-theme', (node) => { + const attributes = node[1] as Record + const mdcSpecificName = attributes?.slug + + const finalComponentName = mdcSpecificName ? camelCase(mdcSpecificName) : componentName + + const pro = parseBoolean(attributes[':pro']) + const prose = parseBoolean(attributes[':prose']) + const appConfig = generateThemeConfig({ pro, prose, componentName: finalComponentName }) + + replaceNodeWithPre( + node, + 'ts', + `export default defineAppConfig(${json5.stringify(appConfig, null, 2)?.replace(/,([ |\t\n]+[}|\])])/g, '$1')})`, + 'app.config.ts' + ) + }) + + visitAndReplace(doc, 'component-code', (node) => { + const attributes = node[1] as ComponentAttributes + const pro = parseBoolean(attributes[':pro']) + const props = attributes[':props'] ? json5.parse(attributes[':props']) : {} + const external = attributes[':external'] ? json5.parse(attributes[':external']) : [] + const externalTypes = attributes[':externalTypes'] ? json5.parse(attributes[':externalTypes']) : [] + const ignore = attributes[':ignore'] ? json5.parse(attributes[':ignore']) : [] + const hide = attributes[':hide'] ? json5.parse(attributes[':hide']) : [] + const slots = attributes[':slots'] ? json5.parse(attributes[':slots']) : {} + + const code = generateComponentCode({ + pro, + props, + external, + externalTypes, + ignore, + hide, + componentName, + slots + }) + + replaceNodeWithPre(node, 'vue', code) + }) + + visitAndReplace(doc, 'component-props', (node) => { + const attributes = node[1] as Record + const mdcSpecificName = attributes?.name + const isProse = parseBoolean(attributes[':prose']) + + const finalComponentName = mdcSpecificName ? camelCase(mdcSpecificName) : componentName + + const { pascalCaseName, componentMeta } = getComponentMeta(finalComponentName) + + if (!componentMeta?.props) return + + const interfaceName = isProse ? `Prose${pascalCaseName}Props` : `${pascalCaseName}Props` + + const interfaceCode = generateTSInterface( + interfaceName, + Object.values(componentMeta.props), + propItemHandler, + `Props for the ${isProse ? 'Prose' : ''}${pascalCaseName} component` + ) + replaceNodeWithPre(node, 'ts', interfaceCode) + }) + + visitAndReplace(doc, 'component-slots', (node) => { + const { pascalCaseName, componentMeta } = getComponentMeta(componentName) + if (!componentMeta?.slots) return + + const interfaceCode = generateTSInterface( + `${pascalCaseName}Slots`, + Object.values(componentMeta.slots), + slotItemHandler, + `Slots for the ${pascalCaseName} component` + ) + replaceNodeWithPre(node, 'ts', interfaceCode) + }) + + visitAndReplace(doc, 'component-emits', (node) => { + const { pascalCaseName, componentMeta } = getComponentMeta(componentName) + const hasEvents = componentMeta?.events && Object.keys(componentMeta.events).length > 0 + + if (hasEvents) { + const interfaceCode = generateTSInterface( + `${pascalCaseName}Emits`, + Object.values(componentMeta.events), + emitItemHandler, + `Emitted events for the ${pascalCaseName} component` + ) + replaceNodeWithPre(node, 'ts', interfaceCode) + } else { + node[0] = 'p' + node[1] = {} + node[2] = 'No events available for this component.' + } + }) + + visitAndReplace(doc, 'component-example', (node) => { + const camelName = camelCase(node[1]['name']) + const name = camelName.charAt(0).toUpperCase() + camelName.slice(1) + const code = components[name].code + replaceNodeWithPre(node, 'vue', code, `${name}.vue`) + }) + + return doc +}