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__ )
2730_LINK = re .compile (r"\bL\(([^)]+), *([^)]+)\)" )
2831_REF = re .compile (r"\bR\(([^)]+), *([^)]+)\)" )
2932_CONST = re .compile (r"\bC\(([^)]+)\)" )
30- _SEM_OPTION_NAME = re .compile (r"\bO\(([^)]+)\)" )
31- _SEM_OPTION_VALUE = re .compile (r"\bV\(([^)]+)\)" )
32- _SEM_ENV_VARIABLE = re .compile (r"\bE\(([^)]+)\)" )
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 )
3338_RULER = re .compile (r"\bHORIZONTALLINE\b" )
34-
35-
36- def _option_name_html (matcher ):
37- text = matcher .group (1 )
38- if '=' not in text and ':' not in text :
39- return f'<code class="ansible-option literal notranslate"><strong>{ text } </strong></code>'
40- return f'<code class="ansible-option-value literal notranslate">{ text } </code>'
41-
42-
43- def html_ify (text ):
39+ _UNESCAPE = re .compile (r"\\(.)" )
40+
41+
42+ def extract_plugin_data (context : Context ) -> t .Tuple [t .Optional [str ], t .Optional [str ]]:
43+ plugin_fqcn = context .get ('plugin_name' )
44+ plugin_type = context .get ('plugin_type' )
45+ if plugin_fqcn is None or plugin_type is None :
46+ return None , None
47+ # if plugin_type == 'role':
48+ # entry_point = context.get('entry_point', 'main')
49+ # # FIXME: use entry_point
50+ return plugin_fqcn , plugin_type
51+
52+
53+ def _unescape_sem_value (text : str ) -> str :
54+ return _UNESCAPE .sub (r'\1' , text )
55+
56+
57+ def _check_plugin (plugin_fqcn : t .Optional [str ], plugin_type : t .Optional [str ],
58+ matcher : 're.Match' ) -> None :
59+ if plugin_fqcn is None or plugin_type is None :
60+ raise Exception (f'The markup { matcher .group (0 )} cannot be used outside a plugin or role' )
61+
62+
63+ def _create_error (text : str , error : str ) -> str : # pylint:disable=unused-argument
64+ return '...' # FIXME
65+
66+
67+ def _option_name_html (plugin_fqcn : t .Optional [str ], plugin_type : t .Optional [str ],
68+ matcher : 're.Match' ) -> str :
69+ _check_plugin (plugin_fqcn , plugin_type , matcher )
70+ text = _unescape_sem_value (matcher .group (1 ))
71+ try :
72+ plugin_fqcn , plugin_type , option_link , option , value = parse_option (
73+ text , plugin_fqcn , plugin_type , require_plugin = False )
74+ except ValueError as exc :
75+ return _create_error (text , str (exc ))
76+ if value is None :
77+ cls = 'ansible-option'
78+ text = f'{ option } '
79+ strong_start = '<strong>'
80+ strong_end = '</strong>'
81+ else :
82+ cls = 'ansible-option-value'
83+ text = f'{ option } ={ value } '
84+ strong_start = ''
85+ strong_end = ''
86+ if plugin_fqcn and plugin_type and plugin_fqcn .count ('.' ) >= 2 :
87+ # TODO: handle role arguments (entrypoint!)
88+ namespace , name , plugin = plugin_fqcn .split ('.' , 2 )
89+ url = f'../../{ namespace } /{ name } /{ plugin } _{ plugin_type } .html'
90+ fragment = f'parameter-{ option_link .replace ("." , "/" )} '
91+ link_start = (
92+ f'<a class="reference internal" href="{ url } #{ fragment } ">'
93+ '<span class="std std-ref"><span class="pre">'
94+ )
95+ link_end = '</span></span></a>'
96+ else :
97+ link_start = ''
98+ link_end = ''
99+ return (
100+ f'<code class="{ cls } literal notranslate">'
101+ f'{ strong_start } { link_start } { text } { link_end } { strong_end } </code>'
102+ )
103+
104+
105+ def _return_value_html (plugin_fqcn : t .Optional [str ], plugin_type : t .Optional [str ],
106+ matcher : 're.Match' ) -> str :
107+ _check_plugin (plugin_fqcn , plugin_type , matcher )
108+ text = _unescape_sem_value (matcher .group (1 ))
109+ try :
110+ plugin_fqcn , plugin_type , rv_link , rv , value = parse_return_value (
111+ text , plugin_fqcn , plugin_type , require_plugin = False )
112+ except ValueError as exc :
113+ return _create_error (text , str (exc ))
114+ cls = 'ansible-return-value'
115+ if value is None :
116+ text = f'{ rv } '
117+ else :
118+ text = f'{ rv } ={ value } '
119+ if plugin_fqcn and plugin_type and plugin_fqcn .count ('.' ) >= 2 :
120+ namespace , name , plugin = plugin_fqcn .split ('.' , 2 )
121+ url = f'../../{ namespace } /{ name } /{ plugin } _{ plugin_type } .html'
122+ fragment = f'return-{ rv_link .replace ("." , "/" )} '
123+ link_start = (
124+ f'<a class="reference internal" href="{ url } #{ fragment } ">'
125+ '<span class="std std-ref"><span class="pre">'
126+ )
127+ link_end = '</span></span></a>'
128+ else :
129+ link_start = ''
130+ link_end = ''
131+ return f'<code class="{ cls } literal notranslate">{ link_start } { text } { link_end } </code>'
132+
133+
134+ def _value_html (matcher : 're.Match' ) -> str :
135+ text = _unescape_sem_value (matcher .group (1 ))
136+ return f'<code class="ansible-value literal notranslate">{ text } </code>'
137+
138+
139+ def _env_var_html (matcher : 're.Match' ) -> str :
140+ text = _unescape_sem_value (matcher .group (1 ))
141+ return f'<code class="xref std std-envvar literal notranslate">{ text } </code>'
142+
143+
144+ @pass_context
145+ def html_ify (context : Context , text : str ) -> str :
44146 ''' convert symbols like I(this is in italics) to valid HTML '''
45147
46148 flog = mlog .fields (func = 'html_ify' )
47149 flog .fields (text = text ).debug ('Enter' )
48150 _counts = {}
49151
152+ plugin_fqcn , plugin_type = extract_plugin_data (context )
153+
50154 text = html_escape (text )
51155 text , _counts ['italic' ] = _ITALIC .subn (r"<em>\1</em>" , text )
52156 text , _counts ['bold' ] = _BOLD .subn (r"<b>\1</b>" , text )
@@ -59,11 +163,12 @@ def html_ify(text):
59163 text , _counts ['link' ] = _LINK .subn (r"<a href='\2'>\1</a>" , text )
60164 text , _counts ['const' ] = _CONST .subn (
61165 r"<code class='docutils literal notranslate'>\1</code>" , text )
62- text , _counts ['option-name' ] = _SEM_OPTION_NAME .subn (_option_name_html , text )
63- text , _counts ['option-value' ] = _SEM_OPTION_VALUE .subn (
64- r"<code class='ansible-value literal notranslate'>\1</code>" , text )
65- text , _counts ['environment-var' ] = _SEM_ENV_VARIABLE .subn (
66- r"<code class='xref std std-envvar literal notranslate'>\1</code>" , text )
166+ text , _counts ['option-name' ] = _SEM_OPTION_NAME .subn (
167+ partial (_option_name_html , plugin_fqcn , plugin_type ), text )
168+ text , _counts ['option-value' ] = _SEM_OPTION_VALUE .subn (_value_html , text )
169+ text , _counts ['environment-var' ] = _SEM_ENV_VARIABLE .subn (_env_var_html , text )
170+ text , _counts ['return-value' ] = _SEM_RET_VALUE .subn (
171+ partial (_return_value_html , plugin_fqcn , plugin_type ), text )
67172 text , _counts ['ruler' ] = _RULER .subn (r"<hr/>" , text )
68173
69174 text = text .strip ()
@@ -134,35 +239,55 @@ def _rst_ify_const(m: 're.Match') -> str:
134239 return f"\\ :literal:`{ rst_escape (m .group (1 ), escape_ending_whitespace = True )} `\\ "
135240
136241
137- def _rst_ify_option_name (m ):
138- return f"\\ :ansopt:`{ rst_escape (m .group (1 ), escape_ending_whitespace = True )} `\\ "
242+ def _rst_ify_option_name (plugin_fqcn : t .Optional [str ], plugin_type : t .Optional [str ],
243+ m : 're.Match' ) -> str :
244+ _check_plugin (plugin_fqcn , plugin_type , m )
245+ text = _unescape_sem_value (m .group (1 ))
246+ text = augment_plugin_name_type (text , plugin_fqcn , plugin_type )
247+ return f"\\ :ansopt:`{ rst_escape (text , escape_ending_whitespace = True )} `\\ "
248+
139249
250+ def _rst_ify_value (m : 're.Match' ) -> str :
251+ text = _unescape_sem_value (m .group (1 ))
252+ return f"\\ :ansval:`{ rst_escape (text , escape_ending_whitespace = True )} `\\ "
140253
141- def _rst_ify_value (m ):
142- return f"\\ :ansval:`{ rst_escape (m .group (1 ), escape_ending_whitespace = True )} `\\ "
143254
255+ def _rst_ify_return_value (plugin_fqcn : t .Optional [str ], plugin_type : t .Optional [str ],
256+ m : 're.Match' ) -> str :
257+ _check_plugin (plugin_fqcn , plugin_type , m )
258+ text = _unescape_sem_value (m .group (1 ))
259+ text = augment_plugin_name_type (text , plugin_fqcn , plugin_type )
260+ return f"\\ :ansretval:`{ rst_escape (text , escape_ending_whitespace = True )} `\\ "
144261
145- def _rst_ify_envvar (m ):
146- return f"\\ :envvar:`{ rst_escape (m .group (1 ), escape_ending_whitespace = True )} `\\ "
147262
263+ def _rst_ify_envvar (m : 're.Match' ) -> str :
264+ text = _unescape_sem_value (m .group (1 ))
265+ return f"\\ :envvar:`{ rst_escape (text , escape_ending_whitespace = True )} `\\ "
148266
149- def rst_ify (text ):
267+
268+ @pass_context
269+ def rst_ify (context : Context , text : str ) -> str :
150270 ''' convert symbols like I(this is in italics) to valid restructured text '''
151271
152272 flog = mlog .fields (func = 'rst_ify' )
153273 flog .fields (text = text ).debug ('Enter' )
154274 _counts = {}
155275
276+ plugin_fqcn , plugin_type = extract_plugin_data (context )
277+
156278 text , _counts ['italic' ] = _ITALIC .subn (_rst_ify_italic , text )
157279 text , _counts ['bold' ] = _BOLD .subn (_rst_ify_bold , text )
158280 text , _counts ['module' ] = _MODULE .subn (_rst_ify_module , text )
159281 text , _counts ['link' ] = _LINK .subn (_rst_ify_link , text )
160282 text , _counts ['url' ] = _URL .subn (_rst_ify_url , text )
161283 text , _counts ['ref' ] = _REF .subn (_rst_ify_ref , text )
162284 text , _counts ['const' ] = _CONST .subn (_rst_ify_const , text )
163- text , _counts ['option-name' ] = _SEM_OPTION_NAME .subn (_rst_ify_option_name , text )
285+ text , _counts ['option-name' ] = _SEM_OPTION_NAME .subn (
286+ partial (_rst_ify_option_name , plugin_fqcn , plugin_type ), text )
164287 text , _counts ['option-value' ] = _SEM_OPTION_VALUE .subn (_rst_ify_value , text )
165288 text , _counts ['environment-var' ] = _SEM_ENV_VARIABLE .subn (_rst_ify_envvar , text )
289+ text , _counts ['return-value' ] = _SEM_RET_VALUE .subn (
290+ partial (_rst_ify_return_value , plugin_fqcn , plugin_type ), text )
166291 text , _counts ['ruler' ] = _RULER .subn ('\n \n .. raw:: html\n \n <hr>\n \n ' , text )
167292
168293 flog .fields (counts = _counts ).info ('Number of macros converted to rst equivalents' )
0 commit comments