Commit a0a7c5a2 by Lin Wang

feat: handle the page content data

parent 5e9d50ba
No preview for this file type
......@@ -10,5 +10,11 @@
},
"dependencies": {
"jsdom": "^26.1.0"
},
"scripts": {
"extract": "DETECTION_MODE=extract bun run src/detect_section_selector_masters/index.ts",
"detect": "DETECTION_MODE=detect bun run src/detect_section_selector_masters/index.ts",
"fix": "DETECTION_MODE=fix bun run src/detect_section_selector_masters/index.ts",
"nav": "DETECTION_MODE=extractNav bun run src/detect_section_selector_masters/index.ts"
}
}
\ No newline at end of file
// spec: https://strikingly.atlassian.net/wiki/spaces/SPEC/pages/3893985371/L5+-+fix+the+custom+color+in+master+section+template
// @ts-nocheck
import { writeFileSync, readFileSync } from "fs";
import {
hasBackgroundOrColor,
removeColorInStyle,
cleanRichText,
hasInlineColor,
extractInlineColors,
getItemRichTextColors,
extractCssVarIndex,
addColorToHtmlString,
getBgValuePath,
getByPath,
getNavigationInfo,
} from "./helper";
type CommandMode = 'detect' | 'extract' | 'fix' | 'extractNav';
const commandMode = (process.env.DETECTION_MODE as CommandMode) || 'extract';
const EXTRACT = commandMode === 'extract';
const DETECT_ONLY = commandMode === 'detect';
// 全量检测逻辑分离
function handleFullDiffNode(node: any, path: string[], output: any) {
// 检查当前节点是否为 RichText
if (node.type === "RichText") {
const getInlineColors = extractInlineColors(node.value);
output.result.push({
type: 'RichText',
path: path.join('.'),
inlineColors: getInlineColors,
id: node.id || null
});
}
// 检查当前节点是否为 Button
if (node.type === "Button") {
output.result.push({
type: 'Button',
path: path.join('.'),
color: node.color,
backgroundSettings: node.backgroundSettings || {},
id: node.id || null
});
}
if (node.type === "SlideSettings" && node.layout_config?.hasOwnProperty("card")) {
output.result.push({
type: 'SlideSettings.layout_config.card',
path: path.join('.'),
card: node.layout_config.card,
card_color: node.layout_config.card_color,
border: node.layout_config.border,
border_color: node.layout_config.border_color,
cardBackgroundSettings: node.layout_config.cardBackgroundSettings || {},
borderBackgroundSettings: node.layout_config.borderBackgroundSettings || {},
id: node.id || null,
})
}
// 检查 section 是否设置了 bg color
if (node.type === "Background") {
const useImage = node.useImage
const useVideo = node.videoHtml
if (useImage) {
output.result.push({
type: "Background.textColor",
textColor: node.textColor || '',
path: path.join("."),
id: node.id || null,
note: "The background is image",
})
} else if (useVideo) {
output.result.push({
type: "Background.videoHtml",
path: path.join("."),
id: node.id || null,
note: "The background is video",
})
} else {
output.result.push({
type: "BackgroundColor",
backgroundColor: node.backgroundColor?.value || '',
path: path.join("."),
id: node.id || null,
note: "The background is background color",
})
}
}
}
// 部分检测与修复逻辑分离
function handlePartialDiffAndFix(node: any, path: string[], output: any, { sectionData, themePreColors } = { sectionData: any, themePreColors: any }) {
// new_grid 逻辑
if (node.template_name === 'new_grid') {
const list = node.components?.repeatable1?.list;
if (Array.isArray(list)) {
list.forEach((item) => {
if (!item) {
return;
}
const colors = getItemRichTextColors(item); // e.g. ['#222222','var(--s-pre-color10)']
// 分离出纯 hex 的值和变量索引
const hexColors = colors.filter((c) => /^#/.test(c.toLowerCase()));
const varIndices = colors
.map((c) => extractCssVarIndex(c))
.filter((i): i is number => i !== null);
const varColors = varIndices.map((idx) => {
const themeColor = themePreColors.find((tc) => tc.key === idx);
return themeColor?.value || "";
});
// 合并 hex 和变量颜色
hexColors.push(...varColors);
// 去重
const uniqueHexColors = Array.from(new Set(hexColors));
const hasDarkHex = uniqueHexColors.some((h) =>
["#000", "#000000"].includes(h.toLowerCase())
);
const hasWhiteHex = uniqueHexColors.some((h) =>
["#fff", "#ffffff"].includes(h.toLowerCase())
);
const hasImage = item?.components?.background1?.useImage;
const bg = item?.components?.background1;
// case ⑦ fix black 'custom color' text on image grid
if (
hasImage &&
(bg?.textColor === "" ||
bg?.textColor === "light" ||
bg?.textColor === "dark") &&
hasDarkHex
) {
if (DETECT_ONLY) {
output.result.push({
type: "RepeatableItem.components.background1.textColor",
path: path.join("."),
id: item.id || null,
note: "The text color is set to black when has image and the image overlay is not set to 'Dark text'.",
whichCase: "case ⑦ fix black 'custom color' text on image grid",
});
} else {
bg.textColor = "dark";
// remove inline color from any RichText under this item
cleanRichText(item);
}
}
// case ⑧ fix closed card border
if (
hasImage &&
(bg?.textColor === "light" || bg?.textColor === "overlay") &&
hasWhiteHex
) {
if (DETECT_ONLY) {
output.result.push({
type: "RepeatableItem.components.background1.textColor",
path: path.join("."),
id: item.id || null,
note: "The text color is set to white when has image and the image overlay set to 'Light text' OR 'Overlay + Light Text'",
whichCase: "case ⑧ fix closed card border",
});
} else {
// remove inline color from any RichText under this item
cleanRichText(item);
}
}
// case ⑩ fix ‘use default’ text on fixed grid cell bg color
if (
!hasImage &&
bg.backgroundColor?.value?.toLowerCase() === "#e8eaec"
) {
Object.values(item?.components ?? {}).forEach((comp: any) => {
if (comp.type === "RichText" && typeof comp.value === "string") {
comp.value = addColorToHtmlString(comp.value);
}
});
}
});
}
}
// feature list 逻辑
if (
node.type === "SlideSettings" &&
node.layout_config?.hasOwnProperty("card")
) {
// case ① fix closed card bg
if (
node.layout_config.card === false &&
node.layout_config.cardBackgroundSettings?.type !== "default"
) {
if (DETECT_ONLY) {
output.result.push({
type: "SlideSettings.layout_config.cardBackgroundSettings.type",
path: path.join("."),
id: node.id || null,
note: "The card bg is not set to use default when card is closed.",
whichCase: "case ① fix closed card bg",
});
} else {
node.layout_config.cardBackgroundSettings =
node.layout_config.cardBackgroundSettings || {};
node.layout_config.cardBackgroundSettings.type = "default";
}
}
// case ② fix closed card border
if (
node.layout_config.border === false &&
node.layout_config.borderBackgroundSettings?.type !== "default"
) {
if (DETECT_ONLY) {
output.result.push({
type: "SlideSettings.layout_config.borderBackgroundSettings.type",
path: path.join("."),
id: node.id || null,
note: "The card border is not set to use default when border is closed.",
whichCase: "case ② fix closed card border",
});
} else {
node.layout_config.borderBackgroundSettings =
node.layout_config.borderBackgroundSettings || {};
node.layout_config.borderBackgroundSettings.type = "default";
}
}
// case ③ keep the ‘custom color’ text on fixed grid cell bg color
// Don't do anything, just keep the default
// case ④ fix opened card bg white
if (
node.layout_config.card === true &&
node.layout_config.card_color === "#ffffff" &&
node.layout_config.cardBackgroundSettings?.type !== "custom"
) {
if (DETECT_ONLY) {
output.result.push({
type: "SlideSettings.layout_config.cardBackgroundSettings.type",
path: path.join("."),
id: node.id || null,
note: "The card bg is not set to custom when card is opened and the card_color is set #ffffff .",
whichCase: "case ④ fix opened card bg white",
});
} else {
node.layout_config.cardBackgroundSettings =
node.layout_config.cardBackgroundSettings || {};
node.layout_config.cardBackgroundSettings.type = "custom";
}
}
// case ⑤ fix opened card border gray
if (
node.layout_config.border === true &&
node.layout_config.card_color === "#ffffff" &&
node.layout_config.border_color === "#cccccc" &&
node.layout_config.borderBackgroundSettings?.type !== "custom"
) {
if (DETECT_ONLY) {
output.result.push({
type: "SlideSettings.layout_config.borderBackgroundSettings.type",
path: path.join("."),
id: node.id || null,
note: "The border color is not set to custom when border is opened and the card_color is set #ffffff and border_color is set #cccccc.",
whichCase: "case ⑤ fix opened card border gray",
});
} else {
node.layout_config.borderBackgroundSettings =
node.layout_config.borderBackgroundSettings || {};
node.layout_config.borderBackgroundSettings.type = "custom";
}
}
// case ⑥ fix ‘custom color’ text on ‘use default' card bg
if (
node.layout_config.card === true &&
(node.layout_config.cardBackgroundSettings?.type === "default" ||
node.layout_config.cardBackgroundSettings?.default?.toLocaleLowerCase() ===
node.layout_config.card_color?.toLocaleLowerCase())
) {
const rpt = sectionData?.components?.repeatable1;
if (rpt?.list) {
rpt.list.forEach((item) => {
if (DETECT_ONLY) {
// only detect if there is any inline color in this item’s RichText
const colors = getItemRichTextColors(item);
if (colors.length > 0) {
output.result.push({
type: "RepeatableItem.components.RichText",
path: path.join("."),
id: item.id || null,
note: "Remove custom-color text on default card background",
whichCase:
"case ⑥ fix ‘custom color’ text on ‘use default' card bg color",
});
}
} else {
cleanRichText(item);
}
});
}
}
// case ⑨ fix the wrong data of 'use default' card color
if (
node.layout_config.card === true &&
!node.layout_config.card_color &&
node.layout_config.cardBackgroundSettings.type !== "default"
) {
if (DETECT_ONLY) {
output.result.push({
type: "SlideSettings.layout_config.cardBackgroundSettings.type",
path: path.join("."),
id: node.id || null,
note: "The card color is not set to use default when card is opened and the card_color is not set.",
whichCase: "case ⑨ fix the wrong data of 'use default' card color",
});
} else {
node.layout_config.cardBackgroundSettings =
node.layout_config.cardBackgroundSettings || {};
node.layout_config.cardBackgroundSettings.type = "default";
}
}
// case ⑨ fix the wrong data of 'use default' card color
if (
node.layout_config.card === true &&
node.layout_config.card_color?.toLowerCase() ===
node.layout_config.cardBackgroundSettings?.default?.toLowerCase() &&
node.layout_config.cardBackgroundSettings.type !== "default"
) {
if (DETECT_ONLY) {
output.result.push({
type: "SlideSettings.layout_config.cardBackgroundSettings.type",
path: path.join("."),
id: node.id || null,
note: "The card color is not set to use default when card is opened and the card_color is set to the default value.",
whichCase: "case ⑨ fix the wrong data of 'use default' card color",
});
} else {
node.layout_config.cardBackgroundSettings =
node.layout_config.cardBackgroundSettings || {};
node.layout_config.cardBackgroundSettings.type = "default";
}
}
// case ⑨ fix the wrong data of 'use default' card color
if (
node.layout_config.border === true &&
!node.layout_config.border_color &&
node.layout_config.borderBackgroundSettings.type !== "default"
) {
if (DETECT_ONLY) {
output.result.push({
type: "SlideSettings.layout_config.borderBackgroundSettings.type",
path: path.join("."),
id: node.id || null,
note: "The border color is not set to use default when border is opened and the border_color is not set.",
whichCase: "case ⑨ fix the wrong data of 'use default' card color",
});
} else {
node.layout_config.borderBackgroundSettings =
node.layout_config.borderBackgroundSettings || {};
node.layout_config.borderBackgroundSettings.type = "default";
}
}
// case ⑨ fix the wrong data of 'use default' card color
if (
node.layout_config.border === true &&
node.layout_config.border_color?.toLowerCase() ===
node.layout_config.borderBackgroundSettings?.default?.toLowerCase() &&
node.layout_config.borderBackgroundSettings.type !== "default"
) {
if (DETECT_ONLY) {
output.result.push({
type: "SlideSettings.layout_config.borderBackgroundSettings.type",
path: path.join("."),
id: node.id || null,
note: "The border color is not set to use default when border is opened and the border_color is set to the default value.",
whichCase: "case ⑨ fix the wrong data of 'use default' card color",
});
} else {
node.layout_config.borderBackgroundSettings =
node.layout_config.borderBackgroundSettings || {};
node.layout_config.borderBackgroundSettings.type = "default";
}
}
}
// 检查当前节点是否为 RichText
if (node.type === "RichText" && typeof node.value === "string" && hasBackgroundOrColor(node.value)) {
let skip = false;
if (sectionData.template_name === "new_grid") {
const patchPath = path.join(".");
const bgPath = getBgValuePath(patchPath);
if (bgPath) {
const bgValue = getByPath(sectionData, bgPath)?.toLowerCase() || "";
const ignoreList = ["#ffffff", "#e8eaec", "#1c1c1c"];
skip = ignoreList.includes(bgValue);
}
}
if (!skip) {
output.result.push({
type: "RichText",
path: path.join("."),
id: node.id || null,
note: "This RichText contains inline styles with background or color.",
});
}
}
// 检查当前节点是否为 Button
if (node.type === "Button") {
if ((node.color || (!node.color && node.hasOwnProperty("backgroundSettings"))) && !node.backgroundSettings?.type) {
output.result.push({
type: "Button",
path: path.join("."),
id: node.id || null,
note: "The Button doesn't selected any option, like 'Use Default'、'Custom Color' or 'preset color'.",
});
}
}
// 检查 section 是否设置了 bg color
if (node.type === "Background") {
const isUnderRepeatable1 = path.some(seg => seg.includes("components.repeatable1"))
const useImage = node.useImage
const useVideo = node.videoHtml
if (!isUnderRepeatable1) {
if (!useImage && !useVideo && node.backgroundColor?.value) {
output.result.push({
type: "BackgroundColor",
path: path.join("."),
id: node.id || null,
note: `The background color is set to ${node.backgroundColor.value}.`,
})
}
}
if (useImage && !node.textColor) {
output.result.push({
type: "Background.textColor",
path: path.join("."),
id: node.id || null,
note: "The background has image but textColor is not set.",
})
}
}
}
function parseJsonForStyles(
themePreColors,
sectionData,
sectionIdx
) {
const output = { sectionIndex: sectionIdx, result: [] };
function traverse(node: any, path: string[] = []) {
if (!node) return;
if (EXTRACT) {
handleFullDiffNode(node, path, output);
} else {
handlePartialDiffAndFix(node, path, output, { themePreColors, sectionData });
}
// 遍历子节点
if (node.components) {
// Only traverse individual Button when no Buttons group is present
let componentKeys = Object.keys(node.components);
if (componentKeys.includes('buttons') && componentKeys.includes('button1')) {
componentKeys = componentKeys.filter(k => k !== 'button1');
}
componentKeys.forEach((key) => {
const child = node.components[key];
traverse(child, [...path, `components.${key}`]);
});
}
// 遍历 items 数组(如果存在)
if (Array.isArray(node.items)) {
node.items.forEach((item, index) => {
traverse(item, [...path, `items[${index}]`]);
});
}
// 遍历 list 数组(如果存在,例如 repeatable1.list)
if (Array.isArray(node.list)) {
node.list.forEach((item, index) => {
traverse(item, [...path, `list[${index}]`]);
});
}
}
// 开始遍历 JSON 数据
traverse(sectionData);
return output;
}
export function mainParse(jsonData) {
const themePreColors = jsonData.customColors?.themePreColors || [];
const result = [];
// 生成导航信息
const navigationInfo = getNavigationInfo(jsonData.navigation.items);
// 遍历 pages 数组
const navLinks = jsonData.navigation.items.reduce((pre, cur) => {
if (cur.items) {
return pre.concat(cur.items);
}
return pre;
}, []);
const pages = navLinks.map((item) => {
return jsonData.pages.find((page) => page.uid === item.id);
});
if (Array.isArray(pages)) {
pages.forEach((page, pageIndex) => {
// 遍历 sections 数组
if (Array.isArray(page.sections)) {
page.sections.forEach((section, sectionIndex) => {
const sectionResult = parseJsonForStyles(
themePreColors,
section,
sectionIndex
);
if (sectionResult.result?.length) {
result.push({
pageIndex,
pageUid: page.uid,
pageTitle: page.title,
sectionIndex: sectionIndex,
result: sectionResult.result,
});
}
});
}
});
}
return { jsonData, result, navigationInfo };
}
\ No newline at end of file
import { JSDOM } from "jsdom";
export const getNavigationInfo = (items, pathArr: string[] = []) => {
let navigationResult = [];
items?.forEach((item, idx) => {
// 构建对象路径
const currentPathArr = [...pathArr, `items[${idx}]`];
const type = item.type;
const itemsCount = item.items ? item.items.length : 0;
navigationResult.push({
patch: currentPathArr.join("."),
type: type,
itemsCount: itemsCount,
});
// 递归处理 dropdown
if (item.type === "dropdown" && item.items) {
navigationResult = navigationResult.concat(
getNavigationInfo(item.items, currentPathArr)
);
}
});
return navigationResult;
}
// 辅助函数:检查 HTML 字符串中所有 style 属性是否包含 background 或 color
export const hasBackgroundOrColor = (htmlString) => {
if (!htmlString || typeof htmlString !== "string") return false;
// 使用正则表达式匹配所有 style 属性
const styleRegex = /style="([^"]*)"/g;
let match;
while ((match = styleRegex.exec(htmlString)) !== null) {
const style = match[1];
if (style.includes("background") || style.includes("color")) {
return true;
}
}
return false;
}
// 新增:移除 style 中 color 声明的函数
export const removeColorInStyle = (htmlString: string) => {
// 把 style="…color:XXX;…" 中的 color:XXX; 删除
return htmlString.replace(/(style="[^"]*?)\s*color:[^;"]*;?/g, "$1");
}
// 新增:递归清洗节点下所有 RichText
export const cleanRichText = (node: any) => {
if (!node) return;
if (node.type === "RichText" && typeof node.value === "string") {
node.value = removeColorInStyle(node.value);
}
if (node.components) {
Object.values(node.components).forEach(cleanRichText);
}
if (Array.isArray(node.items)) {
node.items.forEach(cleanRichText);
}
if (Array.isArray(node.list)) {
node.list.forEach(cleanRichText);
}
}
// 新增:递归检查节点下所有 RichText 是否有 inline color
export const hasInlineColor = (node: any): boolean => {
if (!node) return false;
if (node.type === "RichText" && typeof node.value === "string") {
return extractInlineColors(node.value).length > 0;
}
if (node.components) {
return Object.values(node.components).some(hasInlineColor);
}
if (Array.isArray(node.items)) {
return node.items.some(hasInlineColor);
}
if (Array.isArray(node.list)) {
return node.list.some(hasInlineColor);
}
return false;
}
/**
* 从一个 HTML 字符串中提取所有 inline color 值
*/
export const extractInlineColors = (html: string): string[] => {
const reg = /color\s*:\s*([^;"]+)/gi;
const result: string[] = [];
let m: RegExpExecArray | null;
while ((m = reg.exec(html))) {
result.push(m[1].trim());
}
return result;
}
/**
* 收集一个 RepeatableItem.components 下所有 RichText 的 inline color
*/
export const getItemRichTextColors = (item: any): string[] => {
if (!item || !item.components) return [];
const out: string[] = [];
Object.values(item?.components ?? {}).forEach((comp: any) => {
if (comp.type === "RichText" && typeof comp.value === "string") {
out.push(...extractInlineColors(comp.value));
}
});
return out;
}
/**
* 如果 color 是 CSS 变量 var(--s-pre-color10),提取数字部分 10
*/
export const extractCssVarIndex = (color: string): number | null => {
const m = color?.match(/var\(--s-pre-color(\d+)\)/i);
return m ? parseInt(m[1], 10) : null;
}
export const addColorToHtmlString = (html) => {
/* use JSDOM start */
const dom = new JSDOM(`<body>${html}</body>`);
const { document, NodeFilter } = dom.window;
const walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_TEXT
);
/* use JSDOM end */
// const parser = new DOMParser();
// const document = parser.parseFromString(html, 'text/html');
// const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
const touched = new Set();
// 匹配 color: #50555c(不区分大小写)
const overrideColorRegex = /color\s*:\s*#50555c/gi;
while (walker.nextNode()) {
const textNode = walker.currentNode;
if (!textNode?.nodeValue.trim()) continue;
const parent = textNode.parentElement;
if (!parent || touched.has(parent)) continue;
touched.add(parent);
const oriStyle = parent.getAttribute("style") || "";
// 如果已有 color: #50555C,则替换成 color:rgb(10, 7, 7)
if (overrideColorRegex.test(oriStyle)) {
const newStyle = oriStyle.replace(overrideColorRegex, "color: #222222");
parent.setAttribute("style", newStyle);
continue;
}
// 如果已含 color 则跳过
if (/color\s*:/i.test(oriStyle)) continue;
// 根据 oriStyle 是否为空决定是否加前置分号
let newStyle = oriStyle.trim();
newStyle += "color: #222222;";
parent.setAttribute("style", newStyle);
}
return document.body.innerHTML;
}
export const getBgValuePath = (patchPath: string): string | null => {
const m = patchPath.match(
/^(components\.repeatable1\.list\[\d+\]\.components)\.[^.]+$/
);
if (!m) return null;
return `${m[1]}.background1.backgroundColor.value`;
}
export const getByPath = (obj: any, path: string): any => {
return path.split(".").reduce((acc, key) => {
const arr = key.match(/^(.+)\[(\d+)\]$/);
if (arr) {
const [, prop, idx] = arr;
return acc?.[prop]?.[Number(idx)];
}
return acc?.[key];
}, obj);
}
\ No newline at end of file
import { writeFileSync, mkdirSync, existsSync } from 'fs'
import { fetchSiteData } from '../clients/bobcat/SiteInfo'
import {
section_selectors,
ai_section_selectors
} from '../constant/section_selectors'
import { mainParse } from './handlePageContent'
/*
detect: 设置限制条件,获取指定有问题的字段
extract: 提出整个 page content 指定的字段数据
fix: 修复 page content 数据,修复后获取完整的 page content
extractNav: 获取导航信息,可以查看下拉菜单有多少个
*/
// demo
type CommandMode = 'detect' | 'extract' | 'fix' | 'extractNav';
// await Promise.all(
// section_selectors.map(async (siteId, index) => {
// const jsonData = await fetchSiteData(siteId);
// // jsonData.content is page data under this site
// return jsonData.content;
// })
// )
\ No newline at end of file
const commandMode = (process.env.DETECTION_MODE as CommandMode) || 'extract';
// 确保输出目录存在
let outDir = ''
if (commandMode === 'detect') {
outDir = 'src/detect_section_selector_masters/detectOutput'
} else if (commandMode === 'extract') {
outDir = 'src/detect_section_selector_masters/extractOutput'
} else if (commandMode === 'fix') {
outDir = 'src/detect_section_selector_masters/fixedOutput'
} else if (commandMode === 'extractNav') {
outDir = 'src/detect_section_selector_masters/extractNavOutput'
}
if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true })
await Promise.all(
section_selectors.map(async (siteId, index) => {
const { content } = await fetchSiteData(siteId)
const { jsonData: pageContent, result, navigationInfo } = mainParse(content) || {}
const file = `${outDir}/${siteId}_${commandMode}.json`
let data = []
if (commandMode === 'detect' || commandMode === 'extract') {
data = result
} else if (commandMode === 'fix') {
data = pageContent
} else if (commandMode === 'extractNav') {
data = navigationInfo
}
writeFileSync(file, JSON.stringify(data, null, 2), 'utf-8')
console.log(`✅ ${siteId} output written to ${file}`)
})
)
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment