From 8dfe12da68a2906cf44e1778fabcececfbbccbde Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Sun, 23 May 2021 18:22:55 +0200 Subject: [PATCH] Change anchor tags for sections to be based on the titles, not randomly generated (#288) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Change anchor tags for sections to be based on the titles, not randomly generated * Update lib/livebook_web/helpers.ex Co-authored-by: Jonatan Kłosko --- lib/livebook_web/helpers.ex | 28 ++++++++++ lib/livebook_web/live/session_live.ex | 21 ++++--- .../live/session_live/section_component.ex | 4 +- test/livebook_web/helpers_test.exs | 55 +++++++++++++++++++ 4 files changed, 99 insertions(+), 9 deletions(-) diff --git a/lib/livebook_web/helpers.ex b/lib/livebook_web/helpers.ex index 81c7881fc..00dd49885 100644 --- a/lib/livebook_web/helpers.ex +++ b/lib/livebook_web/helpers.ex @@ -86,4 +86,32 @@ defmodule LivebookWeb.Helpers do pid_str = Phoenix.LiveDashboard.Helpers.encode_pid(pid) Routes.live_dashboard_path(socket, :page, node(), "processes", info: pid_str) end + + @doc """ + Converts human-readable strings to strings which can be used + as HTML element IDs (compatible with HTML5) + + At the same time duplicate IDs are enumerated to avoid duplicates + """ + @spec names_to_html_ids(list(String.t())) :: list(String.t()) + def names_to_html_ids(names) do + names + |> Enum.map(&name_to_html_id/1) + |> Enum.map_reduce(%{}, fn html_id, counts -> + counts = Map.update(counts, html_id, 1, &(&1 + 1)) + + case counts[html_id] do + 1 -> {html_id, counts} + count -> {"#{html_id}-#{count}", counts} + end + end) + |> elem(0) + end + + defp name_to_html_id(name) do + name + |> String.trim() + |> String.downcase() + |> String.replace(~r/\s+/u, "-") + end end diff --git a/lib/livebook_web/live/session_live.ex b/lib/livebook_web/live/session_live.ex index 45d39e8ed..44f2d7b6a 100644 --- a/lib/livebook_web/live/session_live.ex +++ b/lib/livebook_web/live/session_live.ex @@ -737,7 +737,7 @@ defmodule LivebookWeb.SessionLive do data.clients_map |> Enum.map(fn {client_pid, user_id} -> {client_pid, data.users_map[user_id]} end) |> Enum.sort_by(fn {_client_pid, user} -> user.name end), - section_views: Enum.map(data.notebook.sections, §ion_to_view(&1, data)) + section_views: section_views(data.notebook.sections, data) } end @@ -768,12 +768,19 @@ defmodule LivebookWeb.SessionLive do defp evaluated?(cell, data), do: data.cell_infos[cell.id].validity_status == :evaluated - defp section_to_view(section, data) do - %{ - id: section.id, - name: section.name, - cell_views: Enum.map(section.cells, &cell_to_view(&1, data)) - } + defp section_views(sections, data) do + sections + |> Enum.map(& &1.name) + |> names_to_html_ids() + |> Enum.zip(sections) + |> Enum.map(fn {html_id, section} -> + %{ + id: section.id, + html_id: html_id, + name: section.name, + cell_views: Enum.map(section.cells, &cell_to_view(&1, data)) + } + end) end defp cell_to_view(cell, data) do diff --git a/lib/livebook_web/live/session_live/section_component.ex b/lib/livebook_web/live/session_live/section_component.ex index 097371d90..f28db4b60 100644 --- a/lib/livebook_web/live/session_live/section_component.ex +++ b/lib/livebook_web/live/session_live/section_component.ex @@ -4,7 +4,7 @@ defmodule LivebookWeb.SessionLive.SectionComponent do def render(assigns) do ~L"""
@@ -21,7 +21,7 @@ defmodule LivebookWeb.SessionLive.SectionComponent do because we want the content to exactly match section name. %>
- + <%= remix_icon("link", class: "text-xl") %> diff --git a/test/livebook_web/helpers_test.exs b/test/livebook_web/helpers_test.exs index 255477f58..b197c80e1 100644 --- a/test/livebook_web/helpers_test.exs +++ b/test/livebook_web/helpers_test.exs @@ -12,4 +12,59 @@ defmodule LivebookWeb.HelpersTest do Helpers.ansi_to_html_lines("\e[34msmiley\ncat\e[0m") end end + + describe "names_to_html_ids/1" do + test "title case" do + assert(Helpers.names_to_html_ids(["Title of a Section"]) == ["title-of-a-section"]) + end + + # Contains a couple of unicode spaces to ensure that we handle those + test "space characters" do + assert Helpers.names_to_html_ids([" slug \n   with  spaces \t "]) == ["slug-with-spaces"] + end + + test "emoji at end" do + assert Helpers.names_to_html_ids(["Test 🦦 "]) == ["test-🦦"] + end + + test "emoji in middle" do + assert Helpers.names_to_html_ids(["One 🥮 Two"]) == ["one-🥮-two"] + end + + test "returns empty list for an empty list" do + assert Helpers.names_to_html_ids([]) == [] + end + + test "returns id-ified strings for different kinds of names" do + names = [ + "Title of a Section", + " something with \n many space characters \t " + ] + + assert Helpers.names_to_html_ids(names) == [ + "title-of-a-section", + "something-with-many-space-characters" + ] + end + + test "enumerates ids when they would be the same" do + names = [ + "Title of a Section", + "Some other title", + " Title of a Section", + "random", + " Title of a section", + "Title of a Section " + ] + + assert Helpers.names_to_html_ids(names) == [ + "title-of-a-section", + "some-other-title", + "title-of-a-section-2", + "random", + "title-of-a-section-3", + "title-of-a-section-4" + ] + end + end end