mirror of
https://github.com/livebook-dev/livebook.git
synced 2024-09-20 10:05:57 +08:00
Add support for markdown output (#404)
* Add support for markdown output * Make cell indicator absolute * Update output typespec * Move rendering to the client * Polishing
This commit is contained in:
parent
c674a0ec5d
commit
73a79cbdae
|
@ -167,6 +167,6 @@
|
|||
/* Boxes */
|
||||
|
||||
.error-box {
|
||||
@apply mb-3 rounded-lg px-4 py-2 bg-red-100 text-red-400 font-medium;
|
||||
@apply rounded-lg px-4 py-2 bg-red-100 text-red-400 font-medium;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -66,23 +66,23 @@
|
|||
}
|
||||
|
||||
.markdown table {
|
||||
@apply w-full my-4;
|
||||
@apply w-full my-4 shadow-xl-center rounded-lg tiny-scrollbar;
|
||||
}
|
||||
|
||||
.markdown table thead tr {
|
||||
@apply border-b border-gray-200;
|
||||
}
|
||||
|
||||
.markdown table tbody tr:not(:last-child) {
|
||||
@apply border-b border-gray-200;
|
||||
.markdown table tbody tr {
|
||||
@apply border-b border-gray-200 last:border-b-0 hover:bg-gray-50;
|
||||
}
|
||||
|
||||
.markdown table th {
|
||||
@apply p-2 font-bold text-left;
|
||||
@apply py-3 px-6 text-gray-700 font-semibold;
|
||||
}
|
||||
|
||||
.markdown table td {
|
||||
@apply p-2 text-left;
|
||||
@apply py-3 px-6 text-gray-500;
|
||||
}
|
||||
|
||||
.markdown table th[align="center"],
|
||||
|
@ -95,16 +95,6 @@
|
|||
@apply text-right;
|
||||
}
|
||||
|
||||
.markdown table th:first-child,
|
||||
.markdown table td:first-child {
|
||||
@apply pl-0;
|
||||
}
|
||||
|
||||
.markdown table th:last-child,
|
||||
.markdown table td:last-child {
|
||||
@apply pr-0;
|
||||
}
|
||||
|
||||
.markdown code {
|
||||
@apply py-[0.1rem] px-2 rounded-lg text-sm align-middle font-mono bg-gray-200;
|
||||
}
|
||||
|
@ -139,13 +129,13 @@
|
|||
|
||||
/* Overrides for user-entered markdown */
|
||||
|
||||
[data-element="cell"] .markdown h1,
|
||||
[data-element="cell"] .markdown h2 {
|
||||
[data-element="cell"][data-type="markdown"] .markdown h1,
|
||||
[data-element="cell"][data-type="markdown"] .markdown h2 {
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
[data-element="cell"] .markdown h1:after,
|
||||
[data-element="cell"] .markdown h2:after {
|
||||
[data-element="cell"][data-type="markdown"] .markdown h1:after,
|
||||
[data-element="cell"][data-type="markdown"] .markdown h2:after {
|
||||
@apply text-red-400 text-base font-medium;
|
||||
content: "warning: heading levels 1 and 2 are reserved for notebook and section names, please use heading 3 and above.";
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import Menu from "./menu";
|
|||
import UserForm from "./user_form";
|
||||
import VegaLite from "./vega_lite";
|
||||
import Timer from "./timer";
|
||||
import MarkdownRenderer from "./markdown_renderer";
|
||||
import morphdomCallbacks from "./morphdom_callbacks";
|
||||
import { loadUserData } from "./lib/user";
|
||||
|
||||
|
@ -35,6 +36,7 @@ const hooks = {
|
|||
UserForm,
|
||||
VegaLite,
|
||||
Timer,
|
||||
MarkdownRenderer,
|
||||
};
|
||||
|
||||
const csrfToken = document
|
||||
|
|
|
@ -87,7 +87,10 @@ const Cell = {
|
|||
`[data-element="markdown-container"]`
|
||||
);
|
||||
const baseUrl = this.props.sessionPath;
|
||||
const markdown = new Markdown(markdownContainer, source, baseUrl);
|
||||
const markdown = new Markdown(markdownContainer, source, {
|
||||
baseUrl,
|
||||
emptyText: "Empty markdown cell",
|
||||
});
|
||||
|
||||
this.state.liveEditor.onChange((newSource) => {
|
||||
markdown.setContent(newSource);
|
||||
|
|
|
@ -36,10 +36,11 @@ DOMPurify.addHook("afterSanitizeAttributes", (node) => {
|
|||
* Renders markdown content in the given container.
|
||||
*/
|
||||
class Markdown {
|
||||
constructor(container, content, baseUrl = null) {
|
||||
constructor(container, content, { baseUrl = null, emptyText = "" } = {}) {
|
||||
this.container = container;
|
||||
this.content = content;
|
||||
this.baseUrl = baseUrl;
|
||||
this.emptyText = emptyText;
|
||||
|
||||
this.__render();
|
||||
}
|
||||
|
@ -81,7 +82,7 @@ class Markdown {
|
|||
} else {
|
||||
resolve(`
|
||||
<div class="text-gray-300">
|
||||
Empty markdown cell
|
||||
${this.emptyText}
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
|
36
assets/js/markdown_renderer/index.js
Normal file
36
assets/js/markdown_renderer/index.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
import { getAttributeOrThrow } from "../lib/attribute";
|
||||
import Markdown from "../cell/markdown";
|
||||
|
||||
/**
|
||||
* A hook used to render markdown content on the client.
|
||||
*
|
||||
* Configuration:
|
||||
*
|
||||
* * `data-id` - id of the renderer, under which the content event is pushed
|
||||
*/
|
||||
const MarkdownRenderer = {
|
||||
mounted() {
|
||||
this.props = getProps(this);
|
||||
|
||||
const markdown = new Markdown(this.el, "");
|
||||
|
||||
this.handleEvent(
|
||||
`markdown-renderer:${this.props.id}:content`,
|
||||
({ content }) => {
|
||||
markdown.setContent(content);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
updated() {
|
||||
this.props = getProps(this);
|
||||
},
|
||||
};
|
||||
|
||||
function getProps(hook) {
|
||||
return {
|
||||
id: getAttributeOrThrow(hook.el, "data-id"),
|
||||
};
|
||||
}
|
||||
|
||||
export default MarkdownRenderer;
|
|
@ -1,4 +1,6 @@
|
|||
defmodule Livebook.LiveMarkdown.MarkdownHelpers do
|
||||
@moduledoc false
|
||||
|
||||
@doc """
|
||||
Reformats the given markdown document.
|
||||
"""
|
||||
|
|
|
@ -27,7 +27,9 @@ defmodule Livebook.Notebook.Cell.Elixir do
|
|||
| binary()
|
||||
# Standalone text block
|
||||
| {:text, binary()}
|
||||
# A raw image in the given format.
|
||||
# Markdown content
|
||||
| {:markdown, binary()}
|
||||
# A raw image in the given format
|
||||
| {:image, content :: binary(), mime_type :: binary()}
|
||||
# Vega-Lite graphic
|
||||
| {:vega_lite_static, spec :: map()}
|
||||
|
|
24
lib/livebook_web/live/output/markdown_component.ex
Normal file
24
lib/livebook_web/live/output/markdown_component.ex
Normal file
|
@ -0,0 +1,24 @@
|
|||
defmodule LivebookWeb.Output.MarkdownComponent do
|
||||
use LivebookWeb, :live_component
|
||||
|
||||
@impl true
|
||||
def update(assigns, socket) do
|
||||
socket = assign(socket, assigns)
|
||||
|
||||
{:ok,
|
||||
push_event(socket, "markdown-renderer:#{socket.assigns.id}:content", %{
|
||||
content: socket.assigns.content
|
||||
})}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~L"""
|
||||
<div class="markdown"
|
||||
id="markdown-renderer-<%= @id %>"
|
||||
phx-hook="MarkdownRenderer"
|
||||
data-id="<%= @id %>">
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
|
@ -94,7 +94,7 @@ defmodule LivebookWeb.Output.TableDynamicLive do
|
|||
<thead class="text-left">
|
||||
<tr class="border-b border-gray-200 whitespace-nowrap">
|
||||
<%= for {column, idx} <- Enum.with_index(@columns) do %>
|
||||
<th class="py-3 px-6 text-gray-700 font-smibold <%= if(:sorting in @features, do: "cursor-pointer", else: "pointer-events-none") %>"
|
||||
<th class="py-3 px-6 text-gray-700 font-semibold <%= if(:sorting in @features, do: "cursor-pointer", else: "pointer-events-none") %>"
|
||||
phx-click="column_click"
|
||||
phx-value-column_idx="<%= idx %>">
|
||||
<div class="flex items-center space-x-1">
|
||||
|
|
|
@ -57,8 +57,8 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex">
|
||||
<div class="w-1 rounded-lg relative -left-3" data-element="cell-focus-indicator">
|
||||
<div class="flex relative">
|
||||
<div class="w-1 h-full rounded-lg absolute top-0 -left-3" data-element="cell-focus-indicator">
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<div class="pb-4" data-element="editor-box">
|
||||
|
@ -130,8 +130,8 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex">
|
||||
<div class="w-1 rounded-lg relative -left-3" data-element="cell-focus-indicator">
|
||||
<div class="flex relative">
|
||||
<div class="w-1 h-full rounded-lg absolute top-0 -left-3" data-element="cell-focus-indicator">
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<%= render_editor(assigns) %>
|
||||
|
@ -182,8 +182,8 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex">
|
||||
<div class="w-1 rounded-lg relative -left-3" data-element="cell-focus-indicator">
|
||||
<div class="flex relative">
|
||||
<div class="w-1 h-full rounded-lg absolute top-0 -left-3" data-element="cell-focus-indicator">
|
||||
</div>
|
||||
<div>
|
||||
<form phx-change="set_cell_value" phx-submit="queue_bound_cells_evaluation">
|
||||
|
@ -307,6 +307,10 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
live_component(LivebookWeb.Output.TextComponent, id: id, content: text, follow: false)
|
||||
end
|
||||
|
||||
defp render_output(_socket, {:markdown, markdown}, id) do
|
||||
live_component(LivebookWeb.Output.MarkdownComponent, id: id, content: markdown)
|
||||
end
|
||||
|
||||
defp render_output(_socket, {:image, content, mime_type}, id) do
|
||||
live_component(LivebookWeb.Output.ImageComponent,
|
||||
id: id,
|
||||
|
|
Loading…
Reference in a new issue