mirror of
				https://github.com/livebook-dev/livebook.git
				synced 2025-10-31 15:56:05 +08:00 
			
		
		
		
	Document how to render notebooks on GitHub (#677)
* Document how to render notebooks on GitHub * Keep any comments before the notebook title heading
This commit is contained in:
		
							parent
							
								
									a15ec1ca1d
								
							
						
					
					
						commit
						5e5bc2597a
					
				
					 8 changed files with 196 additions and 34 deletions
				
			
		
							
								
								
									
										18
									
								
								README.md
									
										
									
									
									
								
							
							
						
						
									
										18
									
								
								README.md
									
										
									
									
									
								
							|  | @ -181,6 +181,24 @@ variables used by Elixir releases are also available]( | |||
| https://hexdocs.pm/mix/Mix.Tasks.Release.html#module-environment-variables). | ||||
| The notables ones are `RELEASE_NODE` and `RELEASE_DISTRIBUTION`. | ||||
| 
 | ||||
| ## Rendering notebooks as Markdown on GitHub | ||||
| 
 | ||||
| By default GitHub renders the `.livemd` notebooks as regular text files. Depending | ||||
| on your use case and the target audience, you may find it useful to render notebooks | ||||
| content as Markdown files instead. There is an option to override how a particular | ||||
| file gets rendered on GitHub, so all you need to do is add a magic comment in every | ||||
| such notebook: | ||||
| 
 | ||||
| ``` | ||||
| <!-- vim: syntax=markdown --> | ||||
| 
 | ||||
| # My notebook | ||||
| 
 | ||||
| ... | ||||
| ``` | ||||
| 
 | ||||
| For more details see [the documentation](https://github.com/github/linguist/blob/master/docs/overrides.md#using-emacs-or-vim-modelines). | ||||
| 
 | ||||
| ## Development | ||||
| 
 | ||||
| Livebook is primarily a Phoenix web application and can be setup as such: | ||||
|  |  | |||
|  | @ -40,6 +40,8 @@ defmodule Livebook.LiveMarkdown do | |||
|   # | ||||
|   #        - cell metadata, recognised keys: `disable_formatting` | ||||
|   # | ||||
|   #   6. Any comments before the leading heading are kept. | ||||
|   # | ||||
|   # ## Example | ||||
|   # | ||||
|   # Here's an example LiveMarkdown file: | ||||
|  |  | |||
|  | @ -24,14 +24,23 @@ defmodule Livebook.LiveMarkdown.Export do | |||
|   end | ||||
| 
 | ||||
|   defp render_notebook(notebook, ctx) do | ||||
|     comments = | ||||
|       Enum.map(notebook.leading_comments, fn | ||||
|         [line] -> ["<!-- ", line, " -->"] | ||||
|         lines -> ["<!--\n", Enum.intersperse(lines, "\n"), "\n-->"] | ||||
|       end) | ||||
| 
 | ||||
|     name = ["# ", notebook.name] | ||||
|     sections = Enum.map(notebook.sections, &render_section(&1, notebook, ctx)) | ||||
| 
 | ||||
|     metadata = notebook_metadata(notebook) | ||||
| 
 | ||||
|     [name | sections] | ||||
|     |> Enum.intersperse("\n\n") | ||||
|     |> prepend_metadata(metadata) | ||||
|     notebook_with_metadata = | ||||
|       [name | sections] | ||||
|       |> Enum.intersperse("\n\n") | ||||
|       |> prepend_metadata(metadata) | ||||
| 
 | ||||
|     Enum.intersperse(comments ++ [notebook_with_metadata], "\n\n") | ||||
|   end | ||||
| 
 | ||||
|   defp notebook_metadata(notebook) do | ||||
|  |  | |||
|  | @ -30,7 +30,7 @@ defmodule Livebook.LiveMarkdown.Import do | |||
|   defp rewrite_ast(ast) do | ||||
|     {ast, messages1} = rewrite_multiple_primary_headings(ast) | ||||
|     {ast, messages2} = move_primary_heading_top(ast) | ||||
|     ast = trim_comments(ast) | ||||
|     ast = normalize_comments(ast) | ||||
| 
 | ||||
|     {ast, messages1 ++ messages2} | ||||
|   end | ||||
|  | @ -91,12 +91,12 @@ defmodule Livebook.LiveMarkdown.Import do | |||
|     {Enum.reverse(left_rev), Enum.reverse(right_rev)} | ||||
|   end | ||||
| 
 | ||||
|   # Trims one-line comments to allow nice pattern matching | ||||
|   # on Livebook-specific annotations with no regard to surrounding whitespace. | ||||
|   defp trim_comments(ast) do | ||||
|   # Normalizes comments to allow nice pattern matching on Livebook-specific | ||||
|   # annotations with no regard to surrounding whitespace. | ||||
|   defp normalize_comments(ast) do | ||||
|     Enum.map(ast, fn | ||||
|       {:comment, attrs, [line], %{comment: true}} -> | ||||
|         {:comment, attrs, [String.trim(line)], %{comment: true}} | ||||
|       {:comment, attrs, lines, %{comment: true}} -> | ||||
|         {:comment, attrs, MarkdownHelpers.normalize_comment_lines(lines), %{comment: true}} | ||||
| 
 | ||||
|       ast_node -> | ||||
|         ast_node | ||||
|  | @ -254,9 +254,15 @@ defmodule Livebook.LiveMarkdown.Import do | |||
| 
 | ||||
|   defp build_notebook([{:notebook_name, content} | elems], [], sections, messages) do | ||||
|     name = text_from_markdown(content) | ||||
|     {metadata, []} = grab_metadata(elems) | ||||
|     {metadata, elems} = grab_metadata(elems) | ||||
|     # If there are any non-metadata comments we keep them | ||||
|     {comments, []} = grab_leading_comments(elems) | ||||
|     attrs = notebook_metadata_to_attrs(metadata) | ||||
|     notebook = %{Notebook.new() | name: name, sections: sections} |> Map.merge(attrs) | ||||
| 
 | ||||
|     notebook = | ||||
|       %{Notebook.new() | name: name, sections: sections, leading_comments: comments} | ||||
|       |> Map.merge(attrs) | ||||
| 
 | ||||
|     {notebook, messages} | ||||
|   end | ||||
| 
 | ||||
|  | @ -280,6 +286,15 @@ defmodule Livebook.LiveMarkdown.Import do | |||
| 
 | ||||
|   defp grab_metadata(elems), do: {%{}, elems} | ||||
| 
 | ||||
|   defp grab_leading_comments([]), do: {[], []} | ||||
| 
 | ||||
|   # Since these are not metadata comments they get wrapped in a markdown cell, | ||||
|   # so we unpack it | ||||
|   defp grab_leading_comments([{:cell, :markdown, md_ast}]) do | ||||
|     comments = for {:comment, _attrs, lines, %{comment: true}} <- Enum.reverse(md_ast), do: lines | ||||
|     {comments, []} | ||||
|   end | ||||
| 
 | ||||
|   defp parse_input_attrs(data) do | ||||
|     with {:ok, type} <- parse_input_type(data["type"]) do | ||||
|       warnings = | ||||
|  |  | |||
|  | @ -60,6 +60,27 @@ defmodule Livebook.LiveMarkdown.MarkdownHelpers do | |||
|     String.duplicate("`", max_streak + 1) | ||||
|   end | ||||
| 
 | ||||
|   @doc """ | ||||
|   Normalizes comments. | ||||
| 
 | ||||
|   In single-line comments trims all the whitespace and in multi-line | ||||
|   comments removes trailing/leading blank newlines. | ||||
|   """ | ||||
|   @spec normalize_comment_lines(list(String.t())) :: list(String.t()) | ||||
|   def normalize_comment_lines(lines) | ||||
| 
 | ||||
|   def normalize_comment_lines([line]) do | ||||
|     [String.trim(line)] | ||||
|   end | ||||
| 
 | ||||
|   def normalize_comment_lines(lines) do | ||||
|     lines | ||||
|     |> Enum.drop_while(&blank?/1) | ||||
|     |> Enum.reverse() | ||||
|     |> Enum.drop_while(&blank?/1) | ||||
|     |> Enum.reverse() | ||||
|   end | ||||
| 
 | ||||
|   @doc """ | ||||
|   Renders Markdown string from the given `EarmarkParser` AST. | ||||
|   """ | ||||
|  | @ -240,20 +261,11 @@ defmodule Livebook.LiveMarkdown.MarkdownHelpers do | |||
|     end | ||||
|   end | ||||
| 
 | ||||
|   defp render_comment([line]) do | ||||
|     line = String.trim(line) | ||||
|     ["<!-- ", line, " -->"] | ||||
|   end | ||||
| 
 | ||||
|   defp render_comment(lines) do | ||||
|     lines = | ||||
|       lines | ||||
|       |> Enum.drop_while(&blank?/1) | ||||
|       |> Enum.reverse() | ||||
|       |> Enum.drop_while(&blank?/1) | ||||
|       |> Enum.reverse() | ||||
| 
 | ||||
|     ["<!--\n", Enum.intersperse(lines, "\n"), "\n-->"] | ||||
|     case normalize_comment_lines(lines) do | ||||
|       [line] -> ["<!-- ", line, " -->"] | ||||
|       lines -> ["<!--\n", Enum.intersperse(lines, "\n"), "\n-->"] | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   defp render_ruler(attrs) do | ||||
|  |  | |||
|  | @ -13,7 +13,14 @@ defmodule Livebook.Notebook do | |||
|   # A notebook is divided into a number of *sections*, each | ||||
|   # containing a number of *cells*. | ||||
| 
 | ||||
|   defstruct [:name, :version, :sections, :persist_outputs, :autosave_interval_s] | ||||
|   defstruct [ | ||||
|     :name, | ||||
|     :version, | ||||
|     :sections, | ||||
|     :leading_comments, | ||||
|     :persist_outputs, | ||||
|     :autosave_interval_s | ||||
|   ] | ||||
| 
 | ||||
|   alias Livebook.Notebook.{Section, Cell} | ||||
|   alias Livebook.Utils.Graph | ||||
|  | @ -23,6 +30,7 @@ defmodule Livebook.Notebook do | |||
|           name: String.t(), | ||||
|           version: String.t(), | ||||
|           sections: list(Section.t()), | ||||
|           leading_comments: list(list(line :: String.t())), | ||||
|           persist_outputs: boolean(), | ||||
|           autosave_interval_s: non_neg_integer() | nil | ||||
|         } | ||||
|  | @ -38,6 +46,7 @@ defmodule Livebook.Notebook do | |||
|       name: "Untitled notebook", | ||||
|       version: @version, | ||||
|       sections: [], | ||||
|       leading_comments: [], | ||||
|       persist_outputs: default_persist_outputs(), | ||||
|       autosave_interval_s: default_autosave_interval_s() | ||||
|     } | ||||
|  |  | |||
|  | @ -515,6 +515,56 @@ defmodule Livebook.LiveMarkdown.ExportTest do | |||
|     assert expected_document == document | ||||
|   end | ||||
| 
 | ||||
|   test "exports leading notebook comments" do | ||||
|     notebook = %{ | ||||
|       Notebook.new() | ||||
|       | name: "My Notebook", | ||||
|         persist_outputs: true, | ||||
|         leading_comments: [ | ||||
|           ["vim: syntax=markdown"], | ||||
|           ["nowhitespace"], | ||||
|           ["  Multi", "  line"] | ||||
|         ], | ||||
|         sections: [ | ||||
|           %{ | ||||
|             Notebook.Section.new() | ||||
|             | name: "Section 1", | ||||
|               cells: [ | ||||
|                 %{ | ||||
|                   Notebook.Cell.new(:markdown) | ||||
|                   | source: """ | ||||
|                     Cell 1\ | ||||
|                     """ | ||||
|                 } | ||||
|               ] | ||||
|           } | ||||
|         ] | ||||
|     } | ||||
| 
 | ||||
|     expected_document = """ | ||||
|     <!-- vim: syntax=markdown --> | ||||
| 
 | ||||
|     <!-- nowhitespace --> | ||||
| 
 | ||||
|     <!-- | ||||
|       Multi | ||||
|       line | ||||
|     --> | ||||
| 
 | ||||
|     <!-- livebook:{"persist_outputs":true} --> | ||||
| 
 | ||||
|     # My Notebook | ||||
| 
 | ||||
|     ## Section 1 | ||||
| 
 | ||||
|     Cell 1 | ||||
|     """ | ||||
| 
 | ||||
|     document = Export.notebook_to_markdown(notebook) | ||||
| 
 | ||||
|     assert expected_document == document | ||||
|   end | ||||
| 
 | ||||
|   describe "outputs" do | ||||
|     test "does not include outputs by default" do | ||||
|       notebook = %{ | ||||
|  |  | |||
|  | @ -517,6 +517,51 @@ defmodule Livebook.LiveMarkdown.ImportTest do | |||
|            } = notebook | ||||
|   end | ||||
| 
 | ||||
|   test "imports comments preceding the notebook title" do | ||||
|     markdown = """ | ||||
|     <!-- vim: syntax=markdown --> | ||||
| 
 | ||||
|     <!--nowhitespace--> | ||||
| 
 | ||||
|     <!-- | ||||
|       Multi | ||||
|       line | ||||
|     --> | ||||
| 
 | ||||
|     <!-- livebook:{"persist_outputs":true} --> | ||||
| 
 | ||||
|     # My Notebook | ||||
| 
 | ||||
|     ## Section 1 | ||||
| 
 | ||||
|     Cell 1 | ||||
|     """ | ||||
| 
 | ||||
|     {notebook, []} = Import.notebook_from_markdown(markdown) | ||||
| 
 | ||||
|     assert %Notebook{ | ||||
|              name: "My Notebook", | ||||
|              persist_outputs: true, | ||||
|              leading_comments: [ | ||||
|                ["vim: syntax=markdown"], | ||||
|                ["nowhitespace"], | ||||
|                ["  Multi", "  line"] | ||||
|              ], | ||||
|              sections: [ | ||||
|                %Notebook.Section{ | ||||
|                  name: "Section 1", | ||||
|                  cells: [ | ||||
|                    %Cell.Markdown{ | ||||
|                      source: """ | ||||
|                      Cell 1\ | ||||
|                      """ | ||||
|                    } | ||||
|                  ] | ||||
|                } | ||||
|              ] | ||||
|            } = notebook | ||||
|   end | ||||
| 
 | ||||
|   describe "outputs" do | ||||
|     test "imports output snippets as cell textual outputs" do | ||||
|       markdown = """ | ||||
|  | @ -697,18 +742,20 @@ defmodule Livebook.LiveMarkdown.ImportTest do | |||
|   end | ||||
| 
 | ||||
|   describe "backward compatibility" do | ||||
|     markdown = """ | ||||
|     # My Notebook | ||||
|     test "warns if the imported notebook includes a reactive input" do | ||||
|       markdown = """ | ||||
|       # My Notebook | ||||
| 
 | ||||
|     ## Section 1 | ||||
|       ## Section 1 | ||||
| 
 | ||||
|     <!-- livebook:{"livebook_object":"cell_input","name":"length","reactive":true,"type":"text","value":"100"} --> | ||||
|     """ | ||||
|       <!-- livebook:{"livebook_object":"cell_input","name":"length","reactive":true,"type":"text","value":"100"} --> | ||||
|       """ | ||||
| 
 | ||||
|     {_notebook, messages} = Import.notebook_from_markdown(markdown) | ||||
|       {_notebook, messages} = Import.notebook_from_markdown(markdown) | ||||
| 
 | ||||
|     assert [ | ||||
|              "found a reactive input, but those are no longer supported, you can use automatically reevaluating cell instead" | ||||
|            ] == messages | ||||
|       assert [ | ||||
|                "found a reactive input, but those are no longer supported, you can use automatically reevaluating cell instead" | ||||
|              ] == messages | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
		Loading…
	
	Add table
		
		Reference in a new issue