鸿蒙 PC Markdown 编辑器换行兼容:LF、CRLF 与混合换行归一化
鸿蒙 PC Markdown 编辑器换行兼容LF、CRLF 与混合换行归一化换行在编辑器界面里只是“下一行”在文件字节中却可能是 LF、CRLF或单独 CR。若应用打开 Windows文档后统一保存为 LF会制造整文件差异若混合换行不做决策直接继续编辑新输入到底采用哪一种又不明确。Markdown正文相同不等于文件格式相同。本文基于鸿蒙 PC Markdown编辑器 OhMarkdown说明如何线性检测换行、在 CodeMirror中保留行分隔语义、保存时统一归一化、为 Mixed文档显式询问策略并用设备字节测试锁定行为。完整代码位于 https://gitcode.com/VON-/codex_md_oh对应提交3a9146e。四种格式状态应用定义exportenumLineEnding{LFLF,CRLFCRLF,MIXEDMIXED,NONENONE}NONE用于没有任何换行的文档包括空文件和单行文本。它不能简单等同 LF因为“尚无换行”与“明确全是 LF”是不同检测结果保存时可根据新内容重新检测或使用默认策略。MIXED表示存在两种以上终止形式或者出现单独 CR。当前产品不尝试逐行保留混合模式而是在修改后保存前让用户选择统一 LF或 CRLF。未编辑保存可以保留原正文因为序列化对 MIXED不转换。单次扫描检测所有终止符检测函数逐字符统计exportfunctiondetectLineEnding(content:string):LineEnding{letlfCount0;letcrlfCount0;letcrCount0;for(letindex0;indexcontent.length;index1){constcodecontent.charCodeAt(index);if(code13index1content.lengthcontent.charCodeAt(index1)10){crlfCount1;index1;}elseif(code10){lfCount1;}elseif(code13){crCount1;}}if(lfCount0crlfCount0crCount0){returnLineEnding.NONE;}if(crlfCount0lfCount0crCount0){returnLineEnding.CRLF;}if(lfCount0crlfCount0crCount0){returnLineEnding.LF;}returnLineEnding.MIXED;}遇到 CRLF后额外递增 index避免下一轮把其中 LF重复计数。单独 CR进入 crCount哪怕全文只有 CR也返回 MIXED因为产品不提供 CR保存目标保存时需要转成现代格式。扫描复杂度 O(n)没有用多个正则反复遍历全文。文档读取本身已经产生字符串再做一次线性检测成本可控。超大文件仍应记录耗时。为什么不能只检查第一个换行快速实现常找到第一处\n看前一个字符是否\r然后把整篇标为 CRLF或 LF。这会漏掉后半文档的混合换行。代码生成器、复制粘贴和 Git冲突都可能让一份文件前面 CRLF、后面 LF。格式状态用于保存决策必须扫描完整正文。状态栏显示 MIXED也是用户发现问题的入口。性能优化可以在分块读取时增量统计但不能牺牲全文件覆盖。CodeMirror 行分隔配置创建 EditorState时constlineSeparatorcontent.includes(\r\n)?\r\n:\n;returnEditorState.create({doc:content,extensions:[EditorState.lineSeparator.of(lineSeparator),// 其他扩展]});CRLF文档按 CRLF配置新按 Enter产生同类分隔符。LF文档使用 LF。Mixed当前只要包含 CRLF就选 CRLF作为编辑期间新增行策略最终保存仍要求用户统一。这个策略简单但有边界Mixed以 LF为主、只含一处 CRLF时新行仍用 CRLF单独 CR文档会用 LF。更精细做法可以根据计数选择多数格式或使用用户设置。关键是把编辑期策略和保存期策略分开不把 CodeMirror配置当作原始格式检测结果。归一化必须先统一为 LF序列化if(format.lineEndingLineEnding.LF){serializedContentcontent.replace(/\r\n|\r|\n/g,\n);}elseif(format.lineEndingLineEnding.CRLF){serializedContentcontent.replace(/\r\n|\r|\n/g,\n).replace(/\n/g,\r\n);}CRLF转换不能直接把每个\n替换为\r\n否则原有 CRLF中的 LF前面已有 CR会变成\r\r\n。正确做法是先把 CRLF、CR、LF全部归一为单个 LF再将 LF扩展为 CRLF。正则 alternation把\r\n放在单独\r和\n之前确保成对消费。LF目标只做第一步。MIXED和 NONE没有进入分支保留当前字符串对修改后的 Mixed原生保存对话框会先将目标改成 LF或 CRLF。Mixed 保存必须由用户决定保存前privateasyncresolveSaveFormat():PromiseDocumentFormat|undefined{constformat:DocumentFormat{hasUtf8Bom:this.documentFormat.hasUtf8Bom,lineEnding:this.documentFormat.lineEnding};if(!this.documentDirty||format.lineEnding!LineEnding.MIXED){returnformat;}constresultawaitthis.getUIContext().getPromptAction().showDialog({title:$r(app.string.mixed_line_ending_title),message:$r(app.string.mixed_line_ending_message),buttons:[{text:$r(app.string.cancel_action)},{text:$r(app.string.save_lf_action)},{text:$r(app.string.save_crlf_action)}]});if(result.index1){format.lineEndingLineEnding.LF;returnformat;}if(result.index2){format.lineEndingLineEnding.CRLF;returnformat;}returnundefined;}只有文档被修改且原格式 Mixed时询问。未编辑保存直接保留原字符串可达到字节一致修改后必须明确新行和旧混合如何统一。取消返回 undefined保存中止且 dirty保留关闭标签流程也不会继续删除。当前对话框没有“保持混合”选项因为新编辑已让逐行来源难以解释。保真优先并不意味着永远维持异常格式可控转换需要用户知情。保存成功后更新格式写入后this.documentFormat{hasUtf8Bom:saveFormat.hasUtf8Bom,lineEnding:saveFormat.lineEndingLineEnding.NONE?detectLineEnding(this.documentContent):saveFormat.lineEnding};单行 NONE文档如果编辑后出现换行保存结果应反映实际格式。LF或 CRLF目标直接成为新会话格式。状态栏与下一次保存都使用新基线不会继续显示 Mixed。persistedDocumentContent保存内存正文外部冲突检测还比较磁盘格式与当前格式。另一个应用只改变 CRLF/LF即使视觉正文相同也被视为外部变化避免静默覆盖。设备测试覆盖 CRLF和 MixedBOMCRLF用例读取、保存后比较完整十六进制证明 CRLF没有变。Mixed用例awaitwriteRawText(testPath,第一行\r\n第二行\n第三行\r);constopenedawaitreadUtf8Document(testPath);expect(opened.format.lineEnding).assertEqual(LineEnding.MIXED);awaitwriteUtf8Document(testPath,opened.content,{hasUtf8Bom:false,lineEnding:LineEnding.LF});重新读取constnormalizedawaitreadUtf8Document(testPath);expect(normalized.content).assertEqual(第一行\n第二行\n第三行\n);expect(normalized.format.lineEnding).assertEqual(LineEnding.LF);它覆盖 CRLF、LF和单独 CR三种输入确保全部统一为 LF。Web自动化还打开 CRLF文档按 End、Enter输入第三行断言新内容包含\r\n第三行验证 CodeMirror编辑期分隔符。鸿蒙 PC 状态栏下图来自鸿蒙 PC模拟器。状态栏显示当前编码和换行格式让用户在保存前看见 LF、CRLF或 Mixed。截图证明格式元数据进入应用界面具体字节由 ohosTest保证。测试还需补一张 Mixed保存策略对话框覆盖取消、LF和 CRLF三条设备路径。恢复与多标签中的格式DocumentSession保存独立 format标签切换不会串换行。恢复记录和保存中断备份也保存 lineEnding字符串加载时通过parseLineEnding映射到枚举。非法值被校验拒绝。如果只把正文写入恢复 JSON恢复后保存可能采用默认 LF破坏原 CRLF。格式保真必须贯穿异常路径而不只存在于正常打开结果。当前边界单独 CR没有独立保存选项统一视为 Mixed。Mixed编辑期新增行偏向 CRLF只是临时策略。没有全局默认换行设置也没有状态栏点击转换。二进制和非 UTF-8文件在进入换行检测前已拒绝。未来可提供显式“转换换行”命令将转换作为一笔可撤销编辑或保存格式操作还可为 Git仓库读取.gitattributes但需要定义工作区规则优先级。任何自动策略都不能在用户不知情时造成整文件差异。结语换行兼容需要完整扫描而非猜第一行使用 NONE、LF、CRLF、MIXED表达事实CodeMirror按会话设置编辑期分隔符保存时先统一为 LF再转换目标Mixed修改后让用户选择设备测试按字节和内容双重验证。这些规则让鸿蒙 PC Markdown编辑器面对 Windows、Linux和历史文档时保持可预测。换行不可见但正因为不可见应用更应把它当正式格式而不是顺手归一化的空白字符。