Compare commits

...

2 Commits

  1. 9
      CHANGELOG.md
  2. 20
      assets/css/app.scss
  3. 31
      lib/mirage/markdown.ex
  4. 50
      lib/mirage/notes.ex
  5. 6
      lib/mirage/notes/note.ex
  6. 19
      lib/mirage/notes/note_note.ex
  7. 28
      lib/mirage/notes/tags.ex
  8. 3
      lib/mirage_web/controllers/note_controller.ex
  9. 8
      lib/mirage_web/templates/note/show.html.eex
  10. 2
      mix.exs
  11. 13
      priv/repo/migrations/20210209183415_create_note_note.exs
  12. 6
      test/mirage/notes_test.exs
  13. 14
      test/mirage_web/controllers/note_controller_test.exs

9
CHANGELOG.md

@ -5,6 +5,15 @@ See [Conventional Commits](Https://conventionalcommits.org) for commit guideline
<!-- changelog -->
## [v0.19.0](https://git.inhji.de/inhji/mirage/compare/v0.18.1...v0.19.0) (2021-02-09)
### Features:
* backlinks
## [v0.18.1](https://git.inhji.de/inhji/mirage/compare/v0.18.0...v0.18.1) (2021-02-08)

20
assets/css/app.scss

@ -94,7 +94,8 @@ footer {
padding: 2rem;
}
section:not(.hero) + section {
section:not(.hero) + section,
div + section {
margin-top: 2rem;
}
@ -219,6 +220,23 @@ kbd {
}
}
.backlinks {
h4 {
margin-bottom: 1rem;
}
.backlink {
display: block;
padding: 1rem;
border: 1px solid $border-base;
margin-bottom: 1rem;
text-decoration: none;
}
.backlink:last-child {
margin-bottom: 0;
}
}
/* === Forms === */

31
lib/mirage/markdown.ex

@ -1,6 +1,8 @@
defmodule Mirage.Markdown do
import Ecto.Changeset, only: [get_change: 2, put_change: 3]
@crossref_regex ~r/\[\[(?<id>\d+)(?:\|([\w\d\s]+))?\]\]/
def maybe_render_markdown(changeset, markdown_field, html_field) do
if markdown = get_change(changeset, markdown_field) do
html = render(markdown)
@ -11,13 +13,6 @@ defmodule Mirage.Markdown do
end
end
@crossref_regex ~r/\[\[(?<id>\d+)(?:\|([\w\d\s]+))?\]\]/
def get_references(markdown) do
@crossref_regex
|> Regex.scan(markdown, capture: :all_but_first)
end
def render(markdown) do
options = %Earmark.Options{
code_class_prefix: "lang- language-"
@ -29,7 +24,21 @@ defmodule Mirage.Markdown do
|> Earmark.as_html!(options)
end
def replace_with_link([note_id, title], markdown) do
def get_references(markdown) when is_binary(markdown) do
@crossref_regex
|> Regex.scan(markdown, capture: :all_but_first)
end
def get_references(%{content: markdown}) do
markdown
|> get_references()
|> Enum.map(&first_item/1)
end
defp first_item([first, _]), do: first
defp first_item([first]), do: first
defp replace_with_link([note_id, title], markdown) do
case Mirage.Notes.get_note(note_id) do
nil ->
markdown
@ -39,7 +48,7 @@ defmodule Mirage.Markdown do
end
end
def replace_with_link([note_id], markdown) do
defp replace_with_link([note_id], markdown) do
case Mirage.Notes.get_note(note_id) do
nil ->
markdown
@ -49,7 +58,7 @@ defmodule Mirage.Markdown do
end
end
def get_link(id, title) do
"[#{title}](/notes/#{id})"
defp get_link(id, title) do
"[#{title}](/notes/#{id})"
end
end

50
lib/mirage/notes.ex

@ -6,9 +6,9 @@ defmodule Mirage.Notes do
import Ecto.Query, warn: false
alias Mirage.Repo
alias Mirage.Notes.{Note, Topic}
alias Mirage.Notes.{Note, Topic, NoteNote}
@note_preloads [:links, :topics]
@note_preloads [:links, :topics, :backlinks]
@topic_preloads [:notes]
@doc """
@ -84,6 +84,13 @@ defmodule Mirage.Notes do
|> Repo.update()
end
def update_note!(%Note{} = note, attrs) do
note
|> Note.changeset(attrs)
|> Repo.update!()
|> preload_note()
end
@doc """
Deletes a note.
@ -113,6 +120,45 @@ defmodule Mirage.Notes do
Note.changeset(note, attrs)
end
@doc """
Links a note to all references notes in the content-field
## Examples
A content field with the following content:
```
This is a link to [[42|some other note]]
```
will link this note to the note with id 42.
"""
def link_note(%Note{} = note) do
multi = Ecto.Multi.new()
query = from(n in NoteNote, where: n.source_id == ^note.id)
refs =
note
|> Mirage.Markdown.get_references()
|> Enum.map(&String.to_integer/1)
# Delete all old references for this note
multi =
multi
|> Ecto.Multi.delete_all(:delete_links, query)
# Create a link to all referenced notes
multi =
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)
end)
Repo.transaction(multi)
end
@doc """
Returns the list of topics.

6
lib/mirage/notes/note.ex

@ -2,7 +2,7 @@ defmodule Mirage.Notes.Note do
use Ecto.Schema
import Ecto.Changeset
alias Mirage.Notes.{NoteLink, Topic, Tags}
alias Mirage.Notes.{NoteLink, Topic, Tags, Note}
schema "notes" do
field :title, :string
@ -12,6 +12,10 @@ defmodule Mirage.Notes.Note do
has_many :links, NoteLink
many_to_many :topics, Topic, join_through: "notes_topics"
many_to_many :backlinks, Note,
join_through: "notes_notes",
join_keys: [target_id: :id, source_id: :id]
field :topic_string, :string,
virtual: true,
default: ""

19
lib/mirage/notes/note_note.ex

@ -0,0 +1,19 @@
defmodule Mirage.Notes.NoteNote do
use Ecto.Schema
import Ecto.Changeset
alias Mirage.Notes.Note
schema "notes_notes" do
belongs_to :source, Note
belongs_to :target, Note
end
@attrs [:source, :target]
@doc false
def changeset(note_topic, attrs) do
note_topic
|> cast(attrs, @attrs)
|> validate_required(@attrs)
end
end

28
lib/mirage/notes/tags.ex

@ -74,22 +74,22 @@ defmodule Mirage.Notes.Tags do
end
@doc utils: :tag
defp add_tags(content, tags) when is_binary(tags) do
Enum.each(split_tags(tags), &add_tag(content, &1))
content
defp add_tags(note, tags) when is_binary(tags) do
Enum.each(split_tags(tags), &add_tag(note, &1))
note
end
@doc utils: :tag
defp add_tags(content, tags) do
Enum.each(tags, &add_tag(content, &1))
content
defp add_tags(note, tags) do
Enum.each(tags, &add_tag(note, &1))
note
end
@doc utils: :tag
defp remove_tag(content, topic_text) when is_binary(topic_text) do
defp remove_tag(note, topic_text) when is_binary(topic_text) do
case Repo.get_by(Topic, %{text: topic_text}) do
nil -> nil
topic -> remove_tag(content, topic.id)
topic -> remove_tag(note, topic.id)
end
end
@ -112,15 +112,15 @@ defmodule Mirage.Notes.Tags do
end
@doc utils: :tag
defp remove_tags(content, tags) when is_binary(tags) do
Enum.each(split_tags(tags), &remove_tag(content, &1))
content
defp remove_tags(note, tags) when is_binary(tags) do
Enum.each(split_tags(tags), &remove_tag(note, &1))
note
end
@doc utils: :tag
defp remove_tags(content, tags) do
Enum.each(tags, &remove_tag(content, &1))
content
defp remove_tags(note, tags) do
Enum.each(tags, &remove_tag(note, &1))
note
end
@doc utils: :tag

3
lib/mirage_web/controllers/note_controller.ex

@ -22,6 +22,7 @@ defmodule MirageWeb.NoteController do
note
|> Notes.preload_note()
|> Notes.Tags.update_tags(note_params["topic_string"])
|> Notes.link_note()
conn
|> put_flash(:info, "Note created successfully.")
@ -60,8 +61,8 @@ defmodule MirageWeb.NoteController do
case Notes.update_note(note, note_params) do
{:ok, note} ->
note
|> Notes.preload_note()
|> Notes.Tags.update_tags(note_params["topic_string"])
|> Notes.link_note()
conn
|> put_flash(:info, "Note updated successfully.")

8
lib/mirage_web/templates/note/show.html.eex

@ -16,6 +16,14 @@
</article>
</div>
<section class="backlinks">
<h4>Backlinks</h4>
<%= for backlink <- @note.backlinks do %>
<%= link backlink.title, to: Routes.note_path(@conn, :show, backlink), class: "backlink bg-content" %>
<% end %>
</section>
<%= if @current_user do %>
<div class="buttons">
<span><%= link "Edit", to: Routes.note_path(@conn, :edit, @note), class: "button" %></span>

2
mix.exs

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

13
priv/repo/migrations/20210209183415_create_note_note.exs

@ -0,0 +1,13 @@
defmodule Mirage.Repo.Migrations.CreateNoteNote do
use Ecto.Migration
def change do
create table(:notes_notes) do
add :source_id, references(:notes, on_delete: :delete_all)
add :target_id, references(:notes, on_delete: :delete_all)
end
create index(:notes_notes, [:source_id])
create index(:notes_notes, [:target_id])
end
end

6
test/mirage/notes_test.exs

@ -88,6 +88,12 @@ defmodule Mirage.NotesTest do
}]]"
assert note.content_html =~ "Link to myself"
note =
Notes.get_note!(note.id)
|> Notes.preload_note()
IO.inspect(note)
end
test "update_note/2 with invalid data returns error changeset" do

14
test/mirage_web/controllers/note_controller_test.exs

@ -6,7 +6,10 @@ defmodule MirageWeb.NoteControllerTest do
setup :register_and_log_in_user
@create_attrs %{content: "some content", title: "some title", topic_string: "some tag"}
@update_attrs %{content: "some updated content", topic_string: "some other tag"}
@update_attrs %{
content: "some updated content with link [[@NOTE_ID]]",
topic_string: "some other tag"
}
@invalid_attrs %{content: nil}
def fixture(:note) do
@ -59,11 +62,16 @@ defmodule MirageWeb.NoteControllerTest do
setup [:create_note]
test "redirects when data is valid", %{conn: conn, note: note} do
conn = put(conn, Routes.note_path(conn, :update, note), note: @update_attrs)
attrs =
Map.update!(@update_attrs, :content, fn content ->
String.replace(content, "@NOTE_ID", to_string(note.id))
end)
conn = put(conn, Routes.note_path(conn, :update, note), note: attrs)
assert redirected_to(conn) == Routes.note_path(conn, :show, note)
conn = get(conn, Routes.note_path(conn, :show, note))
assert html_response(conn, 200) =~ "some updated content"
assert html_response(conn, 200) =~ "some updated content with link"
assert html_response(conn, 200) =~ "some other tag"
refute html_response(conn, 200) =~ "some tag"
end

Loading…
Cancel
Save