mirror of https://github.com/vuejs/vitepress
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
315 lines
8.7 KiB
315 lines
8.7 KiB
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import path from 'node:path'
|
|
|
|
import { resolveConfig } from 'node/config'
|
|
import { disposeMdItInstance } from 'node/markdown/markdown'
|
|
import { createMarkdownToVueRenderFn } from 'node/markdownToVue'
|
|
import { slash } from 'node/shared'
|
|
|
|
describe('node/markdownToVue', () => {
|
|
let root: string | undefined
|
|
|
|
afterEach(async () => {
|
|
if (root) {
|
|
await rm(root, { recursive: true, force: true })
|
|
root = undefined
|
|
}
|
|
})
|
|
|
|
test('records source line numbers for dead links', async () => {
|
|
root = await mkdtemp(path.join(tmpdir(), 'vitepress-dead-link-'))
|
|
|
|
const file = path.join(root, 'index.md')
|
|
const src = '# Home\n\nIntro\n\n[Missing](./missing.md)\n'
|
|
await writeFile(file, src)
|
|
|
|
const siteConfig = await resolveConfig(root, 'build', 'production')
|
|
const render = await createMarkdownToVueRenderFn(
|
|
siteConfig.srcDir,
|
|
{ cache: false },
|
|
'/',
|
|
false,
|
|
false,
|
|
siteConfig,
|
|
false
|
|
)
|
|
|
|
const result = await render(src, file)
|
|
|
|
expect(result.deadLinks).toContainEqual({
|
|
url: './missing.md',
|
|
resolved: '/missing',
|
|
file: slash(file),
|
|
line: 5,
|
|
column: 1
|
|
})
|
|
})
|
|
|
|
test('records source line numbers after frontmatter', async () => {
|
|
root = await mkdtemp(path.join(tmpdir(), 'vitepress-dead-link-'))
|
|
|
|
const file = path.join(root, 'index.md')
|
|
const src =
|
|
'---\ntitle: Home\n---\n# Home\n\nIntro\n\n[Missing](./missing.md)\n'
|
|
await writeFile(file, src)
|
|
|
|
const siteConfig = await resolveConfig(root, 'build', 'production')
|
|
const render = await createMarkdownToVueRenderFn(
|
|
siteConfig.srcDir,
|
|
{ cache: false },
|
|
'/',
|
|
false,
|
|
false,
|
|
siteConfig,
|
|
false
|
|
)
|
|
|
|
const result = await render(src, file)
|
|
|
|
expect(result.deadLinks).toContainEqual({
|
|
url: './missing.md',
|
|
resolved: '/missing',
|
|
file: slash(file),
|
|
line: 8,
|
|
column: 1
|
|
})
|
|
})
|
|
|
|
test('reports dead links inside included files at their real location', async () => {
|
|
root = await mkdtemp(path.join(tmpdir(), 'vitepress-dead-link-'))
|
|
|
|
const file = path.join(root, 'index.md')
|
|
const partial = path.join(root, 'part.md')
|
|
await writeFile(
|
|
partial,
|
|
'---\nt: 1\n---\nSome text\n[x](./nope)\nMore text\n'
|
|
)
|
|
const src =
|
|
'---\ntitle: x\n---\n\n# Guide\n\n<!-- @include: ./part.md -->\n\npara [a](./a)\nand [b](./b)\n'
|
|
await writeFile(file, src)
|
|
|
|
const siteConfig = await resolveConfig(root, 'build', 'production')
|
|
const render = await createMarkdownToVueRenderFn(
|
|
siteConfig.srcDir,
|
|
{ cache: false },
|
|
'/',
|
|
false,
|
|
false,
|
|
siteConfig,
|
|
false
|
|
)
|
|
|
|
const result = await render(src, file)
|
|
|
|
expect(result.deadLinks).toEqual([
|
|
{
|
|
url: './nope',
|
|
resolved: '/nope',
|
|
file: slash(partial),
|
|
line: 5,
|
|
column: 1,
|
|
via: slash(file)
|
|
},
|
|
{ url: './a', resolved: '/a', file: slash(file), line: 9, column: 6 },
|
|
{ url: './b', resolved: '/b', file: slash(file), line: 10, column: 5 }
|
|
])
|
|
})
|
|
|
|
test('reports the URL as authored alongside the resolved path', async () => {
|
|
root = await mkdtemp(path.join(tmpdir(), 'vitepress-dead-link-'))
|
|
|
|
const file = path.join(root, 'index.md')
|
|
const src = '[a](./a.md)\n\n[b](./b#hash)\n\n[c](./中文.md)\n\n[d](/d)\n'
|
|
await writeFile(file, src)
|
|
|
|
const siteConfig = await resolveConfig(root, 'build', 'production')
|
|
const render = await createMarkdownToVueRenderFn(
|
|
siteConfig.srcDir,
|
|
{ cache: false },
|
|
'/',
|
|
false,
|
|
false,
|
|
siteConfig,
|
|
false
|
|
)
|
|
|
|
const result = await render(src, file)
|
|
|
|
expect(
|
|
result.deadLinks.map(({ url, resolved }) => ({ url, resolved }))
|
|
).toEqual([
|
|
{ url: './a.md', resolved: '/a' },
|
|
{ url: './b#hash', resolved: '/b' },
|
|
{ url: './中文.md', resolved: '/中文' },
|
|
{ url: '/d', resolved: '/d' }
|
|
])
|
|
})
|
|
|
|
test('passes the authored link and its context to ignoreDeadLinks filters', async () => {
|
|
root = await mkdtemp(path.join(tmpdir(), 'vitepress-dead-link-'))
|
|
|
|
const file = path.join(root, 'index.md')
|
|
const src = '[s](./skip.md)\nand [k](./keep.md)\n'
|
|
await writeFile(file, src)
|
|
|
|
const siteConfig = await resolveConfig(root, 'build', 'production')
|
|
const calls: unknown[] = []
|
|
siteConfig.ignoreDeadLinks = [
|
|
(link, context) => {
|
|
calls.push([link, context])
|
|
return link === './skip.md'
|
|
}
|
|
]
|
|
const render = await createMarkdownToVueRenderFn(
|
|
siteConfig.srcDir,
|
|
{ cache: false },
|
|
'/',
|
|
false,
|
|
false,
|
|
siteConfig,
|
|
false
|
|
)
|
|
|
|
const result = await render(src, file)
|
|
|
|
expect(calls).toEqual([
|
|
['./skip.md', { file: slash(file), line: 1, column: 1, url: '/skip' }],
|
|
['./keep.md', { file: slash(file), line: 2, column: 5, url: '/keep' }]
|
|
])
|
|
expect(result.deadLinks.map((l) => l.url)).toEqual(['./keep.md'])
|
|
})
|
|
|
|
test('selects included heading sections after frontmatter', async () => {
|
|
root = await mkdtemp(path.join(tmpdir(), 'vitepress-include-'))
|
|
|
|
const file = path.join(root, 'index.md')
|
|
const source = path.join(root, 'source.md')
|
|
await writeFile(
|
|
source,
|
|
[
|
|
'---',
|
|
'description: Source description',
|
|
'---',
|
|
'# Intro',
|
|
'',
|
|
'intro text',
|
|
'',
|
|
'## Shared',
|
|
'',
|
|
'shared before target',
|
|
'',
|
|
'## Target',
|
|
'',
|
|
'target text',
|
|
'',
|
|
'### Child',
|
|
'',
|
|
'child text',
|
|
'',
|
|
'## Shared',
|
|
'',
|
|
'shared after target',
|
|
''
|
|
].join('\n')
|
|
)
|
|
const src = '<!--@include: ./source.md#target-->'
|
|
await writeFile(file, src)
|
|
|
|
const siteConfig = await resolveConfig(root, 'build', 'production')
|
|
const render = await createMarkdownToVueRenderFn(
|
|
siteConfig.srcDir,
|
|
{ cache: false },
|
|
'/',
|
|
false,
|
|
false,
|
|
siteConfig,
|
|
false
|
|
)
|
|
|
|
const result = await render(src, file)
|
|
|
|
expect(result.vueSrc).toContain('<p>target text</p>')
|
|
expect(result.vueSrc).toContain('<h3 id="child"')
|
|
expect(result.vueSrc).toContain('<p>child text</p>')
|
|
expect(result.vueSrc).not.toContain('Source description')
|
|
expect(result.vueSrc).not.toContain('intro text')
|
|
expect(result.vueSrc).not.toContain('shared before target')
|
|
expect(result.vueSrc).not.toContain('shared after target')
|
|
})
|
|
|
|
test('applies rewrites with mismatched Windows drive letter case', async () => {
|
|
root = await mkdtemp(path.join(tmpdir(), 'vitepress-rewrite-'))
|
|
|
|
const file = path.join(root, 'index.md')
|
|
await writeFile(file, '# Home\n')
|
|
|
|
const siteConfig = await resolveConfig(root, 'build', 'production')
|
|
siteConfig.srcDir = 'c:/site/docs'
|
|
siteConfig.pages = ['en/index.md']
|
|
siteConfig.rewrites = {
|
|
map: { 'en/index.md': 'index.md' },
|
|
inv: { 'index.md': 'en/index.md' }
|
|
}
|
|
;(siteConfig as any).__dirty = true
|
|
|
|
const render = await createMarkdownToVueRenderFn(
|
|
siteConfig.srcDir,
|
|
{ cache: false },
|
|
'/',
|
|
false,
|
|
false,
|
|
siteConfig,
|
|
false
|
|
)
|
|
|
|
const result = await render('# Home\n', 'C:/site/docs/en/index.md')
|
|
|
|
expect(result.pageData.relativePath).toBe('index.md')
|
|
})
|
|
|
|
test('warns when transformPageData rewrites an interpolated value', async () => {
|
|
disposeMdItInstance()
|
|
root = await mkdtemp(path.join(tmpdir(), 'vitepress-eager-'))
|
|
|
|
const file = path.join(root, 'index.md')
|
|
const src = '---\ntitle: Old\n---\n\n# {{ $frontmatter.title }}\n'
|
|
await writeFile(file, src)
|
|
|
|
const siteConfig = await resolveConfig(root, 'build', 'production')
|
|
const warnings: string[] = []
|
|
siteConfig.logger = {
|
|
...siteConfig.logger,
|
|
warn: (msg: string) => warnings.push(msg)
|
|
}
|
|
siteConfig.transformPageData = (pageData) => {
|
|
pageData.frontmatter.title = 'New'
|
|
}
|
|
|
|
const render = await createMarkdownToVueRenderFn(
|
|
siteConfig.srcDir,
|
|
{ cache: false },
|
|
'/',
|
|
false,
|
|
false,
|
|
siteConfig,
|
|
false
|
|
)
|
|
|
|
const result = await render(src, file)
|
|
expect(result.vueSrc).toContain('Old')
|
|
expect(warnings.join('\n')).toContain('{{ $frontmatter.title }}')
|
|
|
|
// keys only added by the transform are left to the runtime - no warning
|
|
warnings.length = 0
|
|
siteConfig.transformPageData = (pageData) => {
|
|
pageData.frontmatter.added = 'later'
|
|
}
|
|
const src2 =
|
|
'---\ntitle: Old\n---\n\n{{ $frontmatter.title }} {{ $frontmatter.added }}\n'
|
|
await writeFile(file, src2)
|
|
await render(src2, file)
|
|
expect(warnings).toHaveLength(0)
|
|
})
|
|
})
|