55"""
66
77import re
8+ from functools import partial
89from html import escape as html_escape
910from urllib .parse import quote
1011
1112import typing as t
1213
13- from jinja2 .runtime import Undefined
14+ from jinja2 .runtime import Context , Undefined
15+ from jinja2 .utils import pass_context
1416
1517from ..logging import log
18+ from ..semantic_helper import parse_option , parse_return_value , augment_plugin_name_type
1619
1720
1821mlog = log .fields (mod = __name__ )
2225_ITALIC = re .compile (r"\bI\(([^)]+)\)" )
2326_BOLD = re .compile (r"\bB\(([^)]+)\)" )
2427_MODULE = re .compile (r"\bM\(([^).]+)\.([^).]+)\.([^)]+)\)" )
28+ _PLUGIN = re .compile (r"\bP\(([^).]+)\.([^).]+)\.([^)]+)#([a-z]+)\)" )
2529_URL = re .compile (r"\bU\(([^)]+)\)" )
2630_LINK = re .compile (r"\bL\(([^)]+), *([^)]+)\)" )
2731_REF = re .compile (r"\bR\(([^)]+), *([^)]+)\)" )
2832_CONST = re .compile (r"\bC\(([^)]+)\)" )
33+ _SEM_PARAMETER_STRING = r"\(((?:[^\\)]+|\\.)+)\)"
34+ _SEM_OPTION_NAME = re .compile (r"\bO" + _SEM_PARAMETER_STRING )
35+ _SEM_OPTION_VALUE = re .compile (r"\bV" + _SEM_PARAMETER_STRING )
36+ _SEM_ENV_VARIABLE = re .compile (r"\bE" + _SEM_PARAMETER_STRING )
37+ _SEM_RET_VALUE = re .compile (r"\bRV" + _SEM_PARAMETER_STRING )
2938_RULER = re .compile (r"\bHORIZONTALLINE\b" )
39+ _UNESCAPE = re .compile (r"\\(.)" )
3040
3141_EMAIL_ADDRESS = re .compile (r"(?:<{mail}>|\({mail}\)|{mail})" .format (mail = r"[\w.+-]+@[\w.-]+\.\w+" ))
3242
3343
34- def html_ify (text ):
44+ def extract_plugin_data (context : Context ) -> t .Tuple [t .Optional [str ], t .Optional [str ]]:
45+ plugin_fqcn = context .get ('plugin_name' )
46+ plugin_type = context .get ('plugin_type' )
47+ if plugin_fqcn is None or plugin_type is None :
48+ return None , None
49+ # if plugin_type == 'role':
50+ # entry_point = context.get('entry_point', 'main')
51+ # # FIXME: use entry_point
52+ return plugin_fqcn , plugin_type
53+
54+
55+ def _unescape_sem_value (text : str ) -> str :
56+ return _UNESCAPE .sub (r'\1' , text )
57+
58+
59+ def _check_plugin (plugin_fqcn : t .Optional [str ], plugin_type : t .Optional [str ],
60+ matcher : 're.Match' ) -> None :
61+ if plugin_fqcn is None or plugin_type is None :
62+ raise Exception (f'The markup { matcher .group (0 )} cannot be used outside a plugin or role' )
63+
64+
65+ def _create_error (text : str , error : str ) -> str : # pylint:disable=unused-argument
66+ return '...' # FIXME
67+
68+
69+ def _option_name_html (plugin_fqcn : t .Optional [str ], plugin_type : t .Optional [str ],
70+ matcher : 're.Match' ) -> str :
71+ _check_plugin (plugin_fqcn , plugin_type , matcher )
72+ text = _unescape_sem_value (matcher .group (1 ))
73+ try :
74+ plugin_fqcn , plugin_type , option_link , option , value = parse_option (
75+ text , plugin_fqcn , plugin_type , require_plugin = False )
76+ except ValueError as exc :
77+ return _create_error (text , str (exc ))
78+ if value is None :
79+ cls = 'ansible-option'
80+ text = f'{ option } '
81+ strong_start = '<strong>'
82+ strong_end = '</strong>'
83+ else :
84+ cls = 'ansible-option-value'
85+ text = f'{ option } ={ value } '
86+ strong_start = ''
87+ strong_end = ''
88+ if plugin_fqcn and plugin_type and plugin_fqcn .count ('.' ) >= 2 :
89+ # TODO: handle role arguments (entrypoint!)
90+ namespace , name , plugin = plugin_fqcn .split ('.' , 2 )
91+ url = f'../../{ namespace } /{ name } /{ plugin } _{ plugin_type } .html'
92+ fragment = f'parameter-{ option_link .replace ("." , "/" )} '
93+ link_start = (
94+ f'<a class="reference internal" href="{ url } #{ fragment } ">'
95+ '<span class="std std-ref"><span class="pre">'
96+ )
97+ link_end = '</span></span></a>'
98+ else :
99+ link_start = ''
100+ link_end = ''
101+ return (
102+ f'<code class="{ cls } literal notranslate">'
103+ f'{ strong_start } { link_start } { text } { link_end } { strong_end } </code>'
104+ )
105+
106+
107+ def _return_value_html (plugin_fqcn : t .Optional [str ], plugin_type : t .Optional [str ],
108+ matcher : 're.Match' ) -> str :
109+ _check_plugin (plugin_fqcn , plugin_type , matcher )
110+ text = _unescape_sem_value (matcher .group (1 ))
111+ try :
112+ plugin_fqcn , plugin_type , rv_link , rv , value = parse_return_value (
113+ text , plugin_fqcn , plugin_type , require_plugin = False )
114+ except ValueError as exc :
115+ return _create_error (text , str (exc ))
116+ cls = 'ansible-return-value'
117+ if value is None :
118+ text = f'{ rv } '
119+ else :
120+ text = f'{ rv } ={ value } '
121+ if plugin_fqcn and plugin_type and plugin_fqcn .count ('.' ) >= 2 :
122+ namespace , name , plugin = plugin_fqcn .split ('.' , 2 )
123+ url = f'../../{ namespace } /{ name } /{ plugin } _{ plugin_type } .html'
124+ fragment = f'return-{ rv_link .replace ("." , "/" )} '
125+ link_start = (
126+ f'<a class="reference internal" href="{ url } #{ fragment } ">'
127+ '<span class="std std-ref"><span class="pre">'
128+ )
129+ link_end = '</span></span></a>'
130+ else :
131+ link_start = ''
132+ link_end = ''
133+ return f'<code class="{ cls } literal notranslate">{ link_start } { text } { link_end } </code>'
134+
135+
136+ def _value_html (matcher : 're.Match' ) -> str :
137+ text = _unescape_sem_value (matcher .group (1 ))
138+ return f'<code class="ansible-value literal notranslate">{ text } </code>'
139+
140+
141+ def _env_var_html (matcher : 're.Match' ) -> str :
142+ text = _unescape_sem_value (matcher .group (1 ))
143+ return f'<code class="xref std std-envvar literal notranslate">{ text } </code>'
144+
145+
146+ @pass_context
147+ def html_ify (context : Context , text : str ) -> str :
35148 ''' convert symbols like I(this is in italics) to valid HTML '''
36149
37150 flog = mlog .fields (func = 'html_ify' )
38151 flog .fields (text = text ).debug ('Enter' )
39152 _counts = {}
40153
154+ plugin_fqcn , plugin_type = extract_plugin_data (context )
155+
41156 text = html_escape (text )
42157 text , _counts ['italic' ] = _ITALIC .subn (r"<em>\1</em>" , text )
43158 text , _counts ['bold' ] = _BOLD .subn (r"<b>\1</b>" , text )
44159 text , _counts ['module' ] = _MODULE .subn (
45160 r"<a href='../../\1/\2/\3_module.html' class='module'>\1.\2.\3</a>" , text )
161+ text , _counts ['plugin' ] = _PLUGIN .subn (
162+ r"<a href='../../\1/\2/\3_\4.html' class='module plugin-\4'>\1.\2.\3</span>" , text )
46163 text , _counts ['url' ] = _URL .subn (r"<a href='\1'>\1</a>" , text )
47164 text , _counts ['ref' ] = _REF .subn (r"<span class='module'>\1</span>" , text )
48165 text , _counts ['link' ] = _LINK .subn (r"<a href='\2'>\1</a>" , text )
49166 text , _counts ['const' ] = _CONST .subn (
50167 r"<code class='docutils literal notranslate'>\1</code>" , text )
168+ text , _counts ['option-name' ] = _SEM_OPTION_NAME .subn (
169+ partial (_option_name_html , plugin_fqcn , plugin_type ), text )
170+ text , _counts ['option-value' ] = _SEM_OPTION_VALUE .subn (_value_html , text )
171+ text , _counts ['environment-var' ] = _SEM_ENV_VARIABLE .subn (_env_var_html , text )
172+ text , _counts ['return-value' ] = _SEM_RET_VALUE .subn (
173+ partial (_return_value_html , plugin_fqcn , plugin_type ), text )
51174 text , _counts ['ruler' ] = _RULER .subn (r"<hr/>" , text )
52175
53176 text = text .strip ()
@@ -95,6 +218,12 @@ def _rst_ify_module(m: 're.Match') -> str:
95218 return f"\\ :ref:`{ rst_escape (fqcn )} <ansible_collections.{ fqcn } _module>`\\ "
96219
97220
221+ def _rst_ify_plugin (m : 're.Match' ) -> str :
222+ fqcn = f'{ m .group (1 )} .{ m .group (2 )} .{ m .group (3 )} '
223+ plugin_type = m .group (4 )
224+ return f"\\ :ref:`{ rst_escape (fqcn )} <ansible_collections.{ fqcn } _{ plugin_type } >`\\ "
225+
226+
98227def _escape_url (url : str ) -> str :
99228 # We include '<>[]{}' in safe to allow urls such as 'https://<HOST>:[PORT]/v{version}/' to
100229 # remain unmangled by percent encoding
@@ -118,20 +247,56 @@ def _rst_ify_const(m: 're.Match') -> str:
118247 return f"\\ :literal:`{ rst_escape (m .group (1 ), escape_ending_whitespace = True )} `\\ "
119248
120249
121- def rst_ify (text ):
250+ def _rst_ify_option_name (plugin_fqcn : t .Optional [str ], plugin_type : t .Optional [str ],
251+ m : 're.Match' ) -> str :
252+ _check_plugin (plugin_fqcn , plugin_type , m )
253+ text = _unescape_sem_value (m .group (1 ))
254+ text = augment_plugin_name_type (text , plugin_fqcn , plugin_type )
255+ return f"\\ :ansopt:`{ rst_escape (text , escape_ending_whitespace = True )} `\\ "
256+
257+
258+ def _rst_ify_value (m : 're.Match' ) -> str :
259+ text = _unescape_sem_value (m .group (1 ))
260+ return f"\\ :ansval:`{ rst_escape (text , escape_ending_whitespace = True )} `\\ "
261+
262+
263+ def _rst_ify_return_value (plugin_fqcn : t .Optional [str ], plugin_type : t .Optional [str ],
264+ m : 're.Match' ) -> str :
265+ _check_plugin (plugin_fqcn , plugin_type , m )
266+ text = _unescape_sem_value (m .group (1 ))
267+ text = augment_plugin_name_type (text , plugin_fqcn , plugin_type )
268+ return f"\\ :ansretval:`{ rst_escape (text , escape_ending_whitespace = True )} `\\ "
269+
270+
271+ def _rst_ify_envvar (m : 're.Match' ) -> str :
272+ text = _unescape_sem_value (m .group (1 ))
273+ return f"\\ :envvar:`{ rst_escape (text , escape_ending_whitespace = True )} `\\ "
274+
275+
276+ @pass_context
277+ def rst_ify (context : Context , text : str ) -> str :
122278 ''' convert symbols like I(this is in italics) to valid restructured text '''
123279
124280 flog = mlog .fields (func = 'rst_ify' )
125281 flog .fields (text = text ).debug ('Enter' )
126282 _counts = {}
127283
284+ plugin_fqcn , plugin_type = extract_plugin_data (context )
285+
128286 text , _counts ['italic' ] = _ITALIC .subn (_rst_ify_italic , text )
129287 text , _counts ['bold' ] = _BOLD .subn (_rst_ify_bold , text )
130288 text , _counts ['module' ] = _MODULE .subn (_rst_ify_module , text )
289+ text , _counts ['plugin' ] = _PLUGIN .subn (_rst_ify_plugin , text )
131290 text , _counts ['link' ] = _LINK .subn (_rst_ify_link , text )
132291 text , _counts ['url' ] = _URL .subn (_rst_ify_url , text )
133292 text , _counts ['ref' ] = _REF .subn (_rst_ify_ref , text )
134293 text , _counts ['const' ] = _CONST .subn (_rst_ify_const , text )
294+ text , _counts ['option-name' ] = _SEM_OPTION_NAME .subn (
295+ partial (_rst_ify_option_name , plugin_fqcn , plugin_type ), text )
296+ text , _counts ['option-value' ] = _SEM_OPTION_VALUE .subn (_rst_ify_value , text )
297+ text , _counts ['environment-var' ] = _SEM_ENV_VARIABLE .subn (_rst_ify_envvar , text )
298+ text , _counts ['return-value' ] = _SEM_RET_VALUE .subn (
299+ partial (_rst_ify_return_value , plugin_fqcn , plugin_type ), text )
135300 text , _counts ['ruler' ] = _RULER .subn ('\n \n .. raw:: html\n \n <hr>\n \n ' , text )
136301
137302 flog .fields (counts = _counts ).info ('Number of macros converted to rst equivalents' )
0 commit comments