mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-01-01 20:51:44 +08:00
Add Elixir source export (#476)
This commit is contained in:
parent
45e8da0652
commit
cfbba9e2ce
9 changed files with 391 additions and 43 deletions
80
lib/livebook/notebook/export/elixir.ex
Normal file
80
lib/livebook/notebook/export/elixir.ex
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
defmodule Livebook.Notebook.Export.Elixir do
|
||||||
|
alias Livebook.Notebook
|
||||||
|
alias Livebook.Notebook.Cell
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Converts the given notebook into a Elixir source code.
|
||||||
|
"""
|
||||||
|
@spec notebook_to_elixir(Notebook.t()) :: String.t()
|
||||||
|
def notebook_to_elixir(notebook) do
|
||||||
|
iodata = render_notebook(notebook)
|
||||||
|
# Add trailing newline
|
||||||
|
IO.iodata_to_binary([iodata, "\n"])
|
||||||
|
end
|
||||||
|
|
||||||
|
defp render_notebook(notebook) do
|
||||||
|
name = ["# Title: ", notebook.name]
|
||||||
|
sections = Enum.map(notebook.sections, &render_section(&1, notebook))
|
||||||
|
|
||||||
|
[name | sections]
|
||||||
|
|> Enum.intersperse("\n\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
defp render_section(section, notebook) do
|
||||||
|
name = ["# ── ", section.name, " ──"]
|
||||||
|
|
||||||
|
name =
|
||||||
|
if section.parent_id do
|
||||||
|
{:ok, parent} = Notebook.fetch_section(notebook, section.parent_id)
|
||||||
|
[name, " (⎇ from ", parent.name, ")"]
|
||||||
|
else
|
||||||
|
name
|
||||||
|
end
|
||||||
|
|
||||||
|
cells =
|
||||||
|
section.cells
|
||||||
|
|> Enum.map(&render_cell(&1, section))
|
||||||
|
|> Enum.reject(&(&1 == []))
|
||||||
|
|
||||||
|
[name | cells]
|
||||||
|
|> Enum.intersperse("\n\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
defp render_cell(%Cell.Markdown{} = cell, _section) do
|
||||||
|
cell.source
|
||||||
|
|> Livebook.LiveMarkdown.MarkdownHelpers.reformat()
|
||||||
|
|> String.split("\n")
|
||||||
|
|> Enum.map_intersperse("\n", &comment_out/1)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp render_cell(%Cell.Elixir{} = cell, section) do
|
||||||
|
code = get_elixir_cell_code(cell)
|
||||||
|
|
||||||
|
if section.parent_id do
|
||||||
|
code
|
||||||
|
|> IO.iodata_to_binary()
|
||||||
|
|> String.split("\n")
|
||||||
|
|> Enum.map_intersperse("\n", &comment_out/1)
|
||||||
|
else
|
||||||
|
code
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp render_cell(_cell, _section), do: []
|
||||||
|
|
||||||
|
defp comment_out(""), do: ""
|
||||||
|
defp comment_out(line), do: ["# ", line]
|
||||||
|
|
||||||
|
defp get_elixir_cell_code(%{source: source, metadata: %{"disable_formatting" => true}}),
|
||||||
|
do: source
|
||||||
|
|
||||||
|
defp get_elixir_cell_code(%{source: source}), do: format_code(source)
|
||||||
|
|
||||||
|
defp format_code(code) do
|
||||||
|
try do
|
||||||
|
Code.format_string!(code)
|
||||||
|
rescue
|
||||||
|
_ -> code
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -14,20 +14,38 @@ defmodule LivebookWeb.SessionController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def download_source(conn, %{"id" => id}) do
|
def download_source(conn, %{"id" => id, "format" => format}) do
|
||||||
if SessionSupervisor.session_exists?(id) do
|
if SessionSupervisor.session_exists?(id) do
|
||||||
notebook = Session.get_notebook(id)
|
notebook = Session.get_notebook(id)
|
||||||
source = Livebook.LiveMarkdown.Export.notebook_to_markdown(notebook)
|
|
||||||
|
|
||||||
send_download(conn, {:binary, source},
|
send_notebook_source(conn, notebook, format)
|
||||||
filename: "notebook.livemd",
|
|
||||||
content_type: "text/plain"
|
|
||||||
)
|
|
||||||
else
|
else
|
||||||
send_resp(conn, 404, "Not found")
|
send_resp(conn, 404, "Not found")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp send_notebook_source(conn, notebook, "livemd") do
|
||||||
|
source = Livebook.LiveMarkdown.Export.notebook_to_markdown(notebook)
|
||||||
|
|
||||||
|
send_download(conn, {:binary, source},
|
||||||
|
filename: "notebook.livemd",
|
||||||
|
content_type: "text/plain"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp send_notebook_source(conn, notebook, "exs") do
|
||||||
|
source = Livebook.Notebook.Export.Elixir.notebook_to_elixir(notebook)
|
||||||
|
|
||||||
|
send_download(conn, {:binary, source},
|
||||||
|
filename: "notebook.exs",
|
||||||
|
content_type: "text/plain"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp send_notebook_source(conn, _notebook, _format) do
|
||||||
|
send_resp(conn, 400, "Invalid format, supported formats: livemd, exs")
|
||||||
|
end
|
||||||
|
|
||||||
defp serve_static(conn, path) do
|
defp serve_static(conn, path) do
|
||||||
case put_cache_header(conn, path) do
|
case put_cache_header(conn, path) do
|
||||||
{:stale, conn} ->
|
{:stale, conn} ->
|
||||||
|
|
|
@ -202,7 +202,7 @@ defmodule LivebookWeb.SessionLive do
|
||||||
<.remix_icon icon="dashboard-2-line" />
|
<.remix_icon icon="dashboard-2-line" />
|
||||||
<span class="font-medium">See on Dashboard</span>
|
<span class="font-medium">See on Dashboard</span>
|
||||||
</a>
|
</a>
|
||||||
<%= live_patch to: Routes.session_path(@socket, :export, @session_id),
|
<%= live_patch to: Routes.session_path(@socket, :export, @session_id, "livemd"),
|
||||||
class: "menu__item text-gray-500" do %>
|
class: "menu__item text-gray-500" do %>
|
||||||
<.remix_icon icon="download-2-line" />
|
<.remix_icon icon="download-2-line" />
|
||||||
<span class="font-medium">Export</span>
|
<span class="font-medium">Export</span>
|
||||||
|
@ -323,6 +323,7 @@ defmodule LivebookWeb.SessionLive do
|
||||||
id: "export",
|
id: "export",
|
||||||
modal_class: "w-full max-w-4xl",
|
modal_class: "w-full max-w-4xl",
|
||||||
session_id: @session_id,
|
session_id: @session_id,
|
||||||
|
tab: @tab,
|
||||||
return_to: Routes.session_path(@socket, :page, @session_id) %>
|
return_to: Routes.session_path(@socket, :page, @session_id) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
"""
|
"""
|
||||||
|
@ -361,6 +362,10 @@ defmodule LivebookWeb.SessionLive do
|
||||||
{:noreply, handle_relative_path(socket, path)}
|
{:noreply, handle_relative_path(socket, path)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_params(%{"tab" => tab}, _url, socket) do
|
||||||
|
{:noreply, assign(socket, tab: tab)}
|
||||||
|
end
|
||||||
|
|
||||||
def handle_params(_params, _url, socket) do
|
def handle_params(_params, _url, socket) do
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,14 +8,13 @@ defmodule LivebookWeb.SessionLive.ExportComponent do
|
||||||
socket = assign(socket, assigns)
|
socket = assign(socket, assigns)
|
||||||
|
|
||||||
socket =
|
socket =
|
||||||
if socket.assigns[:source] do
|
if socket.assigns[:notebook] do
|
||||||
socket
|
socket
|
||||||
else
|
else
|
||||||
# Note: we need to load the notebook, because the local data
|
# Note: we need to load the notebook, because the local data
|
||||||
# has cell contents stripped out
|
# has cell contents stripped out
|
||||||
notebook = Session.get_notebook(socket.assigns.session_id)
|
notebook = Session.get_notebook(socket.assigns.session_id)
|
||||||
source = Livebook.LiveMarkdown.Export.notebook_to_markdown(notebook)
|
assign(socket, :notebook, notebook)
|
||||||
assign(socket, :source, source)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
{:ok, socket}
|
{:ok, socket}
|
||||||
|
@ -32,38 +31,30 @@ defmodule LivebookWeb.SessionLive.ExportComponent do
|
||||||
<p class="text-gray-700">
|
<p class="text-gray-700">
|
||||||
Here you can preview and directly export the notebook source.
|
Here you can preview and directly export the notebook source.
|
||||||
</p>
|
</p>
|
||||||
<div class="flex flex-col space-y-1">
|
<div class="tabs">
|
||||||
<div class="flex justify-between items-center">
|
<%= live_patch to: Routes.session_path(@socket, :export, @session_id, "livemd"),
|
||||||
<span class="text-sm text-gray-700 font-semibold">
|
class: "tab #{if(@tab == "livemd", do: "active")}" do %>
|
||||||
.livemd
|
<span class="font-medium">
|
||||||
|
Live Markdown
|
||||||
</span>
|
</span>
|
||||||
<div class="flex justify-end space-x-2">
|
<% end %>
|
||||||
<span class="tooltip left" aria-label="Copy source">
|
<%= live_patch to: Routes.session_path(@socket, :export, @session_id, "exs"),
|
||||||
<button class="icon-button"
|
class: "tab #{if(@tab == "exs", do: "active")}" do %>
|
||||||
id="export-notebook-source-clipcopy"
|
<span class="font-medium">
|
||||||
phx-hook="ClipCopy"
|
Elixir Script
|
||||||
data-target-id="export-notebook-source">
|
</span>
|
||||||
<.remix_icon icon="clipboard-line" class="text-lg" />
|
<% end %>
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
<span class="tooltip left" aria-label="Download source">
|
|
||||||
<a class="icon-button"
|
|
||||||
href={Routes.session_path(@socket, :download_source, @session_id)}>
|
|
||||||
<.remix_icon icon="download-2-line" class="text-lg" />
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="markdown">
|
|
||||||
<pre><code
|
|
||||||
class="tiny-scrollbar"
|
|
||||||
id="export-notebook-source"
|
|
||||||
phx-hook="Highlight"
|
|
||||||
data-language="markdown"><%= @source %></code></pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<%= live_component component_for_tab(@tab),
|
||||||
|
session_id: @session_id,
|
||||||
|
notebook: @notebook %>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp component_for_tab("livemd"), do: LivebookWeb.SessionLive.ExportLiveMarkdownComponent
|
||||||
|
defp component_for_tab("exs"), do: LivebookWeb.SessionLive.ExportElixirComponent
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
defmodule LivebookWeb.SessionLive.ExportElixirComponent do
|
||||||
|
use LivebookWeb, :live_component
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def update(assigns, socket) do
|
||||||
|
socket = assign(socket, assigns)
|
||||||
|
|
||||||
|
source = Livebook.Notebook.Export.Elixir.notebook_to_elixir(socket.assigns.notebook)
|
||||||
|
socket = assign(socket, :source, source)
|
||||||
|
|
||||||
|
{:ok, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def render(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div class="flex flex-col space-y-3">
|
||||||
|
<p class="text-gray-700">
|
||||||
|
<span class="font-semibold">Note:</span>
|
||||||
|
the script export is available as a convenience, rather than
|
||||||
|
an exact reproduction of the notebook and in some cases it may
|
||||||
|
not even compile. For example, if you define a macro in one cell
|
||||||
|
and import it in another cell, it works fine in Livebook,
|
||||||
|
because each cell is compiled separately. However, when running
|
||||||
|
the script it gets compiled as a whole and consequently doing so
|
||||||
|
doesn't work. Additionally, branching sections are commented out.
|
||||||
|
</p>
|
||||||
|
<div class="flex flex-col space-y-1">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class="text-sm text-gray-700 font-semibold">
|
||||||
|
.exs
|
||||||
|
</span>
|
||||||
|
<div class="flex justify-end space-x-2">
|
||||||
|
<span class="tooltip left" aria-label="Copy source">
|
||||||
|
<button class="icon-button"
|
||||||
|
id="export-notebook-source-clipcopy"
|
||||||
|
phx-hook="ClipCopy"
|
||||||
|
data-target-id="export-notebook-source">
|
||||||
|
<.remix_icon icon="clipboard-line" class="text-lg" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
<span class="tooltip left" aria-label="Download source">
|
||||||
|
<a class="icon-button"
|
||||||
|
href={Routes.session_path(@socket, :download_source, @session_id, "exs")}>
|
||||||
|
<.remix_icon icon="download-2-line" class="text-lg" />
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="markdown">
|
||||||
|
<pre><code
|
||||||
|
class="tiny-scrollbar"
|
||||||
|
id="export-notebook-source"
|
||||||
|
phx-hook="Highlight"
|
||||||
|
data-language="elixir"><%= @source %></code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,49 @@
|
||||||
|
defmodule LivebookWeb.SessionLive.ExportLiveMarkdownComponent do
|
||||||
|
use LivebookWeb, :live_component
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def update(assigns, socket) do
|
||||||
|
socket = assign(socket, assigns)
|
||||||
|
|
||||||
|
source = Livebook.LiveMarkdown.Export.notebook_to_markdown(socket.assigns.notebook)
|
||||||
|
socket = assign(socket, :source, source)
|
||||||
|
|
||||||
|
{:ok, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def render(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div class="flex flex-col space-y-1">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class="text-sm text-gray-700 font-semibold">
|
||||||
|
.livemd
|
||||||
|
</span>
|
||||||
|
<div class="flex justify-end space-x-2">
|
||||||
|
<span class="tooltip left" aria-label="Copy source">
|
||||||
|
<button class="icon-button"
|
||||||
|
id="export-notebook-source-clipcopy"
|
||||||
|
phx-hook="ClipCopy"
|
||||||
|
data-target-id="export-notebook-source">
|
||||||
|
<.remix_icon icon="clipboard-line" class="text-lg" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
<span class="tooltip left" aria-label="Download source">
|
||||||
|
<a class="icon-button"
|
||||||
|
href={Routes.session_path(@socket, :download_source, @session_id, "livemd")}>
|
||||||
|
<.remix_icon icon="download-2-line" class="text-lg" />
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="markdown">
|
||||||
|
<pre><code
|
||||||
|
class="tiny-scrollbar"
|
||||||
|
id="export-notebook-source"
|
||||||
|
phx-hook="Highlight"
|
||||||
|
data-language="markdown"><%= @source %></code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
end
|
|
@ -34,8 +34,8 @@ defmodule LivebookWeb.Router do
|
||||||
live "/sessions/:id/settings/runtime", SessionLive, :runtime_settings
|
live "/sessions/:id/settings/runtime", SessionLive, :runtime_settings
|
||||||
live "/sessions/:id/settings/file", SessionLive, :file_settings
|
live "/sessions/:id/settings/file", SessionLive, :file_settings
|
||||||
live "/sessions/:id/bin", SessionLive, :bin
|
live "/sessions/:id/bin", SessionLive, :bin
|
||||||
live "/sessions/:id/export", SessionLive, :export
|
get "/sessions/:id/export/download/:format", SessionController, :download_source
|
||||||
get "/sessions/:id/export/download", SessionController, :download_source
|
live "/sessions/:id/export/:tab", SessionLive, :export
|
||||||
live "/sessions/:id/cell-settings/:cell_id", SessionLive, :cell_settings
|
live "/sessions/:id/cell-settings/:cell_id", SessionLive, :cell_settings
|
||||||
live "/sessions/:id/cell-upload/:cell_id", SessionLive, :cell_upload
|
live "/sessions/:id/cell-upload/:cell_id", SessionLive, :cell_upload
|
||||||
live "/sessions/:id/delete-section/:section_id", SessionLive, :delete_section
|
live "/sessions/:id/delete-section/:section_id", SessionLive, :delete_section
|
||||||
|
|
120
test/livebook/notebook/export/elixir_test.exs
Normal file
120
test/livebook/notebook/export/elixir_test.exs
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
defmodule Livebook.Notebook.Export.ElixirTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
alias Livebook.Notebook.Export
|
||||||
|
alias Livebook.Notebook
|
||||||
|
|
||||||
|
test "acceptance" do
|
||||||
|
notebook = %{
|
||||||
|
Notebook.new()
|
||||||
|
| name: "My Notebook",
|
||||||
|
metadata: %{"author" => "Sherlock Holmes"},
|
||||||
|
sections: [
|
||||||
|
%{
|
||||||
|
Notebook.Section.new()
|
||||||
|
| name: "Section 1",
|
||||||
|
metadata: %{"created_at" => "2021-02-15"},
|
||||||
|
cells: [
|
||||||
|
%{
|
||||||
|
Notebook.Cell.new(:markdown)
|
||||||
|
| metadata: %{"updated_at" => "2021-02-15"},
|
||||||
|
source: """
|
||||||
|
Make sure to install:
|
||||||
|
|
||||||
|
* Erlang
|
||||||
|
* Elixir
|
||||||
|
* PostgreSQL\
|
||||||
|
"""
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
Notebook.Cell.new(:elixir)
|
||||||
|
| metadata: %{"readonly" => true},
|
||||||
|
source: """
|
||||||
|
Enum.to_list(1..10)\
|
||||||
|
"""
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
Notebook.Cell.new(:markdown)
|
||||||
|
| metadata: %{},
|
||||||
|
source: """
|
||||||
|
This is it for this section.\
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
Notebook.Section.new()
|
||||||
|
| id: "s2",
|
||||||
|
name: "Section 2",
|
||||||
|
metadata: %{},
|
||||||
|
cells: [
|
||||||
|
%{
|
||||||
|
Notebook.Cell.new(:input)
|
||||||
|
| type: :text,
|
||||||
|
name: "length",
|
||||||
|
value: "100",
|
||||||
|
reactive: true
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
Notebook.Cell.new(:elixir)
|
||||||
|
| metadata: %{},
|
||||||
|
source: """
|
||||||
|
IO.gets("length: ")\
|
||||||
|
"""
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
Notebook.Cell.new(:input)
|
||||||
|
| type: :range,
|
||||||
|
name: "length",
|
||||||
|
value: "100",
|
||||||
|
props: %{min: 50, max: 150, step: 2}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
Notebook.Section.new()
|
||||||
|
| name: "Section 3",
|
||||||
|
metadata: %{},
|
||||||
|
parent_id: "s2",
|
||||||
|
cells: [
|
||||||
|
%{
|
||||||
|
Notebook.Cell.new(:elixir)
|
||||||
|
| metadata: %{},
|
||||||
|
source: """
|
||||||
|
Process.info()\
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
expected_document = """
|
||||||
|
# Title: My Notebook
|
||||||
|
|
||||||
|
# ── Section 1 ──
|
||||||
|
|
||||||
|
# Make sure to install:
|
||||||
|
|
||||||
|
# * Erlang
|
||||||
|
# * Elixir
|
||||||
|
# * PostgreSQL
|
||||||
|
|
||||||
|
Enum.to_list(1..10)
|
||||||
|
|
||||||
|
# This is it for this section.
|
||||||
|
|
||||||
|
# ── Section 2 ──
|
||||||
|
|
||||||
|
IO.gets("length: ")
|
||||||
|
|
||||||
|
# ── Section 3 ── (⎇ from Section 2)
|
||||||
|
|
||||||
|
# Process.info()
|
||||||
|
"""
|
||||||
|
|
||||||
|
document = Export.Elixir.notebook_to_elixir(notebook)
|
||||||
|
|
||||||
|
assert expected_document == document
|
||||||
|
end
|
||||||
|
end
|
|
@ -39,16 +39,25 @@ defmodule LivebookWeb.SessionControllerTest do
|
||||||
|
|
||||||
describe "download_source" do
|
describe "download_source" do
|
||||||
test "returns not found when the given session does not exist", %{conn: conn} do
|
test "returns not found when the given session does not exist", %{conn: conn} do
|
||||||
conn = get(conn, Routes.session_path(conn, :download_source, "nonexistent"))
|
conn = get(conn, Routes.session_path(conn, :download_source, "nonexistent", "livemd"))
|
||||||
|
|
||||||
assert conn.status == 404
|
assert conn.status == 404
|
||||||
assert conn.resp_body == "Not found"
|
assert conn.resp_body == "Not found"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns live markdown notebook source", %{conn: conn} do
|
test "returns bad request when given an invalid format", %{conn: conn} do
|
||||||
{:ok, session_id} = SessionSupervisor.create_session()
|
{:ok, session_id} = SessionSupervisor.create_session()
|
||||||
|
|
||||||
conn = get(conn, Routes.session_path(conn, :download_source, session_id))
|
conn = get(conn, Routes.session_path(conn, :download_source, session_id, "invalid"))
|
||||||
|
|
||||||
|
assert conn.status == 400
|
||||||
|
assert conn.resp_body == "Invalid format, supported formats: livemd, exs"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "handles live markdown notebook source", %{conn: conn} do
|
||||||
|
{:ok, session_id} = SessionSupervisor.create_session()
|
||||||
|
|
||||||
|
conn = get(conn, Routes.session_path(conn, :download_source, session_id, "livemd"))
|
||||||
|
|
||||||
assert conn.status == 200
|
assert conn.status == 200
|
||||||
assert get_resp_header(conn, "content-type") == ["text/plain"]
|
assert get_resp_header(conn, "content-type") == ["text/plain"]
|
||||||
|
@ -59,5 +68,20 @@ defmodule LivebookWeb.SessionControllerTest do
|
||||||
|
|
||||||
SessionSupervisor.close_session(session_id)
|
SessionSupervisor.close_session(session_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "handles elixir notebook source", %{conn: conn} do
|
||||||
|
{:ok, session_id} = SessionSupervisor.create_session()
|
||||||
|
|
||||||
|
conn = get(conn, Routes.session_path(conn, :download_source, session_id, "exs"))
|
||||||
|
|
||||||
|
assert conn.status == 200
|
||||||
|
assert get_resp_header(conn, "content-type") == ["text/plain"]
|
||||||
|
|
||||||
|
assert conn.resp_body == """
|
||||||
|
# Title: Untitled notebook
|
||||||
|
"""
|
||||||
|
|
||||||
|
SessionSupervisor.close_session(session_id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue