97 lines
3.9 KiB
TypeScript
97 lines
3.9 KiB
TypeScript
/**
|
||
* # 模块 Elementor 图片 WP 尺寸变体解析(仅服务端)
|
||
* 原站 image widget 的 image_size=medium/thumbnail/... 时 WP 渲染 -300x272 这类裁剪/缩放
|
||
* 变体文件;dump 的 image.url 只存原图。这里在服务端按 uploads 目录里实际存在的
|
||
* `<base>-<w>x<h>.<ext>` 变体择优改写 image.url(选宽度 ≤ 目标里最大的;无变体保持原图)。
|
||
*/
|
||
import { readdirSync } from "node:fs";
|
||
import { join, dirname } from "node:path";
|
||
|
||
/** # 常量 WP 尺寸名 → 目标宽度(Elementor 常用档位) **/
|
||
const SIZE_TARGET: Record<string, number> = {
|
||
thumbnail: 150,
|
||
medium: 300,
|
||
medium_large: 768,
|
||
large: 1024,
|
||
};
|
||
|
||
/** # 成员变量 目录列表缓存(同一目录只扫一次盘) **/
|
||
const dirCache = new Map<string, string[]>();
|
||
|
||
/** # 成员变量 变体解析结果缓存(url+size → 最终 url) **/
|
||
const resultCache = new Map<string, string>();
|
||
|
||
/** # 函数 对外:媒体 URL 的 WP 尺寸变体解析(服务端;无变体返回原 URL)
|
||
* 参数 url 原图 URL;targetWidth WP 档位目标宽(150/300/600/768/1024)
|
||
*/
|
||
export function resolveMediaVariantUrl(url: string, targetWidth: number): string {
|
||
if (typeof window !== "undefined" || !url?.includes("/uploads/")) return url;
|
||
return findVariant(url, targetWidth) ?? url;
|
||
}
|
||
|
||
/** # 函数 查找某原图在给定目标宽度下的最佳变体 URL(无则返回 null)
|
||
* 参数 url 原图 URL(本地 /uploads/... 或原站绝对地址);targetWidth WP 档位目标宽
|
||
*/
|
||
function findVariant(url: string, targetWidth: number): string | null {
|
||
// 归一化到本地 /uploads/ 路径(dump 里可能存原站绝对地址)
|
||
const up = url.indexOf("/uploads/");
|
||
if (up === -1) return null;
|
||
const localPath = url.slice(up); // /uploads/2022/06/Group-201.png
|
||
const m = localPath.match(/^(.*\/)([^/]+)\.(\w+)$/);
|
||
if (!m) return null;
|
||
const [, dir, base, ext] = m;
|
||
const fsDir = join("public", dir);
|
||
let files = dirCache.get(fsDir);
|
||
if (!files) {
|
||
try {
|
||
files = readdirSync(fsDir);
|
||
} catch {
|
||
files = [];
|
||
}
|
||
dirCache.set(fsDir, files);
|
||
}
|
||
// 候选:<base>-<w>x<h>.<ext>;取宽度 ≤ 目标里最大的(同宽取更矮的,WP medium 缩放优先于裁剪)
|
||
const prefix = `${base}-`;
|
||
let best: { w: number; h: number; file: string } | null = null;
|
||
for (const f of files) {
|
||
const fm = f.match(new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(\\d+)x(\\d+)\\.${ext}$`));
|
||
if (!fm) continue;
|
||
const w = Number(fm[1]);
|
||
const h = Number(fm[2]);
|
||
if (w > targetWidth) continue;
|
||
if (!best || w > best.w || (w === best.w && h < best.h)) best = { w, h, file: f };
|
||
}
|
||
return best ? `${dir}${best.file}` : null;
|
||
}
|
||
|
||
/** # 函数 就地解析文档内 image widget 的尺寸变体(服务端 loader 调用;浏览器端空操作)
|
||
* 参数 doc elementor 文档(数组或 {elements} 包装),image.url 被改写为变体地址
|
||
*/
|
||
export function resolveImageVariants(doc: unknown): void {
|
||
if (typeof window !== "undefined") return;
|
||
const walk = (el: unknown) => {
|
||
if (Array.isArray(el)) {
|
||
for (const x of el) walk(x);
|
||
return;
|
||
}
|
||
if (!el || typeof el !== "object") return;
|
||
const e = el as { id?: unknown; widgetType?: unknown; settings?: Record<string, unknown>; elements?: unknown[] };
|
||
if (e.widgetType === "image") {
|
||
const st = e.settings ?? {};
|
||
const target = SIZE_TARGET[String(st.image_size ?? "")];
|
||
const img = st.image as { url?: string } | undefined;
|
||
if (target && img?.url && img.url.includes("/uploads/")) {
|
||
const key = `${img.url}|${target}`;
|
||
let resolved = resultCache.get(key);
|
||
if (resolved === undefined) {
|
||
resolved = findVariant(img.url, target) ?? "";
|
||
resultCache.set(key, resolved);
|
||
}
|
||
if (resolved) img.url = resolved;
|
||
}
|
||
}
|
||
for (const c of Array.isArray(e.elements) ? e.elements : []) walk(c);
|
||
};
|
||
walk(doc);
|
||
}
|