diff --git a/.changeset/fix-adapt-css.md b/.changeset/fix-adapt-css.md new file mode 100644 index 00000000..428f5833 --- /dev/null +++ b/.changeset/fix-adapt-css.md @@ -0,0 +1,6 @@ +--- +"rrweb": patch +"rrweb-snapshot": patch +--- + +#1575 Fix that postcss could fall over when trying to process css content split arbitrarily diff --git a/packages/rrweb-snapshot/src/rebuild.ts b/packages/rrweb-snapshot/src/rebuild.ts index f83ac0bc..899eb438 100644 --- a/packages/rrweb-snapshot/src/rebuild.ts +++ b/packages/rrweb-snapshot/src/rebuild.ts @@ -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); } } } diff --git a/packages/rrweb-snapshot/test/css.test.ts b/packages/rrweb-snapshot/test/css.test.ts index a26e0005..b11d4f98 100644 --- a/packages/rrweb-snapshot/test/css.test.ts +++ b/packages/rrweb-snapshot/test/css.test.ts @@ -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,