mirror of https://github.com/vuejs/vitepress
feat: auto-add width/height to local images to avoid layout shift (#5311)
Co-authored-by: Divyansh Singh <40380293+brc-dd@users.noreply.github.com>pull/4932/merge
parent
e21ffab582
commit
3868b64e41
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
@ -1,33 +1,141 @@
|
||||
import path from 'node:path'
|
||||
import { MarkdownItAsync } from 'markdown-it-async'
|
||||
import { imagePlugin } from 'node/markdown/plugins/image'
|
||||
import attrsPlugin from 'markdown-it-attrs'
|
||||
import { imagePlugin, type Options } from 'node/markdown/plugins/image'
|
||||
|
||||
describe('node/markdown/plugins/image', () => {
|
||||
const srcDir = path.resolve(import.meta.dirname, '../../../../e2e')
|
||||
const publicDir = path.join(srcDir, 'public')
|
||||
const env = { path: path.join(srcDir, 'index.md') }
|
||||
|
||||
function createRenderer(options?: Options) {
|
||||
const md = new MarkdownItAsync()
|
||||
imagePlugin(md as any)
|
||||
|
||||
test('default image output', async () => {
|
||||
const html = await md.renderAsync('')
|
||||
expect(html.trim()).toMatchInlineSnapshot(
|
||||
`"<p><img src="./foo.png" alt="logo"></p>"`
|
||||
)
|
||||
// same registration order as createMarkdownRenderer
|
||||
imagePlugin(md, publicDir, options)
|
||||
attrsPlugin(md as any)
|
||||
|
||||
return md
|
||||
}
|
||||
|
||||
describe('node/markdown/plugins/image', () => {
|
||||
const md = createRenderer()
|
||||
|
||||
describe('src normalization', () => {
|
||||
test('default image output', async () => {
|
||||
const html = await md.renderAsync('')
|
||||
|
||||
expect(html.trim()).toMatchInlineSnapshot(
|
||||
`"<p><img src="./foo.png" alt="logo"></p>"`
|
||||
)
|
||||
})
|
||||
|
||||
test.for([
|
||||
['foo.png', './foo.png'],
|
||||
['./foo.png', './foo.png'],
|
||||
['../foo.png', '../foo.png'],
|
||||
['../../foo.png', '../../foo.png'],
|
||||
['/foo.png', '/foo.png'],
|
||||
['https://example.com/foo.png', 'https://example.com/foo.png']
|
||||
])('normalizes image src: %s → %s', async ([src, expected]) => {
|
||||
const html = await md.renderAsync(``)
|
||||
|
||||
expect(html).toContain(`src="${expected}"`)
|
||||
})
|
||||
})
|
||||
|
||||
test.for([
|
||||
['foo.png', './foo.png'],
|
||||
['./foo.png', './foo.png'],
|
||||
['../foo.png', '../foo.png'],
|
||||
['../../foo.png', '../../foo.png'],
|
||||
['/foo.png', '/foo.png'],
|
||||
['https://example.com/foo.png', 'https://example.com/foo.png']
|
||||
])('normalizes image src: %s → %s', async ([src, expected]) => {
|
||||
const html = await md.renderAsync(``)
|
||||
expect(html).toContain(`src="${expected}"`)
|
||||
describe('dimensions', () => {
|
||||
test('adds width and height from local image dimensions', async () => {
|
||||
const html = await md.renderAsync('', env)
|
||||
|
||||
expect(html).toContain('width="48"')
|
||||
expect(html).toContain('height="48"')
|
||||
})
|
||||
|
||||
test('adds width and height from public image dimensions', async () => {
|
||||
const html = await md.renderAsync('', env)
|
||||
|
||||
expect(html).toContain('width="48"')
|
||||
expect(html).toContain('height="48"')
|
||||
})
|
||||
|
||||
test('adds width and height when the image url is encoded', async () => {
|
||||
const html = await md.renderAsync(
|
||||
'',
|
||||
env
|
||||
)
|
||||
|
||||
expect(html).toContain('src="./assets/vitepress logo.png"')
|
||||
expect(html).toContain('width="48"')
|
||||
expect(html).toContain('height="48"')
|
||||
})
|
||||
|
||||
test('does not override explicit width and height', async () => {
|
||||
const html = await md.renderAsync(
|
||||
'{width=100 height=200}',
|
||||
env
|
||||
)
|
||||
|
||||
expect(html).toContain('width="100"')
|
||||
expect(html).toContain('height="200"')
|
||||
})
|
||||
|
||||
test('scales height proportionally when only width is set', async () => {
|
||||
const html = await md.renderAsync(
|
||||
'{width=96}',
|
||||
env
|
||||
)
|
||||
|
||||
// 48x48 image scaled to width=96 → height=96
|
||||
expect(html).toContain('width="96"')
|
||||
expect(html).toContain('height="96"')
|
||||
})
|
||||
|
||||
test('scales width proportionally when only height is set', async () => {
|
||||
const html = await md.renderAsync(
|
||||
'{height=24}',
|
||||
env
|
||||
)
|
||||
|
||||
// 48x48 image scaled to height=24 → width=24
|
||||
expect(html).toContain('width="24"')
|
||||
expect(html).toContain('height="24"')
|
||||
})
|
||||
|
||||
test('ignores non-numeric width when scaling', async () => {
|
||||
const html = await md.renderAsync(
|
||||
'{width=50%}',
|
||||
env
|
||||
)
|
||||
|
||||
expect(html).toContain('width="50%"')
|
||||
expect(html).not.toContain('height=')
|
||||
})
|
||||
|
||||
test('does not add dimensions for external images', async () => {
|
||||
const html = await md.renderAsync(
|
||||
'',
|
||||
env
|
||||
)
|
||||
|
||||
expect(html).not.toContain('width=')
|
||||
expect(html).not.toContain('height=')
|
||||
})
|
||||
})
|
||||
|
||||
test('adds loading="lazy" attribute when lazyLoading is enabled', async () => {
|
||||
const mdLazy = new MarkdownItAsync()
|
||||
imagePlugin(mdLazy as any, { lazyLoading: true })
|
||||
const html = await mdLazy.renderAsync('')
|
||||
expect(html).toContain('loading="lazy"')
|
||||
describe('lazy loading', () => {
|
||||
const mdLazy = createRenderer({ lazyLoading: true })
|
||||
|
||||
test('adds loading="lazy" when lazyLoading is enabled', async () => {
|
||||
const html = await mdLazy.renderAsync('')
|
||||
|
||||
expect(html).toContain('loading="lazy"')
|
||||
})
|
||||
|
||||
test('does not override user-specified loading strategy', async () => {
|
||||
const html = await mdLazy.renderAsync('{loading=eager}')
|
||||
|
||||
expect(html).toContain('loading="eager"')
|
||||
expect(html).not.toContain('loading="lazy"')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Loading…
Reference in new issue