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.
This commit is contained in:
Sergey Kuznetsov 2025-03-29 23:23:55 +01:00
parent ba95bf7e57
commit 99ea48ae9d
No known key found for this signature in database
8 changed files with 84 additions and 10 deletions

View file

@ -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"

View file

@ -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.
"""

View file

@ -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,

View file

@ -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}

View file

@ -0,0 +1,12 @@
defmodule LivebookWeb.Output.MarkdownStaticComponent do
use LivebookWeb, :live_component
@impl true
def render(assigns) do
~H"""
<div>{to_html(@output.text)}</div>
"""
end
defp to_html(markdown), do: Earmark.as_html!(markdown) |> Phoenix.HTML.raw()
end

View file

@ -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]}

View file

@ -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},

View file

@ -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"},