moonbit_diff

Unicode-safe text diff and Unified Diff rendering for MoonBit

diff
myers
unified-diff
unicode
moon add pei0331/moonbit_diff@0.5.0
Download zip
Author
Version
0.5.0
License
Apache-2.0
Last updated
22 hours ago
Downloads
16
README

#MoonBit Diff

CI Mooncakes License

MoonBit Diff 是一个无第三方依赖的 MoonBit 文本差异库,提供 Unicode 安全的字符级编辑脚本、脚本压缩与统计、文本往返重建,以及支持自定义文件名、 上下文行数和多 hunk 的 Unified Diff 渲染。

A dependency-free, Unicode-safe text diff and Unified Diff library for MoonBit.

#功能概览

能力API说明
字符级差异diff返回最短的 Equal/Delete/Insert 编辑脚本
相邻操作合并compact将逐字符操作合并为适合展示和传输的文本片段
文本重建reconstruct_old/new从编辑脚本恢复修改前或修改后的文本
差异统计stats统计相同、删除、插入的 Unicode 字符数
全量 Unified Diffunified_diff使用默认标签并输出覆盖全部输入的单个 hunk
上下文 Unified Diffunified_diff_with_context自定义标签和上下文,远距离修改自动拆分为多个 hunk

#安装

从 mooncakes.io 安装已发布版本:

moon add pei0331/moonbit_diff@0.2.0

在使用方的 moon.pkg 中导入:

import {
"pei0331/moonbit_diff" @diff,
}

#快速开始

let edits = @diff.diff("Moon😀 diff", "Moon🌕 patch")
let compacted = @diff.compact(edits)

for edit in compacted {
match edit {
Equal(text) => println("Equal(\{text})")
Delete(text) => println("Delete(\{text})")
Insert(text) => println("Insert(\{text})")
}
}

输出:

Equal(Moon) Delete(😀) Insert(🌕) Equal( ) Delete(diff) Insert(patch)

diff 保留细粒度的一字符一操作语义,适合精确定位;compact 只合并相邻的同类 操作,不改变编辑脚本代表的新旧文本。

#文本重建与统计

let edits = @diff.diff("A😀bc", "A😀🌕d")

assert_eq!(@diff.reconstruct_old(edits), "A😀bc")
assert_eq!(@diff.reconstruct_new(edits), "A😀🌕d")

let summary = @diff.stats(edits)
println("equal: \{summary.equal_count}")
println("deleted: \{summary.delete_count}")
println("inserted: \{summary.insert_count}")

stats 同时接受原始脚本和 compact 后的脚本,始终按 Unicode scalar value 计数,而不是按 UTF-16 code unit 计数。

#Unified Diff

#简单全量输出

unified_diff 适合短文本和教学示例。它使用 oldnew 作为文件标签,并在 一个 hunk 中展示全部行:

let patch = @diff.unified_diff(
"one\ntwo\nthree",
"one\n2\nthree",
)
println(patch)

--- old +++ new @@ -1,3 +1,3 @@ one -two +2 three

#自定义上下文与多 hunk

较长文件建议使用 unified_diff_with_context

let patch = @diff.unified_diff_with_context(
old_source,
new_source,
"src/before.mbt",
"src/after.mbt",
3,
)

最后一个参数是每处修改前后的上下文行数。相距较远的修改会生成独立 hunk,重叠 的上下文窗口会自动合并。负数上下文按 0 处理。文件标签中的 CR/LF 会替换成 空格,避免破坏补丁头部结构。

两种 Unified Diff API 都会先将 CRLF、LF 和 CR 归一化为 LF;归一化后内容相同 时返回空字符串。

#Unicode 语义

MoonBit 的 String.length() 返回 UTF-16 code unit 数量,直接按索引切片可能把 非 BMP 字符拆成无效代理片段。本库先通过 String::to_array() 转换为 Array[Char] 因此 😀🌕𐐷 等字符始终作为完整操作输出。

这里的“字符”指 Unicode scalar value,不是扩展字素簇。由多个 scalar value 组成 的用户感知字符,例如带肤色修饰符的 emoji、国旗和 ZWJ 家庭 emoji,可能产生多个 编辑操作;可用 compact 合并相邻同类操作,但本库目前不执行字素簇分段。

#API 参考

pub(all) enum EditOp {
Equal(String)
Delete(String)
Insert(String)
}

pub(all) struct DiffStats {
equal_count : Int
delete_count : Int
insert_count : Int
}

pub fn diff(String, String) -> Array[EditOp]
pub fn compact(Array[EditOp]) -> Array[EditOp]
pub fn reconstruct_old(Array[EditOp]) -> String
pub fn reconstruct_new(Array[EditOp]) -> String
pub fn stats(Array[EditOp]) -> DiffStats
pub fn unified_diff(String, String) -> String
pub fn unified_diff_with_context(
String,
String,
String,
String,
Int,
) -> String

完整导出接口也可通过以下命令生成和查看:

moon info

#可运行示例

仓库中的 examples/basic 覆盖差异计算、压缩、统计、文本重建和上下文 Diff:

moon run examples/basic

#复杂度与适用范围

  • 字符级和行级回溯使用 LCS 动态规划,修改区间的时间与空间复杂度为 O(N*M)
  • 字符级 API 会先剥离公共前缀和后缀,因此局部修改通常只需处理较短的中间区间。
  • 当前实现适合配置文件、源码片段、日志片段和中小型文本。
  • 对超大文件或完全不同的长文本,建议先按块或按行缩小范围,避免构造过大的矩阵。
  • Unified Diff 是渲染输出 API,当前不提供补丁解析或补丁应用功能。

#开发与验证

安装 MoonBit 工具链后运行:

moon fmt --check moon check --deny-warn moon build --target wasm-gc moon test --target wasm-gc moon run examples/basic

CI 还会在 wasm-gcjsnative 后端执行构建与测试。贡献流程参见 CONTRIBUTING.md,版本变化参见 CHANGELOG.md

#仓库与发布

#License

Apache-2.0,详见 LICENSE

#
DiffStats

pub(all) struct DiffStats {
equal_count : Int
delete_count : Int
insert_count : Int
} derive(Eq,
Debug
)

Counts of Unicode characters represented by an edit script.

#
EditOp

pub(all) enum EditOp {
Equal(String)
Delete(String)
Insert(String)
} derive(Eq,
Debug
)

#
compact

fn compact(edit_script : Array[EditOp]) -> Array[EditOp]

Merge adjacent operations of the same kind into larger text spans.

diff emits one operation per Unicode character. compact is convenient for display and transport while preserving the represented old and new texts.

#
diff

fn diff(old_text : String, new_text : String) -> Array[EditOp]

Return a Unicode character-oriented edit script that transforms old_text into new_text.

Shared prefixes and suffixes are preserved as Equal operations while the changed middle span is minimized with Myers/LCS-style ordering.

#
reconstruct_new

fn reconstruct_new(edit_script : Array[EditOp]) -> String

Reconstruct the updated text represented by an edit script.

#
reconstruct_old

fn reconstruct_old(edit_script : Array[EditOp]) -> String

Reconstruct the original text represented by an edit script.

#
stats

fn stats(edit_script : Array[EditOp]) -> DiffStats

Count equal, deleted, and inserted Unicode characters in an edit script.

The function also accepts compacted scripts and counts their contents by Unicode scalar value rather than UTF-16 code unit length.

#
unified_diff

fn unified_diff(old_text : String, new_text : String) -> String

Render a unified diff string for line-oriented text changes.

Returns an empty string for equal input after CRLF, LF, and CR normalization. The output uses --- old, +++ new, and one hunk covering the full input.

#
unified_diff_with_context

fn unified_diff_with_context(old_text : String, new_text : String, old_label : String, new_label : String, context : Int) -> String

Render a Unified Diff with custom labels and a bounded amount of context.

Distant changes are emitted as separate hunks. A negative context value is treated as zero. Labels have CR and LF characters replaced with spaces so they cannot break the diff header.

Source Files