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