diff --git a/assets/css/js_interop.css b/assets/css/js_interop.css index 5afa62adc..3caf518cf 100644 --- a/assets/css/js_interop.css +++ b/assets/css/js_interop.css @@ -13,6 +13,10 @@ solely client-side operations. @apply hidden; } +[phx-hook="Highlight"][data-highlighted] > [data-source] { + @apply hidden; +} + /* === Session === */ [data-element="session"]:not([data-js-insert-mode]) diff --git a/assets/js/highlight/index.js b/assets/js/highlight/index.js index 00b070a42..5e0ea0420 100644 --- a/assets/js/highlight/index.js +++ b/assets/js/highlight/index.js @@ -1,5 +1,6 @@ import { getAttributeOrThrow } from "../lib/attribute"; import { highlight } from "../cell/live_editor/monaco"; +import { findChildOrThrow } from "../lib/utils"; /** * A hook used to highlight source code in the root element. @@ -7,18 +8,45 @@ import { highlight } from "../cell/live_editor/monaco"; * Configuration: * * * `data-language` - language of the source code + * + * The element should have two children: + * + * * one annotated with `data-source` attribute, it should contain + * the source code to be highlighted + * + * * one annotated with `data-target` where the highlighted code + * is rendered */ const Highlight = { mounted() { this.props = getProps(this); + this.state = { + sourceElement: null, + originalElement: null, + }; - highlightIn(this.el, this.props.language); + this.state.sourceElement = findChildOrThrow(this.el, "[data-source]"); + this.state.targetElement = findChildOrThrow(this.el, "[data-target]"); + + highlightInto( + this.state.targetElement, + this.state.sourceElement, + this.props.language + ).then(() => { + this.el.setAttribute("data-highlighted", "true"); + }); }, updated() { this.props = getProps(this); - highlightIn(this.el, this.props.language); + highlightInto( + this.state.targetElement, + this.state.sourceElement, + this.props.language + ).then(() => { + this.el.setAttribute("data-highlighted", "true"); + }); }, }; @@ -28,11 +56,11 @@ function getProps(hook) { }; } -function highlightIn(element, language) { - const code = element.innerText; +function highlightInto(targetElement, sourceElement, language) { + const code = sourceElement.innerText; - highlight(code, language).then((html) => { - element.innerHTML = html; + return highlight(code, language).then((html) => { + targetElement.innerHTML = html; }); } diff --git a/assets/js/lib/utils.js b/assets/js/lib/utils.js index 84b1fe55d..820ecd73b 100644 --- a/assets/js/lib/utils.js +++ b/assets/js/lib/utils.js @@ -103,3 +103,15 @@ export function throttle(fn, windowMs) { } }; } + +export function findChildOrThrow(element, selector) { + const child = element.querySelector(selector); + + if (!child) { + throw new Error( + `expected a child matching ${selector}, but none was found` + ); + } + + return child; +} diff --git a/assets/js/virtualized_lines/index.js b/assets/js/virtualized_lines/index.js index c6d2277fc..4b5af5669 100644 --- a/assets/js/virtualized_lines/index.js +++ b/assets/js/virtualized_lines/index.js @@ -4,7 +4,7 @@ import { parseBoolean, parseInteger, } from "../lib/attribute"; -import { getLineHeight } from "../lib/utils"; +import { findChildOrThrow, getLineHeight } from "../lib/utils"; /** * A hook used to render text lines as a virtual list, @@ -37,21 +37,8 @@ const VirtualizedLines = { this.state.lineHeight = getLineHeight(this.el); - this.state.templateElement = this.el.querySelector("[data-template]"); - - if (!this.state.templateElement) { - throw new Error( - "VirtualizedLines must have a child with data-template attribute" - ); - } - - this.state.contentElement = this.el.querySelector("[data-content]"); - - if (!this.state.contentElement) { - throw new Error( - "VirtualizedLines must have a child with data-content attribute" - ); - } + this.state.templateElement = findChildOrThrow(this.el, "[data-template]"); + this.state.contentElement = findChildOrThrow(this.el, "[data-content]"); const config = hyperListConfig( this.state.contentElement, diff --git a/lib/livebook_web/helpers.ex b/lib/livebook_web/helpers.ex index 29d4dbd61..92f9d3af8 100644 --- a/lib/livebook_web/helpers.ex +++ b/lib/livebook_web/helpers.ex @@ -214,4 +214,26 @@ defmodule LivebookWeb.Helpers do """ end + + @doc """ + Renders a highlighted code snippet. + + ## Examples + + <.code_preview + source_id="my-snippet" + language="elixir" + source="System.version()" /> + """ + def code_preview(assigns) do + ~H""" +
+
<%= @source %>
+
+ """ + end end diff --git a/lib/livebook_web/live/session_live/bin_component.ex b/lib/livebook_web/live/session_live/bin_component.ex index a5fa4cad6..2119d53fe 100644 --- a/lib/livebook_web/live/session_live/bin_component.ex +++ b/lib/livebook_web/live/session_live/bin_component.ex @@ -88,12 +88,10 @@ defmodule LivebookWeb.SessionLive.BinComponent do -
-
<%= cell.source %>
-
+ <.code_preview + source_id={"bin-cell-#{cell.id}-source"} + language={Cell.type(cell)} + source={cell.source} /> <% end %> <%= if length(@matching_entries) > @limit do %> diff --git a/lib/livebook_web/live/session_live/export_elixir_component.ex b/lib/livebook_web/live/session_live/export_elixir_component.ex index 33787a400..3ba240b1d 100644 --- a/lib/livebook_web/live/session_live/export_elixir_component.ex +++ b/lib/livebook_web/live/session_live/export_elixir_component.ex @@ -47,13 +47,10 @@ defmodule LivebookWeb.SessionLive.ExportElixirComponent do -
-
<%= @source %>
-
+ <.code_preview + source_id="export-notebook-source" + language="elixir" + source={@source} /> """ diff --git a/lib/livebook_web/live/session_live/export_live_markdown_component.ex b/lib/livebook_web/live/session_live/export_live_markdown_component.ex index 70ccf1974..279c8053f 100644 --- a/lib/livebook_web/live/session_live/export_live_markdown_component.ex +++ b/lib/livebook_web/live/session_live/export_live_markdown_component.ex @@ -37,11 +37,10 @@ defmodule LivebookWeb.SessionLive.ExportLiveMarkdownComponent do
-
<%= @source %>
+ <.code_preview + source_id="export-notebook-source" + language="markdown" + source={@source} />
"""