From 1c8a2125bf83dfecec5a0d093114bede89bef532 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Sun, 18 Apr 2021 00:07:29 +0200 Subject: [PATCH] fix: prevent infinite loop for pseudo elements Fixes #96 --- src/element.ts | 58 +++++++++++++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/src/element.ts b/src/element.ts index 3bc11b1d..d6babe4e 100644 --- a/src/element.ts +++ b/src/element.ts @@ -119,35 +119,39 @@ export function handleElement(element: Element, context: Readonly { - if (!isHTMLElement(element)) { - return - } - const pseudoElementStyles = window.getComputedStyle(element, pseudoSelector) - const content = cssValueParser(pseudoElementStyles.content).nodes.find( - isTaggedUnionMember('type', 'string' as const) - ) - if (!content) { - return + // Handle ::before and ::after by creating temporary child elements in the DOM. + // Avoid infinite loop, in case `element` already is already a synthetic element created by us for a pseudo element. + if (isHTMLElement(element) && !element.dataset.pseudoElement) { + const handlePseudoElement = ( + pseudoSelector: '::before' | '::after', + position: 'prepend' | 'append' + ): void => { + const pseudoElementStyles = window.getComputedStyle(element, pseudoSelector) + const content = cssValueParser(pseudoElementStyles.content).nodes.find( + isTaggedUnionMember('type', 'string' as const) + ) + if (!content) { + return + } + // Pseudo elements are inline by default (like a span) + const span = element.ownerDocument.createElement('span') + span.dataset.pseudoElement = pseudoSelector + copyCssStyles(pseudoElementStyles, span.style) + span.textContent = unescapeStringValue(content.value) + element.dataset.pseudoElementOwner = id + cleanupFunctions.push(() => element.removeAttribute('data-pseudo-element-owner')) + const style = element.ownerDocument.createElement('style') + // Hide the *actual* pseudo element temporarily while we have a real DOM equivalent in the DOM + style.textContent = `[data-pseudo-element-owner="${id}"]${pseudoSelector} { display: none !important; }` + element.before(style) + cleanupFunctions.push(() => style.remove()) + element[position](span) + cleanupFunctions.push(() => span.remove()) } - // Pseudo elements are inline by default (like a span) - const span = element.ownerDocument.createElement('span') - span.dataset.pseudoElement = pseudoSelector - copyCssStyles(pseudoElementStyles, span.style) - span.textContent = unescapeStringValue(content.value) - element.dataset.pseudoElementOwner = id - cleanupFunctions.push(() => element.removeAttribute('data-pseudo-element-owner')) - const style = element.ownerDocument.createElement('style') - // Hide the *actual* pseudo element temporarily while we have a real DOM equivalent in the DOM - style.textContent = `[data-pseudo-element-owner="${id}"]${pseudoSelector} { display: none !important; }` - element.before(style) - cleanupFunctions.push(() => style.remove()) - element[position](span) - cleanupFunctions.push(() => span.remove()) + handlePseudoElement('::before', 'prepend') + handlePseudoElement('::after', 'append') + // TODO handle ::marker etc } - handlePseudoElement('::before', 'prepend') - handlePseudoElement('::after', 'append') - // TODO handle ::marker etc if (rectanglesIntersect) { addBackgroundAndBorders(styles, bounds, backgroundContainer, window, context)