Skip to content

Commit 8b4de6f

Browse files
committed
Fix code block overflow when callout boxes are on the same slide
1 parent 1f59c0c commit 8b4de6f

3 files changed

Lines changed: 161 additions & 2 deletions

File tree

src/cdl_slides/assets/themes/cdl-theme.css

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3713,6 +3713,55 @@ section.table-compact td {
37133713
padding: 0.2em 0.4em !important;
37143714
}
37153715

3716+
/* ============================================
3717+
CODE BLOCKS + CALLOUT BOXES COEXISTENCE
3718+
When code blocks share a slide with callout boxes,
3719+
reduce code block max-height and font-size to prevent
3720+
overlap. Uses preprocessor-injected classes.
3721+
============================================ */
3722+
3723+
section.has-code-block.has-callouts-1,
3724+
section.has-code-block.has-callouts-2,
3725+
section.has-code-block.has-callouts-3 {
3726+
display: flex !important;
3727+
flex-direction: column !important;
3728+
overflow: hidden !important;
3729+
}
3730+
3731+
section.has-code-block.has-callouts-1 > pre,
3732+
section.has-code-block.has-callouts-1 > :is(pre, marp-pre) {
3733+
max-height: 45vh !important;
3734+
flex-shrink: 1 !important;
3735+
overflow: hidden !important;
3736+
}
3737+
3738+
section.has-code-block.has-callouts-2 > pre,
3739+
section.has-code-block.has-callouts-2 > :is(pre, marp-pre) {
3740+
max-height: 35vh !important;
3741+
font-size: 16pt !important;
3742+
flex-shrink: 1 !important;
3743+
overflow: hidden !important;
3744+
}
3745+
3746+
section.has-code-block.has-callouts-3 > pre,
3747+
section.has-code-block.has-callouts-3 > :is(pre, marp-pre) {
3748+
max-height: 25vh !important;
3749+
font-size: 14pt !important;
3750+
flex-shrink: 1 !important;
3751+
overflow: hidden !important;
3752+
}
3753+
3754+
section.has-code-block[class*="has-callouts"] > .callout,
3755+
section.has-code-block[class*="has-callouts"] > .note-box,
3756+
section.has-code-block[class*="has-callouts"] > .example-box,
3757+
section.has-code-block[class*="has-callouts"] > .warning-box,
3758+
section.has-code-block[class*="has-callouts"] > .tip-box,
3759+
section.has-code-block[class*="has-callouts"] > .important-box,
3760+
section.has-code-block[class*="has-callouts"] > .definition-box {
3761+
flex-shrink: 0 !important;
3762+
flex-grow: 0 !important;
3763+
}
3764+
37163765
/* ============================================
37173766
ELEMENT-LEVEL SCALING (wrap individual elements)
37183767
Usage: <div class="sized-70">...</div>

src/cdl_slides/preprocessor.py

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -961,13 +961,13 @@ def replace_arrow(match):
961961
# Content height weights (approximate units where 1 unit ~ 30px)
962962
CONTENT_WEIGHTS = {
963963
# Recalibrated based on visual inspection of actual slides
964-
# A slide with H1 + 2 callout boxes + 4 list items should estimate ~10 units (uses ~50% of slide)
964+
# A slide with H1 + 2 callout boxes + 4 list items should estimate ~14 units (uses ~70% of slide)
965965
"h1": 2.0,
966966
"h2": 1.8,
967967
"h3": 1.5,
968968
"paragraph_per_50_chars": 0.4, # Reduced - text wraps efficiently
969969
"list_item": 0.7, # Reduced - list items are compact
970-
"callout_box_base": 2.5, # Reduced - callout overhead is smaller than estimated
970+
"callout_box_base": 4.5, # Each callout box ~80-90px (title + padding + content)
971971
"callout_content_per_50_chars": 0.3,
972972
"code_block_line": 0.6, # Code lines are relatively compact
973973
"table_header": 1.2,
@@ -998,6 +998,21 @@ def replace_arrow(match):
998998
}
999999

10001000

1001+
def _estimate_callout_content_height(slide_content: str) -> float:
1002+
"""Estimate additional height from text content inside callout boxes."""
1003+
total = 0.0
1004+
callout_pattern = re.compile(
1005+
r'<div\s+class="[^"]*(?:note|warning|tip|example|definition|important|callout)-box[^"]*"'
1006+
r"[^>]*>(.*?)</div>",
1007+
re.DOTALL,
1008+
)
1009+
for match in callout_pattern.finditer(slide_content):
1010+
content = match.group(1)
1011+
text_only = re.sub(r"<[^>]+>", "", content).strip()
1012+
total += (len(text_only) / 50) * CONTENT_WEIGHTS["callout_content_per_50_chars"]
1013+
return total
1014+
1015+
10011016
def compute_available_code_lines(slide_content: str, default_max: int = 20) -> int:
10021017
"""
10031018
Compute max code lines for a slide based on other content and scale class.
@@ -1020,6 +1035,7 @@ def compute_available_code_lines(slide_content: str, default_max: int = 20) -> i
10201035
other_height += CONTENT_WEIGHTS["h1"]
10211036

10221037
callout_height = metrics["callout_count"] * CONTENT_WEIGHTS["callout_box_base"]
1038+
callout_height += _estimate_callout_content_height(slide_content)
10231039
list_height = metrics["list_items"] * CONTENT_WEIGHTS["list_item"]
10241040

10251041
if metrics["has_two_column"] and metrics["callout_count"] >= 2:
@@ -1314,6 +1330,32 @@ def inject_scale_class(slide_content: str, scale_class: str) -> str:
13141330
return "\n".join(lines)
13151331

13161332

1333+
def inject_layout_classes(slide_content: str, classes: list) -> str:
1334+
"""Inject layout helper classes into the slide's _class directive."""
1335+
if not classes:
1336+
return slide_content
1337+
1338+
existing_class = re.search(r"<!--\s*_class:\s*([^>]*)\s*-->", slide_content)
1339+
if existing_class:
1340+
old_directive = existing_class.group(0)
1341+
old_classes = existing_class.group(1).strip()
1342+
new_classes = [c for c in classes if c not in old_classes]
1343+
if not new_classes:
1344+
return slide_content
1345+
new_directive = f"<!-- _class: {old_classes} {' '.join(new_classes)} -->"
1346+
return slide_content.replace(old_directive, new_directive)
1347+
1348+
lines = slide_content.split("\n")
1349+
insert_idx = 0
1350+
for i, line in enumerate(lines):
1351+
if line.strip():
1352+
insert_idx = i
1353+
break
1354+
1355+
lines.insert(insert_idx, f"<!-- _class: {' '.join(classes)} -->")
1356+
return "\n".join(lines)
1357+
1358+
13171359
def analyze_and_warn_slides(content, filename="unknown"):
13181360
"""
13191361
Analyze all slides in content and generate warnings.
@@ -1354,6 +1396,13 @@ def analyze_and_warn_slides(content, filename="unknown"):
13541396
)
13551397
slide = inject_scale_class(slide, scale_class)
13561398

1399+
layout_classes = []
1400+
if metrics["callout_count"] > 0 and metrics["has_code_block"]:
1401+
layout_classes.append(f"has-callouts-{min(metrics['callout_count'], 3)}")
1402+
layout_classes.append("has-code-block")
1403+
if layout_classes:
1404+
slide = inject_layout_classes(slide, layout_classes)
1405+
13571406
modified_parts.append(slide)
13581407

13591408
# Rejoin with slide separators

tests/test_preprocessor.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
from cdl_slides.preprocessor import (
66
PYGMENTS_AVAILABLE,
77
analyze_slide_content,
8+
compute_available_code_lines,
89
determine_scale_class,
910
highlight_code_line,
11+
inject_layout_classes,
1012
parse_flow_node,
1113
process_arrow_syntax,
1214
process_flow_blocks,
@@ -408,3 +410,62 @@ def test_malformed_animate_produces_warning(self, copy_fixture_to_work, work_dir
408410
process_markdown(str(src), str(output))
409411
result = output.read_text(encoding="utf-8")
410412
assert "warning-box" in result or "```animate" in result
413+
414+
415+
class TestCodeBlockWithCallouts:
416+
def test_available_lines_reduced_with_one_callout(self):
417+
slide_no_callout = "<!-- _class: scale-80 -->\n# Title\n\n```python\nx = 1\n```\n"
418+
slide_one_callout = (
419+
"<!-- _class: scale-80 -->\n# Title\n\n```python\nx = 1\n```\n\n"
420+
'<div class="note-box">\nSome note.\n</div>\n'
421+
)
422+
lines_no_callout = compute_available_code_lines(slide_no_callout, default_max=30)
423+
lines_one_callout = compute_available_code_lines(slide_one_callout, default_max=30)
424+
assert lines_one_callout < lines_no_callout
425+
426+
def test_available_lines_reduced_with_two_callouts(self):
427+
slide_one = (
428+
"<!-- _class: scale-80 -->\n# Title\n\n```python\nx = 1\n```\n\n"
429+
'<div class="note-box">\nSome note.\n</div>\n'
430+
)
431+
slide_two = (
432+
"<!-- _class: scale-80 -->\n# Title\n\n```python\nx = 1\n```\n\n"
433+
'<div class="note-box">\nSome note.\n</div>\n\n'
434+
'<div class="example-box">\nSome example.\n</div>\n'
435+
)
436+
lines_one = compute_available_code_lines(slide_one)
437+
lines_two = compute_available_code_lines(slide_two)
438+
assert lines_two < lines_one
439+
440+
def test_layout_classes_injected_for_code_and_callouts(self):
441+
from cdl_slides.preprocessor import analyze_and_warn_slides
442+
443+
content = (
444+
"---\nmarp: true\ntheme: cdl-theme\n---\n\n"
445+
"# Title\n\n"
446+
"```python\nx = 1\n```\n\n"
447+
'<div class="note-box">\nNote.\n</div>\n\n'
448+
'<div class="example-box">\nExample.\n</div>\n'
449+
)
450+
modified, warnings = analyze_and_warn_slides(content)
451+
assert "has-callouts-2" in modified
452+
assert "has-code-block" in modified
453+
454+
def test_no_layout_classes_without_code_block(self):
455+
from cdl_slides.preprocessor import analyze_and_warn_slides
456+
457+
content = (
458+
"---\nmarp: true\ntheme: cdl-theme\n---\n\n"
459+
"# Title\n\n"
460+
'<div class="note-box">\nNote.\n</div>\n\n'
461+
'<div class="example-box">\nExample.\n</div>\n'
462+
)
463+
modified, warnings = analyze_and_warn_slides(content)
464+
assert "has-callouts" not in modified
465+
466+
def test_layout_classes_appended_to_existing_scale(self):
467+
slide = "<!-- _class: scale-80 -->\n# Title\n\n```python\nx=1\n```\n"
468+
result = inject_layout_classes(slide, ["has-callouts-1", "has-code-block"])
469+
assert "scale-80" in result
470+
assert "has-callouts-1" in result
471+
assert "has-code-block" in result

0 commit comments

Comments
 (0)