Skip to content
Merged
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
38 changes: 25 additions & 13 deletions src/plugin/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import remarkPlease from '@/markdown/remark-plugins/remark-please'
import remarkReplaceImageUrls from '@/markdown/remark-plugins/replace-image-urls'
import remarkInclude from '@/markdown/remark-plugins/snippets'
import type { CustomTemplateVariables, LlmstxtSettings } from '@/types.d'
import { processVPParams } from '@/utils/dynamic-routes'
import { getDirectoriesAtDepths } from '@/utils/file-utils'
import { getHumanReadableSizeOf } from '@/utils/helpers'
import log from '@/utils/logger'
Expand All @@ -33,7 +34,7 @@ export async function transform(
content: string,
id: string,
settings: LlmstxtSettings & { ignoreFiles: string[]; workDir: string },
mdFiles: Set<string>,
mdFiles: Map<string, string>,
config: VitePressConfig,
// biome-ignore lint/suspicious/noExplicitAny: TODO: Fix type
): Promise<any> {
Expand Down Expand Up @@ -113,14 +114,24 @@ export async function transform(
}
}

llmHint = `<div style="display: none;" hidden="true" aria-hidden="true">${llmHint}</div>\n\n`

modifiedContent = matter.stringify(llmHint + modifiedContent.content, modifiedContent.data)
// Insert llmHint at start or after __VP_PARAMS_END__ if present
if (llmHint) {
const hintBlock = `<div style="display: none;" hidden="true" aria-hidden="true">${llmHint}</div>\n`
let content = modifiedContent.content
const marker = '__VP_PARAMS_END__'
const idx = content.indexOf(marker)
if (idx !== -1) {
content = content.slice(0, idx + marker.length) + hintBlock + content.slice(idx + marker.length)
} else {
content = `${hintBlock}\n${content}`
}
modifiedContent = matter.stringify(content, modifiedContent.data)
}
}

// Add markdown file path to our collection
if (!isMainPage || !settings.excludeIndexPage) {
mdFiles.add(id)
mdFiles.set(id, content)
}

return modifiedContent !== orig ? { code: modifiedContent, map: null } : null
Expand All @@ -134,7 +145,7 @@ export async function generateBundle(
bundle: OutputBundle,
settings: LlmstxtSettings & { ignoreFiles: string[]; workDir: string },
config: VitePressConfig,
mdFiles: Set<string>,
mdFiles: Map<string, string>,
isSsrBuild: boolean,
): Promise<void> {
// Skip processing during SSR build
Expand All @@ -160,8 +171,7 @@ export async function generateBundle(
await fs.mkdir(outDir, { recursive: true })
}

const mdFilesList = Array.from(mdFiles)
const fileCount = mdFilesList.length
const fileCount = mdFiles.size

// Skip if no files found
if (fileCount === 0) {
Expand Down Expand Up @@ -192,15 +202,14 @@ export async function generateBundle(
}
}

const mdFilesList = Array.from(mdFiles)
const preparedFiles: PreparedFile[] = await Promise.all(
mdFilesList.map(async (file) => {
mdFilesList.map(async ([file, content]) => {
const resolvedOutFilePath = path.relative(
settings.workDir,
resolveOutputFilePath(file, settings.workDir, config.vitepress.userConfig?.rewrites),
)

const content = await fs.readFile(file, 'utf-8')

const markdownProcessor = remark()
.use(remarkFrontmatter)
.use(remarkInclude({ srcDir: settings.workDir }))
Expand All @@ -218,6 +227,8 @@ export async function generateBundle(
})
}

content = processVPParams(content)

const processedMarkdown = matter(
String(
await markdownProcessor.process({
Expand Down Expand Up @@ -246,6 +257,7 @@ export async function generateBundle(
// Sort files by title for better organization
preparedFiles.sort((a, b) => a.title.localeCompare(b.title))

const mdFilesKeys = Array.from(mdFiles.keys())
const tasks: Promise<void>[] = []

if (settings.generateLLMsTxt) {
Expand All @@ -259,7 +271,7 @@ export async function generateBundle(

// Get directories at specified depths
const directories = getDirectoriesAtDepths(
mdFilesList,
mdFilesKeys,
settings.workDir,
settings.experimental?.depth ?? 1,
)
Expand Down Expand Up @@ -317,7 +329,7 @@ export async function generateBundle(
if (settings.generateLLMsFullTxt) {
// Get directories at specified depths for llms-full.txt as well
const directories = getDirectoriesAtDepths(
mdFilesList,
mdFilesKeys,
settings.workDir,
settings.experimental?.depth ?? 1,
)
Expand Down
4 changes: 2 additions & 2 deletions src/plugin/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ export function llmstxt(userSettings: LlmstxtSettings = {}): [Plugin, Plugin] {
// Store the resolved Vite config
let config: VitePressConfig

// Set to store all markdown file paths
const mdFiles: Set<string> = new Set()
// Map to store all markdown files content
const mdFiles: Map<string, string> = new Map()

// Flag to identify which build we're in
let isSsrBuild = false
Expand Down
19 changes: 19 additions & 0 deletions src/utils/dynamic-routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const VP_PARAMS_MARKER_REGEX = /^__VP_PARAMS_START([\s\S]+?)__VP_PARAMS_END__/

export function processVPParams(content: string): string {
let params: Record<string, string> = {}

content = content.replace(VP_PARAMS_MARKER_REGEX, (_, paramsString) => {
params = JSON.parse(paramsString)

return ''
})

if (params && Object.keys(params).length > 0) {
content = content.replace(/\{\{\s*\$params\.([\s\S]+?)\s*\}\}/g, (_, paramKey) => {
return params[paramKey] ?? ''
})
}

return content
}
32 changes: 0 additions & 32 deletions tests/plugin/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -592,38 +592,6 @@ This is a test page.`
expect(result).toContain('/guide.md')
expect(result).not.toContain('/guide/index.md')
})

it('can use `index.md` which is specified in `rewrites` rules', async () => {
const configWithRewrites = {
...mockConfig,
vitepress: {
...mockConfig.vitepress,
srcDir: path.resolve(mockConfig.vitepress.srcDir, '..'),
userConfig: {
rewrites: {
'otherdocs/index.md': 'index.md',
},
},
},
}

plugin = llmstxt({ generateLLMsFullTxt: false, generateLLMFriendlyDocsForEachPage: false })
// @ts-ignore
plugin[1].configResolved(configWithRewrites)

// @ts-ignore
await plugin[0].transform(fakeMarkdownDocument, 'docs/page.md')
// @ts-ignore
await plugin[0].transform(fakeMarkdownDocument, 'otherdocs/page.md')
// @ts-ignore
await plugin[1].generateBundle()

expect(readFile).toHaveBeenCalledTimes(3)

expect((readFile.mock.calls as unknown as string[][])[2]?.[0]).toBe(
path.resolve('otherdocs', 'index.md'),
)
})
})

describe('experimental depth option', () => {
Expand Down
48 changes: 48 additions & 0 deletions tests/utils/dynamic-routes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { describe, expect, it } from 'bun:test'
import dedent from 'dedent'
import matter from 'gray-matter'

// @ts-ignore
import { processVPParams } from '@/utils/dynamic-routes'

const generateStringWithVPParams = (params: Record<string, string>, content: string) => {
return `__VP_PARAMS_START${JSON.stringify(params)}__VP_PARAMS_END__${content}`
}

describe('processVPParams', () => {
it('replaces $params.pkg references in title', () => {
const markdown = generateStringWithVPParams({ pkg: 'vitepress' }, '# {{ $params.pkg }}')
const title = processVPParams(markdown)
expect(title).toBe('# vitepress')
})

it('replaces multiple param references', () => {
const markdown = generateStringWithVPParams(
{ pkg: 'vitepress', version: '1.0.0' },
'# {{ $params.pkg }} v{{ $params.version }}',
)
const title = processVPParams(markdown)
expect(title).toBe('# vitepress v1.0.0')
})

it('handles spaces in template syntax', () => {
const markdown = generateStringWithVPParams({ pkg: 'vitepress' }, '# {{ $params.pkg }}')
const title = processVPParams(markdown)
expect(title).toBe('# vitepress')
})

it('uses title from frontmatter with param replacement', () => {
const markdown = generateStringWithVPParams(
{ pkg: 'vitepress' },
dedent`
---
title: "{{ $params.pkg }} Documentation"
---

# Content
`,
)
const title = matter(processVPParams(markdown)).data['title']
expect(title).toBe('vitepress Documentation')
})
})
Loading