Packages
surface
0.3.1
0.12.3
0.12.2
0.12.1
0.12.0
0.11.5
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.2
0.3.1
0.3.0
0.2.1
0.2.0
0.1.1
0.1.0
0.1.0-alpha.2
0.1.0-alpha.1
0.1.0-alpha.0
A component based library for Phoenix LiveView
Current section
Files
Jump to
Current section
Files
lib/surface/components/markdown.ex
defmodule Surface.Components.Markdown do
@moduledoc """
A simple macro component that converts **markdown** into **HTML** at compile-time.
## Global configuration (optional)
A set of global options you can set in `config.exs`. Available options are:
* `default_class` - The default CSS class for the wrapping `<div>`. It
can be overridden using propety `class`.
* `default_opts` - The default set of options to be passed down to `Earmark.as_html/2`.
It can be overridden using propety `opts`.
## CSS Styling
Some CSS libs define their own styles for tags like `<p>`, `<ul>`, `<ol>`, `<strong>`,
`<h1>` to `<h6>`, etc. This can make the rendered HTML look different from what you
expect. One way to fix that is to customize the CSS class on the outer `<div>` of the
generated code.
For instance, in `Bulma`, you can use the class `content` to handle WYSIWYG content
like the HTML generated by the Markdown component.
You can have a default class applied globally using the `default_class` config:
```
config :surface, :components, [
{Surface.Components.Markdown, default_class: "content"}
]
```
Or you can set/override it individually for each `<#Markdown>` instance using
the `class` property.
"""
use Surface.MacroComponent
alias Surface.MacroComponent
alias Surface.IOHelper
@doc "The CSS class for the wrapping `<div>`"
prop class, :string
@doc "Removes the wrapping `<div>`, if `true`"
prop unwrap, :boolean, default: false
@doc """
Keyword list with options to be passed down to `Earmark.as_html/2`.
For a full list of available options, please refer to the
[Earmark.as_html/2](https://hexdocs.pm/earmark/Earmark.html#as_html/2)
documentation.
"""
prop opts, :keyword, default: []
@doc "The markdown text to be translated to HTML"
slot default
def expand(attributes, children, meta) do
props = MacroComponent.eval_static_props!(__MODULE__, attributes, meta.caller)
class = props[:class] || get_config(:default_class)
unwrap = props[:unwrap] || false
config_opts =
case get_config(:default_opts) do
nil -> []
opts -> opts
end
opts = Keyword.merge(config_opts, props[:opts] || [])
html =
children
|> IO.iodata_to_binary()
|> trim_leading_space()
# Need to reconstruct the relative line
|> markdown_as_html!(meta.caller, meta.line, opts)
node = %Surface.AST.Literal{value: html}
cond do
unwrap ->
node
class ->
%Surface.AST.Tag{
element: "div",
directives: [],
attributes: [
%Surface.AST.Attribute{
name: "class",
value: %Surface.AST.Literal{value: class}
}
],
children: [node],
meta: meta
}
true ->
%Surface.AST.Tag{
element: "div",
directives: [],
attributes: [],
children: [node],
meta: meta
}
end
end
defp trim_leading_space(markdown) do
lines =
markdown
|> String.split("\n")
|> Enum.drop_while(fn str -> String.trim(str) == "" end)
case lines do
[first | _] ->
[space] = Regex.run(~r/^\s*/, first)
lines
|> Enum.map(fn line -> String.replace_prefix(line, space, "") end)
|> Enum.join("\n")
_ ->
""
end
end
defp markdown_as_html!(markdown, caller, tag_line, opts) do
markdown
|> Earmark.as_html(struct(Earmark.Options, opts))
|> handle_result!(caller, tag_line)
end
defp handle_result!({_, html, messages}, caller, tag_line) do
{errors, warnings_and_deprecations} =
Enum.split_with(messages, fn {type, _line, _message} -> type == :error end)
Enum.each(warnings_and_deprecations, fn {_type, line, message} ->
actual_line = tag_line + line - 1
IOHelper.warn(message, caller, fn _ -> actual_line end)
end)
if errors != [] do
[{_type, line, message} | _] = errors
actual_line = tag_line + line - 1
IOHelper.compile_error(message, caller.file, actual_line)
end
html
end
end