Skip to content

Commit

Permalink
Add support for custom html converters (#28)
Browse files Browse the repository at this point in the history
  • Loading branch information
adriansalamon authored Jul 18, 2023
1 parent c4aa513 commit 2a93095
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 17 deletions.
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
46 changes: 31 additions & 15 deletions lib/nimble_publisher.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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/<pre><code(?:\s+class="(\w*)")?>([^<]*)<\/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} ->
Expand Down Expand Up @@ -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
Expand Down
9 changes: 7 additions & 2 deletions lib/nimble_publisher/highlighter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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/<pre><code(?:\s+class="(\w*)")?>([^<]*)<\/code><\/pre>/

def highlight(html, options \\ []) do
regex = Keyword.get(options, :regex, @default_regex)

Regex.replace(
~r/<pre><code(?:\s+class="(\w*)")?>([^<]*)<\/code><\/pre>/,
regex,
html,
&highlight_code_block(&1, &2, &3)
)
Expand Down
43 changes: 43 additions & 0 deletions test/nimble_publisher_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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)

"<p>This is a custom markdown converter from a #{extname} file, from the #{from} file, hello #{attrs.hello}</p>\n"
end
end

defmodule Example do
use NimblePublisher,
build: Builder,
from: "test/fixtures/markdown.md",
as: :custom,
html_converter: MarkdownConverter

assert hd(@custom).body ==
"<p>This is a custom markdown converter from a .md file, from the test/fixtures/markdown.md file, hello world</p>\n"
end
end

test "raises if missing separator" do
assert_raise RuntimeError,
~r/could not find separator --- in "test\/fixtures\/invalid.noseparator"/,
Expand All @@ -203,4 +224,26 @@ defmodule NimblePublisherTest do
end
end
end

test "highlights code blocks" do
higlighters = [:makeup_elixir, :makeup_erlang]
input = "<pre><code class=\"elixir\">IO.puts(\"Hello World\")</code></pre>"
output = NimblePublisher.highlight(input, higlighters)

assert output =~ "<pre><code class=\"makeup elixir\"><span class=\"nc\">IO"
end

test "highlights code blocks with custom regex" do
highlighters = [:makeup_elixir]
input = "<code lang=\"elixir\">IO.puts(\"Hello World\")</code>"

output =
NimblePublisher.highlight(
input,
highlighters,
regex: ~r/<code(?:\s+lang="(\w*)")?>([^<]*)<\/code>/
)

assert output =~ "<pre><code class=\"makeup elixir\"><span class=\"nc\">"
end
end

0 comments on commit 2a93095

Please sign in to comment.