178 lines
5.7 KiB
TypeScript
178 lines
5.7 KiB
TypeScript
/* 文件: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);
|
||
}
|