Skip to content

Commit

Permalink
Live viewer: stop using iframes
Browse files Browse the repository at this point in the history
We can just assume the browser has a working URL constructor.
  • Loading branch information
domenic committed Dec 30, 2022
1 parent dd7c5e4 commit 34aebfb
Show file tree
Hide file tree
Showing 2 changed files with 123 additions and 144 deletions.
2 changes: 0 additions & 2 deletions live-viewer/index.html
Expand Up @@ -63,5 +63,3 @@ <h2>jsdom/whatwg-url's components</h2>
<footer>
<a href="https://github.com/jsdom/whatwg-url/tree/master/live-viewer">Fork me on GitHub!</a>
</footer>

<iframe hidden id="browser-iframe"></iframe>
265 changes: 123 additions & 142 deletions live-viewer/live-viewer.mjs
@@ -1,158 +1,139 @@
import whatwgURL from "./whatwg-url.mjs";
(() => {
const urlInput = document.querySelector("#url");
const baseInput = document.querySelector("#base");

const te = new TextEncoder();
const td = new TextDecoder();

// Use an iframe to avoid <base> affecting the main page. This is especially bad in Edge where it
// appears to break Edge's DevTools.
const browserIframeDocument = document.querySelector("#browser-iframe").contentDocument;
const browserAnchor = browserIframeDocument.createElement("a");
const browserBase = browserIframeDocument.createElement("base");
browserIframeDocument.head.appendChild(browserBase);
browserIframeDocument.body.appendChild(browserAnchor);

const components = [
"href",
"protocol",
"username",
"password",
"port",
"hostname",
"pathname",
"search",
"hash",
"origin"
];

urlInput.addEventListener("input", update);
baseInput.addEventListener("input", update);
window.addEventListener("hashchange", setFromFragment);
setFromFragment();

function update() {
const browserResult = getBrowserResult();
const jsdomResult = getJsdomResult();
const mismatchedComponents = getMismatchedComponents(browserResult, jsdomResult);

setResult("browser", browserResult, mismatchedComponents);
setResult("jsdom", jsdomResult, mismatchedComponents);
updateFragmentForSharing();
}

function setResult(kind, result, mismatchedComponents) {
const output = document.querySelector(`#${kind}-output`);
const error = document.querySelector(`#${kind}-error`);

if (result instanceof Error) {
output.hidden = true;
error.hidden = false;
error.textContent = result.toString();
} else {
output.hidden = false;
error.hidden = true;
for (const component of components) {
const componentEl = output.querySelector(`.${component}`).querySelector("td");
setComponentElValue(componentEl, result[component]);
setComponentElMismatch(componentEl, mismatchedComponents.has(component));
}
}
}

function setComponentElValue(componentEl, value) {
// This shows up in Edge where username/password are undefined.
const isNonString = typeof value !== "string";
const isEmptyString = value === "";

componentEl.textContent = isEmptyString ? "(empty string)" : value;
componentEl.classList.toggle("empty-string", isEmptyString);
componentEl.classList.toggle("non-string", isNonString);
}

function setComponentElMismatch(componentEl, isMismatched) {
componentEl.classList.toggle("pass", !isMismatched);
componentEl.classList.toggle("fail", isMismatched);
}

function getMismatchedComponents(result1, result2) {
const mismatched = new Set();
const urlInput = document.querySelector("#url");
const baseInput = document.querySelector("#base");

const te = new TextEncoder();
const td = new TextDecoder();

const components = [
"href",
"protocol",
"username",
"password",
"port",
"hostname",
"pathname",
"search",
"hash",
"origin"
];

urlInput.addEventListener("input", update);
baseInput.addEventListener("input", update);
window.addEventListener("hashchange", setFromFragment);
setFromFragment();

function update() {
const browserResult = getBrowserResult();
const jsdomResult = getJsdomResult();
const mismatchedComponents = getMismatchedComponents(browserResult, jsdomResult);

setResult("browser", browserResult, mismatchedComponents);
setResult("jsdom", jsdomResult, mismatchedComponents);
updateFragmentForSharing();
}

function setResult(kind, result, mismatchedComponents) {
const output = document.querySelector(`#${kind}-output`);
const error = document.querySelector(`#${kind}-error`);

if (result instanceof Error) {
output.hidden = true;
error.hidden = false;
error.textContent = result.toString();
} else {
output.hidden = false;
error.hidden = true;
for (const component of components) {
if (result1[component] !== result2[component]) {
mismatched.add(component);
}
const componentEl = output.querySelector(`.${component}`).querySelector("td");
setComponentElValue(componentEl, result[component]);
setComponentElMismatch(componentEl, mismatchedComponents.has(component));
}
return mismatched;
}

function getBrowserResult() {
// First make sure the base is not invalid by testing it against an about:blank base.
browserBase.href = "about:blank";
browserAnchor.href = baseInput.value;
if (browserAnchor.protocol === ":") {
return new Error("Browser could not parse the base URL");
}

function setComponentElValue(componentEl, value) {
// This shows up in Edge where username/password are undefined.
const isNonString = typeof value !== "string";
const isEmptyString = value === "";

componentEl.textContent = isEmptyString ? "(empty string)" : value;
componentEl.classList.toggle("empty-string", isEmptyString);
componentEl.classList.toggle("non-string", isNonString);
}

function setComponentElMismatch(componentEl, isMismatched) {
componentEl.classList.toggle("pass", !isMismatched);
componentEl.classList.toggle("fail", isMismatched);
}

function getMismatchedComponents(result1, result2) {
const mismatched = new Set();
for (const component of components) {
if (result1[component] !== result2[component]) {
mismatched.add(component);
}

// Now actually parse the URL against the base.
browserAnchor.href = urlInput.value;
browserBase.href = baseInput.value;
if (browserAnchor.protocol === ":") {
return new Error("Browser could not parse the input");
}

return browserAnchor;
}

function getJsdomResult() {
try {
return new whatwgURL.URL(urlInput.value, baseInput.value);
} catch (e) {
return e;
}
return mismatched;
}

function getBrowserResult() {
try {
return new URL(urlInput.value, baseInput.value);
} catch (e) {
return e;
}
}

function updateFragmentForSharing() {
location.hash = `url=${encodeToBase64(urlInput.value)}&base=${encodeToBase64(baseInput.value)}`;
function getJsdomResult() {
try {
return new whatwgURL.URL(urlInput.value, baseInput.value);
} catch (e) {
return e;
}
}

function setFromFragment() {
const pieces = /#url=([^&]*)&base=(.*)/u.exec(location.hash);
if (!pieces) {
return;
}
const [, urlEncoded, baseEncoded] = pieces;
try {
urlInput.value = decodeFromBase64(urlEncoded);
} catch (e) {
// eslint-disable-next-line no-console
console.warn("url hash parameter was not deserializable.");
}

try {
baseInput.value = decodeFromBase64(baseEncoded);
} catch (e) {
// eslint-disable-next-line no-console
console.warn("base hash parameter was not deserializable.");
}
function updateFragmentForSharing() {
location.hash = `url=${encodeToBase64(urlInput.value)}&base=${encodeToBase64(baseInput.value)}`;
}

update();
function setFromFragment() {
const pieces = /#url=([^&]*)&base=(.*)/u.exec(location.hash);
if (!pieces) {
return;
}

// btoa / atob don't work on Unicode.
// This version is a superset of btoa / atob, so it maintains compatibility with older versions of
// the live viewer which used btoa / atob directly.
function encodeToBase64(originalString) {
const bytes = te.encode(originalString);
const byteString = Array.from(bytes, byte => String.fromCharCode(byte)).join("");
const encoded = btoa(byteString);
return encoded;
const [, urlEncoded, baseEncoded] = pieces;
try {
urlInput.value = decodeFromBase64(urlEncoded);
} catch (e) {
// eslint-disable-next-line no-console
console.warn("url hash parameter was not deserializable.");
}

function decodeFromBase64(encoded) {
const byteString = atob(encoded);
const bytes = Uint8Array.from(byteString, char => char.charCodeAt(0));
const originalString = td.decode(bytes);
return originalString;
try {
baseInput.value = decodeFromBase64(baseEncoded);
} catch (e) {
// eslint-disable-next-line no-console
console.warn("base hash parameter was not deserializable.");
}
})();

update();
}

// btoa / atob don't work on Unicode.
// This version is a superset of btoa / atob, so it maintains compatibility with older versions of
// the live viewer which used btoa / atob directly.
function encodeToBase64(originalString) {
const bytes = te.encode(originalString);
const byteString = Array.from(bytes, byte => String.fromCharCode(byte)).join("");
const encoded = btoa(byteString);
return encoded;
}

function decodeFromBase64(encoded) {
const byteString = atob(encoded);
const bytes = Uint8Array.from(byteString, char => char.charCodeAt(0));
const originalString = td.decode(bytes);
return originalString;
}

0 comments on commit 34aebfb

Please sign in to comment.