livebook/lib/livebook/notebook/explore.ex
2021-12-03 15:08:28 +01:00

193 lines
5.8 KiB
Elixir

defmodule Livebook.Notebook.Explore do
@moduledoc false
defmodule NotFoundError do
@moduledoc false
defexception [:slug, plug_status: 404]
def message(%{slug: slug}) do
"could not find an example notebook matching #{inspect(slug)}"
end
end
@type notebook_info :: %{
slug: String.t(),
livemd: String.t(),
title: String.t(),
images: images(),
details: details() | nil
}
@type images :: %{String.t() => binary()}
@type details :: %{
description: String.t(),
cover_url: String.t()
}
images_dir = Path.expand("explore/images", __DIR__)
welcome_config = %{
path: Path.join(__DIR__, "explore/intro_to_livebook.livemd"),
details: %{
description: "Get to know Livebook, see how it works and explore its features.",
cover_url: "/images/logo.png"
}
}
other_configs = [
%{
path: Path.join(__DIR__, "explore/distributed_portals_with_elixir.livemd"),
image_paths: [
Path.join(images_dir, "portal-drop.jpeg"),
Path.join(images_dir, "portal-list.jpeg")
],
details: %{
description:
"A fast-paced introduction to the Elixir language by building distributed data-transfer portals.",
cover_url: "/images/elixir-portal.jpeg"
}
},
%{
path: Path.join(__DIR__, "explore/elixir_and_livebook.livemd"),
details: %{
description: "Learn how to use some of Elixir and Livebook's unique features together.",
cover_url: "/images/elixir.png"
}
},
%{
path: Path.join(__DIR__, "explore/intro_to_vega_lite.livemd"),
details: %{
description: "Learn how to quickly create numerous plots for your data.",
cover_url: "/images/vega_lite.png"
}
},
%{
path: Path.join(__DIR__, "explore/intro_to_kino.livemd"),
details: %{
description: "Display and control rich and interactive widgets in Livebook.",
cover_url: "/images/kino.png"
}
},
%{
path: Path.join(__DIR__, "explore/intro_to_nx.livemd"),
details: %{
description:
"Enter Numerical Elixir, experience the power of multi-dimensional arrays of numbers.",
cover_url: "/images/nx.png"
}
},
# %{
# path: Path.join(__DIR__, "explore/intro_to_axon.livemd"),
# details: %{
# description: "Build Neural Networks in Elixir using a high-level, composable API.",
# cover_url: "/images/axon.png"
# }
# },
%{
path: Path.join(__DIR__, "explore/vm_introspection.livemd"),
details: %{
description: "Extract and visualize information about a remote running node.",
cover_url: "/images/vm_introspection.png"
}
},
%{
path: Path.join(__DIR__, "explore/pong.livemd"),
details: %{
description: "Implement and play multiplayer Pong directly in Livebook.",
cover_url: "/images/pong.png"
}
}
]
user_configs = Application.fetch_env!(:livebook, :explore_notebooks)
notebook_configs = [welcome_config] ++ user_configs ++ other_configs
notebook_infos =
for config <- notebook_configs do
path =
config[:path] ||
raise "missing required :path attribute in notebook configuration: #{inspect(config)}"
@external_resource path
markdown = File.read!(path)
# Parse the file to ensure no warnings and read the title.
# However, in the info we keep just the file contents to save on memory.
{notebook, warnings} = Livebook.LiveMarkdown.Import.notebook_from_markdown(markdown)
if warnings != [] do
items = Enum.map(warnings, &("- " <> &1))
raise "found warnings while importing #{path}:\n\n" <> Enum.join(items, "\n")
end
images =
config
|> Map.get(:image_paths, [])
|> Map.new(fn image_path ->
image_name = Path.basename(image_path)
content = File.read!(image_path)
{image_name, content}
end)
slug =
config[:slug] || path |> Path.basename() |> Path.rootname() |> String.replace("_", "-")
%{
slug: slug,
livemd: markdown,
title: notebook.name,
images: images,
details:
if config_details = config[:details] do
description =
config_details[:description] ||
raise "missing required :description attribute in notebook details: #{inspect(config_details)}"
cover_url =
config_details[:cover_url] ||
(config_details[:cover_path] &&
Livebook.Utils.read_as_data_url!(config_details.cover_path)) ||
raise "expected either :cover_path or :cover_url in notebooks details: #{inspect(config_details)}"
%{description: description, cover_url: cover_url}
end
}
end
@doc """
Returns a list of example notebooks with metadata.
"""
@spec notebook_infos() :: list(notebook_info())
def notebook_infos(), do: unquote(Macro.escape(notebook_infos))
@doc """
Same as `notebook_infos/0`, but returns only notebooks that have
additional details.
"""
@spec visible_notebook_infos() :: list(notebook_info())
def visible_notebook_infos() do
notebook_infos() |> Enum.filter(& &1.details)
end
@doc """
Finds explore notebook by slug and returns the parsed data structure.
Returns the notebook along with the images it uses as preloaded binaries.
"""
@spec notebook_by_slug!(String.t()) :: {Livebook.Notebook.t(), images()}
def notebook_by_slug!(slug) do
notebook_infos()
|> Enum.find(&(&1.slug == slug))
|> case do
nil ->
raise NotFoundError, slug: slug
notebook_info ->
{notebook, []} = Livebook.LiveMarkdown.Import.notebook_from_markdown(notebook_info.livemd)
{notebook, notebook_info.images}
end
end
end