-module(jot). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function]). -export([document_to_html/1, parse/1, to_html/1]). -export_type([document/0, container/0, inline/0, destination/0]). -type document() :: {document, list(container()), gleam@dict:dict(binary(), binary())}. -type container() :: {paragraph, list(inline())} | {heading, integer(), list({binary(), binary()}), list(inline())} | {codeblock, gleam@option:option(binary()), binary()}. -type inline() :: {text, binary()} | {link, list(inline()), destination()}. -type destination() :: {reference, binary()} | {url, binary()}. -spec drop_lines(list(binary())) -> list(binary()). drop_lines(In) -> case In of [] -> []; [<<"\n"/utf8>> | Rest] -> drop_lines(Rest); [C | Rest@1] -> [C | Rest@1] end. -spec drop_spaces(list(binary())) -> list(binary()). drop_spaces(In) -> case In of [] -> []; [<<" "/utf8>> | Rest] -> drop_spaces(Rest); [C | Rest@1] -> [C | Rest@1] end. -spec slurp_verbatim_line(list(binary()), binary()) -> {binary(), list(binary())}. slurp_verbatim_line(In, Acc) -> case In of [] -> {Acc, []}; [<<"\n"/utf8>> | In@1] -> {<>, In@1}; [C | In@2] -> slurp_verbatim_line(In@2, <>) end. -spec parse_codeblock_end(list(binary()), binary(), integer()) -> gleam@option:option({list(binary())}). parse_codeblock_end(In, Delim, Count) -> case In of [<<"\n"/utf8>> | In@1] when Count =:= 0 -> {some, {In@1}}; _ when Count =:= 0 -> {some, {In}}; [C | In@2] when C =:= Delim -> parse_codeblock_end(In@2, Delim, Count - 1); [] -> {some, {In}}; _ -> none end. -spec parse_codeblock_content(list(binary()), binary(), integer(), binary()) -> {binary(), list(binary())}. parse_codeblock_content(In, Delim, Count, Acc) -> case parse_codeblock_end(In, Delim, Count) of none -> {Acc@1, In@1} = slurp_verbatim_line(In, Acc), parse_codeblock_content(In@1, Delim, Count, Acc@1); {some, {In@2}} -> {Acc, In@2} end. -spec parse_codeblock_language(list(binary()), binary()) -> {gleam@option:option(binary()), list(binary())}. parse_codeblock_language(In, Language) -> case In of [] -> {none, In}; [<<"\n"/utf8>> | In@1] when Language =:= <<""/utf8>> -> {none, In@1}; [<<"\n"/utf8>> | In@2] -> {{some, Language}, In@2}; [C | In@3] -> parse_codeblock_language(In@3, <>) end. -spec parse_codeblock_start(list(binary()), binary(), integer()) -> gleam@option:option({gleam@option:option(binary()), integer(), list(binary())}). parse_codeblock_start(In, Delim, Count) -> case In of [C | In@1] when C =:= Delim -> parse_codeblock_start(In@1, Delim, Count + 1); [<<"\n"/utf8>> | In@2] when Count >= 3 -> {some, {none, Count, In@2}}; [_ | _] when Count >= 3 -> In@3 = drop_spaces(In), {Language, In@4} = parse_codeblock_language(In@3, <<""/utf8>>), {some, {Language, Count, In@4}}; _ -> none end. -spec parse_codeblock(list(binary()), binary()) -> gleam@option:option({container(), list(binary())}). parse_codeblock(In, Delim) -> gleam@option:then( parse_codeblock_start(In, Delim, 1), fun(_use0) -> {Language, Count, In@1} = _use0, {Content, In@2} = parse_codeblock_content( In@1, Delim, Count, <<""/utf8>> ), {some, {{codeblock, Language, Content}, In@2}} end ). -spec parse_ref_value(list(binary()), binary(), binary()) -> gleam@option:option({binary(), binary(), list(binary())}). parse_ref_value(In, Id, Url) -> case In of [] -> {some, {Id, gleam@string:trim(Url), []}}; [<<"\n"/utf8>> | In@1] -> {some, {Id, gleam@string:trim(Url), In@1}}; [C | In@2] -> parse_ref_value(In@2, Id, <>) end. -spec parse_ref_def(list(binary()), binary()) -> gleam@option:option({binary(), binary(), list(binary())}). parse_ref_def(In, Id) -> case In of [<<"]"/utf8>>, <<":"/utf8>> | In@1] -> parse_ref_value(In@1, Id, <<""/utf8>>); [] -> none; [<<"]"/utf8>> | _] -> none; [C | In@2] -> parse_ref_def(In@2, <>) end. -spec id_char(binary()) -> boolean(). id_char(Char) -> case Char of <<"#"/utf8>> -> false; <<"?"/utf8>> -> false; <<"!"/utf8>> -> false; _ -> true end. -spec id_escape(list(binary()), binary()) -> binary(). id_escape(Content, Acc) -> case Content of [] -> Acc; [<<" "/utf8>> | Rest] when Rest =:= [] -> Acc; [<<"\n"/utf8>> | Rest] when Rest =:= [] -> Acc; [<<" "/utf8>> | Rest@1] when Acc =:= <<""/utf8>> -> id_escape(Rest@1, Acc); [<<"\n"/utf8>> | Rest@1] when Acc =:= <<""/utf8>> -> id_escape(Rest@1, Acc); [<<" "/utf8>> | Rest@2] -> id_escape(Rest@2, <>); [<<"\n"/utf8>> | Rest@2] -> id_escape(Rest@2, <>); [C | Rest@3] -> id_escape(Rest@3, <>) end. -spec id_sanitise(binary()) -> binary(). id_sanitise(Content) -> _pipe = Content, _pipe@1 = gleam@string:to_graphemes(_pipe), _pipe@2 = gleam@list:filter(_pipe@1, fun id_char/1), id_escape(_pipe@2, <<""/utf8>>). -spec take_heading_chars_newline_hash(list(binary()), integer(), list(binary())) -> gleam@option:option({list(binary()), list(binary())}). take_heading_chars_newline_hash(In, Level, Acc) -> case In of _ when Level < 0 -> none; [] when Level > 0 -> none; [] when Level =:= 0 -> {some, {Acc, []}}; [<<" "/utf8>> | In@1] when Level =:= 0 -> {some, {Acc, In@1}}; [<<"#"/utf8>> | Rest] -> take_heading_chars_newline_hash(Rest, Level - 1, Acc); _ -> none end. -spec take_heading_chars(list(binary()), integer(), list(binary())) -> {list(binary()), list(binary())}. take_heading_chars(In, Level, Acc) -> case In of [] -> {gleam@list:reverse(Acc), []}; [<<"\n"/utf8>>] -> {gleam@list:reverse(Acc), []}; [<<"\n"/utf8>>, <<"\n"/utf8>> | In@1] -> {gleam@list:reverse(Acc), In@1}; [<<"\n"/utf8>>, <<"#"/utf8>> | Rest] -> case take_heading_chars_newline_hash( Rest, Level - 1, [<<"\n"/utf8>> | Acc] ) of {some, {Acc@1, In@2}} -> take_heading_chars(In@2, Level, Acc@1); none -> {gleam@list:reverse(Acc), In} end; [C | In@3] -> take_heading_chars(In@3, Level, [C | Acc]) end. -spec take_link_chars_destination( list(binary()), boolean(), list(binary()), binary() ) -> gleam@option:option({list(binary()), destination(), list(binary())}). take_link_chars_destination(In, Is_url, Inline_in, Acc) -> case In of [] -> none; [<<")"/utf8>> | In@1] when Is_url -> {some, {Inline_in, {url, Acc}, In@1}}; [<<"]"/utf8>> | In@2] when not Is_url -> {some, {Inline_in, {reference, Acc}, In@2}}; [<<"\n"/utf8>> | Rest] -> take_link_chars_destination(Rest, Is_url, Inline_in, Acc); [C | Rest@1] -> take_link_chars_destination( Rest@1, Is_url, Inline_in, <> ) end. -spec take_link_chars(list(binary()), list(binary())) -> gleam@option:option({list(binary()), destination(), list(binary())}). take_link_chars(In, Inline_in) -> case In of [] -> none; [<<"]"/utf8>>, <<"["/utf8>> | In@1] -> Inline_in@1 = gleam@list:reverse(Inline_in), take_link_chars_destination(In@1, false, Inline_in@1, <<""/utf8>>); [<<"]"/utf8>>, <<"("/utf8>> | In@2] -> Inline_in@2 = gleam@list:reverse(Inline_in), take_link_chars_destination(In@2, true, Inline_in@2, <<""/utf8>>); [C | Rest] -> take_link_chars(Rest, [C | Inline_in]) end. -spec heading_level(list(binary()), integer()) -> gleam@option:option({integer(), list(binary())}). heading_level(In, Level) -> case In of [<<"#"/utf8>> | Rest] -> heading_level(Rest, Level + 1); [] when Level > 0 -> {some, {Level, []}}; [<<" "/utf8>> | Rest@1] when Level =/= 0 -> {some, {Level, Rest@1}}; [<<"\n"/utf8>> | Rest@1] when Level =/= 0 -> {some, {Level, Rest@1}}; _ -> none end. -spec take_inline_text(list(inline()), binary()) -> binary(). take_inline_text(Inlines, Acc) -> case Inlines of [] -> Acc; [First | Rest] -> case First of {text, Text} -> take_inline_text(Rest, <>); {link, Nested, _} -> Acc@1 = take_inline_text(Nested, Acc), take_inline_text(Rest, Acc@1) end end. -spec take_paragraph_chars(list(binary()), list(binary())) -> {list(binary()), list(binary())}. take_paragraph_chars(In, Acc) -> case In of [] -> {gleam@list:reverse(Acc), []}; [<<"\n"/utf8>>] -> {gleam@list:reverse(Acc), []}; [<<"\n"/utf8>>, <<"\n"/utf8>> | Rest] -> {gleam@list:reverse(Acc), Rest}; [C | Rest@1] -> take_paragraph_chars(Rest@1, [C | Acc]) end. -spec close_tag(binary(), binary()) -> binary(). close_tag(Html, Tag) -> <<<<<>/binary, Tag/binary>>/binary, ">"/utf8>>. -spec destination_attribute(destination(), gleam@dict:dict(binary(), binary())) -> list({binary(), binary()}). destination_attribute(Destination, Refs) -> case Destination of {reference, Id} -> case gleam@dict:get(Refs, Id) of {ok, Url} -> [{<<"href"/utf8>>, Url}]; {error, nil} -> [] end; {url, Url@1} -> [{<<"href"/utf8>>, Url@1}] end. -spec attributes_to_html(binary(), list({binary(), binary()})) -> binary(). attributes_to_html(Html, Attributes) -> case Attributes of [] -> Html; [{Key, Value} | Rest] -> Html@1 = <<<<<<<<<>/binary, Key/binary>>/binary, "=\""/utf8>>/binary, Value/binary>>/binary, "\""/utf8>>, attributes_to_html(Html@1, Rest) end. -spec open_tag(binary(), binary(), list({binary(), binary()})) -> binary(). open_tag(Html, Tag, Attributes) -> Html@1 = <<<>/binary, Tag/binary>>, <<(attributes_to_html(Html@1, Attributes))/binary, ">"/utf8>>. -spec inline_to_html(binary(), inline(), gleam@dict:dict(binary(), binary())) -> binary(). inline_to_html(Html, Inline, Refs) -> case Inline of {text, Text} -> <>; {link, Text@1, Destination} -> _pipe = Html, _pipe@1 = open_tag( _pipe, <<"a"/utf8>>, destination_attribute(Destination, Refs) ), _pipe@2 = inlines_to_html(_pipe@1, Text@1, Refs), close_tag(_pipe@2, <<"a"/utf8>>) end. -spec inlines_to_html( binary(), list(inline()), gleam@dict:dict(binary(), binary()) ) -> binary(). inlines_to_html(Html, Inlines, Refs) -> case Inlines of [] -> Html; [Inline | Rest] -> _pipe = Html, _pipe@1 = inline_to_html(_pipe, Inline, Refs), _pipe@2 = inlines_to_html(_pipe@1, Rest, Refs), gleam@string:trim_right(_pipe@2) end. -spec container_to_html( binary(), container(), gleam@dict:dict(binary(), binary()) ) -> binary(). container_to_html(Html, Container, Refs) -> <<(case Container of {paragraph, Inlines} -> _pipe = Html, _pipe@1 = open_tag(_pipe, <<"p"/utf8>>, []), _pipe@2 = inlines_to_html(_pipe@1, Inlines, Refs), close_tag(_pipe@2, <<"p"/utf8>>); {codeblock, Language, Content} -> Code_attrs = case Language of {some, Lang} -> [{<<"class"/utf8>>, <<"language-"/utf8, Lang/binary>>}]; none -> [] end, _pipe@3 = Html, _pipe@4 = open_tag(_pipe@3, <<"pre"/utf8>>, []), _pipe@5 = open_tag(_pipe@4, <<"code"/utf8>>, Code_attrs), _pipe@6 = gleam@string:append(_pipe@5, Content), _pipe@7 = close_tag(_pipe@6, <<"code"/utf8>>), close_tag(_pipe@7, <<"pre"/utf8>>); {heading, Level, Attributes, Inlines@1} -> Tag = <<"h"/utf8, (gleam@int:to_string(Level))/binary>>, _pipe@8 = Html, _pipe@9 = open_tag(_pipe@8, Tag, Attributes), _pipe@10 = inlines_to_html(_pipe@9, Inlines@1, Refs), close_tag(_pipe@10, Tag) end)/binary, "\n"/utf8>>. -spec containers_to_html( list(container()), gleam@dict:dict(binary(), binary()), binary() ) -> binary(). containers_to_html(Containers, Refs, Html) -> case Containers of [] -> Html; [Container | Rest] -> Html@1 = container_to_html(Html, Container, Refs), containers_to_html(Rest, Refs, Html@1) end. -spec document_to_html(document()) -> binary(). document_to_html(Document) -> containers_to_html( erlang:element(2, Document), erlang:element(3, Document), <<""/utf8>> ). -spec parse_link(list(binary())) -> gleam@option:option({inline(), list(binary())}). parse_link(In) -> case take_link_chars(In, []) of none -> none; {some, {Inline_in, Ref, In@1}} -> Inline = parse_inline(Inline_in, <<""/utf8>>, []), Link = {link, Inline, Ref}, {some, {Link, In@1}} end. -spec parse_inline(list(binary()), binary(), list(inline())) -> list(inline()). parse_inline(In, Text, Acc) -> case In of [] when Text =:= <<""/utf8>> -> gleam@list:reverse(Acc); [] -> parse_inline([], <<""/utf8>>, [{text, Text} | Acc]); [<<"["/utf8>> | Rest] -> case parse_link(Rest) of none -> parse_inline(Rest, <>, Acc); {some, {Link, In@1}} -> parse_inline(In@1, <<""/utf8>>, [Link | Acc]) end; [C | Rest@1] -> parse_inline(Rest@1, <>, Acc) end. -spec parse_paragraph(list(binary())) -> {container(), list(binary())}. parse_paragraph(In) -> {Inline_in, In@1} = take_paragraph_chars(In, []), Inline = parse_inline(Inline_in, <<""/utf8>>, []), {{paragraph, Inline}, In@1}. -spec parse_heading(list(binary()), gleam@dict:dict(binary(), binary())) -> {container(), gleam@dict:dict(binary(), binary()), list(binary())}. parse_heading(In, Refs) -> case heading_level(In, 1) of {some, {Level, In@1}} -> In@2 = drop_spaces(In@1), {Inline_in, In@3} = take_heading_chars(In@2, Level, []), Inline = parse_inline(Inline_in, <<""/utf8>>, []), Text = take_inline_text(Inline, <<""/utf8>>), {Refs@1, Attrs} = case id_sanitise(Text) of <<""/utf8>> -> {Refs, []}; Id -> {gleam@dict:insert(Refs, Id, <<"#"/utf8, Id/binary>>), [{<<"id"/utf8>>, Id}]} end, Heading = {heading, Level, Attrs, Inline}, {Heading, Refs@1, In@3}; none -> {P, In@4} = parse_paragraph([<<"#"/utf8>> | In]), {P, Refs, In@4} end. -spec parse_document( list(binary()), gleam@dict:dict(binary(), binary()), list(container()) ) -> document(). parse_document(In, Refs, Ast) -> In@1 = drop_lines(In), In@2 = drop_spaces(In@1), case In@2 of [] -> {document, gleam@list:reverse(Ast), Refs}; [<<"#"/utf8>> | In@3] -> {Heading, Refs@1, In@4} = parse_heading(In@3, Refs), parse_document(In@4, Refs@1, [Heading | Ast]); [<<"~"/utf8>> = Delim | In2] -> case parse_codeblock(In2, Delim) of none -> {Paragraph, In@5} = parse_paragraph(In@2), parse_document(In@5, Refs, [Paragraph | Ast]); {some, {Codeblock, In@6}} -> parse_document(In@6, Refs, [Codeblock | Ast]) end; [<<"`"/utf8>> = Delim | In2] -> case parse_codeblock(In2, Delim) of none -> {Paragraph, In@5} = parse_paragraph(In@2), parse_document(In@5, Refs, [Paragraph | Ast]); {some, {Codeblock, In@6}} -> parse_document(In@6, Refs, [Codeblock | Ast]) end; [<<"["/utf8>> | In2@1] -> case parse_ref_def(In2@1, <<""/utf8>>) of none -> {Paragraph@1, In@7} = parse_paragraph(In@2), parse_document(In@7, Refs, [Paragraph@1 | Ast]); {some, {Id, Url, In@8}} -> Refs@2 = gleam@dict:insert(Refs, Id, Url), parse_document(In@8, Refs@2, Ast) end; _ -> {Paragraph@2, In@9} = parse_paragraph(In@2), parse_document(In@9, Refs, [Paragraph@2 | Ast]) end. -spec parse(binary()) -> document(). parse(Djot) -> _pipe = Djot, _pipe@1 = gleam@string:replace(_pipe, <<"\r\n"/utf8>>, <<"\n"/utf8>>), _pipe@2 = gleam@string:to_graphemes(_pipe@1), parse_document(_pipe@2, gleam@dict:new(), []). -spec to_html(binary()) -> binary(). to_html(Djot) -> _pipe = Djot, _pipe@1 = parse(_pipe), document_to_html(_pipe@1).