diff --git a/lib/livebook/live_markdown/export.ex b/lib/livebook/live_markdown/export.ex index d236b64974e..660c9a0ff7d 100644 --- a/lib/livebook/live_markdown/export.ex +++ b/lib/livebook/live_markdown/export.ex @@ -153,6 +153,10 @@ defmodule Livebook.LiveMarkdown.Export do [delimiter, "output\n", text, "\n", delimiter] end + defp render_output({:vega_lite_static, spec}) do + ["```", "vega-lite\n", Jason.encode!(spec), "\n", "```"] + end + defp render_output(_output), do: :ignored defp get_elixir_cell_code(%{source: source, disable_formatting: true}), diff --git a/lib/livebook/live_markdown/import.ex b/lib/livebook/live_markdown/import.ex index 81ee7312ddd..8ac9431c91e 100644 --- a/lib/livebook/live_markdown/import.ex +++ b/lib/livebook/live_markdown/import.ex @@ -184,7 +184,17 @@ defmodule Livebook.LiveMarkdown.Import do [{"pre", _, [{"code", [{"class", "output"}], [output], %{}}], %{}} | ast], outputs ) do - take_outputs(ast, [output | outputs]) + take_outputs(ast, [{:text, output} | outputs]) + end + + defp take_outputs( + [{"pre", _, [{"code", [{"class", "vega-lite"}], [output], %{}}], %{}} | ast], + outputs + ) do + case Jason.decode(output) do + {:ok, spec} -> take_outputs(ast, [{:vega_lite_static, spec} | outputs]) + _ -> take_outputs(ast, outputs) + end end defp take_outputs(ast, outputs), do: {outputs, ast} @@ -198,7 +208,6 @@ defmodule Livebook.LiveMarkdown.Import do defp build_notebook([{:cell, :elixir, source, outputs} | elems], cells, sections, messages) 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, outputs: outputs} |> Map.merge(attrs) build_notebook(elems, [cell | cells], sections, messages) end diff --git a/test/livebook/live_markdown/export_test.exs b/test/livebook/live_markdown/export_test.exs index 88bd722d5ae..7bf2b78ce90 100644 --- a/test/livebook/live_markdown/export_test.exs +++ b/test/livebook/live_markdown/export_test.exs @@ -530,7 +530,13 @@ defmodule Livebook.LiveMarkdown.ExportTest do | source: """ IO.puts("hey")\ """, - outputs: ["hey"] + outputs: [ + "hey", + {:vega_lite_static, + %{ + "$schema" => "https://vega.github.io/schema/vega-lite/v5.json" + }} + ] } ] } @@ -650,7 +656,7 @@ defmodule Livebook.LiveMarkdown.ExportTest do | source: """ IO.puts("hey")\ """, - outputs: [{:vega_lite_static, %{}}, {:table_dynamic, self()}] + outputs: [{:table_dynamic, self()}] } ] } @@ -671,6 +677,75 @@ defmodule Livebook.LiveMarkdown.ExportTest do assert expected_document == document end + + test "includes vega_lite_static output" do + notebook = %{ + Notebook.new() + | name: "My Notebook", + sections: [ + %{ + Notebook.Section.new() + | name: "Section 1", + cells: [ + %{ + Notebook.Cell.new(:elixir) + | source: """ + Vl.new(width: 500, height: 200) + |> Vl.data_from_series(in: [1, 2, 3, 4, 5], out: [1, 2, 3, 4, 5]) + |> Vl.mark(:line) + |> Vl.encode_field(:x, "in", type: :quantitative) + |> Vl.encode_field(:y, "out", type: :quantitative)\ + """, + outputs: [ + {:vega_lite_static, + %{ + "$schema" => "https://vega.github.io/schema/vega-lite/v5.json", + "data" => %{ + "values" => [ + %{"in" => 1, "out" => 1}, + %{"in" => 2, "out" => 2}, + %{"in" => 3, "out" => 3}, + %{"in" => 4, "out" => 4}, + %{"in" => 5, "out" => 5} + ] + }, + "encoding" => %{ + "x" => %{"field" => "in", "type" => "quantitative"}, + "y" => %{"field" => "out", "type" => "quantitative"} + }, + "height" => 200, + "mark" => "line", + "width" => 500 + }} + ] + } + ] + } + ] + } + + expected_document = """ + # My Notebook + + ## Section 1 + + ```elixir + Vl.new(width: 500, height: 200) + |> Vl.data_from_series(in: [1, 2, 3, 4, 5], out: [1, 2, 3, 4, 5]) + |> Vl.mark(:line) + |> Vl.encode_field(:x, "in", type: :quantitative) + |> Vl.encode_field(:y, "out", type: :quantitative) + ``` + + ```vega-lite + {"$schema":"https://vega.github.io/schema/vega-lite/v5.json","data":{"values":[{"in":1,"out":1},{"in":2,"out":2},{"in":3,"out":3},{"in":4,"out":4},{"in":5,"out":5}]},"encoding":{"x":{"field":"in","type":"quantitative"},"y":{"field":"out","type":"quantitative"}},"height":200,"mark":"line","width":500} + ``` + """ + + document = Export.notebook_to_markdown(notebook, include_outputs: true) + + assert expected_document == document + end end test "includes outputs when notebook has :persist_outputs set" do diff --git a/test/livebook/live_markdown/import_test.exs b/test/livebook/live_markdown/import_test.exs index b803337a4be..d1a75aea246 100644 --- a/test/livebook/live_markdown/import_test.exs +++ b/test/livebook/live_markdown/import_test.exs @@ -582,6 +582,104 @@ defmodule Livebook.LiveMarkdown.ImportTest do assert %Notebook{name: "My Notebook", autosave_interval_s: 10} = notebook end + test "imports notebook with valid vega-lite output" do + markdown = """ + # My Notebook + + ## Section 1 + + ```elixir + Vl.new(width: 500, height: 200) + |> Vl.data_from_series(in: [1, 2, 3, 4, 5], out: [1, 2, 3, 4, 5]) + |> Vl.mark(:line) + |> Vl.encode_field(:x, "in", type: :quantitative) + |> Vl.encode_field(:y, "out", type: :quantitative) + ``` + + ```vega-lite + {"$schema":"https://vega.github.io/schema/vega-lite/v5.json","data":{"values":[{"in":1,"out":1},{"in":2,"out":2},{"in":3,"out":3},{"in":4,"out":4},{"in":5,"out":5}]},"encoding":{"x":{"field":"in","type":"quantitative"},"y":{"field":"out","type":"quantitative"}},"height":200,"mark":"line","width":500} + ``` + """ + + {notebook, []} = Import.notebook_from_markdown(markdown) + + assert %Notebook{ + name: "My Notebook", + sections: [ + %Notebook.Section{ + name: "Section 1", + cells: [ + %Cell.Elixir{ + source: """ + Vl.new(width: 500, height: 200) + |> Vl.data_from_series(in: [1, 2, 3, 4, 5], out: [1, 2, 3, 4, 5]) + |> Vl.mark(:line) + |> Vl.encode_field(:x, \"in\", type: :quantitative) + |> Vl.encode_field(:y, \"out\", type: :quantitative)\ + """, + outputs: [ + vega_lite_static: %{ + "$schema" => "https://vega.github.io/schema/vega-lite/v5.json", + "data" => %{ + "values" => [ + %{"in" => 1, "out" => 1}, + %{"in" => 2, "out" => 2}, + %{"in" => 3, "out" => 3}, + %{"in" => 4, "out" => 4}, + %{"in" => 5, "out" => 5} + ] + }, + "encoding" => %{ + "x" => %{"field" => "in", "type" => "quantitative"}, + "y" => %{"field" => "out", "type" => "quantitative"} + }, + "height" => 200, + "mark" => "line", + "width" => 500 + } + ] + } + ] + } + ] + } = notebook + end + + test "imports notebook with invalid vega-lite output" do + markdown = """ + # My Notebook + + ## Section 1 + + ```elixir + :ok + ``` + + ```vega-lite + not_a_json + ``` + """ + + {notebook, []} = Import.notebook_from_markdown(markdown) + + assert %Notebook{ + name: "My Notebook", + sections: [ + %Notebook.Section{ + name: "Section 1", + cells: [ + %Cell.Elixir{ + source: """ + :ok\ + """, + outputs: [] + } + ] + } + ] + } = notebook + end + test "skips invalid input type and returns a message" do markdown = """ # My Notebook