-module(magic_string@position_index). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/magic_string/position_index.gleam"). -export([new/1, line_count/1, lookup/2]). -export_type([position/0, position_index/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( " UTF-8 byte offset -> (line, UTF-16 column) conversion.\n" "\n" " Source Map v3 columns count UTF-16 code units, not bytes, so for any\n" " non-ASCII source a byte offset is not a column. Build a `PositionIndex`\n" " once with `new`, then `lookup` each offset. Line and column are 0-based.\n" ). -type position() :: {position, integer(), integer()}. -opaque position_index() :: {position_index, bitstring(), list(integer()), integer()}. -file("src/magic_string/position_index.gleam", 56). ?DOC( " Walk the bytes recording the offset that begins each line. `acc` is built in\n" " reverse (newest first) and reversed by the caller.\n" ). -spec line_starts_loop(bitstring(), integer(), list(integer())) -> list(integer()). line_starts_loop(Bytes, Pos, Acc) -> case Bytes of <<>> -> Acc; <<16#0A, Rest/bitstring>> -> line_starts_loop(Rest, Pos + 1, [Pos + 1 | Acc]); <<_, Rest@1/bitstring>> -> line_starts_loop(Rest@1, Pos + 1, Acc); _ -> Acc end. -file("src/magic_string/position_index.gleam", 32). ?DOC( " Build a `PositionIndex` for `source`. A line begins at byte 0 and after each\n" " `\\n` (0x0A) byte; a `\\r\\n` sequence is split by its `\\n`, so the carriage\n" " return is the last byte of the preceding line. Scanning for the 0x0A byte is\n" " UTF-8 safe because 0x0A never appears inside a multibyte sequence.\n" ). -spec new(binary()) -> position_index(). new(Source) -> Bytes = gleam_stdlib:identity(Source), Byte_len = erlang:byte_size(Bytes), Line_starts = lists:reverse(line_starts_loop(Bytes, 0, [0])), {position_index, Bytes, Line_starts, Byte_len}. -file("src/magic_string/position_index.gleam", 40). ?DOC(" Number of lines in the indexed source (always at least 1).\n"). -spec line_count(position_index()) -> integer(). line_count(Index) -> erlang:length(erlang:element(3, Index)). -file("src/magic_string/position_index.gleam", 99). ?DOC( " Length of a string measured in UTF-16 code units: astral codepoints\n" " (>= U+10000) take a surrogate pair (2 units), all others take 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/position_index.gleam", 87). ?DOC( " Count UTF-16 code units in the byte range `[start, end)`. Falls back to the\n" " byte distance only if the range is not on codepoint boundaries (which should\n" " not happen for spans/line starts produced from valid source).\n" ). -spec utf16_count(bitstring(), integer(), integer()) -> integer(). utf16_count(Bytes, Start, End) -> Byte_len = End - Start, Counted = begin gleam@result:'try'( gleam_stdlib:bit_array_slice(Bytes, Start, Byte_len), fun(Slice) -> gleam@result:map( gleam@bit_array:to_string(Slice), fun(Text) -> utf16_len(Text) end ) end ) end, gleam@result:unwrap(Counted, Byte_len). -file("src/magic_string/position_index.gleam", 69). ?DOC( " Find the greatest line start `<= offset`. Returns `(line_index, line_start)`\n" " where `line_index` is how many starts precede or equal `offset`, minus one.\n" " `line_starts` always begins with 0 and `offset >= 0`, so at least one start\n" " qualifies and `idx >= 1` at return.\n" ). -spec find_line(list(integer()), integer(), integer(), integer()) -> {integer(), integer()}. find_line(Line_starts, Offset, Idx, Cur_start) -> case Line_starts of [] -> {Idx - 1, Cur_start}; [Start | Rest] -> gleam@bool:guard( Start > Offset, {Idx - 1, Cur_start}, fun() -> find_line(Rest, Offset, Idx + 1, Start) end ) end. -file("src/magic_string/position_index.gleam", 47). ?DOC( " Convert a byte offset into a 0-based `(line, UTF-16 column)` position.\n" " Offsets are clamped into `[0, byte_len]`, so an end-exclusive span boundary\n" " at end-of-file resolves to the position just past the last byte.\n" ). -spec lookup(position_index(), integer()) -> position(). lookup(Index, Byte_offset) -> Offset = gleam@int:clamp(Byte_offset, 0, erlang:element(4, Index)), {Line, Line_start} = find_line(erlang:element(3, Index), Offset, 0, 0), Column = utf16_count(erlang:element(2, Index), Line_start, Offset), {position, Line, Column}.