diff --git a/index.d.ts b/index.d.ts index 9f2eda37..21a07000 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,11 +1,19 @@ -import { serializedNodeWithId, idNodeMap } from './src/types'; +import { serializedNodeWithId, idNodeMap, INode } from './src/types'; export * from './src/types'; export function snapshot(n: Document): [serializedNodeWithId | null, idNodeMap]; -export function rebuild(n: serializedNodeWithId, doc: Document): Node | null; +export function rebuild( + n: serializedNodeWithId, + doc: Document, +): [Node | null, idNodeMap]; export function serializeNodeWithId( n: Node, doc: Document, map: idNodeMap, ): serializedNodeWithId | null; export function resetId(): void; +export function buildNodeWithSN( + n: serializedNodeWithId, + doc: Document, + map: idNodeMap, +): INode | null; diff --git a/package.json b/package.json index 04fde37f..e0eceea0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rrweb-snapshot", - "version": "0.4.4", + "version": "0.5.1", "description": "rrweb's component to take a snapshot of DOM, aka DOM serializer", "main": "dist/index.js", "module": "dist/module.js", diff --git a/src/index.ts b/src/index.ts index cba7b499..f3238ea2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ import snapshot, { serializeNodeWithId, resetId } from './snapshot'; -import rebuild from './rebuild'; +import rebuild, { buildNodeWithSN } from './rebuild'; export * from './types'; -export { snapshot, serializeNodeWithId, resetId, rebuild }; +export { snapshot, serializeNodeWithId, resetId, rebuild, buildNodeWithSN }; diff --git a/src/rebuild.ts b/src/rebuild.ts index c53cf72b..c995b6cc 100644 --- a/src/rebuild.ts +++ b/src/rebuild.ts @@ -1,4 +1,108 @@ -import { serializedNodeWithId, NodeType, tagMap, elementNode } from './types'; +import { + serializedNodeWithId, + NodeType, + tagMap, + elementNode, + idNodeMap, + INode, +} from './types'; + +// TODO: need a more accurate list +const svgTags = [ + 'altGlyph', + 'altGlyphDef', + 'altGlyphItem', + 'animate', + 'animateColor', + 'animateMotion', + 'animateTransform', + 'animation', + 'circle', + 'clipPath', + 'color-profile', + 'cursor', + 'defs', + 'desc', + 'discard', + 'ellipse', + 'feBlend', + 'feColorMatrix', + 'feComponentTransfer', + 'feComposite', + 'feConvolveMatrix', + 'feDiffuseLighting', + 'feDisplacementMap', + 'feDistantLight', + 'feDropShadow', + 'feFlood', + 'feFuncA', + 'feFuncB', + 'feFuncG', + 'feFuncR', + 'feGaussianBlur', + 'feImage', + 'feMerge', + 'feMergeNode', + 'feMorphology', + 'feOffset', + 'fePointLight', + 'feSpecularLighting', + 'feSpotLight', + 'feTile', + 'feTurbulence', + 'filter', + 'font', + 'font-face', + 'font-face-format', + 'font-face-name', + 'font-face-src', + 'font-face-uri', + 'foreignObject', + 'g', + 'glyph', + 'glyphRef', + 'handler', + 'hatch', + 'hatchpath', + 'hkern', + 'image', + 'line', + 'linearGradient', + 'listener', + 'marker', + 'mask', + 'mesh', + 'meshgradient', + 'meshpatch', + 'meshrow', + 'metadata', + 'missing-glyph', + 'mpath', + 'path', + 'pattern', + 'polygon', + 'polyline', + 'prefetch', + 'radialGradient', + 'rect', + 'set', + 'solidColor', + 'solidcolor', + 'stop', + 'svg', + 'switch', + 'symbol', + 'tbreak', + 'text', + 'textArea', + 'textPath', + 'tref', + 'tspan', + 'unknown', + 'use', + 'view', + 'vkern', +]; const tagMap: tagMap = { script: 'noscript', @@ -23,8 +127,12 @@ function buildNode(n: serializedNodeWithId, doc: Document): Node | null { ); case NodeType.Element: const tagName = getTagName(n); - const node = doc.createElement(tagName); - const extraChildIndexes: number[] = []; + let node: Element; + if (svgTags.indexOf(tagName) < 0) { + node = doc.createElement(tagName); + } else { + node = doc.createElementNS('http://www.w3.org/2000/svg', tagName); + } for (const name in n.attributes) { if (n.attributes.hasOwnProperty(name)) { let value = n.attributes[name]; @@ -33,10 +141,7 @@ function buildNode(n: serializedNodeWithId, doc: Document): Node | null { const isRemoteCss = tagName === 'style' && name === '_cssText'; if (isTextarea || isRemoteCss) { const child = doc.createTextNode(value); - // identify the extra child DOM we added when rebuild - extraChildIndexes.push(node.childNodes.length); node.appendChild(child); - continue; } try { node.setAttribute(name, value); @@ -45,12 +150,6 @@ function buildNode(n: serializedNodeWithId, doc: Document): Node | null { } } } - if (extraChildIndexes.length) { - node.setAttribute( - 'data-extra-child-index', - JSON.stringify(extraChildIndexes), - ); - } return node; case NodeType.Text: return doc.createTextNode(n.textContent); @@ -63,25 +162,42 @@ function buildNode(n: serializedNodeWithId, doc: Document): Node | null { } } -function rebuild(n: serializedNodeWithId, doc: Document): Node | null { - const root = buildNode(n, doc); - if (!root) { +export function buildNodeWithSN( + n: serializedNodeWithId, + doc: Document, + map: idNodeMap, +): INode | null { + let node = buildNode(n, doc); + if (!node) { return null; } - if (n.type === NodeType.Element) { - (root as HTMLElement).setAttribute('data-rrid', String(n.id)); + // use target document as root document + if (n.type === NodeType.Document) { + doc.open(); + node = doc; } + + (node as INode).__sn = n; + map[n.id] = node as INode; if (n.type === NodeType.Document || n.type === NodeType.Element) { for (const childN of n.childNodes) { - const childNode = rebuild(childN, doc); + const childNode = buildNodeWithSN(childN, doc, map); if (!childNode) { console.warn('Failed to rebuild', childN); } else { - root.appendChild(childNode); + node.appendChild(childNode); } } } - return root; + return node as INode; +} + +function rebuild( + n: serializedNodeWithId, + doc: Document, +): [Node | null, idNodeMap] { + const idNodeMap: idNodeMap = {}; + return [buildNodeWithSN(n, doc, idNodeMap), idNodeMap]; } export default rebuild; diff --git a/src/snapshot.ts b/src/snapshot.ts index 5674e86d..d141b35b 100644 --- a/src/snapshot.ts +++ b/src/snapshot.ts @@ -187,24 +187,12 @@ export function serializeNodeWithId( }); (n as INode).__sn = serializedNode; map[serializedNode.id] = n as INode; - return serializedNode; -} - -function _snapshot( - n: Node, - doc: Document, - map: idNodeMap, -): serializedNodeWithId | null { - const serializedNode = serializeNodeWithId(n, doc, map); - if (!serializedNode) { - return null; - } if ( serializedNode.type === NodeType.Document || serializedNode.type === NodeType.Element ) { for (const childN of Array.from(n.childNodes)) { - const serializedChildNode = _snapshot(childN, doc, map); + const serializedChildNode = serializeNodeWithId(childN, doc, map); if (serializedChildNode) { serializedNode.childNodes.push(serializedChildNode); } @@ -216,7 +204,7 @@ function _snapshot( function snapshot(n: Document): [serializedNodeWithId | null, idNodeMap] { resetId(); const idNodeMap: idNodeMap = {}; - return [_snapshot(n, n, idNodeMap), idNodeMap]; + return [serializeNodeWithId(n, n, idNodeMap), idNodeMap]; } export default snapshot; diff --git a/test/__snapshots__/integration.ts.snap b/test/__snapshots__/integration.ts.snap index 87f1446e..f2213003 100644 --- a/test/__snapshots__/integration.ts.snap +++ b/test/__snapshots__/integration.ts.snap @@ -1,9 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`[html file]: about-mozilla.html 1`] = ` -" - The Book of Mozilla, 11:9 - -

- Mammon slept. And the beast reborn spread over the earth and its numbers - grew legion. And they proclaimed the times and sacrificed crops unto the - fire, with the cunning of foxes. And they built a new world in their own - image as promised by the - sacred words, and spoke +

+ Mammon slept. And the beast reborn spread over the earth and its numbers + grew legion. And they proclaimed the times and sacrificed crops unto the + fire, with the cunning of foxes. And they built a new world in their own + image as promised by the + sacred words, and spoke of the beast with their children. Mammon awoke, and lo! it was - naught but a follower. -

- from The Book of Mozilla, 11:9
(10th Edition) + naught but a follower. +

+ from The Book of Mozilla, 11:9
(10th Edition)

" `; exports[`[html file]: basic.html 1`] = ` -" - - - - Document -" +" + + + + Document +" `; exports[`[html file]: cors-style-sheet.html 1`] = ` -" - - - - with style sheet - -" +" + + + + with style sheet + +" `; exports[`[html file]: form-fields.html 1`] = ` -" - - - - form fields - -
-