Skip to content

Commit

Permalink
Add Elixir functions to expose native to_commonmark & to_commonmark_w…
Browse files Browse the repository at this point in the history
…ith_options (#70)
  • Loading branch information
jonklein authored Oct 4, 2024
1 parent 7ebbc1c commit aadbacb
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 3 deletions.
65 changes: 63 additions & 2 deletions lib/mdex.ex
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,63 @@ defmodule MDEx do
end
end

@doc """
Convert an AST to Commonmark using default options.
To customize the output, use `to_commonmark/2`.
## Examples
iex> MDEx.to_commonmark(MDEx.parse_document!("# MDEx"))
{:ok, "# MDEx\\n"}
"""

@spec to_commonmark(ast :: md_ast()) :: {:ok, String.t()} | {:error, MDEx.DecodeError.t()}
def to_commonmark(ast) do
Native.ast_to_commonmark(ast)
end

@doc """
Same as `to_commonmark/1` but raises `MDEx.DecodeError` if the conversion fails.
"""
@spec to_commonmark!(ast :: md_ast()) :: String.t()
def to_commonmark!(ast) do
case to_commonmark(ast) do
{:ok, md} -> md
{:error, error} -> raise error
end
end

@doc """
Convert an AST to Commonmark with custom options.
## Options
See the [Options](#module-options) section for the available options.
## Examples
iex> MDEx.to_commonmark(MDEx.parse_document!("# MDEx"), [])
{:ok, "# MDEx\\n"}
"""
def to_commonmark(md_or_ast, opts) when is_list(md_or_ast) do
md_or_ast
|> maybe_wrap_document()
|> Native.ast_to_commonmark_with_options(comrak_options(opts))
end

@doc """
Same as `to_commonmark/2` but raises `MDEx.DecodeError` if the conversion fails.
"""
@spec to_commonmark!(ast :: md_ast(), keyword()) :: String.t()
def to_commonmark!(ast, opts) do
case to_commonmark(ast, opts) do
{:ok, md} -> md
{:error, error} -> raise error
end
end

defp comrak_options(opts) do
extension = Keyword.get(opts, :extension, %{})
parse = Keyword.get(opts, :parse, %{})
Expand Down Expand Up @@ -204,8 +261,12 @@ defmodule MDEx do

defp maybe_wrap_error({:ok, result}), do: {:ok, result}
defp maybe_wrap_error({:error, {reason, found}}), do: {:error, %MDEx.DecodeError{reason: reason, found: found}}
defp maybe_wrap_error({:error, {reason, found, node}}), do: {:error, %MDEx.DecodeError{reason: reason, found: found, node: node}}
defp maybe_wrap_error({:error, {reason, found, node, kind}}), do: {:error, %MDEx.DecodeError{reason: reason, found: found, node: node, kind: kind}}

defp maybe_wrap_error({:error, {reason, found, node}}),
do: {:error, %MDEx.DecodeError{reason: reason, found: found, node: node}}

defp maybe_wrap_error({:error, {reason, found, node, kind}}),
do: {:error, %MDEx.DecodeError{reason: reason, found: found, node: node, kind: kind}}

defp maybe_wrap_error({:error, {reason, found, node, attr, kind}}),
do: {:error, %MDEx.DecodeError{reason: reason, found: found, node: node, attr: attr, kind: kind}}
Expand Down
2 changes: 2 additions & 0 deletions lib/mdex/native.ex
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,6 @@ defmodule MDEx.Native do
def parse_document(_md, _options), do: :erlang.nif_error(:nif_not_loaded)
def ast_to_html(_ast), do: :erlang.nif_error(:nif_not_loaded)
def ast_to_html_with_options(_ast, _options), do: :erlang.nif_error(:nif_not_loaded)
def ast_to_commonmark(_ast), do: :erlang.nif_error(:nif_not_loaded)
def ast_to_commonmark_with_options(_ast, _options), do: :erlang.nif_error(:nif_not_loaded)
end
75 changes: 74 additions & 1 deletion native/comrak_nif/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ rustler::init!(
markdown_to_html,
markdown_to_html_with_options,
ast_to_html,
ast_to_html_with_options
ast_to_html_with_options,
ast_to_commonmark,
ast_to_commonmark_with_options
]
);

Expand Down Expand Up @@ -157,3 +159,74 @@ fn ast_to_html_with_options<'a>(
}
}
}

#[rustler::nif(schedule = "DirtyCpu")]
fn ast_to_commonmark<'a>(env: Env<'a>, ast: Term<'a>) -> NifResult<Term<'a>> {
let ex_node = types::nodes::ExNode::decode(ast)?;

let arena = Arena::new();
let comrak_ast = ex_node_to_comrak_ast(&arena, &ex_node);

// FIXME: error handling format_html and from_utf8

let inkjet_adapter = InkjetAdapter::default();
let mut plugins = ComrakPlugins::default();
plugins.render.codefence_syntax_highlighter = Some(&inkjet_adapter);

let mut buffer = vec![];
comrak::format_commonmark_with_plugins(comrak_ast, &Options::default(), &mut buffer, &plugins)
.unwrap();
let html = String::from_utf8(buffer).unwrap();

Ok((ok(), html).encode(env))
}

#[rustler::nif(schedule = "DirtyCpu")]
fn ast_to_commonmark_with_options<'a>(
env: Env<'a>,
ast: Term<'a>,
options: ExOptions,
) -> NifResult<Term<'a>> {
let ex_node = types::nodes::ExNode::decode(ast)?;
let arena = Arena::new();
let comrak_ast = ex_node_to_comrak_ast(&arena, &ex_node);

let comrak_options = comrak::Options {
extension: extension_options_from_ex_options(&options),
parse: parse_options_from_ex_options(&options),
render: render_options_from_ex_options(&options),
};

match &options.features.syntax_highlight_theme {
Some(theme) => {
let inkjet_adapter = InkjetAdapter::new(
theme,
options
.features
.syntax_highlight_inline_style
.unwrap_or(true),
);
let mut plugins = ComrakPlugins::default();
plugins.render.codefence_syntax_highlighter = Some(&inkjet_adapter);

let mut buffer = vec![];
comrak::format_commonmark_with_plugins(
comrak_ast,
&comrak_options,
&mut buffer,
&plugins,
)
.unwrap();
let unsafe_html = String::from_utf8(buffer).unwrap();

maybe_sanitize(env, unsafe_html, options.features.sanitize)
}
None => {
let mut buffer = vec![];
comrak::format_commonmark(comrak_ast, &comrak_options, &mut buffer).unwrap();
let unsafe_html = String::from_utf8(buffer).unwrap();

maybe_sanitize(env, unsafe_html, options.features.sanitize)
}
}
}
25 changes: 25 additions & 0 deletions test/format_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@ defmodule MDEx.FormatTest do
greentext: true
]

def assert_commonmark(document, extension \\ []) do
opts = [
extension: Keyword.merge(@extension, extension),
render: [unsafe_: true]
]

assert {:ok, ast} = MDEx.parse_document(document, opts)
assert {:ok, markdown} = MDEx.to_commonmark(ast, opts)

assert markdown == document
end

def assert_format(document, expected, extension \\ []) do
opts = [
extension: Keyword.merge(@extension, extension),
Expand All @@ -28,12 +40,21 @@ defmodule MDEx.FormatTest do

assert {:ok, ast} = MDEx.parse_document(document, opts)
assert {:ok, html} = MDEx.to_html(ast, opts)

# IO.puts(html)
assert html == expected
end

test "text" do
assert_format("mdex", "<p>mdex</p>\n")

assert_commonmark("""
mdex
""")

assert_commonmark("""
Hello ~world~ there
""")
end

test "front matter" do
Expand All @@ -54,6 +75,10 @@ defmodule MDEx.FormatTest do
""",
"<blockquote>\n<p>MDEx</p>\n</blockquote>\n"
)

assert_commonmark("""
> MDEx
""")
end

describe "list" do
Expand Down

0 comments on commit aadbacb

Please sign in to comment.