Skip to content

Commit

Permalink
Merge pull request #925 from capricorn86/task/921-matchmediamatches-s…
Browse files Browse the repository at this point in the history
…upport

Task/921 matchmediamatches support
  • Loading branch information
capricorn86 committed May 18, 2023
2 parents 9ed4cbf + 1c8e533 commit abf540f
Show file tree
Hide file tree
Showing 28 changed files with 2,363 additions and 190 deletions.
16 changes: 15 additions & 1 deletion packages/happy-dom/README.md
Expand Up @@ -258,7 +258,11 @@ const window = new Window({
disableJavaScriptEvaluation: true,
disableCSSFileLoading: true,
disableIframePageLoading: true,
enableFileSystemHttpRequests: true
enableFileSystemHttpRequests: true,
device: {
mediaType: 'print',
prefersColorScheme = 'dark
}
}
});
```
Expand All @@ -273,6 +277,8 @@ window.happyDOM.settings.disableJavaScriptEvaluation = true;
window.happyDOM.settings.disableCSSFileLoading = true;
window.happyDOM.settings.disableIframePageLoading = true;
window.happyDOM.settings.enableFileSystemHttpRequests = true;
window.happyDOM.settings.device.mediaType = 'print';
window.happyDOM.settings.device.prefersColorScheme = 'dark';
```
**disableJavaScriptFileLoading**
Expand All @@ -295,6 +301,14 @@ Set it to "true" to disable page loading in HTMLIFrameElement. Defaults to "fals
Set it to "true" to enable file system HTTP requests using XMLHttpRequest. Defaults to "false".
**device.mediaType**
Used by media queries. Acceptable values are "screen" or "print". Defaults to "screen".
**device.prefersColorScheme**
Used by media queries. Acceptable values are "light" or "dark". Defaults to "dark".
# Performance
| Operation | JSDOM | Happy DOM |
Expand Down
27 changes: 21 additions & 6 deletions packages/happy-dom/src/css/CSSParser.ts
Expand Up @@ -33,10 +33,13 @@ export default class CSSParser {
if (match[0] === '{') {
const selectorText = css.substring(lastIndex, match.index).trim();

if (selectorText.startsWith('@keyframes')) {
if (
selectorText.startsWith('@keyframes') ||
selectorText.startsWith('@-webkit-keyframes')
) {
const newRule = new CSSKeyframesRule();

(<string>newRule.name) = selectorText.replace('@keyframes ', '');
(<string>newRule.name) = selectorText.replace(/@(-webkit-){0,1}keyframes +/, '');
newRule.parentStyleSheet = parentStyleSheet;
cssRules.push(newRule);
parentRule = newRule;
Expand All @@ -51,22 +54,34 @@ export default class CSSParser {
newRule.parentStyleSheet = parentStyleSheet;
cssRules.push(newRule);
parentRule = newRule;
} else if (selectorText.startsWith('@container')) {
const conditionText = selectorText.replace(/@container */, '');
} else if (
selectorText.startsWith('@container') ||
selectorText.startsWith('@-webkit-container')
) {
const conditionText = selectorText.replace(/@(-webkit-){0,1}container +/, '');
const newRule = new CSSContainerRule();

(<string>newRule.conditionText) = conditionText;
newRule.parentStyleSheet = parentStyleSheet;
cssRules.push(newRule);
parentRule = newRule;
} else if (selectorText.startsWith('@supports')) {
const conditionText = selectorText.replace(/@supports */, '');
} else if (
selectorText.startsWith('@supports') ||
selectorText.startsWith('@-webkit-supports')
) {
const conditionText = selectorText.replace(/@(-webkit-){0,1}supports +/, '');
const newRule = new CSSSupportsRule();

(<string>newRule.conditionText) = conditionText;
newRule.parentStyleSheet = parentStyleSheet;
cssRules.push(newRule);
parentRule = newRule;
} else if (selectorText.startsWith('@')) {
// Unknown rule.
// We will create a new rule to let it grab its content, but we will not add it to the cssRules array.
const newRule = new CSSRule();
newRule.parentStyleSheet = parentStyleSheet;
parentRule = newRule;
} else if (parentRule && parentRule.type === CSSRule.KEYFRAMES_RULE) {
const newRule = new CSSKeyframeRule();
(<string>newRule.keyText) = selectorText.trim();
Expand Down
Expand Up @@ -3,8 +3,8 @@ import Attr from '../../nodes/attr/Attr';
import CSSRule from '../CSSRule';
import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum';
import DOMException from '../../exception/DOMException';
import CSSStyleDeclarationElementStyle from './utilities/CSSStyleDeclarationElementStyle';
import CSSStyleDeclarationPropertyManager from './utilities/CSSStyleDeclarationPropertyManager';
import CSSStyleDeclarationElementStyle from './element-style/CSSStyleDeclarationElementStyle';
import CSSStyleDeclarationPropertyManager from './property-manager/CSSStyleDeclarationPropertyManager';

/**
* CSS Style Declaration.
Expand Down
Expand Up @@ -3,18 +3,22 @@ import IElement from '../../../nodes/element/IElement';
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 CSSStyleDeclarationPropertyManager from '../property-manager/CSSStyleDeclarationPropertyManager';
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 CSSStyleDeclarationElementDefaultCSS from './CSSStyleDeclarationElementDefaultCSS';
import CSSStyleDeclarationElementInheritedProperties from './CSSStyleDeclarationElementInheritedProperties';
import CSSStyleDeclarationCSSParser from './CSSStyleDeclarationCSSParser';
import CSSStyleDeclarationElementDefaultCSS from './config/CSSStyleDeclarationElementDefaultCSS';
import CSSStyleDeclarationElementInheritedProperties from './config/CSSStyleDeclarationElementInheritedProperties';
import CSSStyleDeclarationElementMeasurementProperties from './config/CSSStyleDeclarationElementMeasurementProperties';
import CSSStyleDeclarationCSSParser from '../css-parser/CSSStyleDeclarationCSSParser';
import QuerySelector from '../../../query-selector/QuerySelector';
import CSSMeasurementConverter from '../measurement-converter/CSSMeasurementConverter';
import MediaQueryList from '../../../match-media/MediaQueryList';

const CSS_VARIABLE_REGEXP = /var\( *(--[^) ]+)\)/g;
const CSS_MEASUREMENT_REGEXP = /[0-9.]+(px|rem|em|vw|vh|%|vmin|vmax|cm|mm|in|pt|pc|Q)/g;

type IStyleAndElement = {
element: IElement | IShadowRoot | IDocument;
Expand Down Expand Up @@ -160,98 +164,79 @@ export default class CSSStyleDeclarationElementStyle {

// Concatenates all parent element CSS to one string.
const targetElement = parentElements[parentElements.length - 1];
let inheritedCSSText = '';
const propertyManager = new CSSStyleDeclarationPropertyManager();
const cssVariables: { [k: string]: string } = {};
let rootFontSize: string | number = 16;
let parentFontSize: string | number = 16;

for (const parentElement of parentElements) {
if (parentElement !== targetElement) {
parentElement.cssTexts.sort((a, b) => a.priorityWeight - b.priorityWeight);

if (CSSStyleDeclarationElementDefaultCSS[(<IElement>parentElement.element).tagName]) {
inheritedCSSText +=
CSSStyleDeclarationElementDefaultCSS[(<IElement>parentElement.element).tagName];
}
parentElement.cssTexts.sort((a, b) => a.priorityWeight - b.priorityWeight);

for (const cssText of parentElement.cssTexts) {
inheritedCSSText += cssText.cssText;
}

if (parentElement.element['_attributes']['style']?.value) {
inheritedCSSText += parentElement.element['_attributes']['style'].value;
}
let elementCSSText = '';
if (CSSStyleDeclarationElementDefaultCSS[(<IElement>parentElement.element).tagName]) {
elementCSSText +=
CSSStyleDeclarationElementDefaultCSS[(<IElement>parentElement.element).tagName];
}
}

const cssVariables: { [k: string]: string } = {};
const properties = {};
let targetCSSText =
CSSStyleDeclarationElementDefaultCSS[(<IElement>targetElement.element).tagName] || '';

targetElement.cssTexts.sort((a, b) => a.priorityWeight - b.priorityWeight);

for (const cssText of targetElement.cssTexts) {
targetCSSText += cssText.cssText;
}

if (targetElement.element['_attributes']['style']?.value) {
targetCSSText += targetElement.element['_attributes']['style'].value;
}

const combinedCSSText = inheritedCSSText + targetCSSText;

if (this.cache.propertyManager && this.cache.cssText === combinedCSSText) {
return this.cache.propertyManager;
}

// 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;
for (const cssText of parentElement.cssTexts) {
elementCSSText += cssText.cssText;
}

if (CSSStyleDeclarationElementInheritedProperties[name]) {
const cssValue = this.getCSSValue(value, cssVariables);
if (cssValue && (!properties[name]?.important || important)) {
properties[name] = {
value: cssValue,
important
};
}
if (parentElement.element['_attributes']['style']?.value) {
elementCSSText += parentElement.element['_attributes']['style'].value;
}
});

// Parses the target element CSS.
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
};

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

if (
CSSStyleDeclarationElementInheritedProperties[name] ||
parentElement === targetElement
) {
const cssValue = this.parseCSSVariablesInValue(value, cssVariables);
if (cssValue && (!propertyManager.get(name)?.important || important)) {
propertyManager.set(name, cssValue, important);
if (name === 'font' || name === 'font-size') {
const fontSize = propertyManager.properties['font-size'];
if (fontSize !== null) {
const parsedValue = this.parseMeasurementsInValue({
value: fontSize.value,
rootFontSize,
parentFontSize,
parentSize: parentFontSize
});
if ((<IElement>parentElement.element).tagName === 'HTML') {
rootFontSize = parsedValue;
} else if (parentElement !== targetElement) {
parentFontSize = parsedValue;
}
}
}
}
}
}
});
});
}

const propertyManager = new CSSStyleDeclarationPropertyManager();
for (const name of CSSStyleDeclarationElementMeasurementProperties) {
const property = propertyManager.properties[name];
if (property) {
property.value = this.parseMeasurementsInValue({
value: property.value,
rootFontSize,
parentFontSize,

for (const name of Object.keys(properties)) {
propertyManager.set(name, properties[name].value, properties[name].important);
// TODO: Only "font-size" is supported when using percentage values. Add support for other properties.
parentSize: name === 'font-size' ? parentFontSize : 0
});
}
}

this.cache.cssText = combinedCSSText;
this.cache.propertyManager = propertyManager;

return propertyManager;
Expand All @@ -274,7 +259,7 @@ export default class CSSStyleDeclarationElementStyle {
return;
}

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

for (const rule of options.cssRules) {
if (rule.type === CSSRuleTypeEnum.styleRule) {
Expand All @@ -289,10 +274,6 @@ export default class CSSStyleDeclarationElementStyle {
}
} else {
for (const element of options.elements) {
// Skip @-rules.
if (selectorText.startsWith('@')) {
continue;
}
const matchResult = QuerySelector.match(<IElement>element.element, selectorText);
if (matchResult) {
element.cssTexts.push({
Expand All @@ -305,7 +286,12 @@ export default class CSSStyleDeclarationElementStyle {
}
} else if (
rule.type === CSSRuleTypeEnum.mediaRule &&
defaultView.matchMedia((<CSSMediaRule>rule).conditionText).matches
// TODO: We need to send in a predfined root font size as it will otherwise be calculated using Window.getComputedStyle(), which will cause a never ending loop. Is there another solution?
new MediaQueryList({
ownerWindow,
media: (<CSSMediaRule>rule).conditionText,
rootFontSize: this.element.tagName === 'HTML' ? 16 : null
}).matches
) {
this.parseCSSRules({
elements: options.elements,
Expand All @@ -317,23 +303,60 @@ export default class CSSStyleDeclarationElementStyle {
}

/**
* Returns CSS value.
* Parses CSS variables in a value.
*
* @param value Value.
* @param cssVariables CSS variables.
* @returns CSS value.
*/
private getCSSValue(value: string, cssVariables: { [k: string]: string }): string {
private parseCSSVariablesInValue(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], cssVariables[match[1]] || '');
}

return newValue;
}

/**
* Parses measurements in a value.
*
* @param options Options.
* @param options.value Value.
* @param options.rootFontSize Root font size.
* @param options.parentFontSize Parent font size.
* @param [options.parentSize] Parent width.
* @returns CSS value.
*/
private parseMeasurementsInValue(options: {
value: string;
rootFontSize: string | number;
parentFontSize: string | number;
parentSize: string | number;
}): string {
const regexp = new RegExp(CSS_MEASUREMENT_REGEXP);
let newValue = options.value;
let match;

while ((match = regexp.exec(options.value)) !== null) {
if (match[1] !== 'px') {
const valueInPixels = CSSMeasurementConverter.toPixels({
ownerWindow: this.element.ownerDocument.defaultView,
value: match[0],
rootFontSize: options.rootFontSize,
parentFontSize: options.parentFontSize,
parentSize: options.parentSize
});

if (valueInPixels !== null) {
newValue = newValue.replace(match[0], valueInPixels + 'px');
}
}
newValue = newValue.replace(match[0], cssVariableValue);
}

return newValue;
}
}
Expand Up @@ -69,7 +69,7 @@ export default {
HEADER: 'display: block;',
HGROUP: 'display: block;',
HR: 'display: block;',
HTML: 'display: block;direction: ltr;',
HTML: 'display: block;direction: ltr;font: 16px "Times New Roman";',
I: '',
IFRAME: '',
INS: '',
Expand Down

0 comments on commit abf540f

Please sign in to comment.