Fix php-transformer dropping inline SVG icons/diagrams as decorative#331
Merged
Conversation
Inline SVG artwork (service icons, pipe/boiler diagrams) was silently discarded during HTML-to-blocks conversion, surfacing as empty `core/html` blocks and diagrams collapsed to whitespace + comments. Root cause was NOT the safe-SVG element allowlist stripping shapes: the sanitizer (`sanitizeInlineSvgMarkup`/`safeFallbackHtml`) already preserves all shape markup and strips only script/style/foreignObject/handlers. The data loss was in `convertElement`'s SVG branch: any SVG classified "safe decorative" (passive shapes + `aria-hidden`/`role=presentation`) that was neither icon-like nor a positioned visual layer was dropped entirely (`return null`), and visual-layer matches became an empty group. `aria-hidden` hides a graphic from assistive tech; it does not make the artwork visually disposable, so real icons/diagrams marked `aria-hidden` vanished. Changes: - Preserve any safe-decorative SVG that carries real drawable artwork as faithful inline `core/html`, instead of dropping it. The only SVGs that still collapse to a styleable group / are dropped are genuine decorative chrome the materialized CSS recreates: a positioned visual layer or a `preserveAspectRatio="none"` stretched band (which distorts geometry and is never used for meaningful icons/diagrams). Keeps the existing decorative wave-layer/divider behavior intact. - Restore camelCase casing of SVG element names (linearGradient, radialGradient, clipPath, filter primitives, ...) the HTML parser lowercases. A lowercased `<lineargradient>` is an unknown element the browser ignores, so the gradient fill disappeared even when preserved. - External-sprite `<use href="sprite.svg#id">` cannot be inlined (the symbol lives in another file), so an SVG whose only artwork is an external reference is reported through the bounded inline-SVG fallback rather than emitted as broken markup. Local `<use href="#id">` against an in-document `<symbol>`/`<defs>` is preserved. - Expand the passive-SVG safe element/attribute allowlist to the full set of safe structure/geometry/text tags and attributes. Security stripping (script, on* handlers, foreignObject, javascript:/external url()) is unchanged. Adds parity fixtures html-inline-svg-diagram-preserved (aria-hidden diagram with defs/linearGradient/stop/line/polyline/polygon/ellipse/ circle/rect survives; script + onclick stripped) and html-inline-svg-local-use-symbol (local use(#id)+symbol preserved; external-sprite use reported via fallback). Full parity suite green (170 fixtures); php -l clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Inline SVG artwork (service icons, hero pipe-diagrams, boiler illustrations) was still being stripped to empty during HTML-to-blocks conversion despite #311 — service icons surfaced as empty
core/htmlblocks and diagrams ascore/htmlwith only whitespace + HTML comments, every drawing element gone.Root cause (the hypothesis was inverted)
The investigation hypothesis was that #311's safe-SVG element allowlist was too narrow and stripped shapes like
line/polyline/ellipse/defs/linearGradient/stop. That is not what happens. The sanitizer (sanitizeInlineSvgMarkup→safeFallbackHtml) preserves all shape markup verbatim and strips onlyscript/style/foreignObject/event-handlers/javascript:. Reproductions confirmed diagrams using the full geometry set survive the sanitizer untouched.The real data loss is in
HtmlTransformer::convertElement()'s SVG branch. An SVG classified safe-decorative (isSafeDecorativeSvgElement= passive-shape allowlist +aria-hidden/role=presentation) was:return null) when it was neither icon-like nor a visual layer — e.g. a<svg aria-hidden="true">inside<div class="svc">, the empty-contentservice-icon case; orcore/groupwhenisVisualLayerElement()matched a fuzzy class token likehero— the comments/whitespace-only diagram case.aria-hidden="true"hides a graphic from the accessibility tree; it does not mean the artwork is visually disposable. Real icons and diagrams routinely set it, so they were silently erased. Expanding the allowlist as hypothesized would have made this worse (more SVGs classified passive-decorative → more dropped).Fix (SVG sanitize/classification code only)
core/htmlinstead of being dropped. The only SVGs that still collapse to a styleable group / are dropped are genuine decorative chrome the materialized CSS recreates: a positioned visual layer or apreserveAspectRatio="none"stretched band (distorts geometry → never used for meaningful icons/diagrams). This keeps the intentional decorativewave-layer/wave-dividerbehavior (html-hero-decorative-wave-layer,html-inline-svg-decorative) green.<linearGradient>was emitted as<lineargradient>— an unknown element the browser ignores, so the gradient fill disappeared even when "preserved".restoreSvgAttributeCasingonly fixed attributes; it now also restores element names (linearGradient,radialGradient,clipPath, filter primitives, ...) and is renamedrestoreSvgCasing.<use>verdict.<use href="sprite.svg#id">references a symbol in another file that does not travel with the imported markup, so it cannot be inlined. Such SVGs are now reported via the bounded inline-SVG fallback rather than emitted as broken external markup. Local<use href="#id">against an in-document<symbol>/<defs>is preserved.text,tspan,marker,pattern,symbol,use,font-*,text-anchor,marker-*,pattern*,ref{x,y}, ...).Security stripping is unchanged —
<script>,on*handlers,<foreignObject>,javascript:URLs, and externalurl()references are still removed. Verified a decorative SVG carrying an inline<script>+onclickkeeps the artwork while both unsafe parts are scrubbed.Before / after (real diagram)
Input:
<section class="system-diagram"><svg aria-hidden="true" viewBox="0 0 400 300">…defs/linearGradient/stop, line, polyline, polygon, ellipse, circle, rect, comments, script, onclick…</svg></section>core/groupwith emptyinnerBlocks(nocore/html; drawing gone).core/htmlcontaining every shape, with<linearGradient>correctly cased,<script>/onclickstripped.Tests
html-inline-svg-diagram-preserved(aria-hidden diagram survives, unsafe parts scrubbed) andhtml-inline-svg-local-use-symbol(localuse(#id)+symbolpreserved; external-spriteusereported via fallback).php -lclean. Existing SVG/security fixtures unchanged and green.trunk(corpus-detectors classed/styled-span assertions +html-paragraph-heading-classed-span-hoist) are in the span/paragraph-hoist path and untouched by this PR.AI assistance