From f4b1cc25a4ec51e4ebad8f2d0810ce0f983d53d9 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 15 Aug 2024 11:54:25 +0200 Subject: [PATCH 1/6] Add a MyST pane --- panel/_templates/js_resources.html | 2 +- panel/config.py | 2 + panel/models/html.ts | 2 +- panel/models/index.ts | 1 + panel/models/myst.py | 20 ++++++++++ panel/models/myst.ts | 60 ++++++++++++++++++++++++++++++ panel/pane/__init__.py | 3 +- panel/pane/markup.py | 25 ++++++++++++- 8 files changed, 111 insertions(+), 4 deletions(-) create mode 100644 panel/models/myst.py create mode 100644 panel/models/myst.ts diff --git a/panel/_templates/js_resources.html b/panel/_templates/js_resources.html index b0912f0f7e..0ea2c3fb9a 100644 --- a/panel/_templates/js_resources.html +++ b/panel/_templates/js_resources.html @@ -28,7 +28,7 @@ {%- for name, file in js_module_exports.items() %} {%- endfor %} diff --git a/panel/config.py b/panel/config.py index 5f7955d03f..b6829de377 100644 --- a/panel/config.py +++ b/panel/config.py @@ -668,6 +668,7 @@ class panel_extension(_pyviz_extension): 'jsoneditor': 'panel.models.jsoneditor', 'katex': 'panel.models.katex', 'mathjax': 'panel.models.mathjax', + 'myst': 'panel.models.myst', 'perspective': 'panel.models.perspective', 'plotly': 'panel.models.plotly', 'tabulator': 'panel.models.tabulator', @@ -689,6 +690,7 @@ class panel_extension(_pyviz_extension): 'gridstack': ['GridStack'], 'katex': ['katex'], 'mathjax': ['MathJax'], + 'myst': ['mystjs'], 'perspective': ["customElements.get('perspective-viewer')"], 'plotly': ['Plotly'], 'tabulator': ['Tabulator'], diff --git a/panel/models/html.ts b/panel/models/html.ts index fe549519e2..dfeefcd7a9 100644 --- a/panel/models/html.ts +++ b/panel/models/html.ts @@ -181,7 +181,7 @@ export class HTMLView extends PanelMarkupView { return processed_text.join("") } - private contains_tex(html: string): boolean { + contains_tex(html: string): boolean { if (!this.provider.MathJax) { return false } diff --git a/panel/models/index.ts b/panel/models/index.ts index ba9cf6e1fd..f732a98742 100644 --- a/panel/models/index.ts +++ b/panel/models/index.ts @@ -27,6 +27,7 @@ export {JSONEditor} from "./jsoneditor" export {KaTeX} from "./katex" export {Location} from "./location" export {MathJax} from "./mathjax" +export {MyST} from "./myst" export {PDF} from "./pdf" export {Perspective} from "./perspective" export {Player} from "./player" diff --git a/panel/models/myst.py b/panel/models/myst.py new file mode 100644 index 0000000000..2f139b0c6f --- /dev/null +++ b/panel/models/myst.py @@ -0,0 +1,20 @@ +from ..config import config +from ..io.resources import bundled_files +from ..util import classproperty +from .markup import HTML + + +class MyST(HTML): + """ + A bokeh model to render MyST markdown on the client side + """ + + __javascript_module_exports__ = ['* as mystjs'] + + __javascript_modules_raw__ = [ + f"{config.npm_cdn}/mystjs@0.0.15/+esm" + ] + + @classproperty + def __javascript_modules__(cls): + return bundled_files(cls, 'javascript_modules') diff --git a/panel/models/myst.ts b/panel/models/myst.ts new file mode 100644 index 0000000000..4b36e1ec97 --- /dev/null +++ b/panel/models/myst.ts @@ -0,0 +1,60 @@ +import type * as p from "@bokehjs/core/properties" +import type {Dict} from "@bokehjs/core/types" +import {Markup} from "@bokehjs/models/widgets/markup" +import {HTMLView, HTML} from "./html" + + +export class MySTView extends HTMLView { + declare model: MyST + + override process_tex(): string { + const myst = new (window as any).mystjs.MyST(); + const text = myst.render(this.model.text) + if (this.model.disable_math || !this.contains_tex(text)) { + return text + } + + const tex_parts = this.provider.MathJax.find_tex(text) + const processed_text: string[] = [] + + let last_index: number | undefined = 0 + for (const part of tex_parts) { + processed_text.push(text.slice(last_index, part.start.n)) + processed_text.push(this.provider.MathJax.tex2svg(part.math, {display: part.display}).outerHTML) + + last_index = part.end.n + } + + if (last_index! < text.length) { + processed_text.push(text.slice(last_index)) + } + + return processed_text.join("") + } +} + +export namespace MyST { + export type Attrs = p.AttrsOf + + export type Props = Markup.Props & { + events: p.Property> + run_scripts: p.Property + } +} + +export interface MyST extends MyST.Attrs {} + +export class MyST extends HTML { + declare properties: MyST.Props + + constructor(attrs?: Partial) { + super(attrs) + } + + static override __module__ = "panel.models.myst" + + static { + this.prototype.default_view = MySTView + this.define(({}) => ({})) + } +} diff --git a/panel/pane/__init__.py b/panel/pane/__init__.py index 5ca76b23ce..e403bd05b9 100644 --- a/panel/pane/__init__.py +++ b/panel/pane/__init__.py @@ -40,7 +40,7 @@ ) from .ipywidget import IPyLeaflet, IPyWidget, Reacton # noqa from .markup import ( # noqa - HTML, JSON, DataFrame, Markdown, Str, + HTML, JSON, DataFrame, Markdown, MyST, Str, ) from .media import Audio, Video # noqa from .perspective import Perspective # noqa @@ -78,6 +78,7 @@ "LaTeX", "Markdown", "Matplotlib", + "MyST", "Pane", "PaneBase", "ParamFunction", diff --git a/panel/pane/markup.py b/panel/pane/markup.py index 583903a932..7238ea7c98 100644 --- a/panel/pane/markup.py +++ b/panel/pane/markup.py @@ -14,9 +14,13 @@ import param # type: ignore +from pyviz_comms import JupyterComm + from ..io.resources import CDN_DIST from ..models.markup import HTML as _BkHTML, JSON as _BkJSON, HTMLStreamEvent -from ..util import HTML_SANITIZER, escape, prefix_length +from ..util import ( + HTML_SANITIZER, escape, lazy_load, prefix_length, +) from .base import ModelPane if TYPE_CHECKING: @@ -483,6 +487,25 @@ def _process_param_change(self, params): return super()._process_param_change(params) +class MyST(HTMLBasePane): + """ + The `MyST` pane renders MyST flavored Markdown client side, unlike + the `Markdown` pane which renders Markdown -> HTML serverside. + """ + + def _get_model( + self, doc: Document, root: Model | None = None, + parent: Model | None = None, comm: Comm | None = None + ) -> Model: + self._bokeh_model = lazy_load( + 'panel.models.myst', 'MyST', isinstance(comm, JupyterComm), root + ) + model = super()._get_model(doc, root, parent, comm) + return model + + def _transform_object(self, obj: Any) -> dict[str, Any]: + return dict(object=obj) + class JSON(HTMLBasePane): """ From ca754de1687c0ee7d3b6f24a2ec35d004da4f130 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 15 Aug 2024 13:31:42 +0200 Subject: [PATCH 2/6] Cleanup --- panel/models/myst.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/panel/models/myst.ts b/panel/models/myst.ts index 4b36e1ec97..680284cfd0 100644 --- a/panel/models/myst.ts +++ b/panel/models/myst.ts @@ -1,6 +1,4 @@ import type * as p from "@bokehjs/core/properties" -import type {Dict} from "@bokehjs/core/types" -import {Markup} from "@bokehjs/models/widgets/markup" import {HTMLView, HTML} from "./html" @@ -8,7 +6,7 @@ export class MySTView extends HTMLView { declare model: MyST override process_tex(): string { - const myst = new (window as any).mystjs.MyST(); + const myst = new (window as any).mystjs.MyST() const text = myst.render(this.model.text) if (this.model.disable_math || !this.contains_tex(text)) { return text @@ -35,11 +33,7 @@ export class MySTView extends HTMLView { export namespace MyST { export type Attrs = p.AttrsOf - - export type Props = Markup.Props & { - events: p.Property> - run_scripts: p.Property - } + export type Props = HTML.Props } export interface MyST extends MyST.Attrs {} From dbe7b3af96e08e75c2b743da2bc08c102ab018d9 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 15 Aug 2024 13:36:26 +0200 Subject: [PATCH 3/6] Fold mystjs renderer into Markdown pane --- panel/pane/__init__.py | 3 +-- panel/pane/markup.py | 23 +++++++++-------------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/panel/pane/__init__.py b/panel/pane/__init__.py index e403bd05b9..5ca76b23ce 100644 --- a/panel/pane/__init__.py +++ b/panel/pane/__init__.py @@ -40,7 +40,7 @@ ) from .ipywidget import IPyLeaflet, IPyWidget, Reacton # noqa from .markup import ( # noqa - HTML, JSON, DataFrame, Markdown, MyST, Str, + HTML, JSON, DataFrame, Markdown, Str, ) from .media import Audio, Video # noqa from .perspective import Perspective # noqa @@ -78,7 +78,6 @@ "LaTeX", "Markdown", "Matplotlib", - "MyST", "Pane", "PaneBase", "ParamFunction", diff --git a/panel/pane/markup.py b/panel/pane/markup.py index 7238ea7c98..dafac0fe66 100644 --- a/panel/pane/markup.py +++ b/panel/pane/markup.py @@ -365,7 +365,7 @@ class Markdown(HTMLBasePane): Additional markdown-it-py plugins to use.""") renderer = param.Selector(default='markdown-it', objects=[ - 'markdown-it', 'myst', 'markdown'], doc=""" + 'markdown-it', 'myst', 'markdown', 'myst-client'], doc=""" Markdown renderer implementation.""") renderer_options = param.Dict(default={}, nested_refs=True, doc=""" @@ -453,6 +453,8 @@ def hilite(token, langname, attrs): return parser def _transform_object(self, obj: Any) -> dict[str, Any]: + if self.renderer == 'mystjs': + return dict(object=obj) import markdown if obj is None: obj = '' @@ -486,26 +488,19 @@ def _process_param_change(self, params): params['css_classes'] = ['markdown'] + params['css_classes'] return super()._process_param_change(params) - -class MyST(HTMLBasePane): - """ - The `MyST` pane renders MyST flavored Markdown client side, unlike - the `Markdown` pane which renders Markdown -> HTML serverside. - """ - def _get_model( self, doc: Document, root: Model | None = None, parent: Model | None = None, comm: Comm | None = None ) -> Model: - self._bokeh_model = lazy_load( - 'panel.models.myst', 'MyST', isinstance(comm, JupyterComm), root - ) + if self.renderer == 'mystjs': + self._bokeh_model = lazy_load( + 'panel.models.myst', 'MyST', isinstance(comm, JupyterComm), root + ) + else: + self._bokeh_model = _BkHTML model = super()._get_model(doc, root, parent, comm) return model - def _transform_object(self, obj: Any) -> dict[str, Any]: - return dict(object=obj) - class JSON(HTMLBasePane): """ From b075903689d0e136601f48a0aa820e570e796e10 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 15 Aug 2024 13:40:33 +0200 Subject: [PATCH 4/6] Remove empty line --- panel/models/myst.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/panel/models/myst.ts b/panel/models/myst.ts index 680284cfd0..b9a318c598 100644 --- a/panel/models/myst.ts +++ b/panel/models/myst.ts @@ -1,7 +1,6 @@ import type * as p from "@bokehjs/core/properties" import {HTMLView, HTML} from "./html" - export class MySTView extends HTMLView { declare model: MyST From d5b8e873329f47e001dab93175219fdbb8989bdf Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 15 Aug 2024 15:15:02 +0200 Subject: [PATCH 5/6] Switch to parser and html converter --- panel/_templates/autoload_panel_js.js | 2 +- panel/dist/css/markdown.css | 6 ++++-- panel/models/myst.py | 21 +++++++++++++++++++-- panel/models/myst.ts | 11 +++++++++-- panel/pane/markup.py | 2 +- 5 files changed, 34 insertions(+), 8 deletions(-) diff --git a/panel/_templates/autoload_panel_js.js b/panel/_templates/autoload_panel_js.js index 01d9ade156..4fa3febf4f 100644 --- a/panel/_templates/autoload_panel_js.js +++ b/panel/_templates/autoload_panel_js.js @@ -182,7 +182,7 @@ calls it with the rendered model. console.debug("Bokeh: injecting script tag for BokehJS library: ", url); element.textContent = ` import ${name} from "${url}" - window.${name} = ${name} + window.${name.replace('* as ', '')} = ${name.replace('* as ', '')} window._bokeh_on_load() ` document.head.appendChild(element); diff --git a/panel/dist/css/markdown.css b/panel/dist/css/markdown.css index f82f241016..033604dada 100644 --- a/panel/dist/css/markdown.css +++ b/panel/dist/css/markdown.css @@ -317,7 +317,8 @@ h2:hover a.header-anchor::before { visibility: visible; } -.codehilite { +.codehilite, +pre > code.hljs { padding: 1rem 1.25rem; margin-top: 1rem; margin-bottom: 1rem; @@ -341,6 +342,7 @@ pre { } .codehilite > pre > code, -.codehilite > code { +.codehilite > code, +pre > code.hljs { white-space: break-spaces; } diff --git a/panel/models/myst.py b/panel/models/myst.py index 2f139b0c6f..12725e60fb 100644 --- a/panel/models/myst.py +++ b/panel/models/myst.py @@ -9,12 +9,29 @@ class MyST(HTML): A bokeh model to render MyST markdown on the client side """ - __javascript_module_exports__ = ['* as mystjs'] + __javascript_module_exports__ = ['* as mystparser', '* as myst2html'] __javascript_modules_raw__ = [ - f"{config.npm_cdn}/mystjs@0.0.15/+esm" + f"{config.npm_cdn}/myst-parser@1.5.3/+esm", + f"{config.npm_cdn}/myst-to-html@1.5.3/+esm" ] @classproperty def __javascript_modules__(cls): return bundled_files(cls, 'javascript_modules') + + __javascript_raw__ = [ + 'https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js' + ] + + @classproperty + def __javascript__(cls): + return bundled_files(cls) + + __css_raw__ = [ + 'https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github-dark.min.css' + ] + + @classproperty + def __css__(cls): + return bundled_files(cls, 'css') diff --git a/panel/models/myst.ts b/panel/models/myst.ts index b9a318c598..7b49cb353f 100644 --- a/panel/models/myst.ts +++ b/panel/models/myst.ts @@ -4,9 +4,16 @@ import {HTMLView, HTML} from "./html" export class MySTView extends HTMLView { declare model: MyST + override set_html(html: string | null): void { + super.set_html(html) + for (const el of this.container.querySelectorAll("pre code")) { + (window as any).hljs.highlightElement(el) + } + } + override process_tex(): string { - const myst = new (window as any).mystjs.MyST() - const text = myst.render(this.model.text) + const parsed = (window as any).mystparser.mystParse(this.model.text) + const text = (window as any).myst2html.mystToHtml(parsed) if (this.model.disable_math || !this.contains_tex(text)) { return text } diff --git a/panel/pane/markup.py b/panel/pane/markup.py index dafac0fe66..065dfef2eb 100644 --- a/panel/pane/markup.py +++ b/panel/pane/markup.py @@ -365,7 +365,7 @@ class Markdown(HTMLBasePane): Additional markdown-it-py plugins to use.""") renderer = param.Selector(default='markdown-it', objects=[ - 'markdown-it', 'myst', 'markdown', 'myst-client'], doc=""" + 'markdown-it', 'myst', 'markdown', 'mystjs'], doc=""" Markdown renderer implementation.""") renderer_options = param.Dict(default={}, nested_refs=True, doc=""" From 3ac9842aa3757fd671c6c7951ed042f54ea8fd09 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 15 Aug 2024 15:15:51 +0200 Subject: [PATCH 6/6] Apply suggestions from code review --- panel/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panel/config.py b/panel/config.py index b6829de377..7d7346a0a0 100644 --- a/panel/config.py +++ b/panel/config.py @@ -690,7 +690,7 @@ class panel_extension(_pyviz_extension): 'gridstack': ['GridStack'], 'katex': ['katex'], 'mathjax': ['MathJax'], - 'myst': ['mystjs'], + 'myst': ['mystparser'], 'perspective': ["customElements.get('perspective-viewer')"], 'plotly': ['Plotly'], 'tabulator': ['Tabulator'],