try to inline linked stylesheet when in same origin
This commit is contained in:
@@ -1,4 +1,15 @@
|
||||
import { serializedNodeWithId, NodeType } from './types';
|
||||
import { serializedNodeWithId, NodeType, tagMap, elementNode } from './types';
|
||||
|
||||
const tagMap: tagMap = {
|
||||
script: 'noscript',
|
||||
};
|
||||
function getTagName(n: elementNode): string {
|
||||
let tagName = tagMap[n.tagName] ? tagMap[n.tagName] : n.tagName;
|
||||
if (tagName === 'link' && n.attributes._cssText) {
|
||||
tagName = 'style';
|
||||
}
|
||||
return tagName;
|
||||
}
|
||||
|
||||
function buildNode(n: serializedNodeWithId): Node | null {
|
||||
switch (n.type) {
|
||||
@@ -11,14 +22,15 @@ function buildNode(n: serializedNodeWithId): Node | null {
|
||||
n.systemId,
|
||||
);
|
||||
case NodeType.Element:
|
||||
const tagName = n.tagName === 'script' ? 'noscript' : n.tagName;
|
||||
const tagName = getTagName(n);
|
||||
const node = document.createElement(tagName);
|
||||
for (const name in n.attributes) {
|
||||
if (n.attributes.hasOwnProperty(name)) {
|
||||
let value = n.attributes[name];
|
||||
value = typeof value === 'boolean' ? '' : value;
|
||||
// textarea hack
|
||||
if (n.tagName === 'textarea' && name === 'value') {
|
||||
const isTextarea = tagName === 'textarea' && name === 'value';
|
||||
const isRemoteCss = tagName === 'style' && name === '_cssText';
|
||||
if (isTextarea || isRemoteCss) {
|
||||
const child = document.createTextNode(value);
|
||||
node.appendChild(child);
|
||||
continue;
|
||||
|
||||
@@ -15,7 +15,18 @@ function resetId() {
|
||||
_id = 1;
|
||||
}
|
||||
|
||||
function serializeNode(n: Node): serializedNode | false {
|
||||
function getCssRulesString(s: CSSStyleSheet): string | null {
|
||||
try {
|
||||
const rules = s.rules || s.cssRules;
|
||||
return rules
|
||||
? Array.from(rules).reduce((prev, cur) => (prev += cur.cssText), '')
|
||||
: null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function serializeNode(n: Node, doc: Document): serializedNode | false {
|
||||
switch (n.nodeType) {
|
||||
case n.DOCUMENT_NODE:
|
||||
return {
|
||||
@@ -31,10 +42,23 @@ function serializeNode(n: Node): serializedNode | false {
|
||||
};
|
||||
case n.ELEMENT_NODE:
|
||||
const tagName = (n as HTMLElement).tagName.toLowerCase();
|
||||
const attributes: attributes = {};
|
||||
let attributes: attributes = {};
|
||||
for (const { name, value } of Array.from((n as HTMLElement).attributes)) {
|
||||
attributes[name] = value;
|
||||
}
|
||||
// remote css
|
||||
if (tagName === 'link' && attributes.hasOwnProperty('href')) {
|
||||
const stylesheet = Array.from(doc.styleSheets).find(
|
||||
s => s.href === attributes.href,
|
||||
);
|
||||
const cssText = getCssRulesString(stylesheet as CSSStyleSheet);
|
||||
if (cssText) {
|
||||
attributes = {
|
||||
_cssText: cssText,
|
||||
};
|
||||
}
|
||||
}
|
||||
// form fields
|
||||
if (
|
||||
tagName === 'input' ||
|
||||
tagName === 'textarea' ||
|
||||
@@ -91,8 +115,8 @@ function serializeNode(n: Node): serializedNode | false {
|
||||
}
|
||||
}
|
||||
|
||||
function _snapshot(n: Node): serializedNodeWithId | null {
|
||||
const _serializedNode = serializeNode(n);
|
||||
function _snapshot(n: Node, doc: Document): serializedNodeWithId | null {
|
||||
const _serializedNode = serializeNode(n, doc);
|
||||
if (!_serializedNode) {
|
||||
// TODO: dev only
|
||||
console.warn(n, 'not serialized');
|
||||
@@ -106,15 +130,15 @@ function _snapshot(n: Node): serializedNodeWithId | null {
|
||||
serializedNode.type === NodeType.Element
|
||||
) {
|
||||
for (const childN of Array.from(n.childNodes)) {
|
||||
serializedNode.childNodes.push(_snapshot(childN));
|
||||
serializedNode.childNodes.push(_snapshot(childN, doc));
|
||||
}
|
||||
}
|
||||
return serializedNode;
|
||||
}
|
||||
|
||||
function snapshot(n: Node): serializedNodeWithId | null {
|
||||
function snapshot(n: Document): serializedNodeWithId | null {
|
||||
resetId();
|
||||
return _snapshot(n);
|
||||
return _snapshot(n, n);
|
||||
}
|
||||
|
||||
export default snapshot;
|
||||
|
||||
@@ -53,3 +53,7 @@ export type serializedNode =
|
||||
| commentNode;
|
||||
|
||||
export type serializedNodeWithId = serializedNode & { id: number };
|
||||
|
||||
export type tagMap = {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
16
test/html/cors-style-sheet.html
Normal file
16
test/html/cors-style-sheet.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>with style sheet</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/pure@2.85.0/index.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -13,4 +13,23 @@
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
<!-- TEST_DIVIDER -->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>with style sheet</title>
|
||||
<style>body { margin: 0px; }p { color: red; }</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -64,7 +64,7 @@ describe('integration tests', () => {
|
||||
before(async () => {
|
||||
this.server = await server();
|
||||
this.browser = await puppeteer.launch({
|
||||
headless: false,
|
||||
// headless: false,
|
||||
executablePath: '/home/yanzhen/Desktop/chrome-linux/chrome',
|
||||
});
|
||||
|
||||
@@ -79,33 +79,35 @@ describe('integration tests', () => {
|
||||
this.code = code;
|
||||
});
|
||||
|
||||
after(() => {
|
||||
this.browser.close();
|
||||
this.server.close();
|
||||
after(async () => {
|
||||
await this.browser.close();
|
||||
await this.server.close();
|
||||
});
|
||||
|
||||
for (const html of htmls) {
|
||||
for (const html of htmls.slice(0, 10)) {
|
||||
it('[html file]: ' + html.filePath, async () => {
|
||||
const page: puppeteer.Page = await this.browser.newPage();
|
||||
await page.goto(`http://localhost:3030/html/${html.filePath}`);
|
||||
// console for debug
|
||||
// tslint:disable-next-line: no-console
|
||||
page.on('console', msg => console.log(msg.text()));
|
||||
await page.goto(`http://localhost:3030/html`);
|
||||
await page.setContent(html.src);
|
||||
page.once('load', async () => {
|
||||
await page.evaluate(() => {
|
||||
const x = new XMLSerializer();
|
||||
return x.serializeToString(document);
|
||||
});
|
||||
const rebuildHtml = (await page.evaluate(`${this.code}
|
||||
const x = new XMLSerializer();
|
||||
const snap = rrweb.snapshot(document);
|
||||
x.serializeToString(rrweb.rebuild(snap));
|
||||
`)).replace(/\n\n/g, '');
|
||||
await page.goto(`data:text/html,${html.dest}`);
|
||||
const destHtml = (await page.evaluate(() => {
|
||||
const x = new XMLSerializer();
|
||||
return x.serializeToString(document);
|
||||
})).replace(/\n\n/g, '');
|
||||
expect(rebuildHtml).to.equal(destHtml);
|
||||
await page.evaluate(() => {
|
||||
const x = new XMLSerializer();
|
||||
return x.serializeToString(document);
|
||||
});
|
||||
const rebuildHtml = (await page.evaluate(`${this.code}
|
||||
const x = new XMLSerializer();
|
||||
const snap = rrweb.snapshot(document);
|
||||
x.serializeToString(rrweb.rebuild(snap));
|
||||
`)).replace(/\n\n/g, '');
|
||||
await page.goto(`http://localhost:3030/html`);
|
||||
await page.setContent(html.dest);
|
||||
const destHtml = (await page.evaluate(() => {
|
||||
const x = new XMLSerializer();
|
||||
return x.serializeToString(document);
|
||||
})).replace(/\n\n/g, '');
|
||||
expect(rebuildHtml).to.equal(destHtml);
|
||||
}).timeout(5000);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user