Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for custom markdown parsers #28

Merged
merged 7 commits into from
Jul 18, 2023
Merged
Show file tree
Hide file tree
Changes from 6 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
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
43 changes: 28 additions & 15 deletions lib/nimble_publisher.ex
Original file line number Diff line number Diff line change
Expand Up @@ -47,30 +47,39 @@ 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.
josevalim marked this conversation as resolved.
Show resolved Hide resolved

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)
adriansalamon marked this conversation as resolved.
Show resolved Hide resolved
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 +124,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
Loading