Skip to content

Commit

Permalink
feat: better CSS parsing (#21978)
Browse files Browse the repository at this point in the history
  • Loading branch information
daibhin authored Apr 30, 2024
1 parent fdc8b7d commit 9f65672
Show file tree
Hide file tree
Showing 2 changed files with 211 additions and 5 deletions.
210 changes: 208 additions & 2 deletions patches/rrweb@2.0.0-alpha.13.patch
Original file line number Diff line number Diff line change
Expand Up @@ -528,11 +528,110 @@ index 22fee601e786c1d8dfb5c01d2e359c8bcbac7c42..20c3e14adfde860563e8dd902041bd14
let playbackRate = 1;
if (typeof mediaAttributes.rr_mediaPlaybackRate === 'number') {
playbackRate = mediaAttributes.rr_mediaPlaybackRate;
diff --git a/es/rrweb/packages/rrweb-snapshot/es/css.js b/es/rrweb/packages/rrweb-snapshot/es/css.js
new file mode 100644
index 0000000000000000000000000000000000000000..0a6c5930c017852afd3d7f0170f6eca821619dd3
--- /dev/null
+++ b/es/rrweb/packages/rrweb-snapshot/es/css.js
@@ -0,0 +1,87 @@
+import postcss from '../../../../../../../../../postcss/lib/postcss.js'
+
+const MEDIA_SELECTOR = /(max|min)-device-(width|height)/
+const MEDIA_SELECTOR_GLOBAL = new RegExp(MEDIA_SELECTOR.source, 'g')
+
+export const mutate = (cssText) => {
+ const ast = postcss([mediaSelectorPlugin, pseudoClassPlugin]).process(cssText)
+ return ast.css
+}
+
+const mediaSelectorPlugin = {
+ postcssPlugin: 'postcss-custom-selectors',
+ prepare() {
+ return {
+ postcssPlugin: 'postcss-custom-selectors',
+ AtRule: function (atrule) {
+ if (atrule.params.match(MEDIA_SELECTOR_GLOBAL)) {
+ atrule.params = atrule.params.replace(MEDIA_SELECTOR_GLOBAL, '$1-$2')
+ }
+ },
+ }
+ },
+}
+
+// Adapted from https://github.com/giuseppeg/postcss-pseudo-classes/blob/master/index.js
+const pseudoClassPlugin = {
+ postcssPlugin: 'postcss-hover-classes',
+ prepare: function () {
+ const fixed = []
+ return {
+ Rule: function (rule) {
+ if (fixed.indexOf(rule) !== -1) {
+ return
+ }
+ fixed.push(rule)
+
+ rule.selectors.forEach(function (selector) {
+ if (!selector.includes(':')) {
+ return
+ }
+
+ const selectorParts = selector.replace(/\n/g, ' ').split(' ')
+ const pseudoedSelectorParts = []
+
+ selectorParts.forEach(function (selectorPart) {
+ const pseudos = selectorPart.match(/::?([^:]+)/g)
+
+ if (!pseudos) {
+ pseudoedSelectorParts.push(selectorPart)
+ return
+ }
+
+ const baseSelector = selectorPart.substr(0, selectorPart.length - pseudos.join('').length)
+
+ const classPseudos = pseudos.map(function (pseudo) {
+ const pseudoToCheck = pseudo.replace(/\(.*/g, '')
+ if (pseudoToCheck !== ':hover') {
+ return pseudo
+ }
+
+ // Ignore pseudo-elements!
+ if (pseudo.match(/^::/)) {
+ return pseudo
+ }
+
+ // Kill the colon
+ pseudo = pseudo.substr(1)
+
+ // Replace left and right parens
+ pseudo = pseudo.replace(/\(/g, '\\(')
+ pseudo = pseudo.replace(/\)/g, '\\)')
+
+ return '.' + '\\:' + pseudo
+ })
+
+ pseudoedSelectorParts.push(baseSelector + classPseudos.join(''))
+ })
+
+ const newSelector = pseudoedSelectorParts.join(' ')
+ if (newSelector && newSelector !== selector) {
+ rule.selector += ',\n' + newSelector
+ }
+ })
+ },
+ }
+ },
+}
diff --git a/es/rrweb/packages/rrweb-snapshot/es/rrweb-snapshot.js b/es/rrweb/packages/rrweb-snapshot/es/rrweb-snapshot.js
index 38a23aaae8d683fa584329eced277dd8de55d1ff..278e06bc6c8c964581d461405a0f0a4544344fa1 100644
index 38a23aaae8d683fa584329eced277dd8de55d1ff..8aeee467a3bab9baeefb1a97f2b131bedbd0fa3c 100644
--- a/es/rrweb/packages/rrweb-snapshot/es/rrweb-snapshot.js
+++ b/es/rrweb/packages/rrweb-snapshot/es/rrweb-snapshot.js
@@ -1255,54 +1255,19 @@ function parse(css, options = {}) {
@@ -1,3 +1,5 @@
+import {mutate} from './css.js';
+
var NodeType;
(function (NodeType) {
NodeType[NodeType["Document"] = 0] = "Document";
@@ -1255,54 +1257,19 @@ function parse(css, options = {}) {
});
}
function selector() {
Expand Down Expand Up @@ -595,3 +694,110 @@ index 38a23aaae8d683fa584329eced277dd8de55d1ff..278e06bc6c8c964581d461405a0f0a45
}
function declaration() {
const pos = position();
@@ -1662,56 +1629,60 @@ function adaptCssForReplay(cssText, cache) {
const cachedStyle = cache === null || cache === void 0 ? void 0 : cache.stylesWithHoverClass.get(cssText);
if (cachedStyle)
return cachedStyle;
- const ast = parse(cssText, {
- silent: true,
- });
- if (!ast.stylesheet) {
- return cssText;
- }
- const selectors = [];
- const medias = [];
- function getSelectors(rule) {
- if ('selectors' in rule && rule.selectors) {
- rule.selectors.forEach((selector) => {
- if (HOVER_SELECTOR.test(selector)) {
- selectors.push(selector);
- }
- });
+ let result = cssText;
+ try {
+ result = mutate(cssText)
+ } catch (error) {
+ const ast = parse(cssText, {
+ silent: true,
+ });
+ if (!ast.stylesheet) {
+ return cssText;
+ }
+ const selectors = [];
+ const medias = [];
+ function getSelectors(rule) {
+ if ('selectors' in rule && rule.selectors) {
+ rule.selectors.forEach((selector) => {
+ if (HOVER_SELECTOR.test(selector)) {
+ selectors.push(selector);
+ }
+ });
+ }
+ if ('media' in rule && rule.media && MEDIA_SELECTOR.test(rule.media)) {
+ medias.push(rule.media);
+ }
+ if ('rules' in rule && rule.rules) {
+ rule.rules.forEach(getSelectors);
+ }
}
- if ('media' in rule && rule.media && MEDIA_SELECTOR.test(rule.media)) {
- medias.push(rule.media);
+ getSelectors(ast.stylesheet);
+ if (selectors.length > 0) {
+ const selectorMatcher = new RegExp(selectors
+ .filter((selector, index) => selectors.indexOf(selector) === index)
+ .sort((a, b) => b.length - a.length)
+ .map((selector) => {
+ return escapeRegExp(selector);
+ })
+ .join('|'), 'g');
+ result = result.replace(selectorMatcher, (selector) => {
+ const newSelector = selector.replace(HOVER_SELECTOR_GLOBAL, '$1.\\:hover');
+ return `${selector}, ${newSelector}`;
+ });
}
- if ('rules' in rule && rule.rules) {
- rule.rules.forEach(getSelectors);
+ if (medias.length > 0) {
+ const mediaMatcher = new RegExp(medias
+ .filter((media, index) => medias.indexOf(media) === index)
+ .sort((a, b) => b.length - a.length)
+ .map((media) => {
+ return escapeRegExp(media);
+ })
+ .join('|'), 'g');
+ result = result.replace(mediaMatcher, (media) => {
+ return media.replace(MEDIA_SELECTOR_GLOBAL, '$1-$2');
+ });
}
}
- getSelectors(ast.stylesheet);
- let result = cssText;
- if (selectors.length > 0) {
- const selectorMatcher = new RegExp(selectors
- .filter((selector, index) => selectors.indexOf(selector) === index)
- .sort((a, b) => b.length - a.length)
- .map((selector) => {
- return escapeRegExp(selector);
- })
- .join('|'), 'g');
- result = result.replace(selectorMatcher, (selector) => {
- const newSelector = selector.replace(HOVER_SELECTOR_GLOBAL, '$1.\\:hover');
- return `${selector}, ${newSelector}`;
- });
- }
- if (medias.length > 0) {
- const mediaMatcher = new RegExp(medias
- .filter((media, index) => medias.indexOf(media) === index)
- .sort((a, b) => b.length - a.length)
- .map((media) => {
- return escapeRegExp(media);
- })
- .join('|'), 'g');
- result = result.replace(mediaMatcher, (media) => {
- return media.replace(MEDIA_SELECTOR_GLOBAL, '$1-$2');
- });
- }
cache === null || cache === void 0 ? void 0 : cache.stylesWithHoverClass.set(cssText, result);
return result;
}
6 changes: 3 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 9f65672

Please sign in to comment.