defmodule Earmark do
@type ast_meta :: map()
@type ast_tag :: binary()
@type ast_attribute_name :: binary()
@type ast_attribute_value :: binary()
@type ast_attribute :: {ast_attribute_name(), ast_attribute_value()}
@type ast_attributes :: list(ast_attribute())
@type ast_tuple :: {ast_tag(), ast_attributes(), ast(), ast_meta()}
@type ast_node :: binary() | ast_tuple()
@type ast :: list(ast_node())
@moduledoc """
### API
Earmark now exposes a well-defined and stable Abstract Syntax Tree
#### Earmark.as_ast
WARNING: This is just a proxy towards `EarmarkParser.as_ast` and is deprecated, it will be removed in version 1.5!
Replace your calls to `Earmark.as_ast` with `EarmarkParse.as_ast` as soon as possible.
**N.B.** If all you use is `Earmark.as_ast` consider _only_ using `EarmarkParser`.
The function is described below and the other two API functions `as_html` and `as_html!` are now based upon
the structure of the result of `as_ast`.
{:ok, ast, []} = EarmarkParser.as_ast(markdown)
{:ok, ast, deprecation_messages} = EarmarkParser.as_ast(markdown)
{:error, ast, error_messages} = EarmarkParser.as_ast(markdown)
#### 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)
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
#### Options
Options can be passed into `as_ast/2`as well as `as_html/2` or `as_html!/2` according to the documentation.
{status, html_doc, errors} = Earmark.as_html(markdown, options)
html_doc = Earmark.as_html!(markdown, options)
{status, ast, errors} = EarmarkParser.as_ast(markdown, options)
### 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]: \\n \\nlink \\nSome text \\nlink{: .classy} \\nhello {:world} mypara mypara
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
@tag :hello...
As for all other options `code_class_prefix` can be passed into the `earmark` executable as follows:
earmark --code-class-prefix "language- lang-" ...
#### 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 unless
there are exterior bars.
However in order to be more GFM compatible the `gfm_tables: true` option
can be used to interpret only interior vertical bars as a table if a seperation
line is given, therefor
Language|Rating
--------|------
Elixir | awesome
is a table (iff `gfm_tables: true`) while
Language|Rating
Elixir | awesome
never is.
#### HTML Blocks
HTML is not parsed recursively or detected in all conditons right now, though GFM compliance
is a goal.
But for now the following holds:
A HTML Block defined by a tag starting a line and the same tag starting a different line is parsed
as one HTML AST node, marked with %{verbatim: true}
E.g.
iex(3)> lines = [ "
Becomes
While
mypara
will be transformed into
Code\\\```\\n", []}
* `smartypants`: boolean
Turns on smartypants processing, so quotes become curly, two
or three 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 rendered as links from now on.
However, by setting the `pure_links` option to `false` this can be disabled and pre 1.4
behavior can be used.
* `compact_output`: boolean
If set to true, no cosmetic newlines will be emitted by Earmark. False by default.
"""
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
{status, ast, messages} = EarmarkParser.as_ast(lines, options)
{status, Earmark.Transform.transform(ast, options), messages}
end
@doc """
`as_ast` is a compatibility function to call `EarmarkParser.as_ast`
It is deprecated and will be removed in 1.5!
Options are passes like to `as_html`, some do not have an effect though (e.g. `smartypants`) as formatting and escaping is not done
for the AST.
iex(12)> markdown = "```elixir\\nIO.puts 42\\n```"
...(12)> {:ok, ast, []} = EarmarkParser.as_ast(markdown, code_class_prefix: "lang-")
...(12)> ast
[{"pre", [], [{"code", [{"class", "elixir lang-elixir"}], ["IO.puts 42"], %{}}], %{}}]
"""
def as_ast(lines, options \\ %Options{}) do
{status, ast, messages} = _as_ast(lines, options)
message =
{:warning, 0,
"DEPRECATION: Earmark.as_ast will be removed in version 1.5, please use EarmarkParser.as_ast, which is of the same type"}
messages1 = [message | messages]
{status, ast, messages1}
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
{_status, html, messages} = as_html(lines, options)
emit_messages(messages, options)
html
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: to_string(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 _as_ast(lines, options)
defp _as_ast(lines, %Options{} = options) do
EarmarkParser.as_ast(lines, options |> Map.delete(:__struct__) |> Enum.into([]))
end
defp _as_ast(lines, options) do
EarmarkParser.as_ast(lines, options)
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