Skip to content

Commit

Permalink
Represent attrs as map instead of list (#79)
Browse files Browse the repository at this point in the history
  • Loading branch information
leandrocp authored Oct 14, 2024
1 parent 2ea47f6 commit f028c36
Show file tree
Hide file tree
Showing 12 changed files with 329 additions and 419 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## 0.3.0-dev

### Breaking changes
* Represent attrs as map instead of list, ie: `%{"num_backticks" => 1, "literal" => ":elixir"}` instead of `[{"num_backticks", 1}, {"literal", ":elixir"}]`.
* Removed `MDEx.attribute/2` in favor of pattern matching key/value pairs in the attrs map directly.

## 0.2.0 (2024-10-09)

### Breaking changes
Expand Down
8 changes: 4 additions & 4 deletions examples/alerts.exs
Original file line number Diff line number Diff line change
Expand Up @@ -96,14 +96,14 @@ html =
{"document", attrs, children ++ tailwind}

# inject a html block to render a note alert
{"block_quote", _attrs, [{"paragraph", [], ["[!NOTE]", _note_attrs, note_content]}]} ->
{"block_quote", _attrs, [{"paragraph", %{}, ["[!NOTE]", _note_attrs, note_content]}]} ->
alert = note_html.(note_content)
{"html_block", [{"literal", alert}], []}
{"html_block", %{"literal" => alert}, []}

# inject a html block to render a caution alert
{"block_quote", _attrs, [{"paragraph", [], ["[!CAUTION]", _note_attrs, note_content]}]} ->
{"block_quote", _attrs, [{"paragraph", %{}, ["[!CAUTION]", _note_attrs, note_content]}]} ->
alert = caution_html.(note_content)
{"html_block", [{"literal", alert}], []}
{"html_block", %{"literal" => alert}, []}

node ->
node
Expand Down
2 changes: 1 addition & 1 deletion examples/highlight.exs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ ast =
|> Enum.map(fn
"==" <> rest ->
marked_text = "<mark>" <> String.replace_suffix(rest, "==", "</mark>")
{"html_block", [{"literal", marked_text}], []}
{"html_block", %{"literal" => marked_text}, []}

text ->
text
Expand Down
15 changes: 5 additions & 10 deletions examples/mermaid.exs
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,12 @@ html =
{"document", attrs, children ++ mermaid}

# inject the mermaid <pre> block without escaping the content
{"code_block", _attrs, children} = node ->
with ["mermaid"] <- MDEx.attribute(node, "info"),
[code] <- MDEx.attribute(node, "literal") do
code = """
<pre class="mermaid">#{code}</pre>
"""
{"code_block", %{"info" => "mermaid", "literal" => code}, children} ->
code = """
<pre class="mermaid">#{code}</pre>
"""

{"html_block", [{"literal", code}], children}
else
_ -> node
end
{"html_block", %{"literal" => code}, children}

node ->
node
Expand Down
106 changes: 23 additions & 83 deletions lib/mdex.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ defmodule MDEx do
## Example
[
{"document", [], [
{"heading", [{"level", 1}], ["Elixir"]}
{"document", %{}, [
{"heading", %{"level" => 1}, ["Elixir"]}
]}
]
Expand All @@ -37,7 +37,7 @@ defmodule MDEx do
## Example
{"heading", [{"level", 1}, {"setext", false}], children}
{"heading", %{"level" => 1, "setext" => false}, children}
"""
@type md_element :: {name :: String.t(), attributes :: [md_attribute()], children :: [md_node()]}

Expand All @@ -46,10 +46,10 @@ defmodule MDEx do
## Examples
{"level", 1}
{"delimiter", "period"}
%{"level" => 1}
%{"delimiter" => "period"}
"""
@type md_attribute :: {String.t(), term()}
@type md_attribute :: %{required(String.t()) => term()}

@typedoc """
Text element. It has no attributes or children so it's represented just as a string.
Expand All @@ -67,18 +67,18 @@ defmodule MDEx do
iex> MDEx.parse_document!("# Languages\\n Elixir and Rust")
[
{"document", [], [
{"heading", [{"level", 1}, {"setext", false}], ["Languages"]},
{"paragraph", [], ["Elixir and Rust"]}
{"document", %{}, [
{"heading", %{"level" => 1, "setext" => false}, ["Languages"]},
{"paragraph", %{}, ["Elixir and Rust"]}
]}
]
iex> MDEx.parse_document!("Darth Vader is ||Luke's father||", extension: [spoiler: true])
[
{"document", [], [
{"paragraph", [], [
{"document", %{}, [
{"paragraph", %{}, [
"Darth Vader is ",
{"spoilered_text", [], ["Luke's father"]}
{"spoilered_text", %{}, ["Luke's father"]}
]}]}
]
"""
Expand Down Expand Up @@ -111,7 +111,7 @@ defmodule MDEx do
iex> MDEx.to_html("Implemented with:\\n1. Elixir\\n2. Rust")
{:ok, "<p>Implemented with:</p>\\n<ol>\\n<li>Elixir</li>\\n<li>Rust</li>\\n</ol>\\n"}
iex> MDEx.to_html([{"document", [], [{"heading", [{"level", 3}], ["MDEx"]}]}])
iex> MDEx.to_html([{"document", %{}, [{"heading", %{"level" => 3}, ["MDEx"]}]}])
{:ok, "<h3>MDEx</h3>\\n"}
"""
@spec to_html(md_or_ast :: String.t() | md_ast()) :: {:ok, String.t()} | {:error, MDEx.DecodeError.t()}
Expand Down Expand Up @@ -187,7 +187,7 @@ defmodule MDEx do
## Example
iex> MDEx.to_commonmark([{"document", [], [{"heading", [{"level", 3}], ["Hello"]}]}])
iex> MDEx.to_commonmark([{"document", %{}, [{"heading", %{"level" => 3}, ["Hello"]}]}])
{:ok, "### Hello\\n"}
"""
@spec to_commonmark(ast :: md_ast()) :: {:ok, String.t()} | {:error, MDEx.DecodeError.t()}
Expand All @@ -202,12 +202,12 @@ defmodule MDEx do
## Example
iex> MDEx.to_commonmark([{"document", [], [{"heading", [{}], ["Hello"]}]}])
iex> MDEx.to_commonmark([{"document", %{}, [{"heading", nil, ["Hello"]}]}])
{:error,
%MDEx.DecodeError{
reason: :missing_attr_field,
found: "[{}]",
node: "(<<\\"heading\\">>, [{}], [<<\\"Hello\\">>])",
found: "nil",
node: "(<<\\"heading\\">>, nil, [<<\\"Hello\\">>])",
attr: nil,
kind: nil
}}
Expand Down Expand Up @@ -264,7 +264,7 @@ defmodule MDEx do
defp maybe_wrap_document([{"document", _, _} | _] = tree), do: tree

defp maybe_wrap_document([fragment]) when is_tuple(fragment) do
[{"document", [], [fragment]}]
[{"document", %{}, [fragment]}]
end

defp maybe_wrap_document(fragment) when is_list(fragment) do
Expand All @@ -278,7 +278,7 @@ defmodule MDEx do
"""

[{"document", [], [{"paragraph", [], fragment}]}]
[{"document", %{}, [{"paragraph", %{}, fragment}]}]
end

defp maybe_wrap_error({:ok, result}), do: {:ok, result}
Expand All @@ -300,12 +300,13 @@ defmodule MDEx do
## Example
iex> ast = [{"document", [], [{"heading", [{"level", 1}, {"setext", false}], ["Hello"]}]}]
iex> ast = [{"document", %{}, [{"heading", %{"level" => 1, "setext" => false}, ["Hello"]}]}]
iex> MDEx.traverse_and_update(ast, fn
...> {"heading", _attrs, children} -> {"heading", [{"level", 2}], children}
...> {"heading", %{"level" => 6}, children} -> {"heading", %{"level" => 6}, children}
...> {"heading", %{"level" => level}, children} -> {"heading", %{"level" => level + 1}, children}
...> other -> other
...> end)
[{"document", [], [{"heading", [{"level", 2}], ["Hello"]}]}]
[{"document", %{}, [{"heading", %{"level" => 2}, ["Hello"]}]}]
See more on the [examples](https://github.com/leandrocp/mdex/tree/main/examples) directory.
"""
Expand All @@ -314,65 +315,4 @@ defmodule MDEx do
(md_node() -> md_node() | [md_node()] | nil)
) :: md_node() | md_ast()
defdelegate traverse_and_update(ast, fun), to: MDEx.Traversal

# https://github.com/philss/floki/blob/28c9ed8d10d851b63ec87fb8ab9c5acd3c7ea90c/lib/floki.ex#L670
@doc """
Returns a list with attribute values for a given `attribute_name`, otherwise returns an empty list.
## Example
iex> MDEx.attribute({
...> "code_block",
...> [{"info", "mermaid"}, {"literal", "graph TD;\\n A-->B;"}],
...> []
...> }, "literal")
["graph TD;\\n A-->B;"]
iex> MDEx.attribute({
...> "code_block",
...> [{"info", "mermaid"}, {"literal", "graph TD;\\n A-->B;"}],
...> []
...> }, "other")
[]
"""
@spec attribute(md_ast() | md_node(), String.t()) :: list()
def attribute(ast_or_node, attribute_name) do
attribute_values(ast_or_node, attribute_name)
end

defp attribute_values(element, attr_name) when is_tuple(element) do
attribute_values([element], attr_name)
end

defp attribute_values(elements, attr_name) do
values =
Enum.reduce(
elements,
[],
fn
{_, attributes, _}, acc ->
case attribute_match?(attributes, attr_name) do
{_attr_name, value} ->
[value | acc]

_ ->
acc
end

_, acc ->
acc
end
)

Enum.reverse(values)
end

defp attribute_match?(attributes, attribute_name) do
Enum.find(
attributes,
fn {attr_name, _} ->
attr_name == attribute_name
end
)
end
end
6 changes: 3 additions & 3 deletions lib/mdex/sigil.ex
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,14 @@ defmodule MDEx.Sigil do
# markdown to ast
iex> ~M|# Hello|AST
[{"document", [], [{"heading", [{"level", 1}, {"setext", false}], ["Hello"]}]}]
[{"document", %{}, [{"heading", %{"level" => 1, "setext" => false}, ["Hello"]}]}]
# ast to markdown
iex> ~M|[{"document", [], [{"heading", [{"level", 1}], ["Hello"]}]}]|MD
iex> ~M|[{"document", %{}, [{"heading", %{"level" => 1}, ["Hello"]}]}]|MD
"# Hello\n"
# ast to html
iex> ~M|[{"document", [], [{"heading", [{"level", 1}], ["Hello"]}]}]|
iex> ~M|[{"document", %{}, [{"heading", %{"level" => 1}, ["Hello"]}]}]|
"<h1>Hello</h1>\n"
"""
Expand Down
Loading

0 comments on commit f028c36

Please sign in to comment.