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:
Eoghan Murray
2026-04-01 12:00:00 +08:00
committed by GitHub
parent 16ef32ef1c
commit ea3c555b49
3 changed files with 88 additions and 7 deletions

View File

@@ -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);
}
}
}

View File

@@ -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,