makearmy-app/components/utilities/SVGNestPanel.tsx

149 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useEffect, useRef, useState } from "react";
/**
* Inline loader for a static app (SVGnest) placed under /public/svgnest.
* It fetches index.html, injects the HTML into a container, then
* re-executes <script> tags (both external and inline) in order.
* CSS <link> files are fetched and inlined into <style> tags to avoid path issues.
*
* NOTE: This runs in the light DOM (not shadow). That maximizes compatibility
* with older scripts that rely on document-level selectors. If we see style bleed,
* we can optionally switch to a Shadow DOM variant later.
*/
export default function SVGNestPanel() {
const containerRef = useRef<HTMLDivElement | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
async function load() {
try {
setLoading(true);
// 1) Fetch index.html
const htmlRes = await fetch("/svgnest/index.html", { cache: "no-cache" });
if (!htmlRes.ok) throw new Error(`Failed to fetch index.html (HTTP ${htmlRes.status})`);
const htmlText = await htmlRes.text();
if (cancelled) return;
// 2) Parse
const parser = new DOMParser();
const doc = parser.parseFromString(htmlText, "text/html");
// 3) Find and inline CSS (convert <link rel="stylesheet"> to <style>)
const linkEls = Array.from(doc.querySelectorAll<HTMLLinkElement>('link[rel="stylesheet"][href]'));
for (const link of linkEls) {
const href = link.getAttribute("href") || "";
const absHref = absolutizeAsset(href);
try {
const cssRes = await fetch(absHref, { cache: "no-cache" });
if (cssRes.ok) {
const cssText = await cssRes.text();
const style = doc.createElement("style");
style.setAttribute("data-inlined-from", href);
style.textContent = cssText;
link.replaceWith(style);
} else {
// leave the original link as a fallback
}
} catch {
// leave the original link as a fallback
}
}
// 4) Extract scripts (to re-execute later) & remove them from doc body
const scriptInfos: { src?: string; inline?: string; type?: string; nomodule?: boolean; async?: boolean; defer?: boolean }[] = [];
doc.querySelectorAll("script").forEach((s) => {
scriptInfos.push({
src: s.getAttribute("src") || undefined,
inline: s.textContent || undefined,
type: s.getAttribute("type") || undefined,
nomodule: s.hasAttribute("nomodule"),
async: s.hasAttribute("async"),
defer: s.hasAttribute("defer"),
});
s.remove(); // prevent duplicate execution when we inject innerHTML
});
// 5) Inject BODY HTML into our container
const host = containerRef.current!;
host.innerHTML = doc.body.innerHTML;
// 6) Sequentially (re)execute scripts in original order
// We do this after body is in place so DOM targets exist.
for (const info of scriptInfos) {
await runScript(host, info);
}
} catch (e: any) {
if (!cancelled) setError(e?.message || String(e));
} finally {
if (!cancelled) setLoading(false);
}
}
load();
return () => { cancelled = true; };
}, []);
return (
<div className="w-full">
{error && (
<div className="mb-3 rounded border border-rose-600/40 bg-rose-600/10 p-3 text-sm">
Couldnt load SVGnest: {error}
</div>
)}
{loading && (
<div className="mb-3 text-sm text-zinc-400">Loading SVGnest</div>
)}
<div ref={containerRef} className="w-full min-h-[70vh]" />
</div>
);
}
/** Convert a (possibly relative) asset path from /svgnest/index.html into an absolute path */
function absolutizeAsset(path: string): string {
// Ignore full URLs
try {
const u = new URL(path);
return u.toString();
} catch {
// not a full URL
}
// Handle protocol-relative //host/path -> treat as absolute
if (path.startsWith("//")) return window.location.protocol + path;
// If already absolute (/x/y), leave as-is
if (path.startsWith("/")) return path;
// Otherwise treat as relative to /svgnest/
return `/svgnest/${path.replace(/^\.?\//, "")}`;
}
/** Append a new <script> to the container, respecting order. Supports src and inline. */
function runScript(container: HTMLElement, info: { src?: string; inline?: string; type?: string; nomodule?: boolean; async?: boolean; defer?: boolean; }): Promise<void> {
return new Promise((resolve, reject) => {
const s = document.createElement("script");
if (info.type) s.type = info.type;
if (info.nomodule) s.noModule = true;
// Preserve async/defer semantics if they existed
if (info.async) s.async = true;
if (info.defer) s.defer = true;
if (info.src) {
s.src = absolutizeAsset(info.src);
s.onload = () => resolve();
s.onerror = () => reject(new Error(`Failed to load script: ${s.src}`));
container.appendChild(s);
} else {
// Inline script: execute synchronously
s.textContent = info.inline || "";
container.appendChild(s);
resolve();
}
});
}