Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cdata-rebuild-html.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"rrweb-snapshot": patch
---

Fix that recording of CDATA sections were breaking rebuild
10 changes: 8 additions & 2 deletions packages/rrweb-snapshot/src/rebuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export function applyCssSplits(
): void {
const childTextNodes = [];
for (const scn of n.childNodes) {
if (scn.type === NodeType.Text) {
if (scn.type === NodeType.Text || scn.type === NodeType.CDATA) {
childTextNodes.push(scn);
}
}
Expand Down Expand Up @@ -432,7 +432,13 @@ function buildNode(
}
return doc.createTextNode(n.textContent);
case NodeType.CDATA:
return doc.createCDATASection(n.textContent);
/*
https://developer.mozilla.org/en-US/docs/Web/API/Document/createCDATASection
expected: DOMException: Failed to execute 'createCDATASection' on 'Document': This operation is not supported for HTML documents.
"createTextNode() can often be used in its place"
*/
//return doc.createCDATASection(n.textContent);
return doc.createTextNode(n.textContent);
case NodeType.Comment:
return doc.createComment(n.textContent);
default:
Expand Down
34 changes: 13 additions & 21 deletions packages/rrweb-snapshot/src/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@
// should warn? maybe a text node isn't attached to a parent node yet?
return false;
} else {
el = dom.parentElement(node)!;

Check warning on line 285 in packages/rrweb-snapshot/src/snapshot.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb-snapshot/src/snapshot.ts#L285

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
}
try {
if (typeof maskTextClass === 'string') {
Expand Down Expand Up @@ -465,18 +465,16 @@
newlyAddedElement,
rootId,
});
case n.TEXT_NODE:
return serializeTextNode(n as Text, {
doc,
needsMask,
maskTextFn,
rootId,
cssCaptured,
});
case n.CDATA_SECTION_NODE:
case n.TEXT_NODE:
return {
type: NodeType.CDATA,
textContent: '',
type: n.nodeType === n.TEXT_NODE ? NodeType.Text : NodeType.CDATA,
textContent: serializeTextContent(n as Text, {
doc,
needsMask,
maskTextFn,
cssCaptured,
}),
rootId,
};
case n.COMMENT_NODE:
Expand All @@ -496,17 +494,16 @@
return docId === 1 ? undefined : docId;
}

function serializeTextNode(
n: Text,
function serializeTextContent(
n: Text | CDATASection,
options: {
doc: Document;
needsMask: boolean;
maskTextFn: MaskTextFn | undefined;
rootId: number | undefined;
cssCaptured?: boolean;
},
): serializedNode {
const { needsMask, maskTextFn, rootId, cssCaptured } = options;
): string {
const { needsMask, maskTextFn, cssCaptured } = options;
// The parent node may not be a html element which has a tagName attribute.
// So just let it be undefined which is ok in this use case.
const parent = dom.parentNode(n);
Expand All @@ -531,12 +528,7 @@
? maskTextFn(textContent, dom.parentElement(n))
: textContent.replace(/[\S]/g, '*');
}

return {
type: NodeType.Text,
textContent: textContent || '',
rootId,
};
return textContent || '';
}

function serializeElementNode(
Expand Down Expand Up @@ -702,10 +694,10 @@
const recordInlineImage = () => {
image.removeEventListener('load', recordInlineImage);
try {
canvasService!.width = image.naturalWidth;

Check warning on line 697 in packages/rrweb-snapshot/src/snapshot.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb-snapshot/src/snapshot.ts#L697

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
canvasService!.height = image.naturalHeight;

Check warning on line 698 in packages/rrweb-snapshot/src/snapshot.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb-snapshot/src/snapshot.ts#L698

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
canvasCtx!.drawImage(image, 0, 0);

Check warning on line 699 in packages/rrweb-snapshot/src/snapshot.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb-snapshot/src/snapshot.ts#L699

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
attributes.rr_dataURL = canvasService!.toDataURL(

Check warning on line 700 in packages/rrweb-snapshot/src/snapshot.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb-snapshot/src/snapshot.ts#L700

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
dataURLOptions.type,
dataURLOptions.quality,
);
Expand Down
12 changes: 12 additions & 0 deletions packages/rrweb-snapshot/test/__snapshots__/cdata.svg.snap.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<style>.Icon &gt; span { color: blue; }</style>
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
<style>.Icon &gt; span { color: red; }</style>
<div>&lt;/svg&gt;&amp; this is not markup&lt;svg/&gt;</div>
</svg>
</body>
</html>
88 changes: 88 additions & 0 deletions packages/rrweb-snapshot/test/__snapshots__/cdata.svg.snap.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
{
"type": 0,
"childNodes": [
{
"type": 2,
"tagName": "html",
"attributes": {},
"childNodes": [
{
"type": 2,
"tagName": "head",
"attributes": {},
"childNodes": [
{
"type": 2,
"tagName": "style",
"attributes": {
"_cssText": ".Icon > span { color: blue; }"
},
"childNodes": [
{
"type": 3,
"textContent": "",
"id": 5
}
],
"id": 4
}
],
"id": 3
},
{
"type": 2,
"tagName": "body",
"attributes": {},
"childNodes": [
{
"type": 2,
"tagName": "svg",
"attributes": {
"xmlns": "http://www.w3.org/2000/svg",
"version": "1.1"
},
"childNodes": [
{
"type": 2,
"tagName": "style",
"attributes": {
"_cssText": ".Icon > span { color: red; }"
},
"childNodes": [
{
"type": 4,
"textContent": "",
"id": 9
}
],
"isSVG": true,
"id": 8
},
{
"type": 2,
"tagName": "div",
"attributes": {},
"childNodes": [
{
"type": 4,
"textContent": "</svg>& this is not markup<svg/>",
"id": 11
}
],
"isSVG": true,
"id": 10
}
],
"isSVG": true,
"id": 7
}
],
"id": 6
}
],
"id": 2
}
],
"compatMode": "BackCompat",
"id": 1
}
49 changes: 49 additions & 0 deletions packages/rrweb-snapshot/test/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,55 @@ iframe.contentDocument.querySelector('center').clientHeight
);
expect(snapshotResult).toMatchSnapshot();
});

it('correctly records CDATA section in SVG', async () => {
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank', {
waitUntil: 'load',
});
await page.evaluate(`
const regularStyle = document.createElement('style');
regularStyle.innerText = '.Icon > span{ color: blue; }'
document.head.append(regularStyle);
const defsSvg = (new window.DOMParser()).parseFromString(
'<svg xmlns="http://www.w3.org/2000/svg" version="1.1"><style><![CDATA[.Icon > span{ color: red; }]]></style><div><![CDATA[</svg>& this is not markup<svg/>]]></div></svg>', 'image/svg+xml');
document.body.appendChild(defsSvg.documentElement);
`);
await waitForRAF(page); // a small wait
const cdataType = await page.evaluate(
`document.querySelector('svg style').childNodes[0].nodeType`,
);
assert(cdataType === 4);
const snapshotResult = JSON.stringify(
await page.evaluate(`${code};
const snapshotResult = rrwebSnapshot.snapshot(document);
snapshotResult
`),
null,
2,
);
const fname = `./__snapshots__/cdata.svg.snap.json`;
expect(snapshotResult).toMatchFileSnapshot(fname);

await waitForRAF(page);
const rebuildHtml = (await page.evaluate(`
const x = new XMLSerializer();
const node = rrwebSnapshot.rebuild(JSON.parse('${snapshotResult.replace(
/\n/g,
'',
)}'), { doc: document })

let out = x.serializeToString(node);
if (document.querySelector('html').getAttribute('xmlns') !== 'http://www.w3.org/1999/xhtml') {
// this is just an artefact of serializeToString
out = out.replace(' xmlns=\"http://www.w3.org/1999/xhtml\"', '');
}
out; // return
`)) as string;

const fhname = `./__snapshots__/cdata.svg.snap.html`;
expect(rebuildHtml.replace(/></g, '>\n<')).toMatchFileSnapshot(fhname);
});
});

describe('iframe integration tests', function (this: ISuite) {
Expand Down
2 changes: 1 addition & 1 deletion packages/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -609,7 +609,7 @@
export type customElementCallback = (c: customElementParam) => void;

/**
* @deprecated

Check warning on line 612 in packages/types/src/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/types/src/index.ts#L612

[tsdoc/syntax] tsdoc-missing-deprecation-message: The @deprecated block must include a deprecation message, e.g. describing the recommended alternative
*/
interface INode extends Node {
__sn: serializedNodeWithId;
Expand Down Expand Up @@ -791,7 +791,7 @@

export type cdataNode = {
type: NodeType.CDATA;
textContent: '';
textContent: string;
};

export type commentNode = {
Expand Down
Loading