状态:已实现(10-04 第一批;10-10 批①补转义族与替换引擎)。本页每个 test 块都由moon test 真跑;块数与条数现读 moon test --package text,三档(wasm/js/wasm-gc)读数一致。 期望值仍是冻结状态:要改任何一条,必须单独一笔并给外部读数来源(AGENTS.md 红线一,门禁 G5/G11 双重盯着)。
///|
test "is_blank 空白档" {
assert_true(@text.is_blank(" "))
assert_true(@text.is_blank("\t \n"))
assert_false(@text.is_blank(" a"))
}///|
test "is_empty 与 is_blank 分档" {
assert_true(@text.is_empty(""))
assert_false(@text.is_empty(" ")) // 非空但全空白:empty 假、blank 真
assert_true(@text.is_blank(" "))
}///|
test "sub_before / sub_after 未命中" {
assert_eq(@text.sub_before("path/to/file.txt", "/"), "path") // 首个分隔符,不是最后一个
assert_eq(@text.sub_before("nope", "/"), "nope") // 未命中 → 整串
assert_eq(@text.sub_after("path/to/file.txt", "/"), "to/file.txt")
assert_eq(@text.sub_after("nope", "/"), "") // 未命中 → 空串
}
///|
test "sub_between 可选带界定符" {
assert_eq(@text.sub_between("hutool{abc}end", "{", "}"), "abc")
assert_eq(
@text.sub_between("hutool{abc}end", "{", "}", include_sep=true),
"{abc}",
)
assert_eq(@text.sub_between("hutool abc end", "{", "}"), "")
}///|
test "split 保留空段" {
assert_eq(@text.split("a,b,,c", ','), ["a", "b", "", "c"])
assert_eq(@text.split("", ','), []) // 空输入是空数组,不是 [""]
assert_eq(@text.split(" a , b ", ',', trim_result=true), ["a", "b"])
}
///|
test "join" {
assert_eq(@text.join(["a", "b"], "-"), "a-b")
assert_eq(@text.join(["a", "", "b"], "-"), "a--b")
}///|
test "format 三条硬语义" {
assert_eq(@text.format("a{}b", ["1"]), "a1b")
assert_eq(@text.format("id={} name={}", ["7"]), "id=7 name={}")
assert_eq(@text.format("keep \\{}", []), "keep {}")
}///|
test "compare_version" {
assert_eq(@text.compare_version("1.2.3", "1.2.10"), -1)
assert_eq(@text.compare_version("1.0.0", "1.0"), 0) // 缺段补零
assert_eq(@text.compare_version("2", "10"), -1) // 若按字典序这里会得 1
}///|
test "hide 码点下标" {
assert_eq(@text.hide("13012345678", 3, 7, '*'), "130****5678")
assert_eq(@text.hide("中文测试", 1, 3, '*'), "中**试") // 区间外字符保留
assert_eq(@text.hide("abc", 1, 99, '*'), "a**") // 越界夹紧,不报错
}///|
test "naming 已冻结的三条" {
assert_eq(@text.to_underline_case("userName"), "user_name")
assert_eq(@text.to_camel_case("user_name"), "userName")
assert_eq(@text.to_camel_case("userName"), "userName") // 无分隔符 → 原样返回
}///|
test "escape 实体族:XML 收撇号、HTML4 不收" {
assert_eq(@text.escape_xml("a<b&c>d\"e'f"), "a<b&c>d"e'f")
assert_eq(@text.escape_html4("a<b&c>d\"e'f"), "a<b&c>d"e'f")
// 一次只解一层:`&amp;` → `&`,要回到 `&` 得再解一次
assert_eq(@text.unescape_xml("&amp;"), "&")
// HTML4 认 `©`;XML 的还原表只有那 5 条 + ` `
assert_eq(@text.unescape_xml("©"), "©")
assert_eq(@text.unescape_html4("©"), "\u{00a9}")
}///|
test "escape 百分号族:`%uXXXX` 不是 UTF-8 百分号编码" {
assert_eq(@text.escape("中文 a"), "%u4e2d%u6587%20a")
assert_eq(@text.escape_all("ab"), "%61%62")
assert_eq(@text.escape("%41"), "%2541") // 已编码的再编码一次
assert_eq(@text.unescape("%41"), "A")
// 不做 UTF-8 解码:`%e4%b8%ad` 出三个字符,不是"中"
assert_eq(@text.unescape("%e4%b8%ad"), "\u{00e4}\u{00b8}\u{00ad}")
}///|
test "unescape 的失败两档与 safe 档" {
let a = try {
let _ = @text.unescape("%zz")
"未抛错"
} catch {
@text.BadHex(x) => "BadHex " + x
@text.ShortInput => "ShortInput"
_ => "其它"
}
assert_eq(a, "BadHex zz")
let b = try {
let _ = @text.unescape("%2")
"未抛错"
} catch {
@text.BadHex(x) => "BadHex " + x
@text.ShortInput => "ShortInput"
_ => "其它"
}
assert_eq(b, "ShortInput")
assert_eq(@text.safe_unescape("%zz"), "%zz")
}///|
test "unicode 三件:一把尺子,三种入参形状" {
assert_eq(@text.to_unicode("中a文"), "\\u4e2da\\u6587")
assert_eq(@text.to_unicode_all("ab"), "\\u0061\\u0062")
assert_eq(@text.unicode_of(0x4e2d), "\\u4e2d")
assert_eq(@text.unicode_of(-1), "\\uffffffff") // 参照就是给 8 位 f
assert_eq(@text.unicode_to_string("\\\\u4e2d"), "\\中") // 双反斜杠只挡住第一个:参照吃掉第二个当转义起始
}///|
test "encode_blank 只动空白" {
assert_eq(@text.encode_blank(" a "), "%20a%20")
assert_eq(@text.encode_blank("\t\n"), "%20%20")
assert_eq(@text.encode_blank("中文"), "中文")
}///|
test "replacer:长键优先、链不回头扫" {
let t = @text.lookup_replacer([("ab", "X"), ("abc", "Y")])
assert_eq(t.replace("xaby abc"), "xXy Y")
let chain = @text.replacer_chain([
@text.lookup_replacer([("ab", "1")]),
@text.lookup_replacer([("1", "X")]), // 第一件产出的 `1` 不会被第二件再换掉
])
assert_eq(chain.replace("ab"), "1")
let empty_key = try {
let _ = @text.lookup_replacer([("", "E")])
"未抛错"
} catch {
@text.EmptyKey => "EmptyKey"
_ => "其它"
}
assert_eq(empty_key, "EmptyKey")
}pub suberror TextError {
BadHex(String)
ShortInput
LoneSurrogate(Int)
EmptyKey
}pub struct Replacer {
// private fields
}fn compare_version(a : String, b : String) -> Intfn encode_blank(s : String) -> Stringfn escape(s : String) -> Stringfn escape_all(s : String) -> Stringfn escape_by(s : String, to_escape : (Char) -> Bool) -> Stringfn escape_html4(s : String) -> Stringfn escape_xml(s : String) -> Stringfn hide(s : String, start : Int, end : Int, mask : Char) -> Stringfn is_blank_char(c : Char) -> Boolfn safe_unescape(s : String) -> Stringfn sub_after(s : String, sep : String) -> Stringfn sub_before(s : String, sep : String) -> Stringfn sub_between(s : String, left : String, right : String, include_sep? : Bool) -> Stringfn to_camel_case(name : String) -> Stringfn to_kebab_case(name : String) -> Stringfn to_pascal_case(name : String) -> Stringfn to_underline_case(name : String) -> Stringfn to_unicode(s : String) -> Stringfn to_unicode_all(s : String) -> Stringfn trim_opt(s : String?) -> String?fn unicode_of(cp : Int) -> Stringfn upper_first(s : String) -> StringInstall
Download zipHutool-style utility library for MoonBit: all packages depend only on moonbitlang/core (no FFI, fully synchronous); the single exception is the `sched` package, which imports official `moonbitlang/async` for timer firing and therefore ships on three targets, not four
Dependencies