add backlinks
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing

This commit is contained in:
2025-02-15 06:00:37 +00:00
parent 6c2aba84ef
commit 0c5442f0cd
25 changed files with 502 additions and 147 deletions

View File

@ -88,6 +88,42 @@ defmodule Memex.Contexts do
)
end
@doc """
Returns the list of contexts that link to a particular slug.
## Examples
iex> backlink(%User{id: 123})
[%Context{}, ...]
iex> backlink("[other-context]", %User{id: 123})
[%Context{content: "[other-context]"}, ...]
"""
@spec backlink(String.t(), User.t()) :: [Context.t()]
def backlink(link, %{id: user_id}) when user_id |> is_binary() do
link = link |> String.replace("[", "\\[") |> String.replace("]", "\\]")
link_regex = "(^|[^\[])#{link}($|[^\]])"
Repo.all(
from c in Context,
where: fragment("? ~ ?", c.content, ^link_regex),
order_by: c.slug
)
end
def backlink(link, _invalid_user) do
link = link |> String.replace("[", "\\[") |> String.replace("]", "\\]")
link_regex = "(^|[^\[])#{link}($|[^\]])"
Repo.all(
from c in Context,
where: fragment("? ~ ?", c.content, ^link_regex),
where: c.visibility == :public,
order_by: c.slug
)
end
@doc """
Gets a single context.

View File

@ -86,6 +86,42 @@ defmodule Memex.Notes do
)
end
@doc """
Returns the list of notes that link to a particular slug.
## Examples
iex> backlink(%User{id: 123})
[%Note{}, ...]
iex> backlink("[other-note]", %User{id: 123})
[%Note{content: "[other-note]"}, ...]
"""
@spec backlink(String.t(), User.t()) :: [Note.t()]
def backlink(link, %{id: user_id}) when user_id |> is_binary() do
link = link |> String.replace("[", "\\[") |> String.replace("]", "\\]")
link_regex = "(^|[^\[])#{link}($|[^\]])"
Repo.all(
from n in Note,
where: fragment("? ~ ?", n.content, ^link_regex),
order_by: n.slug
)
end
def backlink(link, _invalid_user) do
link = link |> String.replace("[", "\\[") |> String.replace("]", "\\]")
link_regex = "(^|[^\[])#{link}($|[^\]])"
Repo.all(
from n in Note,
where: fragment("? ~ ?", n.content, ^link_regex),
where: n.visibility == :public,
order_by: n.slug
)
end
@doc """
Gets a single note.

View File

@ -142,6 +142,50 @@ defmodule Memex.Pipelines do
)
end
@doc """
Returns the list of pipelines that link to a particular slug.
## Examples
iex> backlink(%User{id: 123})
[%Pipeline{}, ...]
iex> backlink("[other-pipeline]", %User{id: 123})
[%Pipeline{description: "[other-pipeline]"}, ...]
"""
@spec backlink(String.t(), User.t()) :: [Pipeline.t()]
def backlink(link, %{id: user_id}) when user_id |> is_binary() do
link = link |> String.replace("[", "\\[") |> String.replace("]", "\\]")
link_regex = "(^|[^\[])#{link}($|[^\]])"
Repo.all(
from p in Pipeline,
left_join: s in assoc(p, :steps),
where:
fragment("? ~ ?", p.description, ^link_regex) or
fragment("? ~ ?", s.content, ^link_regex),
distinct: true,
order_by: p.slug
)
end
def backlink(link, _invalid_user) do
link = link |> String.replace("[", "\\[") |> String.replace("]", "\\]")
link_regex = "(^|[^\[])#{link}($|[^\]])"
Repo.all(
from p in Pipeline,
left_join: s in assoc(p, :steps),
where:
fragment("? ~ ?", p.description, ^link_regex) or
fragment("? ~ ?", s.content, ^link_regex),
where: p.visibility == :public,
distinct: true,
order_by: p.slug
)
end
@doc """
Creates a pipeline.

View File

@ -1,6 +1,6 @@
defmodule MemexWeb.ContextLive.Show do
use MemexWeb, :live_view
alias Memex.Contexts
alias Memex.{Contexts, Pipelines}
@impl true
def mount(_params, _session, socket) do
@ -23,7 +23,9 @@ defmodule MemexWeb.ContextLive.Show do
socket
|> assign(
context: context,
page_title: page_title(live_action, context)
page_title: page_title(live_action, context),
context_backlinks: Contexts.backlink("[#{context.slug}]", current_user),
pipeline_backlinks: Pipelines.backlink("[[#{context.slug}]]", current_user)
)
{:noreply, socket}

View File

@ -11,6 +11,27 @@
<.context_content context={@context} />
<div
:if={@context_backlinks ++ @pipeline_backlinks != []}
class="flex flex-wrap justify-end items-center self-end"
>
<p><%= gettext("Backlinked by:") %></p>
<.link
:for={backlink <- @context_backlinks}
class="m-1 hover:underline"
patch={~p"/context/#{backlink}"}
>
<%= gettext("[%{slug}]", slug: backlink.slug) %>
</.link>
<.link
:for={backlink <- @pipeline_backlinks}
class="m-1 hover:underline"
patch={~p"/pipeline/#{backlink}"}
>
<%= gettext("[[%{slug}]]", slug: backlink.slug) %>
</.link>
</div>
<p class="self-end">
<%= gettext("Visibility: %{visibility}", visibility: @context.visibility) %>
</p>

View File

@ -1,5 +1,5 @@
<div class="mx-auto flex flex-col justify-center items-stretch space-y-4 max-w-lg">
<h1 class="title text-primary-400 text-xl">
<div class="flex flex-col justify-center items-stretch mx-auto space-y-4 max-w-lg">
<h1 class="text-xl title text-primary-400">
<%= gettext("memEx") %>
</h1>
@ -31,7 +31,7 @@
</p>
</li>
<li class="flex flex-col justify-center items-center text-right space-y-2">
<li class="flex flex-col justify-center items-center space-y-2 text-right">
<.link navigate={~p"/faq"} class="btn btn-primary">
<%= gettext("read more on how to use memEx") %>
</.link>
@ -41,7 +41,7 @@
<hr class="hr" />
<ul class="flex flex-col space-y-4">
<h2 class="title text-primary-400 text-lg">
<h2 class="text-lg title text-primary-400">
<%= gettext("features") %>
</h2>
@ -71,12 +71,21 @@
<%= gettext("accessible from any internet-capable device") %>
</p>
</li>
<li class="flex flex-col justify-center items-center space-y-2">
<b class="whitespace-nowrap">
<%= gettext("backlinks:") %>
</b>
<p>
<%= gettext("view referencing items from the referenced item") %>
</p>
</li>
</ul>
<hr class="hr" />
<ul class="flex flex-col justify-center space-y-4">
<h2 class="title text-primary-400 text-lg">
<h2 class="text-lg title text-primary-400">
<%= gettext("instance information") %>
</h2>
@ -124,7 +133,7 @@
<hr class="hr" />
<ul class="flex flex-col space-y-2">
<h2 class="title text-primary-400 text-lg">
<h2 class="text-lg title text-primary-400">
<%= gettext("get involved") %>
</h2>

View File

@ -1,6 +1,6 @@
defmodule MemexWeb.NoteLive.Show do
use MemexWeb, :live_view
alias Memex.Notes
alias Memex.{Contexts, Notes, Pipelines}
@impl true
def mount(_params, _session, socket) do
@ -22,8 +22,11 @@ defmodule MemexWeb.NoteLive.Show do
socket =
socket
|> assign(
context_backlinks: Contexts.backlink("[[#{note.slug}]]", current_user),
note_backlinks: Notes.backlink("[#{note.slug}]", current_user),
note: note,
page_title: page_title(live_action, note)
page_title: page_title(live_action, note),
pipeline_backlinks: Pipelines.backlink("[[[#{note.slug}]]]", current_user)
)
{:noreply, socket}

View File

@ -11,6 +11,34 @@
<.note_content note={@note} />
<div
:if={@note_backlinks ++ @context_backlinks ++ @pipeline_backlinks != []}
class="flex flex-wrap justify-end items-center self-end"
>
<p><%= gettext("Backlinked by:") %></p>
<.link
:for={backlink <- @note_backlinks}
class="m-1 hover:underline"
patch={~p"/note/#{backlink}"}
>
<%= gettext("[%{slug}]", slug: backlink.slug) %>
</.link>
<.link
:for={backlink <- @context_backlinks}
class="m-1 hover:underline"
patch={~p"/context/#{backlink}"}
>
<%= gettext("[[%{slug}]]", slug: backlink.slug) %>
</.link>
<.link
:for={backlink <- @pipeline_backlinks}
class="m-1 hover:underline"
patch={~p"/pipeline/#{backlink}"}
>
<%= gettext("[[[%{slug}]]]", slug: backlink.slug) %>
</.link>
</div>
<p class="self-end">
<%= gettext("Visibility: %{visibility}", visibility: @note.visibility) %>
</p>

View File

@ -23,6 +23,7 @@ defmodule MemexWeb.PipelineLive.Show do
socket
|> assign(
page_title: page_title(live_action, pipeline),
pipeline_backlinks: Pipelines.backlink("[#{pipeline.slug}]", current_user),
pipeline: pipeline,
steps: pipeline |> Steps.list_steps(current_user)
)

View File

@ -11,6 +11,17 @@
<.pipeline_content pipeline={@pipeline} />
<div :if={@pipeline_backlinks != []} class="flex flex-wrap justify-end items-center self-end">
<p><%= gettext("Backlinked by:") %></p>
<.link
:for={backlink <- @pipeline_backlinks}
class="m-1 hover:underline"
patch={~p"/pipeline/#{backlink}"}
>
<%= gettext("[%{slug}]", slug: backlink.slug) %>
</.link>
</div>
<p class="self-end">
<%= gettext("Visibility: %{visibility}", visibility: @pipeline.visibility) %>
</p>