-module(sprocket@internal@patch). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]). -export([op_code/2, patch_to_json/2, create/2]). -export_type([patch/0]). -type patch() :: no_op | {update, gleam@option:option(list(sprocket@internal@reconcile:reconciled_attribute())), gleam@option:option(list({integer(), patch()}))} | {replace, sprocket@internal@reconcile:reconciled_element()} | {insert, sprocket@internal@reconcile:reconciled_element()} | remove | {change, binary()} | {move, integer(), patch()}. -spec attr_key(sprocket@internal@reconcile:reconciled_attribute()) -> binary(). attr_key(Attribute) -> case Attribute of {reconciled_attribute, Name, _} -> Name; {reconciled_event_handler, _, Id} -> Id; {reconciled_client_hook, _, Id@1} -> Id@1 end. -spec compare_attributes_helper( gleam@dict:dict(binary(), sprocket@internal@reconcile:reconciled_attribute()), list(sprocket@internal@reconcile:reconciled_attribute()) ) -> gleam@option:option(list(sprocket@internal@reconcile:reconciled_attribute())). compare_attributes_helper(Old_attributes, New_attributes) -> case New_attributes of [New_attr | Rest_new] -> case gleam@dict:get(Old_attributes, attr_key(New_attr)) of {ok, Old_attr} -> case Old_attr =:= New_attr of true -> case compare_attributes_helper( gleam@dict: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 {maps:size(Old_attributes), erlang:length(New_attributes)} of {Old_size, New_size} when Old_size > New_size -> {some, New_attributes}; {_, _} -> none end end. -spec build_attrs_map(list(sprocket@internal@reconcile:reconciled_attribute())) -> gleam@dict:dict(binary(), sprocket@internal@reconcile:reconciled_attribute()). build_attrs_map(Attributes) -> _pipe = Attributes, gleam@list:fold( _pipe, gleam@dict:new(), fun(Acc, Attr) -> gleam@dict:insert(Acc, attr_key(Attr), Attr) end ). -spec compare_attributes( list(sprocket@internal@reconcile:reconciled_attribute()), list(sprocket@internal@reconcile:reconciled_attribute()) ) -> gleam@option:option(list(sprocket@internal@reconcile:reconciled_attribute())). compare_attributes(Old_attributes, New_attributes) -> compare_attributes_helper(build_attrs_map(Old_attributes), New_attributes). -spec build_key_map(list(sprocket@internal@reconcile:reconciled_element())) -> gleam@dict:dict(binary(), {integer(), sprocket@internal@reconcile:reconciled_element()}). build_key_map(Children) -> _pipe = Children, gleam@list:index_fold( _pipe, gleam@dict:new(), fun(Acc, Child, Index) -> case Child of {reconciled_element, _, {some, Key}, _, _} -> gleam@dict:insert(Acc, Key, {Index, Child}); _ -> Acc end end ). -spec zip_all(list(IRR), list(IRT)) -> list({gleam@option:option(IRR), gleam@option:option(IRT)}). 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 op_code(patch(), boolean()) -> binary(). op_code(Op, Debug) -> case Debug of true -> case Op of no_op -> <<"NoOp"/utf8>>; {update, _, _} -> <<"Update"/utf8>>; {replace, _} -> <<"Replace"/utf8>>; {insert, _} -> <<"Insert"/utf8>>; remove -> <<"Remove"/utf8>>; {change, _} -> <<"Change"/utf8>>; {move, _, _} -> <<"Move"/utf8>> end; false -> case Op of no_op -> <<"0"/utf8>>; {update, _, _} -> <<"1"/utf8>>; {replace, _} -> <<"2"/utf8>>; {insert, _} -> <<"3"/utf8>>; remove -> <<"4"/utf8>>; {change, _} -> <<"5"/utf8>>; {move, _, _} -> <<"6"/utf8>> end end. -spec attrs_to_json(list(sprocket@internal@reconcile:reconciled_attribute())) -> gleam@json:json(). attrs_to_json(Attrs) -> _pipe = Attrs, _pipe@1 = gleam@list:flat_map(_pipe, fun(Attr) -> case Attr of {reconciled_attribute, Name, Value} -> [{Name, gleam@json:string(Value)}]; {reconciled_event_handler, Kind, Id} -> [{gleam@string:concat( [<<"spkt-event"/utf8>>, <<"-"/utf8>>, Kind] ), gleam@json:string(Id)}]; {reconciled_client_hook, Name@1, Id@1} -> [{<<"spkt-event"/utf8>>, gleam@json:string(Name@1)}, {gleam@string:concat( [<<"spkt-event"/utf8>>, <<"-id"/utf8>>] ), gleam@json:string(Id@1)}] end end), gleam@json:object(_pipe@1). -spec map_key_to_str({integer(), patch()}, boolean()) -> {binary(), gleam@json:json()}. map_key_to_str(C, Debug) -> {Index, Patch} = C, {gleam@int:to_string(Index), patch_to_json(Patch, Debug)}. -spec patch_to_json(patch(), boolean()) -> gleam@json:json(). patch_to_json(Patch, Debug) -> sprocket@render:renderer( sprocket@renderers@json:json_renderer(), fun(Render_json) -> case Patch of no_op -> gleam@json:preprocessed_array( [gleam@json:string(op_code(Patch, Debug))] ); {update, Attrs, Children} -> gleam@json:preprocessed_array( [gleam@json:string(op_code(Patch, Debug)), gleam@json:nullable(Attrs, fun attrs_to_json/1), gleam@json:nullable( Children, fun(_capture) -> children_to_json(_capture, Debug) end )] ); {replace, El} -> gleam@json:preprocessed_array( [gleam@json:string(op_code(Patch, Debug)), Render_json(El)] ); {insert, El@1} -> gleam@json:preprocessed_array( [gleam@json:string(op_code(Patch, Debug)), Render_json(El@1)] ); remove -> gleam@json:preprocessed_array( [gleam@json:string(op_code(Patch, Debug))] ); {change, Text} -> gleam@json:preprocessed_array( [gleam@json:string(op_code(Patch, Debug)), gleam@json:string(Text)] ); {move, Index, Move_patch} -> gleam@json:preprocessed_array( [gleam@json:string(op_code(Patch, Debug)), gleam@json:int(Index), patch_to_json(Move_patch, Debug)] ) end end ). -spec children_to_json(list({integer(), patch()}), boolean()) -> gleam@json:json(). children_to_json(Children, Debug) -> _pipe = Children, _pipe@1 = gleam@list:map( _pipe, fun(_capture) -> map_key_to_str(_capture, Debug) end ), gleam@json:object(_pipe@1). -spec compare_child_at_index( gleam@dict:dict(integer(), patch()), list(sprocket@internal@reconcile:reconciled_element()), sprocket@internal@reconcile:reconciled_element(), integer() ) -> gleam@dict:dict(integer(), patch()). compare_child_at_index(Acc, Old_children, New_child, Index) -> case sprocket@internal@utils@list:element_at(Old_children, Index, 0) of {ok, Old_child} -> case create(Old_child, New_child) of no_op -> Acc; Patch -> gleam@dict:insert(Acc, Index, Patch) end; {error, nil} -> gleam@dict:insert(Acc, Index, {insert, New_child}) end. -spec create( sprocket@internal@reconcile:reconciled_element(), sprocket@internal@reconcile:reconciled_element() ) -> patch(). create(Old, New) -> case {Old, New} of {{reconciled_element, Old_tag, Old_key, Old_attrs, Old_children}, {reconciled_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; {{reconciled_component, Old_fc, _, Old_props, _, Old_el}, {reconciled_component, New_fc, _, New_props, _, New_el}} when (Old_fc =:= New_fc) andalso (Old_props =:= New_props) -> case create(Old_el, New_el) of no_op -> no_op; Patch -> {update, none, {some, [{0, Patch}]}} end; {{reconciled_fragment, _, Old_children@1}, {reconciled_fragment, _, New_children@1}} -> case compare_children(Old_children@1, New_children@1) of {some, Children@1} -> {update, none, {some, Children@1}}; none -> no_op end; {{reconciled_text, Old_text}, {reconciled_text, New_text}} -> case Old_text =:= New_text of true -> no_op; false -> {change, New_text} end; {{reconciled_custom, Old_kind, Old_data}, {reconciled_custom, New_kind, New_data}} -> case {Old_kind =:= New_kind, Old_data =:= New_data} of {true, true} -> no_op; {_, _} -> {replace, New} end; {_, {reconciled_ignore_update, _}} -> no_op; {_, _} -> {replace, New} end. -spec compare_children( list(sprocket@internal@reconcile:reconciled_element()), list(sprocket@internal@reconcile:reconciled_element()) ) -> gleam@option:option(list({integer(), patch()})). compare_children(Old_children, New_children) -> Old_key_map = build_key_map(Old_children), New_key_map = build_key_map(New_children), Removals = begin _pipe = zip_all(Old_children, New_children), gleam@list:index_fold( _pipe, gleam@dict:new(), fun(Acc, Child, Index) -> case Child of {{some, {reconciled_element, _, {some, Old_key}, _, _}}, {some, {reconciled_element, _, {some, _}, _, _}}} -> case gleam@dict:get(New_key_map, Old_key) of {ok, _} -> Acc; {error, nil} -> gleam@dict:insert(Acc, Index, remove) end; {_, none} -> gleam@dict:insert(Acc, Index, remove); _ -> Acc end end ) end, _pipe@1 = New_children, _pipe@2 = gleam@list:index_fold( _pipe@1, Removals, fun(Acc@1, New_child, Index@1) -> case New_child of {reconciled_element, _, {some, Key}, _, _} -> case gleam@dict:get(Old_key_map, Key) of {ok, Found} -> {Old_index, Old_child} = Found, Child_patch = create(Old_child, New_child), case Index@1 =:= Old_index of true -> gleam@dict:insert( Acc@1, Index@1, Child_patch ); false -> gleam@dict:insert( Acc@1, Index@1, {move, Old_index, Child_patch} ) end; {error, nil} -> compare_child_at_index( Acc@1, Old_children, New_child, Index@1 ) end; _ -> compare_child_at_index( Acc@1, Old_children, New_child, Index@1 ) end end ), _pipe@3 = gleam@dict:filter(_pipe@2, fun(_, Child_el) -> case Child_el of no_op -> false; _ -> true end end), (fun(Diff_map) -> case maps:size(Diff_map) > 0 of true -> {some, maps:to_list(Diff_map)}; false -> none end end)(_pipe@3).