ast_renderer.ex (6951B)
1 defmodule EarmarkParser.AstRenderer do 2 alias EarmarkParser.Block 3 alias EarmarkParser.Context 4 alias EarmarkParser.Options 5 6 import Context, only: [clear_value: 1, modify_value: 2, prepend: 2, prepend: 3] 7 8 import EarmarkParser.Ast.Emitter 9 import EarmarkParser.Ast.Inline, only: [convert: 3] 10 import EarmarkParser.Helpers.AstHelpers 11 import EarmarkParser.Ast.Renderer.{HtmlRenderer, FootnoteRenderer, TableRenderer} 12 13 @moduledoc false 14 15 def render(blocks, context = %Context{options: %Options{}}, loose? \\ true) do 16 _render(blocks, context, loose?) 17 end 18 19 defp _render(blocks, context, loose?) 20 defp _render([], context, _loose?), do: context 21 22 defp _render([block | blocks], context, loose?) do 23 context1 = render_block(block, clear_value(context), loose?) 24 _render(blocks, prepend(context1, context), loose?) 25 end 26 27 defp render_block(block, context, loose?) 28 ############# 29 # Paragraph # 30 ############# 31 defp render_block(%Block.Para{lnb: lnb, lines: lines, attrs: attrs} = para, context, _loose?) do 32 context1 = convert(lines, lnb, context) 33 value = context1.value |> Enum.reverse() 34 35 ast = 36 emit("p", value, attrs) 37 |> annotate(para) 38 39 prepend(context, ast, context1) 40 end 41 42 ######## 43 # Html # 44 ######## 45 defp render_block(%Block.Html{annotation: annotation, html: html}, context, _loose?) do 46 render_html_block(html, context, annotation) 47 end 48 49 defp render_block(%Block.HtmlOneline{annotation: annotation, html: html}, context, _loose?) do 50 render_html_oneline(html, context, annotation) 51 end 52 53 defp render_block(%Block.HtmlComment{lines: lines}, context, _loose?) do 54 lines1 = lines |> Enum.map(&render_html_comment_line/1) 55 prepend(context, emit(:comment, lines1, [], %{comment: true})) 56 end 57 58 ######### 59 # Ruler # 60 ######### 61 defp render_block(%Block.Ruler{type: "-", attrs: attrs}, context, _loose?) do 62 prepend(context, emit("hr", [], merge_attrs(attrs, %{"class" => "thin"}))) 63 end 64 65 defp render_block(%Block.Ruler{type: "_", attrs: attrs}, context, _loose?) do 66 prepend(context, emit("hr", [], merge_attrs(attrs, %{"class" => "medium"}))) 67 end 68 69 defp render_block(%Block.Ruler{type: "*", attrs: attrs}, context, _loose?) do 70 prepend(context, emit("hr", [], merge_attrs(attrs, %{"class" => "thick"}))) 71 end 72 73 ########### 74 # Heading # 75 ########### 76 defp render_block( 77 %Block.Heading{lnb: lnb, level: level, content: content, attrs: attrs}, 78 context, 79 _loose? 80 ) do 81 context1 = convert(content, lnb, clear_value(context)) 82 83 modify_value( 84 context1, 85 fn _ -> 86 [ 87 emit( 88 "h#{level}", 89 context1.value |> Enum.reverse(), 90 attrs) 91 ] 92 end 93 ) 94 end 95 96 ############## 97 # Blockquote # 98 ############## 99 defp render_block(%Block.BlockQuote{blocks: blocks, attrs: attrs}, context, _loose?) do 100 context1 = render(blocks, clear_value(context)) 101 102 modify_value(context1, fn ast -> 103 [emit("blockquote", ast, attrs)] 104 end) 105 end 106 107 ######### 108 # Table # 109 ######### 110 defp render_block( 111 %Block.Table{lnb: lnb, header: header, rows: rows, alignments: aligns, attrs: attrs}, 112 context, 113 _loose? 114 ) do 115 {rows_ast, context1} = render_rows(rows, lnb, aligns, context) 116 117 {rows_ast1, context2} = 118 if header do 119 {header_ast, context3} = render_header(header, lnb, aligns, context1) 120 {[header_ast | rows_ast], context3} 121 else 122 {rows_ast, context1} 123 end 124 125 prepend( 126 clear_value(context2), 127 emit("table", rows_ast1, attrs) 128 ) 129 end 130 131 ######## 132 # Code # 133 ######## 134 defp render_block( 135 %Block.Code{language: language, attrs: attrs} = block, 136 context = %Context{options: options}, 137 _loose? 138 ) do 139 classes = 140 if language && language != "", 141 do: [code_classes(language, options.code_class_prefix)], 142 else: [] 143 144 lines = render_code(block) 145 146 prepend( 147 context, 148 emit("pre", emit("code", lines, classes), attrs) 149 ) 150 end 151 152 ######### 153 # Lists # 154 ######### 155 @start_rgx ~r{\A\d+} 156 defp render_block( 157 %Block.List{type: type, bullet: bullet, blocks: items, attrs: attrs}, 158 context, 159 _loose? 160 ) do 161 context1 = render(items, clear_value(context)) 162 163 start_map = 164 case bullet && Regex.run(@start_rgx, bullet) do 165 nil -> %{} 166 ["1"] -> %{} 167 [start1] -> %{start: _normalize_start(start1)} 168 end 169 170 prepend( 171 context, 172 emit(to_string(type), context1.value, merge_attrs(attrs, start_map)), 173 context1 174 ) 175 end 176 177 # format a spaced list item 178 defp render_block( 179 %Block.ListItem{blocks: blocks, attrs: attrs, loose?: loose?}, 180 context, 181 _loose? 182 ) do 183 context1 = render(blocks, clear_value(context), loose?) 184 prepend( 185 context, 186 emit("li", context1.value, attrs), 187 context1 188 ) 189 end 190 191 ######## 192 # Text # 193 ######## 194 195 defp render_block(%Block.Text{line: line, lnb: lnb}, context, loose?) do 196 context1 = convert(line, lnb, clear_value(context)) 197 ast = context1.value |> Enum.reverse() 198 199 if loose? do 200 modify_value(context1, fn _ -> [emit("p", ast)] end) 201 else 202 modify_value(context1, fn _ -> ast end) 203 end 204 end 205 206 ################## 207 # Footnote Block # 208 ################## 209 210 @empty_set MapSet.new([]) 211 defp render_block(%Block.FnList{}=fn_list, context, _loose?) do 212 if MapSet.equal?(context.referenced_footnote_ids, @empty_set) do 213 context 214 else 215 render_defined_fns(fn_list, context) 216 end 217 end 218 ####################################### 219 # Isolated IALs are rendered as paras # 220 ####################################### 221 222 defp render_block(%Block.Ial{verbatim: verbatim}, context, _loose?) do 223 prepend(context, emit("p", "{:#{verbatim}}")) 224 end 225 226 #################### 227 # IDDef is ignored # 228 #################### 229 230 defp render_block(%Block.IdDef{}, context, _loose?), do: context 231 232 # Helpers 233 # ------- 234 235 # Seems to be dead code but as GFM list handling is broken maybe we have a bug 236 # that does not call this correctly, anyhow AST triplets do not exits anymore 237 # so this code would break if called 238 # defp _fix_text_lines(ast, loose?) 239 # defp _fix_text_lines(ast, false), do: Enum.map(ast, &_fix_tight_text_line/1) 240 # defp _fix_text_lines(ast, true), do: Enum.map(ast, &_fix_loose_text_line/1) 241 242 # defp _fix_loose_text_line(node) 243 # defp _fix_loose_text_line({:text, _, lines}), do: emit("p", lines) 244 # defp _fix_loose_text_line(node), do: node 245 246 # defp _fix_tight_text_line(node) 247 # defp _fix_tight_text_line({:text, _, lines}), do: lines 248 # defp _fix_tight_text_line(node), do: node 249 250 # INLINE CANDIDATE 251 defp _normalize_start(start) do 252 case String.trim_leading(start, "0") do 253 "" -> "0" 254 start1 -> start1 255 end 256 end 257 258 end 259 260 # SPDX-License-Identifier: Apache-2.0