Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 33 additions & 12 deletions lib/mdex.ex
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@ defmodule MDEx do
end

def to_html(%Document{} = document, options) when is_list(options) do
run_pipeline(document, options, &Comrak.document_to_html/2)
run_pipeline(document, options, &Comrak.document_to_html/2, &Comrak.markdown_to_html/2)
rescue
ErlangError ->
{:error, %DecodeError{document: document}}
Expand Down Expand Up @@ -1155,20 +1155,41 @@ defmodule MDEx do
defp maybe_trim(result) when is_binary(result), do: {:ok, String.trim(result)}
defp maybe_trim(error), do: error

defp run_pipeline(document, options, converter) do
defp run_pipeline(document, options, converter, markdown_converter \\ nil) do
{document_opt, options} = pop_deprecated_document_option(options)

document
|> Document.put_options(options)
|> maybe_apply_document_option(document_opt)
|> Document.run()
|> then(fn document ->
document =
document
|> apply_codefence_renderers_to_document(document.options[:codefence_renderers])
|> ComrakConverter.from_mdex()
|> converter.(Document.rust_options!(document.options))
|> maybe_trim()
end)
|> Document.put_options(options)
|> maybe_apply_document_option(document_opt)

case source_markdown(document, markdown_converter) do
{:ok, markdown} ->
markdown
|> markdown_converter.(Document.rust_options!(document.options))
|> maybe_trim()

:error ->
document
|> Document.run()
|> then(fn document ->
document
|> apply_codefence_renderers_to_document(document.options[:codefence_renderers])
|> ComrakConverter.from_mdex()
|> converter.(Document.rust_options!(document.options))
|> maybe_trim()
end)
end
end

defp source_markdown(_document, nil), do: :error

defp source_markdown(document, _markdown_converter) do
if document.options[:codefence_renderers] in [nil, %{}] do
Document.unparsed_markdown(document)
else
:error
end
end

defp apply_codefence_renderers_to_document(document, renderers) when renderers in [nil, %{}] do
Expand Down
71 changes: 39 additions & 32 deletions lib/mdex/comrak_converter.ex
Original file line number Diff line number Diff line change
@@ -1,43 +1,50 @@
defmodule MDEx.ComrakConverter do
@moduledoc false

def to_mdex(value), do: convert(value, ["MDExNative", "Comrak"], MDEx)
def from_mdex(value), do: convert(value, ["MDEx"], MDExNative.Comrak)

defp convert(nodes, from, to) when is_list(nodes), do: Enum.map(nodes, &convert(&1, from, to))

defp convert(%module{} = node, from, to) do
case convert_module(module, from, to) do
{:ok, target} ->
fields =
node
|> Map.from_struct()
|> Map.new(fn
{key, value} when key in [:nodes, :sourcepos, :attrs] ->
{key, convert(value, from, to)}

{key, value} ->
{key, value}
end)
@nodes ~w(
Alert Attributes BlockDirective BlockQuote Code CodeBlock DescriptionDetails
DescriptionItem DescriptionList DescriptionTerm Document Emph Escaped EscapedTag
FootnoteDefinition FootnoteReference FrontMatter Heading HeexBlock HeexInline
Highlight HtmlBlock HtmlInline Image Insert LineBreak Link List ListItem Math
MultilineBlockQuote Paragraph Raw ShortCode SoftBreak Sourcepos SpoileredText
Strikethrough Strong Subscript Subtext Superscript Table TableCell TableRow
TaskItem Text ThematicBreak Underline WikiLink
)a

def to_mdex(value), do: convert(value, :to_mdex)
def from_mdex(value), do: convert(value, :from_mdex)

defp convert(nodes, direction) when is_list(nodes), do: Enum.map(nodes, &convert(&1, direction))

defp convert(%module{} = node, direction) do
target = translate!(module, direction)

fields =
node
|> Map.from_struct()
|> convert_nested(:nodes, direction)
|> convert_nested(:sourcepos, direction)
|> convert_nested(:attrs, direction)

struct(target, fields)
end

struct(target, fields)
defp convert(value, _direction), do: value

:error ->
raise ArgumentError, "cannot convert #{inspect(module)}"
defp convert_nested(fields, key, direction) do
case fields do
%{^key => value} -> %{fields | key => convert(value, direction)}
_ -> fields
end
end

defp convert(value, _from, _to), do: value
for name <- @nodes do
native = Module.concat(MDExNative.Comrak, name)
mdex = Module.concat(MDEx, name)

defp convert_module(module, from, to) do
parts = Module.split(module)

case Enum.split(parts, length(from)) do
{^from, [_name]} ->
{:ok, Module.safe_concat([to, List.last(parts)])}

_ ->
:error
end
defp translate!(unquote(native), :to_mdex), do: unquote(mdex)
defp translate!(unquote(mdex), :from_mdex), do: unquote(native)
end

defp translate!(module, _direction), do: raise(ArgumentError, "cannot convert #{inspect(module)}")
end
11 changes: 11 additions & 0 deletions lib/mdex/document.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2276,6 +2276,17 @@ defmodule MDEx.Document do
end
end

@doc false
def unparsed_markdown(%MDEx.Document{nodes: [], buffer: [_ | _] = buffer, current_steps: [], halted: false} = document) do
if get_private(document, :auto_close, false) do
:error
else
{:ok, buffer_to_binary(buffer)}
end
end

def unparsed_markdown(%MDEx.Document{}), do: :error

defp buffer_to_binary(buffer) do
buffer
|> Enum.reverse()
Expand Down
31 changes: 31 additions & 0 deletions test/mdex/comrak_converter_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,16 @@ defmodule MDEx.ComrakConverterTest do
MDEx.ComrakConverter.to_mdex(native_code)
end

test "rebuilds structs when source and target fields differ" do
native_code =
%MDExNative.Comrak.Code{literal: "elixir"}
|> Map.delete(:attrs)
|> Map.put(:future_field, true)

assert %MDEx.Code{literal: "elixir", attrs: nil} =
MDEx.ComrakConverter.to_mdex(native_code)
end

test "converts mdex document structs to native structs" do
document = %MDEx.Document{
nodes: [
Expand Down Expand Up @@ -118,6 +128,27 @@ defmodule MDEx.ComrakConverterTest do
end
end

test "round-trips every struct MDExNative.Comrak defines" do
natives = native_structs()
refute Enum.empty?(natives)

for module <- natives do
["MDExNative", "Comrak", suffix] = Module.split(module)
native = module.__struct__()
mdex = MDEx.ComrakConverter.to_mdex(native)

assert mdex.__struct__ == Module.concat(MDEx, suffix)
assert MDEx.ComrakConverter.from_mdex(mdex) == native
end
end

defp native_structs do
for module <- Application.spec(:mdex_native, :modules),
match?(["MDExNative", "Comrak", _suffix], Module.split(module)),
Code.ensure_loaded?(module) and function_exported?(module, :__struct__, 0),
do: module
end

defp fields(module) do
module.__struct__()
|> Map.from_struct()
Expand Down
53 changes: 51 additions & 2 deletions test/mdex/html_format_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,11 @@ defmodule MDEx.HTMLFormatTest do
render: [unsafe: true]
]

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

# IO.puts(html)
assert direct_html == html
assert html == String.trim(expected)
end

Expand All @@ -54,10 +55,11 @@ defmodule MDEx.HTMLFormatTest do
render: [unsafe: true]
]

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

# IO.puts(html)
assert direct_html == html
assert html == String.trim(expected)
end

Expand Down Expand Up @@ -669,4 +671,51 @@ defmodule MDEx.HTMLFormatTest do
block_directive: true
)
end

describe "rendering without an AST" do
@markdown "# Title\n\nSome **bold** text with `code` and a [link](http://example.com).\n"

test "renders the same HTML as the pipeline" do
with_step = MDEx.Document.append_steps(MDEx.new(markdown: @markdown), noop: & &1)

assert MDEx.to_html!(@markdown) == MDEx.to_html!(with_step)
end

test "preserves buffered Markdown order" do
document =
MDEx.new(markdown: "# Last")
|> MDEx.Document.put_markdown("# First\n", :top)

with_step = MDEx.Document.append_steps(document, noop: & &1)

assert MDEx.to_html!(document) == MDEx.to_html!(with_step)
end

test "still runs pipeline steps" do
upcase = &MDEx.Document.update_nodes(&1, MDEx.Text, fn node -> %{node | literal: String.upcase(node.literal)} end)
document = MDEx.Document.append_steps(MDEx.new(markdown: @markdown), upcase: upcase)

assert MDEx.to_html!(document) =~ "<h1>TITLE</h1>"
end

test "still renders documents that were already parsed" do
document = %MDEx.Document{nodes: [%MDEx.Paragraph{nodes: [%MDEx.Text{literal: "parsed"}]}]}

assert MDEx.to_html!(document) == "<p>parsed</p>"
end

test "keeps parsed nodes when more Markdown is buffered" do
document =
MDEx.parse_document!("# Parsed\n")
|> MDEx.Document.put_markdown("# Appended\n")

assert MDEx.to_html!(document) == "<h1>Parsed</h1>\n<h1>Appended</h1>"
end

test "does not render the buffer of a halted document" do
document = MDEx.Document.put_markdown(%MDEx.Document{halted: true}, "# Buffered\n")

assert MDEx.to_html!(document) == ""
end
end
end