-module(glimra). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]). -export([new_syntax_highlighter/0, enable_line_numbers/1, set_trim_source/2, set_theme/2, link_static_stylesheet/0, syntax_highlight/3, codeblock_renderer/1, to_css/1, add_static_stylesheet/2]). -export_type([config/1, no_theme/0, has_theme/0, syntax_highlighting_error/0, highlight_event/0]). -opaque config(VVY) :: {config, boolean(), boolean(), gleam@option:option(glimra@theme:theme())} | {gleam_phantom, VVY}. -type no_theme() :: any(). -type has_theme() :: any(). -type syntax_highlighting_error() :: {unsupported_language, binary()} | tree_sitter_error | unmatched_highlight_events. -type highlight_event() :: {highlight_start, integer()} | {source, integer(), integer()} | highlight_end. -spec new_syntax_highlighter() -> config(no_theme()). new_syntax_highlighter() -> {config, false, true, none}. -spec enable_line_numbers(config(VWK)) -> config(VWK). enable_line_numbers(Config) -> erlang:setelement(2, Config, true). -spec set_trim_source(config(VWN), boolean()) -> config(VWN). set_trim_source(Config, Trim_source) -> erlang:setelement(3, Config, Trim_source). -spec set_theme(config(no_theme()), glimra@theme:theme()) -> config(has_theme()). set_theme(Config, Theme) -> {config, Line_numbers, Trim_source, _} = Config, {config, Line_numbers, Trim_source, {some, Theme}}. -spec parse_highlights(list(integer())) -> {binary(), list(integer())}. parse_highlights(Highlights) -> case Highlights of [Highlight | Rest_of_highlights] -> {libglimra:get_highlight_name(Highlight), Rest_of_highlights}; [] -> {<<""/utf8>>, []} end. -spec prepend_with_snippet( list(lustre@internals@vdom:element(nil)), binary(), binary() ) -> list(lustre@internals@vdom:element(nil)). prepend_with_snippet(Siblings, Highlight_name, Snippet) -> case gleam@string:is_empty(Snippet) of true -> Siblings; false -> [lustre@element@html:span( [lustre@attribute:class(Highlight_name)], [lustre@element@html:text(Snippet)] ) | Siblings] end. -spec pre_styling(config(has_theme())) -> binary(). pre_styling(Config) -> {config, _, _, {some, Theme}} = case Config of {config, _, _, {some, _}} -> Config; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Assertion pattern match failed"/utf8>>, value => _assert_fail, module => <<"glimra"/utf8>>, function => <<"pre_styling"/utf8>>, line => 418}) end, <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"\npre {\n"/utf8, " background-color: rgb("/utf8>>/binary, (gleam@int:to_string( erlang:element( 2, erlang:element( 2, Theme ) ) ))/binary>>/binary, ", "/utf8>>/binary, (gleam@int:to_string( erlang:element( 3, erlang:element( 2, Theme ) ) ))/binary>>/binary, ", "/utf8>>/binary, (gleam@int:to_string( erlang:element( 4, erlang:element( 2, Theme ) ) ))/binary>>/binary, ");\n"/utf8>>/binary, " padding-right: "/utf8>>/binary, (gleam@float:to_string( erlang:element( 3, Theme ) ))/binary>>/binary, "rem;\n"/utf8>>/binary, " padding-left: "/utf8>>/binary, (gleam@float:to_string( erlang:element(3, Theme) ))/binary>>/binary, "rem;\n"/utf8>>/binary, " padding-top: "/utf8>>/binary, (gleam@float:to_string( erlang:element(4, Theme) ))/binary>>/binary, "rem;\n"/utf8>>/binary, " padding-bottom: "/utf8>>/binary, (gleam@float:to_string(erlang:element(4, Theme)))/binary>>/binary, "rem;\n"/utf8>>/binary, " border-radius: "/utf8>>/binary, (gleam@float:to_string(erlang:element(5, Theme)))/binary>>/binary, "rem;\n"/utf8>>/binary, "}\n"/utf8>>. -spec code_styling(config(has_theme())) -> binary(). code_styling(Config) -> {config, Line_numbers, _, _} = Config, case Line_numbers of true -> <<<<<<"\ncode {\n"/utf8, " counter-reset: step;\n"/utf8>>/binary, " counter-increment: step 0;\n"/utf8>>/binary, "}\n"/utf8>>; false -> <<""/utf8>> end. -spec link_static_stylesheet() -> lustre@internals@vdom:element(any()). link_static_stylesheet() -> lustre@element@html:link( [lustre@attribute:href(<<"/glimra.css"/utf8>>), lustre@attribute:rel(<<"stylesheet"/utf8>>)] ). -spec prepend_with_linebreak( list(lustre@internals@vdom:element(nil)), list(lustre@internals@vdom:element(nil)) ) -> list(lustre@internals@vdom:element(nil)). prepend_with_linebreak(Siblings, Children) -> [lustre@element@html:span( [lustre@attribute:class(<<"line"/utf8>>)], lists:reverse(Children) ) | Siblings]. -spec do_syntax_highlight( binary(), list(highlight_event()), list(lustre@internals@vdom:element(nil)), list(lustre@internals@vdom:element(nil)), list(integer()), binary() ) -> {ok, list(lustre@internals@vdom:element(nil))} | {error, syntax_highlighting_error()}. do_syntax_highlight(Source, Events, Code_block, Code_row, Highlights, Snippet) -> {Highlight_name, Rest_of_highlights} = parse_highlights(Highlights), case Events of [Event | Rest] -> case Event of {highlight_start, Highlight_type} -> case Snippet of <<""/utf8>> -> do_syntax_highlight( Source, Rest, Code_block, Code_row, [Highlight_type | Highlights], Snippet ); _ -> do_syntax_highlight( Source, Rest, Code_block, prepend_with_snippet( Code_row, Highlight_name, Snippet ), [Highlight_type | Highlights], <<""/utf8>> ) end; highlight_end -> case gleam@string:is_empty(Highlight_name) andalso gleam@list:is_empty( Rest_of_highlights ) of true -> {error, unmatched_highlight_events}; false -> do_syntax_highlight( Source, Rest, Code_block, prepend_with_snippet( Code_row, Highlight_name, Snippet ), Rest_of_highlights, <<""/utf8>> ) end; {source, Start, End} -> New_snippet = gleam@string:slice(Source, Start, End - Start), case gleam_stdlib:contains_string( New_snippet, <<"\n"/utf8>> ) of true -> _assert_subject = gleam@string:split_once( New_snippet, <<"\n"/utf8>> ), {ok, {Before_linebreak, _}} = case _assert_subject of {ok, {_, _}} -> _assert_subject; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Assertion pattern match failed"/utf8>>, value => _assert_fail, module => <<"glimra"/utf8>>, function => <<"do_syntax_highlight"/utf8>>, line => 325}) end, New_start = (Start + gleam@string:length( Before_linebreak )) + 1, do_syntax_highlight( Source, [{source, New_start, End} | Rest], prepend_with_linebreak( Code_block, prepend_with_snippet( Code_row, Highlight_name, <> ) ), [], Highlights, <<""/utf8>> ); false -> do_syntax_highlight( Source, Rest, Code_block, Code_row, Highlights, <> ) end end; [] -> {ok, prepend_with_linebreak( Code_block, prepend_with_snippet(Code_row, Highlight_name, Snippet) )} end. -spec syntax_highlight(config(any()), binary(), binary()) -> {ok, lustre@internals@vdom:element(nil)} | {error, syntax_highlighting_error()}. syntax_highlight(Config, Source, Language) -> Language@1 = gleam@string:lowercase(Language), Language_supported = gleam@list:contains( libglimra:get_supported_languages(), Language@1 ), gleam@bool:guard( not Language_supported, {error, {unsupported_language, Language@1}}, fun() -> Source@1 = case erlang:element(3, Config) of true -> gleam@string:trim(Source); false -> Source end, gleam@result:'try'( begin _pipe = libglimra:get_highlight_events(Source@1, Language@1), gleam@result:replace_error(_pipe, tree_sitter_error) end, fun(Events) -> gleam@result:'try'( do_syntax_highlight( Source@1, Events, [], [], [], <<""/utf8>> ), fun(Reversed_lines) -> {ok, lustre@element@html:code( [lustre@attribute:attribute( <<"data-lang"/utf8>>, Language@1 )], lists:reverse(Reversed_lines) )} end ) end ) end ). -spec codeblock_renderer(config(has_theme())) -> fun((gleam@dict:dict(binary(), binary()), gleam@option:option(binary()), binary()) -> lustre@internals@vdom:element(nil)). codeblock_renderer(Syntax_highlighter) -> fun(Attrs, Language, Source) -> To_attributes = fun(Attrs@1) -> gleam@dict:fold( Attrs@1, [], fun(Attrs@2, Key, Val) -> [lustre@attribute:attribute(Key, Val) | Attrs@2] end ) end, Language@1 = gleam@option:unwrap(Language, <<"text"/utf8>>), lustre@element@html:pre( To_attributes(Attrs), [begin _pipe = Syntax_highlighter, _pipe@1 = syntax_highlight(_pipe, Source, Language@1), gleam@result:unwrap( _pipe@1, lustre@element@html:code( [lustre@attribute:attribute( <<"data-lang"/utf8>>, Language@1 )], [lustre@element:text(Source)] ) ) end] ) end. -spec line_styling(config(has_theme())) -> binary(). line_styling(Config) -> {config, Line_numbers, _, _} = Config, case Line_numbers of true -> <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"\n."/utf8, "line"/utf8>>/binary, " { display: block; }\n"/utf8>>/binary, "\n."/utf8>>/binary, "line"/utf8>>/binary, "::before {\n"/utf8>>/binary, " content: counter(step);\n"/utf8>>/binary, " counter-increment: step;\n"/utf8>>/binary, " width: 1rem;\n"/utf8>>/binary, " margin-right: 1.5rem;\n"/utf8>>/binary, " display: inline-block;\n"/utf8>>/binary, " text-align: right;\n"/utf8>>/binary, " color: rgba(115,138,148,.4);\n"/utf8>>/binary, "}\n\n."/utf8>>/binary, "line"/utf8>>/binary, ":empty::before { content: counter(step) \"\\200b\" }\n"/utf8>>; false -> <<<<<<<<<<"\n."/utf8, "line"/utf8>>/binary, " { display: block; }\n"/utf8>>/binary, "\n."/utf8>>/binary, "line"/utf8>>/binary, ":empty::before { content: \"\\200b\"; }\n"/utf8>> end. -spec to_css(config(has_theme())) -> binary(). to_css(Config) -> {config, _, _, {some, Theme}} = case Config of {config, _, _, {some, _}} -> Config; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Assertion pattern match failed"/utf8>>, value => _assert_fail, module => <<"glimra"/utf8>>, function => <<"to_css"/utf8>>, line => 114}) end, _pipe = gleam@string_builder:new(), _pipe@1 = gleam@string_builder:append( _pipe, <<"\n/* generated by glimra */\n"/utf8>> ), _pipe@2 = gleam@string_builder:append(_pipe@1, pre_styling(Config)), _pipe@3 = gleam@string_builder:append(_pipe@2, code_styling(Config)), _pipe@4 = gleam@string_builder:append(_pipe@3, line_styling(Config)), _pipe@5 = gleam@string_builder:append(_pipe@4, glimra@theme:to_css(Theme)), gleam@string_builder:to_string(_pipe@5). -spec add_static_stylesheet( lustre@ssg:config(VWS, VWT, VWU), config(has_theme()) ) -> lustre@ssg:config(VWS, VWT, VWU). add_static_stylesheet(Ssg_config, Syntax_highlighter) -> lustre@ssg:add_static_asset( Ssg_config, <<"/glimra.css"/utf8>>, to_css(Syntax_highlighter) ).