Coverage for /run/media/veganeco/water/status600.com/pypi/reptilian_climates/modules_series_2/ships/fields/gardens_pip/rich/markup.py: 21%

1 import re

2 from ast import literal_eval

3 from operator import attrgetter

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

5

6 from ._emoji_replace import _emoji_replace

7 from .emoji import EmojiVariant

8 from .errors import MarkupError

9 from .style import Style

10 from .text import Span, Text

11

12 RE_TAGS = re.compile(

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

14 re.VERBOSE,

15)

16

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

18

19

20 class 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

48 def 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

73 def _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

106 def 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 style: (Union[str, Style]): The style to use.

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

118 emoji_variant (str, optional): Optional emoji variant, either "text" or "emoji". Defaults to None.

119

120

121 Raises:

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

123

124 Returns:

125 Text: A test instance.

126 """

127 emoji_replace = _emoji_replace

128 if "[" not in markup:

129 return Text(

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

131 style=style,

132 )

133 text = Text(style=style)

134 append = text.append

135 normalize = Style.normalize

136

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

138 pop = style_stack.pop

139

140 spans: List[Span] = []

141 append_span = spans.append

142

143 _Span = Span

144 _Tag = Tag

145

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

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

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

149 if tag.name == style_name:

150 return pop(-index)

151 raise KeyError(style_name)

152

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

154 if plain_text is not None:

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

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

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

158 elif tag is not None:

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

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

161

162 if style_name: # explicit close

163 style_name = normalize(style_name)

164 try:

165 start, open_tag = pop_style(style_name)

166 except KeyError:

167 raise MarkupError(

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

169 ) from None

170 else: # implicit close

171 try:

172 start, open_tag = pop()

173 except IndexError:

174 raise MarkupError(

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

176 ) from None

177

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

179 if open_tag.parameters:

180 handler_name = ""

181 parameters = open_tag.parameters.strip()

182 handler_match = RE_HANDLER.match(parameters)

183 if handler_match is not None:

184 handler_name, match_parameters = handler_match.groups()

185 parameters = (

186 "()" if match_parameters is None else match_parameters

187 )

188

189 try:

190 meta_params = literal_eval(parameters)

191 except SyntaxError as error:

192 raise MarkupError(

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

194 )

195 except Exception as error:

196 raise MarkupError(

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

198 ) from None

199

200 if handler_name:

201 meta_params = (

202 handler_name,

203 meta_params

204 if isinstance(meta_params, tuple)

205 else (meta_params,),

206 )

207

208 else:

209 meta_params = ()

210

211 append_span(

212 _Span(

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

214 )

215 )

216 else:

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

218

219 else: # Opening tag

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

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

222

223 text_length = len(text)

224 while style_stack:

225 start, tag = style_stack.pop()

226 style = str(tag)

227 if style:

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

229

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

231 return text

232

233

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

235 MARKUP = [

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

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

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

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

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

241 ]

242

243 from rich import print

244 from rich.table import Table

245

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

247

248 for markup in MARKUP:

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

250

251 print(grid)