-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).