-module(spruce@markdown).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/spruce/markdown.gleam").
-export([dark_theme/0, light_theme/0, adaptive_theme/0, default_options/0, with_theme/2, with_width/2, render_with/3, render/2, print/2]).
-export_type([theme/0, options/0, alert/0, alert_kind/0, fence/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(
" Markdown to ANSI terminal rendering.\n"
"\n"
" Rendering is driven by the [`mork`](https://codeberg.org/krig/mork)\n"
" parser (canonical home: <https://git.liten.app/krig/mork>) and walks its\n"
" document AST directly rather than going through `mork`'s HTML output.\n"
"\n"
" ## GFM support\n"
"\n"
" The following GitHub Flavored Markdown extensions are supported:\n"
"\n"
" - Tables (rendered via `spruce/table`)\n"
" - Task list items (`- [x]` / `- [ ]`)\n"
" - Strikethrough (`~~text~~`)\n"
" - Extended autolinks for bare URLs and `www.` links\n"
"\n"
" In addition, GitHub-style alerts (`> [!NOTE]`, `[!TIP]`, `[!IMPORTANT]`,\n"
" `[!WARNING]`, `[!CAUTION]`) and Astro/Starlight `:::type[Title]` container\n"
" directives are rendered as colored callouts.\n"
"\n"
" ## Known limitations\n"
"\n"
" These are GitHub/Markdown features that are *not* rendered. Most stem from\n"
" upstream `mork` (tracked in its\n"
" [TODO.md](https://git.liten.app/krig/mork/src/branch/main/TODO.md)); a few\n"
" are deliberate choices here.\n"
"\n"
" - **Emoji shortcodes** (`:rocket:`) are not expanded. `mork` only expands\n"
" them in its HTML output path, not in the document AST this module\n"
" renders, so enabling `mork.emojis` has no effect here.\n"
" - **Email autolinks** (bare `me@example.com`) are not linked; extended\n"
" email autolinking is unimplemented upstream (`mork` TODO: \"autolink\n"
" (email)\").\n"
" - **Footnotes**: a `[^1]` reference renders as literal `[^1]` and the\n"
" definition body is dropped. Footnote bodies are not yet implemented\n"
" upstream, and inline footnotes (`^[...]`) are unsupported.\n"
" - **GFM table column alignment** (`:--`, `:-:`, `--:`) is parsed by `mork`\n"
" but ignored here: every cell is left-aligned, because `spruce/table`\n"
" does not expose per-column alignment.\n"
" - **Heading ID attributes** (`## Title {#id}`) are stripped from the\n"
" rendered text (via `mork.heading_ids`), since a terminal has no anchors\n"
" to link to. The id itself is parsed but not rendered.\n"
" - **Raw HTML is not sanitized.** Inline and block HTML is passed through\n"
" (rendered dimmed), not escaped or stripped. `mork` does not implement\n"
" GFM's tagfilter. This is harmless in a terminal, but do not rely on this\n"
" module to neutralize untrusted HTML.\n"
"\n"
).
-opaque theme() :: {theme,
spruce@style:style(),
spruce@style:style(),
spruce@style:style(),
spruce@style:style(),
spruce@style:style(),
spruce@style:style(),
spruce@style:style(),
spruce@style:style(),
spruce@style:style(),
spruce@style:style(),
spruce@style:style(),
spruce@style:style(),
spruce@style:style(),
spruce@style:style(),
spruce@highlight:theme(),
spruce@style:color(),
spruce@style:color(),
spruce@style:style(),
spruce@style:style()}.
-opaque options() :: {options, theme(), gleam@option:option(integer())}.
-type alert() :: {alert,
alert_kind(),
gleam@option:option(list(mork@document:inline())),
list(mork@document:block())}.
-type alert_kind() :: alert_note |
alert_tip |
alert_important |
alert_warning |
alert_caution.
-type fence() :: {fence, binary(), integer()}.
-file("src/spruce/markdown.gleam", 100).
?DOC(" Build a theme tuned for dark terminal backgrounds.\n").
-spec dark_theme() -> theme().
dark_theme() ->
{theme,
begin
_pipe = spruce@style:new(),
_pipe@1 = spruce@style:bold(_pipe),
spruce@style:fg(_pipe@1, {hex, 16#7dd3fc})
end,
begin
_pipe@2 = spruce@style:new(),
_pipe@3 = spruce@style:bold(_pipe@2),
spruce@style:fg(_pipe@3, {hex, 16#93c5fd})
end,
begin
_pipe@4 = spruce@style:new(),
_pipe@5 = spruce@style:bold(_pipe@4),
spruce@style:fg(_pipe@5, {hex, 16#c4b5fd})
end,
begin
_pipe@6 = spruce@style:new(),
_pipe@7 = spruce@style:bold(_pipe@6),
spruce@style:fg(_pipe@7, {hex, 16#f0abfc})
end,
begin
_pipe@8 = spruce@style:new(),
_pipe@9 = spruce@style:bold(_pipe@8),
spruce@style:fg(_pipe@9, {hex, 16#f9a8d4})
end,
begin
_pipe@10 = spruce@style:new(),
_pipe@11 = spruce@style:bold(_pipe@10),
spruce@style:fg(_pipe@11, {hex, 16#fda4af})
end,
begin
_pipe@12 = spruce@style:new(),
spruce@style:italic(_pipe@12)
end,
begin
_pipe@13 = spruce@style:new(),
spruce@style:bold(_pipe@13)
end,
begin
_pipe@14 = spruce@style:new(),
spruce@style:strikethrough(_pipe@14)
end,
begin
_pipe@15 = spruce@style:new(),
spruce@style:reverse(_pipe@15)
end,
begin
_pipe@16 = spruce@style:new(),
_pipe@17 = spruce@style:fg(_pipe@16, {hex, 16#fbbf24}),
spruce@style:dim(_pipe@17)
end,
begin
_pipe@18 = spruce@style:new(),
_pipe@19 = spruce@style:underline(_pipe@18),
spruce@style:fg(_pipe@19, {hex, 16#60a5fa})
end,
begin
_pipe@20 = spruce@style:new(),
spruce@style:dim(_pipe@20)
end,
begin
_pipe@21 = spruce@style:new(),
spruce@style:dim(_pipe@21)
end,
spruce@highlight:dark_theme(),
{hex, 16#64748b},
{hex, 16#94a3b8},
begin
_pipe@22 = spruce@style:new(),
spruce@style:dim(_pipe@22)
end,
begin
_pipe@23 = spruce@style:new(),
spruce@style:bold(_pipe@23)
end}.
-file("src/spruce/markdown.gleam", 125).
?DOC(" Build a theme tuned for light terminal backgrounds.\n").
-spec light_theme() -> theme().
light_theme() ->
{theme,
begin
_pipe = spruce@style:new(),
_pipe@1 = spruce@style:bold(_pipe),
spruce@style:fg(_pipe@1, {hex, 16#0369a1})
end,
begin
_pipe@2 = spruce@style:new(),
_pipe@3 = spruce@style:bold(_pipe@2),
spruce@style:fg(_pipe@3, {hex, 16#1d4ed8})
end,
begin
_pipe@4 = spruce@style:new(),
_pipe@5 = spruce@style:bold(_pipe@4),
spruce@style:fg(_pipe@5, {hex, 16#6d28d9})
end,
begin
_pipe@6 = spruce@style:new(),
_pipe@7 = spruce@style:bold(_pipe@6),
spruce@style:fg(_pipe@7, {hex, 16#a21caf})
end,
begin
_pipe@8 = spruce@style:new(),
_pipe@9 = spruce@style:bold(_pipe@8),
spruce@style:fg(_pipe@9, {hex, 16#be185d})
end,
begin
_pipe@10 = spruce@style:new(),
_pipe@11 = spruce@style:bold(_pipe@10),
spruce@style:fg(_pipe@11, {hex, 16#be123c})
end,
begin
_pipe@12 = spruce@style:new(),
spruce@style:italic(_pipe@12)
end,
begin
_pipe@13 = spruce@style:new(),
spruce@style:bold(_pipe@13)
end,
begin
_pipe@14 = spruce@style:new(),
spruce@style:strikethrough(_pipe@14)
end,
begin
_pipe@15 = spruce@style:new(),
spruce@style:reverse(_pipe@15)
end,
begin
_pipe@16 = spruce@style:new(),
_pipe@17 = spruce@style:fg(_pipe@16, {hex, 16#92400e}),
spruce@style:dim(_pipe@17)
end,
begin
_pipe@18 = spruce@style:new(),
_pipe@19 = spruce@style:underline(_pipe@18),
spruce@style:fg(_pipe@19, {hex, 16#2563eb})
end,
begin
_pipe@20 = spruce@style:new(),
spruce@style:dim(_pipe@20)
end,
begin
_pipe@21 = spruce@style:new(),
spruce@style:dim(_pipe@21)
end,
spruce@highlight:light_theme(),
{hex, 16#475569},
{hex, 16#64748b},
begin
_pipe@22 = spruce@style:new(),
spruce@style:dim(_pipe@22)
end,
begin
_pipe@23 = spruce@style:new(),
spruce@style:bold(_pipe@23)
end}.
-file("src/spruce/markdown.gleam", 153).
?DOC(
" A theme whose colors adapt to the terminal background (light vs dark),\n"
" resolved per render from `spruce.background`. This is the default theme used\n"
" by `render` and `default_options`. On `Unknown` backgrounds it renders as\n"
" dark.\n"
).
-spec adaptive_theme() -> theme().
adaptive_theme() ->
Adapt = fun(Light, Dark) ->
spruce@style:adaptive({hex, Light}, {hex, Dark})
end,
{theme,
begin
_pipe = spruce@style:new(),
_pipe@1 = spruce@style:bold(_pipe),
spruce@style:fg(_pipe@1, Adapt(16#0369a1, 16#7dd3fc))
end,
begin
_pipe@2 = spruce@style:new(),
_pipe@3 = spruce@style:bold(_pipe@2),
spruce@style:fg(_pipe@3, Adapt(16#1d4ed8, 16#93c5fd))
end,
begin
_pipe@4 = spruce@style:new(),
_pipe@5 = spruce@style:bold(_pipe@4),
spruce@style:fg(_pipe@5, Adapt(16#6d28d9, 16#c4b5fd))
end,
begin
_pipe@6 = spruce@style:new(),
_pipe@7 = spruce@style:bold(_pipe@6),
spruce@style:fg(_pipe@7, Adapt(16#a21caf, 16#f0abfc))
end,
begin
_pipe@8 = spruce@style:new(),
_pipe@9 = spruce@style:bold(_pipe@8),
spruce@style:fg(_pipe@9, Adapt(16#be185d, 16#f9a8d4))
end,
begin
_pipe@10 = spruce@style:new(),
_pipe@11 = spruce@style:bold(_pipe@10),
spruce@style:fg(_pipe@11, Adapt(16#be123c, 16#fda4af))
end,
begin
_pipe@12 = spruce@style:new(),
spruce@style:italic(_pipe@12)
end,
begin
_pipe@13 = spruce@style:new(),
spruce@style:bold(_pipe@13)
end,
begin
_pipe@14 = spruce@style:new(),
spruce@style:strikethrough(_pipe@14)
end,
begin
_pipe@15 = spruce@style:new(),
spruce@style:reverse(_pipe@15)
end,
begin
_pipe@16 = spruce@style:new(),
_pipe@17 = spruce@style:fg(_pipe@16, Adapt(16#92400e, 16#fbbf24)),
spruce@style:dim(_pipe@17)
end,
begin
_pipe@18 = spruce@style:new(),
_pipe@19 = spruce@style:underline(_pipe@18),
spruce@style:fg(_pipe@19, Adapt(16#2563eb, 16#60a5fa))
end,
begin
_pipe@20 = spruce@style:new(),
spruce@style:dim(_pipe@20)
end,
begin
_pipe@21 = spruce@style:new(),
spruce@style:dim(_pipe@21)
end,
spruce@highlight:adaptive_theme(),
Adapt(16#475569, 16#64748b),
Adapt(16#64748b, 16#94a3b8),
begin
_pipe@22 = spruce@style:new(),
spruce@style:dim(_pipe@22)
end,
begin
_pipe@23 = spruce@style:new(),
spruce@style:bold(_pipe@23)
end}.
-file("src/spruce/markdown.gleam", 181).
?DOC(" Default options: the adaptive theme and no width limit.\n").
-spec default_options() -> options().
default_options() ->
{options, adaptive_theme(), none}.
-file("src/spruce/markdown.gleam", 186).
?DOC(" Use a specific theme when rendering.\n").
-spec with_theme(options(), theme()) -> options().
with_theme(Options, Theme) ->
{options, Theme, erlang:element(3, Options)}.
-file("src/spruce/markdown.gleam", 191).
?DOC(" Wrap rendered output to a maximum visual width. Negative values clamp to 0.\n").
-spec with_width(options(), integer()) -> options().
with_width(Options, Width) ->
{options, erlang:element(2, Options), {some, gleam@int:max(0, Width)}}.
-file("src/spruce/markdown.gleam", 1032).
-spec remove_empty(list(binary())) -> list(binary()).
remove_empty(Lines) ->
case Lines of
[] ->
[];
[<<""/utf8>> | Rest] ->
remove_empty(Rest);
[Line | Rest@1] ->
[Line | remove_empty(Rest@1)]
end.
-file("src/spruce/markdown.gleam", 846).
-spec render_rule(spruce:spruce(), options()) -> binary().
render_rule(Sp, Options) ->
Width@1 = case erlang:element(3, Options) of
{some, Width} when Width > 0 ->
Width;
_ ->
40
end,
<<(spruce@internal@layout:indent_prefix(Sp))/binary,
(spruce@style:render(
Sp,
erlang:element(19, erlang:element(2, Options)),
gleam@string:repeat(<<"─"/utf8>>, Width@1)
))/binary>>.
-file("src/spruce/markdown.gleam", 1003).
-spec destination_string(mork@document:destination()) -> binary().
destination_string(Destination) ->
case Destination of
{absolute, Uri} ->
Uri;
{relative, Uri@1} ->
Uri@1;
{anchor, Id} ->
<<"#"/utf8, Id/binary>>
end.
-file("src/spruce/markdown.gleam", 951).
-spec render_link(spruce:spruce(), binary(), binary(), theme()) -> binary().
render_link(Sp, Label, Target, Theme) ->
Visible = spruce@style:render(Sp, erlang:element(13, Theme), Label),
case (Target =:= <<""/utf8>>) orelse (Target =:= Label) of
true ->
Visible;
false ->
<<Visible/binary,
(spruce@style:render(
Sp,
erlang:element(14, Theme),
<<<<" ("/utf8, Target/binary>>/binary, ")"/utf8>>
))/binary>>
end.
-file("src/spruce/markdown.gleam", 966).
-spec render_image(
spruce:spruce(),
list(mork@document:inline()),
binary(),
options()
) -> binary().
render_image(Sp, Text, Target, Options) ->
Label = render_inlines(Sp, Text, Options),
case Label of
<<""/utf8>> ->
Target;
_ ->
Label
end.
-file("src/spruce/markdown.gleam", 885).
-spec render_inline(spruce:spruce(), mork@document:inline(), options()) -> binary().
render_inline(Sp, Inline, Options) ->
case Inline of
{autolink, Uri, Text} ->
Label = case Text of
{some, Text@1} ->
Text@1;
none ->
Uri
end,
render_link(Sp, Label, Uri, erlang:element(2, Options));
{code_span, Text@2} ->
spruce@style:render(
Sp,
erlang:element(12, erlang:element(2, Options)),
<<<<"`"/utf8, Text@2/binary>>/binary, "`"/utf8>>
);
{email_autolink, Mail} ->
render_link(
Sp,
Mail,
<<"mailto:"/utf8, Mail/binary>>,
erlang:element(2, Options)
);
{emphasis, Children} ->
spruce@style:render(
Sp,
erlang:element(8, erlang:element(2, Options)),
render_inlines(Sp, Children, Options)
);
{footnote, Num, _} ->
<<<<"[^"/utf8, (erlang:integer_to_binary(Num))/binary>>/binary,
"]"/utf8>>;
{full_image, Text@3, Data} ->
render_image(
Sp,
Text@3,
destination_string(erlang:element(2, Data)),
Options
);
{full_link, Text@4, Data@1} ->
render_link(
Sp,
render_inlines(Sp, Text@4, Options),
destination_string(erlang:element(2, Data@1)),
erlang:element(2, Options)
);
hard_break ->
<<"\n"/utf8>>;
{highlight, Children@1} ->
spruce@style:render(
Sp,
erlang:element(11, erlang:element(2, Options)),
render_inlines(Sp, Children@1, Options)
);
{inline_footnote, Num@1, _} ->
<<<<"[^"/utf8, (erlang:integer_to_binary(Num@1))/binary>>/binary,
"]"/utf8>>;
{inline_html, Tag, _, Children@2} ->
case Children@2 of
[] ->
spruce@style:render(
Sp,
erlang:element(15, erlang:element(2, Options)),
<<<<"<"/utf8, Tag/binary>>/binary, ">"/utf8>>
);
_ ->
render_inlines(Sp, Children@2, Options)
end;
{raw_html, Raw} ->
spruce@style:render(
Sp,
erlang:element(15, erlang:element(2, Options)),
Raw
);
{ref_image, Text@5, Label@1} ->
render_image(Sp, Text@5, Label@1, Options);
{ref_link, Text@6, _} ->
render_inlines(Sp, Text@6, Options);
soft_break ->
<<" "/utf8>>;
{strikethrough, Children@3} ->
spruce@style:render(
Sp,
erlang:element(10, erlang:element(2, Options)),
render_inlines(Sp, Children@3, Options)
);
{strong, Children@4} ->
spruce@style:render(
Sp,
erlang:element(9, erlang:element(2, Options)),
render_inlines(Sp, Children@4, Options)
);
{text, Text@7} ->
Text@7;
{checkbox, true} ->
<<"[x]"/utf8>>;
{checkbox, false} ->
<<"[ ]"/utf8>>;
{delim, Delimiter, Len, _, _} ->
gleam@string:repeat(Delimiter, Len)
end.
-file("src/spruce/markdown.gleam", 871).
-spec render_inline_list(
list(mork@document:inline()),
spruce:spruce(),
options()
) -> list(binary()).
render_inline_list(Inlines, Sp, Options) ->
case Inlines of
[] ->
[];
[First | Rest] ->
[render_inline(Sp, First, Options) |
render_inline_list(Rest, Sp, Options)]
end.
-file("src/spruce/markdown.gleam", 861).
-spec render_inlines(spruce:spruce(), list(mork@document:inline()), options()) -> binary().
render_inlines(Sp, Inlines, Options) ->
_pipe = Inlines,
_pipe@1 = render_inline_list(_pipe, Sp, Options),
gleam@string:join(_pipe@1, <<""/utf8>>).
-file("src/spruce/markdown.gleam", 980).
-spec fallback_inline(
spruce:spruce(),
binary(),
list(mork@document:inline()),
options()
) -> binary().
fallback_inline(Sp, Raw, Inlines, Options) ->
case render_inlines(Sp, Inlines, Options) of
<<""/utf8>> ->
Raw;
Text ->
Text
end.
-file("src/spruce/markdown.gleam", 832).
-spec render_table_cells(spruce:spruce(), list(mork@document:cell()), options()) -> list(binary()).
render_table_cells(Sp, Cells, Options) ->
case Cells of
[] ->
[];
[{cell, Raw, Inlines} | Rest] ->
[begin
_pipe = fallback_inline(Sp, Raw, Inlines, Options),
gleam@string:trim(_pipe)
end |
render_table_cells(Sp, Rest, Options)]
end.
-file("src/spruce/markdown.gleam", 818).
-spec render_table_rows(
spruce:spruce(),
list(list(mork@document:cell())),
options()
) -> list(list(binary())).
render_table_rows(Sp, Rows, Options) ->
case Rows of
[] ->
[];
[Row | Rest] ->
[render_table_cells(Sp, Row, Options) |
render_table_rows(Sp, Rest, Options)]
end.
-file("src/spruce/markdown.gleam", 804).
-spec render_table_headers(
spruce:spruce(),
list(mork@document:t_head()),
options()
) -> list(binary()).
render_table_headers(Sp, Headers, Options) ->
case Headers of
[] ->
[];
[{t_head, _, Raw, Inlines} | Rest] ->
[begin
_pipe = fallback_inline(Sp, Raw, Inlines, Options),
gleam@string:trim(_pipe)
end |
render_table_headers(Sp, Rest, Options)]
end.
-file("src/spruce/markdown.gleam", 781).
-spec render_table(
spruce:spruce(),
list(mork@document:t_head()),
list(list(mork@document:cell())),
options()
) -> binary().
render_table(Sp, Headers, Rows, Options) ->
Table_ = begin
_pipe = spruce@table:new(),
_pipe@1 = spruce@table:headers(
_pipe,
render_table_headers(Sp, Headers, Options)
),
_pipe@2 = spruce@table:rows(
_pipe@1,
render_table_rows(Sp, Rows, Options)
),
spruce@table:style_fn(_pipe@2, fun(Row, _) -> case Row of
-1 ->
erlang:element(20, erlang:element(2, Options));
_ ->
spruce@style:new()
end end)
end,
case erlang:element(3, Options) of
{some, Width} when Width > 0 ->
spruce@table:render(Sp, spruce@table:width(Table_, Width));
_ ->
spruce@table:render(Sp, Table_)
end.
-file("src/spruce/markdown.gleam", 1025).
-spec prefix_lines(binary(), binary()) -> binary().
prefix_lines(Text, Prefix) ->
_pipe = Text,
_pipe@1 = gleam@string:split(_pipe, <<"\n"/utf8>>),
_pipe@2 = gleam@list:map(
_pipe@1,
fun(Line) -> <<Prefix/binary, Line/binary>> end
),
gleam@string:join(_pipe@2, <<"\n"/utf8>>).
-file("src/spruce/markdown.gleam", 1018).
-spec wrap(binary(), gleam@option:option(integer())) -> binary().
wrap(Text, Width) ->
case Width of
{some, Width@1} when Width@1 > 0 ->
spruce@align:wrap(Text, Width@1);
_ ->
Text
end.
-file("src/spruce/markdown.gleam", 279).
-spec render_paragraph(spruce:spruce(), list(mork@document:inline()), options()) -> binary().
render_paragraph(Sp, Inlines, Options) ->
_pipe = render_inlines(Sp, Inlines, Options),
_pipe@1 = wrap(_pipe, erlang:element(3, Options)),
prefix_lines(_pipe@1, spruce@internal@layout:indent_prefix(Sp)).
-file("src/spruce/markdown.gleam", 856).
-spec render_html_block(spruce:spruce(), binary(), theme()) -> binary().
render_html_block(Sp, Raw, Theme) ->
_pipe = spruce@style:render(Sp, erlang:element(15, Theme), Raw),
prefix_lines(_pipe, spruce@internal@layout:indent_prefix(Sp)).
-file("src/spruce/markdown.gleam", 992).
-spec heading_style(theme(), integer()) -> spruce@style:style().
heading_style(Theme, Level) ->
case Level of
1 ->
erlang:element(2, Theme);
2 ->
erlang:element(3, Theme);
3 ->
erlang:element(4, Theme);
4 ->
erlang:element(5, Theme);
5 ->
erlang:element(6, Theme);
_ ->
erlang:element(7, Theme)
end.
-file("src/spruce/markdown.gleam", 261).
-spec render_heading(
spruce:spruce(),
integer(),
binary(),
list(mork@document:inline()),
options()
) -> binary().
render_heading(Sp, Level, Raw, Inlines, Options) ->
Text@1 = case render_inlines(Sp, Inlines, Options) of
<<""/utf8>> ->
Raw;
Text ->
Text
end,
Marker = gleam@string:repeat(<<"#"/utf8>>, gleam@int:clamp(Level, 1, 6)),
Line = <<<<Marker/binary, " "/utf8>>/binary,
(gleam@string:trim(Text@1))/binary>>,
<<(spruce@internal@layout:indent_prefix(Sp))/binary,
(spruce@style:render(
Sp,
heading_style(erlang:element(2, Options), Level),
Line
))/binary>>.
-file("src/spruce/markdown.gleam", 1011).
-spec option_string(gleam@option:option(binary())) -> binary().
option_string(Value) ->
case Value of
{some, Value@1} ->
Value@1;
none ->
<<""/utf8>>
end.
-file("src/spruce/markdown.gleam", 289).
-spec render_code(
spruce:spruce(),
gleam@option:option(binary()),
binary(),
options()
) -> binary().
render_code(Sp, Lang, Text, Options) ->
Title = option_string(Lang),
Highlighted = spruce@highlight:highlight_named_with(
Sp,
Text,
Title,
erlang:element(16, erlang:element(2, Options))
),
Options_ = begin
_pipe = spruce@box:options(
Title,
erlang:element(17, erlang:element(2, Options))
),
spruce@box:padding(_pipe, 1, 0, 0, 0)
end,
spruce@box:render(Sp, Highlighted, Options_).
-file("src/spruce/markdown.gleam", 469).
-spec alert_properties(alert_kind()) -> {spruce@style:color(),
binary(),
binary()}.
alert_properties(Kind) ->
Adapt = fun(Light, Dark) ->
spruce@style:adaptive({hex, Light}, {hex, Dark})
end,
case Kind of
alert_note ->
{Adapt(16#1d4ed8, 16#60a5fa), <<"ℹ"/utf8>>, <<"Note"/utf8>>};
alert_tip ->
{Adapt(16#15803d, 16#4ade80), <<"✔"/utf8>>, <<"Tip"/utf8>>};
alert_important ->
{Adapt(16#7e22ce, 16#c084fc), <<"◉"/utf8>>, <<"Important"/utf8>>};
alert_warning ->
{Adapt(16#b45309, 16#fbbf24), <<"⚠"/utf8>>, <<"Warning"/utf8>>};
alert_caution ->
{Adapt(16#b91c1c, 16#f87171), <<"✖"/utf8>>, <<"Caution"/utf8>>}
end.
-file("src/spruce/markdown.gleam", 417).
-spec take_until_break(
list(mork@document:inline()),
list(mork@document:inline())
) -> {list(mork@document:inline()), list(mork@document:inline())}.
take_until_break(Inlines, Acc) ->
case Inlines of
[] ->
{lists:reverse(Acc), []};
[soft_break | Rest] ->
{lists:reverse(Acc), Rest};
[hard_break | Rest@1] ->
{lists:reverse(Acc), Rest@1};
[First | Rest@2] ->
take_until_break(Rest@2, [First | Acc])
end.
-file("src/spruce/markdown.gleam", 406).
?DOC(
" Split inlines that follow `[!TYPE]` into an optional same-line title (the\n"
" inlines before the first line break) and the remaining body inlines.\n"
).
-spec split_alert_first_line(list(mork@document:inline())) -> {gleam@option:option(list(mork@document:inline())),
list(mork@document:inline())}.
split_alert_first_line(Inlines) ->
{Before, After} = take_until_break(Inlines, []),
Title = case Before of
[] ->
none;
_ ->
{some, Before}
end,
{Title, After}.
-file("src/spruce/markdown.gleam", 391).
-spec alert_kind_from_name(binary()) -> {ok, alert_kind()} | {error, nil}.
alert_kind_from_name(Name) ->
case string:lowercase(gleam@string:trim(Name)) of
<<"note"/utf8>> ->
{ok, alert_note};
<<"info"/utf8>> ->
{ok, alert_note};
<<"tip"/utf8>> ->
{ok, alert_tip};
<<"important"/utf8>> ->
{ok, alert_important};
<<"warning"/utf8>> ->
{ok, alert_warning};
<<"caution"/utf8>> ->
{ok, alert_caution};
<<"danger"/utf8>> ->
{ok, alert_caution};
_ ->
{error, nil}
end.
-file("src/spruce/markdown.gleam", 386).
-spec alert_kind_from_tag(binary()) -> {ok, alert_kind()} | {error, nil}.
alert_kind_from_tag(Tag) ->
gleam@bool:guard(
not gleam_stdlib:string_starts_with(Tag, <<"!"/utf8>>),
{error, nil},
fun() -> alert_kind_from_name(gleam@string:drop_start(Tag, 1)) end
).
-file("src/spruce/markdown.gleam", 363).
?DOC(
" Detect a `> [!TYPE] optional title` alert at the head of a block quote and\n"
" split out its (optional) custom title and remaining body blocks.\n"
).
-spec detect_alert(list(mork@document:block())) -> gleam@option:option(alert()).
detect_alert(Blocks) ->
case Blocks of
[{paragraph, _, Inlines} | Rest] ->
case Inlines of
[{text, <<"["/utf8>>}, {text, Tag}, {text, <<"]"/utf8>>} | Tail] ->
case alert_kind_from_tag(Tag) of
{ok, Kind} ->
{Title, Body_inlines} = split_alert_first_line(Tail),
Body = case Body_inlines of
[] ->
Rest;
_ ->
[{paragraph, <<""/utf8>>, Body_inlines} |
Rest]
end,
{some, {alert, Kind, Title, Body}};
{error, _} ->
none
end;
_ ->
none
end;
_ ->
none
end.
-file("src/spruce/markdown.gleam", 764).
-spec render_list_item_blocks(
list(mork@document:block()),
mork@document:list_pack(),
spruce:spruce(),
options()
) -> binary().
render_list_item_blocks(Blocks, Pack, Sp, Options) ->
Separator = case Pack of
tight ->
<<"\n"/utf8>>;
loose ->
<<"\n\n"/utf8>>
end,
_pipe = Blocks,
_pipe@1 = render_block_list(_pipe, Sp, Options),
_pipe@2 = remove_empty(_pipe@1),
gleam@string:join(_pipe@2, Separator).
-file("src/spruce/markdown.gleam", 749).
-spec render_list_labels(
list(mork@document:list_item()),
mork@document:list_pack(),
spruce:spruce(),
options()
) -> list(binary()).
render_list_labels(Items, Pack, Sp, Options) ->
case Items of
[] ->
[];
[{list_item, Blocks, _, _} | Rest] ->
[render_list_item_blocks(Blocks, Pack, Sp, Options) |
render_list_labels(Rest, Pack, Sp, Options)]
end.
-file("src/spruce/markdown.gleam", 722).
-spec render_list(
spruce:spruce(),
mork@document:list_pack(),
list(mork@document:list_item()),
gleam@option:option(integer()),
options()
) -> binary().
render_list(Sp, Pack, Items, Start, Options) ->
Labels = render_list_labels(Items, Pack, Sp, Options),
List_@1 = begin
_pipe = Labels,
gleam@list:fold(
_pipe,
spruce@list:new(),
fun(List_, Label) -> spruce@list:item(List_, Label) end
)
end,
case Start of
none ->
spruce@list:render(Sp, List_@1);
{some, Start@1} ->
List_@2 = begin
_pipe@1 = List_@1,
_pipe@2 = spruce@list:kind(_pipe@1, ordered),
spruce@list:enumerator(
_pipe@2,
fun(Index, _) ->
<<(erlang:integer_to_binary((Start@1 + Index) - 1))/binary,
". "/utf8>>
end
)
end,
spruce@list:render(Sp, List_@2)
end.
-file("src/spruce/markdown.gleam", 321).
-spec render_plain_quote(
spruce:spruce(),
list(mork@document:block()),
options()
) -> binary().
render_plain_quote(Sp, Blocks, Options) ->
Content = spruce@style:render(
Sp,
begin
_pipe = spruce@style:new(),
spruce@style:italic(_pipe)
end,
render_blocks(Sp, Blocks, Options)
),
Quote_block = begin
_pipe@1 = spruce@block:new(),
_pipe@2 = spruce@block:border(_pipe@1, thick),
_pipe@3 = spruce@block:border_sides(_pipe@2, false, false, false, true),
_pipe@4 = spruce@block:border_colors(
_pipe@3,
erlang:element(18, erlang:element(2, Options)),
erlang:element(18, erlang:element(2, Options)),
erlang:element(18, erlang:element(2, Options)),
erlang:element(18, erlang:element(2, Options))
),
spruce@block:padding(_pipe@4, 0, 0, 0, 1)
end,
spruce@block:render(Sp, Content, Quote_block).
-file("src/spruce/markdown.gleam", 429).
-spec render_admonition(spruce:spruce(), alert(), options()) -> binary().
render_admonition(Sp, Alert, Options) ->
{alert, Kind, Title, Body} = Alert,
{Color, Icon, Default_title} = alert_properties(Kind),
Title_text = case Title of
{some, Inlines} ->
case gleam@string:trim(render_inlines(Sp, Inlines, Options)) of
<<""/utf8>> ->
Default_title;
Text ->
Text
end;
none ->
Default_title
end,
Header = case spruce:supports_color(Sp) of
true ->
<<<<(spruce@style:render(
Sp,
begin
_pipe = spruce@style:new(),
spruce@style:fg(_pipe, Color)
end,
Icon
))/binary,
" "/utf8>>/binary,
(spruce@style:render(
Sp,
begin
_pipe@1 = spruce@style:new(),
_pipe@2 = spruce@style:bold(_pipe@1),
spruce@style:fg(_pipe@2, Color)
end,
Title_text
))/binary>>;
false ->
<<<<Icon/binary, " "/utf8>>/binary, Title_text/binary>>
end,
Content = case render_blocks(Sp, Body, Options) of
<<""/utf8>> ->
Header;
Body_text ->
<<<<Header/binary, "\n\n"/utf8>>/binary, Body_text/binary>>
end,
Admonition_block = begin
_pipe@3 = spruce@block:new(),
_pipe@4 = spruce@block:border(_pipe@3, thick),
_pipe@5 = spruce@block:border_sides(_pipe@4, false, false, false, true),
_pipe@6 = spruce@block:border_colors(
_pipe@5,
Color,
Color,
Color,
Color
),
spruce@block:padding(_pipe@6, 0, 0, 0, 1)
end,
spruce@block:render(Sp, Content, Admonition_block).
-file("src/spruce/markdown.gleam", 310).
-spec render_quote(spruce:spruce(), list(mork@document:block()), options()) -> binary().
render_quote(Sp, Blocks, Options) ->
case detect_alert(Blocks) of
{some, Alert} ->
render_admonition(Sp, Alert, Options);
none ->
render_plain_quote(Sp, Blocks, Options)
end.
-file("src/spruce/markdown.gleam", 243).
-spec render_block(spruce:spruce(), mork@document:block(), options()) -> binary().
render_block(Sp, Block_, Options) ->
case Block_ of
{block_quote, Blocks} ->
render_quote(Sp, Blocks, Options);
{bullet_list, Pack, Items} ->
render_list(Sp, Pack, Items, none, Options);
{code, Lang, Text} ->
render_code(Sp, Lang, Text, Options);
empty ->
<<""/utf8>>;
{heading, Level, _, Raw, Inlines} ->
render_heading(Sp, Level, Raw, Inlines, Options);
{html_block, Raw@1} ->
render_html_block(Sp, Raw@1, erlang:element(2, Options));
newline ->
<<""/utf8>>;
{ordered_list, Pack@1, Items@1, Start} ->
render_list(Sp, Pack@1, Items@1, Start, Options);
{paragraph, _, Inlines@1} ->
render_paragraph(Sp, Inlines@1, Options);
{table, Header, Rows} ->
render_table(Sp, Header, Rows, Options);
thematic_break ->
render_rule(Sp, Options)
end.
-file("src/spruce/markdown.gleam", 229).
-spec render_block_list(list(mork@document:block()), spruce:spruce(), options()) -> list(binary()).
render_block_list(Blocks, Sp, Options) ->
case Blocks of
[] ->
[];
[First | Rest] ->
[render_block(Sp, First, Options) |
render_block_list(Rest, Sp, Options)]
end.
-file("src/spruce/markdown.gleam", 218).
-spec render_blocks(spruce:spruce(), list(mork@document:block()), options()) -> binary().
render_blocks(Sp, Blocks, Options) ->
_pipe = Blocks,
_pipe@1 = render_block_list(_pipe, Sp, Options),
_pipe@2 = remove_empty(_pipe@1),
gleam@string:join(_pipe@2, <<"\n\n"/utf8>>).
-file("src/spruce/markdown.gleam", 633).
-spec parse_directive_title(binary()) -> {ok, gleam@option:option(binary())} |
{error, nil}.
parse_directive_title(After) ->
case After of
<<""/utf8>> ->
{ok, none};
_ ->
case gleam_stdlib:string_starts_with(After, <<"["/utf8>>) of
true ->
case gleam@string:split_once(
gleam@string:drop_start(After, 1),
<<"]"/utf8>>
) of
{ok, {Title, Tail}} ->
case gleam@string:trim(Tail) of
<<""/utf8>> ->
{ok, {some, gleam@string:trim(Title)}};
_ ->
{error, nil}
end;
{error, _} ->
{error, nil}
end;
false ->
{error, nil}
end
end.
-file("src/spruce/markdown.gleam", 664).
-spec is_name_char(binary()) -> boolean().
is_name_char(Char) ->
case Char of
<<"a"/utf8>> ->
true;
<<"b"/utf8>> ->
true;
<<"c"/utf8>> ->
true;
<<"d"/utf8>> ->
true;
<<"e"/utf8>> ->
true;
<<"f"/utf8>> ->
true;
<<"g"/utf8>> ->
true;
<<"h"/utf8>> ->
true;
<<"i"/utf8>> ->
true;
<<"j"/utf8>> ->
true;
<<"k"/utf8>> ->
true;
<<"l"/utf8>> ->
true;
<<"m"/utf8>> ->
true;
<<"n"/utf8>> ->
true;
<<"o"/utf8>> ->
true;
<<"p"/utf8>> ->
true;
<<"q"/utf8>> ->
true;
<<"r"/utf8>> ->
true;
<<"s"/utf8>> ->
true;
<<"t"/utf8>> ->
true;
<<"u"/utf8>> ->
true;
<<"v"/utf8>> ->
true;
<<"w"/utf8>> ->
true;
<<"x"/utf8>> ->
true;
<<"y"/utf8>> ->
true;
<<"z"/utf8>> ->
true;
<<"A"/utf8>> ->
true;
<<"B"/utf8>> ->
true;
<<"C"/utf8>> ->
true;
<<"D"/utf8>> ->
true;
<<"E"/utf8>> ->
true;
<<"F"/utf8>> ->
true;
<<"G"/utf8>> ->
true;
<<"H"/utf8>> ->
true;
<<"I"/utf8>> ->
true;
<<"J"/utf8>> ->
true;
<<"K"/utf8>> ->
true;
<<"L"/utf8>> ->
true;
<<"M"/utf8>> ->
true;
<<"N"/utf8>> ->
true;
<<"O"/utf8>> ->
true;
<<"P"/utf8>> ->
true;
<<"Q"/utf8>> ->
true;
<<"R"/utf8>> ->
true;
<<"S"/utf8>> ->
true;
<<"T"/utf8>> ->
true;
<<"U"/utf8>> ->
true;
<<"V"/utf8>> ->
true;
<<"W"/utf8>> ->
true;
<<"X"/utf8>> ->
true;
<<"Y"/utf8>> ->
true;
<<"Z"/utf8>> ->
true;
_ ->
false
end.
-file("src/spruce/markdown.gleam", 652).
-spec take_name(binary(), binary()) -> {binary(), binary()}.
take_name(Input, Acc) ->
case gleam_stdlib:string_pop_grapheme(Input) of
{ok, {Char, Rest}} ->
case is_name_char(Char) of
true ->
take_name(Rest, <<Acc/binary, Char/binary>>);
false ->
{Acc, Input}
end;
{error, _} ->
{Acc, <<""/utf8>>}
end.
-file("src/spruce/markdown.gleam", 614).
?DOC(
" Parse a directive opener such as `:::note` or `:::tip[Custom Title]`,\n"
" returning the GitHub-alert opener line it maps to.\n"
).
-spec parse_directive_open(binary()) -> {ok, binary()} | {error, nil}.
parse_directive_open(Line) ->
Trimmed = gleam@string:trim(Line),
gleam@result:'try'(
case gleam_stdlib:string_starts_with(Trimmed, <<":::"/utf8>>) of
true ->
{ok, gleam@string:drop_start(Trimmed, 3)};
false ->
{error, nil}
end,
fun(Rest) ->
{Name, After} = take_name(Rest, <<""/utf8>>),
gleam@result:'try'(
alert_kind_from_name(Name),
fun(_) ->
gleam@result:'try'(
parse_directive_title(gleam@string:trim(After)),
fun(Title) ->
Opener = <<<<"> [!"/utf8,
(string:uppercase(Name))/binary>>/binary,
"]"/utf8>>,
case Title of
{some, Title@1} ->
{ok,
<<<<Opener/binary, " "/utf8>>/binary,
Title@1/binary>>};
none ->
{ok, Opener}
end
end
)
end
)
end
).
-file("src/spruce/markdown.gleam", 591).
-spec count_prefix(binary(), binary(), integer()) -> integer().
count_prefix(Input, Marker, Count) ->
case gleam_stdlib:string_pop_grapheme(Input) of
{ok, {Char, Rest}} ->
case Char =:= Marker of
true ->
count_prefix(Rest, Marker, Count + 1);
false ->
Count
end;
{error, _} ->
Count
end.
-file("src/spruce/markdown.gleam", 550).
-spec parse_fence_open(binary()) -> gleam@option:option(fence()).
parse_fence_open(Line) ->
Trimmed = gleam@string:trim(Line),
case gleam_stdlib:string_pop_grapheme(Trimmed) of
{ok, {Marker, _}} ->
case Marker of
<<"`"/utf8>> ->
Length = count_prefix(Trimmed, Marker, 0),
case Length >= 3 of
true ->
{some, {fence, Marker, Length}};
false ->
none
end;
<<"~"/utf8>> ->
Length = count_prefix(Trimmed, Marker, 0),
case Length >= 3 of
true ->
{some, {fence, Marker, Length}};
false ->
none
end;
_ ->
none
end;
{error, _} ->
none
end.
-file("src/spruce/markdown.gleam", 546).
-spec is_indented_code_line(binary()) -> boolean().
is_indented_code_line(Line) ->
gleam_stdlib:string_starts_with(Line, <<" "/utf8>>) orelse gleam_stdlib:string_starts_with(
Line,
<<"\t"/utf8>>
).
-file("src/spruce/markdown.gleam", 577).
-spec trim_fence_close_candidate(binary(), integer()) -> gleam@option:option(binary()).
trim_fence_close_candidate(Line, Spaces) ->
case gleam_stdlib:string_pop_grapheme(Line) of
{ok, {<<" "/utf8>>, Rest}} ->
case Spaces < 3 of
true ->
trim_fence_close_candidate(Rest, Spaces + 1);
false ->
none
end;
{ok, {<<"\t"/utf8>>, _}} ->
none;
{ok, _} ->
{some, gleam@string:trim(Line)};
{error, _} ->
none
end.
-file("src/spruce/markdown.gleam", 569).
-spec closes_fence(binary(), fence()) -> boolean().
closes_fence(Line, Fence) ->
{fence, Marker, Length} = Fence,
case trim_fence_close_candidate(Line, 0) of
{some, Candidate} ->
count_prefix(Candidate, Marker, 0) >= Length;
none ->
false
end.
-file("src/spruce/markdown.gleam", 607).
-spec quote_line(binary()) -> binary().
quote_line(Line) ->
gleam@bool:guard(
Line =:= <<""/utf8>>,
<<">"/utf8>>,
fun() -> <<"> "/utf8, Line/binary>> end
).
-file("src/spruce/markdown.gleam", 603).
-spec is_directive_close(binary()) -> boolean().
is_directive_close(Line) ->
gleam@string:trim(Line) =:= <<":::"/utf8>>.
-file("src/spruce/markdown.gleam", 516).
-spec expand_line_outside_directive(
binary(),
list(binary()),
gleam@option:option(fence())
) -> list(binary()).
expand_line_outside_directive(Line, Rest, Fence) ->
case Fence of
{some, Fence@1} ->
Next_fence = case closes_fence(Line, Fence@1) of
true ->
none;
false ->
{some, Fence@1}
end,
[Line | expand_lines(Rest, false, Next_fence)];
none ->
case is_indented_code_line(Line) of
true ->
[Line | expand_lines(Rest, false, none)];
false ->
case parse_fence_open(Line) of
{some, Fence@2} ->
[Line | expand_lines(Rest, false, {some, Fence@2})];
none ->
case parse_directive_open(Line) of
{ok, Opener} ->
[Opener | expand_lines(Rest, true, none)];
{error, _} ->
[Line | expand_lines(Rest, false, none)]
end
end
end
end.
-file("src/spruce/markdown.gleam", 497).
-spec expand_lines(list(binary()), boolean(), gleam@option:option(fence())) -> list(binary()).
expand_lines(Lines, In_directive, Fence) ->
case Lines of
[] ->
[];
[Line | Rest] ->
case In_directive of
true ->
case is_directive_close(Line) of
true ->
expand_lines(Rest, false, Fence);
false ->
[quote_line(Line) | expand_lines(Rest, true, Fence)]
end;
false ->
expand_line_outside_directive(Line, Rest, Fence)
end
end.
-file("src/spruce/markdown.gleam", 486).
?DOC(
" Rewrite Astro/Starlight `:::type[Title]` … `:::` container directives into\n"
" GitHub-style `> [!TYPE] Title` alert block quotes, so both syntaxes share\n"
" one rendering path. Lines that are not recognized directives pass through\n"
" unchanged.\n"
).
-spec expand_directives(binary()) -> binary().
expand_directives(Markdown) ->
_pipe = Markdown,
_pipe@1 = gleam@string:split(_pipe, <<"\n"/utf8>>),
_pipe@2 = expand_lines(_pipe@1, false, none),
gleam@string:join(_pipe@2, <<"\n"/utf8>>).
-file("src/spruce/markdown.gleam", 201).
?DOC(" Render Markdown to styled terminal text with explicit options.\n").
-spec render_with(spruce:spruce(), binary(), options()) -> binary().
render_with(Sp, Markdown, Options) ->
{document, _, Blocks, _, _} = begin
_pipe = mork:configure(),
_pipe@1 = mork:tables(_pipe, true),
_pipe@2 = mork:tasklists(_pipe@1, true),
_pipe@3 = mork:autolinks(_pipe@2, true),
_pipe@4 = mork:heading_ids(_pipe@3, true),
mork:parse_with_options(_pipe@4, expand_directives(Markdown))
end,
render_blocks(Sp, Blocks, Options).
-file("src/spruce/markdown.gleam", 196).
?DOC(" Render Markdown to styled terminal text using the default options.\n").
-spec render(spruce:spruce(), binary()) -> binary().
render(Sp, Markdown) ->
render_with(Sp, Markdown, default_options()).
-file("src/spruce/markdown.gleam", 214).
?DOC(" Render Markdown with the default options and print it to stdout.\n").
-spec print(spruce:spruce(), binary()) -> nil.
print(Sp, Markdown) ->
gleam_stdlib:println(render(Sp, Markdown)).