Files
C.E.L_Slide_test2/src/slide_css_injector.py
kyeongmin 9062931863
Some checks failed
Multi-MDX Regression (IMP-91) / multi-mdx-regression (push) Failing after 22s
feat(#74): IMP-45 u1~u8 slide-level CSS override (frontmatter slide_overrides.css + --override-slide-css/--slide-css-file + idempotent Step 13 injector)
u1 KNOWN_AXES tuple gains slide_css entry in src/user_overrides_io.py
(snake_case parity with image_overrides); round-trip test extends
to 6 axes.
u2 src/mdx_normalizer.py surfaces nested slide_overrides.css from the
MDX frontmatter into the normalize_mdx_content return dict; absent
key -> {}, non-string css drops. 4 unit cases in tests/test_mdx_normalizer.py
(present / absent / non-string / title-only).
u3 src/slide_css_injector.py NEW (88 lines) mirrors the
inject_image_overrides_style contract from src/image_id_stamper.py:
marker pair <!--IMP45-SLIDE-CSS:OPEN--> / <!--IMP45-SLIDE-CSS:CLOSE-->,
idempotent re-injection, </head> > <body> > document-start three-tier
fallback, empty/None -> unchanged. 8 fixtures in
tests/test_slide_css_injector.py mirror test_image_id_stamper.py.
u4 run_phase_z2_mvp1 accepts override_slide_css: Optional[str] = None;
None -> frontmatter slide_overrides.css fallback. Step 13 calls
inject_slide_css after image override injection and before the
final.html disk write, so CLI/CI/regression renders observe the same
backend artifact.
u5 argparse adds mutually-exclusive --override-slide-css TEXT (inline
CSS, <style> wrapper optional) and --slide-css-file PATH (UTF-8 read,
fail-closed sys.exit(2) on missing path / decode error / both flags
present). Resolved string is forwarded as override_slide_css kwarg.
6 cases in tests/test_phase_z2_cli_overrides.py (inline / file / both
/ missing / non-utf8 / neither).
u6 samples/mdx_batch/04.mdx frontmatter gains slide_overrides.css
block (verbatim of the former MDX04_DEFAULT_OVERRIDE_CSS constant,
no sample/frame gate). Subprocess smoke in
tests/test_phase_z2_slide_css_smoke.py verifies the marker pair and
CSS substring land in final.html.
u7 Front/client removes the sample/frame-gated frontend-only injection:
Home.tsx drops the MDX04_DEFAULT_OVERRIDE_CSS constant and the
sample==="04"+frame==="process_product_two_way" branch (-28 lines);
SlideCanvas.tsx drops the iframe contentDocument.head injection of
that prop (-14 lines). Live preview now reads backend final.html only.
u8 tests/regression/fixtures/89a_pre_baseline_sha.json 04.mdx entry
resyncs to the live SHA ddb6bf2f... / 28042 bytes (overwrites the
earlier 5-byte-drift d02c76fd... / 28047). Other entries untouched.
Note: 01.mdx baseline drift (ad6f16a3... / 29089 -> live f26a7fac...
/ 29084) predates this branch and is split to a follow-up issue per
the closed-issue fresh validation rule.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 03:26:03 +09:00

88 lines
3.4 KiB
Python

"""IMP-45 (#74) u3 — slide-level CSS override injector for Phase Z final.html.
Mirror of :func:`src.image_id_stamper.inject_image_overrides_style` contract
(image_id_stamper.py:226-264) for the new ``slide_css`` override axis
registered by u1 in :data:`src.user_overrides_io.KNOWN_AXES` and surfaced
by u2 in :func:`src.mdx_normalizer.normalize_mdx_content` under the
``slide_overrides.css`` frontmatter key.
Single entry point :
:func:`inject_slide_css` (html, css) -> str
Semantics (identical contract to image_overrides injector) :
- Empty / falsy ``css`` -> ``html`` returned unchanged (no DOM mutation).
- Marker-wrapped ``<style>`` block; re-injection replaces inner CSS in
place (idempotent on identical input; latest-wins on different input).
- Injection precedence : (1) before first ``</head>`` (case-insensitive),
(2) immediately after the first ``<body ...>`` open tag, (3) at the
start of the document. Phase Z ``slide_base.html`` always emits
``</head>`` so path 1 wins for production renders; paths 2/3 are
defensive fallbacks for fragment inputs.
Marker sentinels (distinct from image_overrides markers so the two
injectors can co-exist on the same document without collision; the
literal form is pinned by the Stage 2 binding contract for IMP-45 /
issue #74) :
<!--IMP45-SLIDE-CSS:OPEN-->
<!--IMP45-SLIDE-CSS:CLOSE-->
Both injectors target ``</head>`` first, so call order determines DOM
order. u4 calls ``inject_image_overrides_style`` first (existing Step 13
behavior) and then ``inject_slide_css``, putting slide-level overrides
after image overrides in cascade order so the editor-authored slide CSS
wins ties at the same specificity (intended by IMP-45 scope).
Guardrails :
- No-hardcoding : ``css`` is caller-supplied verbatim. No sample-id or
frame-id branches.
- AI-isolation : pure deterministic Python; no LLM calls.
- Carve-out (IMP-46 #62) : brand-new module, does not touch the
#76 commit ``1186ad8`` cache region.
"""
from __future__ import annotations
import re
_IMP45_STYLE_MARKER_OPEN: str = "<!--IMP45-SLIDE-CSS:OPEN-->"
_IMP45_STYLE_MARKER_CLOSE: str = "<!--IMP45-SLIDE-CSS:CLOSE-->"
_IMP45_STYLE_BLOCK_RE = re.compile(
re.escape(_IMP45_STYLE_MARKER_OPEN) + r".*?" + re.escape(_IMP45_STYLE_MARKER_CLOSE),
flags=re.DOTALL,
)
_HEAD_CLOSE_RE = re.compile(r"</head\s*>", flags=re.IGNORECASE)
_BODY_OPEN_RE = re.compile(r"<body\b[^>]*>", flags=re.IGNORECASE)
def inject_slide_css(html: str, css: str | None) -> str:
"""Inject a marker-wrapped ``<style>`` block carrying ``css`` into ``html``.
Empty or ``None`` ``css`` -> ``html`` returned unchanged. Re-injection
is idempotent : when a previously-injected marker block is present,
its inner CSS is replaced in place.
Injection precedence : ``</head>`` > ``<body ...>`` > document start.
"""
if not css:
return html
block = (
f"{_IMP45_STYLE_MARKER_OPEN}\n"
f"<style>\n{css}\n</style>\n"
f"{_IMP45_STYLE_MARKER_CLOSE}"
)
if _IMP45_STYLE_MARKER_OPEN in html:
return _IMP45_STYLE_BLOCK_RE.sub(lambda _m: block, html, count=1)
head_close = _HEAD_CLOSE_RE.search(html)
if head_close is not None:
idx = head_close.start()
return html[:idx] + block + "\n" + html[idx:]
body_open = _BODY_OPEN_RE.search(html)
if body_open is not None:
idx = body_open.end()
return html[:idx] + "\n" + block + html[idx:]
return block + "\n" + html