-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: ) 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 -> <>/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) -> <> 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 = <<<>/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 -> <<<>/binary, Title_text/binary>> end, Content = case render_blocks(Sp, Body, Options) of <<""/utf8>> -> Header; Body_text -> <<<
>/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, <>); 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, <<<>/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)).