5
5
"""
6
6
7
7
import re
8
+ from functools import partial
8
9
from html import escape as html_escape
9
10
from urllib .parse import quote
10
11
11
12
import typing as t
12
13
13
- from jinja2 .runtime import Undefined
14
+ from jinja2 .runtime import Context , Undefined
15
+ from jinja2 .utils import pass_context
14
16
15
17
from antsibull_core .logging import log
16
18
19
+ from ..semantic_helper import parse_option , parse_return_value , augment_plugin_name_type
20
+
17
21
18
22
mlog = log .fields (mod = __name__ )
19
23
22
26
_ITALIC = re .compile (r"\bI\(([^)]+)\)" )
23
27
_BOLD = re .compile (r"\bB\(([^)]+)\)" )
24
28
_MODULE = re .compile (r"\bM\(([^).]+)\.([^).]+)\.([^)]+)\)" )
29
+ _PLUGIN = re .compile (r"\bP\(([^).]+)\.([^).]+)\.([^)]+)#([a-z]+)\)" )
25
30
_URL = re .compile (r"\bU\(([^)]+)\)" )
26
31
_LINK = re .compile (r"\bL\(([^)]+), *([^)]+)\)" )
27
32
_REF = re .compile (r"\bR\(([^)]+), *([^)]+)\)" )
28
33
_CONST = re .compile (r"\bC\(([^)]+)\)" )
34
+ _SEM_PARAMETER_STRING = r"\(((?:[^\\)]+|\\.)+)\)"
35
+ _SEM_OPTION_NAME = re .compile (r"\bO" + _SEM_PARAMETER_STRING )
36
+ _SEM_OPTION_VALUE = re .compile (r"\bV" + _SEM_PARAMETER_STRING )
37
+ _SEM_ENV_VARIABLE = re .compile (r"\bE" + _SEM_PARAMETER_STRING )
38
+ _SEM_RET_VALUE = re .compile (r"\bRV" + _SEM_PARAMETER_STRING )
29
39
_RULER = re .compile (r"\bHORIZONTALLINE\b" )
40
+ _UNESCAPE = re .compile (r"\\(.)" )
30
41
31
42
_EMAIL_ADDRESS = re .compile (r"(?:<{mail}>|\({mail}\)|{mail})" .format (mail = r"[\w.+-]+@[\w.-]+\.\w+" ))
32
43
33
44
34
- def html_ify (text ):
45
+ def extract_plugin_data (context : Context ) -> t .Tuple [t .Optional [str ], t .Optional [str ]]:
46
+ plugin_fqcn = context .get ('plugin_name' )
47
+ plugin_type = context .get ('plugin_type' )
48
+ if plugin_fqcn is None or plugin_type is None :
49
+ return None , None
50
+ # if plugin_type == 'role':
51
+ # entry_point = context.get('entry_point', 'main')
52
+ # # FIXME: use entry_point
53
+ return plugin_fqcn , plugin_type
54
+
55
+
56
+ def _unescape_sem_value (text : str ) -> str :
57
+ return _UNESCAPE .sub (r'\1' , text )
58
+
59
+
60
+ def _check_plugin (plugin_fqcn : t .Optional [str ], plugin_type : t .Optional [str ],
61
+ matcher : 're.Match' ) -> None :
62
+ if plugin_fqcn is None or plugin_type is None :
63
+ raise Exception (f'The markup { matcher .group (0 )} cannot be used outside a plugin or role' )
64
+
65
+
66
+ def _create_error (text : str , error : str ) -> str : # pylint:disable=unused-argument
67
+ return '...' # FIXME
68
+
69
+
70
+ def _option_name_html (plugin_fqcn : t .Optional [str ], plugin_type : t .Optional [str ],
71
+ matcher : 're.Match' ) -> str :
72
+ _check_plugin (plugin_fqcn , plugin_type , matcher )
73
+ text = _unescape_sem_value (matcher .group (1 ))
74
+ try :
75
+ plugin_fqcn , plugin_type , option_link , option , value = parse_option (
76
+ text , plugin_fqcn , plugin_type , require_plugin = False )
77
+ except ValueError as exc :
78
+ return _create_error (text , str (exc ))
79
+ if value is None :
80
+ cls = 'ansible-option'
81
+ text = f'{ option } '
82
+ strong_start = '<strong>'
83
+ strong_end = '</strong>'
84
+ else :
85
+ cls = 'ansible-option-value'
86
+ text = f'{ option } ={ value } '
87
+ strong_start = ''
88
+ strong_end = ''
89
+ if plugin_fqcn and plugin_type and plugin_fqcn .count ('.' ) >= 2 :
90
+ # TODO: handle role arguments (entrypoint!)
91
+ namespace , name , plugin = plugin_fqcn .split ('.' , 2 )
92
+ url = f'../../{ namespace } /{ name } /{ plugin } _{ plugin_type } .html'
93
+ fragment = f'parameter-{ quote (option_link .replace ("." , "/" ))} '
94
+ link_start = (
95
+ f'<a class="reference internal" href="{ url } #{ fragment } ">'
96
+ '<span class="std std-ref"><span class="pre">'
97
+ )
98
+ link_end = '</span></span></a>'
99
+ else :
100
+ link_start = ''
101
+ link_end = ''
102
+ return (
103
+ f'<code class="{ cls } literal notranslate">'
104
+ f'{ strong_start } { link_start } { text } { link_end } { strong_end } </code>'
105
+ )
106
+
107
+
108
+ def _return_value_html (plugin_fqcn : t .Optional [str ], plugin_type : t .Optional [str ],
109
+ matcher : 're.Match' ) -> str :
110
+ _check_plugin (plugin_fqcn , plugin_type , matcher )
111
+ text = _unescape_sem_value (matcher .group (1 ))
112
+ try :
113
+ plugin_fqcn , plugin_type , rv_link , rv , value = parse_return_value (
114
+ text , plugin_fqcn , plugin_type , require_plugin = False )
115
+ except ValueError as exc :
116
+ return _create_error (text , str (exc ))
117
+ cls = 'ansible-return-value'
118
+ if value is None :
119
+ text = f'{ rv } '
120
+ else :
121
+ text = f'{ rv } ={ value } '
122
+ if plugin_fqcn and plugin_type and plugin_fqcn .count ('.' ) >= 2 :
123
+ namespace , name , plugin = plugin_fqcn .split ('.' , 2 )
124
+ url = f'../../{ namespace } /{ name } /{ plugin } _{ plugin_type } .html'
125
+ fragment = f'return-{ quote (rv_link .replace ("." , "/" ))} '
126
+ link_start = (
127
+ f'<a class="reference internal" href="{ url } #{ fragment } ">'
128
+ '<span class="std std-ref"><span class="pre">'
129
+ )
130
+ link_end = '</span></span></a>'
131
+ else :
132
+ link_start = ''
133
+ link_end = ''
134
+ return f'<code class="{ cls } literal notranslate">{ link_start } { text } { link_end } </code>'
135
+
136
+
137
+ def _value_html (matcher : 're.Match' ) -> str :
138
+ text = _unescape_sem_value (matcher .group (1 ))
139
+ return f'<code class="ansible-value literal notranslate">{ text } </code>'
140
+
141
+
142
+ def _env_var_html (matcher : 're.Match' ) -> str :
143
+ text = _unescape_sem_value (matcher .group (1 ))
144
+ return f'<code class="xref std std-envvar literal notranslate">{ text } </code>'
145
+
146
+
147
+ @pass_context
148
+ def html_ify (context : Context , text : str ) -> str :
35
149
''' convert symbols like I(this is in italics) to valid HTML '''
36
150
37
151
flog = mlog .fields (func = 'html_ify' )
38
152
flog .fields (text = text ).debug ('Enter' )
39
153
_counts = {}
40
154
155
+ plugin_fqcn , plugin_type = extract_plugin_data (context )
156
+
41
157
text = html_escape (text )
42
158
text , _counts ['italic' ] = _ITALIC .subn (r"<em>\1</em>" , text )
43
159
text , _counts ['bold' ] = _BOLD .subn (r"<b>\1</b>" , text )
44
160
text , _counts ['module' ] = _MODULE .subn (
45
161
r"<a href='../../\1/\2/\3_module.html' class='module'>\1.\2.\3</a>" , text )
162
+ text , _counts ['plugin' ] = _PLUGIN .subn (
163
+ r"<a href='../../\1/\2/\3_\4.html' class='module plugin-\4'>\1.\2.\3</span>" , text )
46
164
text , _counts ['url' ] = _URL .subn (r"<a href='\1'>\1</a>" , text )
47
165
text , _counts ['ref' ] = _REF .subn (r"<span class='module'>\1</span>" , text )
48
166
text , _counts ['link' ] = _LINK .subn (r"<a href='\2'>\1</a>" , text )
49
167
text , _counts ['const' ] = _CONST .subn (
50
168
r"<code class='docutils literal notranslate'>\1</code>" , text )
169
+ text , _counts ['option-name' ] = _SEM_OPTION_NAME .subn (
170
+ partial (_option_name_html , plugin_fqcn , plugin_type ), text )
171
+ text , _counts ['option-value' ] = _SEM_OPTION_VALUE .subn (_value_html , text )
172
+ text , _counts ['environment-var' ] = _SEM_ENV_VARIABLE .subn (_env_var_html , text )
173
+ text , _counts ['return-value' ] = _SEM_RET_VALUE .subn (
174
+ partial (_return_value_html , plugin_fqcn , plugin_type ), text )
51
175
text , _counts ['ruler' ] = _RULER .subn (r"<hr/>" , text )
52
176
53
177
text = text .strip ()
@@ -95,6 +219,12 @@ def _rst_ify_module(m: 're.Match') -> str:
95
219
return f"\\ :ref:`{ rst_escape (fqcn )} <ansible_collections.{ fqcn } _module>`\\ "
96
220
97
221
222
+ def _rst_ify_plugin (m : 're.Match' ) -> str :
223
+ fqcn = f'{ m .group (1 )} .{ m .group (2 )} .{ m .group (3 )} '
224
+ plugin_type = m .group (4 )
225
+ return f"\\ :ref:`{ rst_escape (fqcn )} <ansible_collections.{ fqcn } _{ plugin_type } >`\\ "
226
+
227
+
98
228
def _escape_url (url : str ) -> str :
99
229
# We include '<>[]{}' in safe to allow urls such as 'https://<HOST>:[PORT]/v{version}/' to
100
230
# remain unmangled by percent encoding
@@ -118,20 +248,56 @@ def _rst_ify_const(m: 're.Match') -> str:
118
248
return f"\\ :literal:`{ rst_escape (m .group (1 ), escape_ending_whitespace = True )} `\\ "
119
249
120
250
121
- def rst_ify (text ):
251
+ def _rst_ify_option_name (plugin_fqcn : t .Optional [str ], plugin_type : t .Optional [str ],
252
+ m : 're.Match' ) -> str :
253
+ _check_plugin (plugin_fqcn , plugin_type , m )
254
+ text = _unescape_sem_value (m .group (1 ))
255
+ text = augment_plugin_name_type (text , plugin_fqcn , plugin_type )
256
+ return f"\\ :ansopt:`{ rst_escape (text , escape_ending_whitespace = True )} `\\ "
257
+
258
+
259
+ def _rst_ify_value (m : 're.Match' ) -> str :
260
+ text = _unescape_sem_value (m .group (1 ))
261
+ return f"\\ :ansval:`{ rst_escape (text , escape_ending_whitespace = True )} `\\ "
262
+
263
+
264
+ def _rst_ify_return_value (plugin_fqcn : t .Optional [str ], plugin_type : t .Optional [str ],
265
+ m : 're.Match' ) -> str :
266
+ _check_plugin (plugin_fqcn , plugin_type , m )
267
+ text = _unescape_sem_value (m .group (1 ))
268
+ text = augment_plugin_name_type (text , plugin_fqcn , plugin_type )
269
+ return f"\\ :ansretval:`{ rst_escape (text , escape_ending_whitespace = True )} `\\ "
270
+
271
+
272
+ def _rst_ify_envvar (m : 're.Match' ) -> str :
273
+ text = _unescape_sem_value (m .group (1 ))
274
+ return f"\\ :envvar:`{ rst_escape (text , escape_ending_whitespace = True )} `\\ "
275
+
276
+
277
+ @pass_context
278
+ def rst_ify (context : Context , text : str ) -> str :
122
279
''' convert symbols like I(this is in italics) to valid restructured text '''
123
280
124
281
flog = mlog .fields (func = 'rst_ify' )
125
282
flog .fields (text = text ).debug ('Enter' )
126
283
_counts = {}
127
284
285
+ plugin_fqcn , plugin_type = extract_plugin_data (context )
286
+
128
287
text , _counts ['italic' ] = _ITALIC .subn (_rst_ify_italic , text )
129
288
text , _counts ['bold' ] = _BOLD .subn (_rst_ify_bold , text )
130
289
text , _counts ['module' ] = _MODULE .subn (_rst_ify_module , text )
290
+ text , _counts ['plugin' ] = _PLUGIN .subn (_rst_ify_plugin , text )
131
291
text , _counts ['link' ] = _LINK .subn (_rst_ify_link , text )
132
292
text , _counts ['url' ] = _URL .subn (_rst_ify_url , text )
133
293
text , _counts ['ref' ] = _REF .subn (_rst_ify_ref , text )
134
294
text , _counts ['const' ] = _CONST .subn (_rst_ify_const , text )
295
+ text , _counts ['option-name' ] = _SEM_OPTION_NAME .subn (
296
+ partial (_rst_ify_option_name , plugin_fqcn , plugin_type ), text )
297
+ text , _counts ['option-value' ] = _SEM_OPTION_VALUE .subn (_rst_ify_value , text )
298
+ text , _counts ['environment-var' ] = _SEM_ENV_VARIABLE .subn (_rst_ify_envvar , text )
299
+ text , _counts ['return-value' ] = _SEM_RET_VALUE .subn (
300
+ partial (_rst_ify_return_value , plugin_fqcn , plugin_type ), text )
135
301
text , _counts ['ruler' ] = _RULER .subn ('\n \n .. raw:: html\n \n <hr>\n \n ' , text )
136
302
137
303
flog .fields (counts = _counts ).info ('Number of macros converted to rst equivalents' )
0 commit comments