Packages
livebook
0.7.1
0.19.8
0.19.7
0.19.6
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.6
0.18.5
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
0.17.3
0.17.2
0.17.1
0.17.0
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.7
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.14.0-rc.1
0.14.0-rc.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.1
0.12.0
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.3
0.9.2
0.9.1
0.9.0
0.8.2
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.2
0.3.1
0.3.0
0.2.3
0.2.2
0.2.1
0.2.0
0.1.2
0.1.1
0.1.0
Automate code & data workflows with interactive notebooks
Current section
Files
Jump to
Current section
Files
lib/livebook/live_markdown/export.ex
defmodule Livebook.LiveMarkdown.Export do
alias Livebook.Notebook
alias Livebook.Notebook.Cell
alias Livebook.LiveMarkdown.MarkdownHelpers
def notebook_to_livemd(notebook, opts \\ []) do
include_outputs? = Keyword.get(opts, :include_outputs, notebook.persist_outputs)
js_ref_with_data = if include_outputs?, do: collect_js_output_data(notebook), else: %{}
ctx = %{include_outputs?: include_outputs?, js_ref_with_data: js_ref_with_data}
iodata = render_notebook(notebook, ctx)
# Add trailing newline
IO.iodata_to_binary([iodata, "\n"])
end
defp collect_js_output_data(notebook) do
for section <- notebook.sections,
%{outputs: outputs} <- section.cells,
{_idx, {:js, %{js_view: %{ref: ref, pid: pid}, export: %{}}}} <- outputs do
Task.async(fn ->
{ref, get_js_output_data(pid, ref)}
end)
end
|> Task.await_many(:infinity)
|> Map.new()
end
defp get_js_output_data(pid, ref) do
send(pid, {:connect, self(), %{origin: self(), ref: ref}})
monitor_ref = Process.monitor(pid)
data =
receive do
{:connect_reply, data, %{ref: ^ref}} -> data
{:DOWN, ^monitor_ref, :process, _pid, _reason} -> nil
end
Process.demonitor(monitor_ref, [:flush])
data
end
defp render_notebook(notebook, ctx) do
%{setup_section: %{cells: [setup_cell]}} = notebook
comments =
Enum.map(notebook.leading_comments, fn
[line] -> ["<!-- ", line, " -->"]
lines -> ["<!--\n", Enum.intersperse(lines, "\n"), "\n-->"]
end)
name = ["# ", notebook.name]
setup_cell = render_setup_cell(setup_cell, ctx)
sections = Enum.map(notebook.sections, &render_section(&1, notebook, ctx))
metadata = notebook_metadata(notebook)
notebook_with_metadata =
[name, setup_cell | sections]
|> Enum.reject(&is_nil/1)
|> Enum.intersperse("\n\n")
|> prepend_metadata(metadata)
Enum.intersperse(comments ++ [notebook_with_metadata], "\n\n")
end
defp notebook_metadata(notebook) do
put_unless_default(
%{},
Map.take(notebook, [:persist_outputs, :autosave_interval_s]),
Map.take(Notebook.new(), [:persist_outputs, :autosave_interval_s])
)
end
defp render_section(section, notebook, ctx) do
name = ["## ", section.name]
{cells, _} =
Enum.map_reduce(section.cells, nil, fn cell, prev_cell ->
separator =
if is_struct(cell, Cell.Markdown) and is_struct(prev_cell, Cell.Markdown) do
[~s/<!-- livebook:{"break_markdown":true} -->\n\n/]
else
[]
end
rendered = separator ++ [render_cell(cell, ctx)]
{rendered, cell}
end)
metadata = section_metadata(section, notebook)
[name | cells]
|> Enum.intersperse("\n\n")
|> prepend_metadata(metadata)
end
defp section_metadata(%{parent_id: nil} = _section, _notebook) do
%{}
end
defp section_metadata(section, notebook) do
parent_idx = Notebook.section_index(notebook, section.parent_id)
%{"branch_parent_index" => parent_idx}
end
defp render_setup_cell(%{source: ""}, _ctx), do: nil
defp render_setup_cell(cell, ctx), do: render_cell(cell, ctx)
defp render_cell(%Cell.Markdown{} = cell, _ctx) do
metadata = cell_metadata(cell)
cell.source
|> format_markdown_source()
|> prepend_metadata(metadata)
end
defp render_cell(%Cell.Code{} = cell, ctx) do
delimiter = MarkdownHelpers.code_block_delimiter(cell.source)
code = get_code_cell_code(cell)
outputs = if ctx.include_outputs?, do: render_outputs(cell, ctx), else: []
metadata = cell_metadata(cell)
cell =
[delimiter, "elixir\n", code, "\n", delimiter]
|> prepend_metadata(metadata)
if outputs == [] do
cell
else
[cell, "\n\n", outputs]
end
end
defp render_cell(%Cell.Smart{} = cell, ctx) do
%{Cell.Code.new() | source: cell.source, outputs: cell.outputs}
|> render_cell(ctx)
|> prepend_metadata(%{
"livebook_object" => "smart_cell",
"kind" => cell.kind,
"attrs" => cell.attrs
})
end
defp cell_metadata(%Cell.Code{} = cell) do
put_unless_default(
%{},
Map.take(cell, [:disable_formatting, :reevaluate_automatically]),
Map.take(Cell.Code.new(), [:disable_formatting, :reevaluate_automatically])
)
end
defp cell_metadata(_cell), do: %{}
defp render_outputs(cell, ctx) do
cell.outputs
|> Enum.reverse()
|> Enum.map(fn {_idx, output} -> render_output(output, ctx) end)
|> Enum.reject(&(&1 == :ignored))
|> Enum.intersperse("\n\n")
end
defp render_output({:stdout, text}, _ctx) do
text = String.replace_suffix(text, "\n", "")
delimiter = MarkdownHelpers.code_block_delimiter(text)
text = strip_ansi(text)
[delimiter, "\n", text, "\n", delimiter]
|> prepend_metadata(%{output: true})
end
defp render_output({:text, text}, _ctx) do
delimiter = MarkdownHelpers.code_block_delimiter(text)
text = strip_ansi(text)
[delimiter, "\n", text, "\n", delimiter]
|> prepend_metadata(%{output: true})
end
defp render_output(
{:js, %{export: %{info_string: info_string, key: key}, js_view: %{ref: ref}}},
ctx
)
when is_binary(info_string) do
data = ctx.js_ref_with_data[ref]
payload = if key && is_map(data), do: data[key], else: data
case encode_js_data(payload) do
{:ok, binary} ->
delimiter = MarkdownHelpers.code_block_delimiter(binary)
[delimiter, info_string, "\n", binary, "\n", delimiter]
|> prepend_metadata(%{output: true})
_ ->
:ignored
end
end
defp render_output({:tabs, outputs, _info}, ctx) do
Enum.find_value(outputs, :ignored, fn {_idx, output} ->
case render_output(output, ctx) do
:ignored -> nil
rendered -> rendered
end
end)
end
defp render_output({:grid, outputs, _info}, ctx) do
outputs
|> Enum.map(fn {_idx, output} -> render_output(output, ctx) end)
|> Enum.reject(&(&1 == :ignored))
|> case do
[] -> :ignored
rendered -> Enum.intersperse(rendered, "\n\n")
end
end
defp render_output(_output, _ctx), do: :ignored
defp encode_js_data(data) when is_binary(data), do: {:ok, data}
defp encode_js_data(data), do: Jason.encode(data)
defp get_code_cell_code(%{source: source, disable_formatting: true}),
do: source
defp get_code_cell_code(%{source: source}), do: format_code(source)
defp render_metadata(metadata) do
metadata_json = Jason.encode!(metadata)
"<!-- livebook:#{metadata_json} -->"
end
defp prepend_metadata(iodata, metadata) when metadata == %{}, do: iodata
defp prepend_metadata(iodata, metadata) do
content = render_metadata(metadata)
[content, "\n\n", iodata]
end
defp format_markdown_source(markdown) do
markdown
|> MarkdownHelpers.markdown_to_block_ast()
|> elem(1)
|> rewrite_ast()
|> MarkdownHelpers.markdown_from_ast()
end
# Alters AST of the user-entered markdown.
defp rewrite_ast(ast) do
ast
|> remove_reserved_headings()
|> add_markdown_annotation_before_elixir_block()
end
defp remove_reserved_headings(ast) do
Enum.filter(ast, fn
{"h1", _, _, _} -> false
{"h2", _, _, _} -> false
_ast_node -> true
end)
end
defp add_markdown_annotation_before_elixir_block(ast) do
Enum.flat_map(ast, fn
{"pre", _, [{"code", [{"class", "elixir"}], [_source], %{}}], %{}} = ast_node ->
[{:comment, [], [~s/livebook:{"force_markdown":true}/], %{comment: true}}, ast_node]
ast_node ->
[ast_node]
end)
end
defp format_code(code) do
try do
Code.format_string!(code)
rescue
_ -> code
end
end
defp put_unless_default(map, entries, defaults) do
Enum.reduce(entries, map, fn {key, value}, map ->
if value == defaults[key] do
map
else
Map.put(map, key, value)
end
end)
end
defp strip_ansi(string) do
string
|> Livebook.Utils.ANSI.parse_ansi_string()
|> elem(0)
|> Enum.map(fn {_modifiers, string} -> string end)
end
end