diff --git a/lib/asciinema/asciicasts/asciicast.ex b/lib/asciinema/asciicasts/asciicast.ex index b04e434b8..9dea8bc27 100644 --- a/lib/asciinema/asciicasts/asciicast.ex +++ b/lib/asciinema/asciicasts/asciicast.ex @@ -40,6 +40,7 @@ defmodule Asciinema.Asciicasts.Asciicast do field :archived_at, :utc_datetime_usec field :terminal_line_height, :float field :terminal_font_family, :string + field :markers, :string timestamps() @@ -100,7 +101,8 @@ defmodule Asciinema.Asciicasts.Asciicast do :theme_name, :idle_time_limit, :terminal_line_height, - :terminal_font_family + :terminal_font_family, + :markers ]) |> validate_number(:cols_override, greater_than: 0, less_than: 1024) |> validate_number(:rows_override, greater_than: 0, less_than: 512) @@ -111,6 +113,44 @@ defmodule Asciinema.Asciicasts.Asciicast do ) |> validate_inclusion(:terminal_font_family, custom_terminal_font_families) |> validate_number(:snapshot_at, greater_than: 0) + |> validate_change(:markers, &validate_markers/2) + end + + defp validate_markers(_, markers) do + case parse_markers(markers) do + {:ok, _} -> [] + {:error, index} -> [markers: "invalid syntax in line #{index + 1}"] + end + end + + def parse_markers(markers) do + results = + markers + |> String.trim() + |> String.split("\n") + |> Enum.map(&parse_marker/1) + + case Enum.find_index(results, fn result -> result == :error end) do + nil -> {:ok, results} + index -> {:error, index} + end + end + + defp parse_marker(marker) do + parts = + marker + |> String.trim() + |> String.split(~r/\s+-\s+/, parts: 2) + |> Kernel.++([""]) + |> Enum.take(2) + + with [t, l] <- parts, + {t, ""} <- Float.parse(t), + true <- String.length(l) < 100 do + {t, l} + else + _ -> :error + end end def snapshot_changeset(struct, snapshot) do diff --git a/lib/asciinema_web/templates/asciicast/edit.html.eex b/lib/asciinema_web/templates/asciicast/edit.html.eex index 71753ffdd..734dd3754 100644 --- a/lib/asciinema_web/templates/asciicast/edit.html.eex +++ b/lib/asciinema_web/templates/asciicast/edit.html.eex @@ -81,6 +81,15 @@ +
+ <%= label f, :markers, class: "col-sm-4 col-md-3 col-lg-3 col-form-label" %> +
+ <%= textarea f, :markers, class: "form-control", rows: 6, placeholder: "Example:\n\n5.0 - Intro\n11.3 - Installation\n32.0 - Configuration\n66.5 - Tips & Tricks" %> + <%= error_tag f, :markers %> + Each line is a marker defined as [time] - [label]. +
+
+

diff --git a/lib/asciinema_web/views/asciicast_view.ex b/lib/asciinema_web/views/asciicast_view.ex index 053e5b9b4..4ebeaa627 100644 --- a/lib/asciinema_web/views/asciicast_view.ex +++ b/lib/asciinema_web/views/asciicast_view.ex @@ -2,6 +2,7 @@ defmodule AsciinemaWeb.AsciicastView do use AsciinemaWeb, :view import Scrivener.HTML alias Asciinema.Asciicasts + alias Asciinema.Asciicasts.Asciicast alias AsciinemaWeb.Endpoint alias AsciinemaWeb.Router.Helpers.Extra, as: RoutesX alias AsciinemaWeb.UserView @@ -42,6 +43,7 @@ defmodule AsciinemaWeb.AsciicastView do terminalLineHeight: asciicast.terminal_line_height, customTerminalFontFamily: asciicast.terminal_font_family, poster: poster(asciicast.snapshot), + markers: markers(asciicast.markers), idleTimeLimit: asciicast.idle_time_limit, title: title(asciicast), author: author_username(asciicast), @@ -135,6 +137,15 @@ defmodule AsciinemaWeb.AsciicastView do "data:text/plain," <> text <> @csi_init <> "?25l" end + defp markers(nil), do: nil + + defp markers(markers) do + case Asciicast.parse_markers(markers) do + {:ok, markers} -> Enum.map(markers, &Tuple.to_list/1) + {:error, _} -> nil + end + end + defp line_to_text(segments) do segments |> Enum.map(&segment_to_text/1) diff --git a/priv/repo/migrations/20230515080531_add_markers_to_asciicasts.exs b/priv/repo/migrations/20230515080531_add_markers_to_asciicasts.exs new file mode 100644 index 000000000..196115c96 --- /dev/null +++ b/priv/repo/migrations/20230515080531_add_markers_to_asciicasts.exs @@ -0,0 +1,9 @@ +defmodule Asciinema.Repo.Migrations.AddMarkersToAsciicasts do + use Ecto.Migration + + def change do + alter table(:asciicasts) do + add :markers, :text + end + end +end diff --git a/test/asciinema/asciicasts_test.exs b/test/asciinema/asciicasts_test.exs index b068bd3a4..d34b7a861 100644 --- a/test/asciinema/asciicasts_test.exs +++ b/test/asciinema/asciicasts_test.exs @@ -315,4 +315,18 @@ defmodule Asciinema.AsciicastsTest do assert stream_v0 == stream_v2 end end + + describe "parse_markers/1" do + test "returns markers for valid syntax" do + result = Asciicast.parse_markers("1.0 - Intro\n2.5\n5.0 - Tips & Tricks\n") + + assert result == {:ok, [{1.0, "Intro"}, {2.5, ""}, {5.0, "Tips & Tricks"}]} + end + + test "returns error for invalid syntax" do + result = Asciicast.parse_markers("1.0 - Intro\nFoobar\n") + + assert result == {:error, 1} + end + end end