3 Commits

  1. 13
      CHANGELOG.md
  2. 102
      lib/mirage/search.ex
  3. 101
      lib/mirage_web/live/goto_anything_live.ex
  4. 33
      lib/mirage_web/live/goto_anything_live.html.leex
  5. 40
      lib/mirage_web/live/search_live.html.leex
  6. 2
      lib/mirage_web/live/search_live/index.ex
  7. 28
      lib/mirage_web/live/search_live/index.html.leex
  8. 15
      lib/mirage_web/live/search_live/search_result_component.ex
  9. 2
      lib/mirage_web/router.ex
  10. 2
      lib/mirage_web/templates/layout/root.html.leex
  11. 2
      mix.exs

13
CHANGELOG.md

@ -5,6 +5,19 @@ See [Conventional Commits](Https://conventionalcommits.org) for commit guideline
<!-- changelog -->
## [v0.83.0](https://git.inhji.de/inhji/mirage/compare/v0.82.1...v0.83.0) (2021-03-21)
### Chores:
* remove goto anything
### Features:
* hightlight search results!
## [v0.82.1](https://git.inhji.de/inhji/mirage/compare/v0.82.0...v0.82.1) (2021-03-20)

102
lib/mirage/search.ex

@ -13,37 +13,115 @@ defmodule Mirage.Search do
end
@doc """
Searches notes and links for `search_string`
Searches notes and links for `search_term`
## Note
type CANNOT be an atom, otherwise it gets assigned wrongly!
"""
def search_notes(search_string) do
search_string = String.downcase(search_string)
def search_notes(search_term) do
search_term = String.downcase(search_term)
link_query =
from l in Mirage.Links.Link,
where: contains(l.title, ^search_string),
or_where: contains(l.url, ^search_string),
or_where: contains(l.content, ^search_string),
where: contains(l.title, ^search_term),
or_where: contains(l.content, ^search_term),
or_where: contains(l.url, ^search_term),
select: %{
type: "link",
id: l.id,
title: l.title,
type: "link"
content: l.content,
url: l.url
}
note_query =
from n in Mirage.Notes.Note,
where: contains(n.content, ^search_string),
or_where: contains(n.title, ^search_string),
where: contains(n.content, ^search_term),
or_where: contains(n.title, ^search_term),
select: %{
type: "note",
id: n.id,
title: n.title,
type: "note"
content: n.content,
url: ""
},
union_all: ^link_query
union: ^link_query
Repo.all(note_query)
note_query
|> Repo.all()
|> Enum.map(&find_occurrence(&1, search_term))
|> Enum.filter(&remove_false_positives/1)
|> Enum.map(&highlight_occurrence(&1, search_term))
end
defp remove_false_positives(result) do
case Map.get(result, :occurrence) do
nil -> false
_ -> true
end
end
defp highlight_occurrence(%{occurrence: occurrence} = result, search_term) do
if is_nil(occurrence) do
result
else
{index, length, prop} = occurrence
fffound = Map.get(result, prop)
replaced = String.replace(fffound, search_term, "<mark>#{search_term}</mark>")
result
|> Map.put_new(:found_value, replaced)
|> Map.put_new(:found_prop, prop)
end
end
defp find_occurrence(result, search_term) do
occurrence =
Enum.reduce_while(
[
{&maybe_get_occurrence_from/3, :title},
{&maybe_get_occurrence_from/3, :content},
{&maybe_get_occurrence_from/3, :url}
],
nil,
fn {strategy, prop}, acc ->
case strategy.(result, search_term, prop) do
:error -> {:cont, acc}
{:error, _} -> {:cont, acc}
{:ok, index, length, prop} -> {:halt, {index, length, prop}}
end
end
)
result
|> Map.put_new(:occurrence, occurrence)
end
defp maybe_get_occurrence_from(result, search_term, prop) do
value = result |> Map.get(prop)
if is_nil(value) or value === "" do
:error
else
value = value |> String.downcase()
case find_index(value, search_term) do
-1 ->
IO.inspect("Term #{search_term} not found in '#{value}'")
:error
index ->
IO.inspect("Term #{search_term} found in '#{value}'")
{:ok, index, String.length(search_term), prop}
end
end
end
def find_index(string, value_to_find) do
case String.split(string, value_to_find, parts: 2) do
[left, _] -> String.length(left)
[_] -> -1
end
end
end

101
lib/mirage_web/live/goto_anything_live.ex

@ -1,101 +0,0 @@
defmodule MirageWeb.GotoAnythingLive do
use MirageWeb, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(:results, [])
|> assign(:selection, -1)
|> close_popup()}
end
@impl true
def handle_event("hotkey", %{"key" => "p", "ctrlKey" => true}, socket) do
{:noreply, open_popup(socket)}
end
@impl true
def handle_event("hotkey", %{"key" => "Escape", "ctrlKey" => false}, socket) do
{:noreply, close_popup(socket)}
end
@impl true
def handle_event("hotkey", _params, socket) do
{:noreply, socket}
end
@impl true
def handle_event("search", %{"key" => "ArrowRight", "value" => _}, %{assigns: assigns} = socket) do
results = assigns.results
selection = assigns.selection
socket =
if Enum.empty?(results) do
socket |> close_popup()
else
note = Enum.at(results, selection)
socket
|> close_popup()
|> push_redirect(to: Routes.note_show_path(socket, :show, note))
end
{:noreply, socket}
end
@impl true
def handle_event("search", %{"ctrlKey" => false, "key" => "Escape", "value" => _}, socket) do
{:noreply, close_popup(socket)}
end
@impl true
def handle_event("search", %{"ctrlKey" => false, "key" => "ArrowUp", "value" => _}, socket) do
{:noreply, dec_selection(socket)}
end
@impl true
def handle_event("search", %{"ctrlKey" => false, "key" => "ArrowDown", "value" => _}, socket) do
{:noreply, inc_selection(socket)}
end
@impl true
def handle_event("search", %{"key" => _, "value" => query}, socket) do
results = Mirage.Notes.search_notes(query)
{:noreply,
socket
|> assign(:results, results)
|> assign(:selection, 0)}
end
defp open_popup(socket), do: socket |> assign(%{popup_class: "open"})
defp close_popup(socket), do: socket |> assign(%{popup_class: "closed"})
defp inc_selection(%{assigns: assigns} = socket) do
selection = assigns.selection
results = Enum.count(assigns.results)
selection =
if selection >= results do
selection
else
selection + 1
end
socket |> assign(:selection, selection)
end
defp dec_selection(%{assigns: assigns} = socket) do
selection = assigns.selection
selection =
if selection <= 0 do
selection
else
selection - 1
end
socket |> assign(:selection, selection)
end
end

33
lib/mirage_web/live/goto_anything_live.html.leex

@ -1,33 +0,0 @@
<div id="goto-anything" class="<%= @popup_class %>" phx-window-keydown="hotkey" phx-hook="GotoAnything">
<form action="#">
<input id="query"
type="text"
placeholder="Search for anything..."
autocomplete="off"
phx-keydown="search"
phx-debounce="10">
</form>
<div class="results">
<%= if Enum.empty?(@results) do %>
<div class="card result">
<strong>No results..</strong>
<div>yet</div>
</div>
<% else %>
<%= for {note, index} <- @results |> Enum.with_index() do %>
<%= live_redirect to: Routes.note_show_path(@socket, :show, note.id) do %>
<div class='card result <%= if index == @selection, do: "active", else: "" %>'>
<strong><%= note.title %></strong>
<p class="tags">
<span class="tag">Views: <%= note.views %></span>
<%= for topic <- note.topics do %>
<span class="tag"><%= topic.text %></span>
<% end %>
</p>
</div>
<% end %>
<% end %>
<% end %>
</div>
</div>

40
lib/mirage_web/live/search_live.html.leex

@ -1,40 +0,0 @@
<header class="hero">
<h1>Search</h1>
<form action="#">
<input id="query"
type="text"
placeholder="Search for anything..."
autocomplete="off"
phx-keydown="search"
phx-debounce="10">
</form>
<div class="results">
<%= if Enum.empty?(@results) do %>
<div class="card result">
<strong>No results..</strong>
<div>yet</div>
</div>
<% else %>
<%= for {result, index} <- @results |> Enum.with_index() do %>
<%= if result.type == "note" do %>
<article>
<%= live_redirect to: Routes.note_show_path(@socket, :show, result.id) do %>
<h2><strong><%= result.title %></strong></h2>
<% end %>
</article>
<% end %>
<%= if result.type == "link" do %>
<article>
<%= live_redirect to: Routes.link_show_path(@socket, :show, result.id) do %>
<h2><strong><%= result.title %></strong></h2>
<% end %>
</article>
<% end %>
<% end %>
<% end %>
</div>
</header>

2
lib/mirage_web/live/search_live.ex → lib/mirage_web/live/search_live/index.ex

@ -1,4 +1,4 @@
defmodule MirageWeb.SearchLive do
defmodule MirageWeb.SearchLive.Index do
use MirageWeb, :live_view
@impl true

28
lib/mirage_web/live/search_live/index.html.leex

@ -0,0 +1,28 @@
<header class="hero">
<h1>Search</h1>
<form action="#">
<input id="query"
type="text"
placeholder="Search for anything..."
autocomplete="off"
phx-keydown="search"
phx-debounce="200">
</form>
<div class="results">
<%= if Enum.empty?(@results) do %>
<% else %>
<%= for {result, index} <- @results |> Enum.with_index() do %>
<%= if result.type == "note" do %>
<%= live_component @socket, MirageWeb.SearchLive.SearchResultComponent, result: result, url: Routes.note_show_path(@socket, :show, result.id) %>
<% end %>
<%= if result.type == "link" do %>
<%= live_component @socket, MirageWeb.SearchLive.SearchResultComponent, result: result, url: Routes.link_show_path(@socket, :show, result.id) %>
<% end %>
<% end %>
<% end %>
</div>
</header>

15
lib/mirage_web/live/search_live/search_result_component.ex

@ -0,0 +1,15 @@
defmodule MirageWeb.SearchLive.SearchResultComponent do
use MirageWeb, :live_component
def render(assigns) do
~L"""
<article>
<%= live_redirect to: @url do %>
<h2><strong><%= @result.title || @result.url %></strong></h2>
<% end %>
<p><%= raw @result.found_value %></p>
<footer>Found in <strong><%= @result.found_prop %></strong> <%= inspect(@result.occurrence) %></footer>
</article>
"""
end
end

2
lib/mirage_web/router.ex

@ -28,7 +28,7 @@ defmodule MirageWeb.Router do
live "/", HomeLive, :index
live "/blog", BlogLive, :index
live "/search", SearchLive, :index
live "/search", SearchLive.Index, :index
resources "/topics", TopicController

2
lib/mirage_web/templates/layout/root.html.leex

@ -44,7 +44,5 @@
<%= raw @_s.footer_text.value_html %>
</p>
</footer>
<% live_render @conn, MirageWeb.GotoAnythingLive %>
</body>
</html>

2
mix.exs

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

Loading…
Cancel
Save