inline.ex (12700B)
1 defmodule EarmarkParser.Ast.Inline do 2 @moduledoc false 3 4 alias EarmarkParser.{Context, Message, Parser} 5 alias EarmarkParser.Helpers.PureLinkHelpers 6 alias Parser.LinkParser 7 8 import EarmarkParser.Ast.Emitter 9 import EarmarkParser.Ast.Renderer.AstWalker 10 import EarmarkParser.Helpers 11 import EarmarkParser.Helpers.AttrParser 12 import EarmarkParser.Helpers.StringHelpers, only: [behead: 2] 13 import EarmarkParser.Helpers.AstHelpers 14 import Context, only: [set_value: 2] 15 16 @typep conversion_data :: {String.t(), non_neg_integer(), EarmarkParser.Context.t(), boolean()} 17 def convert(src, lnb, context) 18 19 def convert(list, lnb, context) when is_list(list) do 20 _convert(Enum.join(list, "\n"), lnb, context, true) 21 end 22 23 def convert(src, lnb, context) do 24 _convert(src, lnb, context, true) 25 end 26 27 defp _convert(src, current_lnb, context, use_linky?) 28 29 defp _convert(src, _, %{options: %{parse_inline: false}} = context, _) do 30 prepend(context, src) 31 end 32 33 defp _convert("", _, context, _), do: context 34 35 defp _convert(src, current_lnb, context, use_linky?) do 36 {src1, lnb1, context1, use_linky1?} = _convert_next(src, current_lnb, context, use_linky?) 37 _convert(src1, lnb1, context1, use_linky1?) 38 end 39 40 defp all_converters do 41 [ 42 converter_for_escape: &converter_for_escape/1, 43 converter_for_autolink: &converter_for_autolink/1, 44 # only if use_linky? 45 converter_for_link_and_image: &converter_for_link_and_image/1, 46 converter_for_reflink: &converter_for_reflink/1, 47 converter_for_footnote: &converter_for_footnote/1, 48 converter_for_nolink: &converter_for_nolink/1, 49 # 50 converter_for_strikethrough_gfm: &converter_for_strikethrough_gfm/1, 51 converter_for_strong: &converter_for_strong/1, 52 converter_for_em: &converter_for_em/1, 53 # only for option sub_sup 54 converter_for_sub: &converter_for_sub/1, 55 converter_for_sup: &converter_for_sup/1, 56 # 57 converter_for_code: &converter_for_code/1, 58 converter_for_br: &converter_for_br/1, 59 converter_for_inline_ial: &converter_for_inline_ial/1, 60 converter_for_pure_link: &converter_for_pure_link/1, 61 converter_for_text: &converter_for_text/1 62 ] 63 end 64 65 defp _convert_next(src, lnb, context, use_linky?) do 66 _find_and_execute_converter({src, lnb, context, use_linky?}) 67 end 68 69 defp _find_and_execute_converter({src, lnb, context, use_linky?}) do 70 all_converters() 71 |> Enum.find_value(fn {_converter_name, converter} -> 72 converter.({src, lnb, context, use_linky?}) 73 end) 74 end 75 76 ###################### 77 # 78 # Converters 79 # 80 ###################### 81 @escape_rule ~r{^\\([\\`*\{\}\[\]()\#+\-.!_>])} 82 def converter_for_escape({src, lnb, context, use_linky?}) do 83 if match = Regex.run(@escape_rule, src) do 84 [match, escaped] = match 85 {behead(src, match), lnb, prepend(context, escaped), use_linky?} 86 end 87 end 88 89 @autolink_rgx ~r{^<([^ >]+(@|:\/)[^ >]+)>} 90 def converter_for_autolink({src, lnb, context, use_linky?}) do 91 if match = Regex.run(@autolink_rgx, src) do 92 [match, link, protocol] = match 93 {href, text} = convert_autolink(link, protocol) 94 out = render_link(href, text) 95 {behead(src, match), lnb, prepend(context, out), use_linky?} 96 end 97 end 98 99 def converter_for_pure_link({src, lnb, context, use_linky?}) do 100 if context.options.pure_links do 101 case PureLinkHelpers.convert_pure_link(src) do 102 {ast, length} -> {behead(src, length), lnb, prepend(context, ast), use_linky?} 103 _ -> nil 104 end 105 end 106 end 107 108 def converter_for_link_and_image({src, lnb, context, use_linky?}) do 109 if use_linky? do 110 match = LinkParser.parse_link(src, lnb) 111 112 if match do 113 {match1, text, href, title, link_or_img} = match 114 115 out = 116 case link_or_img do 117 :link -> output_link(context, text, href, title, lnb) 118 :wikilink -> maybe_output_wikilink(context, text, href, title, lnb) 119 :image -> render_image(text, href, title) 120 end 121 122 if out do 123 {behead(src, match1), lnb, prepend(context, out), use_linky?} 124 end 125 end 126 end 127 end 128 129 @link_text ~S{(?:\[[^]]*\]|[^][]|\])*} 130 @reflink ~r{^!?\[(#{@link_text})\]\s*\[([^]]*)\]}x 131 def converter_for_reflink({src, lnb, context, use_linky?}) do 132 if use_linky? do 133 if match = Regex.run(@reflink, src) do 134 {match_, alt_text, id} = 135 case match do 136 [match__, id, ""] -> {match__, id, id} 137 [match__, alt_text, id] -> {match__, alt_text, id} 138 end 139 140 case reference_link(context, match_, alt_text, id, lnb) do 141 {:ok, out} -> {behead(src, match_), lnb, prepend(context, out), use_linky?} 142 _ -> nil 143 end 144 end 145 end 146 end 147 148 def converter_for_footnote({src, lnb, context, use_linky?}) do 149 if use_linky? do 150 case Regex.run(context.rules.footnote, src) do 151 [match, id] -> 152 case footnote_link(context, match, id) do 153 {:ok, out} -> 154 {behead(src, match), lnb, _prepend_footnote(context, out, id), use_linky?} 155 156 _ -> 157 converter_for_text( 158 {src, lnb, 159 Message.add_message( 160 context, 161 {:error, lnb, "footnote #{id} undefined, reference to it ignored"} 162 ), use_linky?} 163 ) 164 end 165 166 _ -> 167 nil 168 end 169 end 170 end 171 172 @nolink ~r{^!?\[((?:\[[^]]*\]|[^][])*)\]} 173 def converter_for_nolink({src, lnb, context, use_linky?}) do 174 if use_linky? do 175 case Regex.run(@nolink, src) do 176 [match, id] -> 177 case reference_link(context, match, id, id, lnb) do 178 {:ok, out} -> {behead(src, match), lnb, prepend(context, out), use_linky?} 179 _ -> nil 180 end 181 182 _ -> 183 nil 184 end 185 end 186 end 187 188 ################################ 189 # Simple Tags: em, strong, del # 190 ################################ 191 @strikethrough_rgx ~r{\A~~(?=\S)([\s\S]*?\S)~~} 192 def converter_for_strikethrough_gfm({src, _, _, _} = conv_tuple) do 193 if match = Regex.run(@strikethrough_rgx, src) do 194 _converter_for_simple_tag(conv_tuple, match, "del") 195 end 196 end 197 198 @strong_rgx ~r{\A__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)} 199 def converter_for_strong({src, _, _, _} = conv_tuple) do 200 if match = Regex.run(@strong_rgx, src) do 201 _converter_for_simple_tag(conv_tuple, match, "strong") 202 end 203 end 204 205 @emphasis_rgx ~r{\A\b_((?:__|[\s\S])+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)} 206 def converter_for_em({src, _, _, _} = conv_tuple) do 207 if match = Regex.run(@emphasis_rgx, src) do 208 _converter_for_simple_tag(conv_tuple, match, "em") 209 end 210 end 211 212 @sub_rgx ~r{\A~(?=\S)(.*?\S)~} 213 def converter_for_sub({src, _, %{options: %{sub_sup: true}}, _} = conv_tuple) do 214 if match = Regex.run(@sub_rgx, src) do 215 _converter_for_simple_tag(conv_tuple, match, "sub") 216 end 217 end 218 219 def converter_for_sub(_), do: nil 220 221 @sup_rgx ~r{\A\^(?=\S)(.*?\S)\^} 222 def converter_for_sup({src, _, %{options: %{sub_sup: true}}, _} = conv_tuple) do 223 if match = Regex.run(@sup_rgx, src) do 224 _converter_for_simple_tag(conv_tuple, match, "sup") 225 end 226 end 227 228 def converter_for_sup(_), do: nil 229 230 @squash_ws ~r{\s+} 231 @code ~r{^ 232 (`+) # $1 = Opening run of ` 233 (.+?) # $2 = The code block 234 (?<!`) 235 \1 # Matching closer 236 (?!`) 237 }xs 238 def converter_for_code({src, lnb, context, use_linky?}) do 239 if match = Regex.run(@code, src) do 240 [match, _, content] = match 241 # Commonmark 242 content1 = 243 content 244 |> String.trim() 245 |> String.replace(@squash_ws, " ") 246 247 out = codespan(content1) 248 {behead(src, match), lnb, prepend(context, out), use_linky?} 249 end 250 end 251 252 @inline_ial ~r<^\s*\{:\s*(.*?)\s*}> 253 254 def converter_for_inline_ial({src, lnb, context, use_linky?}) do 255 if match = Regex.run(@inline_ial, src) do 256 [match, ial] = match 257 {context1, ial_attrs} = parse_attrs(context, ial, lnb) 258 new_tags = augment_tag_with_ial(context.value, ial_attrs) 259 {behead(src, match), lnb, set_value(context1, new_tags), use_linky?} 260 end 261 end 262 263 def converter_for_br({src, lnb, context, use_linky?}) do 264 if match = Regex.run(context.rules.br, src, return: :index) do 265 [{0, match_len}] = match 266 {behead(src, match_len), lnb, prepend(context, emit("br")), use_linky?} 267 end 268 end 269 270 @line_ending ~r{\r\n?|\n} 271 @spec converter_for_text(conversion_data()) :: conversion_data() 272 def converter_for_text({src, lnb, context, _}) do 273 matched = 274 case Regex.run(context.rules.text, src) do 275 [match] -> match 276 end 277 278 line_count = matched |> String.split(@line_ending) |> Enum.count() 279 280 ast = hard_line_breaks(matched, context.options.gfm) 281 ast = walk_ast(ast, &gruber_line_breaks/1) 282 {behead(src, matched), lnb + line_count - 1, prepend(context, ast), true} 283 end 284 285 ###################### 286 # 287 # Helpers 288 # 289 ###################### 290 defp _converter_for_simple_tag({src, lnb, context, use_linky?}, match, for_tag) do 291 {match1, content} = 292 case match do 293 [m, _, c] -> {m, c} 294 [m, c] -> {m, c} 295 end 296 297 context1 = _convert(content, lnb, set_value(context, []), use_linky?) 298 299 {behead(src, match1), lnb, prepend(context, emit(for_tag, context1.value |> Enum.reverse())), 300 use_linky?} 301 end 302 303 defp _prepend_footnote(context, out, id) do 304 context 305 |> Map.update!(:referenced_footnote_ids, &MapSet.put(&1, id)) 306 |> prepend(out) 307 end 308 309 defp convert_autolink(link, separator) 310 311 defp convert_autolink(link, _separator = "@") do 312 link = if String.at(link, 6) == ":", do: behead(link, 7), else: link 313 text = link 314 href = "mailto:" <> text 315 {href, text} 316 end 317 318 defp convert_autolink(link, _separator) do 319 {link, link} 320 end 321 322 @gruber_line_break Regex.compile!(" {2,}(?>\n)", "m") 323 defp gruber_line_breaks(text) do 324 text 325 |> String.split(@gruber_line_break) 326 |> Enum.intersperse(emit("br")) 327 |> _remove_leading_empty() 328 end 329 330 @gfm_hard_line_break ~r{\\\n} 331 defp hard_line_breaks(text, gfm) 332 defp hard_line_breaks(text, false), do: text 333 defp hard_line_breaks(text, nil), do: text 334 335 defp hard_line_breaks(text, _) do 336 text 337 |> String.split(@gfm_hard_line_break) 338 |> Enum.intersperse(emit("br")) 339 |> _remove_leading_empty() 340 end 341 342 defp output_image_or_link(context, link_or_image, text, href, title, lnb) 343 344 defp output_image_or_link(_context, "!" <> _, text, href, title, _lnb) do 345 render_image(text, href, title) 346 end 347 348 defp output_image_or_link(context, _, text, href, title, lnb) do 349 output_link(context, text, href, title, lnb) 350 end 351 352 defp output_link(context, text, href, title, lnb) do 353 context1 = %{context | options: %{context.options | pure_links: false}} 354 355 context2 = _convert(text, lnb, set_value(context1, []), String.starts_with?(text, "!")) 356 357 if title do 358 emit("a", Enum.reverse(context2.value), href: href, title: title) 359 else 360 emit("a", Enum.reverse(context2.value), href: href) 361 end 362 end 363 364 defp maybe_output_wikilink(context, text, href, title, lnb) do 365 if context.options.wikilinks do 366 {tag, attrs, content, meta} = output_link(context, text, href, title, lnb) 367 {tag, attrs, content, Map.put(meta, :wikilink, true)} 368 end 369 end 370 371 defp reference_link(context, match, alt_text, id, lnb) do 372 id = id |> replace(~r{\s+}, " ") |> String.downcase() 373 374 case Map.fetch(context.links, id) do 375 {:ok, link} -> 376 {:ok, output_image_or_link(context, match, alt_text, link.url, link.title, lnb)} 377 378 _ -> 379 nil 380 end 381 end 382 383 defp footnote_link(context, _match, id) do 384 case Map.fetch(context.footnotes, id) do 385 {:ok, _} -> 386 {:ok, render_footnote_link("fn:#{id}", "fnref:#{id}", id)} 387 388 _ -> 389 nil 390 end 391 end 392 393 defp prepend(%Context{} = context, prep) do 394 _prepend(context, prep) 395 end 396 397 defp _prepend(context, value) 398 399 defp _prepend(context, [bin | rest]) when is_binary(bin) do 400 _prepend(_prepend(context, bin), rest) 401 end 402 403 defp _prepend(%Context{value: [str | rest]} = context, prep) 404 when is_binary(str) and is_binary(prep) do 405 %{context | value: [str <> prep | rest]} 406 end 407 408 defp _prepend(%Context{value: value} = context, prep) when is_list(prep) do 409 %{context | value: Enum.reverse(prep) ++ value} 410 end 411 412 defp _prepend(%Context{value: value} = context, prep) do 413 %{context | value: [prep | value]} 414 end 415 416 defp _remove_leading_empty(list) 417 defp _remove_leading_empty(["" | rest]), do: rest 418 defp _remove_leading_empty(list), do: list 419 end 420 421 # SPDX-License-Identifier: Apache-2.0