mirror of https://github.com/vuejs/vitepress
commit
dae648e8d4
@ -0,0 +1,119 @@
|
||||
import { readFileSync, readdirSync } from 'node:fs'
|
||||
import { join, resolve, sep } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { newPage, realErrors, waitForHydration, type TestPage } from './helpers'
|
||||
|
||||
const dir = resolve(fileURLToPath(import.meta.url), '..')
|
||||
const dist = (...p: string[]) =>
|
||||
join(dir, 'fixture/.vitepress/dist-sharded', ...p)
|
||||
const origin = () => `http://localhost:${process.env['SHARDED_PORT']}`
|
||||
|
||||
// output-relative paths of everything under assets/
|
||||
const files = () =>
|
||||
readdirSync(dist('assets'), { recursive: true, withFileTypes: true })
|
||||
.filter((e) => e.isFile())
|
||||
.map((e) =>
|
||||
join(e.parentPath, e.name)
|
||||
.slice(dist('assets').length + 1)
|
||||
.split(sep)
|
||||
.join('/')
|
||||
)
|
||||
|
||||
const isShardedPath = (f: string) => /^[0-2]\/[^/]+$/.test(f)
|
||||
|
||||
describe('assetsShards emit', () => {
|
||||
test('page chunks land in numbered subdirectories', () => {
|
||||
const pages = files().filter((f) => /\.md\.[\w-]+(\.lean)?\.js$/.test(f))
|
||||
expect(pages.length).toBeGreaterThan(0)
|
||||
expect(pages.every(isShardedPath)).toBe(true)
|
||||
})
|
||||
|
||||
test('imported assets are sharded too, shared chunks and the app are not', () => {
|
||||
const all = files()
|
||||
const assets = all.filter(
|
||||
(f) => /\.(png|woff2|css)$/.test(f) && !/^vp-icons\./.test(f)
|
||||
)
|
||||
expect(assets.length).toBeGreaterThan(0)
|
||||
expect(assets.every(isShardedPath)).toBe(true)
|
||||
expect(all.some((f) => /^chunks\/framework\.[\w-]+\.js$/.test(f))).toBe(
|
||||
true
|
||||
)
|
||||
expect(all.some((f) => /^app\.[\w-]+\.js$/.test(f))).toBe(true)
|
||||
})
|
||||
|
||||
test('hash map entries carry the shard and resolve to real files', () => {
|
||||
const map: Record<string, string> = JSON.parse(
|
||||
readFileSync(dist('hashmap.json'), 'utf-8')
|
||||
)
|
||||
// hash map keys are lowercased page names, file names keep their case
|
||||
const lower = new Set(files().map((f) => f.toLowerCase()))
|
||||
expect(Object.keys(map).length).toBeGreaterThan(0)
|
||||
for (const [page, entry] of Object.entries(map)) {
|
||||
expect(entry).toMatch(/^[0-2]\/[\w-]+$/)
|
||||
const [shard, hash] = entry.split('/')
|
||||
const chunk = `${shard}/${page}.${hash}`.toLowerCase()
|
||||
expect(lower.has(`${chunk}.js`)).toBe(true)
|
||||
expect(lower.has(`${chunk}.lean.js`)).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
test('preload links point at files that exist', () => {
|
||||
const existing = new Set(files().map((f) => `assets/${f}`))
|
||||
for (const page of [
|
||||
'index',
|
||||
'sub/page',
|
||||
'sub/deep/page2',
|
||||
'moved/target'
|
||||
]) {
|
||||
const links = [
|
||||
...readFileSync(dist(`${page}.html`), 'utf-8').matchAll(
|
||||
/<link rel="modulepreload" href="\/([^"]+)">/g
|
||||
)
|
||||
].map((m) => m[1]!)
|
||||
expect(links.some((l) => l.includes('.md.'))).toBe(true)
|
||||
for (const link of links) expect(existing.has(link)).toBe(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('assetsShards in the browser', () => {
|
||||
let t: TestPage
|
||||
|
||||
beforeAll(async () => {
|
||||
t = await newPage()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await t.page.close()
|
||||
await t.browser.close()
|
||||
})
|
||||
|
||||
test('pages hydrate with sharded chunks', async () => {
|
||||
await t.page.goto(`${origin()}/`)
|
||||
await waitForHydration(t.page)
|
||||
expect(await t.page.textContent('h1')).toContain('Home')
|
||||
})
|
||||
|
||||
test('client-side navigation loads page chunks from their shard', async () => {
|
||||
await t.page.evaluate(() => ((window as any).__spa_marker = 1))
|
||||
await t.page.click('.vp-doc a[href="/sub/page.html"]')
|
||||
await t.page.waitForFunction(() =>
|
||||
document.querySelector('h1')?.textContent?.includes('Sub page')
|
||||
)
|
||||
expect(
|
||||
await t.page.evaluate(() => (window as any).__spa_marker === 1)
|
||||
).toBe(true)
|
||||
const chunk = await t.page.evaluate(() =>
|
||||
performance
|
||||
.getEntriesByType('resource')
|
||||
.map((r) => r.name)
|
||||
.find((n) => /\/assets\/\d+\/sub_page\.md\.[\w-]+\.js$/.test(n))
|
||||
)
|
||||
expect(chunk).toBeDefined()
|
||||
})
|
||||
|
||||
test('no console or page errors across the whole flow', () => {
|
||||
expect(realErrors(t.errors)).toEqual([])
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,10 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
|
||||
export default {
|
||||
watch: '../../fixtures/external-data/**/*.json',
|
||||
async load(files: string[]) {
|
||||
return Promise.all(
|
||||
files.map(async (file) => JSON.parse(await readFile(file, 'utf-8')))
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
<script setup>
|
||||
import { data } from './files.data'
|
||||
</script>
|
||||
|
||||
# External data
|
||||
|
||||
<pre id="external-data">{{ JSON.stringify(data) }}</pre>
|
||||
@ -0,0 +1,45 @@
|
||||
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
test('loads data outside the site root', async () => {
|
||||
await goto('/external-data/')
|
||||
expect(await page.textContent('pre#external-data')).toBe('[{"a":true}]')
|
||||
})
|
||||
|
||||
test.runIf(!process.env.VITE_TEST_BUILD)(
|
||||
'updates external data when files change, are added, or are deleted',
|
||||
async () => {
|
||||
await goto('/external-data/')
|
||||
const a = fileURLToPath(
|
||||
new URL('../../fixtures/external-data/a.json', import.meta.url)
|
||||
)
|
||||
const nested = fileURLToPath(
|
||||
new URL('../../fixtures/external-data/nested/', import.meta.url)
|
||||
)
|
||||
const original = await readFile(a, 'utf-8')
|
||||
|
||||
async function expectData(data: unknown) {
|
||||
await page.waitForFunction(
|
||||
(expected) =>
|
||||
document.querySelector('pre#external-data')?.textContent === expected,
|
||||
JSON.stringify(data)
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
await writeFile(a, '{"a":false}\n')
|
||||
await expectData([{ a: false }])
|
||||
|
||||
await mkdir(nested)
|
||||
await writeFile(path.join(nested, 'b.json'), '{"b":true}\n')
|
||||
await expectData([{ a: false }, { b: true }])
|
||||
|
||||
await rm(nested, { recursive: true })
|
||||
await expectData([{ a: false }])
|
||||
} finally {
|
||||
await rm(nested, { recursive: true, force: true })
|
||||
await writeFile(a, original)
|
||||
}
|
||||
}
|
||||
)
|
||||
@ -0,0 +1,30 @@
|
||||
describe('navbar layout', () => {
|
||||
test.each([959, 960, 1440])(
|
||||
'reserves space beside the title only for a visible sidebar at %ipx',
|
||||
async (width) => {
|
||||
await page.setViewportSize({ width, height: 720 })
|
||||
|
||||
const searchOffset = async (path: string) => {
|
||||
await goto(path)
|
||||
await page.evaluate(() => document.fonts.ready)
|
||||
|
||||
const title = await page.locator('.VPNavBarTitle').boundingBox()
|
||||
const search = await page.locator('.VPNavBarSearch').boundingBox()
|
||||
return search!.x - title!.x
|
||||
}
|
||||
|
||||
const homeOffset = await searchOffset('/')
|
||||
|
||||
for (const path of ['/navbar/no-sidebar', '/missing-page']) {
|
||||
expect(await searchOffset(path)).toBeCloseTo(homeOffset, 0)
|
||||
}
|
||||
|
||||
const sidebarOffset = await searchOffset('/home')
|
||||
if (width >= 960) {
|
||||
expect(sidebarOffset).toBeGreaterThan(homeOffset)
|
||||
} else {
|
||||
expect(sidebarOffset).toBeCloseTo(homeOffset, 0)
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
@ -0,0 +1,7 @@
|
||||
---
|
||||
sidebar: false
|
||||
---
|
||||
|
||||
# Page without a sidebar
|
||||
|
||||
The navigation title should keep its natural width when the sidebar is hidden.
|
||||
@ -0,0 +1 @@
|
||||
# HTML link destination
|
||||
@ -0,0 +1,15 @@
|
||||
---
|
||||
sidebar: false
|
||||
outline: false
|
||||
---
|
||||
|
||||
# Link prefetching
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="400" height="150">
|
||||
<a href="./svg.html"><text x="10" y="25">SVG link</text></a>
|
||||
<a href="./svg.html#section"><text x="10" y="50">Same page, another hash</text></a>
|
||||
<a xlink:href="./xlink.html"><text x="10" y="75">SVG xlink</text></a>
|
||||
<a href="./new-tab.html" target="_blank"><text x="10" y="100">New tab</text></a>
|
||||
</svg>
|
||||
|
||||
[HTML link](./html.md)
|
||||
@ -0,0 +1 @@
|
||||
# New tab destination
|
||||
@ -0,0 +1,43 @@
|
||||
test.runIf(process.env.VITE_TEST_BUILD)(
|
||||
'prefetches SVG links without errors and respects new-tab targets',
|
||||
async () => {
|
||||
const errors: string[] = []
|
||||
const onPageError = (error: Error) => errors.push(error.message)
|
||||
page.on('pageerror', onPageError)
|
||||
|
||||
try {
|
||||
await goto('/prefetch/')
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
async () => ({
|
||||
errors,
|
||||
pages: await page
|
||||
.locator('link[rel="prefetch"]')
|
||||
.evaluateAll((links) =>
|
||||
links
|
||||
.map(
|
||||
(link) =>
|
||||
link
|
||||
.getAttribute('href')
|
||||
?.match(/prefetch_(.*?)\.md\./)?.[1]
|
||||
)
|
||||
.filter(Boolean)
|
||||
.sort()
|
||||
)
|
||||
}),
|
||||
{ timeout: 10_000 }
|
||||
)
|
||||
.toEqual({ errors: [], pages: ['html', 'svg', 'xlink'] })
|
||||
|
||||
await page.locator('svg a[href="./svg.html"]').click()
|
||||
await page.waitForSelector('h1', { state: 'visible' })
|
||||
await expect
|
||||
.poll(() => page.locator('h1').textContent())
|
||||
.toContain('SVG link destination')
|
||||
expect(errors).toEqual([])
|
||||
} finally {
|
||||
page.off('pageerror', onPageError)
|
||||
}
|
||||
}
|
||||
)
|
||||
@ -0,0 +1,3 @@
|
||||
# SVG link destination
|
||||
|
||||
## Section
|
||||
@ -0,0 +1 @@
|
||||
# SVG xlink destination
|
||||
@ -0,0 +1,53 @@
|
||||
---
|
||||
dir: rtl
|
||||
---
|
||||
|
||||
# RTL Page
|
||||
|
||||
با `--flag` اجرا کنید و `useData()` را بخوانید. این یک [پیوند بیرونی](https://vitepress.dev/) است.
|
||||
|
||||
## Section One
|
||||
|
||||
```js:line-numbers
|
||||
const a = 1
|
||||
```
|
||||
|
||||
> A blockquote keeps its bar on the reading side.
|
||||
|
||||
| Column | Value |
|
||||
| ------ | ----- |
|
||||
| a | 1 |
|
||||
|
||||
Filler paragraph one. Filler paragraph one. Filler paragraph one. Filler paragraph one.
|
||||
|
||||
Filler paragraph two. Filler paragraph two. Filler paragraph two. Filler paragraph two.
|
||||
|
||||
Filler paragraph three. Filler paragraph three. Filler paragraph three. Filler paragraph three.
|
||||
|
||||
Filler paragraph four. Filler paragraph four. Filler paragraph four. Filler paragraph four.
|
||||
|
||||
Filler paragraph five. Filler paragraph five. Filler paragraph five. Filler paragraph five.
|
||||
|
||||
Filler paragraph six. Filler paragraph six. Filler paragraph six. Filler paragraph six.
|
||||
|
||||
Filler paragraph seven. Filler paragraph seven. Filler paragraph seven. Filler paragraph seven.
|
||||
|
||||
Filler paragraph eight. Filler paragraph eight. Filler paragraph eight. Filler paragraph eight.
|
||||
|
||||
## Section Two
|
||||
|
||||
Filler paragraph nine. Filler paragraph nine. Filler paragraph nine. Filler paragraph nine.
|
||||
|
||||
Filler paragraph ten. Filler paragraph ten. Filler paragraph ten. Filler paragraph ten.
|
||||
|
||||
Filler paragraph eleven. Filler paragraph eleven. Filler paragraph eleven. Filler paragraph eleven.
|
||||
|
||||
Filler paragraph twelve. Filler paragraph twelve. Filler paragraph twelve. Filler paragraph twelve.
|
||||
|
||||
Filler paragraph thirteen. Filler paragraph thirteen. Filler paragraph thirteen. Filler paragraph thirteen.
|
||||
|
||||
Filler paragraph fourteen. Filler paragraph fourteen. Filler paragraph fourteen. Filler paragraph fourteen.
|
||||
|
||||
Filler paragraph fifteen. Filler paragraph fifteen. Filler paragraph fifteen. Filler paragraph fifteen.
|
||||
|
||||
Filler paragraph sixteen. Filler paragraph sixteen. Filler paragraph sixteen. Filler paragraph sixteen.
|
||||
@ -0,0 +1,154 @@
|
||||
const box = (selector: string) =>
|
||||
page
|
||||
.locator(selector)
|
||||
.first()
|
||||
.evaluate((el) => {
|
||||
const { x, y, width, height } = el.getBoundingClientRect()
|
||||
return { x, y, width, height, right: x + width, bottom: y + height }
|
||||
})
|
||||
|
||||
// screen positions of the first and last characters of an element's text,
|
||||
// which is how the bidi algorithm's reordering shows up
|
||||
const glyphEnds = (selector: string) =>
|
||||
page
|
||||
.locator(selector)
|
||||
.first()
|
||||
.evaluate((el) => {
|
||||
const node = el.firstChild as Text
|
||||
const range = document.createRange()
|
||||
range.setStart(node, 0)
|
||||
range.setEnd(node, 1)
|
||||
const first = range.getBoundingClientRect().x
|
||||
range.setStart(node, node.length - 1)
|
||||
range.setEnd(node, node.length)
|
||||
const last = range.getBoundingClientRect().x
|
||||
return { first, last }
|
||||
})
|
||||
|
||||
// scrolls a heading into view until the outline marks it active. The scroll
|
||||
// is re-issued on every check, so a lost scroll event or a dev-server reload
|
||||
// triggered by another spec cannot leave the wait hanging (seen on Windows CI)
|
||||
const activateHeading = (id: string) =>
|
||||
page.waitForFunction((id) => {
|
||||
if (document.querySelector(`.outline-link.active[href="#${id}"]`)) {
|
||||
return true
|
||||
}
|
||||
document.getElementById(id)?.scrollIntoView()
|
||||
return false
|
||||
}, id)
|
||||
|
||||
describe('rtl', () => {
|
||||
beforeAll(async () => {
|
||||
await goto('/rtl/')
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await page.setViewportSize({ width: 1280, height: 720 })
|
||||
})
|
||||
|
||||
test('sets the direction on the html element', async () => {
|
||||
expect(await page.getAttribute('html', 'dir')).toBe('rtl')
|
||||
if (process.env['VITE_TEST_BUILD']) {
|
||||
const html = await (await page.request.get(page.url())).text()
|
||||
expect(html).toContain('<html lang="en-US" dir="rtl">')
|
||||
}
|
||||
})
|
||||
|
||||
test('lays the page out from the right', async () => {
|
||||
const width = await page.evaluate(() => innerWidth)
|
||||
const sidebar = await box('.VPSidebar')
|
||||
expect(sidebar.right).toBeGreaterThan(width - 1)
|
||||
expect(sidebar.x).toBeGreaterThan(width / 2)
|
||||
|
||||
const heading = await box('.vp-doc h1')
|
||||
const title = await glyphEnds('.vp-doc h1')
|
||||
expect(title.last).toBeGreaterThan(heading.x + heading.width / 2)
|
||||
|
||||
const anchor = await box('.vp-doc h2 .header-anchor')
|
||||
const h2 = await box('.vp-doc h2')
|
||||
expect(anchor.x).toBeGreaterThan(h2.x + h2.width / 2)
|
||||
})
|
||||
|
||||
test('keeps the outline marker on the reading side and moving', async () => {
|
||||
const outline = await box('.VPDocAsideOutline .content')
|
||||
await activateHeading('section-one')
|
||||
const before = await box('.outline-marker')
|
||||
expect(before.x).toBeGreaterThan(outline.x + outline.width / 2)
|
||||
|
||||
await activateHeading('section-two')
|
||||
const after = await box('.outline-marker')
|
||||
expect(after.y).toBeGreaterThan(before.y)
|
||||
expect(Math.abs(after.x - before.x)).toBeLessThan(1)
|
||||
})
|
||||
|
||||
test('mirrors the sidebar caret when a group collapses', async () => {
|
||||
const group = page.locator('.VPSidebarItem.level-0.collapsible').first()
|
||||
await group.locator('.caret').first().click()
|
||||
expect(
|
||||
await group.evaluate((el) => el.classList.contains('collapsed'))
|
||||
).toBe(true)
|
||||
expect(
|
||||
await group
|
||||
.locator('.caret-icon')
|
||||
.first()
|
||||
.evaluate((el) => getComputedStyle(el).scale)
|
||||
).toBe('-1 1')
|
||||
await group.locator('.caret').first().click()
|
||||
})
|
||||
|
||||
test('keeps code left-to-right', async () => {
|
||||
const wrapper = page.locator('.vp-doc div[class*="language-"]').first()
|
||||
expect(await wrapper.getAttribute('dir')).toBe('ltr')
|
||||
expect(
|
||||
await wrapper
|
||||
.locator('pre')
|
||||
.evaluate((el) => getComputedStyle(el).direction)
|
||||
).toBe('ltr')
|
||||
|
||||
const block = await box('.vp-doc div[class*="language-"]')
|
||||
const copy = await box('.vp-doc div[class*="language-"] > button.copy')
|
||||
const gutter = await box(
|
||||
'.vp-doc div[class*="language-"] > .line-numbers-wrapper'
|
||||
)
|
||||
expect(copy.x).toBeGreaterThan(block.x + block.width / 2)
|
||||
expect(gutter.right).toBeLessThan(block.x + block.width / 2)
|
||||
|
||||
const flag = await glyphEnds('.vp-doc p code')
|
||||
expect(flag.first).toBeLessThan(flag.last)
|
||||
})
|
||||
|
||||
test('mirrors the external link icon', async () => {
|
||||
const mask = await page
|
||||
.locator('.vp-doc a[href^="https://"]')
|
||||
.first()
|
||||
.evaluate((el) => {
|
||||
const style = getComputedStyle(el, '::after')
|
||||
return style.maskImage || style.webkitMaskImage
|
||||
})
|
||||
expect(mask).toContain('matrix(-1 0 0 1 24 0)')
|
||||
})
|
||||
|
||||
test('slides the mobile sidebar in from the right', async () => {
|
||||
await page.setViewportSize({ width: 375, height: 812 })
|
||||
await goto('/rtl/')
|
||||
const viewport = await page.evaluate(
|
||||
() => document.documentElement.clientWidth
|
||||
)
|
||||
const closed = await box('.VPSidebar')
|
||||
expect(closed.x).toBeGreaterThanOrEqual(viewport - 1)
|
||||
|
||||
// open the sidebar and wait for its slide-in to settle against the right
|
||||
// edge, reopening it if a reload closed it meanwhile (see activateHeading)
|
||||
await page.waitForFunction(() => {
|
||||
const sidebar = document.querySelector('.VPSidebar')!
|
||||
if (!sidebar.classList.contains('open')) {
|
||||
document.querySelector<HTMLElement>('.VPLocalNav .menu')?.click()
|
||||
return false
|
||||
}
|
||||
const { right } = sidebar.getBoundingClientRect()
|
||||
return Math.abs(right - document.documentElement.clientWidth) < 1
|
||||
})
|
||||
const open = await box('.VPSidebar')
|
||||
expect(open.x).toBeLessThan(viewport)
|
||||
})
|
||||
})
|
||||
@ -0,0 +1 @@
|
||||
{ "a": true }
|
||||
@ -0,0 +1,84 @@
|
||||
import { readdirSync, readFileSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
// the default theme is laid out with logical properties so that `dir: 'rtl'`
|
||||
// mirrors it without a build step; this keeps physical inline-axis
|
||||
// declarations from creeping back in
|
||||
|
||||
const root = fileURLToPath(
|
||||
new URL('../../../../src/client/theme-default/', import.meta.url)
|
||||
)
|
||||
|
||||
// declarations that must stay physical: centering paired with
|
||||
// translate(-50%), which anchoring the inline-start edge would push
|
||||
// off-centre in rtl
|
||||
const physicalByDesign: Record<string, string[]> = {
|
||||
'components/VPHero.vue': ['left: 50%', 'left: 50%']
|
||||
}
|
||||
|
||||
// a declaration starts a block, follows another declaration, or sits on its
|
||||
// own line, so one-line rules are scanned like any other
|
||||
const physicalInlineAxis =
|
||||
/(?<=^|[{;])\s*(?:(?:margin|padding|border|inset|scroll-margin|scroll-padding)-(?:left|right)(?:-[a-z]+)?|left|right|border-(?:top|bottom)-(?:left|right)-radius)\s*:[^;}]*|(?<=^|[{;])\s*(?:text-align|float|clear)\s*:\s*(?:left|right)\b|(?<=^|[{;])\s*(?:background-position(?:-x)?|(?:-webkit-)?mask-position|object-position|transform-origin|perspective-origin)\s*:[^;}]*\b(?:left|right)\b/gm
|
||||
|
||||
// shorthands set the two inline sides independently; they are physical
|
||||
// whenever those values differ (values with function calls are skipped)
|
||||
const shorthand =
|
||||
/(?<=^|[{;])\s*(?:(margin|padding|inset)[ \t]*:(?![^;}]*\()[ \t]*([^\s;{}!]+)[ \t]+([^\s;{}!]+)[ \t]+([^\s;{}!]+)[ \t]+([^\s;{}!]+)|(border-radius)[ \t]*:(?![^;}]*\()[ \t]*([^\s;{}!]+)[ \t]+([^\s;{}!]+)(?:[ \t]+([^\s;{}!]+))?(?:[ \t]+([^\s;{}!]+))?)[ \t]*(?:!important)?[ \t]*(?=[;}])/gm
|
||||
|
||||
function styles(file: string): string {
|
||||
const source = readFileSync(root + file, 'utf8')
|
||||
if (file.endsWith('.css')) return source
|
||||
return Array.from(
|
||||
source.matchAll(/<style[^>]*>([\s\S]*?)<\/style>/g),
|
||||
(m) => m[1]
|
||||
).join('\n')
|
||||
}
|
||||
|
||||
function physicalDeclarations(css: string): string[] {
|
||||
const found = Array.from(css.matchAll(physicalInlineAxis), (m) => m[0].trim())
|
||||
for (const m of css.matchAll(shorthand)) {
|
||||
const decl = m[0].trim()
|
||||
if (m[1]) {
|
||||
// top right bottom left
|
||||
if (m[3] !== m[5]) found.push(decl)
|
||||
} else {
|
||||
// top-left top-right bottom-right bottom-left, expanded from the
|
||||
// two- and three-value forms
|
||||
const [tl, tr, br, bl] =
|
||||
m[10] !== undefined
|
||||
? [m[7], m[8], m[9], m[10]]
|
||||
: m[9] !== undefined
|
||||
? [m[7], m[8], m[9], m[8]]
|
||||
: [m[7], m[8], m[7], m[8]]
|
||||
if (tl !== tr || br !== bl) found.push(decl)
|
||||
}
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
describe('client/theme-default/logical-properties', () => {
|
||||
const files = readdirSync(root, { recursive: true, encoding: 'utf8' })
|
||||
.map((file) => file.replaceAll('\\', '/'))
|
||||
.filter((file) => /\.(vue|css)$/.test(file))
|
||||
.sort()
|
||||
|
||||
test('every allowlisted file still exists', () => {
|
||||
expect(
|
||||
Object.keys(physicalByDesign).filter((file) => !files.includes(file))
|
||||
).toEqual([])
|
||||
})
|
||||
|
||||
test.each(files)('%s uses no physical inline-axis properties', (file) => {
|
||||
const found = physicalDeclarations(styles(file))
|
||||
const allowed = [...(physicalByDesign[file] ?? [])]
|
||||
const unexpected = found.filter((decl) => {
|
||||
const i = allowed.indexOf(decl)
|
||||
if (i === -1) return true
|
||||
allowed.splice(i, 1)
|
||||
return false
|
||||
})
|
||||
expect(unexpected).toEqual([])
|
||||
expect(allowed).toEqual([])
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,73 @@
|
||||
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import path from 'node:path'
|
||||
|
||||
import {
|
||||
createMarkdownRenderer,
|
||||
disposeMdItInstance,
|
||||
type MarkdownRenderer
|
||||
} from 'node/markdown/markdown'
|
||||
|
||||
describe('image dimensions with URL suffixes', () => {
|
||||
let root: string
|
||||
let md: MarkdownRenderer
|
||||
|
||||
beforeAll(async () => {
|
||||
root = await mkdtemp(path.join(tmpdir(), 'vitepress-image-url-'))
|
||||
await mkdir(path.join(root, 'public'))
|
||||
for (const file of [
|
||||
'diagram.svg',
|
||||
'diagram space.svg',
|
||||
'diagram#hash.svg',
|
||||
'public/diagram.svg'
|
||||
]) {
|
||||
await writeFile(
|
||||
path.join(root, file),
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="120" height="80"><g id="layer"/></svg>'
|
||||
)
|
||||
}
|
||||
disposeMdItInstance()
|
||||
md = await createMarkdownRenderer(root, { highlight: (code) => code })
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
disposeMdItInstance()
|
||||
await rm(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
describe.each([
|
||||
'./diagram.svg',
|
||||
'/diagram.svg',
|
||||
'./diagram%20space.svg',
|
||||
'./diagram%23hash.svg'
|
||||
])('%s', (pathname) => {
|
||||
test.each(['?v=1', '#layer', '?v=1#layer'])(
|
||||
'reads dimensions while preserving %s',
|
||||
async (suffix) => {
|
||||
const src = pathname + suffix
|
||||
const html = await md.renderAsync(``, {
|
||||
path: path.join(root, 'index.md')
|
||||
})
|
||||
|
||||
expect(html).toContain(`src="${decodeURIComponent(src)}"`)
|
||||
expect(html).toContain('width="120"')
|
||||
expect(html).toContain('height="80"')
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test.each([
|
||||
['width=240', '240', '160'],
|
||||
['height=40', '60', '40'],
|
||||
['width=240 height=90', '240', '90']
|
||||
])('respects explicit dimensions: %s', async (attrs, width, height) => {
|
||||
const html = await md.renderAsync(
|
||||
`{${attrs}}`,
|
||||
{ path: path.join(root, 'index.md') }
|
||||
)
|
||||
|
||||
expect(html).toContain('src="/diagram.svg?v=1#layer"')
|
||||
expect(html).toContain(`width="${width}"`)
|
||||
expect(html).toContain(`height="${height}"`)
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,73 @@
|
||||
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 { compileScript, parse } from 'vue/compiler-sfc'
|
||||
|
||||
describe('page data script serialization', () => {
|
||||
let root: string
|
||||
let file: string
|
||||
let render: Awaited<ReturnType<typeof createMarkdownToVueRenderFn>>
|
||||
|
||||
beforeAll(async () => {
|
||||
root = await mkdtemp(path.join(tmpdir(), 'vitepress-page-data-'))
|
||||
file = path.join(root, 'index.md')
|
||||
await writeFile(file, '# Page\n')
|
||||
const siteConfig = await resolveConfig(root, 'build', 'production')
|
||||
disposeMdItInstance()
|
||||
render = await createMarkdownToVueRenderFn(
|
||||
root,
|
||||
{ cache: false },
|
||||
'/',
|
||||
false,
|
||||
false,
|
||||
siteConfig
|
||||
)
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
disposeMdItInstance()
|
||||
await rm(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
describe.each([
|
||||
['no script', ''],
|
||||
['named export', '<script>\nexport const custom = true\n</script>'],
|
||||
[
|
||||
'default export',
|
||||
'<script>\nexport default { name: "Custom" }\n</script>'
|
||||
],
|
||||
['script setup', '<script setup>\nconst custom = true\n</script>']
|
||||
])('%s', (_, script) => {
|
||||
test.each([
|
||||
'Document </script> tags literally',
|
||||
"Regular-expression substitutions: $&, $`, $' and $$"
|
||||
])('preserves %j', async (description) => {
|
||||
const src = [
|
||||
'---',
|
||||
`description: ${JSON.stringify(description)}`,
|
||||
'---',
|
||||
'',
|
||||
'# Page',
|
||||
'',
|
||||
script
|
||||
].join('\n')
|
||||
|
||||
const result = await render(src, file)
|
||||
const { descriptor, errors } = parse(result.vueSrc, { filename: file })
|
||||
|
||||
expect(errors).toEqual([])
|
||||
expect(() => compileScript(descriptor, { id: 'page-data' })).not.toThrow()
|
||||
const module = await import(
|
||||
'data:text/javascript;base64,' +
|
||||
Buffer.from(descriptor.script!.content).toString('base64')
|
||||
)
|
||||
expect(module.__pageData).toEqual(result.pageData)
|
||||
expect(module.__pageData.description).toBe(description)
|
||||
expect(module.__pageData.frontmatter.description).toBe(description)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,38 @@
|
||||
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import path from 'node:path'
|
||||
|
||||
import { glob } from 'node/utils/glob'
|
||||
|
||||
describe('node/utils/glob', () => {
|
||||
let root: string
|
||||
|
||||
beforeEach(async () => {
|
||||
root = await mkdtemp(path.join(tmpdir(), 'vitepress-glob-'))
|
||||
for (const dir of ['drafts', 'dist', 'node_modules']) {
|
||||
await mkdir(path.join(root, dir))
|
||||
await writeFile(path.join(root, dir, 'post.md'), '# Post')
|
||||
}
|
||||
await writeFile(path.join(root, 'published.md'), '# Published')
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test.each([{ ignore: 'drafts/**' }, { ignore: ['drafts/**'] }])(
|
||||
'accepts an ignore pattern as $ignore',
|
||||
async ({ ignore }) => {
|
||||
expect(await glob(['**/*.md'], { cwd: root, ignore })).toEqual([
|
||||
'published.md'
|
||||
])
|
||||
}
|
||||
)
|
||||
|
||||
test('keeps the default exclusions without a custom ignore pattern', async () => {
|
||||
expect(await glob(['**/*.md'], { cwd: root })).toEqual([
|
||||
'drafts/post.md',
|
||||
'published.md'
|
||||
])
|
||||
})
|
||||
})
|
||||
@ -1,8 +0,0 @@
|
||||
{
|
||||
"plugins": {
|
||||
"postcss-rtlcss": {
|
||||
"ltrPrefix": ":where([dir=\"ltr\"])",
|
||||
"rtlPrefix": ":where([dir=\"rtl\"])"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,18 +0,0 @@
|
||||
diff --git a/dist/rolldown.mjs b/dist/rolldown.mjs
|
||||
index 7e04d96c703656b81220193b1f01426778b6a42f..26e96378fc0ad985237302e673ed0ab07674bf41 100644
|
||||
--- a/dist/rolldown.mjs
|
||||
+++ b/dist/rolldown.mjs
|
||||
@@ -196,10 +196,11 @@ function resolveCache(options) {
|
||||
async function transpileScript(code, filename = "__sfc.ts") {
|
||||
const result = await transform(filename, code, {
|
||||
lang: "ts",
|
||||
- sourcemap: false
|
||||
+ sourcemap: false,
|
||||
+ typescript: { onlyRemoveTypeImports: true }
|
||||
});
|
||||
if (result.errors.length) throw new AggregateError(result.errors, `[vue-sfc-transformer] failed to transpile script in ${filename}`);
|
||||
- return result.code ?? code;
|
||||
+ return (result.code ?? code).replace(/\n?export \{\};?[\s\n]*$/, "");
|
||||
}
|
||||
function vueSfcPlugin(pluginOptions) {
|
||||
const cwd = pluginOptions.cwd ?? process.cwd();
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue