Skip to content

Commit e6ec2d6

Browse files
committed
Add semantic markup support.
1 parent d567311 commit e6ec2d6

File tree

13 files changed

+481
-11
lines changed

13 files changed

+481
-11
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.0.0, < 2.0.0"
3939
asyncio-pool = "*"
4040
docutils = "*"
41-
jinja2 = "*"
41+
jinja2 = ">= 3.0"
4242
rstcheck = ">= 3.0.0, < 7.0.0"
4343
sphinx = "*"
4444

src/antsibull_docs/jinja2/filters.py

+12-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
import typing as t
1313

14-
from jinja2.runtime import Undefined
14+
from jinja2.runtime import Context, Undefined
1515

1616
from antsibull_core.logging import log
1717

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

2323

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

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

@@ -56,9 +62,13 @@ def _create_error(text: str, error: str) -> str:
5662

5763

5864
class _Context:
65+
j2_context: Context
5966
counts: t.Dict[str, int]
67+
plugin_fqcn: t.Optional[str]
68+
plugin_type: t.Optional[str]
6069

61-
def __init__(self):
70+
def __init__(self, j2_context: Context):
71+
self.j2_context = j2_context
6272
self.counts = {
6373
'italic': 0,
6474
'bold': 0,
@@ -74,6 +84,7 @@ def __init__(self):
7484
'return-value': 0,
7585
'ruler': 0,
7686
}
87+
self.plugin_fqcn, self.plugin_type = extract_plugin_data(j2_context)
7788

7889

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

177188

189+
class _OptionName(Command):
190+
command = 'O'
191+
parameter_count = 1
192+
escaped_content = True
193+
194+
def handle(self, parameters: t.List[str], context: t.Any) -> str:
195+
context.counts['option-name'] += 1
196+
if context.plugin_fqcn is None or context.plugin_type is None:
197+
raise Exception('The markup O(...) cannot be used outside a plugin or role')
198+
text = augment_plugin_name_type(parameters[0], context.plugin_fqcn, context.plugin_type)
199+
return f"\\ :ansopt:`{rst_escape(text, escape_ending_whitespace=True)}`\\ "
200+
201+
202+
class _OptionValue(Command):
203+
command = 'V'
204+
parameter_count = 1
205+
escaped_content = True
206+
207+
def handle(self, parameters: t.List[str], context: t.Any) -> str:
208+
context.counts['option-value'] += 1
209+
return f"\\ :ansval:`{rst_escape(parameters[0], escape_ending_whitespace=True)}`\\ "
210+
211+
212+
class _EnvVariable(Command):
213+
command = 'E'
214+
parameter_count = 1
215+
escaped_content = True
216+
217+
def handle(self, parameters: t.List[str], context: t.Any) -> str:
218+
context.counts['environment-var'] += 1
219+
return f"\\ :envvar:`{rst_escape(parameters[0], escape_ending_whitespace=True)}`\\ "
220+
221+
222+
class _RetValue(Command):
223+
command = 'RV'
224+
parameter_count = 1
225+
escaped_content = True
226+
227+
def handle(self, parameters: t.List[str], context: t.Any) -> str:
228+
context.counts['return-value'] += 1
229+
if context.plugin_fqcn is None or context.plugin_type is None:
230+
raise Exception('The markup RV(...) cannot be used outside a plugin or role')
231+
text = augment_plugin_name_type(parameters[0], context.plugin_fqcn, context.plugin_type)
232+
return f"\\ :ansretval:`{rst_escape(text, escape_ending_whitespace=True)}`\\ "
233+
234+
178235
class _HorizontalLine(Command):
179236
command = 'HORIZONTALLINE'
180237
parameter_count = 0
@@ -194,16 +251,21 @@ def handle(self, parameters: t.List[str], context: t.Any) -> str:
194251
_Link(),
195252
_Ref(),
196253
_Const(),
254+
_OptionName(),
255+
_OptionValue(),
256+
_EnvVariable(),
257+
_RetValue(),
197258
_HorizontalLine(),
198259
])
199260

200261

201-
def rst_ify(text: str) -> str:
262+
@pass_context
263+
def rst_ify(context: Context, text: str) -> str:
202264
''' convert symbols like I(this is in italics) to valid restructured text '''
203265
flog = mlog.fields(func='rst_ify')
204266
flog.fields(text=text).debug('Enter')
205267

206-
our_context = _Context()
268+
our_context = _Context(context)
207269

208270
try:
209271
text = convert_text(text, _COMMAND_SET, rst_escape, our_context)

src/antsibull_docs/lint_extra_docs.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import re
1111
import typing as t
1212

13+
from sphinx_antsibull_ext import roles as antsibull_roles
14+
1315
from .extra_docs import (
1416
find_extra_docs,
1517
lint_required_conditions,
@@ -33,7 +35,7 @@ def lint_optional_conditions(content: str, path: str, collection_name: str
3335
3436
Return a list of errors.
3537
'''
36-
return check_rst_content(content, filename=path)
38+
return check_rst_content(content, filename=path, ignore_roles=list(antsibull_roles.ROLES))
3739

3840

3941
def lint_collection_extra_docs_files(path_to_collection: str

0 commit comments

Comments
 (0)