From e7753e1c24ab62b2ddbee1fe6d7e5fe3e8012d87 Mon Sep 17 00:00:00 2001 From: Yanzhen Yu Date: Wed, 1 Apr 2026 12:00:00 +0800 Subject: [PATCH] change relative path into absolute path --- README.md | 5 ---- src/snapshot.ts | 38 ++++++++++++++++++++++++-- test/__snapshots__/integration.ts.snap | 18 ++++++++++-- test/css/style.css | 2 ++ test/html/with-relative-res.html | 13 +++++++++ 5 files changed, 66 insertions(+), 10 deletions(-) create mode 100644 test/html/with-relative-res.html diff --git a/README.md b/README.md index d61b9f71..854d85b9 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,3 @@ Snapshot the DOM into a stateful and serializable data structure. Also provide the ability to rebuild the DOM via snapshot. - -## TODO - -- [ ] Replace any url in css rules into absolute path. -- [ ] Select a suitable build strategy. diff --git a/src/snapshot.ts b/src/snapshot.ts index 04eddcf3..e4527e92 100644 --- a/src/snapshot.ts +++ b/src/snapshot.ts @@ -28,6 +28,35 @@ function getCssRulesString(s: CSSStyleSheet): string | null { } } +const URL_IN_CSS_REF = /url\((['"])([^'"]*)\1\)/gm; +function absoluteToStylesheet(cssText: string, href: string): string { + return cssText.replace(URL_IN_CSS_REF, (_1, _2, filePath) => { + const stack = href.split('/'); + const parts = filePath.split('/'); + stack.pop(); + for (const part of parts) { + if (part === '.') { + continue; + } else if (part === '..') { + stack.pop(); + } else { + stack.push(part); + } + } + return `url('${stack.join('/')}')`; + }); +} + +const RELATIVE_PATH = /^(\.\.|\.|)\//; +function absoluteToDoc(doc: Document, attributeValue: string): string { + if (!RELATIVE_PATH.test(attributeValue)) { + return attributeValue; + } + const a: HTMLAnchorElement = document.createElement('a'); + a.href = attributeValue; + return a.href; +} + function serializeNode(n: Node, doc: Document): serializedNode | false { switch (n.nodeType) { case n.DOCUMENT_NODE: @@ -46,7 +75,12 @@ function serializeNode(n: Node, doc: Document): serializedNode | false { const tagName = (n as HTMLElement).tagName.toLowerCase(); let attributes: attributes = {}; for (const { name, value } of Array.from((n as HTMLElement).attributes)) { - attributes[name] = value; + // relative path in attribute + if (name === 'src' || name === 'href') { + attributes[name] = absoluteToDoc(doc, value); + } else { + attributes[name] = value; + } } // remote css if (tagName === 'link') { @@ -56,7 +90,7 @@ function serializeNode(n: Node, doc: Document): serializedNode | false { const cssText = getCssRulesString(stylesheet as CSSStyleSheet); if (cssText) { attributes = { - _cssText: cssText, + _cssText: absoluteToStylesheet(cssText, stylesheet!.href!), }; } } diff --git a/test/__snapshots__/integration.ts.snap b/test/__snapshots__/integration.ts.snap index 6aba1b5e..45ae353e 100644 --- a/test/__snapshots__/integration.ts.snap +++ b/test/__snapshots__/integration.ts.snap @@ -96,7 +96,7 @@ exports[`[html file]: iframe.html 1`] = ` iframe - + " `; @@ -110,6 +110,18 @@ exports[`[html file]: invalid-attribute.html 1`] = ` " `; +exports[`[html file]: with-relative-res.html 1`] = ` +" + + + + Document + + + + \\"\\"" +`; + exports[`[html file]: with-script.html 1`] = ` " @@ -117,7 +129,7 @@ exports[`[html file]: with-script.html 1`] = ` with script - + " `; @@ -127,7 +139,7 @@ exports[`[html file]: with-style-sheet.html 1`] = ` with style sheet - + " `; diff --git a/test/css/style.css b/test/css/style.css index f1be9954..fe10f55d 100644 --- a/test/css/style.css +++ b/test/css/style.css @@ -1,8 +1,10 @@ body { margin: 0; + background: url('../a.jpg'); } p { color: red; + background: url('./b.jpg'); } body > p { color: yellow; diff --git a/test/html/with-relative-res.html b/test/html/with-relative-res.html new file mode 100644 index 00000000..9e75f035 --- /dev/null +++ b/test/html/with-relative-res.html @@ -0,0 +1,13 @@ + + + + + + + Document + + + + + + \ No newline at end of file