Packages
livebook
0.5.2
0.19.8
0.19.7
0.19.6
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.6
0.18.5
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
0.17.3
0.17.2
0.17.1
0.17.0
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.7
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.14.0-rc.1
0.14.0-rc.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.1
0.12.0
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.3
0.9.2
0.9.1
0.9.0
0.8.2
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.2
0.3.1
0.3.0
0.2.3
0.2.2
0.2.1
0.2.0
0.1.2
0.1.1
0.1.0
Automate code & data workflows with interactive notebooks
Current section
Files
Jump to
Current section
Files
lib/livebook/js_interop.ex
defmodule Livebook.JSInterop do
@moduledoc false
alias Livebook.Delta
@doc """
Returns the result of applying `delta` to `string`.
The delta operation lengths (retain, delete) are treated
such that they match the JavaScript strings behavior.
JavaScript uses UTF-16 encoding, in which every character is stored
as either one or two 16-bit code units. JS treats the number of units
as string length and this also impacts position-based functions like `String.slice`.
To match this behavior we first convert normal UTF-8 string
into a list of UTF-16 code points, then apply the delta to this list
and finally convert back to a UTF-8 string.
"""
@spec apply_delta_to_string(Delta.t(), String.t()) :: String.t()
def apply_delta_to_string(delta, string) do
code_units = string_to_utf16_code_units(string)
delta.ops
|> apply_to_code_units(code_units)
|> utf16_code_units_to_string()
end
defp apply_to_code_units([], code_units), do: code_units
defp apply_to_code_units([{:retain, n} | ops], code_units) do
{left, right} = Enum.split(code_units, n)
left ++ apply_to_code_units(ops, right)
end
defp apply_to_code_units([{:insert, inserted} | ops], code_units) do
string_to_utf16_code_units(inserted) ++ apply_to_code_units(ops, code_units)
end
defp apply_to_code_units([{:delete, n} | ops], code_units) do
apply_to_code_units(ops, Enum.slice(code_units, n..-1))
end
@doc """
Returns a column number in the Elixir string corresponding to
the given column interpreted in terms of UTF-16 code units.
"""
@spec js_column_to_elixir(pos_integer(), String.t()) :: pos_integer()
def js_column_to_elixir(column, line) do
line
|> string_to_utf16_code_units()
|> Enum.take(column - 1)
|> utf16_code_units_to_string()
|> String.length()
|> Kernel.+(1)
end
@doc """
Returns a column represented in terms of UTF-16 code units
corresponding to the given column number in Elixir string.
"""
@spec elixir_column_to_js(pos_integer(), String.t()) :: pos_integer()
def elixir_column_to_js(column, line) do
line
|> string_take(column - 1)
|> string_to_utf16_code_units()
|> length()
|> Kernel.+(1)
end
defp string_take(_string, 0), do: ""
defp string_take(string, n) when n > 0, do: String.slice(string, 0..(n - 1))
# UTF-16 helpers
defp string_to_utf16_code_units(string) do
string
|> :unicode.characters_to_binary(:utf8, :utf16)
|> utf16_binary_to_code_units([])
|> Enum.reverse()
end
defp utf16_binary_to_code_units(<<>>, code_units), do: code_units
defp utf16_binary_to_code_units(<<code_unit::size(16), rest::binary>>, code_units) do
utf16_binary_to_code_units(rest, [code_unit | code_units])
end
defp utf16_code_units_to_string(code_units) do
code_units
|> Enum.reverse()
|> code_units_to_utf16_binary(<<>>)
|> :unicode.characters_to_binary(:utf16, :utf8)
end
defp code_units_to_utf16_binary([], utf16_binary), do: utf16_binary
defp code_units_to_utf16_binary([code_unit | code_units], utf16_binary) do
code_units_to_utf16_binary(code_units, <<code_unit::size(16), utf16_binary::binary>>)
end
end