Skip to content

Commit 178ec57

Browse files
committed
Add replaceChromeGridTemplateAreas utility function to fix grid-template-area styles that are improperly improperly parsed by Chrome into rule.cssText and causing broken recordings when attempting to play back
1 parent 53b83bb commit 178ec57

File tree

3 files changed

+138
-4
lines changed

3 files changed

+138
-4
lines changed

.changeset/clean-plants-look.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"rrweb-snapshot": patch
3+
---
4+
5+
Fix issue with chrome improperly parsing grid-template-areas to grid-template shorthand.

packages/rrweb-snapshot/src/utils.ts

+42-4
Original file line numberDiff line numberDiff line change
@@ -148,10 +148,14 @@ export function stringifyRule(rule: CSSRule, sheetHref: string | null): string {
148148
return importStringified;
149149
} else {
150150
let ruleStringified = rule.cssText;
151-
if (isCSSStyleRule(rule) && rule.selectorText.includes(':')) {
152-
// Safari does not escape selectors with : properly
153-
// see https://bugs.webkit.org/show_bug.cgi?id=184604
154-
ruleStringified = fixSafariColons(ruleStringified);
151+
if (isCSSStyleRule(rule)) {
152+
ruleStringified = replaceChromeGridTemplateAreas(rule);
153+
154+
if (rule.selectorText.includes(':')) {
155+
// Safari does not escape selectors with : properly
156+
// see https://bugs.webkit.org/show_bug.cgi?id=184604
157+
ruleStringified = fixSafariColons(ruleStringified);
158+
}
155159
}
156160
if (sheetHref) {
157161
return absolutifyURLs(ruleStringified, sheetHref);
@@ -160,6 +164,40 @@ export function stringifyRule(rule: CSSRule, sheetHref: string | null): string {
160164
}
161165
}
162166

167+
export function replaceChromeGridTemplateAreas(rule: CSSStyleRule): string {
168+
// chrome does not correctly provide the grid-template-areas in the rule.cssText
169+
// when it parses them to grid-template short-hand syntax
170+
// e.g. https://bugs.chromium.org/p/chromium/issues/detail?id=1303968
171+
// so, we manually rebuild the cssText using rule.style when
172+
// we find the cssText contains grid-template:, rule.style contains grid-template-areas, but
173+
// cssText does not include grid-template-areas
174+
const hasGridTemplateInCSSText = rule.cssText.includes('grid-template:');
175+
const hasGridTemplateAreaInStyleRules =
176+
rule.style.getPropertyValue('grid-template-areas') !== '';
177+
const hasGridTemplateAreaInCSSText = rule.cssText.includes(
178+
'grid-template-areas:',
179+
);
180+
181+
if (
182+
hasGridTemplateInCSSText &&
183+
hasGridTemplateAreaInStyleRules &&
184+
!hasGridTemplateAreaInCSSText
185+
) {
186+
const styleDeclarations = [];
187+
188+
for (let i = 0; i < rule.style.length; i++) {
189+
const styleName = rule.style[i];
190+
const styleValue = rule.style.getPropertyValue(styleName);
191+
192+
styleDeclarations.push(`${styleName}: ${styleValue}`);
193+
}
194+
195+
return `${rule.selectorText} { ${styleDeclarations.join('; ')}; }`
196+
}
197+
198+
return rule.cssText;
199+
}
200+
163201
export function fixSafariColons(cssStringified: string): string {
164202
// Replace e.g. [aa:bb] with [aa\\:bb]
165203
const regex = /(\[(?:[\w-]+)[^\\])(:(?:[\w-]+)\])/gm;

packages/rrweb-snapshot/test/utils.test.ts

+91
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { NodeType, serializedNode } from '../src/types';
66
import {
77
escapeImportStatement,
88
extractFileExtension,
9+
replaceChromeGridTemplateAreas,
910
fixSafariColons,
1011
isNodeMetaEqual,
1112
} from '../src/utils';
@@ -268,6 +269,96 @@ describe('utils', () => {
268269
expect(out5).toEqual(`@import url("/foo.css;900;800\\"") layer;`);
269270
});
270271
});
272+
273+
describe('replaceChromeGridTemplateAreas', () => {
274+
it('does not alter corectly parsed grid template rules', () => {
275+
const cssText = '#wrapper { display: grid; width: 100%; height: 100%; grid-template: minmax(2, 1fr); margin: 0px auto; }';
276+
const mockCssRule = {
277+
cssText,
278+
selectorText: '#wrapper',
279+
style: {
280+
getPropertyValue (prop) {
281+
return {
282+
'grid-template-areas': ''
283+
}[prop]
284+
}
285+
}
286+
} as Partial<CSSStyleRule> as CSSStyleRule
287+
288+
expect(replaceChromeGridTemplateAreas(mockCssRule)).toEqual(cssText);
289+
});
290+
291+
it('fixes incorrectly parsed grid template rules', () => {
292+
const cssText1 = '#wrapper { grid-template-areas: "header header" "main main" "footer footer"; grid-template-rows: minmax(2, 1fr); grid-template-columns: minmax(2, 1fr); display: grid; margin: 0px auto; }';
293+
const cssText2 = '.some-class { color: purple; grid-template: "TopNav TopNav" 65px "SideNav Content" 52px "SideNav Content" / 255px auto; column-gap: 32px; }';
294+
295+
const mockCssRule1 = {
296+
cssText: cssText1,
297+
selectorText: '#wrapper',
298+
style: {
299+
length: 5,
300+
0: 'grid-template-areas',
301+
1: 'grid-template-rows',
302+
2: 'grid-template-columns',
303+
3: 'display',
304+
4: 'margin',
305+
getPropertyValue: (key: string): string => {
306+
switch (key) {
307+
case 'grid-template-areas':
308+
return '"header header" "main main" "footer footer"'
309+
case 'grid-template-rows':
310+
return 'minmax(2, 1fr)';
311+
case 'grid-template-columns':
312+
return 'minmax(2, 1fr)';
313+
case'display':
314+
return 'grid';
315+
case'margin':
316+
return '0px auto'
317+
default:
318+
return ''
319+
}
320+
},
321+
} as Record<string | number, any>
322+
} as Partial<CSSStyleRule> as CSSStyleRule
323+
324+
const mockCssRule2 = {
325+
cssText: cssText2,
326+
selectorText: '.some-class',
327+
style: {
328+
length: 5,
329+
0: 'color',
330+
1: 'grid-template-areas',
331+
2: 'grid-template-rows',
332+
3: 'grid-template-columns',
333+
4: 'column-gap',
334+
getPropertyValue: (key: string): string => {
335+
switch (key) {
336+
case'color':
337+
return 'purple';
338+
case 'grid-template-areas':
339+
return '"TopNav TopNav" "SideNav Content" "SideNav Content"'
340+
case 'grid-template-rows':
341+
return '65px 52px auto';
342+
case 'grid-template-columns':
343+
return '255px auto';
344+
case'column-gap':
345+
return '32px'
346+
default:
347+
return ''
348+
}
349+
},
350+
} as Record<string | number, any>
351+
} as Partial<CSSStyleRule> as CSSStyleRule
352+
353+
expect(replaceChromeGridTemplateAreas(mockCssRule1)).toEqual(
354+
'#wrapper { grid-template-areas: "header header" "main main" "footer footer"; grid-template-rows: minmax(2, 1fr); grid-template-columns: minmax(2, 1fr); display: grid; margin: 0px auto; }'
355+
);
356+
expect(replaceChromeGridTemplateAreas(mockCssRule2)).toEqual(
357+
'.some-class { color: purple; grid-template-areas: "TopNav TopNav" "SideNav Content" "SideNav Content"; grid-template-rows: 65px 52px auto; grid-template-columns: 255px auto; column-gap: 32px; }'
358+
);
359+
});
360+
});
361+
271362
describe('fixSafariColons', () => {
272363
it('parses : in attribute selectors correctly', () => {
273364
const out1 = fixSafariColons('[data-foo] { color: red; }');

0 commit comments

Comments
 (0)