From 99ea48ae9db75096a301228cef5c3e8e334a27e5 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Sat, 29 Mar 2025 23:23:55 +0100 Subject: [PATCH] Add 'Render ... Markdown blocks' option to published apps > Disclaimer: it's a proof-of-a-concept for an idea discussion. It's an attempt to introduce a "Render ... Markdown blocks" feature to the published apps as an option. It shall consider to render markdown block from the original notebook as an addition to the evaluable cells. Due to the limited understanding of how app rendering pipeline works, this commit should be considered only as a proof-of-a-concept, that most likely will be reworked fully. P.S. Eventually, it should also take image and section titles cells and render them. --- lib/livebook/notebook/app_settings.ex | 13 ++++--- lib/livebook/notebook/cell.ex | 7 ++++ lib/livebook_web/live/app_session_live.ex | 35 ++++++++++++++++--- lib/livebook_web/live/output.ex | 13 +++++++ .../live/output/markdown_static_component.ex | 12 +++++++ .../session_live/app_settings_component.ex | 11 ++++++ mix.exs | 2 +- mix.lock | 1 + 8 files changed, 84 insertions(+), 10 deletions(-) create mode 100644 lib/livebook_web/live/output/markdown_static_component.ex diff --git a/lib/livebook/notebook/app_settings.ex b/lib/livebook/notebook/app_settings.ex index a68ea7a43..97840b51f 100644 --- a/lib/livebook/notebook/app_settings.ex +++ b/lib/livebook/notebook/app_settings.ex @@ -14,7 +14,8 @@ defmodule Livebook.Notebook.AppSettings do access_type: access_type(), password: String.t() | nil, show_source: boolean(), - output_type: output_type() + output_type: output_type(), + render_static: boolean() } @type access_type :: :public | :protected @@ -33,6 +34,7 @@ defmodule Livebook.Notebook.AppSettings do field :password, :string field :show_source, :boolean field :output_type, Ecto.Enum, values: [:all, :rich] + field :render_static, :boolean end @doc """ @@ -49,7 +51,8 @@ defmodule Livebook.Notebook.AppSettings do access_type: :protected, password: generate_password(), show_source: false, - output_type: :all + output_type: :all, + render_static: false } end @@ -82,14 +85,16 @@ defmodule Livebook.Notebook.AppSettings do :auto_shutdown_ms, :access_type, :show_source, - :output_type + :output_type, + :render_static ]) |> validate_required([ :slug, :multi_session, :access_type, :show_source, - :output_type + :output_type, + :render_static ]) |> validate_format(:slug, ~r/^[a-z0-9-]+$/, message: "should only contain lowercase alphanumeric characters and dashes" diff --git a/lib/livebook/notebook/cell.ex b/lib/livebook/notebook/cell.ex index 708d0a9fd..411327fea 100644 --- a/lib/livebook/notebook/cell.ex +++ b/lib/livebook/notebook/cell.ex @@ -49,6 +49,13 @@ defmodule Livebook.Notebook.Cell do def evaluable?(%Cell.Smart{}), do: true def evaluable?(_cell), do: false + @doc """ + Checks if the given cell can be statically rendered + """ + @spec static?(t()) :: boolean() + def static?(%Cell.Markdown{}), do: true + def static?(_), do: false + @doc """ Extracts all inputs from the given indexed output. """ diff --git a/lib/livebook_web/live/app_session_live.ex b/lib/livebook_web/live/app_session_live.ex index 45de41e21..b1c6cc1bf 100644 --- a/lib/livebook_web/live/app_session_live.ex +++ b/lib/livebook_web/live/app_session_live.ex @@ -442,17 +442,42 @@ defmodule LivebookWeb.AppSessionLive do defp data_to_view(data) do changed_input_ids = Session.Data.changed_input_ids(data) - %{ - notebook_name: data.notebook.name, - cell_views: - for {cell, _section} <- Notebook.evaluable_cells_with_section(data.notebook) do + shall_render = fn cell -> + if data.notebook.app_settings.render_static do + Cell.evaluable?(cell) or Cell.static?(cell) + else + Cell.evaluable?(cell) + end + end + + cell_views = + data.notebook + |> Notebook.cells_with_section() + |> Enum.filter(fn {cell, _section} -> shall_render.(cell) end) + |> Enum.map(fn + {%Livebook.Notebook.Cell.Markdown{} = cell, _section} -> + out_id = :rand.uniform(31337) + output = {out_id, %{type: :markdown_static, text: cell.source, chunk: false}} + + %{ + id: cell.id, + input_views: [], + outputs: [output], + outputs_batch_number: 0 + } + + {cell, _section} -> %{ id: cell.id, input_views: input_views_for_cell(cell, data, changed_input_ids), outputs: filter_outputs(cell.outputs, data.notebook.app_settings.output_type), outputs_batch_number: data.cell_infos[cell.id].eval.outputs_batch_number } - end, + end) + + %{ + notebook_name: data.notebook.name, + cell_views: cell_views, app_status: data.app_data.status, show_source: data.notebook.app_settings.show_source, slug: data.notebook.app_settings.slug, diff --git a/lib/livebook_web/live/output.ex b/lib/livebook_web/live/output.ex index b7965c67d..eb85f0392 100644 --- a/lib/livebook_web/live/output.ex +++ b/lib/livebook_web/live/output.ex @@ -64,6 +64,19 @@ defmodule LivebookWeb.Output do """ end + defp render_output(%{type: :markdown_static} = output, %{id: id, session_id: session_id}) do + assigns = %{id: id, session_id: session_id, output: output} + + ~H""" + <.live_component + module={Output.MarkdownStaticComponent} + id={@id} + session_id={@session_id} + output={@output} + /> + """ + end + defp render_output(%{type: :image} = output, %{id: id}) do assigns = %{id: id, content: output.content, mime_type: output.mime_type} diff --git a/lib/livebook_web/live/output/markdown_static_component.ex b/lib/livebook_web/live/output/markdown_static_component.ex new file mode 100644 index 000000000..6250d4c3d --- /dev/null +++ b/lib/livebook_web/live/output/markdown_static_component.ex @@ -0,0 +1,12 @@ +defmodule LivebookWeb.Output.MarkdownStaticComponent do + use LivebookWeb, :live_component + + @impl true + def render(assigns) do + ~H""" +
{to_html(@output.text)}
+ """ + end + + defp to_html(markdown), do: Earmark.as_html!(markdown) |> Phoenix.HTML.raw() +end diff --git a/lib/livebook_web/live/session_live/app_settings_component.ex b/lib/livebook_web/live/session_live/app_settings_component.ex index 1ce0b820a..503e6489e 100644 --- a/lib/livebook_web/live/session_live/app_settings_component.ex +++ b/lib/livebook_web/live/session_live/app_settings_component.ex @@ -105,6 +105,17 @@ defmodule LivebookWeb.SessionLive.AppSettingsComponent do ''' } /> + <.checkbox_field + field={f[:render_static]} + label="Render sections and Markdown blocks" + help={ + ~S''' + When enabled, renders all the added + h2-sections and Markdown blocks of + the original notebook. + ''' + } + /> <%= if Ecto.Changeset.get_field(@changeset, :multi_session) do %> <.checkbox_field field={f[:show_existing_sessions]} diff --git a/mix.exs b/mix.exs index 3ad8d96c4..6ad75dd2f 100644 --- a/mix.exs +++ b/mix.exs @@ -114,7 +114,7 @@ defmodule Livebook.MixProject do {:bandit, "~> 1.0"}, {:plug, "~> 1.16"}, {:plug_crypto, "~> 2.0"}, - {:earmark_parser, "~> 1.4"}, + {:earmark, "~> 1.4"}, {:ecto, "~> 3.10"}, {:phoenix_ecto, "~> 4.4"}, {:aws_credentials, "~> 0.3.0", runtime: false}, diff --git a/mix.lock b/mix.lock index 0933c4b7b..76477faf8 100644 --- a/mix.lock +++ b/mix.lock @@ -9,6 +9,7 @@ "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, "dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"}, + "earmark": {:hex, :earmark, "1.4.47", "7e7596b84fe4ebeb8751e14cbaeaf4d7a0237708f2ce43630cfd9065551f94ca", [:mix], [], "hexpm", "3e96bebea2c2d95f3b346a7ff22285bc68a99fbabdad9b655aa9c6be06c698f8"}, "earmark_parser": {:hex, :earmark_parser, "1.4.43", "34b2f401fe473080e39ff2b90feb8ddfeef7639f8ee0bbf71bb41911831d77c5", [:mix], [], "hexpm", "970a3cd19503f5e8e527a190662be2cee5d98eed1ff72ed9b3d1a3d466692de8"}, "ecto": {:hex, :ecto, "3.12.5", "4a312960ce612e17337e7cefcf9be45b95a3be6b36b6f94dfb3d8c361d631866", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6eb18e80bef8bb57e17f5a7f068a1719fbda384d40fc37acb8eb8aeca493b6ea"}, "eini": {:hex, :eini_beam, "2.2.4", "02143b1dce4dda4243248e7d9b3d8274b8d9f5a666445e3d868e2cce79e4ff22", [:rebar3], [], "hexpm", "12de479d144b19e09bb92ba202a7ea716739929afdf9dff01ad802e2b1508471"},