Skip to content

Commit dcbc59a

Browse files
committed
Add semantic markup support.
1 parent 267fa20 commit dcbc59a

File tree

30 files changed

+527
-49
lines changed

30 files changed

+527
-49
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
major_changes:
2+
- Support new semantic markup in documentation (https://github.com/ansible-community/antsibull-docs/pull/4).

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ ansible-pygments = "*"
3838
antsibull-core = ">= 1.2.0, < 2.0.0"
3939
asyncio-pool = "*"
4040
docutils = "*"
41-
jinja2 = "*"
41+
jinja2 = ">= 3.0"
4242
packaging = "*"
4343
rstcheck = ">= 3.0.0, < 7.0.0"
4444
sphinx = "*"

src/antsibull_docs/jinja2/filters.py

+12-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
from collections.abc import Mapping, Sequence
1515

16-
from jinja2.runtime import Undefined
16+
from jinja2.runtime import Context, Undefined
1717

1818
from antsibull_core.logging import log
1919

@@ -23,6 +23,17 @@
2323
_EMAIL_ADDRESS = re.compile(r"(?:<{mail}>|\({mail}\)|{mail})".format(mail=r"[\w.+-]+@[\w.-]+\.\w+"))
2424

2525

26+
def extract_plugin_data(context: Context) -> t.Tuple[t.Optional[str], t.Optional[str]]:
27+
plugin_fqcn = context.get('plugin_name')
28+
plugin_type = context.get('plugin_type')
29+
if plugin_fqcn is None or plugin_type is None:
30+
return None, None
31+
# if plugin_type == 'role':
32+
# entry_point = context.get('entry_point', 'main')
33+
# # FIXME: use entry_point
34+
return plugin_fqcn, plugin_type
35+
36+
2637
def documented_type(text) -> str:
2738
''' Convert any python type to a type for documentation '''
2839

src/antsibull_docs/jinja2/htmlify.py

+120-3
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,14 @@
1212

1313
import typing as t
1414

15+
from jinja2.runtime import Context
16+
from jinja2.utils import pass_context
17+
1518
from antsibull_core.logging import log
1619

20+
from ..semantic_helper import parse_option, parse_return_value
21+
22+
from .filters import extract_plugin_data
1723
from .parser import Command, CommandSet, convert_text
1824

1925

@@ -35,9 +41,13 @@ def _create_error(text: str, error: str) -> str:
3541

3642

3743
class _Context:
44+
j2_context: Context
3845
counts: t.Dict[str, int]
46+
plugin_fqcn: t.Optional[str]
47+
plugin_type: t.Optional[str]
3948

40-
def __init__(self):
49+
def __init__(self, j2_context: Context):
50+
self.j2_context = j2_context
4151
self.counts = {
4252
'italic': 0,
4353
'bold': 0,
@@ -53,6 +63,7 @@ def __init__(self):
5363
'return-value': 0,
5464
'ruler': 0,
5565
}
66+
self.plugin_fqcn, self.plugin_type = extract_plugin_data(j2_context)
5667

5768

5869
# In the following, we make heavy use of escaped whitespace ("\ ") being removed from the output.
@@ -159,6 +170,107 @@ def handle(self, parameters: t.List[str], context: t.Any) -> str:
159170
return f"<code class='docutils literal notranslate'>{html_escape(parameters[0])}</code>"
160171

161172

173+
class _OptionName(Command):
174+
command = 'O'
175+
parameter_count = 1
176+
escaped_content = True
177+
178+
def handle(self, parameters: t.List[str], context: t.Any) -> str:
179+
context.counts['option-name'] += 1
180+
if context.plugin_fqcn is None or context.plugin_type is None:
181+
raise Exception('The markup O(...) cannot be used outside a plugin or role')
182+
text = parameters[0]
183+
try:
184+
plugin_fqcn, plugin_type, option_link, option, value = parse_option(
185+
text, context.plugin_fqcn, context.plugin_type, require_plugin=False)
186+
except ValueError as exc:
187+
return _create_error(f'O({text})', str(exc))
188+
if value is None:
189+
cls = 'ansible-option'
190+
text = f'{option}'
191+
strong_start = '<strong>'
192+
strong_end = '</strong>'
193+
else:
194+
cls = 'ansible-option-value'
195+
text = f'{option}={value}'
196+
strong_start = ''
197+
strong_end = ''
198+
if plugin_fqcn and plugin_type and plugin_fqcn.count('.') >= 2:
199+
# TODO: handle role arguments (entrypoint!)
200+
namespace, name, plugin = plugin_fqcn.split('.', 2)
201+
url = f'../../{namespace}/{name}/{plugin}_{plugin_type}.html'
202+
fragment = f'parameter-{quote(option_link.replace(".", "/"))}'
203+
link_start = (
204+
f'<a class="reference internal" href="{url}#{fragment}">'
205+
'<span class="std std-ref"><span class="pre">'
206+
)
207+
link_end = '</span></span></a>'
208+
else:
209+
link_start = ''
210+
link_end = ''
211+
return (
212+
f'<code class="{cls} literal notranslate">'
213+
f'{strong_start}{link_start}{text}{link_end}{strong_end}</code>'
214+
)
215+
216+
217+
class _OptionValue(Command):
218+
command = 'V'
219+
parameter_count = 1
220+
escaped_content = True
221+
222+
def handle(self, parameters: t.List[str], context: t.Any) -> str:
223+
context.counts['option-value'] += 1
224+
text = parameters[0]
225+
return f'<code class="ansible-value literal notranslate">{html_escape(text)}</code>'
226+
227+
228+
class _EnvVariable(Command):
229+
command = 'E'
230+
parameter_count = 1
231+
escaped_content = True
232+
233+
def handle(self, parameters: t.List[str], context: t.Any) -> str:
234+
context.counts['environment-var'] += 1
235+
text = parameters[0]
236+
return f'<code class="xref std std-envvar literal notranslate">{html_escape(text)}</code>'
237+
238+
239+
class _RetValue(Command):
240+
command = 'RV'
241+
parameter_count = 1
242+
escaped_content = True
243+
244+
def handle(self, parameters: t.List[str], context: t.Any) -> str:
245+
context.counts['return-value'] += 1
246+
if context.plugin_fqcn is None or context.plugin_type is None:
247+
raise Exception('The markup RV(...) cannot be used outside a plugin or role')
248+
text = parameters[0]
249+
try:
250+
plugin_fqcn, plugin_type, rv_link, rv, value = parse_return_value(
251+
text, context.plugin_fqcn, context.plugin_type, require_plugin=False)
252+
except ValueError as exc:
253+
return _create_error(f'RV({text})', str(exc))
254+
cls = 'ansible-return-value'
255+
if value is None:
256+
text = f'{rv}'
257+
else:
258+
text = f'{rv}={value}'
259+
if plugin_fqcn and plugin_type and plugin_fqcn.count('.') >= 2:
260+
namespace, name, plugin = plugin_fqcn.split('.', 2)
261+
url = f'../../{namespace}/{name}/{plugin}_{plugin_type}.html'
262+
fragment = f'return-{quote(rv_link.replace(".", "/"))}'
263+
link_start = (
264+
f'<a class="reference internal" href="{url}#{fragment}">'
265+
'<span class="std std-ref"><span class="pre">'
266+
)
267+
link_end = '</span></span></a>'
268+
else:
269+
link_start = ''
270+
link_end = ''
271+
return f'<code class="{cls} literal notranslate">{link_start}{text}{link_end}</code>'
272+
273+
162274
class _HorizontalLine(Command):
163275
command = 'HORIZONTALLINE'
164276
parameter_count = 0
@@ -178,16 +290,21 @@ def handle(self, parameters: t.List[str], context: t.Any) -> str:
178290
_Link(),
179291
_Ref(),
180292
_Const(),
293+
_OptionName(),
294+
_OptionValue(),
295+
_EnvVariable(),
296+
_RetValue(),
181297
_HorizontalLine(),
182298
])
183299

184300

185-
def html_ify(text: str) -> str:
301+
@pass_context
302+
def html_ify(context: Context, text: str) -> str:
186303
''' convert symbols like I(this is in italics) to valid HTML '''
187304
flog = mlog.fields(func='html_ify')
188305
flog.fields(text=text).debug('Enter')
189306

190-
our_context = _Context()
307+
our_context = _Context(context)
191308

192309
try:
193310
text = convert_text(text, _COMMAND_SET, html_escape, our_context)

src/antsibull_docs/jinja2/rstify.py

+65-3
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,14 @@
1111

1212
import typing as t
1313

14+
from jinja2.runtime import Context
15+
from jinja2.utils import pass_context
16+
1417
from antsibull_core.logging import log
1518

19+
from ..semantic_helper import augment_plugin_name_type
20+
21+
from .filters import extract_plugin_data
1622
from .parser import Command, CommandSet, convert_text
1723

1824

@@ -63,9 +69,13 @@ def _create_error(text: str, error: str) -> str:
6369

6470

6571
class _Context:
72+
j2_context: Context
6673
counts: t.Dict[str, int]
74+
plugin_fqcn: t.Optional[str]
75+
plugin_type: t.Optional[str]
6776

68-
def __init__(self):
77+
def __init__(self, j2_context: Context):
78+
self.j2_context = j2_context
6979
self.counts = {
7080
'italic': 0,
7181
'bold': 0,
@@ -81,6 +91,7 @@ def __init__(self):
8191
'return-value': 0,
8292
'ruler': 0,
8393
}
94+
self.plugin_fqcn, self.plugin_type = extract_plugin_data(j2_context)
8495

8596

8697
# In the following, we make heavy use of escaped whitespace ("\ ") being removed from the output.
@@ -182,6 +193,52 @@ def handle(self, parameters: t.List[str], context: t.Any) -> str:
182193
return f"\\ :literal:`{rst_escape(parameters[0], escape_ending_whitespace=True)}`\\ "
183194

184195

196+
class _OptionName(Command):
197+
command = 'O'
198+
parameter_count = 1
199+
escaped_content = True
200+
201+
def handle(self, parameters: t.List[str], context: t.Any) -> str:
202+
context.counts['option-name'] += 1
203+
if context.plugin_fqcn is None or context.plugin_type is None:
204+
raise Exception('The markup O(...) cannot be used outside a plugin or role')
205+
text = augment_plugin_name_type(parameters[0], context.plugin_fqcn, context.plugin_type)
206+
return f"\\ :ansopt:`{rst_escape(text, escape_ending_whitespace=True)}`\\ "
207+
208+
209+
class _OptionValue(Command):
210+
command = 'V'
211+
parameter_count = 1
212+
escaped_content = True
213+
214+
def handle(self, parameters: t.List[str], context: t.Any) -> str:
215+
context.counts['option-value'] += 1
216+
return f"\\ :ansval:`{rst_escape(parameters[0], escape_ending_whitespace=True)}`\\ "
217+
218+
219+
class _EnvVariable(Command):
220+
command = 'E'
221+
parameter_count = 1
222+
escaped_content = True
223+
224+
def handle(self, parameters: t.List[str], context: t.Any) -> str:
225+
context.counts['environment-var'] += 1
226+
return f"\\ :envvar:`{rst_escape(parameters[0], escape_ending_whitespace=True)}`\\ "
227+
228+
229+
class _RetValue(Command):
230+
command = 'RV'
231+
parameter_count = 1
232+
escaped_content = True
233+
234+
def handle(self, parameters: t.List[str], context: t.Any) -> str:
235+
context.counts['return-value'] += 1
236+
if context.plugin_fqcn is None or context.plugin_type is None:
237+
raise Exception('The markup RV(...) cannot be used outside a plugin or role')
238+
text = augment_plugin_name_type(parameters[0], context.plugin_fqcn, context.plugin_type)
239+
return f"\\ :ansretval:`{rst_escape(text, escape_ending_whitespace=True)}`\\ "
240+
241+
185242
class _HorizontalLine(Command):
186243
command = 'HORIZONTALLINE'
187244
parameter_count = 0
@@ -201,16 +258,21 @@ def handle(self, parameters: t.List[str], context: t.Any) -> str:
201258
_Link(),
202259
_Ref(),
203260
_Const(),
261+
_OptionName(),
262+
_OptionValue(),
263+
_EnvVariable(),
264+
_RetValue(),
204265
_HorizontalLine(),
205266
])
206267

207268

208-
def rst_ify(text: str) -> str:
269+
@pass_context
270+
def rst_ify(context: Context, text: str) -> str:
209271
''' convert symbols like I(this is in italics) to valid restructured text '''
210272
flog = mlog.fields(func='rst_ify')
211273
flog.fields(text=text).debug('Enter')
212274

213-
our_context = _Context()
275+
our_context = _Context(context)
214276

215277
try:
216278
text = convert_text(text, _COMMAND_SET, rst_escape, our_context)

0 commit comments

Comments
 (0)