livebook/lib/live_book/live_markdown/export.ex
Jonatan Kłosko 9fed524ed5
Markdown snippets (#56)
* Extend LiveMarkdown format to support Elixir snippets in Markdown cells

* Highlight Markdown code blocks using Monaco editor API

* Use livebook metadata for forcing markdown as well
2021-02-24 15:41:00 +01:00

92 lines
2.3 KiB
Elixir

defmodule LiveBook.LiveMarkdown.Export do
alias LiveBook.Notebook
alias LiveBook.LiveMarkdown.MarkdownHelpers
@doc """
Converts the given notebook into a Markdown document.
"""
@spec notebook_to_markdown(Notebook.t()) :: String.t()
def notebook_to_markdown(notebook) do
iodata = render_notebook(notebook)
# Add trailing newline
IO.iodata_to_binary([iodata, "\n"])
end
defp render_notebook(notebook) do
name = "# #{notebook.name}"
sections = Enum.map(notebook.sections, &render_section/1)
[name | sections]
|> Enum.intersperse("\n\n")
|> prepend_metadata(notebook.metadata)
end
defp render_section(section) do
name = "## #{section.name}"
cells = Enum.map(section.cells, &render_cell/1)
[name | cells]
|> Enum.intersperse("\n\n")
|> prepend_metadata(section.metadata)
end
defp render_cell(%{type: :markdown} = cell) do
cell.source
|> format_markdown_source()
|> prepend_metadata(cell.metadata)
end
defp render_cell(%{type: :elixir} = cell) do
"""
```elixir
#{cell.source}
```\
"""
|> prepend_metadata(cell.metadata)
end
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
|> EarmarkParser.as_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
end