Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/prompt_toolkit/formatted_text/html.py: 22%

74 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-20 06:09 +0000

1from __future__ import annotations 

2 

3import xml.dom.minidom as minidom 

4from string import Formatter 

5from typing import Any 

6 

7from .base import FormattedText, StyleAndTextTuples 

8 

9__all__ = ["HTML"] 

10 

11 

12class HTML: 

13 """ 

14 HTML formatted text. 

15 Take something HTML-like, for use as a formatted string. 

16 

17 :: 

18 

19 # Turn something into red. 

20 HTML('<style fg="ansired" bg="#00ff44">...</style>') 

21 

22 # Italic, bold, underline and strike. 

23 HTML('<i>...</i>') 

24 HTML('<b>...</b>') 

25 HTML('<u>...</u>') 

26 HTML('<s>...</s>') 

27 

28 All HTML elements become available as a "class" in the style sheet. 

29 E.g. ``<username>...</username>`` can be styled, by setting a style for 

30 ``username``. 

31 """ 

32 

33 def __init__(self, value: str) -> None: 

34 self.value = value 

35 document = minidom.parseString(f"<html-root>{value}</html-root>") 

36 

37 result: StyleAndTextTuples = [] 

38 name_stack: list[str] = [] 

39 fg_stack: list[str] = [] 

40 bg_stack: list[str] = [] 

41 

42 def get_current_style() -> str: 

43 "Build style string for current node." 

44 parts = [] 

45 if name_stack: 

46 parts.append("class:" + ",".join(name_stack)) 

47 

48 if fg_stack: 

49 parts.append("fg:" + fg_stack[-1]) 

50 if bg_stack: 

51 parts.append("bg:" + bg_stack[-1]) 

52 return " ".join(parts) 

53 

54 def process_node(node: Any) -> None: 

55 "Process node recursively." 

56 for child in node.childNodes: 

57 if child.nodeType == child.TEXT_NODE: 

58 result.append((get_current_style(), child.data)) 

59 else: 

60 add_to_name_stack = child.nodeName not in ( 

61 "#document", 

62 "html-root", 

63 "style", 

64 ) 

65 fg = bg = "" 

66 

67 for k, v in child.attributes.items(): 

68 if k == "fg": 

69 fg = v 

70 if k == "bg": 

71 bg = v 

72 if k == "color": 

73 fg = v # Alias for 'fg'. 

74 

75 # Check for spaces in attributes. This would result in 

76 # invalid style strings otherwise. 

77 if " " in fg: 

78 raise ValueError('"fg" attribute contains a space.') 

79 if " " in bg: 

80 raise ValueError('"bg" attribute contains a space.') 

81 

82 if add_to_name_stack: 

83 name_stack.append(child.nodeName) 

84 if fg: 

85 fg_stack.append(fg) 

86 if bg: 

87 bg_stack.append(bg) 

88 

89 process_node(child) 

90 

91 if add_to_name_stack: 

92 name_stack.pop() 

93 if fg: 

94 fg_stack.pop() 

95 if bg: 

96 bg_stack.pop() 

97 

98 process_node(document) 

99 

100 self.formatted_text = FormattedText(result) 

101 

102 def __repr__(self) -> str: 

103 return f"HTML({self.value!r})" 

104 

105 def __pt_formatted_text__(self) -> StyleAndTextTuples: 

106 return self.formatted_text 

107 

108 def format(self, *args: object, **kwargs: object) -> HTML: 

109 """ 

110 Like `str.format`, but make sure that the arguments are properly 

111 escaped. 

112 """ 

113 return HTML(FORMATTER.vformat(self.value, args, kwargs)) 

114 

115 def __mod__(self, value: object) -> HTML: 

116 """ 

117 HTML('<b>%s</b>') % value 

118 """ 

119 if not isinstance(value, tuple): 

120 value = (value,) 

121 

122 value = tuple(html_escape(i) for i in value) 

123 return HTML(self.value % value) 

124 

125 

126class HTMLFormatter(Formatter): 

127 def format_field(self, value: object, format_spec: str) -> str: 

128 return html_escape(format(value, format_spec)) 

129 

130 

131def html_escape(text: object) -> str: 

132 # The string interpolation functions also take integers and other types. 

133 # Convert to string first. 

134 if not isinstance(text, str): 

135 text = f"{text}" 

136 

137 return ( 

138 text.replace("&", "&amp;") 

139 .replace("<", "&lt;") 

140 .replace(">", "&gt;") 

141 .replace('"', "&quot;") 

142 ) 

143 

144 

145FORMATTER = HTMLFormatter()