JS如何按字符索引正确高亮文本 解决标签插入致索引偏移问题
问题根源
原有代码出现高亮错位的原因很简单:每插入一对<mark>标签,字符串总长度就会增加13个字符(<mark>占6位,</mark>占7位),但后续处理用的还是原始文本对应的索引,没有累计之前插入标签带来的偏移量,越靠后的高亮区间错位就越明显。如果传入的索引数组没有按位置排序、或者区间存在重叠,直接遍历插入还会出现标签嵌套、区间错乱的问题。
可行实现方案
方案1:倒序插入法(实现最简单,适配区间无重叠、已排序场景)
从字符串末尾往开头处理内容,后插入的标签完全不会影响前面位置的字符索引,不需要额外计算偏移量,代码改动量最小。
const content = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum tincidunt consequat quam, ut convallis massa euismod at. Proin vel fermentum leo. Suspendisse erat turpis, gravida quis dui sed, mattis consectetur dolor. Curabitur nec tincidunt augue. Donec sapien eros, molestie vel volutpat vitae, vulputate sed ante. Donec finibus consequat dignissim. Donec at nibh eget nibh ultrices gravida. Fusce id molestie eros. Aenean maximus ante et mi elementum, eget pharetra mauris mollis. "; // 先把区间按起始位置从大到小排序,保证从后往前替换 const positions = [[10, 15], [20, 27], [39, 45]].sort((a, b) => b[0] - a[0]); let renderContent = content; positions.forEach(([start, end]) => { renderContent = renderContent.slice(0, start) + '<mark>' + renderContent.slice(start, end) + '</mark>' + renderContent.slice(end); }) document.querySelector("#test").innerHTML = renderContent;
配套的HTML结构:
<div id="test"></div>
方案2:分段拼接法(稳定性最高,适配区间乱序、重叠场景)
先对所有高亮区间做排序、合并处理,再把文本拆成普通段、高亮段依次拼接,全程基于原始文本的索引操作,完全不会出现索引偏移问题,哪怕区间乱序、有重叠也能正常渲染。
const content = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum tincidunt consequat quam, ut convallis massa euismod at. Proin vel fermentum leo. Suspendisse erat turpis, gravida quis dui sed, mattis consectetur dolor. Curabitur nec tincidunt augue. Donec sapien eros, molestie vel volutpat vitae, vulputate sed ante. Donec finibus consequat dignissim. Donec at nibh eget nibh ultrices gravida. Fusce id molestie eros. Aenean maximus ante et mi elementum, eget pharetra mauris mollis. "; const rawPositions = [[10, 15], [20, 27], [39, 45]]; // 1. 按区间起始位置升序排序 const sortedPositions = [...rawPositions].sort((a, b) => a[0] - b[0]); // 2. 合并重叠区间,避免标签嵌套异常 const mergedPositions = []; sortedPositions.forEach(([currStart, currEnd]) => { if (!mergedPositions.length) { mergedPositions.push([currStart, currEnd]); return; } const lastPos = mergedPositions.at(-1); if (currStart <= lastPos[1]) { lastPos[1] = Math.max(lastPos[1], currEnd); } else { mergedPositions.push([currStart, currEnd]); } }) // 3. 逐段拼接最终内容 let renderContent = ''; let cursor = 0; // 标记当前处理到原始文本的索引位置 mergedPositions.forEach(([start, end]) => { // 拼接高亮区间前的普通文本 if (start > cursor) { renderContent += content.slice(cursor, start); } // 拼接带高亮标签的内容 renderContent += `<mark>${content.slice(start, end)}</mark>`; cursor = end; }) // 拼接最后一段剩余的普通文本 if (cursor < content.length) { renderContent += content.slice(cursor); } document.querySelector("#test").innerHTML = renderContent;
补充说明
- 如果待处理的文本里包含
<、>、&这类HTML特殊字符,拼接前要先做转义,避免出现标签解析错误或者XSS风险 - 如果无法保证传入的高亮区间是按顺序排列、且无重叠,优先选第二种分段拼接方案,容错率更高
- 尽量不要在遍历插入的过程中反复修改存原始内容的变量,否则只要涉及索引计算,很容易因为字符串长度变化出现偏移问题
内容的提问来源于stack exchange,提问作者Mike
相关产品推荐
相关产品推荐

