Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/rich/markup.py: 20%

117 statements  

« prev     ^ index     » next       coverage.py v7.3.1, created at 2023-09-18 06:13 +0000

1import re 

2from ast import literal_eval 

3from operator import attrgetter 

4from typing import Callable, Iterable, List, Match, NamedTuple, Optional, Tuple, Union 

5 

6from ._emoji_replace import _emoji_replace 

7from .emoji import EmojiVariant 

8from .errors import MarkupError 

9from .style import Style 

10from .text import Span, Text 

11 

12RE_TAGS = re.compile( 

13 r"""((\\*)\[([a-z#/@][^[]*?)])""", 

14 re.VERBOSE, 

15) 

16 

17RE_HANDLER = re.compile(r"^([\w.]*?)(\(.*?\))?$") 

18 

19 

20class Tag(NamedTuple): 

21 """A tag in console markup.""" 

22 

23 name: str 

24 """The tag name. e.g. 'bold'.""" 

25 parameters: Optional[str] 

26 """Any additional parameters after the name.""" 

27 

28 def __str__(self) -> str: 

29 return ( 

30 self.name if self.parameters is None else f"{self.name} {self.parameters}" 

31 ) 

32 

33 @property 

34 def markup(self) -> str: 

35 """Get the string representation of this tag.""" 

36 return ( 

37 f"[{self.name}]" 

38 if self.parameters is None 

39 else f"[{self.name}={self.parameters}]" 

40 ) 

41 

42 

43_ReStringMatch = Match[str] # regex match object 

44_ReSubCallable = Callable[[_ReStringMatch], str] # Callable invoked by re.sub 

45_EscapeSubMethod = Callable[[_ReSubCallable, str], str] # Sub method of a compiled re 

46 

47 

48def escape( 

49 markup: str, 

50 _escape: _EscapeSubMethod = re.compile(r"(\\*)(\[[a-z#/@][^[]*?])").sub, 

51) -> str: 

52 """Escapes text so that it won't be interpreted as markup. 

53 

54 Args: 

55 markup (str): Content to be inserted in to markup. 

56 

57 Returns: 

58 str: Markup with square brackets escaped. 

59 """ 

60 

61 def escape_backslashes(match: Match[str]) -> str: 

62 """Called by re.sub replace matches.""" 

63 backslashes, text = match.groups() 

64 return f"{backslashes}{backslashes}\\{text}" 

65 

66 markup = _escape(escape_backslashes, markup) 

67 if markup.endswith("\\") and not markup.endswith("\\\\"): 

68 return markup + "\\" 

69 

70 return markup 

71 

72 

73def _parse(markup: str) -> Iterable[Tuple[int, Optional[str], Optional[Tag]]]: 

74 """Parse markup in to an iterable of tuples of (position, text, tag). 

75 

76 Args: 

77 markup (str): A string containing console markup 

78 

79 """ 

80 position = 0 

81 _divmod = divmod 

82 _Tag = Tag 

83 for match in RE_TAGS.finditer(markup): 

84 full_text, escapes, tag_text = match.groups() 

85 start, end = match.span() 

86 if start > position: 

87 yield start, markup[position:start], None 

88 if escapes: 

89 backslashes, escaped = _divmod(len(escapes), 2) 

90 if backslashes: 

91 # Literal backslashes 

92 yield start, "\\" * backslashes, None 

93 start += backslashes * 2 

94 if escaped: 

95 # Escape of tag 

96 yield start, full_text[len(escapes) :], None 

97 position = end 

98 continue 

99 text, equals, parameters = tag_text.partition("=") 

100 yield start, None, _Tag(text, parameters if equals else None) 

101 position = end 

102 if position < len(markup): 

103 yield position, markup[position:], None 

104 

105 

106def render( 

107 markup: str, 

108 style: Union[str, Style] = "", 

109 emoji: bool = True, 

110 emoji_variant: Optional[EmojiVariant] = None, 

111) -> Text: 

112 """Render console markup in to a Text instance. 

113 

114 Args: 

115 markup (str): A string containing console markup. 

116 emoji (bool, optional): Also render emoji code. Defaults to True. 

117 

118 Raises: 

119 MarkupError: If there is a syntax error in the markup. 

120 

121 Returns: 

122 Text: A test instance. 

123 """ 

124 emoji_replace = _emoji_replace 

125 if "[" not in markup: 

126 return Text( 

127 emoji_replace(markup, default_variant=emoji_variant) if emoji else markup, 

128 style=style, 

129 ) 

130 text = Text(style=style) 

131 append = text.append 

132 normalize = Style.normalize 

133 

134 style_stack: List[Tuple[int, Tag]] = [] 

135 pop = style_stack.pop 

136 

137 spans: List[Span] = [] 

138 append_span = spans.append 

139 

140 _Span = Span 

141 _Tag = Tag 

142 

143 def pop_style(style_name: str) -> Tuple[int, Tag]: 

144 """Pop tag matching given style name.""" 

145 for index, (_, tag) in enumerate(reversed(style_stack), 1): 

146 if tag.name == style_name: 

147 return pop(-index) 

148 raise KeyError(style_name) 

149 

150 for position, plain_text, tag in _parse(markup): 

151 if plain_text is not None: 

152 # Handle open brace escapes, where the brace is not part of a tag. 

153 plain_text = plain_text.replace("\\[", "[") 

154 append(emoji_replace(plain_text) if emoji else plain_text) 

155 elif tag is not None: 

156 if tag.name.startswith("/"): # Closing tag 

157 style_name = tag.name[1:].strip() 

158 

159 if style_name: # explicit close 

160 style_name = normalize(style_name) 

161 try: 

162 start, open_tag = pop_style(style_name) 

163 except KeyError: 

164 raise MarkupError( 

165 f"closing tag '{tag.markup}' at position {position} doesn't match any open tag" 

166 ) from None 

167 else: # implicit close 

168 try: 

169 start, open_tag = pop() 

170 except IndexError: 

171 raise MarkupError( 

172 f"closing tag '[/]' at position {position} has nothing to close" 

173 ) from None 

174 

175 if open_tag.name.startswith("@"): 

176 if open_tag.parameters: 

177 handler_name = "" 

178 parameters = open_tag.parameters.strip() 

179 handler_match = RE_HANDLER.match(parameters) 

180 if handler_match is not None: 

181 handler_name, match_parameters = handler_match.groups() 

182 parameters = ( 

183 "()" if match_parameters is None else match_parameters 

184 ) 

185 

186 try: 

187 meta_params = literal_eval(parameters) 

188 except SyntaxError as error: 

189 raise MarkupError( 

190 f"error parsing {parameters!r} in {open_tag.parameters!r}; {error.msg}" 

191 ) 

192 except Exception as error: 

193 raise MarkupError( 

194 f"error parsing {open_tag.parameters!r}; {error}" 

195 ) from None 

196 

197 if handler_name: 

198 meta_params = ( 

199 handler_name, 

200 meta_params 

201 if isinstance(meta_params, tuple) 

202 else (meta_params,), 

203 ) 

204 

205 else: 

206 meta_params = () 

207 

208 append_span( 

209 _Span( 

210 start, len(text), Style(meta={open_tag.name: meta_params}) 

211 ) 

212 ) 

213 else: 

214 append_span(_Span(start, len(text), str(open_tag))) 

215 

216 else: # Opening tag 

217 normalized_tag = _Tag(normalize(tag.name), tag.parameters) 

218 style_stack.append((len(text), normalized_tag)) 

219 

220 text_length = len(text) 

221 while style_stack: 

222 start, tag = style_stack.pop() 

223 style = str(tag) 

224 if style: 

225 append_span(_Span(start, text_length, style)) 

226 

227 text.spans = sorted(spans[::-1], key=attrgetter("start")) 

228 return text 

229 

230 

231if __name__ == "__main__": # pragma: no cover 

232 MARKUP = [ 

233 "[red]Hello World[/red]", 

234 "[magenta]Hello [b]World[/b]", 

235 "[bold]Bold[italic] bold and italic [/bold]italic[/italic]", 

236 "Click [link=https://www.willmcgugan.com]here[/link] to visit my Blog", 

237 ":warning-emoji: [bold red blink] DANGER![/]", 

238 ] 

239 

240 from rich import print 

241 from rich.table import Table 

242 

243 grid = Table("Markup", "Result", padding=(0, 1)) 

244 

245 for markup in MARKUP: 

246 grid.add_row(Text(markup), markup) 

247 

248 print(grid)