@@ -277,17 +277,22 @@ def interpolate_component(
277277 raise ValueError (f"Unknown component return value: { type (result_template )} " )
278278
279279
280- type InterpolateRawTextInfo = tuple [str , Template ]
280+ type InterpolateRawTextsFromTemplateInfo = tuple [str , Template ]
281281
282282
283- def interpolate_raw_text (
283+ def interpolate_raw_texts_from_template (
284284 render_api : RenderService ,
285285 bf : list [str ],
286286 last_container_tag : str | None ,
287287 template : Template ,
288288 ip_info : InterpolateInfo ,
289289) -> RenderQueueItem | None :
290- container_tag , content_t = t .cast (InterpolateRawTextInfo , ip_info )
290+ """
291+ Interpolate and join a template of raw texts together and escape them.
292+
293+ @NOTE: This interpolator expects a Template.
294+ """
295+ container_tag , content_t = t .cast (InterpolateRawTextsFromTemplateInfo , ip_info )
291296 bf .append (
292297 render_api .escape_html_content_in_tag (
293298 container_tag ,
@@ -296,122 +301,153 @@ def interpolate_raw_text(
296301 )
297302
298303
299- type InterpolateEscapableRawTextInfo = tuple [str , Template ]
304+ type InterpolateEscapableRawTextsFromTemplateInfo = tuple [str , Template ]
300305
301306
302- def interpolate_escapable_raw_text (
307+ def interpolate_escapable_raw_texts_from_template (
303308 render_api : RenderService ,
304309 bf : list [str ],
305310 last_container_tag : str | None ,
306311 template : Template ,
307312 ip_info : InterpolateInfo ,
308313) -> RenderQueueItem | None :
309- container_tag , content_t = t .cast (InterpolateEscapableRawTextInfo , ip_info )
314+ """
315+ Interpolate and join a template of escapable raw texts together and escape them.
316+
317+ @NOTE: This interpolator expects a Template.
318+ """
319+ container_tag , content_t = t .cast (
320+ InterpolateEscapableRawTextsFromTemplateInfo , ip_info
321+ )
310322 bf .append (
311323 render_api .escape_html_text (
312324 resolve_text_without_recursion (template , container_tag , content_t )
313325 )
314326 )
315327
316328
317- type InterpolateStructTextInfo = tuple [str , int ]
329+ type InterpolateNormalTextInfo = tuple [str , int ]
318330
319331
320- def interpolate_struct_text (
332+ def interpolate_normal_text_from_interpolation (
321333 render_api : RenderService ,
322334 bf : list [str ],
323335 last_container_tag : str | None ,
324336 template : Template ,
325337 ip_info : InterpolateInfo ,
326338) -> RenderQueueItem | None :
327- container_tag , ip_index = t .cast (InterpolateStructTextInfo , ip_info )
339+ """
340+ Interpolate a single normal text either into structured content or an escaped string.
341+
342+ @NOTE: This expects a SINGLE interpolation referenced via i_index.
343+ """
344+ container_tag , ip_index = t .cast (InterpolateNormalTextInfo , ip_info )
328345 value = format_interpolation (template .interpolations [ip_index ])
329- return interpolate_text (
346+ return interpolate_normal_text_from_value (
330347 render_api , bf , last_container_tag , template , (container_tag , value )
331348 )
332349
333350
334- # @TODO: can we coerce this to still use typing even if we just str() everything ? -- `| object`
335- type UserTextValueItem = None | str | Template | HasHTMLDunder
336- # @TODO: See above about `| object`
337- type UserTextValue = (
338- UserTextValueItem | Sequence [UserTextValueItem ] | t .Iterable [UserTextValueItem ]
339- )
351+ type InterpolateNormalTextValueInfo = tuple [str | None , object ]
340352
341353
342- def interpolate_user_text (
354+ def interpolate_normal_text_from_value (
343355 render_api : RenderService ,
344356 bf : list [str ],
345357 last_container_tag : str | None ,
346358 template : Template ,
347359 ip_info : InterpolateInfo ,
348360) -> RenderQueueItem | None :
349- return interpolate_text (render_api , bf , last_container_tag , template , ip_info )
361+ """
362+ Resolve a single text value interpolated within a normal element.
363+
364+ @NOTE: This could be a str(), None, Iterable, Template or HasHTMLDunder.
365+ """
366+ container_tag , value = t .cast (InterpolateNormalTextValueInfo , ip_info )
367+ if container_tag is None :
368+ container_tag = last_container_tag
350369
370+ if isinstance (value , str ):
371+ # @NOTE: Must be subclass of str, if you want __html__ to work you need
372+ # to wrap that object in markupsafe.Markup.
373+ bf .append (render_api .escape_html_text (value ))
374+ elif isinstance (value , Template ):
375+ return (
376+ container_tag ,
377+ iter (
378+ render_api .walk_template (
379+ bf , value , render_api .transform_api .transform_template (value )
380+ )
381+ ),
382+ )
383+ elif isinstance (value , Iterable ):
384+ # If we don't guard against str instances then str subclasses will be
385+ # treated as a sequence/iterable of chars.
386+ return (
387+ container_tag ,
388+ (
389+ (
390+ interpolate_normal_text_from_value ,
391+ template ,
392+ (container_tag , v if v is not None else None ),
393+ )
394+ for v in t .cast (Iterable , value )
395+ ),
396+ )
397+ elif value is None :
398+ # @DESIGN: Ignore None.
399+ return
400+ else :
401+ # @DESIGN: Everything that isn't an object we recognize is
402+ # coerced to a str() and emitted.
403+ bf .append (render_api .escape_html_text (str (value )))
351404
352- type InterpolateTextInfo = tuple [str , object ]
353405
406+ type InterpolateDynamicTextsFromTemplateInfo = tuple [None , Template ]
354407
355- def interpolate_text (
408+
409+ def interpolate_dynamic_texts_from_template (
356410 render_api : RenderService ,
357411 bf : list [str ],
358412 last_container_tag : str | None ,
359413 template : Template ,
360414 ip_info : InterpolateInfo ,
361415) -> RenderQueueItem | None :
362- container_tag , value = t .cast (InterpolateTextInfo , ip_info )
416+ container_tag , text_t = t .cast (InterpolateDynamicTextsFromTemplateInfo , ip_info )
417+ # Try to use the dynamic container if possible.
363418 if container_tag is None :
364419 container_tag = last_container_tag
365-
366- if type (value ) is str : # ONLY native strings, not Markup/__html__, etc.
367- if container_tag not in ("style" , "script" , "title" , "textarea" , "<!--" ):
368- bf .append (render_api .escape_html_text (value ))
369- else :
370- # @TODO: How could this happen?
371- raise ValueError (
372- f"We cannot escape text within { container_tag } when multiple interpolations could occur."
373- )
374- # bf.append(render_api.escape_html_content_in_tag(container_tag, str(value)))
420+ if container_tag is None :
421+ raise NotImplementedError (
422+ "We cannot interpolate texts without knowing what tag they are contained in."
423+ )
424+ elif container_tag in CDATA_CONTENT_ELEMENTS :
425+ return interpolate_raw_texts_from_template (
426+ render_api , bf , last_container_tag , template , (container_tag , text_t )
427+ )
428+ elif container_tag in RCDATA_CONTENT_ELEMENTS :
429+ return interpolate_escapable_raw_texts_from_template (
430+ render_api , bf , last_container_tag , template , (container_tag , text_t )
431+ )
375432 else :
376- if isinstance (value , Template ):
377- return (
378- container_tag ,
379- iter (
380- render_api .walk_template (
381- bf , value , render_api .transform_api .transform_template (value )
382- )
383- ),
384- )
385- elif not isinstance (value , str ) and (
386- isinstance (value , t .Sequence ) or hasattr (value , "__iter__" )
387- ):
388- # If we don't guard against str instances then str subclasses will be
389- # treated as a sequence/iterable of chars.
390- return (
391- container_tag ,
392- (
393- (
394- interpolate_user_text ,
395- template ,
396- (container_tag , v if v is not None else None ),
397- )
398- for v in t .cast (Iterable , value )
399- ),
400- )
401- elif value is None :
402- # @DESIGN: Ignore None.
403- return
433+ return (
434+ container_tag ,
435+ iter (walk_dynamic_template (bf , template , text_t , container_tag )),
436+ )
437+
438+
439+ def walk_dynamic_template (
440+ bf : list [str ], template : Template , text_t : Template , container_tag : str
441+ ) -> t .Iterable [tuple [InterpolatorProto , Template , InterpolateInfo ]]:
442+ """
443+ Walk a text template when we determine we can at runtime.
444+ """
445+ for part in text_t :
446+ if isinstance (part , str ):
447+ bf .append (part )
404448 else :
405- if container_tag not in ("style" , "script" , "title" , "textarea" , "<!--" ):
406- if hasattr (value , "__html__" ):
407- value = t .cast (HasHTMLDunder , value )
408- bf .append (value .__html__ ())
409- else :
410- # @DESIGN: Everything that isn't an object we recognize is
411- # coerced to a str() and emitted.
412- bf .append (render_api .escape_html_text (str (value )))
413- else :
414- raise NotImplementedError ("We cannot handle text escaping here." )
449+ ip_info = (container_tag , part .value )
450+ yield (interpolate_normal_text_from_interpolation , template , ip_info )
415451
416452
417453@dataclass (frozen = True )
@@ -474,27 +510,45 @@ def _stream_component_interpolation(
474510 )
475511 return Interpolation ((interpolate_component , info ), "" , None , "tdom_component" )
476512
477- def _stream_raw_text_interpolation (self , last_container_tag , text_t ):
513+ def _stream_raw_texts_interpolation (
514+ self , last_container_tag : str , text_t : Template
515+ ):
478516 info = (last_container_tag , text_t )
479517 return Interpolation (
480- (interpolate_raw_text , info ), "" , None , "html_raw_text_template "
518+ (interpolate_raw_texts_from_template , info ), "" , None , "html_raw_texts "
481519 )
482520
483- def _stream_escapable_raw_text_interpolation (self , last_container_tag , text_t ):
521+ def _stream_escapable_raw_texts_interpolation (
522+ self , last_container_tag : str , text_t : Template
523+ ):
484524 info = (last_container_tag , text_t )
485525 return Interpolation (
486- (interpolate_escapable_raw_text , info ),
526+ (interpolate_escapable_raw_texts_from_template , info ),
487527 "" ,
488528 None ,
489- "html_escapable_raw_text_template " ,
529+ "html_escapable_raw_texts " ,
490530 )
491531
492- def _stream_text_interpolation (
493- self , last_container_tag : str | None , values_index : int
532+ def _stream_normal_text_interpolation (
533+ self , last_container_tag : str , values_index : int
494534 ):
495535 info = (last_container_tag , values_index )
496536 return Interpolation (
497- (interpolate_struct_text , info ), "" , None , "html_normal_interpolation"
537+ (interpolate_normal_text_from_interpolation , info ),
538+ "" ,
539+ None ,
540+ "html_normal_text" ,
541+ )
542+
543+ def _stream_dynamic_texts_interpolation (
544+ self , last_container_tag : None , text_t : Template
545+ ):
546+ info = (last_container_tag , text_t )
547+ return Interpolation (
548+ (interpolate_dynamic_texts_from_template , info ),
549+ "" ,
550+ None ,
551+ "html_dynamic_text" ,
498552 )
499553
500554 def streamer (
@@ -563,14 +617,22 @@ def streamer(
563617 for part in iter (ref )
564618 ]
565619 )
566- if last_container_tag in CDATA_CONTENT_ELEMENTS :
620+ if ref .is_literal :
621+ yield ref .strings [0 ] # Trust literals.
622+ elif last_container_tag is None :
623+ # We can't know how to handle this right now, so wait until render time and if
624+ # we still cannot know then probably fail.
625+ yield self ._stream_dynamic_texts_interpolation (
626+ last_container_tag , text_t
627+ )
628+ elif last_container_tag in CDATA_CONTENT_ELEMENTS :
567629 # Must be handled all at once.
568- yield self ._stream_raw_text_interpolation (
630+ yield self ._stream_raw_texts_interpolation (
569631 last_container_tag , text_t
570632 )
571633 elif last_container_tag in RCDATA_CONTENT_ELEMENTS :
572634 # We can handle all at once because there are no non-text children and everything must be string-ified.
573- yield self ._stream_escapable_raw_text_interpolation (
635+ yield self ._stream_escapable_raw_texts_interpolation (
574636 last_container_tag , text_t
575637 )
576638 else :
@@ -580,7 +642,7 @@ def streamer(
580642 if isinstance (part , str ):
581643 yield part
582644 else :
583- yield self ._stream_text_interpolation (
645+ yield self ._stream_normal_text_interpolation (
584646 last_container_tag , part .value
585647 )
586648 case _:
0 commit comments