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