Files
bDS2/lib/bds/post_links.ex

147 lines
4.1 KiB
Elixir

defmodule BDS.PostLinks do
@moduledoc false
import Ecto.Query
alias BDS.Posts.Link
alias BDS.Posts.Post
alias BDS.Projects
alias BDS.Repo
def sync_post_links(%Post{} = post) do
links =
post
|> post_body()
|> extract_links()
|> Enum.map(&resolve_post_link(post.project_id, &1))
|> Enum.reject(&is_nil/1)
|> Enum.uniq_by(fn %{target_post_id: target_post_id, link_text: link_text} ->
{target_post_id, link_text}
end)
Repo.transaction(fn ->
Repo.delete_all(from link in Link, where: link.source_post_id == ^post.id)
now = System.system_time(:second)
Enum.each(links, fn %{target_post_id: target_post_id, link_text: link_text} ->
%Link{}
|> Link.changeset(%{
id: Ecto.UUID.generate(),
source_post_id: post.id,
target_post_id: target_post_id,
link_text: link_text,
created_at: now
})
|> Repo.insert!()
end)
end)
:ok
end
def delete_post_links(post_id) when is_binary(post_id) do
Repo.delete_all(
from link in Link,
where: link.source_post_id == ^post_id or link.target_post_id == ^post_id
)
:ok
end
def list_outgoing_links(post_id) when is_binary(post_id) do
Repo.all(from link in Link, where: link.source_post_id == ^post_id, order_by: [asc: link.created_at])
end
def list_incoming_links(post_id) when is_binary(post_id) do
Repo.all(from link in Link, where: link.target_post_id == ^post_id, order_by: [asc: link.created_at])
end
defp post_body(%Post{content: content}) when is_binary(content), do: content
defp post_body(%Post{project_id: project_id, file_path: file_path}) do
if file_path in [nil, ""] do
""
else
project = Projects.get_project!(project_id)
full_path = Path.join(Projects.project_data_dir(project), file_path)
case File.read(full_path) do
{:ok, contents} ->
case String.split(contents, "\n---\n", parts: 2) do
[_frontmatter, body] -> String.trim_trailing(body, "\n")
_parts -> contents
end
{:error, _reason} ->
""
end
end
end
defp extract_links(body) when is_binary(body) do
markdown_links =
Regex.scan(~r/\[([^\]]+)\]\(([^)]+)\)/, body)
|> Enum.map(fn [_full, link_text, href] -> %{link_text: normalize_link_text(link_text), href: href} end)
html_links =
Regex.scan(~r/<a\s+[^>]*href=["']([^"']+)["'][^>]*>(.*?)<\/a>/is, body)
|> Enum.map(fn [_full, href, link_text] -> %{link_text: normalize_link_text(link_text), href: href} end)
markdown_links ++ html_links
end
defp resolve_post_link(project_id, %{href: href, link_text: link_text}) do
path =
href
|> to_string()
|> String.trim()
|> URI.parse()
|> Map.get(:path)
with path when is_binary(path) <- path,
slug when is_binary(slug) <- extract_slug(path),
%Post{id: target_post_id} <- Repo.get_by(Post, project_id: project_id, slug: slug) do
%{target_post_id: target_post_id, link_text: link_text}
else
_ -> nil
end
end
defp extract_slug(path) do
segments = path |> String.split("/", trim: true)
case segments do
[year, month, day, slug] ->
if numeric_year?(year) and numeric_month_or_day?(month) and numeric_month_or_day?(day),
do: slug,
else: nil
[language, year, month, day, slug] ->
if language_code?(language) and numeric_year?(year) and numeric_month_or_day?(month) and
numeric_month_or_day?(day),
do: slug,
else: nil
[slug] -> slug
[language, slug] -> if(language_code?(language), do: slug, else: nil)
_other -> nil
end
end
defp numeric_year?(value), do: String.match?(value, ~r/^\d{4}$/)
defp numeric_month_or_day?(value), do: String.match?(value, ~r/^\d{2}$/)
defp language_code?(value), do: String.match?(value, ~r/^[a-z]{2}$/)
defp normalize_link_text(value) do
value
|> to_string()
|> String.replace(~r/<[^>]+>/, "")
|> String.trim()
|> case do
"" -> nil
trimmed -> trimmed
end
end
end