aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ext/js/dom/sandbox/css-style-applier.js53
-rw-r--r--ext/js/dom/simple-dom-parser.js18
2 files changed, 65 insertions, 6 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;
+ }
+ }
}
diff --git a/ext/js/dom/simple-dom-parser.js b/ext/js/dom/simple-dom-parser.js
index 09f3e914..bc327f5e 100644
--- a/ext/js/dom/simple-dom-parser.js
+++ b/ext/js/dom/simple-dom-parser.js
@@ -22,6 +22,8 @@
class SimpleDOMParser {
constructor(content) {
this._document = parse5.parse(content);
+ // eslint-disable-next-line no-control-regex
+ this._patternHtmlWhitespace = /[\t\r\n\x0C ]+/g;
}
getElementById(id, root=null) {
@@ -54,11 +56,10 @@ class SimpleDOMParser {
getElementsByClassName(className, root=null) {
const results = [];
- const classNamePattern = new RegExp(`(^|\\s)${escapeRegExp(className)}(\\s|$)`);
for (const node of this._allNodes(root)) {
if (typeof node.tagName === 'string') {
const nodeClassName = this.getAttribute(node, 'class');
- if (nodeClassName !== null && classNamePattern.test(nodeClassName)) {
+ if (nodeClassName !== null && this._hasToken(nodeClassName, className)) {
results.push(node);
}
}
@@ -114,4 +115,17 @@ class SimpleDOMParser {
}
}
}
+
+ _hasToken(tokenListString, token) {
+ let start = 0;
+ const pattern = this._patternHtmlWhitespace;
+ pattern.lastIndex = 0;
+ while (true) {
+ const match = pattern.exec(tokenListString);
+ const end = match === null ? tokenListString.length : match.index;
+ if (end > start && tokenListString.substring(start, end) === token) { return true; }
+ if (match === null) { return false; }
+ start = end + match[0].length;
+ }
+ }
}