Skip to content

Commit a6893f7

Browse files
authored
Fix adapt css with split (#1600)
Fix for #1575 where postcss was raising an exception * adapt the entire CSS as a whole in one pass with postcss, rather than adapting each split part separately * break up the postcss output again and assign to individual text nodes (kind of inverse of splitCssText at record side) * impose an upper bound of 30 iterations on the substring searches to preempt possible pathological behavior * add tests to demonstrate the scenario and prevent regression More technical details: * Fix algorithm; checks against `ix_end` within loop were incorrect when `ix_start` was bigger than zero. * Fix that length check against wrong array was causing 'should record style mutations with multiple child nodes and replay them correctly' test to fail. Note on last point: I haven't looked into things more deeply than that the test was complaining about missing .length after `replayer.pause(1000);`
1 parent a95b3e8 commit a6893f7

File tree

3 files changed

+88
-7
lines changed

3 files changed

+88
-7
lines changed

.changeset/fix-adapt-css.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"rrweb": patch
3+
"rrweb-snapshot": patch
4+
---
5+
6+
#1575 Fix that postcss could fall over when trying to process css content split arbitrarily

packages/rrweb-snapshot/src/rebuild.ts

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -102,15 +102,44 @@ export function applyCssSplits(
102102
// unexpected: remerge the last two so that we don't discard any css
103103
cssTextSplits.splice(-2, 2, cssTextSplits.slice(-2).join(''));
104104
}
105+
let adaptedCss = '';
106+
if (hackCss) {
107+
adaptedCss = adaptCssForReplay(cssTextSplits.join(''), cache);
108+
}
109+
let startIndex = 0;
105110
for (let i = 0; i < childTextNodes.length; i++) {
111+
if (i === cssTextSplits.length) {
112+
break;
113+
}
106114
const childTextNode = childTextNodes[i];
107-
const cssTextSection = cssTextSplits[i];
108-
if (childTextNode && cssTextSection) {
109-
// id will be assigned when these child nodes are
110-
// iterated over in buildNodeWithSN
111-
childTextNode.textContent = hackCss
112-
? adaptCssForReplay(cssTextSection, cache)
113-
: cssTextSection;
115+
if (!hackCss) {
116+
childTextNode.textContent = cssTextSplits[i];
117+
} else if (i < cssTextSplits.length - 1) {
118+
let endIndex = startIndex;
119+
let endSearch = cssTextSplits[i + 1].length;
120+
121+
// don't do hundreds of searches, in case a mismatch
122+
// is caused close to start of string
123+
endSearch = Math.min(endSearch, 30);
124+
125+
let found = false;
126+
for (; endSearch > 2; endSearch--) {
127+
const searchBit = cssTextSplits[i + 1].substring(0, endSearch);
128+
const searchIndex = adaptedCss.substring(startIndex).indexOf(searchBit);
129+
found = searchIndex !== -1;
130+
if (found) {
131+
endIndex += searchIndex;
132+
break;
133+
}
134+
}
135+
if (!found) {
136+
// something went wrong, put a similar sized chunk in the right place
137+
endIndex += cssTextSplits[i].length;
138+
}
139+
childTextNode.textContent = adaptedCss.substring(startIndex, endIndex);
140+
startIndex = endIndex;
141+
} else {
142+
childTextNode.textContent = adaptedCss.substring(startIndex);
114143
}
115144
}
116145
}

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

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,52 @@ describe('applyCssSplits css rejoiner', function () {
289289
expect((sn3.childNodes[2] as textNode).textContent).toEqual('');
290290
});
291291

292+
it('applies css splits correctly when split parts are invalid by themselves', () => {
293+
const badFirstHalf = 'a:hov';
294+
const badSecondHalf = 'er { color: red; }';
295+
const markedCssText = [badFirstHalf, badSecondHalf].join('/* rr_split */');
296+
applyCssSplits(sn, markedCssText, true, mockLastUnusedArg);
297+
expect(
298+
(sn.childNodes[0] as textNode).textContent +
299+
(sn.childNodes[1] as textNode).textContent,
300+
).toEqual('a:hover,\na.\\:hover { color: red; }');
301+
});
302+
303+
it('applies css splits correctly when split parts are invalid by themselves x3', () => {
304+
let sn3 = {
305+
type: NodeType.Element,
306+
tagName: 'style',
307+
childNodes: [
308+
{
309+
type: NodeType.Text,
310+
textContent: '',
311+
},
312+
{
313+
type: NodeType.Text,
314+
textContent: '',
315+
},
316+
{
317+
type: NodeType.Text,
318+
textContent: '',
319+
},
320+
],
321+
} as serializedElementNodeWithId;
322+
const badStartThird = '.a:hover { background-color';
323+
const badMidThird = ': red; } input:hover {';
324+
const badEndThird = 'border: 1px solid purple; }';
325+
const markedCssText = [badStartThird, badMidThird, badEndThird].join(
326+
'/* rr_split */',
327+
);
328+
applyCssSplits(sn3, markedCssText, true, mockLastUnusedArg);
329+
expect((sn3.childNodes[0] as textNode).textContent).toEqual(
330+
badStartThird.replace('.a:hover', '.a:hover,\n.a.\\:hover'),
331+
);
332+
expect((sn3.childNodes[1] as textNode).textContent).toEqual(
333+
badMidThird.replace('input:hover', 'input:hover,\ninput.\\:hover'),
334+
);
335+
expect((sn3.childNodes[2] as textNode).textContent).toEqual(badEndThird);
336+
});
337+
292338
it('maintains entire css text when there are too few child nodes', () => {
293339
let sn1 = {
294340
type: NodeType.Element,

0 commit comments

Comments
 (0)