Packages
earmark
0.1.1
1.5.0-pre1
retired
1.5.0-pre
retired
1.4.49
retired
1.4.48
retired
1.4.47
retired
1.4.46
retired
1.4.45
retired
1.4.44
retired
1.4.43
retired
1.4.42
retired
1.4.41
retired
1.4.40
retired
1.4.39
retired
1.4.38
retired
1.4.37
retired
1.4.36
retired
1.4.35
retired
1.4.34
retired
1.4.33
retired
1.4.32
retired
1.4.31
retired
1.4.30
retired
1.4.29
retired
1.4.28
retired
1.4.27
retired
1.4.26
retired
1.4.25
retired
1.4.24
retired
1.4.23
retired
1.4.22
retired
1.4.21
retired
1.4.20
retired
1.4.19
retired
1.4.18
retired
1.4.17
retired
1.4.16
retired
1.4.16-pre2
retired
1.4.16-pre1
retired
1.4.16-pre
retired
1.4.15
retired
1.4.14
retired
1.4.13
retired
1.4.12
retired
1.4.10
retired
1.4.9
retired
1.4.8
retired
1.4.7
retired
1.4.6
retired
1.4.5
retired
1.4.4
retired
1.4.3
retired
1.4.2
retired
1.4.1
retired
1.4.0
retired
1.3.6
retired
1.3.5
retired
1.3.4
retired
1.3.3
retired
1.3.2
retired
1.3.1
retired
1.3.0
retired
1.2.6
retired
1.2.5
retired
1.2.4
retired
1.2.3
retired
1.2.2
retired
1.2.1
retired
1.2.0
retired
1.1.1
retired
1.1.0
retired
1.0.3
retired
1.0.2
retired
1.0.1
retired
1.0.0
retired
0.2.1
retired
0.2.0
retired
0.1.19
retired
0.1.18
retired
0.1.17
retired
0.1.16
retired
0.1.15
retired
0.1.14
retired
0.1.13
retired
0.1.12
retired
0.1.10
retired
0.1.9
retired
0.1.8
retired
0.1.7
retired
0.1.6
retired
0.1.5
retired
0.1.4
retired
0.1.3
retired
0.1.2
retired
0.1.1
retired
0.1.0
retired
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).
Current section
Files
Jump to
Current section
Files
lib/earmark/block.ex
defmodule Earmark.Block do
@moduledoc """
Given a list of parsed blocks, convert them into blocks.
That list of blocks is the final representation of the
document (in internal form).
"""
alias Earmark.Line
alias Earmark.Parser
defmodule Heading, do: defstruct content: nil, level: nil
defmodule Ruler, do: defstruct type: nil
defmodule BlockQuote, do: defstruct blocks: []
defmodule List, do: defstruct type: :ul, blocks: []
defmodule ListItem, do: defstruct type: :ul, spaced: true, blocks: []
defmodule Para, do: defstruct lines: []
defmodule Code, do: defstruct lines: [], language: nil
defmodule Html, do: defstruct html: [], tag: nil
defmodule HtmlOther, do: defstruct html: []
defmodule IdDef, do: defstruct id: nil, url: nil, title: nil
@doc false
# Given a list of `Line.xxx` structs, group them into related blocks.
# Then extract any id definitions, and build a hashdict from them. Not
# for external consumption.
def parse(lines) do
blocks = lines_to_blocks(lines)
links = links_from_blocks(blocks)
{ blocks, links }
end
@doc false
# Public to allow easier testing
def lines_to_blocks(lines) do
lines
|> parse([])
|> consolidate_list_items([])
end
defp parse([], result), do: result # consolidate also reverses, so no need
###################
# setext headings #
###################
defp parse([ %Line.Blank{},
%Line.Text{content: heading},
%Line.SetextUnderlineHeading{level: level},
%Line.Blank{}
|
rest
], result) do
parse(rest, [ %Heading{content: heading, level: level} | result ])
end
#################
# Other heading #
#################
defp parse([ %Line.Heading{content: content, level: level} | rest ], result) do
parse(rest, [ %Heading{content: content, level: level} | result ])
end
#########
# Ruler #
#########
defp parse([ %Line.Ruler{type: type} | rest], result) do
parse(rest, [ %Ruler{type: type} | result ])
end
###############
# Block Quote #
###############
defp parse( lines = [ %Line.BlockQuote{} | _ ], result) do
{quote_lines, rest} = Enum.split_while(lines, &is_blockquote_or_text/1)
lines = for line <- quote_lines, do: line.content
{blocks, _} = Parser.parse(lines, true)
parse(rest, [ %BlockQuote{blocks: blocks} | result ])
end
#############
# Paragraph #
#############
defp parse( lines = [ %Line.Text{} | _ ], result) do
{para_lines, rest} = Enum.split_while(lines, &is_text/1)
line_text = (for line <- para_lines, do: line.line)
parse(rest, [ %Para{lines: line_text} | result ])
end
#########
# Lists #
#########
# We handle lists in two passes. In the first, we build list items,
# in the second we combine adjacent items into lists. This is pass one
defp parse( [first = %Line.ListItem{type: type} | rest ], result) do
{spaced, list_lines, rest} = read_list_lines(rest, [])
spaced = (spaced || blank_line_in?(list_lines)) && peek(rest, Line.ListItem, type)
lines = for line <- [first | list_lines], do: properly_indent(line, 1)
{blocks, _} = Parser.parse(lines, true)
parse(rest, [ %ListItem{type: type, blocks: blocks, spaced: spaced} | result ])
end
#################
# Indented code #
#################
defp parse( list = [%Line.Indent{} | _], result) do
{code_lines, rest} = Enum.split_while(list, &is_indent_or_blank/1)
code = (for line <- code_lines, do: properly_indent(line, 1))
parse(rest, [ %Code{lines: code} | result ])
end
###############
# Fenced code #
###############
defp parse([%Line.Fence{delimiter: delimiter, language: language} | rest], result) do
{code_lines, rest} = Enum.split_while(rest, fn (line) ->
!match?(%Line.Fence{delimiter: ^delimiter, language: _}, line)
end)
unless length(rest) == 0, do: rest = tl(rest)
code = (for line <- code_lines, do: line.line)
parse(rest, [ %Code{lines: code, language: language} | result ])
end
##############
# HTML block #
##############
defp parse([ opener = %Line.HtmlOpenTag{tag: tag} | rest], result) do
{html_lines, rest} = html_match_to_closing(tag, rest, [opener])
html = (for line <- Enum.reverse(html_lines), do: line.line)
parse(rest, [ %Html{tag: tag, html: html} | result ])
end
####################
# HTML on one line #
####################
defp parse([ %Line.HtmlOneLine{line: line} | rest], result) do
parse(rest, [ %HtmlOther{html: [ line ]} | result ])
end
################
# HTML Comment #
################
defp parse([ line = %Line.HtmlComment{complete: true} | rest], result) do
parse(rest, [ %HtmlOther{html: [ line.line ]} | result ])
end
defp parse(lines = [ %Line.HtmlComment{complete: false} | _], result) do
{html_lines, rest} = Enum.split_while(lines, fn (line) ->
!(line.line =~ ~r/-->/)
end)
unless length(rest) == 0 do
html_lines = html_lines ++ [ hd(rest) ]
rest = tl(rest)
end
html = (for line <- html_lines, do: line.line)
parse(rest, [ %HtmlOther{html: html} | result ])
end
#################
# ID definition #
#################
# the title may be on the line following the iddef
defp parse( [ defn = %Line.IdDef{title: title}, maybe_title | rest ], result)
when title == nil
do
title = case maybe_title do
%Line.Text{content: content} -> Line.matches_id_title(content)
%Line.Indent{content: content} -> Line.matches_id_title(content)
_ -> nil
end
if title do
parse(rest, [ %IdDef{id: defn.id, url: defn.url, title: title} | result])
else
parse([maybe_title | rest], [ %IdDef{id: defn.id, url: defn.url} | result])
end
end
# or not
defp parse( [ defn = %Line.IdDef{} | rest ], result) do
parse(rest, [ %IdDef{id: defn.id, url: defn.url, title: defn.title} | result])
end
###############
# Blank Lines #
###############
# We've reached the point where empty lines are no longer significant
defp parse( [ %Line.Blank{} | rest ], result) do
parse(rest, result)
end
##################################################
# Consolidate one or more list items into a list #
##################################################
defp consolidate_list_items([], result), do: result # no need to reverse
# We have a list, and the next element is an item of the same type
defp consolidate_list_items(
[list = %List{type: type, blocks: items},
item = %ListItem{type: type} | rest], result)
do
items = [ item | items ] # original list is reversed
consolidate_list_items([ %{ list | blocks: items } | rest ], result)
end
# We have an item, but no open list
defp consolidate_list_items([ item = %ListItem{type: type} | rest], result) do
consolidate_list_items([ %List{ type: type, blocks: [ item ] } | rest ], result)
end
# Nothing to see here, move on
defp consolidate_list_items([ head | rest ], result) do
consolidate_list_items(rest, [ head | result ])
end
##################################################
# Called to slurp in the lines for a list item.
# basically, we allow indents and blank lines, and
# we allow text lines only after an indent (and initially)
# immediately after the start
defp read_list_lines([ line = %Line.Text{} | rest ], []) do
read_list_lines(rest, [ line ])
end
# Immediately after another text line
defp read_list_lines([ line = %Line.Text{} | rest ], result =[ %Line.Text{} | _]) do
read_list_lines(rest, [ line | result ])
end
# Immediately after an indent
defp read_list_lines([ line = %Line.Text{} | rest ], result =[ %Line.Indent{} | _]) do
read_list_lines(rest, [ line | result ])
end
# Always allow blank lines and indents, and text lines with at least
# two spaces
defp read_list_lines([ line = %Line.Blank{} | rest ], result) do
read_list_lines(rest, [ line | result ])
end
defp read_list_lines([ line = %Line.Indent{} | rest ], result) do
read_list_lines(rest, [ line | result ])
end
defp read_list_lines([ line = %Line.Text{line: <<" ", _ :: binary>>} | rest ],
result)
do
read_list_lines(rest, [ line | result ])
end
# no match, must be done
defp read_list_lines(lines, result) do
{ trailing_blanks, rest } = Enum.split_while(result, &is_blank/1)
spaced = length(trailing_blanks) > 0
{ spaced, Enum.reverse(rest), lines }
end
#####################################################
# Traverse the block list and build a list of links #
#####################################################
defp links_from_blocks(blocks) do
visit(blocks, HashDict.new, &link_extractor/2)
end
defp link_extractor(item = %IdDef{id: id}, result) do
Dict.put(result, String.downcase(id), item)
end
defp link_extractor(_, result), do: result
##################################
# Visitor pattern for each block #
##################################
def visit([], result, _func), do: result
def visit([ item = %BlockQuote{blocks: blocks} | rest], result, func) do
result = func.(item, result)
result = visit(blocks, result, func)
visit(rest, result, func)
end
def visit([ item = %List{blocks: blocks} | rest], result, func) do
result = func.(item, result)
result = visit(blocks, result, func)
visit(rest, result, func)
end
def visit([ item = %ListItem{blocks: blocks} | rest], result, func) do
result = func.(item, result)
result = visit(blocks, result, func)
visit(rest, result, func)
end
def visit([ item | rest], result, func) do
result = func.(item, result)
visit(rest, result, func)
end
###################################################################
# Consume HTML, taking care of nesting. Assumes one tag per line. #
###################################################################
# run out of input
defp html_match_to_closing(tag, [], result) do
IO.puts(:stderr, "Failed to find closing <#{tag}>")
{ result, [] }
end
# find closing tag
defp html_match_to_closing(tag,
[closer = %Line.HtmlCloseTag{tag: tag} | rest],
result)
do
{ [closer | result], rest }
end
# a nested open tag
defp html_match_to_closing(tag,
[opener = %Line.HtmlOpenTag{tag: new_tag} | rest],
result)
do
{ html_lines, rest } = html_match_to_closing(new_tag, rest, [opener])
html_match_to_closing(tag, rest, html_lines ++ result)
end
# anything else
defp html_match_to_closing(tag, [ line | rest ], result) do
html_match_to_closing(tag, rest, [ line | result ])
end
###########
# Helpers #
###########
defp is_blank(%Line.Blank{}), do: true
defp is_blank(_), do: false
# Gruber's tests have
#
# para text...
# * and more para text
#
# So list markers inside paragraphs are ignored. But he also has
#
# * line
# * line
#
# And expects it to be a nested list. These seem to be in conflict
#
# I think the second is a better interpretation, so I commented
# out the 2nd match below.
defp is_text(%Line.Text{}), do: true
# defp is_text(%Line.ListItem{}), do: true
defp is_text(_), do: false
defp is_blockquote_or_text(%Line.BlockQuote{}), do: true
defp is_blockquote_or_text(struct), do: is_text(struct)
defp is_indent_or_blank(%Line.Indent{}), do: true
defp is_indent_or_blank(line), do: is_blank(line)
defp peek([], _, _), do: false
defp peek([head | _], struct, type) do
head.__struct__ == struct && head.type == type
end
defp blank_line_in?([]), do: false
defp blank_line_in?([ %Line.Blank{} | _ ]), do: true
defp blank_line_in?([ _ | rest ]), do: blank_line_in?(rest)
# Add additional spaces for any indentation past level 1
defp properly_indent(%Line.Indent{level: level, content: content}, target_level)
when level == target_level do
content
end
defp properly_indent(%Line.Indent{level: level, content: content}, target_level)
when level > target_level do
String.duplicate(" ", level-target_level) <> content
end
defp properly_indent(line, _) do
line.content
end
end