defmodule Wiki.Content.Renderer do alias Wiki.Repo alias Wiki.Content.Page alias WikiWeb.Router.Helpers, as: Routes alias WikiWeb.Endpoint import Ecto.Query @spec render(String.t(), integer()) :: {String.t(), [integer()]} def render(content, user_id) do {content, linked_pages} = replace_page_links(content, user_id) { render_markdown(content), Enum.uniq(linked_pages) } end @page_link_regex ~r/\[\[.*?\]\]/ @spec replace_page_links(content :: String.t(), user_id :: integer()) :: {String.t(), [integer()]} defp replace_page_links(content, user_id) do results = Regex.scan(@page_link_regex, content, return: :index) results |> Enum.reverse() |> Enum.reduce({content, []}, fn [{start, length}], {str, acc} -> # can't use String.slice here, because the indicies produced by Regex.scan are character indices, and slice operates on graphemes match = :erlang.binary_part(str, start, length) title = String.slice(match, 2..-3) lower_title = String.downcase(title) str_before = :erlang.binary_part(str, 0, start) str_after = :erlang.binary_part(str, start + length, byte_size(str) - start - length) {path, acc} = from(p in Page) |> where([p], p.user_id == ^user_id) |> where([p], fragment("lower(?)", p.title) == ^lower_title) |> select([p], p.id) |> Repo.one() |> case do nil -> { Routes.page_path(Endpoint, :new, title: title), acc } linked_page_id -> { Routes.page_path(Endpoint, :show, linked_page_id), [linked_page_id | acc] } end page_link = "[#{title}](#{path})" { str_before <> page_link <> str_after, acc } end) end @spec render_markdown(content :: String.t()) :: String.t() defp render_markdown(content) do {:ok, html, _errors} = Earmark.as_html(content) html end end