Files
note2any/src/xiaohongshu/paginator.ts
2025-10-08 12:53:49 +08:00

178 lines
5.7 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.

/* 文件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<PageInfo[]> {
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);
}