如何不使用JavaScript的.replace()方法按位置替换字符串实现文本标记
当然可以用光标选择的起止位置实现精准替换!
你的思路完全正确——用文本的起止位置来定位替换是解决重复文本歧义问题的最佳方案,不过你的现有代码在处理HTML标签的字符偏移时逻辑有点绕,容易算错位置,导致<mark>标签插错地方。我帮你梳理下问题,再给你一个更可靠的实现思路。
现有代码的核心问题
你在characterCounter里手动计数HTML标签的字符数来调整偏移,但这种方式很容易出错:
- HTML标签的长度不固定(比如
<mark class="red">比<mark>长很多),手动累加计数很容易算错 - 当文本中已经存在其他HTML标签时,纯文本的位置和原始字符串的位置完全不对齐,你的计数逻辑没有考虑到这一点
正确的实现思路
要解决这个问题,我们需要先把带HTML的原始文本转换成纯文本,同时建立「纯文本字符位置」和「原始HTML字符串位置」的映射关系。这样用户选中的纯文本起止位置(比如第10到20个纯文本字符),就能精准对应到原始HTML中的准确区间,然后直接在这个区间插入<mark>标签。
改进后的代码示例
/** * 建立纯文本位置到原始HTML位置的映射 * @param htmlText 带HTML的原始文本 * @returns { text: string, positions: number[] } text是纯文本,positions数组中每个元素对应纯文本字符在原始HTML中的起始位置 */ private createTextPositionMap(htmlText: string): { text: string, positions: number[] } { const text: string[] = []; const positions: number[] = []; let inTag = false; for (let i = 0; i < htmlText.length; i++) { const char = htmlText[i]; if (char === '<') { inTag = true; } else if (char === '>') { inTag = false; } else if (!inTag) { text.push(char); positions.push(i); } } return { text: text.join(''), positions: positions }; } /** * 根据纯文本的起止位置,在原始HTML中插入mark标签 * @param htmlText 带HTML的原始文本 * @param start 纯文本的起始位置 * @param end 纯文本的结束位置 * @param marker 标记信息(颜色、ID等) * @returns 插入mark标签后的HTML文本 */ private insertMarkTag(htmlText: string, start: number, end: number, marker: any): string { const positionMap = this.createTextPositionMap(htmlText); // 如果起止位置超出纯文本范围,直接返回原文本 if (start < 0 || end > positionMap.text.length || start >= end) { return htmlText; } // 获取原始HTML中的起始和结束位置 const htmlStart = positionMap.positions[start]; // 结束位置要取结束字符的下一个位置,因为substring是左闭右开 const htmlEnd = positionMap.positions[end - 1] + 1; // 拼接新的HTML文本:前半部分 + mark标签包裹的选中内容 + 后半部分 const before = htmlText.substring(0, htmlStart); const selectedText = htmlText.substring(htmlStart, htmlEnd); const markTag = `<mark class="${marker.color}" alt="${marker.id}">${selectedText}</mark>`; const after = htmlText.substring(htmlEnd); return before + markTag + after; } /** * 更新章节中的标记 */ public characterCounter(chapter: any, marker: any): any { chapter.blocks.some((block: any) => { if (marker.blockId === block.blockId) { block.blockElement.text = this.insertMarkTag( block.blockElement.text, marker.start, marker.end, marker ); return true; // 找到对应block后退出循环 } return false; }); return chapter; } /** * 为所有章节添加标记 */ public addMarking(markersList: any[], chaptersArray: any[]): any[] { markersList.forEach((marker) => { // 先找主章节 let chapterIndex = chaptersArray.findIndex(ch => ch.id === marker.chapterId); if (chapterIndex !== -1) { this.characterCounter(chaptersArray[chapterIndex], marker); return; } // 主章节找不到,找子章节 chaptersArray.forEach((chapter) => { let subChapterIndex = chapter.subChapters?.findIndex(sub => sub.id === marker.chapterId); if (subChapterIndex !== -1) { this.characterCounter(chapter.subChapters[subChapterIndex], marker); } }); }); return chaptersArray; }
代码说明
- createTextPositionMap:遍历原始HTML文本,提取纯文本的同时,记录每个纯文本字符在原始HTML中的位置,这样我们就能把用户选中的纯文本位置转换成原始HTML中的准确位置。
- insertMarkTag:利用上面的映射关系,找到原始HTML中需要替换的区间,直接插入
<mark>标签,避免了手动计数HTML标签的复杂逻辑。 - 简化了
characterCounter和addMarking的逻辑,用findIndex替代map.indexOf,代码更简洁易读。
额外注意点
- 如果需要支持多个标记叠加(比如同一个文本被多次标记),建议先按标记的起始位置排序,然后从后往前插入标签,这样不会影响前面标签的位置偏移。
- 你的代码里有
*ngClass="",这在Angular里是多余的,直接用class="${marker.color}"就可以了。
内容的提问来源于stack exchange,提问作者Miguel Frias
相关产品推荐
相关产品推荐

