diff --git a/README.md b/README.md
index 4d8977e..87fea03 100644
--- a/README.md
+++ b/README.md
@@ -158,7 +158,7 @@ Markdown 图片默认保持比例并限制在内容区,也可以显式覆盖
{fit="cover" position="50% 30%" max-height="65vh"}
```
-主题默认使用 `1024 × 768` 的 4:3 画布。表格默认字号为 `30px`;代码默认字号为 `28px`,未指定高度时会从当前位置自动延伸到所在内容区底部,长行会正常换行,超出后只上下滚动。需要按页调整时,在该页 frontmatter 中添加尺寸类:
+主题默认使用 `1024 × 768` 的 4:3 画布。表格默认字号为 `30px`;代码默认字号为 `28px`,未指定高度时会按照内容使用自然高度,并根据前后正文自动限制在所在内容区内,长行会正常换行,超出后只上下滚动。需要按页调整时,在该页 frontmatter 中添加尺寸类:
```md
---
@@ -180,7 +180,7 @@ const slides = await discover('slides')
````
-同样的包裹方式也适用于表格,例如 `
`。不添加固定高度类时,单个代码框会占满标题或前置正文之后的剩余区域;同一区域有多个代码框时保持内容高度并限制在内容区内。
+同样的包裹方式也适用于表格,例如 `
`。不添加固定高度类时,代码框保持内容的自然高度;内容较长时会自动扣除前后正文占用的空间并限制在内容区内。同一区域有多个代码框时也遵循这一规则。
公开放映页和演示者预览中的图片可以点击进入全屏预览,也可以聚焦图片后按 `Enter` 或空格打开;通过关闭按钮、点击背景或按 `Esc` 返回幻灯片。预览底部提供 `− / 百分比 / +` 工具栏,100% 对应图片原始像素尺寸;还可以使用鼠标滚轮、触控板或触屏双指缩放,并在放大后拖动查看细节,缩放范围为 10%–400%。
diff --git a/dist-cli/theme/components/ContentScaleControls.vue b/dist-cli/theme/components/ContentScaleControls.vue
index 6dc68d2..858f963 100644
--- a/dist-cli/theme/components/ContentScaleControls.vue
+++ b/dist-cli/theme/components/ContentScaleControls.vue
@@ -927,7 +927,8 @@ function hasExplicitCodeHeight(code: HTMLPreElement) {
function availableCodeHeight(code: HTMLPreElement, region: HTMLElement) {
const layout = code.closest('.slidev-layout')
- if (!layout || layout.clientHeight <= 0)
+ const host = code.closest('.easy-content-code-host')
+ if (!layout || !host || layout.clientHeight <= 0)
return 0
const layoutRect = layout.getBoundingClientRect()
const regionRect = region.getBoundingClientRect()
@@ -941,7 +942,33 @@ function availableCodeHeight(code: HTMLPreElement, region: HTMLElement) {
const layoutPaddingBottom = Number.parseFloat(getComputedStyle(layout).paddingBottom) || 0
const regionBottom = regionRect.bottom - regionPaddingBottom * scale
const layoutBottom = layoutRect.bottom - layoutPaddingBottom * scale
- return Math.max(0, Math.floor((Math.min(regionBottom, layoutBottom) - codeRect.top) / scale))
+ const contentBottom = Math.min(regionBottom, layoutBottom)
+ let trailingBottom = codeRect.bottom
+
+ region.querySelectorAll('*').forEach((element) => {
+ if (host.contains(element) || !(host.compareDocumentPosition(element) & Node.DOCUMENT_POSITION_FOLLOWING))
+ return
+ const style = getComputedStyle(element)
+ if (style.display === 'none' || style.position === 'absolute' || style.position === 'fixed')
+ return
+ const rect = element.getBoundingClientRect()
+ if (rect.width <= 0 || rect.height <= 0)
+ return
+ const marginBottom = Number.parseFloat(style.marginBottom) || 0
+ trailingBottom = Math.max(trailingBottom, rect.bottom + marginBottom * scale)
+ })
+
+ const trailingHeight = Math.max(0, trailingBottom - codeRect.bottom)
+ const codeStyle = getComputedStyle(code)
+ const minimumHeight = (
+ (Number.parseFloat(codeStyle.lineHeight) || 0)
+ + (Number.parseFloat(codeStyle.paddingTop) || 0)
+ + (Number.parseFloat(codeStyle.paddingBottom) || 0)
+ + (Number.parseFloat(codeStyle.borderTopWidth) || 0)
+ + (Number.parseFloat(codeStyle.borderBottomWidth) || 0)
+ )
+ const available = (contentBottom - codeRect.top - trailingHeight) / scale
+ return Math.max(1, Math.floor(Math.max(minimumHeight, available)))
}
function setHostLength(host: HTMLElement, property: string, value?: number) {
@@ -961,7 +988,6 @@ function fitCodeHeights() {
if (!host)
return
if (hasExplicitCodeHeight(code)) {
- setHostLength(host, '--easy-code-auto-height')
setHostLength(host, '--easy-code-auto-max-height')
return
}
@@ -974,7 +1000,6 @@ function fitCodeHeights() {
})
groups.forEach((codes, region) => {
- const single = codes.length === 1
codes.forEach((code) => {
const host = code.closest('.easy-content-code-host')
if (!host)
@@ -982,7 +1007,6 @@ function fitCodeHeights() {
const available = availableCodeHeight(code, region)
if (!available)
return
- setHostLength(host, '--easy-code-auto-height', single ? available : undefined)
setHostLength(host, '--easy-code-auto-max-height', available)
})
})
@@ -1048,7 +1072,6 @@ function cleanupCodeHosts() {
const base = code && codeStyles.get(code)
host.querySelector(':scope > .easy-content-scale-toolbar')?.remove()
host.style.removeProperty('--easy-code-scaled-font-size')
- host.style.removeProperty('--easy-code-auto-height')
host.style.removeProperty('--easy-code-auto-max-height')
host.classList.remove('easy-content-scale-host', 'easy-content-code-host')
delete host.dataset.easyScaleKey
diff --git a/dist-cli/theme/styles/base.css b/dist-cli/theme/styles/base.css
index 6975ab3..a656099 100644
--- a/dist-cli/theme/styles/base.css
+++ b/dist-cli/theme/styles/base.css
@@ -135,8 +135,8 @@
.slidev-layout pre {
box-sizing: border-box;
- height: var(--easy-code-height, var(--easy-code-auto-height, auto));
- max-height: var(--easy-code-height, var(--easy-code-auto-max-height, var(--easy-code-auto-height, none)));
+ height: var(--easy-code-height, auto);
+ max-height: var(--easy-code-height, var(--easy-code-auto-max-height, none));
overflow-x: hidden;
overflow-y: auto;
border-radius: 10px;
diff --git a/examples/content-repo/slides/getting-started/slides.md b/examples/content-repo/slides/getting-started/slides.md
index 83b9b3c..5e3375c 100644
--- a/examples/content-repo/slides/getting-started/slides.md
+++ b/examples/content-repo/slides/getting-started/slides.md
@@ -144,6 +144,25 @@ layout: default
表格和代码尺寸既可以按页设置,也可以用 div 包裹后只调整一个内容块。
-->
+---
+layout: default
+---
+
+## 智能代码高度
+
+```text
+用户提问 → 模型回答
+
+看起来:一次请求,一台“AI”
+实际上:网关 → 调度 → 推理 → 检索 → 存储
+```
+
+### 代码之后的正文仍保留在内容区内
+
+
+
---
layout: quote
---
diff --git a/packages/slidev-theme-easy-jyy/components/ContentScaleControls.vue b/packages/slidev-theme-easy-jyy/components/ContentScaleControls.vue
index 6dc68d2..858f963 100644
--- a/packages/slidev-theme-easy-jyy/components/ContentScaleControls.vue
+++ b/packages/slidev-theme-easy-jyy/components/ContentScaleControls.vue
@@ -927,7 +927,8 @@ function hasExplicitCodeHeight(code: HTMLPreElement) {
function availableCodeHeight(code: HTMLPreElement, region: HTMLElement) {
const layout = code.closest('.slidev-layout')
- if (!layout || layout.clientHeight <= 0)
+ const host = code.closest('.easy-content-code-host')
+ if (!layout || !host || layout.clientHeight <= 0)
return 0
const layoutRect = layout.getBoundingClientRect()
const regionRect = region.getBoundingClientRect()
@@ -941,7 +942,33 @@ function availableCodeHeight(code: HTMLPreElement, region: HTMLElement) {
const layoutPaddingBottom = Number.parseFloat(getComputedStyle(layout).paddingBottom) || 0
const regionBottom = regionRect.bottom - regionPaddingBottom * scale
const layoutBottom = layoutRect.bottom - layoutPaddingBottom * scale
- return Math.max(0, Math.floor((Math.min(regionBottom, layoutBottom) - codeRect.top) / scale))
+ const contentBottom = Math.min(regionBottom, layoutBottom)
+ let trailingBottom = codeRect.bottom
+
+ region.querySelectorAll('*').forEach((element) => {
+ if (host.contains(element) || !(host.compareDocumentPosition(element) & Node.DOCUMENT_POSITION_FOLLOWING))
+ return
+ const style = getComputedStyle(element)
+ if (style.display === 'none' || style.position === 'absolute' || style.position === 'fixed')
+ return
+ const rect = element.getBoundingClientRect()
+ if (rect.width <= 0 || rect.height <= 0)
+ return
+ const marginBottom = Number.parseFloat(style.marginBottom) || 0
+ trailingBottom = Math.max(trailingBottom, rect.bottom + marginBottom * scale)
+ })
+
+ const trailingHeight = Math.max(0, trailingBottom - codeRect.bottom)
+ const codeStyle = getComputedStyle(code)
+ const minimumHeight = (
+ (Number.parseFloat(codeStyle.lineHeight) || 0)
+ + (Number.parseFloat(codeStyle.paddingTop) || 0)
+ + (Number.parseFloat(codeStyle.paddingBottom) || 0)
+ + (Number.parseFloat(codeStyle.borderTopWidth) || 0)
+ + (Number.parseFloat(codeStyle.borderBottomWidth) || 0)
+ )
+ const available = (contentBottom - codeRect.top - trailingHeight) / scale
+ return Math.max(1, Math.floor(Math.max(minimumHeight, available)))
}
function setHostLength(host: HTMLElement, property: string, value?: number) {
@@ -961,7 +988,6 @@ function fitCodeHeights() {
if (!host)
return
if (hasExplicitCodeHeight(code)) {
- setHostLength(host, '--easy-code-auto-height')
setHostLength(host, '--easy-code-auto-max-height')
return
}
@@ -974,7 +1000,6 @@ function fitCodeHeights() {
})
groups.forEach((codes, region) => {
- const single = codes.length === 1
codes.forEach((code) => {
const host = code.closest('.easy-content-code-host')
if (!host)
@@ -982,7 +1007,6 @@ function fitCodeHeights() {
const available = availableCodeHeight(code, region)
if (!available)
return
- setHostLength(host, '--easy-code-auto-height', single ? available : undefined)
setHostLength(host, '--easy-code-auto-max-height', available)
})
})
@@ -1048,7 +1072,6 @@ function cleanupCodeHosts() {
const base = code && codeStyles.get(code)
host.querySelector(':scope > .easy-content-scale-toolbar')?.remove()
host.style.removeProperty('--easy-code-scaled-font-size')
- host.style.removeProperty('--easy-code-auto-height')
host.style.removeProperty('--easy-code-auto-max-height')
host.classList.remove('easy-content-scale-host', 'easy-content-code-host')
delete host.dataset.easyScaleKey
diff --git a/packages/slidev-theme-easy-jyy/styles/base.css b/packages/slidev-theme-easy-jyy/styles/base.css
index 6975ab3..a656099 100644
--- a/packages/slidev-theme-easy-jyy/styles/base.css
+++ b/packages/slidev-theme-easy-jyy/styles/base.css
@@ -135,8 +135,8 @@
.slidev-layout pre {
box-sizing: border-box;
- height: var(--easy-code-height, var(--easy-code-auto-height, auto));
- max-height: var(--easy-code-height, var(--easy-code-auto-max-height, var(--easy-code-auto-height, none)));
+ height: var(--easy-code-height, auto);
+ max-height: var(--easy-code-height, var(--easy-code-auto-max-height, none));
overflow-x: hidden;
overflow-y: auto;
border-radius: 10px;
diff --git a/tests/core.test.ts b/tests/core.test.ts
index 2a047cd..3588141 100644
--- a/tests/core.test.ts
+++ b/tests/core.test.ts
@@ -65,7 +65,9 @@ describe('theme defaults', () => {
expect(themePackage.dependencies['@slidev/client']).toBe('52.19.1')
expect(themeCss).toContain('--easy-accent: #1d4ed8')
expect(themeCss.slice(0, themeCss.indexOf('.easy-table-sm'))).not.toContain('--easy-code-height')
- expect(themeCss).toContain('var(--easy-code-auto-height, auto)')
+ expect(themeCss).toContain('height: var(--easy-code-height, auto)')
+ expect(themeCss).toContain('var(--easy-code-auto-max-height, none)')
+ expect(themeCss).not.toContain('--easy-code-auto-height')
expect(themeCss).toContain('--easy-code-scaled-font-size')
expect(themeCss).toContain('overflow-x: hidden')
expect(themeCss).toContain('white-space: pre-wrap')
diff --git a/tests/e2e/presenter.spec.ts b/tests/e2e/presenter.spec.ts
index e962f65..942ecfd 100644
--- a/tests/e2e/presenter.spec.ts
+++ b/tests/e2e/presenter.spec.ts
@@ -261,14 +261,14 @@ test('presenter advances animations, switches notes, shows timing, and syncs the
await presenter.getByTitle('Go to next slide').click()
await expect(page.getByRole('heading', { name: '内容优先' })).toBeVisible()
await expect(presenter.getByText('依次介绍四个核心能力')).toBeVisible()
- await expect(presenter.getByText('2 / 8', { exact: true })).toBeVisible()
+ await expect(presenter.getByText('2 / 9', { exact: true })).toBeVisible()
await presenter.getByTitle('Go to next slide').click()
await expect(presenter.getByText('1/4', { exact: true })).toBeVisible()
await expect(page).toHaveURL(/#\/2\?clicks=1$/)
})
-test('table and code controls adjust font size while code fills the available height', async ({ page }) => {
+test('table and code controls adjust font size while code uses smart height', async ({ page }) => {
await page.goto('/getting-started/#/7')
const table = page.locator('.slidev-layout table').first()
const tablePreset = table.locator('..').locator('..')
@@ -311,15 +311,8 @@ test('table and code controls adjust font size while code fills the available he
await expect.poll(() => code.evaluate(element => Number.parseFloat(getComputedStyle(element).fontSize))).toBe(initialCodeSize)
await expect.poll(() => code.evaluate((element) => {
- const region = element.closest('.easy-two-cols-grid > div')!
- const layout = element.closest('.slidev-layout') as HTMLElement
- const scale = layout.getBoundingClientRect().height / layout.clientHeight
- const contentBottom = Math.min(
- region.getBoundingClientRect().bottom - Number.parseFloat(getComputedStyle(region).paddingBottom) * scale,
- layout.getBoundingClientRect().bottom - Number.parseFloat(getComputedStyle(layout).paddingBottom) * scale,
- )
- return Math.abs(contentBottom - element.getBoundingClientRect().bottom) / scale
- })).toBeLessThanOrEqual(3)
+ return element.scrollHeight - element.clientHeight
+ })).toBeGreaterThan(0)
const overflow = await code.evaluate(element => ({
horizontal: getComputedStyle(element).overflowX,
lineHeight: Number.parseFloat(getComputedStyle(element).lineHeight),
@@ -365,16 +358,46 @@ test('table and code controls adjust font size while code fills the available he
await layout.evaluate((element) => {
element.classList.remove('easy-code-height-sm', 'easy-code-height-md', 'easy-code-height-lg')
})
+ await expect.poll(() => code.evaluate(element => element.scrollHeight - element.clientHeight)).toBeGreaterThan(0)
+})
+
+test('auto-height code keeps its natural height and reserves room for following content', async ({ page }) => {
+ await page.goto('/getting-started/#/8')
+ const layout = page.locator('.slidev-page-8 .slidev-layout')
+ const code = layout.locator('pre').first()
+ const following = layout.getByRole('heading', { name: '代码之后的正文仍保留在内容区内' })
+
+ await expect(following).toBeVisible()
await expect.poll(() => code.evaluate((element) => {
- const region = element.closest('.easy-two-cols-grid > div')!
- const layout = element.closest('.slidev-layout') as HTMLElement
- const scale = layout.getBoundingClientRect().height / layout.clientHeight
- const contentBottom = Math.min(
- region.getBoundingClientRect().bottom - Number.parseFloat(getComputedStyle(region).paddingBottom) * scale,
- layout.getBoundingClientRect().bottom - Number.parseFloat(getComputedStyle(layout).paddingBottom) * scale,
- )
- return Math.abs(contentBottom - element.getBoundingClientRect().bottom) / scale
- })).toBeLessThanOrEqual(3)
+ const layoutElement = element.closest('.slidev-layout') as HTMLElement
+ const scale = layoutElement.getBoundingClientRect().height / layoutElement.clientHeight
+ return Math.abs(element.getBoundingClientRect().height / scale - element.scrollHeight)
+ })).toBeLessThanOrEqual(1)
+ const bounds = await layout.evaluate((element) => {
+ const codeElement = element.querySelector('pre')!
+ const followingElement = Array.from(element.querySelectorAll('h3')).find(item => item.textContent?.includes('代码之后的正文'))!
+ return {
+ codeBottom: codeElement.getBoundingClientRect().bottom,
+ followingBottom: followingElement.getBoundingClientRect().bottom,
+ followingTop: followingElement.getBoundingClientRect().top,
+ layoutBottom: element.getBoundingClientRect().bottom - Number.parseFloat(getComputedStyle(element).paddingBottom),
+ }
+ })
+ expect(bounds.codeBottom).toBeLessThan(bounds.followingTop)
+ expect(bounds.followingBottom).toBeLessThanOrEqual(bounds.layoutBottom + 1)
+
+ await code.evaluate((element) => {
+ element.textContent = Array.from({ length: 40 }, (_, index) => `line ${index + 1}: a deliberately long code sample`).join('\n')
+ })
+ await expect.poll(() => code.evaluate(element => element.scrollHeight - element.clientHeight)).toBeGreaterThan(100)
+ const longContentBounds = await layout.evaluate((element) => {
+ const followingElement = Array.from(element.querySelectorAll('h3')).find(item => item.textContent?.includes('代码之后的正文'))!
+ return {
+ followingBottom: followingElement.getBoundingClientRect().bottom,
+ layoutBottom: element.getBoundingClientRect().bottom - Number.parseFloat(getComputedStyle(element).paddingBottom),
+ }
+ })
+ expect(longContentBounds.followingBottom).toBeLessThanOrEqual(longContentBounds.layoutBottom + 1)
})
test('image fullscreen preserves layout and table scaling stays out of non-interactive routes', async ({ page }) => {
@@ -601,7 +624,7 @@ test('stacked image layouts constrain wide images by available height', async ({
test('sample slides stay inside the 4:3 canvas', async ({ page }) => {
await page.goto('/getting-started/#/1')
- for (let slide = 1; slide <= 8; slide += 1) {
+ for (let slide = 1; slide <= 9; slide += 1) {
const layout = page.locator('.slidev-layout').first()
await expect(layout).toBeVisible()
const box = await layout.boundingBox()