Browse Source

add support of invalid nesting tags

Gildas 2 months ago
parent
commit
3619f54f05

File diff suppressed because it is too large
+ 0 - 0
lib/single-file-extension-bootstrap.js


File diff suppressed because it is too large
+ 0 - 0
lib/single-file-extension-editor-helper.js


+ 18 - 0
lib/single-file-extension-editor.js

@@ -1324,6 +1324,7 @@
 				const contentDocument = (new DOMParser()).parseFromString(docContent, "text/html");
 				if (detectSavedPage(contentDocument)) {
 					await singlefile.helper.display(document, docContent, { disableFramePointerEvents: true });
+					singlefile.helper.fixInvalidNesting(document);
 					const infobarElement = document.querySelector(singlefile.helper.INFOBAR_TAGNAME);
 					if (infobarElement) {
 						infobarElement.remove();
@@ -1385,6 +1386,7 @@
 						element.style.setProperty(pointerEvents, "none", "important");
 					});
 					document.replaceChild(contentDocument.documentElement, document.documentElement);
+					singlefile.helper.fixInvalidNesting(document);
 					document.querySelectorAll("[data-single-file-note-refs]").forEach(noteRefElement => noteRefElement.dataset.singleFileNoteRefs = noteRefElement.dataset.singleFileNoteRefs.replace(/,/g, " "));
 					deserializeShadowRoots(document);
 					document.querySelectorAll(NOTE_TAGNAME).forEach(containerElement => attachNoteListeners(containerElement, true));
@@ -2225,6 +2227,7 @@
 		function getContent(compressHTML) {
 			unhighlightCutElement();
 			serializeShadowRoots(document);
+			singlefile.helper.markInvalidNesting(document);
 			const doc = document.cloneNode(true);
 			disableHighlight(doc);
 			resetSelectedElements(doc);
@@ -3352,6 +3355,7 @@ pre code {
 			const NOTE_HEADER_HEIGHT = ${JSON.stringify(NOTE_HEADER_HEIGHT)};
 			const PAGE_MASK_ACTIVE_CLASS = ${JSON.stringify(PAGE_MASK_ACTIVE_CLASS)};
 			const REMOVED_CONTENT_CLASS = ${JSON.stringify(REMOVED_CONTENT_CLASS)};
+			const NESTING_TRACK_ID_ATTRIBUTE_NAME = ${JSON.stringify(singlefile.helper.NESTING_TRACK_ID_ATTRIBUTE_NAME)};
 			const reflowNotes = ${minifyText(reflowNotes.toString())};			
 			const addNoteRef = ${minifyText(addNoteRef.toString())};
 			const deleteNoteRef = ${minifyText(deleteNoteRef.toString())};
@@ -3378,6 +3382,20 @@ pre code {
 			if (document.documentElement.dataset && document.documentElement.dataset.sfz !== undefined) {
 				waitResourcesLoad().then(reflowNotes);
 			}
+			const trackIds = {};
+			document.querySelectorAll("[" + NESTING_TRACK_ID_ATTRIBUTE_NAME + "]").forEach(element => trackIds[element.getAttribute(NESTING_TRACK_ID_ATTRIBUTE_NAME)] = element);
+			Object.keys(trackIds).forEach(id => {
+				const element = trackIds[id];
+				const idParts = id.split(".");
+				if (idParts.length > 1) {
+					const parentId = idParts.slice(0, -1).join(".");
+					const expectedParent = trackIds[parentId];
+					if (expectedParent && element.parentElement !== expectedParent) {
+						expectedParent.appendChild(element);
+					}
+				}
+			});
+			document.querySelectorAll("[" + NESTING_TRACK_ID_ATTRIBUTE_NAME + "]").forEach(element => element.removeAttribute(NESTING_TRACK_ID_ATTRIBUTE_NAME));
 		})()`);
 		}
 

File diff suppressed because it is too large
+ 0 - 0
lib/single-file.js


+ 4 - 4
package-lock.json

@@ -9,7 +9,7 @@
 			"version": "1.2.4",
 			"license": "AGPL-3.0-or-later",
 			"dependencies": {
-				"single-file-core": "1.5.59"
+				"single-file-core": "1.5.61"
 			},
 			"devDependencies": {
 				"@rollup/plugin-node-resolve": "16.0.3",
@@ -1642,9 +1642,9 @@
 			}
 		},
 		"node_modules/single-file-core": {
-			"version": "1.5.59",
-			"resolved": "https://registry.npmjs.org/single-file-core/-/single-file-core-1.5.59.tgz",
-			"integrity": "sha512-EqVvpTwUM/pVLSmMSDMF8BYxt0ZRndUhADJ+DYtIfq1QHOUBvnBmvgaI+6Bo2dq5AmsSRhpQyf1b0a4DIyBEOw==",
+			"version": "1.5.61",
+			"resolved": "https://registry.npmjs.org/single-file-core/-/single-file-core-1.5.61.tgz",
+			"integrity": "sha512-H979BB9jXdZ6QAgykIaPfwAGEq+eo/JGQh9uhVSOFLXPwthVN9JSfMs48ge6kwr7qFQPHl/f9JBvvSZNjB9hSA==",
 			"license": "AGPL-3.0-or-later"
 		},
 		"node_modules/smob": {

+ 1 - 1
package.json

@@ -10,7 +10,7 @@
 	},
 	"type": "module",
 	"dependencies": {
-		"single-file-core": "1.5.59"
+		"single-file-core": "1.5.61"
 	},
 	"devDependencies": {
 		"eslint": "^9.39.1",

+ 94 - 0
src/core/content/content-bootstrap.js

@@ -24,6 +24,7 @@
 /* global browser, document, location, setTimeout, XMLHttpRequest, Node, DOMParser, Blob, URL, Image, OffscreenCanvas, CustomEvent */
 
 const MAX_CONTENT_SIZE = 32 * (1024 * 1024);
+const NESTING_TRACK_ID_ATTRIBUTE_NAME = "data-sf-nesting-track-id";
 
 const singlefile = globalThis.singlefileBootstrap;
 const pendingResponses = new Map();
@@ -306,6 +307,7 @@ async function openEditor(document) {
 		content = await getContent();
 	} else {
 		serializeShadowRoots(document);
+		markInvalidNesting(document);
 		content = singlefile.helper.serialize(document);
 	}
 	for (let blockIndex = 0; blockIndex * MAX_CONTENT_SIZE < content.length; blockIndex++) {
@@ -380,4 +382,96 @@ function serializeShadowRoots(node) {
 			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;
 }

+ 18 - 0
src/ui/content/content-ui-editor-web.js

@@ -300,6 +300,7 @@ import { downloadPageForeground } from "../../core/common/download.js";
 			const contentDocument = (new DOMParser()).parseFromString(docContent, "text/html");
 			if (detectSavedPage(contentDocument)) {
 				await singlefile.helper.display(document, docContent, { disableFramePointerEvents: true });
+				singlefile.helper.fixInvalidNesting(document);
 				const infobarElement = document.querySelector(singlefile.helper.INFOBAR_TAGNAME);
 				if (infobarElement) {
 					infobarElement.remove();
@@ -361,6 +362,7 @@ import { downloadPageForeground } from "../../core/common/download.js";
 					element.style.setProperty(pointerEvents, "none", "important");
 				});
 				document.replaceChild(contentDocument.documentElement, document.documentElement);
+				singlefile.helper.fixInvalidNesting(document);
 				document.querySelectorAll("[data-single-file-note-refs]").forEach(noteRefElement => noteRefElement.dataset.singleFileNoteRefs = noteRefElement.dataset.singleFileNoteRefs.replace(/,/g, " "));
 				deserializeShadowRoots(document);
 				document.querySelectorAll(NOTE_TAGNAME).forEach(containerElement => attachNoteListeners(containerElement, true));
@@ -1201,6 +1203,7 @@ import { downloadPageForeground } from "../../core/common/download.js";
 	function getContent(compressHTML) {
 		unhighlightCutElement();
 		serializeShadowRoots(document);
+		singlefile.helper.markInvalidNesting(document);
 		const doc = document.cloneNode(true);
 		disableHighlight(doc);
 		resetSelectedElements(doc);
@@ -2328,6 +2331,7 @@ pre code {
 			const NOTE_HEADER_HEIGHT = ${JSON.stringify(NOTE_HEADER_HEIGHT)};
 			const PAGE_MASK_ACTIVE_CLASS = ${JSON.stringify(PAGE_MASK_ACTIVE_CLASS)};
 			const REMOVED_CONTENT_CLASS = ${JSON.stringify(REMOVED_CONTENT_CLASS)};
+			const NESTING_TRACK_ID_ATTRIBUTE_NAME = ${JSON.stringify(singlefile.helper.NESTING_TRACK_ID_ATTRIBUTE_NAME)};
 			const reflowNotes = ${minifyText(reflowNotes.toString())};			
 			const addNoteRef = ${minifyText(addNoteRef.toString())};
 			const deleteNoteRef = ${minifyText(deleteNoteRef.toString())};
@@ -2354,6 +2358,20 @@ pre code {
 			if (document.documentElement.dataset && document.documentElement.dataset.sfz !== undefined) {
 				waitResourcesLoad().then(reflowNotes);
 			}
+			const trackIds = {};
+			document.querySelectorAll("[" + NESTING_TRACK_ID_ATTRIBUTE_NAME + "]").forEach(element => trackIds[element.getAttribute(NESTING_TRACK_ID_ATTRIBUTE_NAME)] = element);
+			Object.keys(trackIds).forEach(id => {
+				const element = trackIds[id];
+				const idParts = id.split(".");
+				if (idParts.length > 1) {
+					const parentId = idParts.slice(0, -1).join(".");
+					const expectedParent = trackIds[parentId];
+					if (expectedParent && element.parentElement !== expectedParent) {
+						expectedParent.appendChild(element);
+					}
+				}
+			});
+			document.querySelectorAll("[" + NESTING_TRACK_ID_ATTRIBUTE_NAME + "]").forEach(element => element.removeAttribute(NESTING_TRACK_ID_ATTRIBUTE_NAME));
 		})()`);
 	}
 

Some files were not shown because too many files changed in this diff