// 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