143 lines
5.8 KiB
TypeScript
143 lines
5.8 KiB
TypeScript
/**
|
||
* # 脚本 导入原站页面级生成 CSS(post-<legacy_id>.css)到 contents.meta.page_css
|
||
*
|
||
* 背景:DB dump 的 meta.elementor 缺少 widget 级 custom_css(如 EAEL infobox 的
|
||
* `.elementor-element-7f1dc73 .eael-infobox .infobox-icon img{width:80px}`),这些规则
|
||
* 只存在于原站 Elementor 生成的页面样式表 post-<post_id>.css 中。
|
||
*
|
||
* 做法:
|
||
* 1. 抓 https://www.dusuniot.com/wp-content/uploads/elementor/css/post-<legacy_id>.css
|
||
* 2. 只保留选择器含 .elementor-element-<id>(id 属于本文档)的规则(含 @media 块内),
|
||
* 把 `.elementor-element.elementor-element-<id>` / `.elementor-element-<id>` 改写为
|
||
* `[data-el="<id>"]`(与渲染器 DOM 对应),剥掉 `.elementor-<pid>` 页面作用域前缀
|
||
* 3. 拼接后写回 meta.page_css,渲染器(ElementorRenderer pageCss prop)注入 <style>
|
||
*
|
||
* 用法:bun scripts/import-page-css.ts <slug> [<slug> ...] (缺省 = 全部方案落地页)
|
||
*/
|
||
import { SQL } from "bun";
|
||
|
||
/** # 常量 方案落地页 slug 全集(缺省导入目标) **/
|
||
const SOLUTION_SLUGS = [
|
||
"smart-apartment-gateway",
|
||
"smart-apartment-gateway-oem",
|
||
"oil-gas-management-solution",
|
||
"energy-storage-ems-solution",
|
||
"smart-robot-controller-solution",
|
||
"vehicle-iot-solution",
|
||
"smart-building-solution",
|
||
"smart-retail-solution",
|
||
"smart-home-solution",
|
||
"smart-apartment-solution",
|
||
"smart-elderly-care-solution",
|
||
"ble-positioning-solution",
|
||
"ble-mesh-lighting-control-solution",
|
||
"home-assistant-grow-lighting-solution",
|
||
];
|
||
|
||
/** # 常量 原站页面 CSS 地址模板 **/
|
||
const CSS_URL = (pid: number) => `https://www.dusuniot.com/wp-content/uploads/elementor/css/post-${pid}.css`;
|
||
|
||
/** # 函数 提取文档全部元素 id 集合(含嵌套;顶层为数组或 {elements} 包装均可) **/
|
||
function collectIds(el: unknown, out: Set<string> = new Set()): Set<string> {
|
||
if (Array.isArray(el)) {
|
||
for (const x of el) collectIds(x, out);
|
||
return out;
|
||
}
|
||
if (!el || typeof el !== "object") return out;
|
||
const e = el as { id?: unknown; elements?: unknown[] };
|
||
if (typeof e.id === "string") out.add(e.id);
|
||
for (const c of Array.isArray(e.elements) ? e.elements : []) collectIds(c, out);
|
||
return out;
|
||
}
|
||
|
||
/** # 函数 把选择器中的 elementor-element-<id> 改写为 data-el 属性选择器
|
||
* 规则保留条件:含文档 id,或引用 .ele-<自定类>(页面自定义类,渲染器把 _css_classes
|
||
* 落到节点 class 上,如 .ele-sol-review-box / .ele-secondDep-dif);其余丢弃
|
||
*/
|
||
function translateSelector(sel: string, ids: Set<string>, pid: number): string | null {
|
||
const idIn = [...sel.matchAll(/elementor-element-(\w+)/g)].map((m) => m[1]);
|
||
const hasDocId = idIn.some((id) => ids.has(id));
|
||
const hasCustomCls = /\.ele-[a-z][\w-]*/i.test(sel) && !/elementor-element-/.test(sel);
|
||
if (!hasDocId && !hasCustomCls) return null;
|
||
let out = sel;
|
||
for (const id of new Set(idIn)) {
|
||
if (!ids.has(id)) continue;
|
||
out = out.split(`.elementor-element.elementor-element-${id}`).join(`[data-el="${id}"]`);
|
||
out = out.split(`.elementor-element-${id}`).join(`[data-el="${id}"]`);
|
||
}
|
||
// 剥页面作用域前缀(.elementor-<pid> 空格相连)与多余空白
|
||
out = out.replace(new RegExp(`\\.elementor-${pid}\\s*`, "g"), "");
|
||
return out.replace(/\s+/g, " ").trim();
|
||
}
|
||
|
||
/** # 函数 解析 CSS 顶层规则(含 @media 嵌套块),返回过滤改写后的规则串
|
||
* 用花括号配对扫描,避免依赖完整 CSS 解析器
|
||
*/
|
||
function filterCss(css: string, ids: Set<string>, pid: number): string {
|
||
let out = "";
|
||
let i = 0;
|
||
const n = css.length;
|
||
while (i < n) {
|
||
// 跳过注释
|
||
if (css.startsWith("/*", i)) {
|
||
const end = css.indexOf("*/", i + 2);
|
||
i = end === -1 ? n : end + 2;
|
||
continue;
|
||
}
|
||
const brace = css.indexOf("{", i);
|
||
if (brace === -1) break;
|
||
const sel = css.slice(i, brace).trim();
|
||
// 找配对右括号
|
||
let depth = 1;
|
||
let j = brace + 1;
|
||
while (j < n && depth > 0) {
|
||
if (css[j] === "{") depth++;
|
||
else if (css[j] === "}") depth--;
|
||
j++;
|
||
}
|
||
const body = css.slice(brace + 1, j - 1);
|
||
if (sel.startsWith("@media") || sel.startsWith("@supports")) {
|
||
const inner = filterCss(body, ids, pid);
|
||
if (inner) out += `${sel}{${inner}}\n`;
|
||
} else if (sel.startsWith("@keyframes") || sel.startsWith("@font-face")) {
|
||
// 全局块整体保留(字体/动画与节点无关)
|
||
out += `${sel}{${body}}\n`;
|
||
} else {
|
||
const parts = sel.split(",").map((p) => translateSelector(p, ids, pid)).filter((p): p is string => !!p);
|
||
if (parts.length) out += `${parts.join(",")}{${body}}\n`;
|
||
}
|
||
i = j;
|
||
}
|
||
return out;
|
||
}
|
||
|
||
const slugs = process.argv.slice(2).length ? process.argv.slice(2) : SOLUTION_SLUGS;
|
||
const sql = new SQL("postgres://dusuniot:dusuniot@127.0.0.1:25432/dusuniot", { max: 1 });
|
||
for (const slug of slugs) {
|
||
const rows = await sql`select id, legacy_id, meta::text as meta from contents where slug = ${slug} and type = ${"post"} limit 1`;
|
||
const row = rows[0];
|
||
if (!row) {
|
||
console.log("SKIP(无内容):", slug);
|
||
continue;
|
||
}
|
||
const meta = JSON.parse(row.meta);
|
||
const ids = collectIds(meta.elementor);
|
||
const pid = row.legacy_id;
|
||
if (!pid) {
|
||
console.log("SKIP(无 legacy_id):", slug);
|
||
continue;
|
||
}
|
||
const res = await fetch(CSS_URL(pid), { headers: { "User-Agent": "Mozilla/5.0" } });
|
||
if (!res.ok) {
|
||
console.log("SKIP(css", res.status, "):", slug, CSS_URL(pid));
|
||
continue;
|
||
}
|
||
const raw = await res.text();
|
||
const pageCss = filterCss(raw, ids, pid);
|
||
meta.page_css = pageCss;
|
||
// ::text::jsonb 双重转换:Bun SQL 绑定字符串直接 ::jsonb 会把整串存成 JSON 字符串值
|
||
await sql`update contents set meta = ${JSON.stringify(meta)}::text::jsonb where id = ${row.id}`;
|
||
console.log("OK:", slug, "post", pid, "css", raw.length, "->", pageCss.length);
|
||
}
|
||
await sql.end();
|