-module(magic_string). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/magic_string.gleam"). -export([describe_error/1, new/1, overwrite/4, remove/3, append_left/3, append_right/3, prepend/2, append/2, to_string/1, generate_map/3, default_map_options/0, bundle/0, add_source/4, bundle_to_string/1, bundle_generate_map/2]). -export_type([edit/0, magic_string/0, piece/0, edit_error/0, map_options/0, bundle_source/0, bundle/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( " String editing by byte offset, with a source map as a byproduct.\n" "\n" " Keep the original source, record overwrite/remove/insert edits keyed by\n" " UTF-8 byte offset, then `to_string` and `generate_map` replay the log.\n" " Surviving slices of the original produce mapping segments; inserted text\n" " does not.\n" ). -type edit() :: {edit, integer(), integer(), binary()}. -opaque magic_string() :: {magic_string, binary(), binary(), binary(), gleam@dict:dict(integer(), edit()), gleam@dict:dict(integer(), binary()), gleam@dict:dict(integer(), binary())}. -type piece() :: {piece, binary(), gleam@option:option(integer())}. -type edit_error() :: {overlap, {integer(), integer()}, {integer(), integer()}} | {swallowed_insert, integer(), {integer(), integer()}} | {out_of_bounds, integer(), integer()} | {inverted_range, integer(), integer()}. -type map_options() :: {map_options, gleam@option:option(binary()), gleam@option:option(binary()), boolean()}. -type bundle_source() :: {bundle_source, binary(), binary(), magic_string()}. -opaque bundle() :: {bundle, list(bundle_source())}. -file("src/magic_string.gleam", 93). -spec range_str({integer(), integer()}) -> binary(). range_str(R) -> <<<<<<<<"["/utf8, (erlang:integer_to_binary(erlang:element(1, R)))/binary>>/binary, ", "/utf8>>/binary, (erlang:integer_to_binary(erlang:element(2, R)))/binary>>/binary, ")"/utf8>>. -file("src/magic_string.gleam", 66). ?DOC(" Human-readable description of an `EditError`.\n"). -spec describe_error(edit_error()) -> binary(). describe_error(Err) -> case Err of {overlap, Existing, Attempted} -> <<<<<<"edit "/utf8, (range_str(Attempted))/binary>>/binary, " overlaps existing edit "/utf8>>/binary, (range_str(Existing))/binary>>; {swallowed_insert, At, Range} -> <<<<<<"insert at offset "/utf8, (erlang:integer_to_binary(At))/binary>>/binary, " falls inside overwrite/remove range "/utf8>>/binary, (range_str(Range))/binary>>; {out_of_bounds, Offset, Source_length} -> <<<<<<<<"offset "/utf8, (erlang:integer_to_binary(Offset))/binary>>/binary, " is out of bounds (source is "/utf8>>/binary, (erlang:integer_to_binary(Source_length))/binary>>/binary, " bytes)"/utf8>>; {inverted_range, Start, End} -> <<<<<<<<"range ["/utf8, (erlang:integer_to_binary(Start))/binary>>/binary, ", "/utf8>>/binary, (erlang:integer_to_binary(End))/binary>>/binary, ") has end < start"/utf8>> end. -file("src/magic_string.gleam", 98). ?DOC(" Create an editor over `source` with no edits.\n"). -spec new(binary()) -> magic_string(). new(Source) -> {magic_string, Source, <<""/utf8>>, <<""/utf8>>, maps:new(), maps:new(), maps:new()}. -file("src/magic_string.gleam", 228). ?DOC( " A new range swallows an insert if any recorded `append_left`/`append_right`\n" " index lies strictly between `start` and `end`.\n" ). -spec check_no_swallowed_inserts(magic_string(), integer(), integer()) -> {ok, nil} | {error, edit_error()}. check_no_swallowed_inserts(Ms, Start, End) -> Swallowed = begin _pipe = lists:append( maps:keys(erlang:element(6, Ms)), maps:keys(erlang:element(7, Ms)) ), gleam@list:find(_pipe, fun(At) -> (At > Start) andalso (At < End) end) end, case Swallowed of {ok, At@1} -> {error, {swallowed_insert, At@1, {Start, End}}}; {error, nil} -> {ok, nil} end. -file("src/magic_string.gleam", 207). ?DOC( " `[start, end)` overlaps an existing edit if any recorded range shares at\n" " least one byte with it. An edit at the SAME start is last-write-wins, not\n" " an overlap.\n" ). -spec check_no_overlap(magic_string(), integer(), integer()) -> {ok, nil} | {error, edit_error()}. check_no_overlap(Ms, Start, End) -> Conflict = begin _pipe = maps:values(erlang:element(5, Ms)), gleam@list:find( _pipe, fun(E) -> ((erlang:element(2, E) /= Start) andalso (erlang:element(2, E) < End)) andalso (Start < erlang:element(3, E)) end ) end, case Conflict of {ok, E@1} -> {error, {overlap, {erlang:element(2, E@1), erlang:element(3, E@1)}, {Start, End}}}; {error, nil} -> {ok, nil} end. -file("src/magic_string.gleam", 197). -spec check_bound(integer(), integer()) -> {ok, nil} | {error, edit_error()}. check_bound(Offset, Len) -> case (Offset < 0) orelse (Offset > Len) of true -> {error, {out_of_bounds, Offset, Len}}; false -> {ok, nil} end. -file("src/magic_string.gleam", 188). -spec check_range(integer(), integer(), integer()) -> {ok, nil} | {error, edit_error()}. check_range(Start, End, Len) -> gleam@result:'try'( check_bound(Start, Len), fun(_use0) -> nil = _use0, gleam@result:'try'( check_bound(End, Len), fun(_use0@1) -> nil = _use0@1, case End < Start of true -> {error, {inverted_range, Start, End}}; false -> {ok, nil} end end ) end ). -file("src/magic_string.gleam", 115). ?DOC( " Replace the original byte range `[start, end)` with `with`. The\n" " replacement is inserted text and produces no mapping segment.\n" "\n" " Returns `Error` if `[start, end)` overlaps an existing range, would\n" " swallow an existing insert, or is out of bounds. Same-start is\n" " last-write-wins; adjacent ranges that only share a boundary are fine.\n" ). -spec overwrite(magic_string(), integer(), integer(), binary()) -> {ok, magic_string()} | {error, edit_error()}. overwrite(Ms, Start, End, Content) -> Len = erlang:byte_size(erlang:element(2, Ms)), gleam@result:'try'( check_range(Start, End, Len), fun(_use0) -> nil = _use0, gleam@result:'try'( check_no_overlap(Ms, Start, End), fun(_use0@1) -> nil = _use0@1, gleam@result:'try'( check_no_swallowed_inserts(Ms, Start, End), fun(_use0@2) -> nil = _use0@2, {ok, {magic_string, erlang:element(2, Ms), erlang:element(3, Ms), erlang:element(4, Ms), gleam@dict:insert( erlang:element(5, Ms), Start, {edit, Start, End, Content} ), erlang:element(6, Ms), erlang:element(7, Ms)}} end ) end ) end ). -file("src/magic_string.gleam", 135). ?DOC( " Delete the original byte range `[start, end)`. Returns `Error` for the\n" " same reasons as `overwrite` (it is `overwrite` with an empty replacement).\n" ). -spec remove(magic_string(), integer(), integer()) -> {ok, magic_string()} | {error, edit_error()}. remove(Ms, Start, End) -> overwrite(Ms, Start, End, <<""/utf8>>). -file("src/magic_string.gleam", 244). ?DOC( " An insert at `index` is rejected if it lies strictly inside an existing\n" " overwrite/remove range, or is out of bounds.\n" ). -spec check_insert_at(magic_string(), integer()) -> {ok, nil} | {error, edit_error()}. check_insert_at(Ms, Index) -> gleam@result:'try'( check_bound(Index, erlang:byte_size(erlang:element(2, Ms))), fun(_use0) -> nil = _use0, Enclosing = begin _pipe = maps:values(erlang:element(5, Ms)), gleam@list:find( _pipe, fun(E) -> (Index > erlang:element(2, E)) andalso (Index < erlang:element( 3, E )) end ) end, case Enclosing of {ok, E@1} -> {error, {swallowed_insert, Index, {erlang:element(2, E@1), erlang:element(3, E@1)}}}; {error, nil} -> {ok, nil} end end ). -file("src/magic_string.gleam", 151). ?DOC( " Insert `content` just LEFT of `index`: after the content that ends at\n" " `index`, before anything inserted to the right of `index`. Repeated calls\n" " at the same index append in call order.\n" "\n" " Returns `Error(SwallowedInsert)` if `index` falls strictly inside an\n" " existing overwrite/remove range (the insert would never be visited and so\n" " would be silently dropped), or `Error(OutOfBounds)` if `index` is outside\n" " the source.\n" ). -spec append_left(magic_string(), integer(), binary()) -> {ok, magic_string()} | {error, edit_error()}. append_left(Ms, Index, Content) -> gleam@result:'try'( check_insert_at(Ms, Index), fun(_use0) -> nil = _use0, Existing = begin _pipe = gleam_stdlib:map_get(erlang:element(6, Ms), Index), gleam@result:unwrap(_pipe, <<""/utf8>>) end, {ok, {magic_string, erlang:element(2, Ms), erlang:element(3, Ms), erlang:element(4, Ms), erlang:element(5, Ms), gleam@dict:insert( erlang:element(6, Ms), Index, <> ), erlang:element(7, Ms)}} end ). -file("src/magic_string.gleam", 171). ?DOC( " Insert `content` just RIGHT of `index`: before the content that starts at\n" " `index`, after anything inserted to the left of `index`. Repeated calls at\n" " the same index append in call order.\n" "\n" " Returns `Error` for the same reasons as `append_left`.\n" ). -spec append_right(magic_string(), integer(), binary()) -> {ok, magic_string()} | {error, edit_error()}. append_right(Ms, Index, Content) -> gleam@result:'try'( check_insert_at(Ms, Index), fun(_use0) -> nil = _use0, Existing = begin _pipe = gleam_stdlib:map_get(erlang:element(7, Ms), Index), gleam@result:unwrap(_pipe, <<""/utf8>>) end, {ok, {magic_string, erlang:element(2, Ms), erlang:element(3, Ms), erlang:element(4, Ms), erlang:element(5, Ms), erlang:element(6, Ms), gleam@dict:insert( erlang:element(7, Ms), Index, <> )}} end ). -file("src/magic_string.gleam", 257). ?DOC( " Prepend `content` to the very start of the output. Repeated calls stack so\n" " the most recent prepend ends up first.\n" ). -spec prepend(magic_string(), binary()) -> magic_string(). prepend(Ms, Content) -> {magic_string, erlang:element(2, Ms), <>, erlang:element(4, Ms), erlang:element(5, Ms), erlang:element(6, Ms), erlang:element(7, Ms)}. -file("src/magic_string.gleam", 262). ?DOC(" Append `content` to the very end of the output.\n"). -spec append(magic_string(), binary()) -> magic_string(). append(Ms, Content) -> {magic_string, erlang:element(2, Ms), erlang:element(3, Ms), <<(erlang:element(4, Ms))/binary, Content/binary>>, erlang:element(5, Ms), erlang:element(6, Ms), erlang:element(7, Ms)}. -file("src/magic_string.gleam", 391). ?DOC( " Slice `[start, start + len)` bytes of `bytes` back into a String. The range\n" " always lands on UTF-8 boundaries (offsets come from edit/insert positions and\n" " byte lengths), so a slice/decode failure can only mean an out-of-range\n" " request, for which the empty string is the safe fragment.\n" ). -spec slice_bytes(bitstring(), integer(), integer()) -> binary(). slice_bytes(Bytes, Start, Len) -> case gleam_stdlib:bit_array_slice(Bytes, Start, Len) of {ok, Slice} -> case gleam@bit_array:to_string(Slice) of {ok, Text} -> Text; {error, nil} -> <<""/utf8>> end; {error, nil} -> <<""/utf8>> end. -file("src/magic_string.gleam", 380). -spec next_boundary(list(integer()), integer(), integer()) -> integer(). next_boundary(Boundaries, Pos, Byte_len) -> _pipe = Boundaries, _pipe@1 = gleam@list:filter(_pipe, fun(B) -> B > Pos end), _pipe@2 = gleam@list:first(_pipe@1), gleam@result:unwrap(_pipe@2, Byte_len). -file("src/magic_string.gleam", 357). ?DOC(" Helper so the end-of-source check reads as an early return without nesting.\n"). -spec bool_at_end(boolean(), list(piece()), fun(() -> list(piece()))) -> list(piece()). bool_at_end(At_end, Acc, Cont) -> case At_end of true -> Acc; false -> Cont() end. -file("src/magic_string.gleam", 368). -spec push_insert( list(piece()), gleam@dict:dict(integer(), binary()), integer() ) -> list(piece()). push_insert(Acc, Inserts, Pos) -> case gleam_stdlib:map_get(Inserts, Pos) of {ok, <<""/utf8>>} -> Acc; {ok, Text} -> [{piece, Text, none} | Acc]; {error, nil} -> Acc end. -file("src/magic_string.gleam", 343). ?DOC( " Emit the surviving original slice from `pos` to the next boundary, carrying\n" " `pos` as the mapping offset, then continue the walk.\n" ). -spec emit_slice( magic_string(), bitstring(), integer(), list(integer()), integer(), list(piece()) ) -> list(piece()). emit_slice(Ms, Bytes, Byte_len, Boundaries, Pos, Acc) -> Stop = next_boundary(Boundaries, Pos, Byte_len), Text = slice_bytes(Bytes, Pos, Stop - Pos), walk( Ms, Bytes, Byte_len, Boundaries, Stop, [{piece, Text, {some, Pos}} | Acc] ). -file("src/magic_string.gleam", 314). ?DOC(" Tail-recursive walk building the body piece list in reverse.\n"). -spec walk( magic_string(), bitstring(), integer(), list(integer()), integer(), list(piece()) ) -> list(piece()). walk(Ms, Bytes, Byte_len, Boundaries, Pos, Acc) -> Acc@1 = push_insert(Acc, erlang:element(6, Ms), Pos), Acc@2 = push_insert(Acc@1, erlang:element(7, Ms), Pos), bool_at_end( Pos >= Byte_len, Acc@2, fun() -> case gleam_stdlib:map_get(erlang:element(5, Ms), Pos) of {ok, Edit} -> case erlang:element(3, Edit) > Pos of true -> walk( Ms, Bytes, Byte_len, Boundaries, erlang:element(3, Edit), [{piece, erlang:element(4, Edit), none} | Acc@2] ); false -> emit_slice( Ms, Bytes, Byte_len, Boundaries, Pos, Acc@2 ) end; {error, nil} -> emit_slice(Ms, Bytes, Byte_len, Boundaries, Pos, Acc@2) end end ). -file("src/magic_string.gleam", 302). ?DOC( " Sorted, de-duplicated set of offsets at which a surviving slice must stop:\n" " every edit start and every insert index, plus the end of the source.\n" ). -spec compute_boundaries(magic_string(), integer()) -> list(integer()). compute_boundaries(Ms, Byte_len) -> _pipe = lists:append( [maps:keys(erlang:element(5, Ms)), maps:keys(erlang:element(6, Ms)), maps:keys(erlang:element(7, Ms)), [Byte_len]] ), _pipe@1 = gleam@list:unique(_pipe), gleam@list:sort(_pipe@1, fun gleam@int:compare/2). -file("src/magic_string.gleam", 281). ?DOC( " Replay the edit log into an ordered list of output pieces.\n" "\n" " Walks the original source from offset 0: at each position it emits any\n" " left-then-right inserts bound to that offset, then either the replacement of\n" " an edit beginning there (advancing past the edit's end) or the surviving\n" " original slice up to the next boundary (carrying its original offset). `intro`\n" " and `outro` wrap the result.\n" ). -spec materialize(magic_string()) -> list(piece()). materialize(Ms) -> Bytes = gleam_stdlib:identity(erlang:element(2, Ms)), Byte_len = erlang:byte_size(Bytes), Boundaries = compute_boundaries(Ms, Byte_len), Body = walk(Ms, Bytes, Byte_len, Boundaries, 0, []), Intro_pieces = case erlang:element(3, Ms) of <<""/utf8>> -> []; Text -> [{piece, Text, none}] end, Outro_pieces = case erlang:element(4, Ms) of <<""/utf8>> -> []; Text@1 -> [{piece, Text@1, none}] end, lists:append([Intro_pieces, lists:reverse(Body), Outro_pieces]). -file("src/magic_string.gleam", 267). ?DOC(" Materialize the edited output as a string.\n"). -spec to_string(magic_string()) -> binary(). to_string(Ms) -> _pipe = materialize(Ms), gleam@list:fold( _pipe, <<""/utf8>>, fun(Acc, Piece) -> <> end ). -file("src/magic_string.gleam", 489). ?DOC( " Length of `text` in UTF-16 code units: astral codepoints (>= U+10000) count\n" " as 2 (a surrogate pair), everything else as 1.\n" ). -spec utf16_len(binary()) -> integer(). utf16_len(Text) -> _pipe = Text, _pipe@1 = gleam@string:to_utf_codepoints(_pipe), gleam@list:fold( _pipe@1, 0, fun(Acc, Cp) -> case gleam_stdlib:identity(Cp) >= 16#10000 of true -> Acc + 2; false -> Acc + 1 end end ). -file("src/magic_string.gleam", 471). ?DOC( " Advance a generated `(line, col)` position past `text`. Columns are UTF-16\n" " code units; lines increment per `\\n`.\n" ). -spec advance(integer(), integer(), binary()) -> {integer(), integer()}. advance(Gen_line, Gen_col, Text) -> case gleam@string:split(Text, <<"\n"/utf8>>) of [] -> {Gen_line, Gen_col}; [Single] -> {Gen_line, Gen_col + utf16_len(Single)}; Lines -> Newlines = erlang:length(Lines) - 1, Last = begin _pipe = gleam@list:last(Lines), gleam@result:unwrap(_pipe, <<""/utf8>>) end, {Gen_line + Newlines, utf16_len(Last)} end. -file("src/magic_string.gleam", 483). -spec byte_size(binary()) -> integer(). byte_size(Text) -> erlang:byte_size(gleam_stdlib:identity(Text)). -file("src/magic_string.gleam", 437). ?DOC( " Produce one segment per generated line covered by a surviving slice. The\n" " first line maps the slice's start (at the current generated column); each\n" " subsequent line (after a newline in the slice) maps the corresponding\n" " original line start at generated column 0. `hires` is false, so there is one\n" " segment per line, not per token.\n" ). -spec slice_segments( binary(), integer(), magic_string@position_index:position_index(), integer(), integer(), integer() ) -> list(magic_string@codec:segment()). slice_segments(Text, Offset, Index, Source_idx, Gen_line, Gen_col) -> Lines = gleam@string:split(Text, <<"\n"/utf8>>), {Segs, _} = gleam@list:index_fold( Lines, {[], 0}, fun(State, Line, I) -> {Acc, Byte_off} = State, Pos = magic_string@position_index:lookup(Index, Offset + Byte_off), Seg_gen_col = case I of 0 -> Gen_col; _ -> 0 end, Seg = {segment, Gen_line + I, Seg_gen_col, Source_idx, erlang:element(2, Pos), erlang:element(3, Pos)}, Next_off = (Byte_off + byte_size(Line)) + 1, {[Seg | Acc], Next_off} end ), Segs. -file("src/magic_string.gleam", 408). ?DOC( " Convert a piece list into mapping segments, threading the running generated\n" " position. `gen_line` / `gen_col` accumulate across the whole materialized\n" " output (and, for bundles, across sources). Returns the produced segments plus\n" " the generated position just past the last piece.\n" ). -spec pieces_to_segments( list(piece()), magic_string@position_index:position_index(), integer(), integer(), integer() ) -> {list(magic_string@codec:segment()), integer(), integer()}. pieces_to_segments(Pieces, Index, Source_idx, Gen_line, Gen_col) -> gleam@list:fold( Pieces, {[], Gen_line, Gen_col}, fun(Acc, Piece) -> {Segs, Gline, Gcol} = Acc, case erlang:element(3, Piece) of none -> {Next_line, Next_col} = advance( Gline, Gcol, erlang:element(2, Piece) ), {Segs, Next_line, Next_col}; {some, Offset} -> New_segs = slice_segments( erlang:element(2, Piece), Offset, Index, Source_idx, Gline, Gcol ), {Next_line@1, Next_col@1} = advance( Gline, Gcol, erlang:element(2, Piece) ), {lists:append(New_segs, Segs), Next_line@1, Next_col@1} end end ). -file("src/magic_string.gleam", 505). ?DOC( " Generate a Source Map v3 for a single edited source. `filename` is recorded\n" " as the sole entry of `sources`; `include_content` controls whether the\n" " original text is embedded in `sourcesContent`.\n" ). -spec generate_map(magic_string(), binary(), map_options()) -> magic_string@codec:source_map(). generate_map(Ms, Filename, Opts) -> Index = magic_string@position_index:new(erlang:element(2, Ms)), Pieces = materialize(Ms), {Segments, _, _} = pieces_to_segments(Pieces, Index, 0, 0, 0), Sources_content = case erlang:element(4, Opts) of true -> [{some, erlang:element(2, Ms)}]; false -> [none] end, {source_map, 3, erlang:element(2, Opts), erlang:element(3, Opts), [Filename], Sources_content, [], magic_string@codec:generate_mappings(Segments)}. -file("src/magic_string.gleam", 538). ?DOC(" Sensible defaults: no `file`, no `sourceRoot`, embed original content.\n"). -spec default_map_options() -> map_options(). default_map_options() -> {map_options, none, none, true}. -file("src/magic_string.gleam", 558). ?DOC(" An empty bundle.\n"). -spec bundle() -> bundle(). bundle() -> {bundle, []}. -file("src/magic_string.gleam", 564). ?DOC( " Append a source to the bundle. `filename` is the name recorded in the map's\n" " `sources`; `content` is the original text; `ms` is its edit log.\n" ). -spec add_source(bundle(), binary(), binary(), magic_string()) -> bundle(). add_source(B, Filename, Content, Ms) -> {bundle, [{bundle_source, Filename, Content, Ms} | erlang:element(2, B)]}. -file("src/magic_string.gleam", 573). -spec bundle_sources(bundle()) -> list(bundle_source()). bundle_sources(B) -> lists:reverse(erlang:element(2, B)). -file("src/magic_string.gleam", 579). ?DOC( " Materialize the bundle output: each source's edited text, joined by newlines\n" " (one line break between sources).\n" ). -spec bundle_to_string(bundle()) -> binary(). bundle_to_string(B) -> _pipe = bundle_sources(B), _pipe@1 = gleam@list:map( _pipe, fun(Src) -> to_string(erlang:element(4, Src)) end ), gleam@string:join(_pipe@1, <<"\n"/utf8>>). -file("src/magic_string.gleam", 591). ?DOC( " Generate one Source Map v3 spanning every source in output order.\n" "\n" " Walks each source's pieces, offsetting their segments into chunk-space: the\n" " generated line/column accumulates across the whole output, and the newline\n" " joining each source to the next advances the generated line by one (and\n" " resets the column). Each source is assigned its index in `sources`.\n" ). -spec bundle_generate_map(bundle(), map_options()) -> magic_string@codec:source_map(). bundle_generate_map(B, Opts) -> Srcs = bundle_sources(B), {Segments, _, _, _} = gleam@list:fold( Srcs, {[], 0, 0, 0}, fun(Acc, Src) -> {Segs, Gen_line, _, Source_idx} = Acc, Index = magic_string@position_index:new(erlang:element(3, Src)), Pieces = materialize(erlang:element(4, Src)), {New_segs, End_line, _} = pieces_to_segments( Pieces, Index, Source_idx, Gen_line, 0 ), {lists:append(New_segs, Segs), End_line + 1, 0, Source_idx + 1} end ), Sources = gleam@list:map(Srcs, fun(Src@1) -> erlang:element(2, Src@1) end), Sources_content = gleam@list:map( Srcs, fun(Src@2) -> case erlang:element(4, Opts) of true -> {some, erlang:element(3, Src@2)}; false -> none end end ), {source_map, 3, erlang:element(2, Opts), erlang:element(3, Opts), Sources, Sources_content, [], magic_string@codec:generate_mappings(Segments)}.