diff --git a/lib/livebook/notebook/app_settings.ex b/lib/livebook/notebook/app_settings.ex index a68ea7a43b0..97840b51f85 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 708d0a9fd22..411327fea89 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 45de41e213a..b1c6cc1bf9f 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 b7965c67d49..eb85f03928b 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 00000000000..6250d4c3d12 --- /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 1ce0b820afc..503e6489e69 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 3ad8d96c460..6ad75dd2feb 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 0933c4b7b81..76477faf861 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"},