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