Skip to content

Commit a93f732

Browse files
Make text within certain tags more explicit.
1 parent 747cbca commit a93f732

File tree

1 file changed

+143
-81
lines changed

1 file changed

+143
-81
lines changed

tdom/transformer.py

Lines changed: 143 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)