
本文详解 writable div 实现语法高亮时出现文本反转(如输入 def 显示为 fed)的根本原因,并提供安全、稳定、可维护的解决方案,包括 dom 操作修正、推荐专业编辑器库及替代架构建议。
本文详解 writable div 实现语法高亮时出现文本反转(如输入 `def` 显示为 `fed`)的根本原因,并提供安全、稳定、可维护的解决方案,包括 dom 操作修正、推荐专业编辑器库及替代架构建议。
你遇到的“输入 def 却显示为 fed”现象,并非字符顺序被算法反转,而是由 innerHTML = text 触发的 DOM 重建导致光标位置丢失 + 浏览器自动修复 HTML 结构所引发的视觉错乱。根本原因在于:你用 innerText 获取纯文本,再用 replace() 插入 HTML 标签,最后通过 innerHTML = text 全量覆写整个 div 内容——这会销毁当前所有 DOM 节点(包括用户光标位置、选区、已渲染的 ),浏览器在重新解析新 HTML 时,可能因标签嵌套不完整、换行符处理异常或编辑器内部状态不同步,造成光标跳转到错误位置,进而让用户误以为“文字被反转”。
以下是一个最小化复现与修正示例:
<div id="code-editor" spellcheck="false" contenteditable="true" style="font-family: monospace; white-space: pre; padding: 8px; border: 1px solid #ccc;"></div> <script> const editor = document.getElementById("code-editor"); const pythonKeywords = ['def', 'class', 'if', 'else', 'for', 'while', 'import', 'from', 'return']; // ✅ 安全正则:转义关键词中的特殊字符(如'+'、'?'等) const escapedKeywords = pythonKeywords.map(k => k.replace(/[.*+?^${}()|[]]/g, '$&')); const keywordRegex = new RegExp(`b(${escapedKeywords.join('|')})b`, 'g'); editor.addEventListener('input', () => { // ⚠️ 错误做法(导致反转假象): // const text = editor.innerText; // editor.innerHTML = text.replace(keywordRegex, '<span style="color:#f200ff">$&</span>'); // ✅ 正确做法:仅更新文本节点,保留 DOM 结构和光标 highlightKeywords(editor, keywordRegex); }); function highlightKeywords(node, regex) { if (node.nodeType === Node.TEXT_NODE && node.textContent.trim()) { const parent = node.parentNode; const text = node.textContent; // 分割文本:保留非关键词部分 + 包裹关键词 const parts = text.split(regex); const matches = [...text.matchAll(regex)]; const fragment = document.createDocumentFragment(); let lastIndex = 0; matches.forEach(match => { const [fullMatch] = match; const matchIndex = match.index; // 添加前缀纯文本 if (matchIndex > lastIndex) { fragment.appendChild(document.createTextNode(text.slice(lastIndex, matchIndex))); } // 添加高亮 span const span = document.createElement('span'); span.style.color = '#f200ff'; span.textContent = fullMatch; fragment.appendChild(span); lastIndex = matchIndex + fullMatch.length; }); // 添加后缀纯文本 if (lastIndex < text.length) { fragment.appendChild(document.createTextNode(text.slice(lastIndex))); } // 替换原文本节点(非全量 innerHTML!) parent.replaceChild(fragment, node); } // 递归处理子节点(但跳过已高亮的 span,避免重复处理) for (let child of node.childNodes) { if (child.nodeType === Node.ELEMENT_NODE && child.tagName !== 'SPAN') { highlightKeywords(child, regex); } } } </script>
⚠️ 关键注意事项:
- 永远不要在 contenteditable 元素中直接赋值 innerHTML —— 它会重置光标、破坏 undo 栈、触发不可预测的 HTML 自动修正(例如将 def 解析为
ef 或插入零宽空格)。 - innerText 会丢弃换行符和空格格式;若需保留缩进,应改用 textContent 并配合 white-space: pre CSS。
- 上述递归高亮方案虽能解决基础问题,但不适用于高频输入场景(如连续打字),存在性能瓶颈和光标偏移风险。
✅ 生产环境强烈推荐成熟方案:
立即学习“Python免费学习笔记(深入)”;
- CodeMirror 6:轻量、模块化、支持主题/语言服务器/LSP,API 稳定,官网 提供开箱即用的 Python 高亮示例。
- Monaco Editor(VS Code 底层):功能最全,适合 ide 级应用,官方 playground 可快速验证。
- Ace Editor:老牌可靠,低资源占用,示例 支持 Python 模式一键切换。
总结:用 contenteditable 手搓代码编辑器是高风险、低回报的选择。文本反转只是表象,背后是 DOM 状态管理的系统性复杂度。优先集成专业编辑器库,聚焦业务逻辑;若必须自研,请基于 TextRange / Selection API 精确操作光标,而非依赖 innerHTML 全量刷新。