// js/loader.js // 负责:加载 manifest、加载模板 HTML(并注入模板 CSS)、渲染接口、加载 override CSS // 依赖:./engine.js 导出的 renderTemplate(template, data, opts) import { renderTemplate } from "./engine.js"; const manifestCache = new Map(); // manifestPath -> manifest object const templateCache = new Map(); // cacheKey -> { html, meta, baseUrl } /** * Resolve a possibly-relative URL against a base (template HTML path or manifest path). * Returns an absolute-ish href (as string) that can be used in link[href="..."] matching. */ function resolveUrl(href, base) { try { // new URL(href, base) will resolve relative href against base return new URL(href, base).toString(); } catch (e) { // fallback: return original return href; } } /** * Load manifest JSON (模板注册表). 支持缓存。 * manifestPath: string (绝对或相对 URL) */ export async function loadManifest(manifestPath = "templates/manifest.json") { if (manifestCache.has(manifestPath)) return manifestCache.get(manifestPath); const res = await fetch(manifestPath); if (!res.ok) throw new Error(`无法加载 manifest: ${manifestPath} (${res.status})`); const manifest = await res.json(); // store base for resolving any relative css/html in manifest entries const base = new URL(location.origin + location.pathname + manifestPath).toString(); manifestCache.set(manifestPath, { data: manifest, base }); return manifestCache.get(manifestPath); } /** * Load template by name using manifest. * manifestPath: path to manifest (from config) — loader will call loadManifest(manifestPath) * templateName: key inside manifest (e.g., "speech") * * Returns an object: * { * html: '...', // body innerHTML of template * meta: { html: '/templates/speech.html', css: '/templates/speech.css' }, * baseUrl: 'https://yourhost/templates/' // used to resolve relative hrefs in template * } */ export async function loadTemplateByName(manifestPath, templateName) { const manifestObj = await loadManifest(manifestPath); const manifest = manifestObj.data; const manifestBase = manifestObj.base; const meta = manifest[templateName]; if (!meta) throw new Error(`模板未注册: ${templateName} (manifest: ${manifestPath})`); // determine absolute paths for template html and template css (if provided) const htmlPath = resolveUrl(meta.html, manifestBase); const cssPath = meta.css ? resolveUrl(meta.css, manifestBase) : null; // cacheKey should include both htmlPath and cssPath const cacheKey = `${htmlPath}::${cssPath || ""}`; if (templateCache.has(cacheKey)) return templateCache.get(cacheKey); // fetch template HTML const res = await fetch(htmlPath); if (!res.ok) throw new Error(`无法加载模板 HTML: ${htmlPath} (${res.status})`); const rawHtml = await res.text(); // inject manifest-declared css (meta.css) into head if present (and not already injected) if (cssPath) { injectCssIfNeeded(cssPath); } // parse template HTML to capture any declared inside template, // and inject those into head too (resolving relative hrefs against htmlPath) const parser = new DOMParser(); const doc = parser.parseFromString(rawHtml, "text/html"); const linkEls = Array.from(doc.querySelectorAll('link[rel="stylesheet"]')); linkEls.forEach((linkEl) => { const href = linkEl.getAttribute("href"); if (!href) return; const resolved = resolveUrl(href, htmlPath); injectCssIfNeeded(resolved); }); // optionally: you might want to extract and move inline