Skip to content

Commit

Permalink
Add support for chunked text and markdown outputs (#318)
Browse files Browse the repository at this point in the history
Co-authored-by: José Valim <[email protected]>
  • Loading branch information
jonatanklosko and josevalim authored Aug 22, 2023
1 parent a37a6f6 commit 703889a
Show file tree
Hide file tree
Showing 10 changed files with 129 additions and 70 deletions.
4 changes: 2 additions & 2 deletions lib/kino.ex
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ defmodule Kino do
def inspect(term, opts \\ []) do
label = if label = opts[:label], do: "#{label}: ", else: ""

{:text, text} = Kino.Output.inspect(term, opts)
output = {:text, label <> text}
{:terminal_text, text, info} = Kino.Output.inspect(term, opts)
output = {:terminal_text, label <> text, info}
Kino.Bridge.put_output(output)

term
Expand Down
25 changes: 17 additions & 8 deletions lib/kino/markdown.ex
Original file line number Diff line number Diff line change
Expand Up @@ -29,23 +29,32 @@ defmodule Kino.Markdown do
This format may come in handy when exploring Markdown
from external sources:
content = File.read!("/path/to/README.md")
Kino.Markdown.new(content)
text = File.read!("/path/to/README.md")
Kino.Markdown.new(text)
'''

@enforce_keys [:content]
@enforce_keys [:text, :chunk]

defstruct [:content]
defstruct [:text, :chunk]

@opaque t :: %__MODULE__{
content: binary()
text: String.t(),
chunk: boolean()
}

@doc """
Creates a new kino displaying the given Markdown content.
## Options
* `:chunk` - whether this is a part of a larger text. Adjacent chunks
are merged into a single text. This is useful for streaming content.
Defaults to `false`
"""
@spec new(binary()) :: t()
def new(content) do
%__MODULE__{content: content}
@spec new(binary(), keyword()) :: t()
def new(text, opts \\ []) do
opts = Keyword.validate!(opts, chunk: false)
%__MODULE__{text: text, chunk: opts[:chunk]}
end
end
50 changes: 24 additions & 26 deletions lib/kino/output.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ defmodule Kino.Output do
"""
@type t ::
ignored()
| stdout()
| text()
| terminal_text()
| plain_text()
| markdown()
| image()
Expand All @@ -27,31 +26,30 @@ defmodule Kino.Output do
"""
@type ignored :: :ignored

@typedoc """
IO text output, adjacent such outputs are treated as a whole.
@typedoc ~S"""
Terminal text content.
Supports ANSI escape codes.
Supports ANSI escape codes and overwriting lines with `\r`.
"""
@type stdout :: {:stdout, binary()}

@typedoc """
Standalone text block visually matching `t:stdout/0`.
Supports ANSI escape codes.
"""
@type text :: {:text, binary()}
@type terminal_text :: {:terminal_text, String.t(), info :: %{chunk: boolean()}}

@typedoc """
Plain text content.
Adjacent outputs with `:chunk` set to `true` are merged and rendered
as a whole.
Similar to `t:markdown/0`, but with no special markup.
"""
@type plain_text :: {:plain_text, binary()}
@type plain_text :: {:plain_text, String.t(), info :: %{chunk: boolean()}}

@typedoc """
Markdown content.
Adjacent outputs with `:chunk` set to `true` are merged and rendered
as a whole.
"""
@type markdown :: {:markdown, binary()}
@type markdown :: {:markdown, String.t(), info :: %{chunk: boolean()}}

@typedoc """
A raw image in the given format.
Expand Down Expand Up @@ -423,27 +421,27 @@ defmodule Kino.Output do
@type ref :: String.t()

@doc """
See `t:text/0`.
See `t:terminal_text/0`.
"""
@spec text(binary()) :: t()
def text(text) when is_binary(text) do
{:text, text}
@spec terminal_text(binary(), boolean()) :: t()
def terminal_text(text, chunk \\ false) when is_binary(text) do
{:terminal_text, text, %{chunk: chunk}}
end

@doc """
See `t:plain_text/0`.
"""
@spec plain_text(binary()) :: t()
def plain_text(text) do
{:plain_text, text}
@spec plain_text(binary(), boolean()) :: t()
def plain_text(text, chunk \\ false) do
{:plain_text, text, %{chunk: chunk}}
end

@doc """
See `t:markdown/0`.
"""
@spec markdown(binary()) :: t()
def markdown(content) when is_binary(content) do
{:markdown, content}
@spec markdown(binary(), boolean()) :: t()
def markdown(content, chunk \\ false) when is_binary(content) do
{:markdown, content, %{chunk: chunk}}
end

@doc """
Expand Down Expand Up @@ -508,7 +506,7 @@ defmodule Kino.Output do
@spec inspect(term(), keyword()) :: t()
def inspect(term, opts \\ []) do
inspected = Kernel.inspect(term, inspect_opts(opts))
text(inspected)
terminal_text(inspected)
end

defp inspect_opts(opts) do
Expand Down
6 changes: 3 additions & 3 deletions lib/kino/render.ex
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,17 @@ end

defimpl Kino.Render, for: Kino.Text do
def to_livebook(%{terminal: true} = text) do
Kino.Output.text(text.content)
Kino.Output.terminal_text(text.text, text.chunk)
end

def to_livebook(text) do
Kino.Output.plain_text(text.content)
Kino.Output.plain_text(text.text, text.chunk)
end
end

defimpl Kino.Render, for: Kino.Markdown do
def to_livebook(markdown) do
Kino.Output.markdown(markdown.content)
Kino.Output.markdown(markdown.text, markdown.chunk)
end
end

Expand Down
22 changes: 14 additions & 8 deletions lib/kino/text.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@ defmodule Kino.Text do
'''

@enforce_keys [:content]
@enforce_keys [:text]

defstruct [:content, terminal: false]
defstruct [:text, :terminal, :chunk]

@opaque t :: %__MODULE__{
content: binary(),
terminal: boolean()
text: String.t(),
terminal: boolean(),
chunk: boolean()
}

@doc """
Expand All @@ -31,10 +32,15 @@ defmodule Kino.Text do
* `:terminal` - whether to render the text as if it were printed to
standard output, supporting ANSI escape codes. Defaults to `false`
* `:chunk` - whether this is a part of a larger text. Adjacent chunks
are merged into a single text. This is useful for streaming content.
Defaults to `false`
"""
@spec new(String.t(), opts) :: t() when opts: [terminal: boolean()]
def new(content, opts \\ []) when is_binary(content) do
opts = Keyword.validate!(opts, terminal: false)
%__MODULE__{content: content, terminal: opts[:terminal]}
@spec new(String.t(), opts) :: t() when opts: [terminal: boolean(), chunk: boolean()]
def new(text, opts \\ []) when is_binary(text) do
opts = Keyword.validate!(opts, terminal: false, chunk: false)
%__MODULE__{text: text, terminal: opts[:terminal], chunk: opts[:chunk]}
end
end
17 changes: 12 additions & 5 deletions test/kino/debug_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ defmodule Kino.Debug.Test do

{kino, output, _frame_ref} = assert_dbg_pipeline_render()

assert output == {:text, "\e[34m13\e[0m"}
assert output == {:terminal_text, "\e[34m13\e[0m", %{chunk: false}}

%{
dbg_line: dbg_line,
Expand Down Expand Up @@ -76,7 +76,10 @@ defmodule Kino.Debug.Test do
"changed" => true
})

assert_output({:frame, [{:text, "\e[34m31\e[0m"}], %{ref: ^frame_ref, type: :replace}})
assert_output(
{:frame, [{:terminal_text, "\e[34m31\e[0m", %{chunk: false}}],
%{ref: ^frame_ref, type: :replace}}
)
end

test "updates result when a pipeline step is moved" do
Expand All @@ -100,7 +103,10 @@ defmodule Kino.Debug.Test do
"changed" => true
})

assert_output({:frame, [{:text, "\e[34m31\e[0m"}], %{ref: ^frame_ref, type: :replace}})
assert_output(
{:frame, [{:terminal_text, "\e[34m31\e[0m", %{chunk: false}}],
%{ref: ^frame_ref, type: :replace}}
)
end

test "handles evaluation error" do
Expand All @@ -124,7 +130,8 @@ defmodule Kino.Debug.Test do
})

assert_output(
{:frame, [{:text, "\e[34m1\e[0m..\e[34m5\e[0m"}], %{ref: ^frame_ref, type: :replace}}
{:frame, [{:terminal_text, "\e[34m1\e[0m..\e[34m5\e[0m", %{chunk: false}}],
%{ref: ^frame_ref, type: :replace}}
)
end

Expand Down Expand Up @@ -163,7 +170,7 @@ defmodule Kino.Debug.Test do

{kino, output} = assert_dbg_default_render()

assert output == {:text, "\e[34m15\e[0m"}
assert output == {:terminal_text, "\e[34m15\e[0m", %{chunk: false}}

%{
dbg_line: dbg_line,
Expand Down
20 changes: 15 additions & 5 deletions test/kino/frame_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,24 @@ defmodule Kino.FrameTest do
frame = Kino.Frame.new()

Kino.Frame.render(frame, 1)
assert_output({:frame, [{:text, "\e[34m1\e[0m"}], %{type: :replace}})

assert_output(
{:frame, [{:terminal_text, "\e[34m1\e[0m", %{chunk: false}}], %{type: :replace}}
)

Kino.Frame.render(frame, Kino.Markdown.new("_hey_"))
assert_output({:frame, [{:markdown, "_hey_"}], %{type: :replace}})
assert_output({:frame, [{:markdown, "_hey_", %{chunk: false}}], %{type: :replace}})
end

test "render/2 sends output to a specific client when the :to is given" do
frame = Kino.Frame.new()

Kino.Frame.render(frame, 1, to: "client1")
assert_output_to("client1", {:frame, [{:text, "\e[34m1\e[0m"}], %{type: :replace}})

assert_output_to(
"client1",
{:frame, [{:terminal_text, "\e[34m1\e[0m", %{chunk: false}}], %{type: :replace}}
)

assert Kino.Frame.get_outputs(frame) == []
end
Expand All @@ -24,7 +31,10 @@ defmodule Kino.FrameTest do
frame = Kino.Frame.new()

Kino.Frame.render(frame, 1, temporary: true)
assert_output_to_clients({:frame, [{:text, "\e[34m1\e[0m"}], %{type: :replace}})

assert_output_to_clients(
{:frame, [{:terminal_text, "\e[34m1\e[0m", %{chunk: false}}], %{type: :replace}}
)

assert Kino.Frame.get_outputs(frame) == []
end
Expand Down Expand Up @@ -65,6 +75,6 @@ defmodule Kino.FrameTest do
frame = Kino.Frame.new()

Kino.Frame.append(frame, 1)
assert_output({:frame, [{:text, "\e[34m1\e[0m"}], %{type: :append}})
assert_output({:frame, [{:terminal_text, "\e[34m1\e[0m", %{chunk: false}}], %{type: :append}})
end
end
2 changes: 1 addition & 1 deletion test/kino/output_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ defmodule Kino.OutputTest do
Kino.Config.configure(inspect: [limit: 1, syntax_colors: []])

list = Enum.to_list(1..100)
assert Kino.Output.inspect(list) == {:text, "[1, ...]"}
assert Kino.Output.inspect(list) == {:terminal_text, "[1, ...]", %{chunk: false}}

Application.delete_env(:kino, :inspect)
end
Expand Down
6 changes: 3 additions & 3 deletions test/kino/text_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ defmodule Kino.TextTest do
describe "new/1" do
test "outputs plain text" do
"Hello!" |> Kino.Text.new() |> Kino.render()
assert_output({:plain_text, "Hello!"})
assert_output({:plain_text, "Hello!", %{chunk: false}})

"Hello!" |> Kino.Text.new(terminal: false) |> Kino.render()
assert_output({:plain_text, "Hello!"})
assert_output({:plain_text, "Hello!", %{chunk: false}})
end

test "outputs terminal text" do
"Hello!" |> Kino.Text.new(terminal: true) |> Kino.render()
assert_output({:text, "Hello!"})
assert_output({:terminal_text, "Hello!", %{chunk: false}})
end
end
end
Loading

0 comments on commit 703889a

Please sign in to comment.