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);`
This commit is contained in:
6
.changeset/fix-adapt-css.md
Normal file
6
.changeset/fix-adapt-css.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"rrweb": patch
|
||||
"rrweb-snapshot": patch
|
||||
---
|
||||
|
||||
#1575 Fix that postcss could fall over when trying to process css content split arbitrarily
|
||||
@@ -102,15 +102,44 @@ export function applyCssSplits(
|
||||
// unexpected: remerge the last two so that we don't discard any css
|
||||
cssTextSplits.splice(-2, 2, cssTextSplits.slice(-2).join(''));
|
||||
}
|
||||
let adaptedCss = '';
|
||||
if (hackCss) {
|
||||
adaptedCss = adaptCssForReplay(cssTextSplits.join(''), cache);
|
||||
}
|
||||
let startIndex = 0;
|
||||
for (let i = 0; i < childTextNodes.length; i++) {
|
||||
if (i === cssTextSplits.length) {
|
||||
break;
|
||||
}
|
||||
const childTextNode = childTextNodes[i];
|
||||
const cssTextSection = cssTextSplits[i];
|
||||
if (childTextNode && cssTextSection) {
|
||||
// id will be assigned when these child nodes are
|
||||
// iterated over in buildNodeWithSN
|
||||
childTextNode.textContent = hackCss
|
||||
? adaptCssForReplay(cssTextSection, cache)
|
||||
: cssTextSection;
|
||||
if (!hackCss) {
|
||||
childTextNode.textContent = cssTextSplits[i];
|
||||
} else if (i < cssTextSplits.length - 1) {
|
||||
let endIndex = startIndex;
|
||||
let endSearch = cssTextSplits[i + 1].length;
|
||||
|
||||
// don't do hundreds of searches, in case a mismatch
|
||||
// is caused close to start of string
|
||||
endSearch = Math.min(endSearch, 30);
|
||||
|
||||
let found = false;
|
||||
for (; endSearch > 2; endSearch--) {
|
||||
const searchBit = cssTextSplits[i + 1].substring(0, endSearch);
|
||||
const searchIndex = adaptedCss.substring(startIndex).indexOf(searchBit);
|
||||
found = searchIndex !== -1;
|
||||
if (found) {
|
||||
endIndex += searchIndex;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
// something went wrong, put a similar sized chunk in the right place
|
||||
endIndex += cssTextSplits[i].length;
|
||||
}
|
||||
childTextNode.textContent = adaptedCss.substring(startIndex, endIndex);
|
||||
startIndex = endIndex;
|
||||
} else {
|
||||
childTextNode.textContent = adaptedCss.substring(startIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,6 +289,52 @@ describe('applyCssSplits css rejoiner', function () {
|
||||
expect((sn3.childNodes[2] as textNode).textContent).toEqual('');
|
||||
});
|
||||
|
||||
it('applies css splits correctly when split parts are invalid by themselves', () => {
|
||||
const badFirstHalf = 'a:hov';
|
||||
const badSecondHalf = 'er { color: red; }';
|
||||
const markedCssText = [badFirstHalf, badSecondHalf].join('/* rr_split */');
|
||||
applyCssSplits(sn, markedCssText, true, mockLastUnusedArg);
|
||||
expect(
|
||||
(sn.childNodes[0] as textNode).textContent +
|
||||
(sn.childNodes[1] as textNode).textContent,
|
||||
).toEqual('a:hover,\na.\\:hover { color: red; }');
|
||||
});
|
||||
|
||||
it('applies css splits correctly when split parts are invalid by themselves x3', () => {
|
||||
let sn3 = {
|
||||
type: NodeType.Element,
|
||||
tagName: 'style',
|
||||
childNodes: [
|
||||
{
|
||||
type: NodeType.Text,
|
||||
textContent: '',
|
||||
},
|
||||
{
|
||||
type: NodeType.Text,
|
||||
textContent: '',
|
||||
},
|
||||
{
|
||||
type: NodeType.Text,
|
||||
textContent: '',
|
||||
},
|
||||
],
|
||||
} as serializedElementNodeWithId;
|
||||
const badStartThird = '.a:hover { background-color';
|
||||
const badMidThird = ': red; } input:hover {';
|
||||
const badEndThird = 'border: 1px solid purple; }';
|
||||
const markedCssText = [badStartThird, badMidThird, badEndThird].join(
|
||||
'/* rr_split */',
|
||||
);
|
||||
applyCssSplits(sn3, markedCssText, true, mockLastUnusedArg);
|
||||
expect((sn3.childNodes[0] as textNode).textContent).toEqual(
|
||||
badStartThird.replace('.a:hover', '.a:hover,\n.a.\\:hover'),
|
||||
);
|
||||
expect((sn3.childNodes[1] as textNode).textContent).toEqual(
|
||||
badMidThird.replace('input:hover', 'input:hover,\ninput.\\:hover'),
|
||||
);
|
||||
expect((sn3.childNodes[2] as textNode).textContent).toEqual(badEndThird);
|
||||
});
|
||||
|
||||
it('maintains entire css text when there are too few child nodes', () => {
|
||||
let sn1 = {
|
||||
type: NodeType.Element,
|
||||
|
||||
Reference in New Issue
Block a user