diff --git a/docs-info.md b/docs-info.md index a13e6ef65..a16087e3c 100644 --- a/docs-info.md +++ b/docs-info.md @@ -89,7 +89,23 @@ body { ### Package manager tabs -Package-manager tabs are a special `tabs` variant. The parser reads framework lines like `react: ...` or `solid: ...`, groups packages, and later generates package-manager-specific commands. +Package-manager tabs are a special `tabs` variant. Lines without a framework prefix apply to every framework. Use prefixes like `react: ...` or `solid: ...` only for framework-specific packages or commands. + +Each line produces a separate command for the selected package manager. For example, this block shows three commands: + +```md + + +@tanstack/intent@latest list +@tanstack/intent@latest validate +@tanstack/intent@latest review + + +``` + +Shared lines can be mixed with framework-specific lines. The selected framework receives its own lines plus every shared line, in source order. A framework without its own lines receives only the shared lines. + +Wrap the lines in a fenced text block inside the component when arguments contain literal Markdown characters such as `#` or `*`. There are various supported package manager formats, including npm, yarn, pnpm, and bun. diff --git a/src/routes/_library/$libraryId/$version.docs.{$}[.]md.tsx b/src/routes/_library/$libraryId/$version.docs.{$}[.]md.tsx index 309a89967..0adfd61c0 100644 --- a/src/routes/_library/$libraryId/$version.docs.{$}[.]md.tsx +++ b/src/routes/_library/$libraryId/$version.docs.{$}[.]md.tsx @@ -61,14 +61,11 @@ export const Route = createFileRoute( const doc = result.doc - // Filter framework-specific content only if framework is explicitly specified - const filteredContent = framework - ? filterFrameworkContent(doc.content, { - framework, - packageManager: pm, - keepMarkers, - }) - : doc.content + const filteredContent = filterFrameworkContent(doc.content, { + framework: framework ?? undefined, + packageManager: pm, + keepMarkers, + }) const markdownContent = `# ${doc.title}\n${filteredContent}` const filename = `${result.docsPath || 'file'}.md` diff --git a/src/utils/markdown/filterFrameworkContent.ts b/src/utils/markdown/filterFrameworkContent.ts index ba423b6f9..d4e364bcd 100644 --- a/src/utils/markdown/filterFrameworkContent.ts +++ b/src/utils/markdown/filterFrameworkContent.ts @@ -18,6 +18,8 @@ * */ +import { parseMarkdown } from '@tanstack/markdown/parser' +import { transformPackageManagerTabs } from '@tanstack/markdown/extensions/tabs' import { getInstallCommand, type PackageManager, @@ -80,7 +82,7 @@ export function extractFrameworksFromMarkdown(markdown: string): Array { /** * Filters framework-specific content and package-manager tabs from raw markdown. - * If no framework is specified, returns markdown unchanged. + * Shared package-manager commands do not require a framework selection. */ export function filterFrameworkContent( markdown: string, @@ -88,14 +90,12 @@ export function filterFrameworkContent( ): string { const { framework, packageManager, keepMarkers = false } = options - if (!framework) { - return markdown - } - - const normalizedFramework = framework.toLowerCase() + const normalizedFramework = framework?.toLowerCase() // First pass: filter framework blocks - let result = filterFrameworkBlocks(markdown, normalizedFramework, keepMarkers) + let result = normalizedFramework + ? filterFrameworkBlocks(markdown, normalizedFramework, keepMarkers) + : markdown // Second pass: filter package-manager tabs result = filterPackageManagerTabs( @@ -145,12 +145,12 @@ function filterFrameworkBlocks( /** * Filters blocks. - * If framework matches and packageManager is provided, outputs the command. + * If shared or matching framework commands exist and packageManager is provided, outputs the commands. * Otherwise returns block as-is. */ function filterPackageManagerTabs( markdown: string, - framework: string, + framework: string | undefined, packageManager: PackageManager | undefined, keepMarkers: boolean, ): string { @@ -175,7 +175,8 @@ function filterPackageManagerTabs( // Parse framework lines const frameworkPackages = parseFrameworkLines(blockContent) - const packages = frameworkPackages[framework] + const packages = + frameworkPackages[framework ?? ''] ?? frameworkPackages[''] // If no match for framework, return as-is if (!packages || packages.length === 0) { @@ -212,34 +213,18 @@ function parseAttribute(attrs: string, name: string): string | undefined { } /** - * Parse framework lines like "react: @tanstack/react-query" from block content. - * Returns { framework: [[packages]] } structure. + * Use the same package groups as the HTML renderer, including shared commands + * under the empty framework key and literal arguments inside code blocks. */ function parseFrameworkLines(content: string): Record { - const result: Record = {} - const lines = content.split('\n') - - for (const line of lines) { - const trimmed = line.trim() - if (!trimmed) continue - - const colonIndex = trimmed.indexOf(':') - if (colonIndex === -1) continue - - const framework = trimmed.slice(0, colonIndex).trim().toLowerCase() - const packagesStr = trimmed.slice(colonIndex + 1).trim() - const packages = packagesStr.split(/\s+/).filter(Boolean) - - if (!framework || packages.length === 0) continue - - if (result[framework]) { - result[framework].push(packages) - } else { - result[framework] = [packages] - } - } - - return result + const component = transformPackageManagerTabs({ + type: 'component', + name: 'tabs', + attributes: {}, + children: parseMarkdown(content, { allowHtml: true }).children, + }) + const metadata = component.properties?.['data-package-manager-meta'] + return metadata ? JSON.parse(metadata).packagesByFramework : {} } type FrameworkSection = { diff --git a/tests/filter-framework-content.test.ts b/tests/filter-framework-content.test.ts index e7af89d5d..76d6ac949 100644 --- a/tests/filter-framework-content.test.ts +++ b/tests/filter-framework-content.test.ts @@ -1,4 +1,7 @@ -import { extractFrameworksFromMarkdown } from '../src/utils/markdown/filterFrameworkContent' +import { + extractFrameworksFromMarkdown, + filterFrameworkContent, +} from '../src/utils/markdown/filterFrameworkContent' function assertEqual(actual: unknown, expected: unknown, message: string) { const actualJson = JSON.stringify(actual) @@ -68,4 +71,114 @@ assertEqual( 'mixed framework sources preserve first-seen order', ) +const sharedCommands = ` +@tanstack/intent@latest list +@tanstack/intent@latest validate +@tanstack/intent@latest review +` + +for (const framework of [ + undefined, + 'react', + 'solid', + 'vue', + 'svelte', + 'angular', + 'lit', +]) { + for (const [packageManager, runner] of [ + ['npm', 'npx'], + ['pnpm', 'pnpx'], + ['yarn', 'yarn dlx'], + ['bun', 'bunx'], + ] as const) { + assertEqual( + filterFrameworkContent(sharedCommands, { framework, packageManager }), + `\`\`\`sh\n${runner} @tanstack/intent@latest list\n${runner} @tanstack/intent@latest validate\n${runner} @tanstack/intent@latest review\n\`\`\``, + `three shared commands for ${framework ?? 'no framework'} and ${packageManager}`, + ) + } +} + +assertEqual( + extractFrameworksFromMarkdown(sharedCommands), + [], + 'shared commands do not advertise framework variants', +) + +const mixedCommands = ` +react: react-only +shared-first +solid: solid-only +shared-last +` + +for (const [framework, commands] of [ + [undefined, ['shared-first', 'shared-last']], + ['react', ['react-only', 'shared-first', 'shared-last']], + ['solid', ['shared-first', 'solid-only', 'shared-last']], + ['another-framework', ['shared-first', 'shared-last']], +] as const) { + assertEqual( + filterFrameworkContent(mixedCommands, { framework, packageManager: 'npm' }), + `\`\`\`sh\n${commands.map((command) => `npx ${command}`).join('\n')}\n\`\`\``, + `shared and framework commands preserve source order for ${framework ?? 'no framework'}`, + ) +} + +assertEqual( + extractFrameworksFromMarkdown(mixedCommands), + ['react', 'solid'], + 'only explicit framework variants are advertised', +) +assertEqual( + filterFrameworkContent(sharedCommands, { framework: 'react' }), + sharedCommands, + 'commands stay unchanged without a selected package manager', +) +assertEqual( + filterFrameworkContent(packageManagerMarkdown, { packageManager: 'npm' }), + packageManagerMarkdown, + 'framework-specific blocks stay unchanged without a selected framework', +) +assertEqual( + filterFrameworkContent(sharedCommands, { + packageManager: 'npm', + keepMarkers: true, + }), + ` +\`\`\`sh +npx @tanstack/intent@latest list +npx @tanstack/intent@latest validate +npx @tanstack/intent@latest review +\`\`\` +`, + 'shared commands preserve requested markers', +) + +const literalCommands = ` +\`\`\`text +@tanstack/intent@latest load # +@tanstack/intent@latest exclude add package#experimental-* +@tanstack/intent@latest review --base main > .intent/review.json +tool --registry https://registry.example.com --filter name:value +\`\`\` +` + +assertEqual( + filterFrameworkContent(literalCommands, { packageManager: 'npm' }), + `\`\`\`sh +npx @tanstack/intent@latest load # +npx @tanstack/intent@latest exclude add package#experimental-* +npx @tanstack/intent@latest review --base main > .intent/review.json +npx tool --registry https://registry.example.com --filter name:value +\`\`\``, + 'literal command arguments survive Markdown export', +) +assertEqual( + extractFrameworksFromMarkdown(literalCommands), + [], + 'colons in arguments do not become framework variants', +) + console.log('filter-framework-content tests passed')