Skip to content

Commit

Permalink
fix: fix css parsing in replayer (#195)
Browse files Browse the repository at this point in the history
This reverts #182 (i.e. we are
going back to #169) as we had
some performance regressions with the CSS changes.

I tested these against the replays that had crashed with the upstream
changes and they seem to work ok.
  • Loading branch information
billyvg authored Jun 7, 2024
2 parents 4ec6b8a + 0c05698 commit 5766ac4
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 217 deletions.
7 changes: 0 additions & 7 deletions .changeset/rotten-spies-enjoy.md

This file was deleted.

165 changes: 97 additions & 68 deletions packages/rrweb-snapshot/src/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,6 @@ export interface Node {
};
}

export interface NodeWithRules extends Node {
/** Array of nodes with the types rule, comment and any of the at-rule types. */
rules: Array<Rule | Comment | AtRule>;
}

export interface Rule extends Node {
/** The list of selectors of the rule, split on commas. Each selector is trimmed from whitespace and comments. */
selectors?: string[];
Expand Down Expand Up @@ -103,11 +98,13 @@ export interface CustomMedia extends Node {
/**
* The @document at-rule.
*/
export interface Document extends NodeWithRules {
export interface Document extends Node {
/** The part following @document. */
document?: string;
/** The vendor prefix in @document, or undefined if there is none. */
vendor?: string;
/** Array of nodes with the types rule, comment and any of the at-rule types. */
rules?: Array<Rule | Comment | AtRule>;
}

/**
Expand All @@ -121,7 +118,10 @@ export interface FontFace extends Node {
/**
* The @host at-rule.
*/
export type Host = NodeWithRules;
export interface Host extends Node {
/** Array of nodes with the types rule, comment and any of the at-rule types. */
rules?: Array<Rule | Comment | AtRule>;
}

/**
* The @import at-rule.
Expand Down Expand Up @@ -153,9 +153,11 @@ export interface KeyFrame extends Node {
/**
* The @media at-rule.
*/
export interface Media extends NodeWithRules {
export interface Media extends Node {
/** The part following @media. */
media?: string;
/** Array of nodes with the types rule, comment and any of the at-rule types. */
rules?: Array<Rule | Comment | AtRule>;
}

/**
Expand All @@ -179,9 +181,11 @@ export interface Page extends Node {
/**
* The @supports at-rule.
*/
export interface Supports extends NodeWithRules {
export interface Supports extends Node {
/** The part following @supports. */
supports?: string;
/** Array of nodes with the types rule, comment and any of the at-rule types. */
rules?: Array<Rule | Comment | AtRule>;
}

/** All at-rules. */
Expand All @@ -201,8 +205,10 @@ export type AtRule =
/**
* A collection of rules
*/
export interface StyleRules extends NodeWithRules {
export interface StyleRules {
source?: string;
/** Array of nodes with the types rule, comment and any of the at-rule types. */
rules: Array<Rule | Comment | AtRule>;
/** Array of Errors. Errors collected during parsing when option silent is true. */
parsingErrors?: ParserError[];
}
Expand All @@ -218,7 +224,7 @@ export interface Stylesheet extends Node {
// https://github.com/visionmedia/css-parse/pull/49#issuecomment-30088027
const commentre = /\/\*[^*]*\*+([^/*][^*]*\*+)*\//g;

export function parse(css: string, options: ParserOptions = {}): Stylesheet {
export function parse(css: string, options: ParserOptions = {}) {
/**
* Positional.
*/
Expand Down Expand Up @@ -425,81 +431,104 @@ export function parse(css: string, options: ParserOptions = {}): Stylesheet {
*/

function selector() {
whitespace();
while (css[0] == '}') {
error('extra closing bracket');
css = css.slice(1);
whitespace();
}
const m = match(/^([^{]+)/);

// Use match logic from https://github.com/NxtChg/pieces/blob/3eb39c8287a97632e9347a24f333d52d916bc816/js/css_parser/css_parse.js#L46C1-L47C1
const m = match(/^(("(?:\\"|[^"])*"|'(?:\\'|[^'])*'|[^{])+)/);
if (!m) {
return;
}

/* @fix Remove all comments from selectors
* http://ostermiller.org/findcomment.html */
const cleanedInput = m[0]
.trim()
const splitSelectors = trim(m[0])
.replace(/\/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*\/+/g, '')

// Handle strings by replacing commas inside them
.replace(/"(?:\\"|[^"])*"|'(?:\\'|[^'])*'/g, (m) => {
return m.replace(/,/g, '\u200C');
});

// Split using a custom function and restore commas in strings
return customSplit(cleanedInput).map((s) =>
s.replace(/\u200C/g, ',').trim(),
);
}

/**
* Split selector correctly, ensuring not to split on comma if inside ().
*/
})
.split(/\s*(?![^(]*\)),\s*/);

function customSplit(input: string) {
const result = [];
let currentSegment = '';
let depthParentheses = 0; // Track depth of parentheses
let depthBrackets = 0; // Track depth of square brackets
let currentStringChar = null;
if (splitSelectors.length <= 1) {
return splitSelectors.map((s) => {
return s.replace(/\u200C/g, ',');
});
}

for (const char of input) {
const hasStringEscape = currentSegment.endsWith('\\');
// For each selector, need to check if we properly split on `,`
// Example case where selector is:
// .bar:has(input:is(:disabled), button:is(:disabled))
let i = 0;
let j = 0;
const len = splitSelectors.length;
const finalSelectors = [];
while (i < len) {
// Look for selectors with opening parens - `(` and search rest of
// selectors for the first one with matching number of closing
// parens `)`
const openingParensCount = (splitSelectors[i].match(/\(/g) || []).length;
const closingParensCount = (splitSelectors[i].match(/\)/g) || []).length;
let unbalancedParens = openingParensCount - closingParensCount;

if (unbalancedParens >= 1) {
// At least one opening parens was found, prepare to look through
// rest of selectors
let foundClosingSelector = false;

// Loop starting with next item in array, until we find matching
// number of ending parens
j = i + 1;
while (j < len) {
// peek into next item to count the number of closing brackets
const nextOpeningParensCount = (splitSelectors[j].match(/\(/g) || [])
.length;
const nextClosingParensCount = (splitSelectors[j].match(/\)/g) || [])
.length;
const nextUnbalancedParens =
nextClosingParensCount - nextOpeningParensCount;

if (nextUnbalancedParens === unbalancedParens) {
// Matching # of closing parens was found, join all elements
// from i to j
finalSelectors.push(splitSelectors.slice(i, j + 1).join(','));

// we will want to skip the items that we have joined together
i = j + 1;

// Use to continue the outer loop
foundClosingSelector = true;

// break out of inner loop so we found matching closing parens
break;
}

// No matching closing parens found, keep moving through index, but
// update the # of unbalanced parents still outstanding
j++;
unbalancedParens -= nextUnbalancedParens;
}

if (currentStringChar) {
if (currentStringChar === char && !hasStringEscape) {
currentStringChar = null;
if (foundClosingSelector) {
// Matching closing selector was found, move to next selector
continue;
}
} else if (char === '(') {
depthParentheses++;
} else if (char === ')') {
depthParentheses--;
} else if (char === '[') {
depthBrackets++;
} else if (char === ']') {
depthBrackets--;
} else if ('\'"'.includes(char)) {
currentStringChar = char;
}

// Split point is a comma that is not inside parentheses or square brackets
if (char === ',' && depthParentheses === 0 && depthBrackets === 0) {
result.push(currentSegment);
currentSegment = '';
} else {
currentSegment += char;
// No matching closing selector was found, either invalid CSS,
// or unbalanced number of opening parens were used as CSS
// selectors. Assume that rest of the list of selectors are
// selectors and break to avoid iterating through the list of
// selectors again.
splitSelectors
.slice(i, len)
.forEach((selector) => selector && finalSelectors.push(selector));
break;
}
}

// Add the last segment
if (currentSegment) {
result.push(currentSegment);
// No opening parens found, contiue looking through list
splitSelectors[i] && finalSelectors.push(splitSelectors[i]);
i++;
}

return result;
return finalSelectors.map((s) => {
return s.replace(/\u200C/g, ',');
});
}

/**
Expand Down Expand Up @@ -936,7 +965,7 @@ function trim(str: string) {
* Adds non-enumerable parent node reference to each node.
*/

function addParent(obj: Stylesheet, parent?: Stylesheet): Stylesheet {
function addParent(obj: Stylesheet, parent?: Stylesheet) {
const isNode = obj && typeof obj.type === 'string';
const childParent = isNode ? obj : parent;

Expand Down
4 changes: 2 additions & 2 deletions packages/rrweb-snapshot/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import snapshot, {
} from './snapshot';
import rebuild, {
buildNodeWithSN,
adaptCssForReplay,
addHoverClass,
createCache,
} from './rebuild';
export * from './types';
Expand All @@ -24,7 +24,7 @@ export {
serializeNodeWithId,
rebuild,
buildNodeWithSN,
adaptCssForReplay,
addHoverClass,
createCache,
transformAttribute,
ignoreAttribute,
Expand Down
Loading

0 comments on commit 5766ac4

Please sign in to comment.