defmodule Earmark do
@moduledoc """
### API
#### Earmark.as_html
{:ok, html_doc, []} = Earmark.as_html(markdown)
{:ok, html_doc, deprecation_messages} = Earmark.as_html(markdown)
{:error, html_doc, error_messages} = Earmark.as_html(markdown)
#### Earmark.as_html!
html_doc = Earmark.as_html!(markdown, options)
All messages are printed to _stderr_.
#### Options:
Options can be passed into `as_html` or `as_html!` according to the [documentation](#as_html/2).
html_doc = Earmark.as_html!(markdown)
html_doc = Earmark.as_html!(markdown, options)
Formats the error_messages returned by `as_html` and adds the filename to each.
Then prints them to stderr and just returns the html_doc
### Command line
$ mix escript.build
$ ./earmark file.md
Some options defined in the `Earmark.Options` struct can be specified as command line switches.
Use
$ ./earmark --help
to find out more, but here is a short example
$ ./earmark --smartypants false --code-class-prefix "a- b-" file.md
will call
Earmark.as_html!( ..., %Earmark.Options{smartypants: false, code_class_prefix: "a- b-"})
## Supports
Standard [Gruber markdown][gruber].
[gruber]: Some text link{: .classy} hello {:world} mypara mypara
hello
\\n"
which can be customized with the `code_class_prefix` option
iex(3)> Earmark.as_html! ["```elixir", " [] |> Enum.into(%{})", "```"] , %Earmark.Options{code_class_prefix: "lang-"}
" [] |> Enum.into(%{})
\\n"
* Tables
Are supported as long as they are preceded by an empty line.
State | Abbrev | Capital
----: | :----: | -------
Texas | TX | Austin
Maine | ME | Augusta
Tables may have leading and trailing vertical bars on each line
| State | Abbrev | Capital |
| ----: | :----: | ------- |
| Texas | TX | Austin |
| Maine | ME | Augusta |
Tables need not have headers, in which case all column alignments
default to left.
| Texas | TX | Austin |
| Maine | ME | Augusta |
Currently we assume there are always spaces around interior vertical
bars. It isn't clear what the expectation is.
### Adding HTML attributes with the IAL extension
#### To block elements
HTML attributes can be added to any block-level element. We use
the Kramdown syntax: add the line `{:` _attrs_ `}` following the block.
_attrs_ can be one or more of:
* `.className`
* `#id`
* name=value, name="value", or name='value'
For example:
# Warning
{: .red}
Do not turn off the engine
if you are at altitude.
{: .boxed #warning spellcheck="true"}
#### To links or images
It is possible to add IAL attributes to generated links or images in the following
format.
iex(4)> markdown = "[link](url) {: .classy}"
...(4)> Earmark.as_html(markdown)
{ :ok, "\\n", []}
For both cases, malformed attributes are ignored and warnings are issued.
iex(5)> [ "Some text", "{:hello}" ] |> Enum.join("\\n") |> Earmark.as_html()
{:error, " [] |> Enum.into(%{})
Becomes
While
mypara
will be transformed into
@tag :hello\\n"
will be rendered as shown in the doctest above.
If you want to integrate with a syntax highlighter with different conventions you can add more classes by specifying prefixes that will be
put before the language string.
Prism.js for example needs a class `language-elixir`. In order to achieve that goal you can add `language-`
as a `code_class_prefix` to `Earmark.Options`.
In the following example we want more than one additional class, so we add more prefixes.
Earmark.as_html!(..., %Earmark.Options{code_class_prefix: "lang- language-"})
which is rendering
...
As for all other options `code_class_prefix` can be passed into the `earmark` executable as follows:
earmark --code-class-prefix "language- lang-" ...
## Timeouts
By default, that is if the `timeout` option is not set Earmark uses parallel mapping as implemented in `Earmark.pmap/2`,
which uses `Task.await` with its default timeout of 5000ms.
In rare cases that might not be enough.
By indicating a longer `timeout` option in milliseconds Earmark will use parallel mapping as implemented in `Earmark.pmap/3`,
which will pass `timeout` to `Task.await`.
In both cases one can override the mapper function with either the `mapper` option (used if and only if `timeout` is nil) or the
`mapper_with_timeout` function (used otherwise).
For the escript only the `timeout` command line argument can be used.
## Security
Please be aware that Markdown is not a secure format. It produces
HTML from Markdown and HTML. It is your job to sanitize and or
filter the output of `Earmark.as_html` if you cannot trust the input
and are to serve the produced HTML on the Web.
"""
alias Earmark.Error
alias Earmark.Options
import Earmark.Message, only: [add_message: 2, emit_messages: 1, sort_messages: 1]
@doc """
Given a markdown document (as either a list of lines or
a string containing newlines), returns a tuple containing either
`{:ok, html_doc, error_messages}`, or `{:error, html_doc, error_messages}`
Where `html_doc` is an HTML representation of the markdown document and
`error_messages` is a list of tuples with the following elements
- `severity` e.g. `:error`, `:warning` or `:deprecation`
- line number in input where the error occurred
- description of the error
`options` can be an `%Earmark.Options{}` structure, or can be passed in as a `Keyword` argument (with legal keys for `%Earmark.Options`
* `renderer`: ModuleName
The module used to render the final document. Defaults to
`Earmark.HtmlRenderer`
* `gfm`: boolean
True by default. Turns on the supported Github Flavored Markdown extensions
* `breaks`: boolean
Only applicable if `gfm` is enabled. Makes all line breaks
significant (so every line in the input is a new line in the
output.
* `code_class_prefix`: binary
Code blocks will be rendered with prefixed class names, which might be necessary for
usage with 3rd party libraries.
Earmark.as_html("\`\`\`elixir\\nCode\\n\`\`\`", code_class_prefix: "my_prefix_")
{:ok, "Code\\\```
\\n", []}
* `smartypants`: boolean
Turns on smartypants processing, so quotes become curly, two
or four hyphens become en and em dashes, and so on. True by
default.
So, to format the document in `original` and disable smartypants,
you'd call
alias Earmark.Options
Earmark.as_html(original, %Options{smartypants: false})
* `pure_links`: boolean
Pure links of the form `~r{\\bhttps?://\\S+\\b}` are not rendered as links in this version yet.
However, by setting the `pure_links` option to `true` one can enable this behavior.
There are three possible cases
- Default (option is set to `nil`), gives a deprecation warning
```elixir
Earmark.as_html("https://github.com/pragdave")
{:ok, "https://github.com/pragdave
\\n",
[
{:deprecation, 1,
"The string \"https://github.com/pragdave/earmark\" will be rendered as a link if the option `pure_links` is enabled.\\nThis will be the case by default in version 1.4.\\nDisable the option explicitly with `false` to avoid this message."}
]}
```
- `pure_links: true`
```elixir
Earmark.as_html("https://github.com/pragdave", pure_links: true)
{:ok,
"\\n", []}
```
- Explicitly setting `pure_links` to `false` surpresses the deprecation warning
```elixir
Earmark.as_html("https://github.com/pragdave", pure_links: false)
{:ok, "https://github.com/pragdave
\\n", []}
```
"""
def as_html(lines, options \\ %Options{})
def as_html(lines, options) when is_list(options) do
as_html(lines, struct(Options, options))
end
def as_html(lines, options) do
{context, html} = _as_html(lines, options)
messages = sort_messages(context)
status =
case Enum.any?(messages, fn {severity, _, _} -> severity == :error || severity == :warning end) do
true -> :error
_ -> :ok
end
{status, html, messages}
end
@doc """
A convenience method that *always* returns an HTML representation of the markdown document passed in.
In case of the presence of any error messages they are prinetd to stderr.
Otherwise it behaves exactly as `as_html`.
"""
def as_html!(lines, options \\ %Options{})
def as_html!(lines, options) when is_list(options) do
as_html!(lines, struct(Options, options))
end
def as_html!(lines, options = %Options{}) do
{context, html} = _as_html(lines, options)
emit_messages(context)
html
end
defp _as_html(lines, options) do
{blocks, context} = Earmark.Parser.parse_markdown(lines, options)
context1 = _add_sanitize_deprecation(context)
case blocks do
[] -> {context, ""}
_ -> options.renderer.render(blocks, context1)
end
end
# TODO: Remove in 1.4
@doc false
def parse(lines, options) do
{blocks, context} = Earmark.Parser.parse_markdown(lines, options)
{blocks, context |> Earmark.Message.add_message({:deprecation, "DEPRECATED: This will not be part of the public API in Earmark 1.4",0})}
end
@doc """
Accesses current hex version of the `Earmark` application. Convenience for
`iex` usage.
"""
def version() do
with {:ok, version} = :application.get_key(:earmark, :vsn), do: version
end
@default_timeout_in_ms 5000
@doc false
def pmap(collection, func, timeout \\ @default_timeout_in_ms) do
collection
|> Enum.map(fn item -> Task.async(fn -> func.(item) end) end)
|> Task.yield_many(timeout)
|> Enum.map(&_join_pmap_results_or_raise(&1, timeout))
end
defp _add_sanitize_deprecation(%Earmark.Context{options: %Options{sanitize: sanitize}}=context) do
if sanitize do
add_message(context, {:deprecation, "DEPRECATED: The sanitize option will be removed in Earmark 1.4", 0})
else
context
end
end
defp _join_pmap_results_or_raise(yield_tuples, timeout)
defp _join_pmap_results_or_raise({_task, {:ok, result}}, _timeout), do: result
defp _join_pmap_results_or_raise({task, {:error, reason}}, _timeout),
do: raise(Error, "#{inspect(task)} has died with reason #{inspect(reason)}")
defp _join_pmap_results_or_raise({task, nil}, timeout),
do:
raise(
Error,
"#{inspect(task)} has not responded within the set timeout of #{timeout}ms, consider increasing it"
)
end
# SPDX-License-Identifier: Apache-2.0