/* 文件:xiaohongshu/paginator.ts — 小红书内容分页器:按切图比例自动分页,确保表格和图片不跨页。 */ import { NMPSettings } from '../settings'; /** * 分页结果 */ export interface PageInfo { index: number; // 页码(从 0 开始) content: string; // 该页的 HTML 内容 height: number; // 该页内容的实际高度(用于调试) } /** * 解析横竖比例字符串为数值 */ function parseAspectRatio(ratio: string): { width: number; height: number } { const parts = ratio.split(':').map(p => parseFloat(p.trim())); if (parts.length === 2 && parts[0] > 0 && parts[1] > 0) { return { width: parts[0], height: parts[1] }; } return { width: 3, height: 4 }; } /** * 计算目标页面高度 */ function getTargetPageHeight(settings: NMPSettings): number { const ratio = parseAspectRatio(settings.sliceImageAspectRatio); return Math.round((settings.sliceImageWidth * ratio.height) / ratio.width); } /** * 判断元素是否为不可分割元素(表格、图片、代码块等) */ function isIndivisibleElement(element: Element): boolean { const tagName = element.tagName.toLowerCase(); // 表格、图片、代码块、公式等不应跨页 return ['table', 'img', 'pre', 'figure', 'svg'].includes(tagName) || element.classList.contains('math-block') || element.classList.contains('mermaid') || element.classList.contains('excalidraw'); } /** * 将 HTML 内容分页 * @param articleElement 文章预览的 DOM 元素 * @param settings 插件设置 * @returns 分页结果数组 */ export async function paginateArticle( articleElement: HTMLElement, settings: NMPSettings ): Promise { const pageHeight = getTargetPageHeight(settings); const pageWidth = settings.sliceImageWidth; // 创建临时容器用于测量 const measureContainer = document.createElement('div'); measureContainer.style.cssText = ` position: absolute; left: -9999px; top: 0; width: ${pageWidth}px; visibility: hidden; `; document.body.appendChild(measureContainer); const pages: PageInfo[] = []; let currentPageContent: Element[] = []; let currentPageHeight = 0; let pageIndex = 0; // 克隆文章内容以避免修改原始 DOM const clonedArticle = articleElement.cloneNode(true) as HTMLElement; const children = Array.from(clonedArticle.children); for (const child of children) { const childClone = child.cloneNode(true) as HTMLElement; measureContainer.innerHTML = ''; measureContainer.appendChild(childClone); // 等待浏览器完成渲染 await new Promise(resolve => setTimeout(resolve, 10)); const childHeight = childClone.offsetHeight; const isIndivisible = isIndivisibleElement(child); // 判断是否需要换页 if (currentPageHeight + childHeight > pageHeight && currentPageContent.length > 0) { // 如果是不可分割元素且加入后会超出,先保存当前页 if (isIndivisible) { pages.push({ index: pageIndex++, content: wrapPageContent(currentPageContent), height: currentPageHeight }); currentPageContent = [child]; currentPageHeight = childHeight; } else { // 可分割元素(段落等),尝试加入当前页 if (currentPageHeight + childHeight <= pageHeight * 1.1) { // 允许 10% 的溢出容差 currentPageContent.push(child); currentPageHeight += childHeight; } else { // 超出太多,换页 pages.push({ index: pageIndex++, content: wrapPageContent(currentPageContent), height: currentPageHeight }); currentPageContent = [child]; currentPageHeight = childHeight; } } } else { // 加入当前页 currentPageContent.push(child); currentPageHeight += childHeight; } } // 保存最后一页 if (currentPageContent.length > 0) { pages.push({ index: pageIndex, content: wrapPageContent(currentPageContent), height: currentPageHeight }); } // 清理临时容器 document.body.removeChild(measureContainer); return pages; } /** * 包装页面内容为完整的 HTML */ function wrapPageContent(elements: Element[]): string { const wrapper = document.createElement('div'); wrapper.className = 'xhs-page-content'; elements.forEach(el => { wrapper.appendChild(el.cloneNode(true)); }); return wrapper.innerHTML; } /** * 渲染单个页面到容器 */ export function renderPage( container: HTMLElement, pageContent: string, settings: NMPSettings ): void { const pageHeight = getTargetPageHeight(settings); const pageWidth = settings.sliceImageWidth; container.innerHTML = ''; container.style.cssText = ` width: ${pageWidth}px; min-height: ${pageHeight}px; max-height: ${pageHeight}px; overflow: hidden; box-sizing: border-box; padding: 40px; background: white; `; const contentDiv = document.createElement('div'); contentDiv.className = 'xhs-page-content'; contentDiv.innerHTML = pageContent; container.appendChild(contentDiv); }