moonjsonc

    Source-preserving JSON/JSONC editing, tolerant syntax trees, formatting and transactional configuration changes in MoonBit

    jsonc
    json
    configuration
    editor
    source-editing
    Download zip
    Author
    Version
    0.1.0
    License
    MIT
    Last updated
    16 hours ago
    Downloads
    7

    No dependencies

    #MoonJSONC

    CI License: MIT

    MoonBit JSON/JSONC source editing for configuration tools, editors and automation.

    按结构化路径修改配置,保留无关注释、排版与数字原文;提供容错语法树、光标定位、 格式化补丁,以及带前置检查的批量事务。核心解析与编辑算法全部使用 MoonBit, 可用于 JavaScript 和 Wasm GC,不依赖浏览器、Node.js 或远程服务。

    #Why source editing?

    A configuration updater should be able to change a command without deleting the maintainer's explanation, reformatting unrelated fields or rounding a large numeric identifier. MoonJSONC returns localized, reviewable text edits:

    { // Keep the team convention documented. "scripts": {"test": "moon test"}, "buildId": 9007199254740993 }

    Set ["scripts", "test"] to "moon test --target wasm-gc"; the comment, unrelated whitespace and 9007199254740993 remain exactly as written.

    #Install and use from MoonBit

    Tested/pinned toolchain: moonc 0.10.14+7d59c7ec9, with matching core.

    moon add YeeHh2004/moonjsonc@0.1.0

    Add the import to your package's moon.pkg:

    import {
    "YeeHh2004/moonjsonc" @jsonc,
    }

    fn main {
    let source = "{/* keep */\"enabled\":false,\"id\":9007199254740993}"
    let edits = @jsonc.modify(source, [Key("enabled")], Some("true")).unwrap()
    println(@jsonc.apply_edits(source, edits).unwrap())
    }

    Output:

    {/* keep */"enabled":true,"id":9007199254740993}

    The example uses unwrap for brevity. Applications should handle Result errors and display diagnostics; errors never include a partially committed transaction. examples/consumer is an independent, executable installation example.

    #Public capabilities

    AreaAPIsUseful for
    Syntaxscan, parse, node_valueTokens, JSONC options, tolerant trees, bounded diagnostics
    Navigationfind_node, node_at_offset, get_location, to_pointer, visitEditor settings, structured inspection, source events
    Presentationformat, strip_commentsDocument/range formatting, comment removal with offsets intact
    Source updatesmodify, insert, apply_editsCreate, replace, remove and insert without rewriting unrelated source
    Coherent batchestransactSnapshot checks, Test preconditions, all-or-error multi-step changes

    Paths use Key(String) and Index(Int). Replacement values are strict JSON source strings, so exact numeric spelling survives. All positions are UTF-16 code units on both backends. Editing refuses malformed or duplicate-key documents; tolerant parsing remains available for editor diagnostics.

    #Run the three integration examples

    git clone https://github.com/YeeHh2004/moonjsonc.git cd moonjsonc moon run examples/basic --target js moon run examples/editor --target wasm-gc moon run examples/transaction --target js

    1. Project configuration upgrade: change build/test commands while keeping developer comments and large identifiers.
    2. Editor settings: connect cursors, paths, diagnostics and nested values; inspect incomplete input and format selected lines.
    3. Deployment updates: check a version/snapshot, change several fields and preview a single patch; reject the entire result on a failed precondition.

    #CLI and browser workbench

    Node.js 22+ hosts the compiled engine. Algorithms execute in MoonBit-generated JavaScript; Node supplies file I/O and the browser supplies the interface.

    npm ci --ignore-scripts npm run build node cli/moonjsonc.mjs --help node cli/moonjsonc.mjs check config.jsonc node cli/moonjsonc.mjs set config.jsonc '["service","replicas"]' '4' node cli/moonjsonc.mjs format config.jsonc --check npm run serve

    Commands above use shell quoting that works in Bash and PowerShell. Preview is the default. Add --write explicitly to update a file, or --json to inspect patches. A transaction reads an array of operations from a JSON file:

    [ {"op":"test","path":["version"],"value":"3"}, {"op":"set","path":["service","replicas"],"value":"4"}, {"op":"insert","path":["service","regions"],"index":1,"value":"\"west\""}, {"op":"set","path":["version"],"value":"4"} ]

    node cli/moonjsonc.mjs transaction deployment.jsonc operations.json

    The workbench is at http://127.0.0.1:4173. It includes all three presets, editable source, patch/diagnostic inspection, explicit adoption and download. It sends no source data to a server. Transport documentation describes embedding the same engine in another host.

    #Verification and scope

    CI checks formatting, both target builds and tests, generated public interfaces, Node workflows, an independent MoonBit consumer, package contents and Chromium interactions on Linux; compiler/build/host checks also run on Windows. Seeded tests compare scanning, parsing, formatting, stripping and structural edits against Microsoft's jsonc-parser@3.3.1, with deliberate differences recorded.

    See reproducible development commands and validation evidence. This project implements the main functional areas of that reference, with its own typed API and explicit bounds; it does not claim full drop-in compatibility. It is a source-editing library, not JSON Schema validation, a JSON5 parser, a YAML converter or a filesystem transaction manager.

    #License and provenance

    MIT. Original, AI-assisted implementation. Microsoft node-jsonc-parser is a functional reference and test-only dependency, not embedded runtime code. NOTICE records references, runtime licenses and generated-output notices. CONTRIBUTING explains focused changes and verification.

    Diagnostic

    pub(all) struct Diagnostic {
    code : String
    offset : Int
    length : Int
    message : String
    } derive(Eq,
    Debug
    )

    Diagnostic::equal

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

    Diagnostic::not_equal

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

    Document

    pub(all) struct Document {
    text : String
    tokens : Array[Token]
    root : Node?
    diagnostics : Array[Diagnostic]
    } derive(
    Debug
    )

    Document::to_repr

    EditError

    pub(all) enum EditError {
    InvalidDocument(Array[Diagnostic])
    InvalidValue(Array[Diagnostic])
    InvalidPath(String)
    InvalidEdit(String)
    Conflict(String)
    InvalidOptions(String)
    } derive(Eq,
    Debug
    )

    EditError::equal

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

    EditError::not_equal

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

    Event

    pub(all) struct Event {
    kind : String
    offset : Int
    length : Int
    path : Array[PathSegment]
    value : String
    } derive(Eq,
    Debug
    )

    Source-ordered visitor events. value preserves numeric spelling and comment text.

    Event::equal

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

    Event::not_equal

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

    Event::to_repr

    FormatOptions

    pub(all) struct FormatOptions {
    indent_size : Int
    use_tabs : Bool
    eol : String
    final_newline : Bool
    } derive(Eq,
    Debug
    )

    FormatOptions::default

    fn FormatOptions::default() -> FormatOptions

    FormatOptions::equal

    FormatOptions::not_equal

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

    Location

    pub(all) struct Location {
    path : Array[PathSegment]
    is_property_key : Bool
    offset : Int
    line : Int
    column : Int
    } derive(Eq,
    Debug
    )

    Source positions are zero-based UTF-16 line and column indices.

    Location::equal

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

    Location::not_equal

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

    Location::to_repr

    Node

    pub(all) struct Node {
    kind : NodeKind
    offset : Int
    length : Int
    value : Json
    children : Array[Node]
    } derive(
    Debug
    )

    Scalar value is decoded Json; original numeric spelling stays in source.

    Node::to_repr

    NodeKind

    pub(all) enum NodeKind {
    ObjectNode
    ArrayNode
    PropertyNode
    StringNode
    NumberNode
    BooleanNode
    NullNode
    } derive(Eq,
    Debug
    )

    NodeKind::equal

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

    NodeKind::not_equal

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

    NodeKind::to_repr

    Operation

    pub(all) enum Operation {
    Set(Array[PathSegment], String)
    Remove(Array[PathSegment])
    Insert(Array[PathSegment], Int, String)
    Test(Array[PathSegment], String)
    } derive(Eq,
    Debug
    )

    Values are strict JSON source to preserve exact number spelling.

    Operation::equal

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

    Operation::not_equal

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

    ParseOptions

    pub(all) struct ParseOptions {
    allow_comments : Bool
    allow_trailing_comma : Bool
    allow_empty : Bool
    max_depth : Int
    max_errors : Int
    } derive(Eq,
    Debug
    )

    ParseOptions::default

    fn ParseOptions::default() -> ParseOptions

    ParseOptions::equal

    ParseOptions::not_equal

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

    PathSegment

    pub(all) enum PathSegment {
    Key(String)
    Index(Int)
    } derive(Eq,
    Debug
    )

    PathSegment::equal

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

    PathSegment::not_equal

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

    TextEdit

    pub(all) struct TextEdit {
    offset : Int
    length : Int
    content : String
    } derive(Eq,
    Debug
    )

    TextEdit::equal

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

    TextEdit::not_equal

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

    TextEdit::to_repr

    TextRange

    pub(all) struct TextRange {
    offset : Int
    length : Int
    } derive(Eq,
    Debug
    )

    TextRange::equal

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

    TextRange::not_equal

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

    Token

    pub(all) struct Token {
    kind : TokenKind
    offset : Int
    length : Int
    value : String
    error : String?
    } derive(Eq,
    Debug
    )

    Token::equal

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

    Token::not_equal

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

    Token::to_repr

    TokenKind

    pub(all) enum TokenKind {
    OpenBrace
    CloseBrace
    OpenBracket
    CloseBracket
    Colon
    Comma
    StringToken
    NumberToken
    TrueToken
    FalseToken
    NullToken
    LineComment
    BlockComment
    Whitespace
    LineBreak
    Unknown
    Eof
    } derive(Eq,
    Debug
    )

    All offsets and lengths are UTF-16 code units, on every backend.

    TokenKind::equal

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

    TokenKind::not_equal

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

    Transaction

    pub(all) struct Transaction {
    text : String
    edits : Array[TextEdit]
    applied : Int
    } derive(Eq,
    Debug
    )

    Transaction::equal

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

    Transaction::not_equal

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

    apply_edits

    fn apply_edits(text : String, edits : Array[TextEdit]) -> Result[String, EditError]

    Apply independent edits whose offsets refer to the original source. Edits may be supplied in any order; overlapping edits, duplicate insertion points, invalid ranges and boundaries inside a UTF-16 surrogate pair are rejected. The caller's source and edit array are never mutated.

    find_node

    fn find_node(root : Node, path : Array[PathSegment]) -> Node?

    Resolve an exact path. Duplicate matching keys are intentionally ambiguous.

    format

    fn format(text : String, options? : FormatOptions, range? : TextRange?) -> Result[Array[TextEdit], EditError]

    context is nevertheless computed from the complete document.

    get_location

    fn get_location(doc : Document, offset : Int) -> Result[Location, EditError]

    Empty Key("") denotes an object key slot; use is_property_key to distinguish it. At a comma or closing bracket offset, the cursor is still before that token. At a property's closing quote and in the gap before its colon, it remains a key position. After the colon it is a value position. Unclosed containers at EOF retain their current key/index slot. This single-prefix scan is linear in source units plus tokens and stores one frame per open container.

    insert

    fn insert(text : String, path : Array[PathSegment], index : Int, value : String, options? : FormatOptions) -> Result[Array[TextEdit], EditError]

    Insert before an existing array element, or append at length. Unlike modify, this shifts existing elements. The array must exist; indices are 0..length.

    modify

    fn modify(text : String, path : Array[PathSegment], value : String?, options? : FormatOptions) -> Result[Array[TextEdit], EditError]

    Set or remove a JSONC path using source-coordinate edits. None removes; missing removals are idempotent. Missing parents are created without replacing existing scalar data. Index -1 and length append; sparse arrays are rejected.

    node_at_offset

    fn node_at_offset(node : Node, offset : Int, include_right_bound? : Bool) -> Node?

    Return the deepest containing node. Ranges are half-open unless explicitly including the right endpoint; ties prefer the earlier source child.

    node_value

    fn node_value(node : Node) -> Json

    Cached JSON projection of a parsed node. Keep source spans for exact numbers; a Double estimate cannot represent every valid JSON numeric spelling.

    parse

    fn parse(text : String, options? : ParseOptions) -> Document

    Parse a tolerant, source-positioned JSONC tree. Diagnostics make partial trees unsuitable for mutation. Duplicate keys are retained, diagnosed, and project to last-key-wins Json. Depth is 1..512, diagnostics 1..1000; values <=100000.

    scan

    fn scan(text : String, ignore_trivia? : Bool) -> Array[Token]

    Lex all JSONC tokens, always ending with Eof. Trivia is retained by default. String values are decoded; all other values retain their source spelling.

    strip_comments

    fn strip_comments(text : String, replace_with_spaces? : Bool) -> String

    comment tokens (including any internal line breaks) are deleted.

    to_pointer

    fn to_pointer(path : Array[PathSegment]) -> String

    Encode a typed path as RFC 6901 JSON Pointer. Numeric object keys stay keys.

    transact

    fn transact(text : String, operations : Array[Operation], options? : FormatOptions, expected_source? : String?) -> Result[Transaction, EditError]

    Apply a sequence in memory, committing no partial result on failure. Test ignores object order and spelling of decoded strings, preserves array order, and compares numbers by their exact JSON lexeme (1 differs from 1.0). applied counts successful operations including Test and no-op removals. The final patch uses original-source coordinates and can be applied once.

    visit

    fn visit(doc : Document, callback : (Event) -> Bool) -> Int

    Trivia whitespace is omitted; comments and separators are delivered explicitly.

    Powered by MoonBit

    Site sourceReport issueModulesSkillsBuild queueStatistics

    © 2026 mooncakes.io