Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion docs-info.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<!-- ::start:tabs variant="package-manager" mode="local-install" -->

@tanstack/intent@latest list
@tanstack/intent@latest validate
@tanstack/intent@latest review

<!-- ::end:tabs -->
```

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 `<package>#<skill>` or `*`.

There are various supported package manager formats, including npm, yarn, pnpm, and bun.

Expand Down
13 changes: 5 additions & 8 deletions src/routes/_library/$libraryId/$version.docs.{$}[.]md.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
57 changes: 21 additions & 36 deletions src/utils/markdown/filterFrameworkContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
* <!-- ::end:tabs -->
*/

import { parseMarkdown } from '@tanstack/markdown/parser'
import { transformPackageManagerTabs } from '@tanstack/markdown/extensions/tabs'
import {
getInstallCommand,
type PackageManager,
Expand Down Expand Up @@ -80,22 +82,20 @@ export function extractFrameworksFromMarkdown(markdown: string): Array<string> {

/**
* 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,
options: FilterOptions,
): 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(
Expand Down Expand Up @@ -145,12 +145,12 @@ function filterFrameworkBlocks(

/**
* Filters <!-- ::start:tabs variant="package-manager" --> 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 {
Expand All @@ -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) {
Expand Down Expand Up @@ -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<string, string[][]> {
const result: Record<string, string[][]> = {}
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 = {
Expand Down
115 changes: 114 additions & 1 deletion tests/filter-framework-content.test.ts
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -68,4 +71,114 @@ assertEqual(
'mixed framework sources preserve first-seen order',
)

const sharedCommands = `<!-- ::start:tabs variant="package-manager" mode="local-install" -->
@tanstack/intent@latest list
@tanstack/intent@latest validate
@tanstack/intent@latest review
<!-- ::end:tabs -->`

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 = `<!-- ::start:tabs variant="package-manager" mode="local-install" -->
react: react-only
shared-first
solid: solid-only
shared-last
<!-- ::end:tabs -->`

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,
}),
`<!-- ::start:tabs variant="package-manager" mode="local-install" -->
\`\`\`sh
npx @tanstack/intent@latest list
npx @tanstack/intent@latest validate
npx @tanstack/intent@latest review
\`\`\`
<!-- ::end:tabs -->`,
'shared commands preserve requested markers',
)

const literalCommands = `<!-- ::start:tabs variant="package-manager" mode="local-install" -->
\`\`\`text
@tanstack/intent@latest load <package>#<skill>
@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
\`\`\`
<!-- ::end:tabs -->`

assertEqual(
filterFrameworkContent(literalCommands, { packageManager: 'npm' }),
`\`\`\`sh
npx @tanstack/intent@latest load <package>#<skill>
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')
Loading