Files
dusuniot/scripts/import-page-css.ts
T

143 lines
5.8 KiB
TypeScript
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.
/**
* # 脚本 导入原站页面级生成 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();