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  antsibull_core .logging  import  log 
1618
19+ from  ..semantic_helper  import  parse_option , parse_return_value , augment_plugin_name_type 
20+ 
1721
1822mlog  =  log .fields (mod = __name__ )
1923
2226_ITALIC  =  re .compile (r"\bI\(([^)]+)\)" )
2327_BOLD  =  re .compile (r"\bB\(([^)]+)\)" )
2428_MODULE  =  re .compile (r"\bM\(([^).]+)\.([^).]+)\.([^)]+)\)" )
29+ _PLUGIN  =  re .compile (r"\bP\(([^).]+)\.([^).]+)\.([^)]+)#([a-z]+)\)" )
2530_URL  =  re .compile (r"\bU\(([^)]+)\)" )
2631_LINK  =  re .compile (r"\bL\(([^)]+), *([^)]+)\)" )
2732_REF  =  re .compile (r"\bR\(([^)]+), *([^)]+)\)" )
2833_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 )
2939_RULER  =  re .compile (r"\bHORIZONTALLINE\b" )
40+ _UNESCAPE  =  re .compile (r"\\(.)" )
3041
3142_EMAIL_ADDRESS  =  re .compile (r"(?:<{mail}>|\({mail}\)|{mail})" .format (mail = r"[\w.+-]+@[\w.-]+\.\w+" ))
3243
3344
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 :
35149    ''' convert symbols like I(this is in italics) to valid HTML ''' 
36150
37151    flog  =  mlog .fields (func = 'html_ify' )
38152    flog .fields (text = text ).debug ('Enter' )
39153    _counts  =  {}
40154
155+     plugin_fqcn , plugin_type  =  extract_plugin_data (context )
156+ 
41157    text  =  html_escape (text )
42158    text , _counts ['italic' ] =  _ITALIC .subn (r"<em>\1</em>" , text )
43159    text , _counts ['bold' ] =  _BOLD .subn (r"<b>\1</b>" , text )
44160    text , _counts ['module' ] =  _MODULE .subn (
45161        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 )
46164    text , _counts ['url' ] =  _URL .subn (r"<a href='\1'>\1</a>" , text )
47165    text , _counts ['ref' ] =  _REF .subn (r"<span class='module'>\1</span>" , text )
48166    text , _counts ['link' ] =  _LINK .subn (r"<a href='\2'>\1</a>" , text )
49167    text , _counts ['const' ] =  _CONST .subn (
50168        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 )
51175    text , _counts ['ruler' ] =  _RULER .subn (r"<hr/>" , text )
52176
53177    text  =  text .strip ()
@@ -95,6 +219,12 @@ def _rst_ify_module(m: 're.Match') -> str:
95219    return  f"\\  :ref:`{ rst_escape (fqcn )}   <ansible_collections.{ fqcn }  _module>`\\  " 
96220
97221
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+ 
98228def  _escape_url (url : str ) ->  str :
99229    # We include '<>[]{}' in safe to allow urls such as 'https://<HOST>:[PORT]/v{version}/' to 
100230    # remain unmangled by percent encoding 
@@ -118,20 +248,56 @@ def _rst_ify_const(m: 're.Match') -> str:
118248    return  f"\\  :literal:`{ rst_escape (m .group (1 ), escape_ending_whitespace = True )}  `\\  " 
119249
120250
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 :
122279    ''' convert symbols like I(this is in italics) to valid restructured text ''' 
123280
124281    flog  =  mlog .fields (func = 'rst_ify' )
125282    flog .fields (text = text ).debug ('Enter' )
126283    _counts  =  {}
127284
285+     plugin_fqcn , plugin_type  =  extract_plugin_data (context )
286+ 
128287    text , _counts ['italic' ] =  _ITALIC .subn (_rst_ify_italic , text )
129288    text , _counts ['bold' ] =  _BOLD .subn (_rst_ify_bold , text )
130289    text , _counts ['module' ] =  _MODULE .subn (_rst_ify_module , text )
290+     text , _counts ['plugin' ] =  _PLUGIN .subn (_rst_ify_plugin , text )
131291    text , _counts ['link' ] =  _LINK .subn (_rst_ify_link , text )
132292    text , _counts ['url' ] =  _URL .subn (_rst_ify_url , text )
133293    text , _counts ['ref' ] =  _REF .subn (_rst_ify_ref , text )
134294    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 )
135301    text , _counts ['ruler' ] =  _RULER .subn ('\n \n .. raw:: html\n \n   <hr>\n \n ' , text )
136302
137303    flog .fields (counts = _counts ).info ('Number of macros converted to rst equivalents' )
0 commit comments