%%%------------------------------------------------------------------- %%% @author Claude Code %%% @copyright (C) 2025, erlsci %%% @doc HTML Renderer for erlmd AST %%% %%% This module renders the erlmd Abstract Syntax Tree to HTML output. %%% It maintains compatibility with the original erlmd HTML output format. %%% %%% @end %%% Created : 2025 by Claude Code %%%------------------------------------------------------------------- -module(erlmd_html). -include("../include/types.hrl"). -export([ render/1, % render(#document{}) -> string() render_block/1, % render_block(block()) -> string() render_inline/1 % render_inline(inline()) -> string() ]). -define(NBSP, 160). -define(SPACE, 32). -define(LF, 10). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% %%% Public API %%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% @doc Render a document to HTML -spec render(#document{}) -> string(). render(#document{blocks = Blocks}) -> HTML = lists:map(fun render_block/1, Blocks), string:strip(lists:flatten(HTML), both, $\n). %% @doc Render a single block element -spec render_block(block()) -> string(). render_block(#paragraph{content = Content}) -> InlineHTML = lists:map(fun render_inline/1, Content), ContentStr = lists:flatten(InlineHTML), % Strip leading and trailing spaces (both sides) Stripped = string:strip(string:strip(ContentStr, left, $ ), right, $ ), "
" ++ Stripped ++ "
\n"; render_block(#header{level = Level, content = Content}) -> Tag = "h" ++ integer_to_list(Level), InlineHTML = lists:map(fun render_inline/1, Content), ContentStr = string:strip(lists:flatten(InlineHTML), right), "<" ++ Tag ++ ">" ++ ContentStr ++ "" ++ Tag ++ ">\n\n"; render_block(#blockquote{blocks = Blocks}) -> BlockHTML = lists:map(fun render_block/1, Blocks), "\n\n " ++ lists:flatten(BlockHTML) ++ ""; render_block(#code_block{content = Content}) -> Encoded = htmlencode(Content), "
" ++ Encoded ++ "\n\n\n";
render_block(#horizontal_rule{}) ->
"" ++ Encoded ++ "";
double -> "" ++ Encoded ++ ""
end;
render_inline(#link{text = Text, url = Url, title = Title}) ->
TextHTML = lists:map(fun render_inline/1, Text),
TitleAttr = case Title of
"" -> "";
_ -> " title=\"" ++ Title ++ "\""
end,
"" ++
lists:flatten(TextHTML) ++ "";
render_inline(#image{alt_text = AltText, url = Url, title = Title}) ->
AltHTML = lists:map(fun render_inline/1, AltText),
"tags lists:map(fun strip_para/1, Blocks); false -> % Loose items keep
tags lists:map(fun render_block/1, Blocks) end, ContentStr = string:strip(lists:flatten(BlockHTML), right, $\n), "
tags from paragraph for tight lists strip_para(#paragraph{content = Content}) -> InlineHTML = lists:map(fun render_inline/1, Content), Ret = lists:flatten(InlineHTML), string:strip(Ret, right, $\n) ++ "\n"; strip_para(Block) -> % Non-paragraph blocks get rendered normally render_block(Block). %% @doc Encode HTML special characters (for code blocks and code spans) htmlencode(List) -> htmlencode(List, []). htmlencode([], Acc) -> lists:flatten(lists:reverse(Acc)); htmlencode([$& | Rest], Acc) -> htmlencode(Rest, ["&" | Acc]); htmlencode([$< | Rest], Acc) -> htmlencode(Rest, ["<" | Acc]); htmlencode([$> | Rest], Acc) -> htmlencode(Rest, [">" | Acc]); htmlencode([?NBSP | Rest], Acc) -> htmlencode(Rest, [" " | Acc]); htmlencode([Else | Rest], Acc) -> htmlencode(Rest, [Else | Acc]). %% @doc Encode text content (handles special entities and characters) htmlencode_text(Text) when is_list(Text) -> htmlencode_text(Text, []); htmlencode_text(Text) -> Text. htmlencode_text([], Acc) -> lists:flatten(lists:reverse(Acc)); % Preserve HTML entities that are already encoded htmlencode_text([$&, $c, $o, $p, $y, $; | Rest], Acc) -> htmlencode_text(Rest, ["©" | Acc]); htmlencode_text([$&, $a, $m, $p, $; | Rest], Acc) -> htmlencode_text(Rest, ["&" | Acc]); htmlencode_text([$& | Rest], Acc) -> htmlencode_text(Rest, ["&" | Acc]); htmlencode_text([$< | Rest], Acc) -> htmlencode_text(Rest, ["<" | Acc]); htmlencode_text([?NBSP | Rest], Acc) -> htmlencode_text(Rest, [" " | Acc]); htmlencode_text([H | Rest], Acc) -> htmlencode_text(Rest, [H | Acc]).