如何基于字符偏移量分割DocumentFragment实现HTML分页
带HTML标签的文本分页实现
需求说明
需要将包含HTML标签的字符串按纯文本字符长度分割为多个合法HTML片段,用于分页场景。每个分页片段必须保证标签完全闭合,不能出现未闭合标签的问题,且分页依据是纯文本的字符数,而非HTML源码的长度。
示例演示
const pageCharacterSize = 10 const testString = 'some <strong>text with HTML</strong> tags' function paginate(string, pageSize) { // 待实现 } const pages = paginate(testString, pageCharacterSize) console.log(pages) // 预期输出: // ['some <strong>text </strong>', '<strong>with HTML</strong> ', 'tags']
思路探索
考虑过使用DocumentFragment或Range API实现,但不清楚如何按字符偏移量精准切片。MDN上有类似功能的演示,但依赖需要坐标参数的caretPositionFromPoint()方法,无法满足需求。
测试用例(基于Vitest)
为明确需求,以下是验证分页逻辑的测试用例:
import { expect, test } from 'vitest' import paginate from './paginate' // 1. 纯文本分页测试 test('纯文本应按指定长度正确拆分', () => { // 单字符组拆分 const testString = 'aa bb cc dd ee'; const expected = ['aa', 'bb', 'cc', 'dd', 'ee'] expect(paginate(testString, 2)).toStrictEqual(expected) // 带空格的字符组拆分 const testString2 = 'a a b b c c'; const expected2 = ['a a', 'b b', 'c c'] expect(paginate(testString2, 3)).toStrictEqual(expected2) // 多字符组拆分 const testString3 = 'aa aa bb bb cc cc'; const expected3 = ['aa aa', 'bb bb', 'cc cc'] expect(paginate(testString3, 5)).toStrictEqual(expected3) // 剩余字符不足一页长度时的拆分 const testString4 = 'aa bb cc'; const expected4 = ['aa', 'bb', 'cc'] expect(paginate(testString4, 4)).toStrictEqual(expected4) // 混合空格的拆分 const testString5 = 'a b c d e f g'; const expected5 = ['a b c', 'd e f', 'g'] expect(paginate(testString5, 5)).toStrictEqual(expected5) // 接近页长的拆分 const testString6 = 'aa bb cc'; const expected6 = ['aa bb', 'cc'] expect(paginate(testString6, 7)).toStrictEqual(expected6) }) // 2. 含HTML标签的文本分页测试 test('含HTML标签的文本拆分时不能遗留未闭合标签', () => { const testString = 'aa <strong>bb</strong> <em>cc dd</em>'; const expected = ['aa', '<strong>bb</strong>', '<em>cc</em>', '<em>dd</em>'] expect(paginate(testString, 3)).toStrictEqual(expected) }) // 3. 跨分页标签处理测试 test('标签跨分页时应自动闭合并在后续页面重新开启', () => { const testString = '<strong>aa bb cc</strong>'; const expected = ['<strong>aa</strong>', '<strong>bb</strong>', '<strong>cc</strong>'] expect(paginate(testString, 2)).toStrictEqual(expected) })
实现方案
以下是基于Range API的分页函数实现,能够准确处理文本字符计数与HTML标签的闭合:
function paginate(htmlString, pageSize) { const pages = []; const tempContainer = document.createElement('div'); tempContainer.innerHTML = htmlString; const range = document.createRange(); let currentCharCount = 0; let openTagStack = []; // 递归遍历DOM节点,处理文本与标签 function traverseNode(node) { if (currentCharCount >= pageSize) return; if (node.nodeType === Node.TEXT_NODE) { const textContent = node.textContent; const remainingChars = pageSize - currentCharCount; if (textContent.length <= remainingChars) { currentCharCount += textContent.length; } else { // 分割文本节点,截取当前页能容纳的部分 const pageText = textContent.slice(0, remainingChars); const remainingText = textContent.slice(remainingChars); const newTextNode = document.createTextNode(remainingText); node.parentNode.insertBefore(newTextNode, node.nextSibling); node.textContent = pageText; // 截取当前页的HTML内容,并补全未闭合标签 range.selectNodeContents(tempContainer); range.setEndAfter(node); const pageHtml = range.cloneContents().innerHTML; const closedPage = pageHtml + openTagStack.reverse().map(tag => `</${tag}>`).join(''); pages.push(closedPage.trim()); // 重置状态,准备下一页 currentCharCount = remainingText.length; const carriedOverTags = [...openTagStack]; openTagStack = []; // 重建容器,携带未闭合的标签开启标记 tempContainer.innerHTML = carriedOverTags.map(tag => `<${tag}>`).join('') + remainingText; // 重新遍历新的容器内容 traverseNode(tempContainer.firstChild); return; } } else if (node.nodeType === Node.ELEMENT_NODE) { const tagName = node.tagName.toLowerCase(); openTagStack.push(tagName); // 遍历子节点 for (let i = 0; i < node.childNodes.length; i++) { traverseNode(node.childNodes[i]); if (currentCharCount >= pageSize) break; } // 标签闭合,从栈中移除 openTagStack.pop(); } } // 开始遍历初始内容 traverseNode(tempContainer.firstChild); // 处理最后一页剩余内容 if (tempContainer.innerHTML.trim()) { pages.push(tempContainer.innerHTML.trim()); } return pages; }
实现原理
- Range API 精准截取:利用Range选择DOM范围,确保截取的内容包含完整的标签结构,避免标签断裂。
- 标签栈跟踪:通过
openTagStack记录当前未闭合的标签,分页时自动补全闭合标签,并在下一页开头重新开启这些标签。 - 文本节点分割:当单个文本节点长度超过剩余分页长度时,分割文本节点,保证分页的字符数准确。
- 递归DOM遍历:遍历整个DOM树,处理文本节点和元素节点,确保所有内容都被正确分页。
内容的提问来源于stack exchange,提问作者emersonthis
相关产品推荐
相关产品推荐

