Files
dinner.tootaio.com/20250916/ppt/js/loader.js
2025-09-15 00:28:27 +08:00

202 lines
6.7 KiB
JavaScript
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.
// 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: '<div...>...</div>', // 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 <link rel="stylesheet"> 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 <style> out,
// but keeping inline <style> inside template HTML is acceptable.
const bodyHtml = doc.body.innerHTML.trim();
const tpl = {
html: bodyHtml,
meta: { ...meta, html: htmlPath, css: cssPath },
baseUrl: new URL(".", htmlPath).toString(),
};
templateCache.set(cacheKey, tpl);
return tpl;
}
/**
* Helper: inject a stylesheet link element into document.head if not already present.
* Performs de-dup by checking existing link[href="..."] after resolving href.
*/
function injectCssIfNeeded(href) {
const absHref = href; // already resolved by resolveUrl
// dedupe: check both full absolute href and possibly same-origin relative forms by comparing full URL
const existing = Array.from(
document.querySelectorAll('link[rel="stylesheet"]')
).some((l) => {
// some browsers return absolute hrefs, so compare URL()
try {
return (
new URL(l.href, location.href).toString() ===
new URL(absHref, location.href).toString()
);
} catch (e) {
return l.getAttribute("href") === absHref;
}
});
if (!existing) {
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = absHref;
document.head.appendChild(link);
}
}
/**
* Render wrapper — 调用 engine.renderTemplate
* tplHtml: string (template HTML body)
* data: object
* opts: { allowRaw: boolean }
*/
export function render(tplHtml, data = {}, opts = { allowRaw: true }) {
return renderTemplate(tplHtml, data, opts);
}
/**
* Load override CSS for an instance (per-slide override), with dedupe.
* href can be absolute or relative; if relative, resolve against current location.
*/
export function loadOverrideCss(href) {
if (!href) return null;
const resolved = resolveUrl(href, location.href);
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = resolved;
link.disabled = true; // 默认禁用
document.head.appendChild(link);
return link;
}
/**
* Optional utility: clear caches (for dev hot-reload)
*/
export function clearCaches() {
manifestCache.clear();
templateCache.clear();
}
/**
* Optional utility: prefetch templates listed in manifest (improves UX)
* manifestPath: path to manifest
* templateNames: array of template names to prefetch; if omitted, prefetch all in manifest.
*/
export async function prefetchTemplates(
manifestPath = "/templates/manifest.json",
templateNames = null
) {
const manifestObj = await loadManifest(manifestPath);
const manifest = manifestObj.data;
const names = templateNames || Object.keys(manifest);
await Promise.all(
names.map((name) => {
const meta = manifest[name];
if (!meta) return Promise.resolve();
const htmlPath = resolveUrl(meta.html, manifestObj.base);
const cssPath = meta.css ? resolveUrl(meta.css, manifestObj.base) : null;
// warm up by fetching html (and cache behavior in loadTemplateByName will handle injection)
return fetch(htmlPath)
.then((r) =>
r.ok
? r.text().then(() => {
if (cssPath) injectCssIfNeeded(cssPath);
})
: Promise.resolve()
)
.catch(() => {});
})
);
}