aboutsummaryrefslogtreecommitdiff
path: root/ext/js/dom/sandbox
diff options
context:
space:
mode:
Diffstat (limited to 'ext/js/dom/sandbox')
-rw-r--r--ext/js/dom/sandbox/css-style-applier.js53
1 files changed, 49 insertions, 4 deletions
diff --git a/ext/js/dom/sandbox/css-style-applier.js b/ext/js/dom/sandbox/css-style-applier.js
index 14564ed6..9952fa7d 100644
--- a/ext/js/dom/sandbox/css-style-applier.js
+++ b/ext/js/dom/sandbox/css-style-applier.js
@@ -21,6 +21,14 @@
*/
class CssStyleApplier {
/**
+ * @typedef {object} CssRule
+ * @property {string} selectors A CSS selector string representing one or more selectors.
+ * @property {[string, string][]} styles A list of CSS property and value pairs.
+ * @property {string} styles[][0] The CSS property.
+ * @property {string} styles[][1] The CSS value.
+ */
+
+ /**
* Creates a new instance of the class.
* @param styleDataUrl The local URL to the JSON file continaing the style rules.
* The style rules should be of the format:
@@ -37,6 +45,9 @@ class CssStyleApplier {
this._styleDataUrl = styleDataUrl;
this._styleData = [];
this._cachedRules = new Map();
+ // eslint-disable-next-line no-control-regex
+ this._patternHtmlWhitespace = /[\t\r\n\x0C ]+/g;
+ this._patternClassNameCharacter = /[0-9a-zA-Z-_]/;
}
/**
@@ -65,7 +76,7 @@ class CssStyleApplier {
const className = element.getAttribute('class');
if (className.length === 0) { continue; }
let cssTextNew = '';
- for (const {selectorText, styles} of this._getRulesForClass(className)) {
+ for (const {selectorText, styles} of this._getCandidateCssRulesForClass(className)) {
if (!element.matches(selectorText)) { continue; }
cssTextNew += this._getCssText(styles);
}
@@ -99,17 +110,22 @@ class CssStyleApplier {
return await response.json();
}
- _getRulesForClass(className) {
+ /**
+ * Gets an array of candidate CSS rules which might match a specific class.
+ * @param {string} className A whitespace-separated list of classes.
+ * @returns {CssRule[]} An array of candidate CSS rules.
+ */
+ _getCandidateCssRulesForClass(className) {
let rules = this._cachedRules.get(className);
if (typeof rules !== 'undefined') { return rules; }
rules = [];
this._cachedRules.set(className, rules);
- const classNamePattern = new RegExp(`.${className}(?![0-9a-zA-Z-])`, '');
+ const classList = this._getTokens(className);
for (const {selectors, styles} of this._styleData) {
const selectorText = selectors.join(',');
- if (!classNamePattern.test(selectorText)) { continue; }
+ if (!this._selectorMatches(selectorText, classList)) { continue; }
rules.push({selectorText, styles});
}
@@ -123,4 +139,33 @@ class CssStyleApplier {
}
return cssText;
}
+
+ _selectorMatches(selectorText, classList) {
+ const pattern = this._patternClassNameCharacter;
+ for (const item of classList) {
+ const prefixedItem = `.${item}`;
+ let start = 0;
+ while (true) {
+ const index = selectorText.indexOf(prefixedItem, start);
+ if (index < 0) { break; }
+ start = index + prefixedItem.length;
+ if (start >= selectorText.length || !pattern.test(selectorText[start])) { return true; }
+ }
+ }
+ return false;
+ }
+
+ _getTokens(tokenListString) {
+ let start = 0;
+ const pattern = this._patternHtmlWhitespace;
+ pattern.lastIndex = 0;
+ const result = [];
+ while (true) {
+ const match = pattern.exec(tokenListString);
+ const end = match === null ? tokenListString.length : match.index;
+ if (end > start) { result.push(tokenListString.substring(start, end)); }
+ if (match === null) { return result; }
+ start = end + match[0].length;
+ }
+ }
}