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/5318/head
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 { 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()
|
const md = new MarkdownItAsync()
|
||||||
imagePlugin(md as any)
|
|
||||||
|
|
||||||
test('default image output', async () => {
|
// same registration order as createMarkdownRenderer
|
||||||
const html = await md.renderAsync('')
|
imagePlugin(md, publicDir, options)
|
||||||
expect(html.trim()).toMatchInlineSnapshot(
|
attrsPlugin(md as any)
|
||||||
`"<p><img src="./foo.png" alt="logo"></p>"`
|
|
||||||
)
|
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([
|
describe('dimensions', () => {
|
||||||
['foo.png', './foo.png'],
|
test('adds width and height from local image dimensions', async () => {
|
||||||
['./foo.png', './foo.png'],
|
const html = await md.renderAsync('', env)
|
||||||
['../foo.png', '../foo.png'],
|
|
||||||
['../../foo.png', '../../foo.png'],
|
expect(html).toContain('width="48"')
|
||||||
['/foo.png', '/foo.png'],
|
expect(html).toContain('height="48"')
|
||||||
['https://example.com/foo.png', 'https://example.com/foo.png']
|
})
|
||||||
])('normalizes image src: %s → %s', async ([src, expected]) => {
|
|
||||||
const html = await md.renderAsync(``)
|
test('adds width and height from public image dimensions', async () => {
|
||||||
expect(html).toContain(`src="${expected}"`)
|
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 () => {
|
describe('lazy loading', () => {
|
||||||
const mdLazy = new MarkdownItAsync()
|
const mdLazy = createRenderer({ lazyLoading: true })
|
||||||
imagePlugin(mdLazy as any, { lazyLoading: true })
|
|
||||||
const html = await mdLazy.renderAsync('')
|
test('adds loading="lazy" when lazyLoading is enabled', async () => {
|
||||||
expect(html).toContain('loading="lazy"')
|
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