-module(checkmark). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/checkmark.gleam"). -export([new/2, document/2, file/2, comments_in/2, should_contain_contents_of/3, should_contain_snippet_from/4, load_snippet_source/2, update/1, check/1, check_or_update/2]). -export_type([checker/1, file/1, code_snippet_source/0, check_error/1, expectation/0, code_segment/0, snippet/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(" Keep code blocks in markdown files or Gleam comments up to date.\n"). -opaque checker(HMF) :: {checker, fun((binary()) -> {ok, binary()} | {error, HMF}), fun((binary(), binary()) -> {ok, nil} | {error, HMF})}. -opaque file(HMG) :: {file, binary(), checker(HMG), boolean(), list(expectation())}. -opaque code_snippet_source() :: {code_snippet_source, binary(), checkmark@internal@code_extractor:file()}. -type check_error(HMH) :: {could_not_read_file, HMH} | {could_not_write_file, HMH} | {tag_not_found, binary()} | {multiple_tags_found, binary(), list(integer())} | {content_did_not_match, binary()} | {failed_to_load_code_segment, binary(), binary()} | could_not_parse_snippet_source. -type expectation() :: {contents_of_file, binary(), binary()} | {code_segment, binary(), binary(), checkmark@internal@code_extractor:file(), code_segment()}. -type code_segment() :: {function, binary()} | {function_body, binary()} | {type_definition, binary()}. -type snippet() :: {snippet, list(binary()), checkmark@internal@parser:fence(), gleam@option:option(checkmark@internal@parser:fence())}. -file("src/checkmark.gleam", 80). ?DOC(" Builds a new checker with the provided file IO functions.\n"). -spec new( fun((binary()) -> {ok, binary()} | {error, HMI}), fun((binary(), binary()) -> {ok, nil} | {error, HMI}) ) -> checker(HMI). new(Read_file, Write_file) -> {checker, Read_file, Write_file}. -file("src/checkmark.gleam", 96). ?DOC( " Starts configuring a markdown file to be checked or updated.\n" " See [`should_contain_contents_of`](#should_contain_contents_of)\n" " and [`should_contain_snippet_from`](#should_contain_snippet_from)\n" " to configure the content.\n" ). -spec document(checker(HMR), binary()) -> file(HMR). document(Checker, Filename) -> {file, Filename, Checker, false, []}. -file("src/checkmark.gleam", 88). -spec file(checker(HMO), binary()) -> file(HMO). file(Checker, Filename) -> document(Checker, Filename). -file("src/checkmark.gleam", 105). ?DOC( " Starts configuring code blocks in comments of\n" " a Gleam source file to be checked or updated.\n" " See [`should_contain_contents_of`](#should_contain_contents_of)\n" " and [`should_contain_snippet_from`](#should_contain_snippet_from)\n" " to configure the content.\n" ). -spec comments_in(checker(HMU), binary()) -> file(HMU). comments_in(Checker, Filename) -> {file, Filename, Checker, true, []}. -file("src/checkmark.gleam", 134). ?DOC( " Specify that the file should contain the contents of another file as a code block.\n" " For example\n" "\n" " ```gleam contents_of\n" " checker\n" " |> checkmark.document(\"README.md\")\n" " |> checkmark.should_contain_contents_of(\"./example.sh\", tagged: \"sh deps\")\n" " ```\n" "\n" " will replace the code block starting with ```` ```sh deps````\n" " with the contents of `example.sh`.\n" " Whitespace is trimmed off the tag when matching it.\n" " Note that you still need to call `check`, `update` or `check_or_update` after this -\n" " this function only adds to the configuration.\n" ). -spec should_contain_contents_of(file(HNC), binary(), binary()) -> file(HNC). should_contain_contents_of(File, Filename, Tag) -> {file, erlang:element(2, File), erlang:element(3, File), erlang:element(4, File), [{contents_of_file, Tag, Filename} | erlang:element(5, File)]}. -file("src/checkmark.gleam", 163). ?DOC( " Specify that the file should contain a code snippet from another file.\n" " For example\n" "\n" " ```gleam snippet_from\n" " checker\n" " |> checkmark.document(\"README.md\")\n" " |> checkmark.should_contain_snippet_from(\n" " snippets,\n" " checkmark.Function(\"wibble\"),\n" " tagged: \"wibble\",\n" " )\n" " ```\n" "\n" " will replace the code block starting with ```` ```gleam wibble````\n" " with the full definition of the `wibble` function.\n" " Whitespace is trimmed off the tag when matching it.\n" " Note that you still need to call `check`, `update` or `check_or_update` after this -\n" " this function only adds to the configuration.\n" ). -spec should_contain_snippet_from( file(HNF), code_snippet_source(), code_segment(), binary() ) -> file(HNF). should_contain_snippet_from(File, Source, Segment, Tag) -> {file, erlang:element(2, File), erlang:element(3, File), erlang:element(4, File), [{code_segment, <<"gleam "/utf8, Tag/binary>>, erlang:element(2, Source), erlang:element(3, Source), Segment} | erlang:element(5, File)]}. -file("src/checkmark.gleam", 304). -spec render_fence(binary(), checkmark@internal@parser:fence()) -> binary(). render_fence(Prefix, Fence) -> <<<<<>, erlang:element(4, Fence)))/binary>>/binary, (erlang:element(2, Fence))/binary>>/binary, (erlang:element(3, Fence))/binary>>. -file("src/checkmark.gleam", 274). -spec render_code( checkmark@internal@parser:fence(), binary(), list(binary()), gleam@option:option(checkmark@internal@parser:fence()) ) -> list(binary()). render_code(Start_fence, Prefix, Content, End_fence) -> End_fence@1 = gleam@option:map( End_fence, fun(_capture) -> render_fence(Prefix, _capture) end ), Without_end_fence = case {erlang:element(4, Start_fence), Prefix, End_fence@1} of {0, <<""/utf8>>, none} -> Content; {0, <<""/utf8>>, {some, End_fence@2}} -> lists:append(Content, [End_fence@2]); {Amount, _, _} -> Indent = <>, Amount))/binary>>, Content@1 = begin _pipe = Content, gleam@list:fold( _pipe, [], fun(Lines, Line) -> [gleam@string:append(Indent, Line) | Lines] end ) end, Content@2 = case End_fence@1 of none -> Content@1; {some, Fence} -> [Fence | Content@1] end, lists:reverse(Content@2) end, [render_fence(Prefix, Start_fence) | Without_end_fence]. -file("src/checkmark.gleam", 254). -spec render_replacements( list(checkmark@internal@parser:section()), gleam@dict:dict(binary(), list(binary())) ) -> binary(). render_replacements(Contents, Replacements) -> _pipe = Contents, _pipe@1 = gleam@list:flat_map(_pipe, fun(Section) -> case Section of {fenced_code, _, Prefix, Lines, Start_fence, End_fence} -> case gleam_stdlib:map_get( Replacements, gleam@string:trim(erlang:element(3, Start_fence)) ) of {ok, Replacement} -> render_code( Start_fence, Prefix, Replacement, End_fence ); {error, _} -> render_code(Start_fence, Prefix, Lines, End_fence) end; {other, _, Lines@1} -> Lines@1 end end), gleam@string:join(_pipe@1, <<""/utf8>>). -file("src/checkmark.gleam", 316). -spec read_file(checker(HOU), binary()) -> {ok, binary()} | {error, check_error(HOU)}. read_file(Checker, Filename) -> _pipe = (erlang:element(2, Checker))(Filename), gleam@result:map_error( _pipe, fun(Field@0) -> {could_not_read_file, Field@0} end ). -file("src/checkmark.gleam", 110). ?DOC(" Loads a Gleam source file to extract snippets from.\n"). -spec load_snippet_source(checker(HMX), binary()) -> {ok, code_snippet_source()} | {error, check_error(HMX)}. load_snippet_source(Checker, Filename) -> gleam@result:'try'( read_file(Checker, Filename), fun(Content) -> _pipe = checkmark@internal@code_extractor:load(Content), _pipe@1 = gleam@result:replace_error( _pipe, could_not_parse_snippet_source ), gleam@result:map( _pipe@1, fun(_capture) -> {code_snippet_source, Filename, _capture} end ) end ). -file("src/checkmark.gleam", 324). -spec read_lines(checker(HOZ), binary()) -> {ok, list(binary())} | {error, check_error(HOZ)}. read_lines(Checker, Filename) -> gleam@result:map( read_file(Checker, Filename), fun(Content) -> _pipe = splitter:new([<<"\n"/utf8>>, <<"\r\n"/utf8>>]), checkmark@internal@lines:to_lines(_pipe, Content, []) end ). -file("src/checkmark.gleam", 230). -spec get_expected_lines(checker(HOA), expectation()) -> {ok, list(binary())} | {error, check_error(HOA)}. get_expected_lines(Checker, Expectation) -> case Expectation of {contents_of_file, _, Filename} -> read_lines(Checker, Filename); {code_segment, _, _, File, Segment} -> _pipe = case Segment of {function, Name} -> checkmark@internal@code_extractor:extract_function( File, Name ); {function_body, Name@1} -> checkmark@internal@code_extractor:extract_function_body( File, Name@1 ); {type_definition, Name@2} -> checkmark@internal@code_extractor:extract_type(File, Name@2) end, gleam@result:map_error( _pipe, fun(E) -> Reason = case E of {name_not_found, Name@3} -> <<"Could not find "/utf8, Name@3/binary>>; span_extraction_failed -> <<"Extracting snippet failed, please report a bug!"/utf8>> end, {failed_to_load_code_segment, erlang:element(3, Expectation), Reason} end ) end. -file("src/checkmark.gleam", 308). -spec parse_file(file(HON)) -> {ok, list(checkmark@internal@parser:section())} | {error, list(check_error(HON))}. parse_file(File) -> _pipe = read_lines(erlang:element(3, File), erlang:element(2, File)), _pipe@1 = gleam@result:map_error(_pipe, fun gleam@list:wrap/1), gleam@result:map( _pipe@1, fun(_capture) -> checkmark@internal@parser:parse(_capture, erlang:element(4, File)) end ). -file("src/checkmark.gleam", 210). ?DOC( " Updates the code blocks in the target file.\n" " See [`should_contain_contents_of`](#should_contain_contents_of)\n" " and [`should_contain_snippet_from`](#should_contain_snippet_from)\n" " to configure the content.\n" ). -spec update(file(HNU)) -> {ok, nil} | {error, list(check_error(HNU))}. update(File) -> gleam@result:'try'( parse_file(File), fun(Contents) -> Results = begin gleam@list:map( erlang:element(5, File), fun(Expectation) -> gleam@result:'try'( get_expected_lines( erlang:element(3, File), Expectation ), fun(Lines) -> {ok, {erlang:element(2, Expectation), Lines}} end ) end ) end, {Replacements, Errors} = gleam@result:partition(Results), case Errors of [] -> Contents@1 = render_replacements( Contents, maps:from_list(Replacements) ), _pipe = (erlang:element(3, erlang:element(3, File)))( erlang:element(2, File), Contents@1 ), gleam@result:map_error( _pipe, fun(E) -> [{could_not_write_file, E}] end ); _ -> {error, Errors} end end ). -file("src/checkmark.gleam", 348). -spec find_match( list(checkmark@internal@parser:section()), binary(), list({integer(), snippet()}) ) -> {ok, snippet()} | {error, check_error(any())}. find_match(Content, Tag, Found) -> case Content of [] -> case Found of [] -> {error, {tag_not_found, Tag}}; [{_, Snippet}] -> {ok, Snippet}; _ -> {error, {multiple_tags_found, Tag, begin _pipe = Found, _pipe@1 = lists:reverse(_pipe), gleam@list:map( _pipe@1, fun(Pair) -> erlang:element(1, Pair) end ) end}} end; [{fenced_code, Line_number, _, Lines, Start_fence, End_fence} | Rest] -> case gleam@string:trim(erlang:element(3, Start_fence)) =:= Tag of true -> Result = {Line_number, {snippet, Lines, Start_fence, End_fence}}, find_match(Rest, Tag, [Result | Found]); false -> find_match(Rest, Tag, Found) end; [_ | Rest@1] -> find_match(Rest@1, Tag, Found) end. -file("src/checkmark.gleam", 332). -spec check_one( list(checkmark@internal@parser:section()), list(binary()), binary() ) -> {ok, nil} | {error, check_error(any())}. check_one(Content, Expected, Tag) -> gleam@result:'try'( find_match(Content, Tag, []), fun(Snippet) -> case erlang:element(2, Snippet) =:= Expected of true -> {ok, nil}; false -> {error, {content_did_not_match, Tag}} end end ). -file("src/checkmark.gleam", 190). ?DOC( " Checks that the target file contain the expected code blocks.\n" " See [`should_contain_contents_of`](#should_contain_contents_of)\n" " and [`should_contain_snippet_from`](#should_contain_snippet_from)\n" " to configure the content.\n" ). -spec check(file(HNO)) -> {ok, nil} | {error, list(check_error(HNO))}. check(File) -> gleam@result:'try'( parse_file(File), fun(Contents) -> Results = begin gleam@list:map( erlang:element(5, File), fun(Expectation) -> gleam@result:'try'( get_expected_lines( erlang:element(3, File), Expectation ), fun(Lines) -> check_one( Contents, Lines, erlang:element(2, Expectation) ) end ) end ) end, {_, Errors} = gleam@result:partition(Results), case Errors of [] -> {ok, nil}; _ -> {error, Errors} end end ). -file("src/checkmark.gleam", 176). ?DOC(" Convenience function for either checking or updating depending on a boolean.\n"). -spec check_or_update(file(HNI), boolean()) -> {ok, nil} | {error, list(check_error(HNI))}. check_or_update(File, Should_update) -> case Should_update of true -> update(File); false -> check(File) end.