Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ locals_without_parens = [
[
locals_without_parens: locals_without_parens,
import_deps: [],
line_length: 120,
plugins: [Styler],
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
export: [locals_without_parens: locals_without_parens]
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ defmodule MySite.RootLayout do
end

if Mix.env() == :dev do
c &Tableau.live_reload/1
c &TableauDevServer.live_reload/1
end
end
end
Expand Down Expand Up @@ -187,7 +187,7 @@ config :tableau, :reloader,
]
```

All you need to do is render the `Tableau.live_reload/1` component right after your `body` tag.
All you need to do is render the `TableauDevServer.live_reload/1` component right after your `body` tag.

```elixir
# lib/layouts/app.ex
Expand Down
35 changes: 4 additions & 31 deletions lib/tableau.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ defmodule Tableau do
* `:timezone` - string - Timezone to use when parsing date times. Defaults to `Etc/UTC`.
* `:base_path` - string - Development server root. Defaults to '/'.
* `:url` - string (required) - The URL of your website.
* `:converters` - mapping of file extensions to converter module. Defaults to `[md: Tableau.MDExConverter]`
* `:markdown` - keyword
* `:mdex` - keyword - Options to pass to `MDEx.to_html/2`
* `:converters` - mapping of file extensions to converter module. Defaults to `[md: Tableau.MDExExtension]`
* `:slug` - keyword - Options to pass to `Slug.slugify/2`

You'll want to check out each extension's documentation to see configuration options.

### Example

```elixir
Expand All @@ -23,39 +23,12 @@ defmodule Tableau do
url: "http://localhost:8080",
timezone: "America/Indiana/Indianapolis",
converters: [
md: Tableau.MDExConverter,
md: Tableau.MDExExtension,
dj: MySite.DjotConverter
],
slug: [
lowercase: false
],
markdown: [
mdex: [
extension: [
table: true,
header_ids: "",
tasklist: true,
strikethrough: true,
autolink: true,
alerts: true,
footnotes: true
],
render: [unsafe: true],
syntax_highlight: [formatter: {:html_inline, theme: "neovim_dark"}],
plugins: [MDExGFM]
]
]
```
"""

@doc """
Convert markdown content to HTML using `MDEx.to_html!/2`.

Will use the globally configured options, but you can also pass it overrides.
"""
def markdown(content, overrides \\ []) do
{:ok, config} = Tableau.Config.get()

MDEx.to_html!(content, Keyword.merge(config.markdown[:mdex], overrides))
end
end
3 changes: 1 addition & 2 deletions lib/tableau/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ defmodule Tableau.Config do
timezone: "Etc/UTC",
reload_log: false,
slug: [],
converters: [md: Tableau.MDExConverter],
markdown: [mdex: []]
converters: []
]

def new(config) do
Expand Down
53 changes: 46 additions & 7 deletions lib/tableau/converter.ex
Original file line number Diff line number Diff line change
@@ -1,13 +1,52 @@
defmodule Tableau.Converter do
@moduledoc """
Defines the interface for the converter module used by extensions to parse markup files.
"""
Convenience module for converting content into HTML. Useful for extension authors to convert content consistently with other
extensions without making beskope code.

By default, will convert markdown content into HTML using `Tableau.MDExExtension`.

You can configure other converters (an extension that implements the convert callback) with:

```elixir
config :tableau, :config,
converters: [
ext: ConverterModule
]
```

This will be merged in with the default of `[md: Tableau.MDExExtension]`.

@doc """
Converts content into HTML.
This can be called via an explicit converter or with an extension that will be looked up via the configuration.

Is given the file path, the frontmatter, the content of the files (sans front matter), and a list of options.
```elixir
Tableau.Converter.convert(content, converter: MyConverter)

Tableau.Converter.convert(content, ext: :md)
```

This makes it somewhat easier to swap out your converter without needing to make big application changes, but you can still
call a converter explicitly in your layout/page code. Doing so will allow you to pass configuration options to the
converter itself (assuming the implementation allows that).

```elixir
Tableau.MDExExtension.convert(content, extension: [autolink: false])
```
"""
@callback convert(filepath :: String.t(), front_matter :: map(), content :: String.t(), opts :: Keyword.t()) ::
String.t()
@default_converters [
md: Tableau.MDExExtension
]
def convert(content, opts \\ []) do
converters =
Keyword.merge(
@default_converters,
Application.get_env(:tableau, :config, [])[:converters] || []
)

with nil <- opts[:converter],
nil <- converters[opts[:ext]] do
content
else
converter -> converter.convert(content)
end
end
end
8 changes: 0 additions & 8 deletions lib/tableau/converters/mdex_converter.ex

This file was deleted.

10 changes: 9 additions & 1 deletion lib/tableau/extension.ex
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,20 @@ defmodule Tableau.Extension do
"""
@callback config(Keyword.t() | map()) :: {:ok, map()} | {:error, any()}

@doc """
Called by content generators to convert content for a specific content type.
"""
@callback convert(String.t(), any()) :: String.t()
@callback convert(String.t()) :: String.t()

@optional_callbacks [
config: 1,
pre_build: 1,
pre_render: 1,
pre_write: 1,
post_write: 1
post_write: 1,
convert: 1,
convert: 2
]

defmacro __using__(opts) do
Expand Down
17 changes: 15 additions & 2 deletions lib/tableau/extensions/common.ex
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,22 @@ defmodule Tableau.Extension.Common do
"""
def entries(paths, callback) do
for path <- paths do
{front_matter, body} = Tableau.YamlFrontMatter.parse!(File.read!(path), atoms: true)
content = File.read!(path)

{front_matter, body} =
case Tableau.YamlFrontMatter.parse(content) do
{:ok, fm, body} -> {fm, body}
{:error, _} -> {%{}, content}
end

"." <> ext = Path.extname(path)
callback.(%{path: path, ext: String.to_atom(ext), front_matter: front_matter, pre_convert_body: body})

callback.(%{
path: path,
ext: String.to_atom(ext),
front_matter: front_matter,
pre_convert_body: body
})
end
end

Expand Down
115 changes: 115 additions & 0 deletions lib/tableau/extensions/mdex_extension.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
defmodule Tableau.MDExExtension do
@moduledoc """
Converter to parse markdown content with `MDEx`.

> #### Warning {: .warning}
> [MDEx](https://github.com/leandrocp/mdex), [mdex_native](https://github.com/leandrocp/mdex_native), and [Lumis](https://github.com/leandrocp/lumis) are constantly evolving projects that change quickly. This module only relies on the `MDEx.to_html/2` function, so breaking changes are unlikely from a Tableau perspective, but if you update any of these libraries, you must check for breaking configuration changes and update your site accordingly.

Configure `MDEx` options (these are passed directly to `MDEx.to_html/2`) with:

```elixir
config :mdex_native, :syntax_highlighter, :lumis

config :tableau, Tableau.MDExExtension,
extension: [
table: true,
header_id_prefix: "",
tasklist: true,
strikethrough: true,
autolink: true,
alerts: true,
footnotes: true
],
render: [unsafe: true],
syntax_highlight: [
engine: :lumis,
opts: [
formatter: {:html_inline, theme: "neovim_dark"}
]
]
```

### Syntax Highlighting

> #### mdex_native {: .tip}
> MDEx allows you to highlight your code with [Lumis](https://github.com/leandrocp/lumis/) or [Syntect](https://docs.rs/syntect/latest/syntect/). Lumis requires you to add `:lumis` to your dependencies (Syntect does not require this).
>
> You need to configure `:mdex_native` which syntax highlighter to use.
> ```elixir
> # config/config.exs
>
> config :mdex_native, :syntax_highlighter, :lumis
> # or
> config :mdex_native, :syntax_highlighter, :syntect
> ```

#### Lumis

Lumis requires you to install the `:lumis` dependency as well as the parsers as dependencies as well. You can see the documentation for this [here](https://docs.lumis.sh/).

In addition to setting the `:mdex_native` configuration, you must also configure the `:engine` option for `:mdex`

You can find full configuration optiosn in the MDEx documentation.

```elixir
# mix.exs
defp deps() do
[
{:lumis, "~> 0.10"},
]
end

# config/config.exs
config :mdex_native, :syntax_highlighter, :lumis

config :tableau, Tableau.MDExExtension,
syntax_highlight: [
engine: :lumis,
opts: [
formatter: {:html_inline, theme: "neovim_dark"}
]
]
```

#### Syntect

In addition to setting the `:mdex_native` configuration, you must also configure the `:engine` option for `:mdex`

You can find full configuration optiosn in the MDEx documentation.

```elixir
# config/config.exs
config :mdex_native, :syntax_highlighter, :syntect

config :tableau, Tableau.MDExExtension,
syntax_highlight: [
engine: :syntect,
opts: [
theme: "Catppuccin Macchiato"
]
]
```
"""

use Tableau.Extension, key: :mdex

@impl true
def convert(body, config_overrides \\ []) do
if Code.ensure_loaded?(MDEx) do
config =
:tableau
|> Application.get_env(__MODULE__, [])
|> Keyword.merge(config_overrides)

MDEx.to_html!(body, config)
else
raise """
MDEx is not installed!

You'll need to add `:mdex` as a dependency in mix.exs.

`{:mdex, "~> 0.14"}`
"""
end
end
end
Loading
Loading