Skip to content

Commit

Permalink
#344@trivial: Continues on CSSStyleDeclaration.
Browse files Browse the repository at this point in the history
  • Loading branch information
capricorn86 committed Sep 30, 2022
1 parent 2e3bfa5 commit 8ec7322
Show file tree
Hide file tree
Showing 15 changed files with 831 additions and 643 deletions.
6 changes: 1 addition & 5 deletions packages/happy-dom/src/css/CSSParser.ts
Expand Up @@ -4,7 +4,6 @@ import CSSStyleRule from './rules/CSSStyleRule';
import CSSKeyframeRule from './rules/CSSKeyframeRule';
import CSSKeyframesRule from './rules/CSSKeyframesRule';
import CSSMediaRule from './rules/CSSMediaRule';
import CSSStyleDeclaration from './declaration/CSSStyleDeclaration';

const COMMENT_REGEXP = /\/\*[^*]*\*\//gm;

Expand Down Expand Up @@ -86,10 +85,7 @@ export default class CSSParser {
case CSSRule.FONT_FACE_RULE:
case CSSRule.KEYFRAME_RULE:
case CSSRule.STYLE_RULE:
const style = new CSSStyleDeclaration();
style.cssText = cssText;
(<CSSRule>style.parentRule) = parentRule;
(<CSSStyleDeclaration>(<CSSStyleRule>parentRule).style) = style;
(<CSSStyleRule>parentRule)._cssText = cssText;
break;
}
}
Expand Down
Expand Up @@ -10,7 +10,6 @@ import CSSStyleDeclarationPropertyManager from './utilities/CSSStyleDeclarationP
* CSS Style Declaration.
*/
export default abstract class AbstractCSSStyleDeclaration {
// Other properties
public readonly parentRule: CSSRule = null;
protected _style: CSSStyleDeclarationPropertyManager = null;
protected _ownerElement: IElement;
Expand Down Expand Up @@ -73,7 +72,7 @@ export default abstract class AbstractCSSStyleDeclaration {
}

if (this._ownerElement) {
const style = new CSSStyleDeclarationPropertyManager(cssText);
const style = new CSSStyleDeclarationPropertyManager({ cssText });
if (!style.size()) {
delete this._ownerElement['_attributes']['style'];
} else {
Expand All @@ -86,7 +85,7 @@ export default abstract class AbstractCSSStyleDeclaration {
this._ownerElement['_attributes']['style'].value = style.toString();
}
} else {
this._style = new CSSStyleDeclarationPropertyManager(cssText);
this._style = new CSSStyleDeclarationPropertyManager({ cssText });
}
}

Expand Down
@@ -0,0 +1,35 @@
/**
* CSS parser.
*/
export default class CSSStyleDeclarationCSSParser {
/**
* Class construtor.
*
* @param cssText CSS string.
* @param callback Callback.
*/
public static parse(
cssText: string,
callback: (name: string, value: string, important: boolean) => void
): void {
const parts = cssText.split(';');

for (const part of parts) {
if (part) {
const [name, value]: string[] = part.trim().split(':');
if (value) {
const trimmedName = name.trim();
const trimmedValue = value.trim();
if (trimmedName && trimmedValue) {
const important = trimmedValue.endsWith(' !important');
const valueWithoutImportant = trimmedValue.replace(' !important', '');

if (valueWithoutImportant) {
callback(trimmedName, valueWithoutImportant, important);
}
}
}
}
}
}
}
Expand Up @@ -4,14 +4,16 @@ import IDocument from '../../../nodes/document/IDocument';
import IHTMLStyleElement from '../../../nodes/html-style-element/IHTMLStyleElement';
import INodeList from '../../../nodes/node/INodeList';
import CSSStyleDeclarationPropertyManager from './CSSStyleDeclarationPropertyManager';
import ICSSStyleDeclarationPropertyValue from './ICSSStyleDeclarationPropertyValue';
import NodeTypeEnum from '../../../nodes/node/NodeTypeEnum';
import CSSRuleTypeEnum from '../../CSSRuleTypeEnum';
import CSSMediaRule from '../../rules/CSSMediaRule';
import CSSRule from '../../CSSRule';
import CSSStyleRule from '../../rules/CSSStyleRule';
import CSSStyleDeclarationElementDefaultProperties from './CSSStyleDeclarationElementDefaultProperties';
import CSSStyleDeclarationElementDefaultCSS from './CSSStyleDeclarationElementDefaultCSS';
import CSSStyleDeclarationElementInheritedProperties from './CSSStyleDeclarationElementInheritedProperties';
import CSSStyleDeclarationCSSParser from './CSSStyleDeclarationCSSParser';

const CSS_VARIABLE_REGEXP = /var\( *(--[^) ]+)\)/g;

/**
* CSS Style Declaration utility
Expand All @@ -32,7 +34,9 @@ export default class CSSStyleDeclarationElement {
return this.getComputedElementStyle(element);
}

return new CSSStyleDeclarationPropertyManager(element['_attributes']['style']?.value);
return new CSSStyleDeclarationPropertyManager({
cssText: element['_attributes']['style']?.value
});
}

/**
Expand All @@ -44,15 +48,14 @@ export default class CSSStyleDeclarationElement {
private static getComputedElementStyle(element: IElement): CSSStyleDeclarationPropertyManager {
const documentElements: Array<{ element: IElement; cssText: string }> = [];
const parentElements: Array<{ element: IElement; cssText: string }> = [];
const inheritedProperties: { [k: string]: ICSSStyleDeclarationPropertyValue } = {};
let styleAndElement = { element: <IElement | IShadowRoot | IDocument>element, cssText: '' };
let shadowRootElements: Array<{ element: IElement; cssText: string }> = [];

if (!element.isConnected) {
return new CSSStyleDeclarationPropertyManager();
}

// Walks through all parent elements and applies style to them.
// Walks through all parent elements and stores them in an array with element and matching CSS text.
while (styleAndElement.element) {
if (styleAndElement.element.nodeType === NodeTypeEnum.elementNode) {
const rootNode = styleAndElement.element.getRootNode();
Expand All @@ -66,120 +69,152 @@ export default class CSSStyleDeclarationElement {

if (styleAndElement.element === element.ownerDocument) {
const styleSheets = <INodeList<IHTMLStyleElement>>(
element.ownerDocument.querySelectorAll('style')
element.ownerDocument.querySelectorAll('style,link[rel="stylesheet"]')
);

for (const styleSheet of styleSheets) {
this.applyCSSTextToElements(documentElements, styleSheet.sheet.cssRules);
const sheet = styleSheet.sheet;
if (sheet) {
this.parseCSSRules({
elements: documentElements,
cssRules: sheet.cssRules
});
}
}

styleAndElement = { element: null, cssText: '' };
} else if ((<IShadowRoot>styleAndElement.element).host) {
const styleSheets = <INodeList<IHTMLStyleElement>>(
(<IShadowRoot>styleAndElement.element).querySelectorAll('style')
(<IShadowRoot>styleAndElement.element).querySelectorAll('style,link[rel="stylesheet"]')
);

styleAndElement = {
element: <IElement>(<IShadowRoot>styleAndElement.element).host,
cssText: ''
};

for (const styleSheet of styleSheets) {
this.applyCSSTextToElements(
shadowRootElements,
styleSheet.sheet.cssRules,
<{ element: IElement; cssText: string }>styleAndElement
);
const sheet = styleSheet.sheet;
if (sheet) {
this.parseCSSRules({
elements: shadowRootElements,
cssRules: sheet.cssRules,
hostElement: <{ element: IElement; cssText: string }>styleAndElement
});
}
}
shadowRootElements = [];
} else {
styleAndElement = { element: <IElement>styleAndElement.element.parentNode, cssText: '' };
}
}

// Concatenates all parent element CSS to one string.
const targetElement = parentElements[parentElements.length - 1];
let inheritedCSSText = CSSStyleDeclarationElementDefaultCSS.default;

// Walks through all parent elements and merges inherited properties.
for (const parentElement of parentElements) {
if (parentElement !== targetElement) {
const propertyManager = new CSSStyleDeclarationPropertyManager(
parentElement.cssText + (parentElement.element['_attributes']['style']?.value || '')
);
const properties = Object.assign(
{},
CSSStyleDeclarationElementDefaultProperties.default,
CSSStyleDeclarationElementDefaultProperties[parentElement.element.tagName],
propertyManager.properties
);
for (const name of Object.keys(properties)) {
if (CSSStyleDeclarationElementInheritedProperties.includes(name)) {
if (!inheritedProperties[name]?.important || properties[name].important) {
inheritedProperties[name] = properties[name];
}
}
}
inheritedCSSText +=
(CSSStyleDeclarationElementDefaultCSS[parentElement.element.tagName] || '') +
parentElement.cssText +
(parentElement.element['_attributes']['style']?.value || '');
}
}

// Merges together styles in the target element with inherited properties.
const targetPropertyManager = new CSSStyleDeclarationPropertyManager(
targetElement.cssText + (targetElement.element['_attributes']['style']?.value || '')
);

const targetProperties = Object.assign(
{},
CSSStyleDeclarationElementDefaultProperties.default,
CSSStyleDeclarationElementDefaultProperties[targetElement.element.tagName],
inheritedProperties
);

for (const name of Object.keys(targetPropertyManager.properties)) {
if (!targetProperties[name]?.important || targetPropertyManager.properties[name].important) {
targetProperties[name] = targetPropertyManager.properties[name];
const cssVariables: { [k: string]: string } = {};
const properties = {};

// Parses the parent element CSS and stores CSS variables and inherited properties.
CSSStyleDeclarationCSSParser.parse(inheritedCSSText, (name, value, important) => {
if (name.startsWith('--')) {
const cssValue = this.getCSSValue(value, cssVariables);
if (cssValue) {
cssVariables[name] = cssValue;
}
return;
}
}

targetPropertyManager.properties = targetProperties;
if (CSSStyleDeclarationElementInheritedProperties[name]) {
const cssValue = this.getCSSValue(value, cssVariables);
if (cssValue && (!properties[name]?.important || important)) {
properties[name] = {
value: cssValue,
important
};
}
}
});

// Parses the target element CSS.
const targetCSSText =
(CSSStyleDeclarationElementDefaultCSS[targetElement.element.tagName] || '') +
targetElement.cssText +
(targetElement.element['_attributes']['style']?.value || '');

CSSStyleDeclarationCSSParser.parse(targetCSSText, (name, value, important) => {
if (name.startsWith('--')) {
const cssValue = this.getCSSValue(value, cssVariables);
if (cssValue && (!properties[name]?.important || important)) {
cssVariables[name] = cssValue;
properties[name] = {
value,
important
};
}
} else {
const cssValue = this.getCSSValue(value, cssVariables);
if (cssValue && (!properties[name]?.important || important)) {
properties[name] = {
value: cssValue,
important
};
}
}
});

const propertyManager = new CSSStyleDeclarationPropertyManager();

for (const name of Object.keys(properties)) {
propertyManager.set(name, properties[name].value, properties[name].important);
}

return targetPropertyManager;
return propertyManager;
}

/**
* Applies CSS text to elements.
*
* @param elements Elements.
* @param cssRules CSS rules.
* @param [hostElement] Host element.
* @param [hostElement.element] Element.
* @param [hostElement.cssText] CSS text.
* @param options Options.
* @param options.elements Elements.
* @param options.cssRules CSS rules.
* @param [options.hostElement] Host element.
* @param [options.hostElement.element] Element.
* @param [options.hostElement.cssText] CSS text.
*/
private static applyCSSTextToElements(
elements: Array<{ element: IElement; cssText: string }>,
cssRules: CSSRule[],
hostElement?: { element: IElement; cssText: string }
): void {
if (!elements.length) {
private static parseCSSRules(options: {
cssRules: CSSRule[];
elements: Array<{ element: IElement; cssText: string }>;
hostElement?: { element: IElement; cssText: string };
}): void {
if (!options.elements.length) {
return;
}

const defaultView = elements[0].element.ownerDocument.defaultView;
const defaultView = options.elements[0].element.ownerDocument.defaultView;

for (const rule of cssRules) {
for (const rule of options.cssRules) {
if (rule.type === CSSRuleTypeEnum.styleRule) {
const selectorText: string = (<CSSStyleRule>rule).selectorText;
if (selectorText) {
if (selectorText.startsWith(':host')) {
if (hostElement) {
const cssText = rule.cssText;
const firstBracket = cssText.indexOf('{');
const lastBracket = cssText.lastIndexOf('}');
hostElement.cssText += cssText.substring(firstBracket + 1, lastBracket);
if (options.hostElement) {
options.hostElement.cssText += (<CSSStyleRule>rule)._cssText;
}
} else {
for (const element of elements) {
for (const element of options.elements) {
if (element.element.matches(selectorText)) {
const cssText = rule.cssText;
const firstBracket = cssText.indexOf('{');
const lastBracket = cssText.lastIndexOf('}');
element.cssText += cssText.substring(firstBracket + 1, lastBracket);
element.cssText += (<CSSStyleRule>rule)._cssText;
}
}
}
Expand All @@ -188,8 +223,33 @@ export default class CSSStyleDeclarationElement {
rule.type === CSSRuleTypeEnum.mediaRule &&
defaultView.matchMedia((<CSSMediaRule>rule).conditionalText).matches
) {
this.applyCSSTextToElements(elements, (<CSSMediaRule>rule).cssRules, hostElement);
this.parseCSSRules({
elements: options.elements,
cssRules: (<CSSMediaRule>rule).cssRules,
hostElement: options.hostElement
});
}
}
}

/**
* Returns CSS value.
*
* @param value Value.
* @param cssVariables CSS variables.
* @returns CSS value.
*/
private static getCSSValue(value: string, cssVariables: { [k: string]: string }): string {
const regexp = new RegExp(CSS_VARIABLE_REGEXP);
let newValue = value;
let match;
while ((match = regexp.exec(value)) !== null) {
const cssVariableValue = cssVariables[match[1]];
if (!cssVariableValue) {
return null;
}
newValue = newValue.replace(match[0], cssVariableValue);
}
return value !== newValue ? newValue : value;
}
}

0 comments on commit 8ec7322

Please sign in to comment.