Skip to main content

src/molt@cst.erl

-module(molt@cst).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/molt/cst.gleam").
-export([from_document/1, to_document_version/2, to_document/1, parse_path/1, get/2, get_where/3, key_name/1, list_keys/1, list_tables/1, value_text/1, zipper_at/2, zipper_where/3, delete/2, delete_where/3, ensure/3, insert_table_node/2, insert_kv/4, move/3, rename/3, insert_array_of_tables_entry/4, replace/3, update/3, set_kv_value/2, update_where/4, leading_comments/2, set_leading_comments/3, trailing_comment/2, set_trailing_comment/3, strip_all_comments/1, document_head_comments/1, set_document_head_comments/2, document_tail_comments/1, set_document_tail_comments/2, build_array_of_tables/1, build_comment_trivia/2, build_inline_kv/2, build_kv/2, build_table/1]).
-export_type([key_position/0, entry_position/0]).

-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.

?MODULEDOC(
    " molt/cst: low-level concrete syntax tree manipulation\n"
    "\n"
    " The functions in this module query and manipulate concrete syntax tree\n"
    " nodes of a TOML document.\n"
    "\n"
    " ## `molt/cst` Functions\n"
    "\n"
    " There are six categories of functions in the `molt/cst` interface.\n"
    "\n"
    " - `conversion`: Conversion between the molt document to a bare CST tree\n"
    "   and back.\n"
    "\n"
    " - `query`: Functions to query information from the CST tree nodes.\n"
    "\n"
    " - `edit`: Functions to make edits to the CST tree. The edits produced by\n"
    "   these functions ensure local consistency but may produce a document tree\n"
    "   that is not semantically valid TOML (duplicated headers, keys that\n"
    "   conflict with a declared table, etc.).\n"
    "\n"
    " - `comments`: Functions to edit comments on CST tree nodes.\n"
    "\n"
    " - `builder`: Functions to build loose CST nodes for use with the `edit`\n"
    "   functions responsible for inserting or updating value nodes.\n"
    "\n"
    " - `advanced`: Advanced functions that return an editable zipper (cursor)\n"
    "   to be used with [`greenwood/zipper`][gz]. This is more completely\n"
    "   described in the [Repairing Invalid TOML][rit] guide.\n"
    "\n"
    " `parse_path` is uncategorized as it is a helper for translating `molt`\n"
    " path strings (`a.b.c[3].d`) into path segments for use with `molt/cst`\n"
    " functions.\n"
    "\n"
    " ## Example Document\n"
    "\n"
    " Examples in this documentation assume the following TOML document:\n"
    "\n"
    " ```toml\n"
    " # Project configuration\n"
    " title = 'molt'\n"
    " version = '1.0.0'\n"
    " enabled = true # Do we need this?\n"
    " max_retries = 3\n"
    " rating = 4.5\n"
    "\n"
    " # Inline table and inline array\n"
    " owner = { name = \\\"Austin\\\", active = true }\n"
    " project.tags = ['toml', \\\"parser\\\", 'gleam']\n"
    "\n"
    " # Implicit table: `database` is never explicitly declared\n"
    " [database.connection]\n"
    " # Default host is localhost\n"
    " host = 'localhost'\n"
    " # Default port is the postgresql port\n"
    " port = 5432\n"
    " \\\"connection options\\\" = []\n"
    "\n"
    " # Concrete table\n"
    " [settings]\n"
    " verbose = false\n"
    " timeout = 30\n"
    "\n"
    " [settings.debug]\n"
    " level = 5\n"
    "\n"
    " # Table array\n"
    " [[plugins]]\n"
    " name = 'formatter'\n"
    " priority = 1\n"
    "\n"
    " [[plugins]]\n"
    " name = 'linter'\n"
    " priority = 2\n"
    " options = { strict = true, fix = false }\n"
    "\n"
    " [[extensions]]\n"
    "\n"
    " [app.'Microsoft Word'.options]\n"
    " verbose = false\n"
    " ```\n"
    "\n"
    " All function examples are operating on the document provided as a string\n"
    " called `config`:\n"
    "\n"
    " ```gleam\n"
    " import molt\n"
    " import molt/cst\n"
    " import molt/types.{IndexSegment, KeySegment}\n"
    "\n"
    " use document <- result.try(molt.parse(config))\n"
    " let example = cst.from_document(document)\n"
    " ```\n"
    "\n"
    " ## Concrete Node Resolution\n"
    "\n"
    " The `molt/cst` functions target concrete container nodes in the syntax\n"
    " tree, and advanced functions may traverse inner values.\n"
    "\n"
    " - Concrete: implicit intermediate nodes are not reachable. In the example\n"
    "   document neither `project` nor `database` are reachable because they do\n"
    "   not exist in the syntax. Only `project.tags` and `database.connection`\n"
    "   exist as addressable nodes.\n"
    "\n"
    " - Container nodes: the main target for `molt/cst` functions are container\n"
    "   nodes: tables (`database.connection`, `settings`), array tables\n"
    "   (`plugins`), and key/value nodes (`title`, `owner`, `project.tags`,\n"
    "   etc.).\n"
    "\n"
    " - Inner value traversal: `molt/cst` functions can navigate into inline\n"
    "   table (`owner.name`) and inline array (`project.tags[-1]`) values owned\n"
    "   by key/value nodes.\n"
    "\n"
    " ## Node Preservation\n"
    "\n"
    " Modification to the CST preserves existing whitespace, comments, and\n"
    " formatting of unaffected nodes. Comments can be added, removed, or modified\n"
    " on any concrete node. Newly added nodes will have uniform formatting\n"
    " applied. If a node with comments attached to it is moved to a different\n"
    " location in the document, the comments follow that node.\n"
    "\n"
    " ## Paths\n"
    "\n"
    " `molt/cst` functions take paths as `List(PathSegment)`, built from\n"
    " `KeySegment` and `IndexSegment` values or parsed from a string with\n"
    " `parse_path`.\n"
    "\n"
    " [gz]: https://greenwood.hexdocs.pm/greenwood/zipper.html\n"
    " [rit]: https://molt.hexdocs.pm/invalid-toml.html\n"
).

-type key_position() :: kv_at_end | {before_key, binary()}.

-type entry_position() :: entry_at_end | {before_index, integer()}.

-file("src/molt/cst.gleam", 185).
?DOC(
    " Extracts the concrete syntax tree from the parsed molt document.\n"
    "\n"
    " `conversion`\n"
).
-spec from_document(molt@types:document()) -> greenwood:node_(molt@types:toml_kind()).
from_document(Doc) ->
    erlang:element(4, Doc).

-file("src/molt/cst.gleam", 208).
?DOC(
    " Converts the concrete syntax tree to a parsed TOML document using the\n"
    " specified TOML version.\n"
    "\n"
    " The tree is validated on conversion (counting any errors), so the resulting\n"
    " `Document` has an accurate `error_count` and is ready for the `molt` logical\n"
    " API. Inspect errors with `molt.has_errors` / `molt.document_errors`.\n"
    "\n"
    " `conversion`\n"
).
-spec to_document_version(
    greenwood:node_(molt@types:toml_kind()),
    molt@types:toml_version()
) -> molt@types:document().
to_document_version(Tree, Version) ->
    {document, Version, molt@internal@validate:count(Tree), Tree, none}.

-file("src/molt/cst.gleam", 196).
?DOC(
    " Converts the concrete syntax tree to a parsed TOML 1.1 document.\n"
    "\n"
    " The tree is validated on conversion (counting any errors), so the resulting\n"
    " `Document` has an accurate `error_count` and is ready for the `molt` logical\n"
    " API. Inspect errors with `molt.has_errors` / `molt.document_errors`.\n"
    "\n"
    " `conversion`\n"
).
-spec to_document(greenwood:node_(molt@types:toml_kind())) -> molt@types:document().
to_document(Tree) ->
    to_document_version(Tree, {toml_version, <<"1.1"/utf8>>}).

-file("src/molt/cst.gleam", 245).
?DOC(
    " Converts a path string into a list of path segments required by\n"
    " `molt/cst` functions, returning a `InvalidPath` if the path syntax\n"
    " is invalid.\n"
    "\n"
    " The path format is fully documented in the `molt` module.\n"
    "\n"
    " ```gleam\n"
    " parse_path(\"a.b.c.d\")\n"
    " // -> [KeySegment(\"a\"), KeySegment(\"b\"), KeySegment(\"c\"), KeySegment(\"d\")]\n"
    "\n"
    " parse_path(\"a.b.-1.d\")\n"
    " // -> [KeySegment(\"a\"), KeySegment(\"b\"), KeySegment(\"-1\"), KeySegment(\"d\")]\n"
    "\n"
    " parse_path(\"a.b.\\\"with space\\\".d\")\n"
    " // -> [KeySegment(\"a\"), KeySegment(\"b\"), KeySegment(\"with space\"), KeySegment(\"d\")]\n"
    "\n"
    " parse_path(\"a.b.\\\"\\f\\\".'\\\\f'\")\n"
    " // -> [KeySegment(\"a\"), KeySegment(\"b\"), KeySegment(\"\\f\"), KeySegment(\"\\\\f\")]\n"
    "\n"
    " parse_path(\"a.b[-1].c\")\n"
    " // -> [KeySegment(\"a\"), KeySegment(\"b\"), IndexSegment(-1), KeySegment(\"c\")]\n"
    "\n"
    " parse_path(\"a.b[3].c\")\n"
    " // -> [KeySegment(\"a\"), KeySegment(\"b\"), IndexSegment(3), KeySegment(\"c\")]\n"
    " ```\n"
).
-spec parse_path(binary()) -> {ok, list(molt@types:path_segment())} |
    {error, molt@error:molt_error()}.
parse_path(Input) ->
    molt@internal@path:parse(Input).

-file("src/molt/cst.gleam", 266).
?DOC(
    " Retrieves a child node at the given `path` relative to `node`.\n"
    "\n"
    " ```gleam\n"
    " cst.get(example, [])\n"
    " // -> the document root itself\n"
    "\n"
    " cst.get(example, [KeySegment(\"rating\")])\n"
    " // -> The `rating` key/value node from root\n"
    "\n"
    " let assert Ok(plugins2) = cst.get(example, [KeySegment(\"plugins\"), IndexSegment(1)])\n"
    " // -> The second `plugins` entry.\n"
    "\n"
    " cst.get(plugins2, [KeySegment(\"name\")])\n"
    " // -> The `name` key/value node from the second `plugins` entry.\n"
    " ```\n"
    "\n"
    " `query`\n"
).
-spec get(
    greenwood:node_(molt@types:toml_kind()),
    list(molt@types:path_segment())
) -> {ok, greenwood:node_(molt@types:toml_kind())} |
    {error, molt@error:molt_error()}.
get(Node, Segments) ->
    case Segments of
        [] ->
            {ok, Node};

        _ ->
            molt@internal@cst@query:get(Node, Segments)
    end.

-file("src/molt/cst.gleam", 286).
?DOC(
    " Look up a node at path matching a predicate. Useful for disambiguating\n"
    " duplicate keys or selecting by node kind.\n"
    "\n"
    " ```gleam\n"
    " assert cst.get_where(example, [KeySegment(\"plugins\")], fn(n) {\n"
    "   list.contains(cst.list_keys(n), \"options\")\n"
    " }) == cst.get(example, path: [KeySegment(\"plugins\"), IndexSegment(-1)])\n"
    " ```\n"
    "\n"
    " `query`\n"
).
-spec get_where(
    greenwood:node_(molt@types:toml_kind()),
    list(molt@types:path_segment()),
    fun((greenwood:node_(molt@types:toml_kind())) -> boolean())
) -> {ok, greenwood:node_(molt@types:toml_kind())} |
    {error, molt@error:molt_error()}.
get_where(Node, Segments, Predicate) ->
    case Segments of
        [] ->
            case Predicate(Node) of
                true ->
                    {ok, Node};

                false ->
                    {error, molt@error:not_found_path2(Segments, [])}
            end;

        _ ->
            gleam@result:'try'(
                molt@internal@cst@query:get_cursor_where(
                    Node,
                    Segments,
                    Predicate
                ),
                fun(Cursor) -> {ok, erlang:element(2, Cursor)} end
            )
    end.

-file("src/molt/cst.gleam", 316).
?DOC(
    " Get the key name from a key/value node.\n"
    "\n"
    " ```gleam\n"
    " let assert Ok(kv) = cst.get(example, [KeySegment(\"rating\")])\n"
    " assert Some(\"rating\") == cst.key_name(kv)\n"
    " ```\n"
    "\n"
    " `query`\n"
).
-spec key_name(greenwood:node_(molt@types:toml_kind())) -> gleam@option:option(binary()).
key_name(Kv) ->
    molt@internal@cst@elements:key_name(erlang:element(3, Kv)).

-file("src/molt/cst.gleam", 323).
?DOC(
    " List key names in a table (direct children only, no dotted key merging).\n"
    "\n"
    " `query`\n"
).
-spec list_keys(greenwood:node_(molt@types:toml_kind())) -> list(binary()).
list_keys(Table) ->
    gleam@list:filter_map(erlang:element(3, Table), fun(El) -> case El of
                {node_element, N} when erlang:element(2, N) =:= key_value ->
                    _pipe = molt@internal@cst@elements:key_name(
                        erlang:element(3, N)
                    ),
                    gleam@option:to_result(_pipe, nil);

                _ ->
                    {error, nil}
            end end).

-file("src/molt/cst.gleam", 350).
?DOC(
    " List all explicit table or array table paths that are immediate children of\n"
    " the provided `node` if it is a document or table node. Returns `None` if the\n"
    " `node` is neither the root of the tree nor a table.\n"
    "\n"
    " ```gleam\n"
    " let assert Some([\n"
    "   [\"database\", \"connection\"],\n"
    "   [\"settings\"],\n"
    "   [\"settings\", \"debug\"],\n"
    "   [\"plugins\"],\n"
    "   [\"plugins\"],\n"
    "   [\"extensions\"],\n"
    "   [\"app\", \"Microsoft Word\", \"options\"],\n"
    " ]) = cst.list_tables(example)\n"
    " ```\n"
    "\n"
    " `query`\n"
).
-spec list_tables(greenwood:node_(molt@types:toml_kind())) -> gleam@option:option(list(list(binary()))).
list_tables(Node) ->
    gleam@bool:guard(
        (erlang:element(2, Node) /= root) andalso (erlang:element(2, Node) /= table),
        none,
        fun() ->
            Tables = gleam@list:filter_map(
                erlang:element(3, Node),
                fun(El) -> case El of
                        {node_element, N} when (erlang:element(2, N) =:= table) orelse (erlang:element(
                            2,
                            N
                        ) =:= array_of_tables) ->
                            {ok,
                                molt@internal@cst@elements:extract_key_segments(
                                    erlang:element(3, N)
                                )};

                        _ ->
                            {error, nil}
                    end end
            ),
            {some, Tables}
        end
    ).

-file("src/molt/cst.gleam", 1393).
-spec emit_node_text(greenwood:node_(molt@types:toml_kind())) -> binary().
emit_node_text(Node) ->
    _pipe = erlang:element(3, Node),
    _pipe@1 = gleam@list:map(_pipe, fun(El) -> case El of
                {token_element, {token, basic_string, Text}} ->
                    <<<<"\""/utf8, Text/binary>>/binary, "\""/utf8>>;

                {token_element, {token, multiline_basic_string, Text@1}} ->
                    <<<<"\"\"\""/utf8, Text@1/binary>>/binary, "\"\"\""/utf8>>;

                {token_element, {token, multiline_basic_string_nl, Text@2}} ->
                    <<<<"\"\"\"\n"/utf8, Text@2/binary>>/binary, "\"\"\""/utf8>>;

                {token_element, {token, literal_string, Text@3}} ->
                    <<<<"'"/utf8, Text@3/binary>>/binary, "'"/utf8>>;

                {token_element, {token, multiline_literal_string, Text@4}} ->
                    <<<<"'''"/utf8, Text@4/binary>>/binary, "'''"/utf8>>;

                {token_element, {token, multiline_literal_string_nl, Text@5}} ->
                    <<<<"'''\n"/utf8, Text@5/binary>>/binary, "'''"/utf8>>;

                {token_element, {token, equals, _}} ->
                    <<"="/utf8>>;

                {token_element, {token, dot, _}} ->
                    <<"."/utf8>>;

                {token_element, {token, comma, _}} ->
                    <<","/utf8>>;

                {token_element, {token, left_bracket, _}} ->
                    <<"["/utf8>>;

                {token_element, {token, right_bracket, _}} ->
                    <<"]"/utf8>>;

                {token_element, {token, left_brace, _}} ->
                    <<"{"/utf8>>;

                {token_element, {token, right_brace, _}} ->
                    <<"}"/utf8>>;

                {token_element, T} ->
                    erlang:element(3, T);

                {node_element, N} ->
                    emit_node_text(N)
            end end),
    erlang:list_to_binary(_pipe@1).

-file("src/molt/cst.gleam", 1368).
-spec extract_value_text(list(greenwood:element(molt@types:toml_kind()))) -> binary().
extract_value_text(Children) ->
    _pipe = Children,
    _pipe@1 = gleam@list:filter_map(_pipe, fun(El) -> case El of
                {token_element, {token, whitespace, _}} ->
                    {error, nil};

                {token_element, {token, newline, _}} ->
                    {error, nil};

                {token_element, {token, comment, _}} ->
                    {error, nil};

                {token_element, {token, basic_string, Text}} ->
                    {ok, <<<<"\""/utf8, Text/binary>>/binary, "\""/utf8>>};

                {token_element, {token, multiline_basic_string, Text@1}} ->
                    {ok,
                        <<<<"\"\"\""/utf8, Text@1/binary>>/binary,
                            "\"\"\""/utf8>>};

                {token_element, {token, multiline_basic_string_nl, Text@2}} ->
                    {ok,
                        <<<<"\"\"\"\n"/utf8, Text@2/binary>>/binary,
                            "\"\"\""/utf8>>};

                {token_element, {token, literal_string, Text@3}} ->
                    {ok, <<<<"'"/utf8, Text@3/binary>>/binary, "'"/utf8>>};

                {token_element, {token, multiline_literal_string, Text@4}} ->
                    {ok, <<<<"'''"/utf8, Text@4/binary>>/binary, "'''"/utf8>>};

                {token_element, {token, multiline_literal_string_nl, Text@5}} ->
                    {ok, <<<<"'''\n"/utf8, Text@5/binary>>/binary, "'''"/utf8>>};

                {token_element, T} ->
                    {ok, erlang:element(3, T)};

                {node_element, N} ->
                    {ok, emit_node_text(N)}
            end end),
    _pipe@2 = erlang:list_to_binary(_pipe@1),
    gleam@string:trim(_pipe@2).

-file("src/molt/cst.gleam", 376).
?DOC(
    " Get the raw value text from a key/value node.\n"
    "\n"
    " ```gleam\n"
    " let assert Ok(kv) = cst.get(example, [KeySegment(\"rating\")])\n"
    " assert \"4.5\" == cst.value_text(kv)\n"
    " ```\n"
    "\n"
    " `query`\n"
).
-spec value_text(greenwood:node_(molt@types:toml_kind())) -> binary().
value_text(Kv) ->
    _pipe = molt@internal@cst@elements:value_tokens(erlang:element(3, Kv)),
    extract_value_text(_pipe).

-file("src/molt/cst.gleam", 392).
?DOC(
    " Returns a zipper focused on the first node matching the concrete path.\n"
    "\n"
    " ```gleam\n"
    " cst.zipper_at(example, [KeySegment(\"plugins\"), IndexSegment(1)])\n"
    " // -> a zipper focused on plugins[1]\n"
    "\n"
    " cst.zipper_at(example:, [KeySegment(\"database\"), KeySegment(\"connection\"), KeySegment(\"port\")])\n"
    " // -> a zipper focused on database.connection.port\n"
    " ```\n"
    "\n"
    " `advanced`\n"
).
-spec zipper_at(
    greenwood:node_(molt@types:toml_kind()),
    list(molt@types:path_segment())
) -> {ok, greenwood:zipper(molt@types:toml_kind())} |
    {error, molt@error:molt_error()}.
zipper_at(Node, Segments) ->
    molt@internal@cst@query:get_cursor(Node, Segments).

-file("src/molt/cst.gleam", 413).
?DOC(
    " Returns a zipper for the given path using a predicate to\n"
    " disambiguate duplicate keys or node kind.\n"
    "\n"
    " ```gleam\n"
    " cst.zipper_where(\n"
    "   example:,\n"
    "   [KeySegment(\"database\"), KeySegment(\"connection\"), KeySegment(\"port\")],\n"
    "   fn(n) { cst.value_text(n) == \"5432\" },\n"
    " )\n"
    "\n"
    " // -> a zipper focused on database.connection.port\n"
    " ```\n"
    "\n"
    " `advanced`\n"
).
-spec zipper_where(
    greenwood:node_(molt@types:toml_kind()),
    list(molt@types:path_segment()),
    fun((greenwood:node_(molt@types:toml_kind())) -> boolean())
) -> {ok, greenwood:zipper(molt@types:toml_kind())} |
    {error, molt@error:molt_error()}.
zipper_where(Node, Segments, Predicate) ->
    molt@internal@cst@query:get_cursor_where(Node, Segments, Predicate).

-file("src/molt/cst.gleam", 433).
?DOC(
    " Delete the node at the given path. Returns the modified root.\n"
    " Works for key/values, tables, and array of table entries.\n"
    "\n"
    " ```gleam\n"
    " cst.delete(example, [KeySegment(\"plugins\"), IndexSegment(0)])\n"
    " // -> plugins[0] (name = 'formatter') has been removed\n"
    "\n"
    " cst.delete(example, [KeySegment(\"database\"), KeySegment(\"connection\")])\n"
    " // -> All of database.connection has been removed\n"
    " ```\n"
    "\n"
    " `edit`\n"
).
-spec delete(
    greenwood:node_(molt@types:toml_kind()),
    list(molt@types:path_segment())
) -> {ok, greenwood:node_(molt@types:toml_kind())} |
    {error, molt@error:molt_error()}.
delete(Node, Segments) ->
    gleam@bool:guard(
        Segments =:= [],
        {error, {invalid_operation, <<"delete"/utf8>>, none}},
        fun() ->
            gleam@result:'try'(
                molt@internal@cst@query:get_cursor(Node, Segments),
                fun(Cursor) -> _pipe = greenwood@zipper:delete(Cursor),
                    _pipe@1 = gleam@option:map(
                        _pipe,
                        fun greenwood@zipper:unzip/1
                    ),
                    gleam@option:to_result(
                        _pipe@1,
                        {invalid_operation, <<"delete"/utf8>>, none}
                    ) end
            )
        end
    ).

-file("src/molt/cst.gleam", 461).
?DOC(
    " Delete a node at path matching a predicate.\n"
    "\n"
    " ```gleam\n"
    " cst.delete_where(\n"
    "   example,\n"
    "   [KeySegment(\"database\"), KeySegment(\"connection\"), KeySegment(\"host\")],\n"
    "   fn(n) { cst.value_text(n) == \"'prod'\"}\n"
    " )\n"
    " // -> NotFound as database.connection.host is 'localhost'\n"
    " ```\n"
    "\n"
    " `edit`\n"
).
-spec delete_where(
    greenwood:node_(molt@types:toml_kind()),
    list(molt@types:path_segment()),
    fun((greenwood:node_(molt@types:toml_kind())) -> boolean())
) -> {ok, greenwood:node_(molt@types:toml_kind())} |
    {error, molt@error:molt_error()}.
delete_where(Node, Segments, Predicate) ->
    gleam@result:'try'(
        molt@internal@cst@query:get_cursor_where(Node, Segments, Predicate),
        fun(Cursor) -> _pipe = greenwood@zipper:delete(Cursor),
            _pipe@1 = gleam@option:map(_pipe, fun greenwood@zipper:unzip/1),
            gleam@option:to_result(
                _pipe@1,
                molt@error:not_found_path2(Segments, [])
            ) end
    ).

-file("src/molt/cst.gleam", 490).
?DOC(
    " Ensure a table or array of table exists at path. Creates it if missing. The\n"
    " new declaration is placed before any existing descendant headers so that\n"
    " parent tables always precede their children.\n"
    "\n"
    " ```gleam\n"
    " cst.ensure(example, path: [KeySegment(\"my app\")], kind: types.Table)\n"
    " // -> creates a new table header [\"my app\"] at the end of the document\n"
    "\n"
    " cst.ensure(example, path: [KeySegment(\"database\")], kind: types.Table)\n"
    " // -> creates a new table header [database] before [database.connection]\n"
    " ```\n"
    "\n"
    " `edit`\n"
).
-spec ensure(
    greenwood:node_(molt@types:toml_kind()),
    list(molt@types:path_segment()),
    molt@types:toml_kind()
) -> {ok, greenwood:node_(molt@types:toml_kind())} |
    {error, molt@error:molt_error()}.
ensure(Node, Segments, Kind) ->
    Key_path = molt@internal@cst@query:collect_key_prefix(Segments),
    case molt@internal@cst@query:get_cursor(Node, Segments) of
        {ok, _} ->
            {ok, Node};

        _ ->
            case Kind of
                table ->
                    {ok, molt@internal@cst@insert:ensure_table(Node, Key_path)};

                array_of_tables ->
                    {ok,
                        molt@internal@cst@insert:ensure_array_of_tables(
                            Node,
                            Key_path
                        )};

                _ ->
                    {error,
                        {type_mismatch,
                            none,
                            <<"Table or ArrayOfTables kind"/utf8>>,
                            molt@internal@utils:toml_kind(Kind)}}
            end
    end.

-file("src/molt/cst.gleam", 523).
?DOC(
    " Inserts a table or array of tables node into the document, placing it before\n"
    " any existing descendant headers so that parent tables always precede their\n"
    " children.\n"
    "\n"
    " ```gleam\n"
    " let table = cst.build_table(path: [\"database\"])\n"
    " cst.insert_table_node(example, table)\n"
    " // -> Inserts a new table node before [database.connection]\n"
    " ```\n"
    "\n"
    " `edit`\n"
).
-spec insert_table_node(
    greenwood:node_(molt@types:toml_kind()),
    greenwood:node_(molt@types:toml_kind())
) -> {ok, greenwood:node_(molt@types:toml_kind())} |
    {error, molt@error:molt_error()}.
insert_table_node(Node, Table) ->
    {ok,
        molt@internal@cst@insert:table_ordered(
            Node,
            Table,
            molt@internal@cst@elements:extract_key_segments(
                erlang:element(3, Table)
            )
        )}.

-file("src/molt/cst.gleam", 1436).
-spec list_init_segments(list(molt@types:path_segment())) -> list(molt@types:path_segment()).
list_init_segments(L) ->
    case L of
        [] ->
            [];

        [_] ->
            [];

        _ ->
            gleam@list:take(L, erlang:length(L) - 1)
    end.

-file("src/molt/cst.gleam", 685).
?DOC(
    " Insert a key/value node into a table-like container resolved by the `into`\n"
    " path segments.\n"
    "\n"
    " The `into` target must be the document root (`[]`), a table declaration\n"
    " `[KeySegment(\"x\"), KeySegment(\"y\")]`, or a specific array of tables entry\n"
    " (`[KeySegment(\"a\"), IndexSegment(2)]`).\n"
    "\n"
    " Local consistency checks prevent the insertion of a key/value node with\n"
    " a key name already present in the target table. If a key/value node is to be\n"
    " inserted `Before` a key that does not exist, an error result will be\n"
    " returned.\n"
    "\n"
    " ```gleam\n"
    " // Append, region-aware: lands before any [sub] headers.\n"
    " insert_kv(doc, into: [KeySegment(\"database\")], kv:, at: KvAtEnd)\n"
    "\n"
    " // Before a named sibling key.\n"
    " insert_kv(doc, into: [KeySegment(\"database\")], kv:, at: BeforeKey(\"port\"))\n"
    "\n"
    " // Into a specific AoT entry.\n"
    " insert_kv(doc, into: [KeySegment(\"a\"), IndexSegment(2)], kv:, at: KvAtEnd)\n"
    " ```\n"
    "\n"
    " `edit`\n"
).
-spec insert_kv(
    greenwood:node_(molt@types:toml_kind()),
    list(molt@types:path_segment()),
    greenwood:node_(molt@types:toml_kind()),
    key_position()
) -> {ok, greenwood:node_(molt@types:toml_kind())} |
    {error, molt@error:molt_error()}.
insert_kv(Node, Segments, Kv, Position) ->
    gleam@bool:guard(
        erlang:element(2, Kv) /= key_value,
        {error,
            {type_mismatch,
                none,
                <<"key_value"/utf8>>,
                molt@internal@utils:toml_kind(erlang:element(2, Kv))}},
        fun() ->
            Focus_result = case Segments of
                [] ->
                    {ok, Node};

                _ ->
                    _pipe = molt@internal@cst@query:get_cursor(Node, Segments),
                    _pipe@1 = gleam@result:map(
                        _pipe,
                        fun(C) -> erlang:element(2, C) end
                    ),
                    gleam@result:replace_error(
                        _pipe@1,
                        molt@error:not_found_path(Segments)
                    )
            end,
            gleam@result:'try'(
                Focus_result,
                fun(Focus) ->
                    gleam@bool:guard(
                        ((erlang:element(2, Focus) /= root) andalso (erlang:element(
                            2,
                            Focus
                        )
                        /= table))
                        andalso (erlang:element(2, Focus) /= array_of_tables),
                        {error,
                            {type_mismatch,
                                none,
                                <<"table-like"/utf8>>,
                                molt@internal@utils:toml_kind(
                                    erlang:element(2, Focus)
                                )}},
                        fun() ->
                            gleam@bool:guard(
                                (erlang:element(2, Focus) =:= array_of_tables)
                                andalso (case gleam@list:last(Segments) of
                                    {ok, {key_segment, _}} ->
                                        true;

                                    _ ->
                                        false
                                end),
                                {error,
                                    {invalid_operation,
                                        <<"insert_kv"/utf8>>,
                                        {some,
                                            <<"ambiguous array_of_tables target; specify an entry index, e.g. a.b[0]"/utf8>>}}},
                                fun() ->
                                    gleam@result:'try'(
                                        case molt@internal@cst@elements:key_name(
                                            erlang:element(3, Kv)
                                        ) of
                                            {some, K} ->
                                                {ok, K};

                                            none ->
                                                {ok, <<""/utf8>>}
                                        end,
                                        fun(Key) ->
                                            Focus_key_path = molt@internal@cst@query:collect_key_prefix(
                                                Segments
                                            ),
                                            Header_key = lists:append(
                                                Focus_key_path,
                                                [Key]
                                            ),
                                            Already_kv = gleam@list:contains(
                                                list_keys(Focus),
                                                Key
                                            ),
                                            Already_header = case list_tables(
                                                Node
                                            ) of
                                                {some, Tables} ->
                                                    gleam@list:contains(
                                                        Tables,
                                                        Header_key
                                                    );

                                                none ->
                                                    false
                                            end,
                                            gleam@bool:guard(
                                                Already_kv orelse Already_header,
                                                {error,
                                                    {invalid_operation,
                                                        <<"insert_kv"/utf8>>,
                                                        {some,
                                                            <<<<"key \""/utf8,
                                                                    Key/binary>>/binary,
                                                                "\" already exists"/utf8>>}}},
                                                fun() ->
                                                    gleam@result:'try'(
                                                        case Position of
                                                            kv_at_end ->
                                                                {ok,
                                                                    kv_region_end};

                                                            {before_key, K@1} ->
                                                                case gleam@list:contains(
                                                                    list_keys(
                                                                        Focus
                                                                    ),
                                                                    K@1
                                                                ) of
                                                                    true ->
                                                                        {ok,
                                                                            {before_kv_key,
                                                                                K@1}};

                                                                    false ->
                                                                        {error,
                                                                            molt@error:not_found_path(
                                                                                lists:append(
                                                                                    Segments,
                                                                                    [{key_segment,
                                                                                            K@1}]
                                                                                )
                                                                            )}
                                                                end
                                                        end,
                                                        fun(Placement) ->
                                                            molt@internal@cst@insert:place(
                                                                Node,
                                                                Segments,
                                                                Kv,
                                                                Placement
                                                            )
                                                        end
                                                    )
                                                end
                                            )
                                        end
                                    )
                                end
                            )
                        end
                    )
                end
            )
        end
    ).

-file("src/molt/cst.gleam", 550).
?DOC(
    " Move a node from one path to another.\n"
    "\n"
    " ```gleam\n"
    " cst.move(\n"
    "   example,\n"
    "   from: [KeySegment(\"settings\"), KeySegment(\"timeout\")],\n"
    "   to: [\n"
    "     KeySegment(\"database\"),\n"
    "     KeySegment(\"connection\"),\n"
    "     KeySegment(\"connection_timeout\")\n"
    "   ],\n"
    " )\n"
    " // -> Moves settings.timeout to database.connection.connection_timeout\n"
    " ```\n"
    "\n"
    " `edit`\n"
).
-spec move(
    greenwood:node_(molt@types:toml_kind()),
    list(molt@types:path_segment()),
    list(molt@types:path_segment())
) -> {ok, greenwood:node_(molt@types:toml_kind())} |
    {error, molt@error:molt_error()}.
move(Node, From, To) ->
    gleam@result:'try'(
        get(Node, From),
        fun(Target) ->
            gleam@result:'try'(
                delete(Node, From),
                fun(Node@1) -> case erlang:element(2, Target) of
                        key_value ->
                            New_key = case gleam@list:last(To) of
                                {ok, {key_segment, Name}} ->
                                    [molt@internal@utils:quote_key(Name)];

                                _ ->
                                    molt@internal@cst@query:collect_key_prefix(
                                        To
                                    )
                            end,
                            Renamed = molt@internal@cst@elements:rewrite_kv_key_in_place(
                                Target,
                                New_key
                            ),
                            insert_kv(
                                Node@1,
                                list_init_segments(To),
                                Renamed,
                                kv_at_end
                            );

                        table ->
                            New_path = molt@internal@cst@query:collect_key_prefix(
                                To
                            ),
                            Rewritten = molt@internal@cst@builder:rewrite_header_path(
                                Target,
                                New_path
                            ),
                            {ok,
                                greenwood:append_child(
                                    Node@1,
                                    {node_element, Rewritten}
                                )};

                        array_of_tables ->
                            New_path = molt@internal@cst@query:collect_key_prefix(
                                To
                            ),
                            Rewritten = molt@internal@cst@builder:rewrite_header_path(
                                Target,
                                New_path
                            ),
                            {ok,
                                greenwood:append_child(
                                    Node@1,
                                    {node_element, Rewritten}
                                )};

                        _ ->
                            {error,
                                {type_mismatch,
                                    none,
                                    <<"key_value_node or table_node"/utf8>>,
                                    molt@internal@utils:toml_kind(
                                        erlang:element(2, Target)
                                    )}}
                    end end
            )
        end
    ).

-file("src/molt/cst.gleam", 601).
?DOC(
    " Rename the last segment of the path. Works for key/values, tables, and\n"
    " array of tables headers.\n"
    "\n"
    " ```gleam\n"
    " cst.rename(\n"
    "   example,\n"
    "   path: [KeySegment(\"plugins\"), IndexSegment(1), KeySegment(\"name\")],\n"
    "   to: \"id\",\n"
    " )\n"
    " // -> renames plugins[1].name to plugins[1].id\n"
    "\n"
    " cst.rename(\n"
    "   example,\n"
    "   path: [KeySegment(\"database\"), KeySegment(\"connection\")],\n"
    "   to: \"conn\",\n"
    " )\n"
    " // -> renames [database.connection] to [database.conn]\n"
    " ```\n"
    "\n"
    " `edit`\n"
).
-spec rename(
    greenwood:node_(molt@types:toml_kind()),
    list(molt@types:path_segment()),
    binary()
) -> {ok, greenwood:node_(molt@types:toml_kind())} |
    {error, molt@error:molt_error()}.
rename(Node, Segments, New_name) ->
    gleam@bool:guard(
        Segments =:= [],
        {error, {invalid_operation, <<"rename"/utf8>>, none}},
        fun() ->
            gleam@result:'try'(
                molt@internal@cst@query:get_cursor(Node, Segments),
                fun(Cursor) -> case erlang:element(2, Cursor) of
                        {node, key_value, [{token_element, _} | Rest], _} = Kv ->
                            Kv@1 = {node,
                                erlang:element(2, Kv),
                                [{token_element,
                                        molt@internal@utils:make_key_token(
                                            New_name
                                        )} |
                                    Rest],
                                erlang:element(4, Kv)},
                            _pipe = greenwood@zipper:set_focus(Cursor, Kv@1),
                            _pipe@1 = greenwood@zipper:unzip(_pipe),
                            {ok, _pipe@1};

                        {node, table, Children, _} = Table ->
                            {Left, Right} = gleam@list:split_while(
                                Children,
                                fun(El) -> case El of
                                        {token_element,
                                            {token, right_bracket, _}} ->
                                            false;

                                        _ ->
                                            true
                                    end end
                            ),
                            case lists:reverse(Left) of
                                [_ | Left@1] ->
                                    Children@1 = begin
                                        _pipe@2 = lists:reverse(
                                            [{token_element,
                                                    molt@internal@utils:make_key_token(
                                                        New_name
                                                    )} |
                                                Left@1]
                                        ),
                                        lists:append(_pipe@2, Right)
                                    end,
                                    Table@1 = {node,
                                        erlang:element(2, Table),
                                        Children@1,
                                        erlang:element(4, Table)},
                                    _pipe@3 = greenwood@zipper:set_focus(
                                        Cursor,
                                        Table@1
                                    ),
                                    _pipe@4 = greenwood@zipper:unzip(_pipe@3),
                                    {ok, _pipe@4};

                                _ ->
                                    {error,
                                        {type_mismatch,
                                            none,
                                            <<"key_value or table"/utf8>>,
                                            molt@internal@utils:toml_kind(
                                                erlang:element(
                                                    2,
                                                    erlang:element(2, Cursor)
                                                )
                                            )}}
                            end;

                        {node, array_of_tables, Children, _} = Table ->
                            {Left, Right} = gleam@list:split_while(
                                Children,
                                fun(El) -> case El of
                                        {token_element,
                                            {token, right_bracket, _}} ->
                                            false;

                                        _ ->
                                            true
                                    end end
                            ),
                            case lists:reverse(Left) of
                                [_ | Left@1] ->
                                    Children@1 = begin
                                        _pipe@2 = lists:reverse(
                                            [{token_element,
                                                    molt@internal@utils:make_key_token(
                                                        New_name
                                                    )} |
                                                Left@1]
                                        ),
                                        lists:append(_pipe@2, Right)
                                    end,
                                    Table@1 = {node,
                                        erlang:element(2, Table),
                                        Children@1,
                                        erlang:element(4, Table)},
                                    _pipe@3 = greenwood@zipper:set_focus(
                                        Cursor,
                                        Table@1
                                    ),
                                    _pipe@4 = greenwood@zipper:unzip(_pipe@3),
                                    {ok, _pipe@4};

                                _ ->
                                    {error,
                                        {type_mismatch,
                                            none,
                                            <<"key_value or table"/utf8>>,
                                            molt@internal@utils:toml_kind(
                                                erlang:element(
                                                    2,
                                                    erlang:element(2, Cursor)
                                                )
                                            )}}
                            end;

                        _ ->
                            {error,
                                {type_mismatch,
                                    none,
                                    <<"key_value or table"/utf8>>,
                                    molt@internal@utils:toml_kind(
                                        erlang:element(
                                            2,
                                            erlang:element(2, Cursor)
                                        )
                                    )}}
                    end end
            )
        end
    ).

-file("src/molt/cst.gleam", 796).
?DOC(
    " Insert an array of tables entry node into an existing `[[array.of.tables]]`\n"
    " group.\n"
    "\n"
    " The array of tables entry must be located using `into` of only `KeySegment`s\n"
    " to find the appropriate siblings to place the table in the correct location.\n"
    "\n"
    " ```gleam\n"
    " let entry = cst.build_array_of_tables([\"plugins\"])\n"
    "\n"
    " // Append after the last [[plugins]].\n"
    " insert_array_of_tables_entry(\n"
    "   doc,\n"
    "   into: [KeySegment(\"plugins\")],\n"
    "   entry:,\n"
    "   at: EntryAtEnd\n"
    " )\n"
    "\n"
    " // Insert before the 2nd [[a.b]] entry.\n"
    " insert_array_of_tables_entry(\n"
    "   doc,\n"
    "   into: [KeySegment(\"a\"), KeySegment(\"b\")],\n"
    "   entry:,\n"
    "   at: BeforeIndex(2)\n"
    " )\n"
    " ```\n"
    "\n"
    " `edit`\n"
).
-spec insert_array_of_tables_entry(
    greenwood:node_(molt@types:toml_kind()),
    list(molt@types:path_segment()),
    greenwood:node_(molt@types:toml_kind()),
    entry_position()
) -> {ok, greenwood:node_(molt@types:toml_kind())} |
    {error, molt@error:molt_error()}.
insert_array_of_tables_entry(Node, Segments, Entry, Position) ->
    gleam@bool:guard(gleam@list:any(Segments, fun(Seg) -> case Seg of
                    {index_segment, _} ->
                        true;

                    _ ->
                        false
                end end), {error,
            {invalid_operation,
                <<"insert_array_of_tables_entry"/utf8>>,
                {some, <<"target path must not contain an index"/utf8>>}}}, fun(
            
        ) ->
            gleam@bool:guard(
                erlang:element(2, Entry) /= array_of_tables,
                {error,
                    {type_mismatch,
                        none,
                        <<"array_of_tables"/utf8>>,
                        molt@internal@utils:toml_kind(erlang:element(2, Entry))}},
                fun() ->
                    Scope_keys = molt@internal@cst@query:collect_key_prefix(
                        Segments
                    ),
                    Entry_keys = molt@internal@cst@elements:extract_key_segments(
                        erlang:element(3, Entry)
                    ),
                    gleam@bool:guard(
                        Entry_keys /= Scope_keys,
                        {error,
                            {invalid_operation,
                                <<"insert_array_of_tables_entry"/utf8>>,
                                {some,
                                    <<<<<<<<"entry path ["/utf8,
                                                    (gleam@string:join(
                                                        Entry_keys,
                                                        <<", "/utf8>>
                                                    ))/binary>>/binary,
                                                "] does not match target segments ["/utf8>>/binary,
                                            (gleam@string:join(
                                                Scope_keys,
                                                <<", "/utf8>>
                                            ))/binary>>/binary,
                                        "]"/utf8>>}}},
                        fun() ->
                            Family_members = gleam@list:filter(
                                erlang:element(3, Node),
                                fun(El) -> case El of
                                        {node_element, N} when erlang:element(
                                            2,
                                            N
                                        ) =:= array_of_tables ->
                                            molt@internal@cst@elements:extract_key_segments(
                                                erlang:element(3, N)
                                            )
                                            =:= Scope_keys;

                                        _ ->
                                            false
                                    end end
                            ),
                            Collision_table = gleam@list:any(
                                erlang:element(3, Node),
                                fun(El@1) -> case El@1 of
                                        {node_element, N@1} when erlang:element(
                                            2,
                                            N@1
                                        ) =:= table ->
                                            molt@internal@cst@elements:extract_key_segments(
                                                erlang:element(3, N@1)
                                            )
                                            =:= Scope_keys;

                                        _ ->
                                            false
                                    end end
                            ),
                            Collision_kv = gleam@list:any(
                                erlang:element(3, Node),
                                fun(El@2) -> case El@2 of
                                        {node_element, N@2} when erlang:element(
                                            2,
                                            N@2
                                        ) =:= key_value ->
                                            molt@internal@cst@elements:key_name(
                                                erlang:element(3, N@2)
                                            )
                                            =:= {some,
                                                gleam@string:join(
                                                    Scope_keys,
                                                    <<"."/utf8>>
                                                )};

                                        _ ->
                                            false
                                    end end
                            ),
                            gleam@bool:guard(
                                Collision_table orelse Collision_kv,
                                {error,
                                    {invalid_operation,
                                        <<"insert_array_of_tables_entry"/utf8>>,
                                        {some,
                                            <<<<"["/utf8,
                                                    (gleam@string:join(
                                                        Scope_keys,
                                                        <<"."/utf8>>
                                                    ))/binary>>/binary,
                                                "] is not an array of tables"/utf8>>}}},
                                fun() ->
                                    gleam@bool:guard(
                                        gleam@list:is_empty(Family_members),
                                        {error,
                                            molt@error:not_found_path(Segments)},
                                        fun() ->
                                            Count = erlang:length(
                                                Family_members
                                            ),
                                            gleam@result:'try'(case Position of
                                                    entry_at_end ->
                                                        {ok,
                                                            {family_scope_end,
                                                                Scope_keys}};

                                                    {before_index, I} ->
                                                        case molt@internal@utils:resolve_insert_position(
                                                            I,
                                                            Count
                                                        ) of
                                                            {ok, Resolved} when Resolved =:= Count ->
                                                                {ok,
                                                                    {family_scope_end,
                                                                        Scope_keys}};

                                                            {ok, Resolved@1} ->
                                                                {ok,
                                                                    {before_family_index,
                                                                        Scope_keys,
                                                                        Resolved@1}};

                                                            {error, nil} ->
                                                                {error,
                                                                    {index_out_of_range,
                                                                        molt@internal@utils:path_to_string(
                                                                            Segments
                                                                        ),
                                                                        I,
                                                                        Count}}
                                                        end
                                                end, fun(Placement) ->
                                                    molt@internal@cst@insert:place(
                                                        Node,
                                                        [],
                                                        Entry,
                                                        Placement
                                                    )
                                                end)
                                        end
                                    )
                                end
                            )
                        end
                    )
                end
            )
        end).

-file("src/molt/cst.gleam", 920).
?DOC(
    " Replace the node at path with a new node.\n"
    "\n"
    " ```gleam\n"
    " let assert Ok(existing) =\n"
    "   cst.get(example, path: [\n"
    "     KeySegment(\"plugins\"),\n"
    "     IndexSegment(0),\n"
    "     KeySegment(\"priority\"),\n"
    "   ])\n"
    "\n"
    " let assert Ok(new_kv) =\n"
    "   cst.set_kv_value(kv: existing, value: value.to_cst(value.int(10)))\n"
    " cst.replace(\n"
    "   example,\n"
    "   path: [KeySegment(\"plugins\"), IndexSegment(0), KeySegment(\"priority\")],\n"
    "   new: new_kv,\n"
    " )\n"
    " // -> changes plugins[0].priority to 10\n"
    " ```\n"
    "\n"
    " `edit`\n"
).
-spec replace(
    greenwood:node_(molt@types:toml_kind()),
    list(molt@types:path_segment()),
    greenwood:node_(molt@types:toml_kind())
) -> {ok, greenwood:node_(molt@types:toml_kind())} |
    {error, molt@error:molt_error()}.
replace(Node, Segments, New) ->
    case Segments of
        [] ->
            {ok, New};

        _ ->
            gleam@result:'try'(
                molt@internal@cst@query:get_cursor(Node, Segments),
                fun(Cursor) -> _pipe = greenwood@zipper:set_focus(Cursor, New),
                    _pipe@1 = greenwood@zipper:unzip(_pipe),
                    {ok, _pipe@1} end
            )
    end.

-file("src/molt/cst.gleam", 956).
?DOC(
    " Update the node at path via a transform function.\n"
    "\n"
    " ```gleam\n"
    " cst.update(example,\n"
    "   path: [\n"
    "     KeySegment(\"database\"),\n"
    "     KeySegment(\"connection\"),\n"
    "     KeySegment(\"port\")\n"
    "   ],\n"
    "   with: fn(kv) {\n"
    "     let assert Ok(updated) =\n"
    "       cst.set_kv_value(kv:, value: value.to_cst(value.int(9090)))\n"
    "     updated\n"
    "   }\n"
    " )\n"
    " // -> Updates database.connection.port to 9090\n"
    " ```\n"
    "\n"
    " `edit`\n"
).
-spec update(
    greenwood:node_(molt@types:toml_kind()),
    list(molt@types:path_segment()),
    fun((greenwood:node_(molt@types:toml_kind())) -> greenwood:node_(molt@types:toml_kind()))
) -> {ok, greenwood:node_(molt@types:toml_kind())} |
    {error, molt@error:molt_error()}.
update(Node, Segments, Transform) ->
    gleam@result:'try'(
        molt@internal@cst@query:get_cursor(Node, Segments),
        fun(Cursor) -> _pipe = greenwood@zipper:map_focus(Cursor, Transform),
            _pipe@1 = greenwood@zipper:unzip(_pipe),
            {ok, _pipe@1} end
    ).

-file("src/molt/cst.gleam", 1421).
-spec replace_value_element(
    list(greenwood:element(molt@types:toml_kind())),
    greenwood:element(molt@types:toml_kind())
) -> list(greenwood:element(molt@types:toml_kind())).
replace_value_element(Children, New_value) ->
    case molt@internal@cst@elements:split_at_equals(Children) of
        {Prefix, [Eq | After_eq]} ->
            {Pre_ws, Value_and_rest} = molt@internal@cst@elements:split_leading_ws(
                After_eq
            ),
            {_, Trailing} = molt@internal@cst@elements:split_before_trivia(
                Value_and_rest
            ),
            lists:append([Prefix, [Eq], Pre_ws, [New_value], Trailing]);

        {_, []} ->
            Children
    end.

-file("src/molt/cst.gleam", 991).
?DOC(
    " Replace the value of a key/value pair node, preserving the key, surrounding\n"
    " whitespace, and any attached comments.\n"
    "\n"
    " The `value` is a CST element produced from a `value.Value` with\n"
    " `value.to_cst`, so the replacement is a correctly-typed value node (integer,\n"
    " string, array, inline table, etc.). Build it from a `value.Value` rather than\n"
    " from raw text.\n"
    "\n"
    " ```gleam\n"
    " import molt/value\n"
    "\n"
    " let assert Ok(existing) =\n"
    "   cst.get(example, path: [\n"
    "     KeySegment(\"plugins\"),\n"
    "     IndexSegment(0),\n"
    "     KeySegment(\"priority\"),\n"
    "   ])\n"
    "\n"
    " let assert Ok(new_kv) =\n"
    "   cst.set_kv_value(kv: existing, value: value.to_cst(value.int(10)))\n"
    " ```\n"
    "\n"
    " `edit`\n"
).
-spec set_kv_value(
    greenwood:node_(molt@types:toml_kind()),
    greenwood:element(molt@types:toml_kind())
) -> {ok, greenwood:node_(molt@types:toml_kind())} |
    {error, molt@error:molt_error()}.
set_kv_value(Kv, Value) ->
    case erlang:element(2, Kv) of
        key_value ->
            {ok,
                {node,
                    erlang:element(2, Kv),
                    replace_value_element(erlang:element(3, Kv), Value),
                    erlang:element(4, Kv)}};

        _ ->
            {error,
                {type_mismatch,
                    none,
                    <<"key_value"/utf8>>,
                    molt@internal@utils:toml_kind(erlang:element(2, Kv))}}
    end.

-file("src/molt/cst.gleam", 1029).
?DOC(
    " Update a node at path matching a predicate via a transform function.\n"
    "\n"
    " ```gleam\n"
    " cst.update_where(\n"
    "   example,\n"
    "   path: [KeySegment(\"database\"), KeySegment(\"connection\"), KeySegment(\"port\")],\n"
    "   where: fn(n) { cst.value_text(n) == \"9999\" },\n"
    "   with: fn(n) {\n"
    "     let assert Ok(updated) =\n"
    "       cst.set_kv_value(kv: n, value: value.to_cst(value.int(9090)))\n"
    "     updated\n"
    "   },\n"
    " )\n"
    " // -> Returns not found because database.connection.port is 5432, not 9999.\n"
    " ```\n"
    "\n"
    " `edit`\n"
).
-spec update_where(
    greenwood:node_(molt@types:toml_kind()),
    list(molt@types:path_segment()),
    fun((greenwood:node_(molt@types:toml_kind())) -> boolean()),
    fun((greenwood:node_(molt@types:toml_kind())) -> greenwood:node_(molt@types:toml_kind()))
) -> {ok, greenwood:node_(molt@types:toml_kind())} |
    {error, molt@error:molt_error()}.
update_where(Node, Segments, Predicate, Transform) ->
    gleam@result:'try'(
        molt@internal@cst@query:get_cursor_where(Node, Segments, Predicate),
        fun(Cursor) -> _pipe = greenwood@zipper:map_focus(Cursor, Transform),
            _pipe@1 = greenwood@zipper:unzip(_pipe),
            {ok, _pipe@1} end
    ).

-file("src/molt/cst.gleam", 1464).
-spec extract_leading_comments(greenwood:node_(molt@types:toml_kind())) -> list(binary()).
extract_leading_comments(Node) ->
    case erlang:element(4, Node) of
        bare ->
            [];

        {trivia, Leading, _} ->
            gleam@list:filter_map(
                Leading,
                fun(T) -> case erlang:element(2, T) of
                        comment ->
                            {ok, erlang:element(3, T)};

                        _ ->
                            {error, nil}
                    end end
            )
    end.

-file("src/molt/cst.gleam", 1057).
?DOC(
    " Get leading comments attached to the node at path.\n"
    "\n"
    " ```gleam\n"
    " cst.leading_comments(\n"
    "   example,\n"
    "   [KeySegment(\"database\"), KeySegment(\"connection\"), KeySegment(\"host\")]\n"
    " )\n"
    " // -> Ok([\"# Default host is localhost\"])\n"
    " ```\n"
    "\n"
    " `comments`\n"
).
-spec leading_comments(
    greenwood:node_(molt@types:toml_kind()),
    list(molt@types:path_segment())
) -> {ok, list(binary())} | {error, molt@error:molt_error()}.
leading_comments(Node, Segments) ->
    gleam@result:'try'(
        get(Node, Segments),
        fun(Target) -> {ok, extract_leading_comments(Target)} end
    ).

-file("src/molt/cst.gleam", 1363).
-spec normalize_comment(binary()) -> binary().
normalize_comment(Text) ->
    gleam@bool:guard(
        gleam_stdlib:string_starts_with(Text, <<"#"/utf8>>),
        Text,
        fun() -> <<"# "/utf8, Text/binary>> end
    ).

-file("src/molt/cst.gleam", 1334).
-spec apply_leading_comments(
    greenwood:node_(molt@types:toml_kind()),
    list(binary())
) -> greenwood:node_(molt@types:toml_kind()).
apply_leading_comments(Node, Comments) ->
    Comment_tokens = gleam@list:flat_map(
        Comments,
        fun(Text) ->
            [{token, comment, normalize_comment(Text)},
                {token, newline, <<""/utf8>>}]
        end
    ),
    Prefix = case erlang:element(4, Node) of
        bare ->
            [];

        {trivia, Leading, _} ->
            gleam@list:take_while(
                Leading,
                fun(T) ->
                    (erlang:element(2, T) =:= newline) orelse (erlang:element(
                        2,
                        T
                    )
                    =:= whitespace)
                end
            )
    end,
    Leading@1 = lists:append(Prefix, Comment_tokens),
    New_trivia = case erlang:element(4, Node) of
        bare ->
            {trivia, Leading@1, []};

        {trivia, _, Trailing} ->
            {trivia, Leading@1, Trailing}
    end,
    {node, erlang:element(2, Node), erlang:element(3, Node), New_trivia}.

-file("src/molt/cst.gleam", 1077).
?DOC(
    " Set leading comments on the node at path.\n"
    "\n"
    " ```gleam\n"
    " cst.set_leading_comments(\n"
    "   example,\n"
    "   path: [KeySegment(\"database\"), KeySegment(\"connection\"), KeySegment(\"port\")],\n"
    "   comments: [\"Listen port\"],\n"
    " )\n"
    " // -> Adds \"# Listen port\" before database.connection.port\n"
    " ```\n"
    "\n"
    " `comments`\n"
).
-spec set_leading_comments(
    greenwood:node_(molt@types:toml_kind()),
    list(molt@types:path_segment()),
    list(binary())
) -> {ok, greenwood:node_(molt@types:toml_kind())} |
    {error, molt@error:molt_error()}.
set_leading_comments(Node, Segments, Cmts) ->
    update(
        Node,
        Segments,
        fun(Target) -> apply_leading_comments(Target, Cmts) end
    ).

-file("src/molt/cst.gleam", 1477).
-spec extract_trailing_comment(greenwood:node_(molt@types:toml_kind())) -> gleam@option:option(binary()).
extract_trailing_comment(Node) ->
    case erlang:element(4, Node) of
        bare ->
            none;

        {trivia, _, Trailing} ->
            _pipe = gleam@list:find_map(
                Trailing,
                fun(T) -> case erlang:element(2, T) of
                        comment ->
                            {ok, erlang:element(3, T)};

                        _ ->
                            {error, nil}
                    end end
            ),
            gleam@option:from_result(_pipe)
    end.

-file("src/molt/cst.gleam", 1095).
?DOC(
    " Get the trailing comment on the node at path, if any.\n"
    "\n"
    " ```gleam\n"
    " cst.trailing_comment(example, [KeySegment(\"enabled\")])\n"
    " // -> Ok(Some(\"# Do we need this?\"))\n"
    " ```\n"
    "\n"
    " `comments`\n"
).
-spec trailing_comment(
    greenwood:node_(molt@types:toml_kind()),
    list(molt@types:path_segment())
) -> {ok, gleam@option:option(binary())} | {error, molt@error:molt_error()}.
trailing_comment(Node, Segments) ->
    gleam@result:'try'(
        get(Node, Segments),
        fun(Target) -> {ok, extract_trailing_comment(Target)} end
    ).

-file("src/molt/cst.gleam", 1491).
-spec apply_trailing_comment(
    greenwood:node_(molt@types:toml_kind()),
    gleam@option:option(binary())
) -> greenwood:node_(molt@types:toml_kind()).
apply_trailing_comment(Node, Comment) ->
    Trailing = case Comment of
        none ->
            [];

        {some, Text} ->
            [{token, whitespace, <<" "/utf8>>},
                {token, comment, normalize_comment(Text)}]
    end,
    New_trivia = case erlang:element(4, Node) of
        bare ->
            {trivia, [], Trailing};

        {trivia, Leading, _} ->
            {trivia, Leading, Trailing}
    end,
    {node, erlang:element(2, Node), erlang:element(3, Node), New_trivia}.

-file("src/molt/cst.gleam", 1119).
?DOC(
    " Set or clear the trailing comment on the node at path. Pass `None` to remove\n"
    " an existing trailing comment.\n"
    "\n"
    " ```gleam\n"
    " cst.set_trailing_comment(example, [KeySegment(\"rating\")], Some(\"Higher!\"))\n"
    " // -> Adds \"# Higher!\" to rating\n"
    "\n"
    " cst.set_trailing_comment(\n"
    "   example,\n"
    "   [KeySegment(\"enabled\")],\n"
    "   None\n"
    " )\n"
    " // -> Removes the comment from enabled.\n"
    " ```\n"
    "\n"
    " `comments`\n"
).
-spec set_trailing_comment(
    greenwood:node_(molt@types:toml_kind()),
    list(molt@types:path_segment()),
    gleam@option:option(binary())
) -> {ok, greenwood:node_(molt@types:toml_kind())} |
    {error, molt@error:molt_error()}.
set_trailing_comment(Node, Segments, Comment) ->
    update(
        Node,
        Segments,
        fun(_capture) -> apply_trailing_comment(_capture, Comment) end
    ).

-file("src/molt/cst.gleam", 1444).
?DOC(" Strip all comments from the document\n").
-spec strip_comments(greenwood:node_(molt@types:toml_kind())) -> greenwood:node_(molt@types:toml_kind()).
strip_comments(Node) ->
    Trivia = case erlang:element(4, Node) of
        bare ->
            bare;

        {trivia, Leading, Trailing} ->
            {trivia,
                gleam@list:filter(
                    Leading,
                    fun(T) -> erlang:element(2, T) /= comment end
                ),
                gleam@list:filter(
                    Trailing,
                    fun(T@1) -> erlang:element(2, T@1) /= comment end
                )}
    end,
    Children = gleam@list:filter_map(
        erlang:element(3, Node),
        fun(El) -> case El of
                {token_element, {token, comment, _}} ->
                    {error, nil};

                {node_element, N} ->
                    {ok, {node_element, strip_comments(N)}};

                _ ->
                    {ok, El}
            end end
    ),
    {node, erlang:element(2, Node), Children, Trivia}.

-file("src/molt/cst.gleam", 1135).
?DOC(
    " Recursively strip all comments from the tree.\n"
    "\n"
    " ```gleam\n"
    " cst.strip_all_comments(example)\n"
    " // -> Removes all leading and trailing comments from the tree\n"
    " ```\n"
    "\n"
    " `comments`\n"
).
-spec strip_all_comments(greenwood:node_(molt@types:toml_kind())) -> greenwood:node_(molt@types:toml_kind()).
strip_all_comments(Node) ->
    strip_comments(Node).

-file("src/molt/cst.gleam", 1144).
?DOC(
    " Read the document-head comments: the Root node's own leading-trivia comment\n"
    " lines, returned verbatim (with the leading `#`). These are the comments that\n"
    " belong to the file rather than to any one statement.\n"
    "\n"
    " `comments`\n"
).
-spec document_head_comments(greenwood:node_(molt@types:toml_kind())) -> list(binary()).
document_head_comments(Tree) ->
    extract_leading_comments(Tree).

-file("src/molt/cst.gleam", 1155).
?DOC(
    " Replace the document-head comments on the Root node's leading trivia,\n"
    " preserving a leading BOM. An empty list clears the comments (keeping the\n"
    " BOM). The blank line that separates the head comment from the first statement\n"
    " is added at emit time (see `emitter.ensure_head_separation`), so it tracks\n"
    " the document's content and line-ending style rather than being baked in here.\n"
    "\n"
    " `comments`\n"
).
-spec set_document_head_comments(
    greenwood:node_(molt@types:toml_kind()),
    list(binary())
) -> greenwood:node_(molt@types:toml_kind()).
set_document_head_comments(Tree, Comments) ->
    {Leading@1, Trailing@1} = case erlang:element(4, Tree) of
        bare ->
            {[], []};

        {trivia, Leading, Trailing} ->
            {Leading, Trailing}
    end,
    Bom = gleam@list:take_while(
        Leading@1,
        fun(T) -> erlang:element(2, T) =:= bom end
    ),
    Body = gleam@list:flat_map(
        Comments,
        fun(Text) ->
            [{token, comment, normalize_comment(Text)},
                {token, newline, <<""/utf8>>}]
        end
    ),
    {node,
        erlang:element(2, Tree),
        erlang:element(3, Tree),
        {trivia, lists:append(Bom, Body), Trailing@1}}.

-file("src/molt/cst.gleam", 1233).
-spec find_postscript(greenwood:node_(molt@types:toml_kind())) -> gleam@option:option(greenwood:node_(molt@types:toml_kind())).
find_postscript(Tree) ->
    _pipe = gleam@list:find_map(erlang:element(3, Tree), fun(El) -> case El of
                {node_element, N} when erlang:element(2, N) =:= post_script ->
                    {ok, N};

                _ ->
                    {error, nil}
            end end),
    gleam@option:from_result(_pipe).

-file("src/molt/cst.gleam", 1182).
?DOC(
    " Read the document-tail comments: the leading-trivia comment lines of the\n"
    " `PostScript` tombstone, if the document has one (it has one exactly when\n"
    " trivia dangles after the final statement). Returns `[]` otherwise.\n"
    "\n"
    " `comments`\n"
).
-spec document_tail_comments(greenwood:node_(molt@types:toml_kind())) -> list(binary()).
document_tail_comments(Tree) ->
    case find_postscript(Tree) of
        {some, Ps} ->
            extract_leading_comments(Ps);

        none ->
            []
    end.

-file("src/molt/cst.gleam", 1194).
?DOC(
    " Replace the document-tail comments. A non-empty list materializes (or\n"
    " updates) the `PostScript` tombstone as the document's last child; an empty\n"
    " list drops the tombstone entirely so no empty node lingers.\n"
    "\n"
    " `comments`\n"
).
-spec set_document_tail_comments(
    greenwood:node_(molt@types:toml_kind()),
    list(binary())
) -> greenwood:node_(molt@types:toml_kind()).
set_document_tail_comments(Tree, Comments) ->
    Without = gleam@list:filter(erlang:element(3, Tree), fun(El) -> case El of
                {node_element, N} when erlang:element(2, N) =:= post_script ->
                    false;

                _ ->
                    true
            end end),
    case Comments of
        [] ->
            {node, erlang:element(2, Tree), Without, erlang:element(4, Tree)};

        _ ->
            Body = gleam@list:flat_map(
                Comments,
                fun(Text) ->
                    [{token, comment, normalize_comment(Text)},
                        {token, newline, <<""/utf8>>}]
                end
            ),
            Leading = [{token, newline, <<""/utf8>>} | Body],
            Ps = {node, post_script, [], {trivia, Leading, []}},
            {node,
                erlang:element(2, Tree),
                lists:append(Without, [{node_element, Ps}]),
                erlang:element(4, Tree)}
    end.

-file("src/molt/cst.gleam", 1251).
?DOC(
    " Build an empty array of tables header node (`[[path.to.table]]\\n`).\n"
    "\n"
    " ```gleam\n"
    " cst.build_array_of_tables([\"plugins\"])  // -> [[plugins]]\n"
    " cst.build_array_of_tables([\"app\", \"hooks\"])  // -> [[app.hooks]]\n"
    " ```\n"
    "\n"
    " `builder`\n"
).
-spec build_array_of_tables(list(binary())) -> greenwood:node_(molt@types:toml_kind()).
build_array_of_tables(Path) ->
    _pipe = molt@internal@cst@builder:build_empty_array_of_tables(Path),
    molt@internal@cst@builder:drop_leading_newlines(_pipe).

-file("src/molt/cst.gleam", 1271).
?DOC(
    " Build comment trivia for attaching to a node via `greenwood.set_trivia` or\n"
    " similar. Leading comments appear on lines before the node; the trailing\n"
    " comment appears on the same line after the value.\n"
    "\n"
    " ```gleam\n"
    " cst.build_comment_trivia(\n"
    "   leading: [\"# Section start\"],\n"
    "   trailing: Some(\"inline note\"),\n"
    " )\n"
    " ```\n"
    "\n"
    " `builder`\n"
).
-spec build_comment_trivia(list(binary()), gleam@option:option(binary())) -> greenwood:trivia(molt@types:toml_kind()).
build_comment_trivia(Leading, Trailing) ->
    Leading@1 = gleam@list:flat_map(
        Leading,
        fun(Text) ->
            [{token, comment, normalize_comment(Text)},
                {token, newline, <<""/utf8>>}]
        end
    ),
    Trailing@1 = case Trailing of
        none ->
            [];

        {some, Text@1} ->
            [{token, whitespace, <<" "/utf8>>},
                {token, comment, normalize_comment(Text@1)}]
    end,
    {trivia, Leading@1, Trailing@1}.

-file("src/molt/cst.gleam", 1295).
?DOC(
    " Build a key/value node for use inside inline tables (no trailing newline).\n"
    "\n"
    " `builder`\n"
).
-spec build_inline_kv(binary(), greenwood:element(molt@types:toml_kind())) -> greenwood:node_(molt@types:toml_kind()).
build_inline_kv(Key, Value) ->
    molt@internal@cst@builder:build_inline_kv(
        molt@internal@utils:quote_key(Key),
        Value
    ).

-file("src/molt/cst.gleam", 1312).
?DOC(
    " Build a key/value node with standard formatting (`key = value\\n`).\n"
    "\n"
    " The key will be quoted if necessary. The value should be produced via\n"
    " `value.to_cst`.\n"
    "\n"
    " ```gleam\n"
    " cst.build_kv(key: \"host\", value: value.to_cst(value.string(\"localhost\")))\n"
    " ```\n"
    "\n"
    " `builder`\n"
).
-spec build_kv(binary(), greenwood:element(molt@types:toml_kind())) -> greenwood:node_(molt@types:toml_kind()).
build_kv(Key, Value) ->
    molt@internal@cst@builder:build_kv_node(
        molt@internal@utils:quote_key(Key),
        Value
    ).

-file("src/molt/cst.gleam", 1327).
?DOC(
    " Build an empty table header node (`[path.to.table]\\n`).\n"
    "\n"
    " ```gleam\n"
    " cst.build_table([\"settings\"])  // -> [settings]\n"
    " cst.build_table([\"database\", \"connection\"])  // -> [database.connection]\n"
    " ```\n"
    "\n"
    " `builder`\n"
).
-spec build_table(list(binary())) -> greenwood:node_(molt@types:toml_kind()).
build_table(Path) ->
    _pipe = molt@internal@cst@builder:build_empty_table(Path),
    molt@internal@cst@builder:drop_leading_newlines(_pipe).