From 2a930953099911290a3bca46901a719102adb9e8 Mon Sep 17 00:00:00 2001 From: Adrian Salamon Date: Wed, 19 Jul 2023 00:30:31 +0200 Subject: [PATCH] Add support for custom html converters (#28) --- README.md | 31 +++++++++++++++++++ lib/nimble_publisher.ex | 46 +++++++++++++++++++---------- lib/nimble_publisher/highlighter.ex | 9 ++++-- test/nimble_publisher_test.exs | 43 +++++++++++++++++++++++++++ 4 files changed, 112 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index aa70c14..c7f2586 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,10 @@ Each article in the articles directory must have the format: * `:parser` - custom module with a `parse/2` function that receives the file path and content as params. See [Custom parser](#custom-parser) for more details. + * `:html_converter` - custom module with a `convert/4` function that receives the + extension, body, and attributes of the markdown file, as well as all options + as params. See [Custom HTML converter](#custom-html-converter) for more details. + ## Examples Let's see a complete example. First add `nimble_publisher` with @@ -193,6 +197,33 @@ It must return: * a 2 element tuple with attributes and body - `{attrs, body}` * a list of 2 element tuple with attributes and body - `[{attrs, body} | _]` +### Custom HTML converter + +You can also define a custom HTML converter that will be used to convert the +file body (typically Markdown) into HTML. For example, you may wish to use an +alternative Markdown parser such as [md](https://github.com/am-kantox/md). +If you want to use the built-in highlighting, you need to call it manually. + +```elixir + use NimblePublisher, + ... + html_converter: MarkdownConverter, + highlighters: [:makeup_elixir] +``` + +```elixir +defmodule MarkdownConverter do + def convert(extname, body, _attrs, opts) when extname in [".md", ".markdown"] do + highlighters = Keyword.get(opts, :highlighters, []) + body |> Md.generate() |> NimblePublisher.highlight(highlighters) + end +end +``` + +The `convert/4` function from this module receives an extension name, a body, +the parsed attributes from the file, and the options passed to +`NimblePublisher`. It must return the converted body as a string. + ### Live reloading If you are using Phoenix, you can enable live reloading by simply telling Phoenix to watch the “posts” directory. Open up "config/dev.exs", search for `live_reload:` and add this to the list of patterns: diff --git a/lib/nimble_publisher.ex b/lib/nimble_publisher.ex index 0dc492c..77b5f9a 100644 --- a/lib/nimble_publisher.ex +++ b/lib/nimble_publisher.ex @@ -47,30 +47,42 @@ defmodule NimblePublisher do {from, paths} end - defp build_entry(builder, path, {_attr, _body} = parsed_contents, opts) do + @doc """ + Highlights all code blocks in an already generated HTML document. + + It uses Makeup and expects the existing highlighters applications to + be already started. + + Options: + + * `:regex` - the regex used to find code blocks in the HTML document. The regex + should have two capture groups: the first one should be the language name + and the second should contain the code to be highlighted. The default + regex to match with generated HTML documents is: + + ~r/
([^<]*)<\/code><\/pre>/
+  """
+  defdelegate highlight(html, options \\ []), to: NimblePublisher.Highlighter
+
+  defp build_entry(builder, path, {_attrs, _body} = parsed_contents, opts) do
     build_entry(builder, path, [parsed_contents], opts)
   end
 
   defp build_entry(builder, path, parsed_contents, opts) when is_list(parsed_contents) do
+    converter_module = Keyword.get(opts, :html_converter)
+    extname = Path.extname(path) |> String.downcase()
+
     Enum.map(parsed_contents, fn {attrs, body} ->
       body =
-        path
-        |> Path.extname()
-        |> String.downcase()
-        |> convert_body(body, opts)
+        case converter_module do
+          nil -> convert_body(extname, body, opts)
+          module -> module.convert(extname, body, attrs, opts)
+        end
 
       builder.build(path, attrs, body)
     end)
   end
 
-  defp highlight(html, []) do
-    html
-  end
-
-  defp highlight(html, _) do
-    NimblePublisher.Highlighter.highlight(html)
-  end
-
   defp parse_contents!(path, contents, nil) do
     case parse_contents(path, contents) do
       {:ok, attrs, body} ->
@@ -115,8 +127,12 @@ defmodule NimblePublisher do
 
   defp convert_body(extname, body, opts) when extname in [".md", ".markdown", ".livemd"] do
     earmark_opts = Keyword.get(opts, :earmark_options, %Earmark.Options{})
-    highlighters = Keyword.get(opts, :highlighters, [])
-    body |> Earmark.as_html!(earmark_opts) |> highlight(highlighters)
+    html = Earmark.as_html!(body, earmark_opts)
+
+    case Keyword.get(opts, :highlighters, []) do
+      [] -> html
+      [_ | _] -> highlight(html)
+    end
   end
 
   defp convert_body(_extname, body, _opts) do
diff --git a/lib/nimble_publisher/highlighter.ex b/lib/nimble_publisher/highlighter.ex
index 6092698..ad1bd8f 100644
--- a/lib/nimble_publisher/highlighter.ex
+++ b/lib/nimble_publisher/highlighter.ex
@@ -4,9 +4,14 @@ defmodule NimblePublisher.Highlighter do
   @doc """
   Highlights all code block in an already generated HTML document.
   """
-  def highlight(html) do
+
+  @default_regex ~r/
([^<]*)<\/code><\/pre>/
+
+  def highlight(html, options \\ []) do
+    regex = Keyword.get(options, :regex, @default_regex)
+
     Regex.replace(
-      ~r/
([^<]*)<\/code><\/pre>/,
+      regex,
       html,
       &highlight_code_block(&1, &2, &3)
     )
diff --git a/test/nimble_publisher_test.exs b/test/nimble_publisher_test.exs
index e3f86fe..ec4d310 100644
--- a/test/nimble_publisher_test.exs
+++ b/test/nimble_publisher_test.exs
@@ -178,6 +178,27 @@ defmodule NimblePublisherTest do
     end
   end
 
+  test "allows for custom markdown parsing function returning parsed html" do
+    defmodule MarkdownConverter do
+      def convert(extname, _body, attrs, opts) do
+        from = Keyword.get(opts, :from)
+
+        "

This is a custom markdown converter from a #{extname} file, from the #{from} file, hello #{attrs.hello}

\n" + end + end + + defmodule Example do + use NimblePublisher, + build: Builder, + from: "test/fixtures/markdown.md", + as: :custom, + html_converter: MarkdownConverter + + assert hd(@custom).body == + "

This is a custom markdown converter from a .md file, from the test/fixtures/markdown.md file, hello world

\n" + end + end + test "raises if missing separator" do assert_raise RuntimeError, ~r/could not find separator --- in "test\/fixtures\/invalid.noseparator"/, @@ -203,4 +224,26 @@ defmodule NimblePublisherTest do end end end + + test "highlights code blocks" do + higlighters = [:makeup_elixir, :makeup_erlang] + input = "
IO.puts(\"Hello World\")
" + output = NimblePublisher.highlight(input, higlighters) + + assert output =~ "
IO"
+  end
+
+  test "highlights code blocks with custom regex" do
+    highlighters = [:makeup_elixir]
+    input = "IO.puts(\"Hello World\")"
+
+    output =
+      NimblePublisher.highlight(
+        input,
+        highlighters,
+        regex: ~r/([^<]*)<\/code>/
+      )
+
+    assert output =~ "
"
+  end
 end