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/_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..7d7346a0a0 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': ['mystparser'], 'perspective': ["customElements.get('perspective-viewer')"], 'plotly': ['Plotly'], 'tabulator': ['Tabulator'], 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/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..12725e60fb --- /dev/null +++ b/panel/models/myst.py @@ -0,0 +1,37 @@ +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 mystparser', '* as myst2html'] + + __javascript_modules_raw__ = [ + 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 new file mode 100644 index 0000000000..7b49cb353f --- /dev/null +++ b/panel/models/myst.ts @@ -0,0 +1,60 @@ +import type * as p from "@bokehjs/core/properties" +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 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 + } + + 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 = HTML.Props +} + +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/markup.py b/panel/pane/markup.py index 583903a932..065dfef2eb 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: @@ -361,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', 'mystjs'], doc=""" Markdown renderer implementation.""") renderer_options = param.Dict(default={}, nested_refs=True, doc=""" @@ -449,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 = '' @@ -482,6 +488,18 @@ def _process_param_change(self, params): params['css_classes'] = ['markdown'] + params['css_classes'] return super()._process_param_change(params) + def _get_model( + self, doc: Document, root: Model | None = None, + parent: Model | None = None, comm: Comm | None = None + ) -> Model: + 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 class JSON(HTMLBasePane):