Packages

Earmark is a pure-Elixir Markdown converter. It is intended to be used as a library (just call Earmark.as_html), but can also be used as a command-line tool (run mix escript.build first). Output generation is pluggable.

Retired package: Deprecated - Earmark is no longer maintained. Migrate to a replacement, for example MDEx (https://hex.pm/packages/mdex).
Security advisory: This version has known vulnerabilities. View advisories

Current section

Files

Jump to
earmark lib earmark_parser parser list_parser.ex
Raw

lib/earmark_parser/parser/list_parser.ex

defmodule EarmarkParser.Parser.ListParser do
alias EarmarkParser.{Block, Line, Options}
alias EarmarkParser.Parser.ListInfo
import EarmarkParser.Helpers.StringHelpers, only: [behead: 2]
import EarmarkParser.Message, only: [add_message: 2]
import ListInfo
@moduledoc false
@not_pending {nil, 0}
def parse_list(lines, result, options \\ %Options{}) do
{items, rest, options1} = _parse_list_items_init(lines, [], options)
list = _make_list(items, _empty_list(items) )
{[list|result], rest, options1}
end
defp _parse_list_items_init([item|rest], list_items, options) do
options1 = %{options|line: item.lnb}
_parse_list_items_start(rest, _make_and_prepend_list_item(item, list_items), new(item, options1))
end
defp _parse_list_items_spaced(input, items, list_info)
defp _parse_list_items_spaced(input, items, %{pending: @not_pending}=list_info) do
_parse_list_items_spaced_np(input, items, list_info)
end
defp _parse_list_items_spaced(input, items, list_info) do
_parse_list_items_spaced_pdg(input, items, list_info)
end
defp _parse_list_items_spaced_np([%Line.Blank{}|rest], items, list_info) do
list_info1 = %{list_info|lines: [""|list_info.lines], options: %{list_info.options|line: list_info.options.line + 1}}
_parse_list_items_spaced_np(rest, items, list_info1)
end
defp _parse_list_items_spaced_np([%Line.Ruler{}|_]=lines, items, list_info) do
_finish_list_items(lines, items, false, list_info)
end
defp _parse_list_items_spaced_np([%Line.ListItem{indent: ii}=item|_]=input, list_items, %{width: w}=list_info)
when ii < w do
if _starts_list?(item, list_items) do
_finish_list_items(input, list_items, false, list_info)
else
{items1, options1} = _finish_list_item(list_items, false, _loose(list_info))
_parse_list_items_init(input, items1, options1)
end
end
defp _parse_list_items_spaced_np([%Line.Indent{indent: ii}=item|rest], list_items, %{width: w}=list_info)
when ii >= w do
indented = _behead_spaces(item.line, w)
_parse_list_items_spaced(rest, list_items, update_list_info(list_info, indented, item, true))
end
defp _parse_list_items_spaced_np([%Line.ListItem{}=line|rest], items, list_info) do
indented = _behead_spaces(line.line, list_info.width)
_parse_list_items_start(rest, items, update_list_info(list_info, indented, line))
end
# BUG: Still do not know how much to indent here???
defp _parse_list_items_spaced_np([%{indent: indent, line: str_line}=line|rest], items, %{width: width}=list_info) when
indent >= width
do
_parse_list_items_spaced(rest, items, update_list_info(list_info, behead(str_line, width), line, true))
end
defp _parse_list_items_spaced_np(input, items, list_info) do
_finish_list_items(input ,items, false, list_info)
end
defp _parse_list_items_spaced_pdg(input, items, list_info)
defp _parse_list_items_spaced_pdg([], items, %{pending: {pending, lnb}}=list_info) do
options1 =
add_message(list_info.options, {:warning, lnb, "Closing unclosed backquotes #{pending} at end of input"})
_finish_list_items([], items, false, %{list_info| options: options1})
end
defp _parse_list_items_spaced_pdg([line|rest], items, list_info) do
indented = _behead_spaces(line.line, list_info.width)
_parse_list_items_spaced(rest, items, update_list_info(list_info, indented, line, true))
end
defp _parse_list_items_start(input, list_items, list_info)
defp _parse_list_items_start(input, list_items, %{pending: @not_pending}=list_info) do
_parse_list_items_start_np(input, list_items, list_info)
end
defp _parse_list_items_start(input, list_items, list_info) do
_parse_list_items_start_pdg(input, list_items, list_info)
end
defp _parse_list_items_start_np(input, list_items, list_info)
defp _parse_list_items_start_np([%Line.Blank{}|input], items, list_info) do
_parse_list_items_spaced(input, items, prepend_line(list_info, ""))
end
defp _parse_list_items_start_np([], list_items, list_info) do
_finish_list_items([], list_items, true, list_info)
end
defp _parse_list_items_start_np([%Line.Ruler{}|_]=input, list_items, list_info) do
_finish_list_items(input, list_items, true, list_info)
end
defp _parse_list_items_start_np([%Line.Heading{}|_]=input, list_items, list_info) do
_finish_list_items(input, list_items, true, list_info)
end
defp _parse_list_items_start_np([%Line.ListItem{indent: ii}=item|_]=input, list_items, %{width: w}=list_info)
when ii < w do
if _starts_list?(item, list_items) do
_finish_list_items(input, list_items, true, list_info)
else
{items1, options1} = _finish_list_item(list_items, true, list_info)
_parse_list_items_init(input, items1, options1)
end
end
# Slurp in everything else before a first blank line
defp _parse_list_items_start_np([%{line: str_line}=line|rest], items, list_info) do
indented = _behead_spaces(str_line, list_info.width)
_parse_list_items_start(rest, items, update_list_info(list_info, indented, line))
end
defp _parse_list_items_start_pdg(input, items, list_info)
defp _parse_list_items_start_pdg([], items, list_info) do
_finish_list_items([], items, true, list_info)
end
defp _parse_list_items_start_pdg([%{line: str_line}=line|rest], items, list_info) do
indented = _behead_spaces(str_line, list_info.width)
_parse_list_items_start(rest, items, update_list_info(list_info, indented, line))
end
defp _behead_spaces(str, len) do
Regex.replace(~r/\A\s{1,#{len}}/, str, "")
end
# INLINE CANDIDATE
defp _empty_list([%Block.ListItem{loose?: loose?, type: type}|_]) do
%Block.List{loose?: loose?, type: type}
end
@start_number_rgx ~r{\A0*(\d+)\.}
defp _extract_start(%{bullet: bullet}) do
case Regex.run(@start_number_rgx, bullet) do
nil -> ""
[_, "1"] -> ""
[_, start] -> ~s{ start="#{start}"}
end
end
defp _finish_list_item([%Block.ListItem{}=item|items], _at_start?, list_info) do
{blocks, _, _, options1} = list_info.lines
|> Enum.reverse
|> EarmarkParser.Parser.parse(%{list_info.options|line: item.lnb}, :list)
loose1? = _already_loose?(items) || list_info.loose?
{[%{item | blocks: blocks, loose?: loose1?}|items], options1}
end
defp _finish_list_items(input, items, at_start?, list_info) do
{items1, options1} = _finish_list_item(items, at_start?, list_info)
{items1, input, options1}
end
defp _make_and_prepend_list_item(%Line.ListItem{bullet: bullet, lnb: lnb, type: type}, list_items) do
[%Block.ListItem{bullet: bullet, lnb: lnb, spaced?: false, type: type}|list_items]
end
defp _make_list(items, list)
defp _make_list([%Block.ListItem{bullet: bullet, lnb: lnb}=item], %Block.List{loose?: loose?}=list) do
%{list | blocks: [%{item | loose?: loose?}|list.blocks],
bullet: bullet,
lnb: lnb,
start: _extract_start(item)}
end
defp _make_list([%Block.ListItem{}=item|rest], %Block.List{loose?: loose?}=list) do
_make_list(rest, %{list | blocks: [%{item | loose?: loose?}|list.blocks]})
end
defp _already_loose?(items)
defp _already_loose?([]), do: false
defp _already_loose?([%{loose?: loose?}|_]), do: loose?
defp _loose(list_info), do: %{list_info|loose?: true}
defp _starts_list?(%{bullet: bullet1}, [%Block.ListItem{bullet: bullet2}|_]) do
String.last(bullet1) != String.last(bullet2)
end
end
# SPDX-License-Identifier: Apache-2.0