Skip to content

Commit 0793433

Browse files
authored
Add semantic markup support. (#4)
1 parent edc7392 commit 0793433

File tree

30 files changed

+521
-49
lines changed

30 files changed

+521
-49
lines changed
Lines changed: 2 additions & 0 deletions
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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ ansible-pygments = "*"
3838
antsibull-core = ">= 1.2.0, < 3.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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,24 @@
1212
from collections.abc import Mapping, Sequence
1313

1414
from antsibull_core.logging import log
15-
from jinja2.runtime import Undefined
15+
from jinja2.runtime import Context, Undefined
1616

1717
mlog = log.fields(mod=__name__)
1818

1919
_EMAIL_ADDRESS = re.compile(r"(?:<{mail}>|\({mail}\)|{mail})".format(mail=r"[\w.+-]+@[\w.-]+\.\w+"))
2020

2121

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

src/antsibull_docs/jinja2/htmlify.py

Lines changed: 118 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@
1212
from urllib.parse import quote
1313

1414
from antsibull_core.logging import log
15+
from jinja2.runtime import Context
16+
from jinja2.utils import pass_context
1517

18+
from ..semantic_helper import parse_option, parse_return_value
19+
from .filters import extract_plugin_data
1620
from .parser import Command, CommandSet, convert_text
1721

1822
mlog = log.fields(mod=__name__)
@@ -33,9 +37,13 @@ def _create_error(text: str, error: str) -> str:
3337

3438

3539
class _Context:
40+
j2_context: Context
3641
counts: t.Dict[str, int]
42+
plugin_fqcn: t.Optional[str]
43+
plugin_type: t.Optional[str]
3744

38-
def __init__(self):
45+
def __init__(self, j2_context: Context):
46+
self.j2_context = j2_context
3947
self.counts = {
4048
'italic': 0,
4149
'bold': 0,
@@ -51,6 +59,7 @@ def __init__(self):
5159
'return-value': 0,
5260
'ruler': 0,
5361
}
62+
self.plugin_fqcn, self.plugin_type = extract_plugin_data(j2_context)
5463

5564

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

159168

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

182296

183-
def html_ify(text: str) -> str:
297+
@pass_context
298+
def html_ify(context: Context, text: str) -> str:
184299
''' convert symbols like I(this is in italics) to valid HTML '''
185300
flog = mlog.fields(func='html_ify')
186301
flog.fields(text=text).debug('Enter')
187302

188-
our_context = _Context()
303+
our_context = _Context(context)
189304

190305
try:
191306
text = convert_text(text, _COMMAND_SET, html_escape, our_context)

src/antsibull_docs/jinja2/rstify.py

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@
1111
from urllib.parse import quote
1212

1313
from antsibull_core.logging import log
14+
from jinja2.runtime import Context
15+
from jinja2.utils import pass_context
1416

17+
from ..semantic_helper import augment_plugin_name_type
18+
from .filters import extract_plugin_data
1519
from .parser import Command, CommandSet, convert_text
1620

1721
mlog = log.fields(mod=__name__)
@@ -61,9 +65,13 @@ def _create_error(text: str, error: str) -> str:
6165

6266

6367
class _Context:
68+
j2_context: Context
6469
counts: t.Dict[str, int]
70+
plugin_fqcn: t.Optional[str]
71+
plugin_type: t.Optional[str]
6572

66-
def __init__(self):
73+
def __init__(self, j2_context: Context):
74+
self.j2_context = j2_context
6775
self.counts = {
6876
'italic': 0,
6977
'bold': 0,
@@ -79,6 +87,7 @@ def __init__(self):
7987
'return-value': 0,
8088
'ruler': 0,
8189
}
90+
self.plugin_fqcn, self.plugin_type = extract_plugin_data(j2_context)
8291

8392

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

182191

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

205264

206-
def rst_ify(text: str) -> str:
265+
@pass_context
266+
def rst_ify(context: Context, text: str) -> str:
207267
''' convert symbols like I(this is in italics) to valid restructured text '''
208268
flog = mlog.fields(func='rst_ify')
209269
flog.fields(text=text).debug('Enter')
210270

211-
our_context = _Context()
271+
our_context = _Context(context)
212272

213273
try:
214274
text = convert_text(text, _COMMAND_SET, rst_escape, our_context)

0 commit comments

Comments
 (0)