mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-09-07 21:44:36 +08:00
* 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
92 lines
2.3 KiB
Elixir
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
|