3 Commits

  1. 11
      CHANGELOG.md
  2. 2
      lib/mirage/links.ex
  3. 119
      lib/mirage/markdown.ex
  4. 29
      lib/mirage/notes.ex
  5. 2
      mix.exs

11
CHANGELOG.md

@ -5,6 +5,17 @@ See [Conventional Commits](Https://conventionalcommits.org) for commit guideline
<!-- changelog -->
## [v0.99.0](https://git.inhji.de/inhji/mirage/compare/v0.98.0...v0.99.0) (2021-03-29)
### Features:
* rework note linking, add linking to links
* add get_link/1
## [v0.98.0](https://git.inhji.de/inhji/mirage/compare/v0.97.0...v0.98.0) (2021-03-29)

2
lib/mirage/links.ex

@ -38,6 +38,8 @@ defmodule Mirage.Links do
"""
def get_link!(id), do: Repo.get!(Link, id)
def get_link(id), do: Repo.get(Link, id)
def get_link_by_url!(url) do
Repo.get_by!(Link, url: url)
end

119
lib/mirage/markdown.ex

@ -1,65 +1,138 @@
defmodule Mirage.Markdown do
@moduledoc """
This module handles markdown rendering with earmark with a few options like footnotes and integration with prism.js.
Additionally it provides functions to find references to notes and links inside the markdown.
## References
To reference a note, use the syntax `[[1]]` where 1 is the note-id.
If you want to supply a different title to the generated link, use the following syntax: `[[1|some title]]`
To reference a link, use the syntax `{{1}}` where 1 is the link-id.
If you want to supply a different title to the generated link, use the following syntax: `{{1|some title}}`
"""
import Ecto.Changeset, only: [get_change: 2, put_change: 3]
@crossref_regex ~r/\[\[(?<id>\d+)(?:\|([\w\d\s]+))?\]\]/
@note_reference_regex ~r/\[\[(?<id>\d+)(?:\|([\w\d\s]+))?\]\]/
@link_reference_regex ~r/{{(?<id>\d+)(?:\|([\w\d\s]+))?}}/
@doc """
Renders markdown inside a changeset. Markdown field and HTML field can be configured.
"""
def maybe_render_markdown(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
@doc """
Renders markdown with Earmark, also finds and replaces to links and notes.
"""
def render(markdown) do
options = %Earmark.Options{
code_class_prefix: "lang- language-",
footnotes: true
}
markdown
|> get_references()
|> Enum.reduce(markdown, &replace_with_link/2)
|> Earmark.as_html!(options)
markdown =
markdown
|> get_references(@note_reference_regex)
|> Enum.reduce(markdown, &replace_with_link_to_note/2)
markdown =
markdown
|> get_references(@link_reference_regex)
|> Enum.reduce(markdown, &replace_with_link_to_link/2)
Earmark.as_html!(markdown, options)
end
@doc """
Returns all note references in a given markdown string.
## Example
iex> get_note_references(markdown)
[["1", "some alternative title"], ["5"]]
"""
def get_note_references(markdown) do
get_references(markdown, @note_reference_regex)
end
@doc """
Returns all link references in a given markdown string.
## Example
iex> get_link_references(markdown)
[["1", "some alternative title"], ["5"]]
"""
def get_link_references(markdown) do
get_references(markdown, @link_reference_regex)
end
def get_references(markdown) when is_binary(markdown) do
@crossref_regex
|> Regex.scan(markdown, capture: :all_but_first)
defp get_references(markdown, regex) when is_binary(markdown) do
regex |> Regex.scan(markdown, capture: :all_but_first)
end
def get_references(%{content: markdown}) do
defp get_references(%{content: markdown}, regex) do
markdown
|> get_references()
|> Enum.map(&first_item/1)
|> get_references(regex)
|> Enum.map(&List.first/1)
end
defp first_item([first, _]), do: first
defp first_item([first]), do: first
defp replace_with_link_to_link([link_id, title], markdown) do
maybe_replace_link(markdown, link_id, title)
end
defp replace_with_link([note_id, title], markdown) do
case Mirage.Notes.get_note(note_id) do
defp replace_with_link_to_link([link_id], markdown) do
maybe_replace_link(markdown, link_id)
end
defp maybe_replace_link(markdown, link_id, title \\ nil) do
case Mirage.Links.get_link(link_id) do
nil ->
markdown
note ->
Regex.replace(@crossref_regex, markdown, get_link(note.id, title), global: false)
link ->
Regex.replace(
@link_reference_regex,
markdown,
get_link_link(link_id, title || link.title || link.url),
global: false
)
end
end
defp replace_with_link([note_id], markdown) do
defp replace_with_link_to_note([note_id, title], markdown) do
maybe_replace_note(markdown, note_id, title)
end
defp replace_with_link_to_note([note_id], markdown) do
maybe_replace_note(markdown, note_id)
end
defp maybe_replace_note(markdown, note_id, title \\ nil) do
case Mirage.Notes.get_note(note_id) do
nil ->
markdown
note ->
String.replace(markdown, "[[#{note_id}]]", get_link(note.id, note.title))
Regex.replace(
@note_reference_regex,
markdown,
get_note_link(note_id, title || note.title),
global: false
)
end
end
defp get_link(id, title) do
"[#{title}](/notes/#{id})"
end
defp get_note_link(id, title), do: "[#{title}](/notes/#{id})"
defp get_link_link(id, title), do: "[#{title}](/links/#{id})"
end

29
lib/mirage/notes.ex

@ -262,24 +262,39 @@ defmodule Mirage.Notes do
"""
def link_note(%Note{} = note) do
multi = Ecto.Multi.new()
query = from(n in NoteNote, where: n.source_id == ^note.id)
note_query = from(n in NoteNote, where: n.source_id == ^note.id)
link_query = from(n in NoteLink, where: n.note_id == ^note.id)
refs =
note_refs =
note
|> Mirage.Markdown.get_references()
|> Mirage.Markdown.get_note_references()
|> Enum.map(&String.to_integer/1)
link_refs =
note
|> Mirage.Markdown.get_link_references()
|> Enum.map(&String.to_integer/1)
# Delete all old references for this note
multi =
multi
|> Ecto.Multi.delete_all(:delete_links, query)
|> Ecto.Multi.delete_all(:delete_note_links, note_query)
|> Ecto.Multi.delete_all(:delete_link_links, link_query)
# Create a link to all referenced notes
multi =
refs
note_refs
|> Enum.map(fn ref -> %NoteNote{source_id: note.id, target_id: ref} end)
|> Enum.reduce(multi, fn link_struct, multi ->
Ecto.Multi.insert(multi, {:link, link_struct.target_id}, link_struct)
|> Enum.reduce(multi, fn note_note, multi ->
Ecto.Multi.insert(multi, {:insert_note_link, note_note.target_id}, note_note)
end)
# Create a link to all references links
multi =
link_refs
|> Enum.map(fn ref -> %NoteLink{note_id: note.id, link_id: ref} end)
|> Enum.reduce(multi, fn note_link, multi ->
Ecto.Multi.insert(multi, {:insert_link_link, note_link.link_id}, note_link)
end)
Repo.transaction(multi)

2
mix.exs

@ -1,7 +1,7 @@
defmodule Mirage.MixProject do
use Mix.Project
@version "0.98.0"
@version "0.99.0"
def project do
[

Loading…
Cancel
Save