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:
Jonatan Kłosko 2021-06-26 16:47:52 +02:00 committed by GitHub
parent c674a0ec5d
commit 73a79cbdae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 95 additions and 31 deletions

View file

@ -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;
}
}

View file

@ -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.";
}

View file

@ -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

View file

@ -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);

View file

@ -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>
`);
}

View 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;

View file

@ -1,4 +1,6 @@
defmodule Livebook.LiveMarkdown.MarkdownHelpers do
@moduledoc false
@doc """
Reformats the given markdown document.
"""

View file

@ -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()}

View 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

View file

@ -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">

View file

@ -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,