|
@@ -24,6 +24,7 @@
|
|
|
/* global browser, document, location, setTimeout, XMLHttpRequest, Node, DOMParser, Blob, URL, Image, OffscreenCanvas, CustomEvent */
|
|
/* global browser, document, location, setTimeout, XMLHttpRequest, Node, DOMParser, Blob, URL, Image, OffscreenCanvas, CustomEvent */
|
|
|
|
|
|
|
|
const MAX_CONTENT_SIZE = 32 * (1024 * 1024);
|
|
const MAX_CONTENT_SIZE = 32 * (1024 * 1024);
|
|
|
|
|
+const NESTING_TRACK_ID_ATTRIBUTE_NAME = "data-sf-nesting-track-id";
|
|
|
|
|
|
|
|
const singlefile = globalThis.singlefileBootstrap;
|
|
const singlefile = globalThis.singlefileBootstrap;
|
|
|
const pendingResponses = new Map();
|
|
const pendingResponses = new Map();
|
|
@@ -306,6 +307,7 @@ async function openEditor(document) {
|
|
|
content = await getContent();
|
|
content = await getContent();
|
|
|
} else {
|
|
} else {
|
|
|
serializeShadowRoots(document);
|
|
serializeShadowRoots(document);
|
|
|
|
|
+ markInvalidNesting(document);
|
|
|
content = singlefile.helper.serialize(document);
|
|
content = singlefile.helper.serialize(document);
|
|
|
}
|
|
}
|
|
|
for (let blockIndex = 0; blockIndex * MAX_CONTENT_SIZE < content.length; blockIndex++) {
|
|
for (let blockIndex = 0; blockIndex * MAX_CONTENT_SIZE < content.length; blockIndex++) {
|
|
@@ -380,4 +382,96 @@ function serializeShadowRoots(node) {
|
|
|
element.appendChild(templateElement);
|
|
element.appendChild(templateElement);
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function markInvalidNesting(doc) {
|
|
|
|
|
+ addTrackIds(doc.body);
|
|
|
|
|
+ const verificationDoc = parseDocContent(serialize(doc));
|
|
|
|
|
+ const markedMap = buildTrackIdMap(doc.body);
|
|
|
|
|
+ const normalizedMap = buildTrackIdMap(verificationDoc.body);
|
|
|
|
|
+ const trackIds = new Set();
|
|
|
|
|
+ Object.keys(markedMap).forEach(id => {
|
|
|
|
|
+ if (id in normalizedMap) {
|
|
|
|
|
+ const markedParent = markedMap[id].parentElement?.getAttribute(NESTING_TRACK_ID_ATTRIBUTE_NAME) || null;
|
|
|
|
|
+ const normalizedParent = normalizedMap[id]?.parentElement?.getAttribute(NESTING_TRACK_ID_ATTRIBUTE_NAME) || null;
|
|
|
|
|
+ if (markedParent !== normalizedParent) {
|
|
|
|
|
+ let current = markedMap[id];
|
|
|
|
|
+ while (current && current !== doc.body) {
|
|
|
|
|
+ const currentId = current.getAttribute(NESTING_TRACK_ID_ATTRIBUTE_NAME);
|
|
|
|
|
+ if (currentId) {
|
|
|
|
|
+ trackIds.add(currentId);
|
|
|
|
|
+ }
|
|
|
|
|
+ current = current.parentElement;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ cleanupTrackIds(doc.body, trackIds);
|
|
|
|
|
+
|
|
|
|
|
+ function addTrackIds(element, index = 0, parentTrackId = "") {
|
|
|
|
|
+ const trackId = parentTrackId ? `${parentTrackId}.${index + 1}` : `${index + 1}`;
|
|
|
|
|
+ element.setAttribute(NESTING_TRACK_ID_ATTRIBUTE_NAME, trackId);
|
|
|
|
|
+ Array.from(element.children).forEach((child, indexChild) => addTrackIds(child, indexChild, trackId));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function buildTrackIdMap(element) {
|
|
|
|
|
+ const trackIds = {};
|
|
|
|
|
+ traverse(element);
|
|
|
|
|
+ return trackIds;
|
|
|
|
|
+
|
|
|
|
|
+ function traverse(element) {
|
|
|
|
|
+ if (element.getAttribute) {
|
|
|
|
|
+ const id = element.getAttribute(NESTING_TRACK_ID_ATTRIBUTE_NAME);
|
|
|
|
|
+ if (id) {
|
|
|
|
|
+ trackIds[id] = element;
|
|
|
|
|
+ }
|
|
|
|
|
+ Array.from(element.children).forEach(traverse);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function cleanupTrackIds(element, toKeep) {
|
|
|
|
|
+ const id = element.getAttribute(NESTING_TRACK_ID_ATTRIBUTE_NAME);
|
|
|
|
|
+ if (id && !toKeep.has(id)) {
|
|
|
|
|
+ element.removeAttribute(NESTING_TRACK_ID_ATTRIBUTE_NAME);
|
|
|
|
|
+ }
|
|
|
|
|
+ Array.from(element.children).forEach(child => cleanupTrackIds(child, toKeep));
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function parseDocContent(content, baseURI) {
|
|
|
|
|
+ const doc = (new DOMParser()).parseFromString(content, "text/html");
|
|
|
|
|
+ if (!doc.head) {
|
|
|
|
|
+ doc.documentElement.insertBefore(doc.createElement("HEAD"), doc.body);
|
|
|
|
|
+ }
|
|
|
|
|
+ let baseElement = doc.querySelector("base");
|
|
|
|
|
+ if (!baseElement || !baseElement.getAttribute("href")) {
|
|
|
|
|
+ if (baseElement) {
|
|
|
|
|
+ baseElement.remove();
|
|
|
|
|
+ }
|
|
|
|
|
+ baseElement = doc.createElement("base");
|
|
|
|
|
+ baseElement.setAttribute("href", baseURI);
|
|
|
|
|
+ doc.head.insertBefore(baseElement, doc.head.firstChild);
|
|
|
|
|
+ }
|
|
|
|
|
+ return doc;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function serialize(doc) {
|
|
|
|
|
+ const docType = doc.doctype;
|
|
|
|
|
+ let docTypeString = "";
|
|
|
|
|
+ if (docType) {
|
|
|
|
|
+ docTypeString = "<!DOCTYPE " + docType.nodeName;
|
|
|
|
|
+ if (docType.publicId) {
|
|
|
|
|
+ docTypeString += " PUBLIC \"" + docType.publicId + "\"";
|
|
|
|
|
+ if (docType.systemId) {
|
|
|
|
|
+ docTypeString += " \"" + docType.systemId + "\"";
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if (docType.systemId) {
|
|
|
|
|
+ docTypeString += " SYSTEM \"" + docType.systemId + "\"";
|
|
|
|
|
+ } if (docType.internalSubset) {
|
|
|
|
|
+ docTypeString += " [" + docType.internalSubset + "]";
|
|
|
|
|
+ }
|
|
|
|
|
+ docTypeString += "> ";
|
|
|
|
|
+ }
|
|
|
|
|
+ return docTypeString + doc.documentElement.outerHTML;
|
|
|
}
|
|
}
|