diff --git a/packages/blocks-engine/src/theme/preserve-dom/__tests__/strategy.test.ts b/packages/blocks-engine/src/theme/preserve-dom/__tests__/strategy.test.ts index 0ec48287..3a8a276a 100644 --- a/packages/blocks-engine/src/theme/preserve-dom/__tests__/strategy.test.ts +++ b/packages/blocks-engine/src/theme/preserve-dom/__tests__/strategy.test.ts @@ -155,6 +155,48 @@ describe('preserveDomStrategy', () => { expect(aggregate.provenanceFlags).toEqual([]); }); + it('converts explicitly decorative inline SVGs to native image blocks instead of html fallback islands', () => { + const aggregate = reconstructNativeAggregate( + [ + sectionSpec({ + sectionIndex: 6, + headings: ['Fast care'], + sectionHtml: + '

Fast care

', + }), + ], + { strategy: preserveDomStrategy }, + ); + + const markup = aggregate.sectionMarkup[0] ?? ''; + expect(markup).toContain(''); + expect(markup).toContain('
'); + expect(markup).not.toContain('\n
${alt}
\n`; } +function svgAltText($: CheerioAPI, svgEl: Element): string | null { + const $svg = $(svgEl); + if (($svg.attr('aria-hidden') ?? '').trim().toLowerCase() === 'true') return ''; + const role = ($svg.attr('role') ?? '').trim().toLowerCase(); + if (role === 'presentation' || role === 'none') return ''; + + const labelledBy = ($svg.attr('aria-labelledby') ?? '').trim(); + if (labelledBy) { + const text = labelledBy + .split(/\s+/) + .map((id) => $(`#${id}`).text().trim()) + .filter(Boolean) + .join(' '); + if (text) return text; + } + + const label = ($svg.attr('aria-label') ?? '').trim(); + if (label) return label; + + const title = $svg.children('title').first().text().trim(); + return title || null; +} + +function dimensionAttr($el: Cheerio, name: 'width' | 'height'): string | null { + const value = ($el.attr(name) ?? '').trim(); + const match = /^\d+(?:\.\d+)?(?:px)?$/i.exec(value); + return match ? match[0].replace(/px$/i, '') : null; +} + +function svgImageBlock($: CheerioAPI, svgEl: Element, sheet: InstanceStyleSheet): string | null { + const alt = svgAltText($, svgEl); + if (alt === null) return null; + + const svg = sanitizeSvgAsset($.html(svgEl).trim()); + if (!svg || !/]/i.test(svg)) return null; + + const $svg = $(svgEl); + const cls = classNameWithInstance($svg, sheet); + const attrs = blockAttrs([], cls); + const figCls = ['wp-block-image', cls].filter(Boolean).join(' '); + const width = dimensionAttr($svg, 'width'); + const height = dimensionAttr($svg, 'height'); + const style = [width ? `width:${width}px` : '', height ? `height:${height}px` : ''].filter(Boolean).join(';'); + const styleAttr = style ? ` style="${escapeHtml(style)}"` : ''; + const src = `data:image/svg+xml,${encodeURIComponent(svg)}`; + return `\n
${escapeHtml(alt)}
\n`; +} + function emitChild($: CheerioAPI, el: Element, sheet: InstanceStyleSheet): ChildResult { const tag = el.tagName?.toLowerCase() ?? ''; const $el = $(el); @@ -121,6 +170,11 @@ function emitChild($: CheerioAPI, el: Element, sheet: InstanceStyleSheet): Child return { markup: imageBlock($, el, sheet), clean: true }; } + if (tag === 'svg') { + const markup = svgImageBlock($, el, sheet); + if (markup) return { markup, clean: true }; + } + const text = $el.text().trim(); const elementChildren = $el.children().toArray(); if ((tag === 'div' || tag === 'span') && !$el.attr('id') && elementChildren.length === 0 && text) {