mooninput

    Structured input editing for MoonBit: masks, exact decimal text, selections and caret mapping

    input-mask
    forms
    editing
    caret
    formatting
    Download zip
    Author
    Version
    0.1.0
    License
    MIT
    Last updated
    5 hours ago
    Downloads
    6

    #MoonInput

    CI MIT

    MoonBit 原生的结构化输入编辑库。 处理手机号、日期、小数和业务编号输入中的格式、选区、光标与中间状态,同时输出显示文本和可提交的原始值。

    #解决什么问题

    在中间插入、删除、粘贴、选择替换,以及输入法组合输入结束后,仍保持正确的值和光标。

    • 联系信息:粘贴或编辑分组手机号,提交纯数字字符串。
    • 数量与金额:编辑带分组的精确十进制文本,显式拒绝超出配置的小数位,保留 -、. 等未完成状态。
    • 预约与登记:输入公历日期与自定义业务编号,区分未完成、完整有效、完整无效。

    规则编译、日期检查、编辑转换、UTF-16 映射和 JSON 协议都使用 MoonBit。网页和 Node CLI 是事件/IO 适配层,没有外部服务或 API 密钥依赖。表单状态库可消费本库输出的值;与已有库的比较见 SELECTION,不宣称生态绝对空白。

    #安装与最小示例

    可复现基线:MoonBit compiler/core 0.10.14+7d59c7ec9;核心支持 JS、Wasm GC。用 moon version --all 核对;安装与固定版本配置见 开发说明。

    moon add YeeHh2004/mooninput@0.1.0

    在 moon.pkg 中加入:

    import {
    "YeeHh2004/mooninput" @input,
    }

    let mask = @input.pattern("### #### ####").unwrap()
    let editor = @input.Editor::new(mask).unwrap()
    let filled = editor.insert("13800138000").unwrap()
    println(filled.display()) // 138 0013 8000
    println(filled.value().unwrap()) // 13800138000
    let edited = filled.select(4, 8).unwrap().insert("9999").unwrap()
    println(edited.display()) // 138 9999 8000
    println(filled.display()) // 原快照仍为 138 0013 8000

    示例里的 unwrap 用于已知合法数据;实际接入应处理 Result。完整可运行例子在 examples/basic,独立模块在 examples/consumer。

    moon run examples/basic --target wasm-gc

    #核心能力

    入口行为
    pattern("AA-####[/**]")数字、字母、可打印字符、分隔符、单个可选后缀
    decimal(precision=2, signed=true)精确小数字符串、千位分组、符号与中间状态
    iso_date()YYYY-MM-DD 格式与 0001–9999 年公历检查
    Editor::select / insert选区替换、插入、格式化粘贴
    Editor::backspace / delete_forward删除选区或相邻可编辑字符
    Editor::display / raw / value / status显示值、原始值、完整值与状态
    Editor::reconcile组合输入/自动填充后的归一化与光标恢复

    # 为 ASCII 数字,A 为英文字母,* 为字母数字,X 为可打印 Unicode 标量;字母自动转大写。反斜杠转义规则字符,[ ] 仅支持一个末尾可选段。位置统一使用 UTF-16 偏移。详细限制及歧义处理见 BEHAVIOR。

    #浏览器与 CLI

    无需安装 MoonBit 的体验方式:下载 Release 中的 mooninput-0.1.0.zip,解压后在目录中执行 node scripts/serve.mjs 或下方 CLI 命令。预编译包仅需要 Node.js 22+,附源码、编译信息和逐文件校验和。

    需要 Node.js 22+ 和上述固定 MoonBit 版本。运行示例不需要 npm 运行时依赖:

    git clone https://github.com/YeeHh2004/mooninput.git cd mooninput node scripts/build.mjs node scripts/serve.mjs

    打开 http://127.0.0.1:4173。三个业务场景和自定义规则工作台使用同一份 MoonBit 编译产物。

    import {bindInput} from './adapter.mjs'; const binding = bindInput(document.querySelector('#amount'), {kind:'decimal', precision:2}, { onChange(state) { console.log(state.value, state.status); }, onError(error) { console.log(error.message); } }); // binding.setValue('1234.50'); binding.undo(); binding.redo(); // 卸载时调用 binding.destroy()。

    复制 web/adapter.mjs 与构建出的 web/mooninput.mjs 接入网页时,保留许可证与 notices。使用 type=text;type=number 不提供所需的文本选区接口。

    CLI 由 Node 处理文件和参数,计算由编译后的 MoonBit 执行:

    node cli/mooninput.mjs --date 2026-10-02 node cli/mooninput.mjs --decimal 9007199254740993.10 node cli/mooninput.mjs --pattern "AA-####" ab1234 node cli/mooninput.mjs --replay examples/events.json

    退出码:0 成功,1 编辑被拒绝或单值未完成/无效,2 参数、文件或规则错误。回放输出每一步状态。

    #验证与发布

    moon fmt --check moon check --target js moon check --target wasm-gc moon test --target js moon test --target wasm-gc node scripts/build.mjs node --test tests/*.test.mjs node scripts/check-consumer.mjs node scripts/check-examples.mjs python scripts/check-mooncake.py

    Linux/Windows CI 覆盖检查、构建、测试。包含 37 项 MoonBit 测试、独立消费者、12,000 次随机编辑模型对照、1,000 组精确小数往返、4,800 个日期样本和浏览器检查。当前结果见 Actions。

    浏览器测试额外需要 npm install、npx playwright install chromium,再运行 node tests/browser.mjs。组合输入采用合成事件测试,不代表所有实体手机/输入法均已验证,见 验证说明。

    Mooncakes 分发 MoonBit 核心、协议包、测试、文档和可运行样例;网页、Node CLI 和工具保留在 GitHub。node scripts/check-consumer.mjs --registry 从公共注册表安装,并核对核心源码及两个后端的运行结果。

    #来源与许可

    原创、AI 辅助实现,MIT。参考 IMask 展示的通用输入编辑需求,未复制其源码,也不宣称 API 兼容。完整说明见 NOTICE。不验证号码是否真实可用,不处理支付,不替代服务端业务校验。

    #English

    MoonInput is an immutable structured-input editing engine written in MoonBit. It handles pattern masks, exact decimal text, Gregorian date validity, selection replacement, deletion, formatted paste and UTF-16 caret mapping on JavaScript and Wasm GC. A browser adapter adds DOM events, composition deferral and bounded undo/redo; a Node CLI replays edits through the compiled engine. See the English API, behavior contract and development guide.

    Editor

    pub struct Editor {
    // private fields
    }

    An immutable snapshot; edits return a new editor or an error.

    Editor::backspace

    fn Editor::backspace(self : Editor) -> Result[Editor, InputError]

    Delete selected raw characters, or the editable scalar before the caret.

    Editor::delete_forward

    fn Editor::delete_forward(self : Editor) -> Result[Editor, InputError]

    Delete selected raw characters, or the editable scalar after the caret.

    Editor::display

    fn Editor::display(self : Editor) -> String

    Editor::display_offset

    fn Editor::display_offset(self : Editor, index : Int) -> Result[Int, InputError]

    Map an editable scalar boundary to a display UTF-16 position.

    Editor::insert

    fn Editor::insert(self : Editor, text : String) -> Result[Editor, InputError]

    Replace the current selection or insert at the caret. Invalid edits are atomic.

    Editor::new

    fn Editor::new(mask : Mask, initial? : String) -> Result[Editor, InputError]

    Create a snapshot from raw or formatted text. Selection starts at the end.

    Editor::raw

    fn Editor::raw(self : Editor) -> String

    Unformatted editable content; numeric input is kept as an exact string.

    Editor::raw_index

    fn Editor::raw_index(self : Editor, offset : Int) -> Result[Int, InputError]

    Map a display UTF-16 offset to an editable scalar index.

    Editor::reconcile

    fn Editor::reconcile(self : Editor, text : String, start : Int, end : Int) -> Result[Editor, InputError]

    Normalize a browser's already-edited display after composition/autofill. Selection refers to the proposed display. Prefixes are formatted by the same MoonBit core to map the caret, never by host-side formatting rules.

    Editor::select

    fn Editor::select(self : Editor, start : Int, end : Int) -> Result[Editor, InputError]

    Set a valid selection. A range expands to whole Unicode scalars; a collapsed caret inside a surrogate pair snaps to its beginning.

    Editor::selection

    fn Editor::selection(self : Editor) -> Selection

    Editor::set_value

    fn Editor::set_value(self : Editor, text : String) -> Result[Editor, InputError]

    Replace all content and position the caret at the end.

    Editor::status

    fn Editor::status(self : Editor) -> Status

    Editor::value

    fn Editor::value(self : Editor) -> Result[String, InputError]

    Return a complete value, or a typed error. Empty is distinct from incomplete.

    InputError

    pub(all) enum InputError {
    InvalidPattern(String)
    InvalidCharacter(Int, String)
    TooLong(Int)
    InvalidSelection
    InvalidConfig(String)
    InvalidValue(String)
    } derive(Eq,
    Debug
    )

    Errors never modify an existing editor.

    InputError::equal

    fn InputError::equal(InputError, InputError) -> Bool

    InputError::message

    fn InputError::message(self : InputError) -> String

    InputError::not_equal

    fn InputError::not_equal(x : InputError, y : InputError) -> Bool

    Mask

    pub struct Mask {
    // private fields
    }

    A compiled immutable input specification.

    Mask::format

    fn Mask::format(self : Mask, text : String) -> Result[String, InputError]

    Normalize a raw or exactly formatted value. Does not allocate an editor.

    Selection

    pub(all) struct Selection {
    start : Int
    end : Int
    } derive(Eq,
    Debug
    )

    Positions use UTF-16 offsets, including on Wasm GC.

    Selection::equal

    fn Selection::equal(Selection, Selection) -> Bool

    Selection::not_equal

    fn Selection::not_equal(x : Selection, y : Selection) -> Bool

    Status

    pub(all) enum Status {
    Empty
    Incomplete
    Complete
    Invalid(String)
    } derive(Eq,
    Debug
    )

    Status::equal

    fn Status::equal(Status, Status) -> Bool

    Status::not_equal

    fn Status::not_equal(x : Status, y : Status) -> Bool

    Status::to_repr

    decimal

    fn decimal(precision? : Int, signed? : Bool, grouped? : Bool) -> Result[Mask, InputError]

    Decimal text editing. Precision limits fractional digits; it never rounds. Uses '.' as radix and optional ',' grouping. At most 256 raw characters.

    iso_date

    fn iso_date() -> Mask

    ISO Gregorian date input, years 0001..9999. Editing accepts partial dates; completed invalid dates remain visible with Invalid status.

    pattern

    fn pattern(source : String) -> Result[Mask, InputError]

    Compile # digit, A ASCII letter, * ASCII alphanumeric, X printable scalar. A single optional suffix in brackets is supported. Backslash escapes literals.