diff --git a/lib/livebook/live_markdown/export.ex b/lib/livebook/live_markdown/export.ex index 34ae21299..5c5f57138 100644 --- a/lib/livebook/live_markdown/export.ex +++ b/lib/livebook/live_markdown/export.ex @@ -26,9 +26,15 @@ defmodule Livebook.LiveMarkdown.Export do 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(notebook.metadata) + |> prepend_metadata(metadata) + end + + defp notebook_metadata(_notebook) do + %{} end defp render_section(section, notebook, ctx) do @@ -54,19 +60,21 @@ defmodule Livebook.LiveMarkdown.Export do |> prepend_metadata(metadata) end - defp section_metadata(%{parent_id: nil} = section, _notebook) do - section.metadata + 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) - Map.put(section.metadata, "branch_parent_index", parent_idx) + %{"branch_parent_index" => parent_idx} end defp render_cell(%Cell.Markdown{} = cell, _ctx) do + metadata = cell_metadata(cell) + cell.source |> format_markdown_source() - |> prepend_metadata(cell.metadata) + |> prepend_metadata(metadata) end defp render_cell(%Cell.Elixir{} = cell, ctx) do @@ -74,9 +82,11 @@ defmodule Livebook.LiveMarkdown.Export do code = get_elixir_cell_code(cell) outputs = if ctx.include_outputs?, do: render_outputs(cell), else: [] + metadata = cell_metadata(cell) + cell = [delimiter, "elixir\n", code, "\n", delimiter] - |> prepend_metadata(cell.metadata) + |> prepend_metadata(metadata) if outputs == [] do cell @@ -98,10 +108,18 @@ defmodule Livebook.LiveMarkdown.Export do |> put_unless_implicit(reactive: cell.reactive, props: cell.props) |> Jason.encode!() + metadata = cell_metadata(cell) + "" - |> prepend_metadata(cell.metadata) + |> prepend_metadata(metadata) end + defp cell_metadata(%Cell.Elixir{} = cell) do + put_unless_implicit(%{}, disable_formatting: cell.disable_formatting) + end + + defp cell_metadata(_cell), do: %{} + defp render_outputs(cell) do cell.outputs |> Enum.reverse() @@ -125,7 +143,7 @@ defmodule Livebook.LiveMarkdown.Export do defp render_output(_output), do: :ignored - defp get_elixir_cell_code(%{source: source, metadata: %{"disable_formatting" => true}}), + defp get_elixir_cell_code(%{source: source, disable_formatting: true}), do: source defp get_elixir_cell_code(%{source: source}), do: format_code(source) diff --git a/lib/livebook/live_markdown/import.ex b/lib/livebook/live_markdown/import.ex index a8d2f13d2..2d6f38d52 100644 --- a/lib/livebook/live_markdown/import.ex +++ b/lib/livebook/live_markdown/import.ex @@ -200,29 +200,31 @@ defmodule Livebook.LiveMarkdown.Import do defp build_notebook([{:cell, :elixir, source, outputs} | elems], cells, sections) do {metadata, elems} = grab_metadata(elems) + attrs = cell_metadata_to_attrs(:elixir, metadata) outputs = Enum.map(outputs, &{:text, &1}) - cell = %{Notebook.Cell.new(:elixir) | source: source, metadata: metadata, outputs: outputs} + cell = %{Notebook.Cell.new(:elixir) | source: source, outputs: outputs} |> Map.merge(attrs) build_notebook(elems, [cell | cells], sections) end defp build_notebook([{:cell, :markdown, md_ast} | elems], cells, sections) do {metadata, elems} = grab_metadata(elems) + attrs = cell_metadata_to_attrs(:markdown, metadata) source = md_ast |> Enum.reverse() |> MarkdownHelpers.markdown_from_ast() - cell = %{Notebook.Cell.new(:markdown) | source: source, metadata: metadata} + cell = %{Notebook.Cell.new(:markdown) | source: source} |> Map.merge(attrs) build_notebook(elems, [cell | cells], sections) end defp build_notebook([{:cell, :input, data} | elems], cells, sections) do - {metadata, elems} = grab_metadata(elems) attrs = parse_input_attrs(data) - cell = %{Notebook.Cell.new(:input) | metadata: metadata} |> Map.merge(attrs) + cell = Notebook.Cell.new(:input) |> Map.merge(attrs) build_notebook(elems, [cell | cells], sections) end defp build_notebook([{:section_name, content} | elems], cells, sections) do name = MarkdownHelpers.text_from_ast(content) {metadata, elems} = grab_metadata(elems) - section = %{Notebook.Section.new() | name: name, cells: cells, metadata: metadata} + attrs = section_metadata_to_attrs(metadata) + section = %{Notebook.Section.new() | name: name, cells: cells} |> Map.merge(attrs) build_notebook(elems, [], [section | sections]) end @@ -242,7 +244,8 @@ defmodule Livebook.LiveMarkdown.Import do defp build_notebook([{:notebook_name, content} | elems], [], sections) do name = MarkdownHelpers.text_from_ast(content) {metadata, []} = grab_metadata(elems) - %{Notebook.new() | name: name, sections: sections, metadata: metadata} + attrs = notebook_metadata_to_attrs(metadata) + %{Notebook.new() | name: name, sections: sections} |> Map.merge(attrs) end # If there's no explicit notebook heading, use the defaults. @@ -279,13 +282,48 @@ defmodule Livebook.LiveMarkdown.Import do end) end + defp notebook_metadata_to_attrs(_metadata) do + %{} + end + + defp section_metadata_to_attrs(metadata) do + Enum.reduce(metadata, %{}, fn + {"branch_parent_index", parent_idx}, attrs -> + # At this point we cannot extract other section id, + # so we temporarily keep the index + Map.put(attrs, :parent_id, {:idx, parent_idx}) + + _entry, attrs -> + attrs + end) + end + + defp cell_metadata_to_attrs(:elixir, metadata) do + Enum.reduce(metadata, %{}, fn + {"disable_formatting", disable_formatting}, attrs -> + Map.put(attrs, :disable_formatting, disable_formatting) + + _entry, attrs -> + attrs + end) + end + + defp cell_metadata_to_attrs(_type, _metadata) do + %{} + end + defp postprocess_notebook(notebook) do sections = Enum.map(notebook.sections, fn section -> # Set parent_id based on the persisted branch_parent_index if present - {parent_idx, metadata} = Map.pop(section.metadata, "branch_parent_index") - parent = parent_idx && Enum.at(notebook.sections, parent_idx) - %{section | metadata: metadata, parent_id: parent && parent.id} + case section.parent_id do + nil -> + section + + {:idx, parent_idx} -> + parent = Enum.at(notebook.sections, parent_idx) + %{section | parent_id: parent.id} + end end) %{notebook | sections: sections} diff --git a/lib/livebook/notebook.ex b/lib/livebook/notebook.ex index 55cc1268a..2857a2ef4 100644 --- a/lib/livebook/notebook.ex +++ b/lib/livebook/notebook.ex @@ -13,19 +13,16 @@ defmodule Livebook.Notebook do # A notebook is divided into a number of *sections*, each # containing a number of *cells*. - defstruct [:name, :version, :sections, :metadata] + defstruct [:name, :version, :sections] alias Livebook.Notebook.{Section, Cell} alias Livebook.Utils.Graph import Livebook.Utils, only: [access_by_id: 1] - @type metadata :: %{String.t() => term()} - @type t :: %__MODULE__{ name: String.t(), version: String.t(), - sections: list(Section.t()), - metadata: metadata() + sections: list(Section.t()) } @version "1.0" @@ -38,8 +35,7 @@ defmodule Livebook.Notebook do %__MODULE__{ name: "Untitled notebook", version: @version, - sections: [], - metadata: %{} + sections: [] } end diff --git a/lib/livebook/notebook/cell.ex b/lib/livebook/notebook/cell.ex index 90c5e5289..fec0ec961 100644 --- a/lib/livebook/notebook/cell.ex +++ b/lib/livebook/notebook/cell.ex @@ -12,16 +12,6 @@ defmodule Livebook.Notebook.Cell do @type id :: Utils.id() - @typedoc """ - Arbitrary cell information persisted as part of the notebook. - - ## Recognised entries - - * `disable_formatting` - whether this particular cell should - not be automatically formatted. Relevant for Elixir cells only. - """ - @type metadata :: %{String.t() => term()} - @type t :: Cell.Elixir.t() | Cell.Markdown.t() | Cell.Input.t() @type type :: :markdown | :elixir | :input diff --git a/lib/livebook/notebook/cell/elixir.ex b/lib/livebook/notebook/cell/elixir.ex index f9a7aef73..6143daf82 100644 --- a/lib/livebook/notebook/cell/elixir.ex +++ b/lib/livebook/notebook/cell/elixir.ex @@ -6,16 +6,16 @@ defmodule Livebook.Notebook.Cell.Elixir do # It consists of text content that the user can edit # and produces some output once evaluated. - defstruct [:id, :metadata, :source, :outputs] + defstruct [:id, :source, :outputs, :disable_formatting] alias Livebook.Utils alias Livebook.Notebook.Cell @type t :: %__MODULE__{ id: Cell.id(), - metadata: Cell.metadata(), source: String.t(), - outputs: list(output()) + outputs: list(output()), + disable_formatting: boolean() } @typedoc """ @@ -47,9 +47,9 @@ defmodule Livebook.Notebook.Cell.Elixir do def new() do %__MODULE__{ id: Utils.random_id(), - metadata: %{}, source: "", - outputs: [] + outputs: [], + disable_formatting: false } end end diff --git a/lib/livebook/notebook/cell/input.ex b/lib/livebook/notebook/cell/input.ex index 447e1ff9a..29c131b25 100644 --- a/lib/livebook/notebook/cell/input.ex +++ b/lib/livebook/notebook/cell/input.ex @@ -6,14 +6,13 @@ defmodule Livebook.Notebook.Cell.Input do # It consists of an input that the user may fill # and then read during code evaluation. - defstruct [:id, :metadata, :type, :name, :value, :reactive, :props] + defstruct [:id, :type, :name, :value, :reactive, :props] alias Livebook.Utils alias Livebook.Notebook.Cell @type t :: %__MODULE__{ id: Cell.id(), - metadata: Cell.metadata(), type: type(), name: String.t(), value: String.t(), @@ -36,7 +35,6 @@ defmodule Livebook.Notebook.Cell.Input do def new() do %__MODULE__{ id: Utils.random_id(), - metadata: %{}, type: :text, name: "input", value: "", diff --git a/lib/livebook/notebook/cell/markdown.ex b/lib/livebook/notebook/cell/markdown.ex index f5adfaadd..baf25754e 100644 --- a/lib/livebook/notebook/cell/markdown.ex +++ b/lib/livebook/notebook/cell/markdown.ex @@ -6,14 +6,13 @@ defmodule Livebook.Notebook.Cell.Markdown do # It consists of Markdown content that the user can edit # and which is then rendered on the page. - defstruct [:id, :metadata, :source] + defstruct [:id, :source] alias Livebook.Utils alias Livebook.Notebook.Cell @type t :: %__MODULE__{ id: Cell.id(), - metadata: Cell.metadata(), source: String.t() } @@ -24,7 +23,6 @@ defmodule Livebook.Notebook.Cell.Markdown do def new() do %__MODULE__{ id: Utils.random_id(), - metadata: %{}, source: "" } end diff --git a/lib/livebook/notebook/export/elixir.ex b/lib/livebook/notebook/export/elixir.ex index 68167973b..bf30c51bd 100644 --- a/lib/livebook/notebook/export/elixir.ex +++ b/lib/livebook/notebook/export/elixir.ex @@ -65,7 +65,7 @@ defmodule Livebook.Notebook.Export.Elixir do defp comment_out(""), do: "" defp comment_out(line), do: ["# ", line] - defp get_elixir_cell_code(%{source: source, metadata: %{"disable_formatting" => true}}), + defp get_elixir_cell_code(%{source: source, disable_formatting: true}), do: source defp get_elixir_cell_code(%{source: source}), do: format_code(source) diff --git a/lib/livebook/notebook/section.ex b/lib/livebook/notebook/section.ex index 84f8449a7..c80ede3b0 100644 --- a/lib/livebook/notebook/section.ex +++ b/lib/livebook/notebook/section.ex @@ -10,20 +10,18 @@ defmodule Livebook.Notebook.Section do # a branching section. Such section logically follows its # parent section and has no impact on any further sections. - defstruct [:id, :name, :cells, :parent_id, :metadata] + defstruct [:id, :name, :cells, :parent_id] alias Livebook.Notebook.Cell alias Livebook.Utils @type id :: Utils.id() - @type metadata :: %{String.t() => term()} @type t :: %__MODULE__{ id: id(), name: String.t(), cells: list(Cell.t()), - parent_id: id() | nil, - metadata: metadata() + parent_id: id() | nil } @doc """ @@ -35,8 +33,7 @@ defmodule Livebook.Notebook.Section do id: Utils.random_id(), name: "Section", cells: [], - parent_id: nil, - metadata: %{} + parent_id: nil } end end diff --git a/lib/livebook_web/live/session_live/elixir_cell_settings_component.ex b/lib/livebook_web/live/session_live/elixir_cell_settings_component.ex index 2943e0749..f134e0d93 100644 --- a/lib/livebook_web/live/session_live/elixir_cell_settings_component.ex +++ b/lib/livebook_web/live/session_live/elixir_cell_settings_component.ex @@ -5,12 +5,14 @@ defmodule LivebookWeb.SessionLive.ElixirCellSettingsComponent do @impl true def update(assigns, socket) do - metadata = assigns.cell.metadata + cell = assigns.cell - assigns = - Map.merge(assigns, %{disable_formatting: Map.get(metadata, "disable_formatting", false)}) + socket = + socket + |> assign(assigns) + |> assign_new(:disable_formatting, fn -> cell.disable_formatting end) - {:ok, assign(socket, assigns)} + {:ok, socket} end @impl true @@ -39,21 +41,13 @@ defmodule LivebookWeb.SessionLive.ElixirCellSettingsComponent do end @impl true - def handle_event("save", params, socket) do - metadata = update_metadata(socket.assigns.cell.metadata, params) + def handle_event("save", %{"disable_formatting" => disable_formatting}, socket) do + disable_formatting = disable_formatting == "true" Session.set_cell_attributes(socket.assigns.session_id, socket.assigns.cell.id, %{ - metadata: metadata + disable_formatting: disable_formatting }) {:noreply, push_patch(socket, to: socket.assigns.return_to)} end - - defp update_metadata(metadata, form_data) do - if form_data["disable_formatting"] == "true" do - Map.put(metadata, "disable_formatting", true) - else - Map.delete(metadata, "disable_formatting") - end - end end diff --git a/test/livebook/live_markdown/export_test.exs b/test/livebook/live_markdown/export_test.exs index 810745444..c3e8ce27b 100644 --- a/test/livebook/live_markdown/export_test.exs +++ b/test/livebook/live_markdown/export_test.exs @@ -8,17 +8,14 @@ defmodule Livebook.LiveMarkdown.ExportTest 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: """ + | source: """ Make sure to install: * Erlang @@ -28,7 +25,7 @@ defmodule Livebook.LiveMarkdown.ExportTest do }, %{ Notebook.Cell.new(:elixir) - | metadata: %{"readonly" => true}, + | disable_formatting: true, source: """ Enum.to_list(1..10)\ """ @@ -85,23 +82,17 @@ defmodule Livebook.LiveMarkdown.ExportTest do } expected_document = """ - - # My Notebook - - ## Section 1 - - Make sure to install: * Erlang * Elixir * PostgreSQL - + ```elixir Enum.to_list(1..10) @@ -355,7 +346,7 @@ defmodule Livebook.LiveMarkdown.ExportTest do assert expected_document == document end - test "does not format code in Elixir cells which explicitly state so in metadata" do + test "does not format code in Elixir cells which have formatting disabled" do notebook = %{ Notebook.new() | name: "My Notebook", @@ -366,7 +357,7 @@ defmodule Livebook.LiveMarkdown.ExportTest do cells: [ %{ Notebook.Cell.new(:elixir) - | metadata: %{"disable_formatting" => true}, + | disable_formatting: true, source: """ [1,2,3] # Comment\ """ diff --git a/test/livebook/live_markdown/import_test.exs b/test/livebook/live_markdown/import_test.exs index 75ccf803b..350d6d2eb 100644 --- a/test/livebook/live_markdown/import_test.exs +++ b/test/livebook/live_markdown/import_test.exs @@ -7,23 +7,17 @@ defmodule Livebook.LiveMarkdown.ImportTest do test "acceptance" do markdown = """ - - # My Notebook - - ## Section 1 - - Make sure to install: * Erlang * Elixir * PostgreSQL - + ```elixir Enum.to_list(1..10) @@ -56,14 +50,11 @@ defmodule Livebook.LiveMarkdown.ImportTest do assert %Notebook{ name: "My Notebook", - metadata: %{"author" => "Sherlock Holmes"}, sections: [ %Notebook.Section{ name: "Section 1", - metadata: %{"created_at" => "2021-02-15"}, cells: [ %Cell.Markdown{ - metadata: %{"updated_at" => "2021-02-15"}, source: """ Make sure to install: @@ -73,13 +64,12 @@ defmodule Livebook.LiveMarkdown.ImportTest do """ }, %Cell.Elixir{ - metadata: %{"readonly" => true}, + disable_formatting: true, source: """ Enum.to_list(1..10)\ """ }, %Cell.Markdown{ - metadata: %{}, source: """ This is it for this section.\ """ @@ -89,23 +79,19 @@ defmodule Livebook.LiveMarkdown.ImportTest do %Notebook.Section{ id: section2_id, name: "Section 2", - metadata: %{}, cells: [ %Cell.Input{ - metadata: %{}, type: :text, name: "length", value: "100", reactive: true }, %Cell.Elixir{ - metadata: %{}, source: """ IO.gets("length: ")\ """ }, %Cell.Input{ - metadata: %{}, type: :range, name: "length", value: "100", @@ -115,11 +101,9 @@ defmodule Livebook.LiveMarkdown.ImportTest do }, %Notebook.Section{ name: "Section 3", - metadata: %{}, parent_id: section2_id, cells: [ %Cell.Elixir{ - metadata: %{}, source: """ Process.info()\ """ @@ -152,10 +136,8 @@ defmodule Livebook.LiveMarkdown.ImportTest do sections: [ %Notebook.Section{ name: "Section 1", - metadata: %{}, cells: [ %Cell.Markdown{ - metadata: %{}, source: """ Line 1.\\ Line 2. @@ -213,7 +195,6 @@ defmodule Livebook.LiveMarkdown.ImportTest do name: "Probably section 1", cells: [ %Cell.Markdown{ - metadata: %{}, source: """ ### Heading @@ -226,7 +207,6 @@ defmodule Livebook.LiveMarkdown.ImportTest do name: "Probably section 2", cells: [ %Cell.Markdown{ - metadata: %{}, source: """ **Tiny heading**\ """ @@ -330,7 +310,6 @@ defmodule Livebook.LiveMarkdown.ImportTest do assert %Notebook{ name: "My Notebook", - metadata: %{"author" => "Sherlock Holmes"}, sections: [ %Notebook.Section{ name: "Section", diff --git a/test/livebook/notebook/export/elixir_test.exs b/test/livebook/notebook/export/elixir_test.exs index 5ac8d5c7b..b0c3f1aa4 100644 --- a/test/livebook/notebook/export/elixir_test.exs +++ b/test/livebook/notebook/export/elixir_test.exs @@ -8,17 +8,14 @@ defmodule Livebook.Notebook.Export.ElixirTest 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: """ + | source: """ Make sure to install: * Erlang @@ -28,15 +25,14 @@ defmodule Livebook.Notebook.Export.ElixirTest do }, %{ Notebook.Cell.new(:elixir) - | metadata: %{"readonly" => true}, + | disable_formatting: true, source: """ Enum.to_list(1..10)\ """ }, %{ Notebook.Cell.new(:markdown) - | metadata: %{}, - source: """ + | source: """ This is it for this section.\ """ } @@ -46,7 +42,6 @@ defmodule Livebook.Notebook.Export.ElixirTest do Notebook.Section.new() | id: "s2", name: "Section 2", - metadata: %{}, cells: [ %{ Notebook.Cell.new(:input) @@ -57,8 +52,7 @@ defmodule Livebook.Notebook.Export.ElixirTest do }, %{ Notebook.Cell.new(:elixir) - | metadata: %{}, - source: """ + | source: """ IO.gets("length: ")\ """ }, @@ -74,13 +68,11 @@ defmodule Livebook.Notebook.Export.ElixirTest do %{ Notebook.Section.new() | name: "Section 3", - metadata: %{}, parent_id: "s2", cells: [ %{ Notebook.Cell.new(:elixir) - | metadata: %{}, - source: """ + | source: """ Process.info()\ """ } diff --git a/test/livebook/session/data_test.exs b/test/livebook/session/data_test.exs index 1269936f7..68a3b619d 100644 --- a/test/livebook/session/data_test.exs +++ b/test/livebook/session/data_test.exs @@ -2782,14 +2782,13 @@ defmodule Livebook.Session.DataTest do {:insert_cell, self(), "s1", 0, :elixir, "c1"} ]) - metadata = %{"disable_formatting" => true} - attrs = %{metadata: metadata} + attrs = %{disable_formatting: true} operation = {:set_cell_attributes, self(), "c1", attrs} assert {:ok, %{ notebook: %{ - sections: [%{cells: [%{metadata: ^metadata}]}] + sections: [%{cells: [%{disable_formatting: true}]}] } }, _} = Data.apply_operation(data, operation) end diff --git a/test/livebook/session_test.exs b/test/livebook/session_test.exs index f76b4e720..80af863d5 100644 --- a/test/livebook/session_test.exs +++ b/test/livebook/session_test.exs @@ -173,7 +173,7 @@ defmodule Livebook.SessionTest do pid = self() {_section_id, cell_id} = insert_section_and_cell(session_id) - attrs = %{metadata: %{"disable_formatting" => true}} + attrs = %{disable_formatting: true} Session.set_cell_attributes(session_id, cell_id, attrs) assert_receive {:operation, {:set_cell_attributes, ^pid, ^cell_id, ^attrs}}