/** * 文本提取 - 标题、价格、参数表、描述 * 从 docs/extension/plan.md §6.6 移植(简化版) */ import type { SiteProfile, TextRule } from '../profiles/types'; export interface TextMaterial { kind: TextRule['kind']; content: string; pairs?: Array<{ key: string; value: string }>; // table 模式的结构化结果 } function clean(s: string): string { return s.replace(/\s+/g, ' ').trim(); } function extractOne(rule: TextRule): TextMaterial | null { for (const sel of rule.selectors) { let nodes: NodeListOf; try { nodes = document.querySelectorAll(sel); } catch { continue; } if (!nodes.length) continue; // table 模式:参数表 if (rule.extract === 'table') { const pairs: Array<{ key: string; value: string }> = []; nodes.forEach(row => { const k = clean(row.querySelector(rule.tableKeySelector ?? '')?.textContent ?? ''); const v = clean(row.querySelector(rule.tableValueSelector ?? '')?.textContent ?? ''); if (k && v) pairs.push({ key: k.replace(/[::]$/, ''), value: v }); }); if (pairs.length) { return { kind: rule.kind, content: pairs.map(p => `${p.key}: ${p.value}`).join('\n'), pairs }; } continue; } // join 模式:标题被拆成多个 span if (rule.extract === 'join') { let text = ''; nodes.forEach(n => { text += n.textContent ?? ''; }); text = clean(text); if (text) return { kind: rule.kind, content: text }; continue; } // first 模式:只取第一个 const first = clean(nodes[0].textContent ?? ''); if (first) return { kind: rule.kind, content: first }; } return null; } export function collectTexts(profile: SiteProfile): { materials: TextMaterial[]; missingRequired: string[]; } { const materials: TextMaterial[] = []; const missingRequired: string[] = []; for (const rule of profile.textRules) { const m = extractOne(rule); if (m) materials.push(m); else if (rule.required) missingRequired.push(rule.kind); } return { materials, missingRequired }; }