-module(sprocket@internal@patch). -compile([no_auto_import, nowarn_unused_vars]). -export([patch_to_json/1, create/2]). -export_type([patch/0]). -type patch() :: no_op | {update, gleam@option:option(list(sprocket@render:rendered_attribute())), gleam@option:option(list({integer(), patch()}))} | {replace, sprocket@render:rendered_element()} | {insert, sprocket@render:rendered_element()} | remove | {change, binary()} | {move, integer(), patch()}. -spec attr_key(sprocket@render:rendered_attribute()) -> binary(). attr_key(Attribute) -> case Attribute of {rendered_attribute, Name, _} -> Name; {rendered_event_handler, _, Id} -> Id end. -spec build_attrs_map(list(sprocket@render:rendered_attribute())) -> gleam@map:map_(binary(), sprocket@render:rendered_attribute()). build_attrs_map(Attributes) -> _pipe = Attributes, gleam@list:fold( _pipe, gleam@map:new(), fun(Acc, Attr) -> gleam@map:insert(Acc, attr_key(Attr), Attr) end ). -spec op_code(patch()) -> integer(). op_code(Op) -> case Op of no_op -> 0; {update, _, _} -> 1; {replace, _} -> 2; {insert, _} -> 3; remove -> 4; {change, _} -> 5; {move, _, _} -> 6 end. -spec attrs_to_json(list(sprocket@render:rendered_attribute())) -> gleam@json:json(). attrs_to_json(Attrs) -> _pipe = Attrs, _pipe@1 = gleam@list:map(_pipe, fun(Attr) -> case Attr of {rendered_attribute, Name, Value} -> {Name, gleam@json:string(Value)}; {rendered_event_handler, Kind, Id} -> {gleam@string:concat( [sprocket@internal@constants:constant( event_attr_prefix ), <<"-"/utf8>>, Kind] ), gleam@json:string(Id)} end end), gleam@json:object(_pipe@1). -spec compare_attributes_helper( gleam@map:map_(binary(), sprocket@render:rendered_attribute()), list(sprocket@render:rendered_attribute()) ) -> gleam@option:option(list(sprocket@render:rendered_attribute())). compare_attributes_helper(Old_attributes, New_attributes) -> case New_attributes of [New_attr | Rest_new] -> case gleam@map:get(Old_attributes, attr_key(New_attr)) of {ok, Old_attr} -> case Old_attr =:= New_attr of true -> case compare_attributes_helper( gleam@map:delete( Old_attributes, attr_key(Old_attr) ), Rest_new ) of {some, _} -> {some, New_attributes}; none -> none end; false -> {some, New_attributes} end; _ -> {some, New_attributes} end; _ -> case {gleam@map:size(Old_attributes), gleam@list:length(New_attributes)} of {0, 0} -> none; {Old_size, New_size} when Old_size > New_size -> {some, New_attributes} end end. -spec compare_attributes( list(sprocket@render:rendered_attribute()), list(sprocket@render:rendered_attribute()) ) -> gleam@option:option(list(sprocket@render:rendered_attribute())). compare_attributes(Old_attributes, New_attributes) -> compare_attributes_helper(build_attrs_map(Old_attributes), New_attributes). -spec zip_all(list(KIL), list(KIN)) -> list({gleam@option:option(KIL), gleam@option:option(KIN)}). zip_all(A, B) -> case {A, B} of {[], []} -> []; {[A@1 | Rest_a], [B@1 | Rest_b]} -> [{{some, A@1}, {some, B@1}} | zip_all(Rest_a, Rest_b)]; {[A@2 | Rest_a@1], []} -> [{{some, A@2}, none} | zip_all(Rest_a@1, [])]; {[], [B@2 | Rest_b@1]} -> [{none, {some, B@2}} | zip_all([], Rest_b@1)] end. -spec patch_to_json(patch()) -> gleam@json:json(). patch_to_json(Patch) -> case Patch of no_op -> gleam@json:preprocessed_array([gleam@json:int(op_code(Patch))]); {update, Attrs, Children} -> gleam@json:preprocessed_array( [gleam@json:int(op_code(Patch)), gleam@json:nullable(Attrs, fun attrs_to_json/1), gleam@json:nullable(Children, fun children_to_json/1)] ); {replace, El} -> gleam@json:preprocessed_array( [gleam@json:int(op_code(Patch)), (erlang:element(2, sprocket@internal@render@json:renderer()))( El )] ); {insert, El@1} -> gleam@json:preprocessed_array( [gleam@json:int(op_code(Patch)), (erlang:element(2, sprocket@internal@render@json:renderer()))( El@1 )] ); remove -> gleam@json:preprocessed_array([gleam@json:int(op_code(Patch))]); {change, Text} -> gleam@json:preprocessed_array( [gleam@json:int(op_code(Patch)), gleam@json:string(Text)] ); {move, Index, Move_patch} -> gleam@json:preprocessed_array( [gleam@json:int(op_code(Patch)), gleam@json:int(Index), patch_to_json(Move_patch)] ) end. -spec map_key_to_str({integer(), patch()}) -> {binary(), gleam@json:json()}. map_key_to_str(C) -> {Index, Patch} = C, {gleam@int:to_string(Index), patch_to_json(Patch)}. -spec children_to_json(list({integer(), patch()})) -> gleam@json:json(). children_to_json(Children) -> _pipe = Children, _pipe@1 = gleam@list:map(_pipe, fun map_key_to_str/1), gleam@json:object(_pipe@1). -spec compare_children( list(sprocket@render:rendered_element()), list(sprocket@render:rendered_element()) ) -> gleam@option:option(list({integer(), patch()})). compare_children(Old_children, New_children) -> Key_map = begin _pipe = Old_children, gleam@list:index_fold( _pipe, gleam@map:new(), fun(Acc, Child, Index) -> case Child of {rendered_element, _, {some, Key}, _, _} -> gleam@map:insert(Acc, Key, {Index, Child}); _ -> Acc end end ) end, _pipe@1 = zip_all(Old_children, New_children), _pipe@2 = gleam@list:index_map( _pipe@1, fun(Index@1, Zipped) -> {Old_child, New_child} = Zipped, case {Old_child, New_child} of {{some, Old_child@1}, {some, New_child@1}} -> case {Old_child@1, New_child@1} of {{rendered_element, _, _, _, _}, {rendered_element, _, {some, Key@1}, _, _}} -> case gleam@map:get(Key_map, Key@1) of {ok, Found} -> {Old_index, Old_child@2} = Found, Child_patch = create( Old_child@2, New_child@1 ), case Index@1 =:= Old_index of true -> {Index@1, Child_patch}; false -> {Index@1, {move, Old_index, Child_patch}} end; {error, nil} -> {Index@1, create(Old_child@1, New_child@1)} end; {_, _} -> {Index@1, create(Old_child@1, New_child@1)} end; {none, {some, New_child@2}} -> case New_child@2 of {rendered_element, _, {some, Key@2}, _, _} -> case gleam@map:get(Key_map, Key@2) of {ok, Found@1} -> {Old_index@1, Old_child@3} = Found@1, {Index@1, {move, Old_index@1, create(Old_child@3, New_child@2)}}; {error, nil} -> {Index@1, {insert, New_child@2}} end; _ -> {Index@1, {insert, New_child@2}} end; {{some, _}, _} -> {Index@1, remove} end end ), _pipe@3 = gleam@list:filter_map(_pipe@2, fun(Child_el) -> case Child_el of {_, no_op} -> {error, nil}; {Index@2, Patch} -> {ok, {Index@2, Patch}} end end), (fun(Diff_list) -> case Diff_list of [] -> none; _ -> {some, Diff_list} end end)(_pipe@3). -spec create( sprocket@render:rendered_element(), sprocket@render:rendered_element() ) -> patch(). create(Old, New) -> case {Old, New} of {{rendered_element, Old_tag, Old_key, Old_attrs, Old_children}, {rendered_element, New_tag, New_key, New_attrs, New_children}} when Old_tag =:= New_tag -> case Old_key =:= New_key of true -> case compare_attributes(Old_attrs, New_attrs) of {some, Attrs} -> {update, {some, Attrs}, compare_children(Old_children, New_children)}; none -> case compare_children(Old_children, New_children) of {some, Children} -> {update, none, {some, Children}}; none -> no_op end end; false -> {replace, New} end; {{rendered_component, Old_fc, _, Old_props, _, Old_children@1}, {rendered_component, New_fc, _, New_props, _, New_children@1}} when (Old_fc =:= New_fc) andalso (Old_props =:= New_props) -> case compare_children(Old_children@1, New_children@1) of {some, Children@1} -> {update, none, {some, Children@1}}; none -> no_op end; {{rendered_text, Old_text}, {rendered_text, New_text}} -> case Old_text =:= New_text of true -> no_op; false -> {change, New_text} end; {_, _} -> {replace, New} end.