1"""
2Functions for working with "safe strings": strings that can be displayed safely
3without further escaping in HTML. Marking something as a "safe string" means
4that the producer of the string has already turned characters that should not
5be interpreted by the HTML engine (e.g. '<') into the appropriate entities.
6"""
7
8from collections.abc import Callable
9from functools import wraps
10from typing import Any
11
12from plain.utils.functional import keep_lazy
13
14
15class SafeData:
16 __slots__ = ()
17
18 def __html__(self) -> SafeData:
19 """
20 Return the html representation of a string for interoperability.
21
22 This allows other template engines to understand Plain's SafeData.
23 """
24 return self
25
26
27class SafeString(str, SafeData):
28 """
29 A str subclass that has been specifically marked as "safe" for HTML output
30 purposes.
31 """
32
33 __slots__ = ()
34
35 def __add__(self, rhs: str) -> SafeString | str:
36 """
37 Concatenating a safe string with another safe bytestring or
38 safe string is safe. Otherwise, the result is no longer safe.
39 """
40 t = super().__add__(rhs)
41 if isinstance(rhs, SafeData):
42 return SafeString(t)
43 return t
44
45 def __str__(self) -> str:
46 return self
47
48
49def _safety_decorator[T](
50 safety_marker: Callable[[Any], T], func: Callable[..., Any]
51) -> Callable[..., T]:
52 @wraps(func)
53 def wrapper(*args: Any, **kwargs: Any) -> T:
54 return safety_marker(func(*args, **kwargs))
55
56 return wrapper
57
58
59@keep_lazy(SafeString)
60def mark_safe(s: Any) -> SafeString | SafeData | Callable[..., Any]:
61 """
62 Explicitly mark a string as safe for (HTML) output purposes. The returned
63 object can be used everywhere a string is appropriate.
64
65 If used on a method as a decorator, mark the returned data as safe.
66
67 Can be called multiple times on a single string.
68 """
69 if hasattr(s, "__html__"):
70 return s
71 if callable(s):
72 return _safety_decorator(mark_safe, s)
73 return SafeString(s)