# `NimblePublisher`
[🔗](https://github.com/dashbitco/nimble_publisher/blob/v2.1.0/lib/nimble_publisher.ex#L1)

`NimblePublisher` is a minimal filesystem-based publishing engine with Markdown support and
code highlighting.

```elixir
use NimblePublisher,
  build: Article,
  from: Application.app_dir(:app_name, "priv/articles/**/*.md"),
  as: :articles,
  highlighters: [:makeup_elixir, :makeup_erlang]
```

The example above will get all articles in the given directory,
call `Article.build/3` for each article, passing the filename,
the metadata, and the article body, and define a module attribute
named `@articles` with all built articles returned by the
`Article.build/3` function.

Each article in the articles directory must have the format:

    %{
      title: "Hello world"
    }
    ---
    Body of the "Hello world" article.

    This is a *markdown* document with support for code highlighters:

    ```elixir
    IO.puts "hello world"
    ```

## Options

  * `:build` - the name of the module that will build each entry

  * `:from` - a wildcard pattern where to find all entries. Files with the
    `.md` or `.markdown` extension will be converted to HTML with
    `Earmark`. Other files will be kept as is.

  * `:as` - the name of the module attribute to store all built entries

  * `:comrak_options` - a keyword list of options accepted by
    [`MDExNative.Comrak.markdown_to_html`](https://mdex-native.hexdocs.pm/MDExNative.Comrak.html#markdown_to_html/2)
    to customize Markdown rendering. The default options enable GitHub
    Flavored Markdown and assume you are rendering trusted content,
    as `NimblePublisher` is designed to render your own articles:

    ```elixir
    [
      extension: [table: true, autolink: true, strikethrough: true],
      render: [hardbreaks: false, unsafe: true]
    ]
    ```

  * `:highlighters` - which `Makeup` syntax highlighters to use.
    See "Syntax highlighting" section below for more information.

  * `:parser` - custom module with a `parse/2` function that receives the file path
    and content as params. See [Custom parser](#module-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](#module-custom-html-converter) for more details.

## Examples

Let's see a complete example. First add `nimble_publisher` with
the desired highlighters as a dependency:

    def deps do
      [
        {:nimble_publisher, "~> 1.0", runtime: false}
      ]
    end

In this example, we are building a blog. Each post stays in the
"posts" directory with the format:

    /posts/YEAR/MONTH-DAY-ID.md

A typical blog post will look like this:

    # /posts/2020/04-17-hello-world.md
    %{
      title: "Hello world!",
      author: "José Valim",
      tags: ~w(hello),
      description: "Let's learn how to say hello world"
    }
    ---
    This is the post.

Therefore, we will define a Post struct that expects all of the fields
above. We will also have a `:date` field that we will build from the
filename. Overall, it will look like this:

```elixir
defmodule MyApp.Blog.Post do
  @enforce_keys [:id, :author, :title, :body, :description, :tags, :date]
  defstruct [:id, :author, :title, :body, :description, :tags, :date]

  def build(filename, attrs, body) do
    [year, month_day_id] = filename |> Path.rootname() |> Path.split() |> Enum.take(-2)
    [month, day, id] = String.split(month_day_id, "-", parts: 3)
    date = Date.from_iso8601!("#{year}-#{month}-#{day}")
    struct!(__MODULE__, [id: id, date: date, body: body] ++ Map.to_list(attrs))
  end
end
```

Now, we are ready to define our `MyApp.Blog` with `NimblePublisher`:

```elixir
defmodule MyApp.Blog do
  alias MyApp.Blog.Post

  use NimblePublisher,
    build: Post,
    from: Application.app_dir(:my_app, "priv/posts/**/*.md"),
    as: :posts

  # The @posts variable is first defined by NimblePublisher.
  # Let's further modify it by sorting all posts by descending date.
  @posts Enum.sort_by(@posts, & &1.date, {:desc, Date})

  # Let's also get all tags
  @tags @posts |> Enum.flat_map(& &1.tags) |> Enum.uniq() |> Enum.sort()

  # And finally export them
  def all_posts, do: @posts
  def all_tags, do: @tags
end
```

**Important**: Avoid injecting the `@posts` attribute into multiple functions,
as each call will make a complete copy of all posts. For example, if you want
to define `recent_posts()` as well as `all_posts()`, DO NOT do this:

```elixir
def all_posts, do: @posts
def recent_posts, do: Enum.take(@posts, 3)
```

Instead do this:

```elixir
def all_posts, do: @posts
def recent_posts, do: Enum.take(all_posts(), 3)
```

### Other helpers

You may want to define other helpers to traverse your published resources.
For example, if you want to get posts by ID or with a given tag, you can
define additional functions as shown below:

```elixir
defmodule NotFoundError do
  defexception [:message, plug_status: 404]
end

def get_post_by_id!(id) do
  Enum.find(all_posts(), &(&1.id == id)) ||
    raise NotFoundError, "post with id=#{id} not found"
end

def get_posts_by_tag!(tag) do
  case Enum.filter(all_posts(), &(tag in &1.tags)) do
    [] -> raise NotFoundError, "posts with tag=#{tag} not found"
    posts -> posts
  end
end
```

## Syntax highlighting

You have three options for Syntax Highlighting with NimblePublisher.

### Makeup

[Makeup](https://github.com/elixir-makeup/makeup) is a pure-Elixir
syntax highlighter. To enable it, first add the desired language
highlighters to your `mix.exs` `deps`:

```elixir
{:makeup_elixir, ">= 0.0.0", runtime: false},
{:makeup_erlang, ">= 0.0.0", runtime: false}
```

Then pass the apps above under the :highlighters option when calling
`use NimblePublisher`:

```elixir
use NimblePublisher,
  build: Post,
  highlighters: [:makeup_elixir, :makeup_erlang]
```

Makeup supports [several themes](https://elixir-makeup.github.io/makeup_demo/elixir.html).
Once you pick your favorite, start `iex -S mix`, and run `Makeup.stylesheet(:vim_style, "makeup")`
with the name of your theme as first argument. Then inject the outputted CSS as part of
your website CSS.

### Syntect

Syntect is a Rust syntax highlighter that is optionally embedded as part of `MDExNative`
and uses TextMate-style syntax definitions. It is weights approximately 3MB.

To use it, first add `config :mdex_native, syntax_highlighter: :syntect` to
your `config/config.exs`. Then configure `NimblePublisher` to use it:

```elixir
  use NimblePublisher,
    build: Blog.Post,
    comrak_options: [
      syntax_highlight: [
        engine: :syntect,
        opts: [theme: "TwoDark"]
      ]
    ]
```

See [MDExNative docs](https://mdex-native.hexdocs.pm/syntax_highlighting.html) for
more information.

### Lumis

Lumis is a Rust syntax highlighter that is optionally embedded as part of `MDExNative`
and uses TreeSitter syntax definitions. It is weights approximately 15MB.

To use it, first add `config :mdex_native, syntax_highlighter: :lumis` to
your `config/config.exs`. Then configure `NimblePublisher` to use it:

```elixir
  use NimblePublisher,
    build: Blog.Post,
    comrak_options: [
      syntax_highlight: [
        engine: :lumis,
        opts: [
          formatter: {:html_inline, theme: "catppuccin_macchiato"}
        ]
      ]
    ]
```

See [MDExNative docs](https://mdex-native.hexdocs.pm/syntax_highlighting.html) for
more information.

## Customization

### Custom parser

You may want to define a custom function to parse the content of your files.

```elixir
  use NimblePublisher,
    ...
    parser: Parser,

defmodule Parser do
  def parse(path, contents) do
    [attrs, body] = :binary.split(contents, ["\n---\n"])
    {Jason.decode!(attrs), body}
  end
end
```

The `parse/2` function from this module receives the file path and content as params.
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 [MDex](https://mdex.hexdocs.pm):

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

```elixir
defmodule MarkdownConverter do
  def convert(filepath, body, _attrs, opts) do
    if Path.extname(filepath) in ~w(.markdown .md .livemd) do
      # MDEx has its own syntax highlighter. If your markdown converter
      # does not have one or you want to use NimblePublisher's built-in,
      # call `NimblePublisher.highlight(html)` after conversion.
      MDEx.to_html!(body)
    end
  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:

```elixir
live_reload: [
  patterns: [
    ...,
    ~r"posts/*/.*(md)$"
  ]
]
```

## Learn more

  * [Dashbit's blog post which was the foundation for NimblePublisher](https://dashbit.co/blog/welcome-to-our-blog-how-it-was-made)
  * [Elixir School's lesson on using NimblePublisher, complete with Phoenix integration](https://elixirschool.com/en/lessons/libraries/nimble-publisher/)

# `highlight`

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(?: +class="([^" ]*)")?>([^<]*)</code></pre>/

---

*Consult [api-reference.md](api-reference.md) for complete listing*
