zf

zenflows testing
git clone https://s.sonu.ch/~srfsh/zf.git
Log | Files | Refs | Submodules | README | LICENSE

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