@@ -3,23 +3,28 @@ import { unsafeHTML } from "lit-html/directives/unsafe-html.js";
33import { property } from "lit/decorators.js" ;
44
55import ClipboardJS from "clipboard" ;
6- import { sanitize } from "dompurify" ;
76import hljs from "highlight.js/lib/common" ;
87import { Renderer , parse } from "marked" ;
98
109import {
1110 LightElement ,
1211 createElement ,
1312 createSVGIcon ,
13+ renderDependencies ,
14+ sanitizeHTML ,
1415 showShinyClientMessage ,
16+ throttle ,
1517} from "../utils/_utils" ;
1618
19+ import type { HtmlDep } from "../utils/_utils" ;
20+
1721type ContentType = "markdown" | "semi-markdown" | "html" | "text" ;
1822
1923type ContentMessage = {
2024 id : string ;
2125 content : string ;
2226 operation : "append" | "replace" ;
27+ html_deps ?: HtmlDep [ ] ;
2328} ;
2429
2530type IsStreamingMessage = {
@@ -59,11 +64,11 @@ const markedEscapeOpts = { renderer: rendererEscapeHTML };
5964
6065function contentToHTML ( content : string , content_type : ContentType ) {
6166 if ( content_type === "markdown" ) {
62- return unsafeHTML ( sanitize ( parse ( content ) as string ) ) ;
67+ return unsafeHTML ( sanitizeHTML ( parse ( content ) as string ) ) ;
6368 } else if ( content_type === "semi-markdown" ) {
64- return unsafeHTML ( sanitize ( parse ( content , markedEscapeOpts ) as string ) ) ;
69+ return unsafeHTML ( sanitizeHTML ( parse ( content , markedEscapeOpts ) as string ) ) ;
6570 } else if ( content_type === "html" ) {
66- return unsafeHTML ( sanitize ( content ) ) ;
71+ return unsafeHTML ( sanitizeHTML ( content ) ) ;
6772 } else if ( content_type === "text" ) {
6873 return content ;
6974 } else {
@@ -94,6 +99,8 @@ class MarkdownElement extends LightElement {
9499 protected willUpdate ( changedProperties : PropertyValues ) : void {
95100 if ( changedProperties . has ( "content" ) ) {
96101 this . #isContentBeingAdded = true ;
102+
103+ MarkdownElement . #doUnBind( this ) ;
97104 }
98105 super . willUpdate ( changedProperties ) ;
99106 }
@@ -106,7 +113,14 @@ class MarkdownElement extends LightElement {
106113 } catch ( error ) {
107114 console . warn ( "Failed to highlight code:" , error ) ;
108115 }
109- if ( this . streaming ) this . #appendStreamingDot( ) ;
116+
117+ // Render Shiny HTML dependencies and bind inputs/outputs
118+ if ( this . streaming ) {
119+ this . #appendStreamingDot( ) ;
120+ MarkdownElement . _throttledBind ( this ) ;
121+ } else {
122+ MarkdownElement . #doBind( this ) ;
123+ }
110124
111125 // Update scrollable element after content has been added
112126 this . #updateScrollableElement( ) ;
@@ -148,6 +162,47 @@ class MarkdownElement extends LightElement {
148162 this . querySelector ( `svg.${ SVG_DOT_CLASS } ` ) ?. remove ( ) ;
149163 }
150164
165+ static async #doUnBind( el : HTMLElement ) : Promise < void > {
166+ if ( ! window ?. Shiny ?. unbindAll ) return ;
167+
168+ try {
169+ window . Shiny . unbindAll ( el ) ;
170+ } catch ( err ) {
171+ showShinyClientMessage ( {
172+ status : "error" ,
173+ message : `Failed to unbind Shiny inputs/outputs: ${ err } ` ,
174+ } ) ;
175+ }
176+ }
177+
178+ static async #doBind( el : HTMLElement ) : Promise < void > {
179+ if ( ! window ?. Shiny ?. initializeInputs ) return ;
180+ if ( ! window ?. Shiny ?. bindAll ) return ;
181+
182+ try {
183+ window . Shiny . initializeInputs ( el ) ;
184+ } catch ( err ) {
185+ showShinyClientMessage ( {
186+ status : "error" ,
187+ message : `Failed to initialize Shiny inputs: ${ err } ` ,
188+ } ) ;
189+ }
190+
191+ try {
192+ await window . Shiny . bindAll ( el ) ;
193+ } catch ( err ) {
194+ showShinyClientMessage ( {
195+ status : "error" ,
196+ message : `Failed to bind Shiny inputs/outputs: ${ err } ` ,
197+ } ) ;
198+ }
199+ }
200+
201+ @throttle ( 200 )
202+ private static async _throttledBind ( el : HTMLElement ) : Promise < void > {
203+ await this . #doBind( el ) ;
204+ }
205+
151206 #highlightAndCodeCopy( ) : void {
152207 const el = this . querySelector ( "pre code" ) ;
153208 if ( ! el ) return ;
@@ -244,7 +299,9 @@ if (!customElements.get("shiny-markdown-stream")) {
244299 customElements . define ( "shiny-markdown-stream" , MarkdownElement ) ;
245300}
246301
247- function handleMessage ( message : ContentMessage | IsStreamingMessage ) : void {
302+ async function handleMessage (
303+ message : ContentMessage | IsStreamingMessage
304+ ) : Promise < void > {
248305 const el = document . getElementById ( message . id ) as MarkdownElement ;
249306
250307 if ( ! el ) {
@@ -262,6 +319,10 @@ function handleMessage(message: ContentMessage | IsStreamingMessage): void {
262319 return ;
263320 }
264321
322+ if ( message . html_deps ) {
323+ await renderDependencies ( message . html_deps ) ;
324+ }
325+
265326 if ( message . operation === "replace" ) {
266327 el . setAttribute ( "content" , message . content ) ;
267328 } else if ( message . operation === "append" ) {
0 commit comments