Current section
Files
Jump to
Current section
Files
lib/otzel/attrs.ex
defmodule Otzel.Attrs do
@moduledoc """
Utilities for working with operation attributes.
Attributes are maps of formatting properties applied to content, such as
`%{"bold" => true, "color" => "#ff0000"}`. They are used with Insert and
Retain operations to style text.
## Attribute Values
- Any truthy value applies the attribute
- `nil` removes the attribute
- Missing keys leave the attribute unchanged
## Functions
- `compose/3` - Combines attributes from sequential operations
- `transform/3` - Adjusts attributes for concurrent operations
- `invert/2` - Creates attributes that undo a change
- `diff/2` - Computes the difference between two attribute sets
## Examples
# Compose: second operation wins
Otzel.Attrs.compose(%{"bold" => true}, %{"italic" => true})
#=> %{"bold" => true, "italic" => true}
# Diff: compute changes needed
Otzel.Attrs.diff(%{"bold" => true}, %{"italic" => true})
#=> %{"bold" => nil, "italic" => true}
"""
@typedoc """
Attributes are a map of string keys to values, or nil.
A nil value for a key indicates the attribute should be removed.
A nil attrs map means no attributes are set.
"""
@type t :: %{optional(String.t()) => any} | nil
@doc """
Composes two attribute maps, with the second map taking precedence.
Used when applying sequential operations - the later operation's
attributes override earlier ones.
## Options
- `keep_nils` - When true, preserves nil values in the result (default: false)
## Examples
iex> Otzel.Attrs.compose(%{"bold" => true}, %{"italic" => true})
%{"bold" => true, "italic" => true}
iex> Otzel.Attrs.compose(%{"bold" => true}, %{"bold" => nil})
nil
"""
@spec compose(t, t, keep_nils :: boolean) :: t
def compose(a, b, keep_nils \\ false) do
case {a, b} do
{nil, _} ->
b
{_, nil} ->
a
_ ->
Map.merge(a, b)
end
|> cleanup(keep_nils)
|> nilify()
end
def cleanup(map, false) when is_map(map) do
for {k, v} <- map, v != nil, into: %{}, do: {k, v}
end
def cleanup(other, _), do: other
defp nilify(map) when map_size(map) == 0, do: nil
defp nilify(map), do: map
@doc """
Computes attributes that would undo a change.
Given the attributes that were applied and the original base attributes,
returns attributes that would restore the original state.
## Examples
iex> Otzel.Attrs.invert(%{"bold" => true}, nil)
%{"bold" => nil}
iex> Otzel.Attrs.invert(%{"bold" => nil}, %{"bold" => true})
%{"bold" => true}
"""
@spec invert(t, t) :: t
def invert(nil, base), do: invert(%{}, base)
def invert(attr, nil), do: invert(attr, %{})
def invert(attr, base) do
inverted =
for {k, v} <- base, match?(%{^k => v2} when v2 != v, attr), into: %{}, do: {k, v}
map =
for {k, v} <- attr,
not (Map.has_key?(base, k) || is_nil(v)),
into: inverted,
do: {k, nil}
nilify(map)
end
@doc """
Computes the difference between two attribute maps.
Returns attributes that, when applied to `left`, would produce `right`.
Keys present in `left` but not `right` are set to `nil` (removal).
## Examples
iex> Otzel.Attrs.diff(%{"bold" => true}, %{"bold" => true})
nil
iex> Otzel.Attrs.diff(%{"bold" => true}, %{"italic" => true})
%{"bold" => nil, "italic" => true}
iex> Otzel.Attrs.diff(nil, %{"bold" => true})
%{"bold" => true}
"""
@spec diff(t, t) :: t
def diff(same, same), do: nil
def diff(nil, base), do: base
def diff(left, nil) do
for {k, _} <- left, into: %{}, do: {k, nil}
end
def diff(left, right) do
left_diff =
for k <- Map.keys(left), into: %{} do
case Map.fetch(right, k) do
{:ok, v2} -> {k, v2}
_ -> {k, nil}
end
end
for {k, v} <- right, not is_map_key(left, k), into: left_diff do
{k, v}
end
end
@doc """
Transforms attributes for concurrent operations.
When two operations modify the same content concurrently, this determines
which attributes take effect based on priority.
## Priority
- `:right` - The `into` attributes win (default)
- `:left` - Only `into` attributes not in `from` are kept
## Examples
iex> Otzel.Attrs.transform(%{"bold" => true}, %{"italic" => true}, :right)
%{"italic" => true}
iex> Otzel.Attrs.transform(%{"bold" => true}, %{"bold" => false}, :left)
nil
"""
@spec transform(t, t, Otzel.priority()) :: t
def transform(from, into, priority)
def transform(nil, b, _), do: b
def transform(_, nil, _), do: nil
def transform(_, b, :right), do: b
def transform(a, b, _) do
nilify(for {k, v} <- b, not is_map_key(a, k), into: %{}, do: {k, v})
end
end