Packages
earmark
0.1.0
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/inline.ex
defmodule Earmark.Inline do
@moduledoc """
Match and render inline sequences, passing each to the
renderer.
"""
import Earmark.Helpers
alias Earmark.Context
@doc false
def convert(src, context) when is_list(src) do
convert(Enum.join(src, "\n"), context)
end
def convert(src, context) do
convert_each(src, context, [])
end
defp convert_each("", _context, result) do
result |> Enum.reverse |> Enum.join
end
defp convert_each(src, context, result) do
renderer = context.options.renderer
cond do
# escape
match = Regex.run(context.rules.escape, src) ->
[ match, escaped ] = match
convert_each(behead(src, match), context, [escaped | result])
# autolink
match = Regex.run(context.rules.autolink, src) ->
[ match, link, protocol ] = match
{ href, text } = convert_autolink(link, protocol)
out = renderer.link(href, text)
convert_each(behead(src, match), context, [ out | result ])
# url (gfm)
match = Regex.run(context.rules.url, src) ->
[ match, href ] = match
text = escape(href)
out = renderer.link(href, text)
convert_each(behead(src, match), context, [ out | result ])
# tag
match = Regex.run(context.rules.tag, src) ->
[ match ] = match
out = context.options.do_sanitize.(match)
convert_each(behead(src, match), context, [ out | result ])
# link
match = Regex.run(context.rules.link, src) ->
{ match, text, href, title } = case match do
[ match, text, href ] -> { match, text, href, nil }
[ match, text, href, title ] -> { match, text, href, title }
end
out = output_image_or_link(context, match, text, href, title)
convert_each(behead(src, match), context, [ out | result ])
# reflink
match = Regex.run(context.rules.reflink, src) ->
{ match, alt_text, id } = case match do
[ match, id ] -> { match, nil, id }
[ match, id, "" ] -> { match, id, id }
[ match, alt_text, id ] -> { match, alt_text, id }
end
out = reference_link(context, match, alt_text, id)
convert_each(behead(src, match), context, [ out | result ])
# nolink
match = Regex.run(context.rules.nolink, src) ->
[ match, id ] = match
out = reference_link(context, match, id, id)
convert_each(behead(src, match), context, [ out | result ])
# strikethrough (gfm)
match = Regex.run(context.rules.strikethrough, src) ->
[ match, content ] = match
out = renderer.strikethrough(convert(content, context))
convert_each(behead(src, match), context, [ out | result ])
# strong
match = Regex.run(context.rules.strong, src) ->
{ match, content } = case match do
[ m, _, c ] -> {m, c}
[ m, c ] -> {m, c}
end
out = renderer.strong(convert(content, context))
convert_each(behead(src, match), context, [ out | result ])
# em
match = Regex.run(context.rules.em, src) ->
{ match, content } = case match do
[ m, _, c ] -> {m, c}
[ m, c ] -> {m, c}
end
out = renderer.em(convert(content, context))
convert_each(behead(src, match), context, [ out | result ])
# code
match = Regex.run(context.rules.code, src) ->
[match, _, content] = match
content = String.strip(content) # this from Gruber
out = renderer.codespan(escape(content, true))
convert_each(behead(src, match), context, [ out | result ])
# br
match = Regex.run(context.rules.br, src, return: :index) ->
out = renderer.br()
[ {0, match_len} ] = match
convert_each(behead(src, match_len), context, [ out | result ])
# text
match = Regex.run(context.rules.text, src) ->
[ match ] = match
out = escape(context.options.do_smartypants.(match))
convert_each(behead(src, match), context, [ out | result ])
# No match
true ->
location = String.slice(src, 0, 100)
raise("Failed to parse inline starting at: #{inspect(location)}")
end
end
defp convert_autolink(link, _separator = "@") do
link = (if String.at(link, 6) == ":", do: behead(link, 7), else: link)
text = mangle_link(link)
href = mangle_link("mailto:") <> text
{ href, text }
end
defp convert_autolink(link, _separator) do
link = escape(link)
{ link, link }
end
@doc """
Smartypants transformations convert quotes to the appropriate curly
variants, and -- and ... to – and …
"""
def smartypants(text) do
text
|> replace(~r{--}, "—")
|> replace(~r{(^|[-—/\(\[\{"”“\s])'}, "\\1‘")
|> replace(~r{\'}, "’")
|> replace(~r{(^|[-—/\(\[\{‘\s])\"}, "\\1“")
|> replace(~r{"}, "”")
|> replace(~r{\.\.\.}, "…")
end
@doc false
def mangle_link(link) do
link
end
defp output_image_or_link(context, "!" <> _, text, href, title) do
output_image(context.options.renderer, text, href, title)
end
defp output_image_or_link(context, _, text, href, title) do
output_link(context, text, href, title)
end
defp output_link(context, text, href, title) do
href = escape(href)
title = if title, do: escape(title), else: nil
context.options.renderer.link(href, convert_each(text, context, []), title)
end
defp output_image(renderer, text, href, title) do
href = escape(href)
title = if title, do: escape(title), else: nil
renderer.image(href, escape(text), title)
end
defp reference_link(context, match, alt_text, id) do
id = id |> replace(~r{\s+}, " ") |> String.downcase
case Dict.fetch(context.links, id) do
{:ok, link } -> output_image_or_link(context, match, alt_text, link.url, link.title)
_ -> match
end
end
##############################################################################
# Handle adding option specific rules and processors #
##############################################################################
defp noop(text), do: text
@doc false
# this is called by the command line processor to update
# the inline-specific rules in light of any options
def update_context(context = %Context{options: options}) do
context = %{ context | rules: rules_for(options) }
context = if options.smartypants do
put_in(context.options.do_smartypants, &smartypants/1)
else
put_in(context.options.do_smartypants, &noop/1)
end
if options.sanitize do
put_in(context.options.do_sanitize, &escape/1)
else
put_in(context.options.do_sanitize, &noop/1)
end
end
@inside ~S{(?:\[[^\]]*\]|[^\[\]]|\](?=[^\[]*\]))*}
@href ~S{\s*<?([\s\S]*?)>?(?:\s+['"]([\s\S]*?)['"])?\s*} #"
@code ~r{^
(`+) # $1 = Opening run of `
(.+?) # $2 = The code block
(?<!`)
\1 # Matching closer
(?!`)
}x
defp basic_rules do
[
escape: ~r{^\\([\\`*\{\}\[\]()\#+\-.!_>])},
autolink: ~r{^<([^ >]+(@|:\/)[^ >]+)>},
url: ~r{\z\A}, # noop
tag: ~r{^<!--[\s\S]*?-->|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>},
link: ~r{^!?\[(#{@inside})\]\(#{@href}\)},
reflink: ~r{^!?\[(#{@inside})\]\s*\[([^\]]*)\]},
nolink: ~r{^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]},
strong: ~r{^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)},
em: ~r{^\b_((?:__|[\s\S])+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)},
code: @code,
br: ~r<^ {2,}\n(?!\s*$)>,
text: ~r<^[\s\S]+?(?=[\\<!\[_*`]| {2,}\n|$)>,
strikethrough: ~r{\z\A} # noop
]
end
defp rules_for(options) do
rule_updates = []
if options.gfm do
rule_updates = [
escape: ~r{^\\([\\`*\{\}\[\]()\#+\-.!_>~|])},
url: ~r{^(https?:\/\/[^\s<]+[^<.,:;\"\')\]\s])},
strikethrough: ~r{^~~(?=\S)([\s\S]*?\S)~~},
text: ~r{^[\s\S]+?(?=[\\<!\[_*`~]|https?://| \{2,\}\n|$)}
]
if options.breaks do
break_updates = [
br: ~r{^ *\n(?!\s*$)},
text: ~r{^[\s\S]+?(?=[\\<!\[_*`~]|https?://| *\n|$)}
]
rule_updates = Keyword.merge(rule_updates, break_updates)
end
else
if options.pedantic do
rule_updates = [
strong: ~r{^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)},
em: ~r{^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)}
]
end
end
Keyword.merge(basic_rules, rule_updates)
|> Enum.into %{}
end
end