include internal link rendering

This commit is contained in:
Inhji 2023-07-04 22:46:00 +02:00
parent b2e1b3ebc5
commit a777c9a8b3
3 changed files with 171 additions and 7 deletions

View file

@ -1,13 +1,63 @@
defmodule ChiyaWeb.Markdown do
@options [
footnotes: true,
breaks: true,
escape: true,
wikilinks: true
]
@moduledoc """
Module used for rendering markdown.
Rendering also collects and replaces internal [references](`Mirage.References`) to other entities like Lists and Tags.
"""
import Ecto.Changeset, only: [get_change: 2, put_change: 3]
defp options() do
%Earmark.Options{
code_class_prefix: "lang- language-",
footnotes: true,
breaks: true,
escape: false,
registered_processors: processors()
}
end
@doc """
Renders markdown with Earmark.
"""
def render(markdown) do
markdown
|> Earmark.as_html!(@options)
|> ChiyaWeb.References.replace_references()
|> Earmark.as_html!(options())
end
@doc """
Renders markdown to HTML inside a changeset if markdown changed.
Markdown field and HTML field can be configured.
"""
def maybe_render(changeset, markdown_field, html_field) do
if markdown = get_change(changeset, markdown_field) do
html = render(markdown)
put_change(changeset, html_field, html)
else
changeset
end
end
def processors() do
heading = fn
{_, _, [content], _} = n when is_binary(content) ->
slug = Slugger.slugify_downcase(content)
Earmark.AstTools.merge_atts_in_node(n, id: slug)
node ->
node
end
[
Earmark.TagSpecificProcessors.new([
{"h1", heading},
{"h2", heading},
{"h3", heading},
{"h4", heading},
{"h5", heading},
{"h6", heading}
])
]
end
end

View file

@ -0,0 +1,89 @@
defmodule ChiyaWeb.References do
@moduledoc """
Finds and replaces references to entities in a string.
A reference is an internal link like the following examples:
* `[[sustainablity]]` -> A note named *Sustainability*
* `[[a-long-unfitting-slug|A simple title]]` -> A note named *A long unfitting title*
"""
@reference_regex ~r/\[\[?(?<id>[\d\w-]+)(?:\|(?<title>[\w\d\s'`äöüß]+))?\]\]/
@doc """
Returns a list of references in `string`.
"""
def get_references(nil), do: []
def get_references(string) do
@reference_regex
|> Regex.scan(string, capture: :all)
|> Enum.map(&map_to_tuple/1)
end
@doc """
Checks each reference returned from `get_references/1` and validates its existence.
"""
def validate_references(references) do
Enum.map(references, fn {placeholder, slug, custom_title} ->
note = Chiya.Notes.get_note_by_slug_preloaded(slug)
valid =
case note do
nil -> false
_ -> true
end
# If a custom title was defined, use it,
# otherwise use the note's title
title =
cond do
custom_title != slug ->
custom_title
valid ->
note.name
true ->
slug
end
{placeholder, slug, title, valid}
end)
end
@doc """
Returns a list of slugs that are referenced in `string`, optionally filtering by `filter_type`.
"""
def get_reference_ids(string) do
string
|> get_references()
|> Enum.map(fn {_, note_slug, _} -> note_slug end)
end
@doc """
Finds and replaces references with the matching url in `string`.
"""
def replace_references(string) do
string
|> get_references()
|> validate_references()
|> Enum.reduce(string, fn {placeholder, slug, title, valid}, s ->
String.replace(s, placeholder, get_link(slug, title, valid))
end)
end
defp get_link(slug, title, valid) do
"[#{title}](/note/#{slug})#{get_link_class(valid)}"
end
defp get_link_class(valid) do
if valid, do: "{:.invalid}", else: ""
end
defp map_to_tuple([placeholder, note_slug]),
do: {placeholder, note_slug, note_slug}
defp map_to_tuple([placeholder, note_slug, title]),
do: {placeholder, note_slug, title}
end

View file

@ -0,0 +1,25 @@
defmodule ChiyaWeb.MarkdownTest do
use Chiya.DataCase
alias ChiyaWeb.Markdown
describe "render/1" do
test "renders simple markdown" do
html = Markdown.render("# Title")
assert html =~ "id=\"title\""
assert html =~ "Title"
assert html =~ "</h1>"
end
test "renders a link to a note" do
html = Markdown.render("[[foo]]")
assert html =~ "/note/foo"
end
test "renders a link to a note with custom title" do
html = Markdown.render("[[foo|MyFoo]]")
assert html =~ "/note/foo"
assert html =~ "MyFoo"
end
end
end