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 3 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
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,19 @@ Each article in the articles directory must have the format:
`Makeup.stylesheet(:vim_style, "makeup")` inside `iex -S mix`.
You can replace `:vim_style` by any style of your choice
[defined here](https://elixir-makeup.github.io/makeup_demo/elixir.html).
When using a custom `:converter`, you will need to call this
manually, as shown in the [Custom markdown converter](#custom-markdown-converter)
section.

* `:earmark_options` - an [`%Earmark.Options{}`](https://hexdocs.pm/earmark/Earmark.Options.html) struct

* `: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.

* `:converter` - custom module with a `convert_body/3` function that receives the
adriansalamon marked this conversation as resolved.
Show resolved Hide resolved
body of the markdown file as a param. See [Custom markdown converter](#custom-markdown-converter)
for more details.

## Examples

Let's see a complete example. First add `nimble_publisher` with
Expand Down Expand Up @@ -193,6 +200,34 @@ 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 markdown converter

You can also define a custom markdown converter that will be used to convert the
body of the from markdown into some other format such as HTML. For example, you
may wish to use an alternative markdown parser such as
[md](https://github.com/am-kantox/md). Because the `convert_body/3` function
does not need to return HTML, if we want to use the built-in highlighting, we
need to call it manually.

```elixir
use NimblePublisher,
...
converter: MarkdownConverter,
highlighters: [:makeup_elixir]

defmodule MarkdownConverter do
def convert_body(extname, body, opts) when extname in [".md", ".markdown"] do
highlighters = Keyword.get(opts, :highlighters, [])

Md.generate(body) |> NimblePublisher.highlight(highlighters)
end
end
```

The `convert_body/3` function from this module receives an extension name,
a body 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
45 changes: 33 additions & 12 deletions lib/nimble_publisher.ex
Original file line number Diff line number Diff line change
Expand Up @@ -47,30 +47,51 @@ defmodule NimblePublisher do
{from, paths}
end

@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>/
"""
def highlight(html, highlighters, options \\ [])

def highlight(html, [], _options) do
html
end

def highlight(html, highlighters, options) when is_list(highlighters) do
for highlighter <- highlighters do
Application.ensure_all_started(highlighter)
end

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure we need this code? We already start all highlighters before, on __extract__, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if someone calls this function without having called use NimblePublisher ...?

NimblePublisher.Highlighter.highlight(html, options)
end

defp build_entry(builder, path, {_attr, _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, :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_body(extname, body, 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
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_body(extname, _body, opts) do
from = Keyword.get(opts, :from)

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

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

assert hd(@custom).body ==
"<p>This is a custom markdown converter from a .md file, from the test/fixtures/markdown.md file</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