Skip to content

Commit

Permalink
Add semantic markup support.
Browse files Browse the repository at this point in the history
  • Loading branch information
felixfontein committed Aug 19, 2022
1 parent 8f58a4d commit 8e6b851
Show file tree
Hide file tree
Showing 13 changed files with 486 additions and 11 deletions.
2 changes: 2 additions & 0 deletions changelogs/fragments/4-semantic-markup.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
major_changes:
- Support new semantic markup in documentation (https://github.com/ansible-community/antsibull-docs/pull/4).
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ ansible-pygments = "*"
antsibull-core = ">= 1.0.0, < 2.0.0"
asyncio-pool = "*"
docutils = "*"
jinja2 = "*"
jinja2 = ">= 3.0"
rstcheck = ">= 3.0.0, < 7.0.0"
sphinx = "*"

Expand Down
13 changes: 12 additions & 1 deletion src/antsibull_docs/jinja2/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

import typing as t

from jinja2.runtime import Undefined
from jinja2.runtime import Context, Undefined

from antsibull_core.logging import log

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


def extract_plugin_data(context: Context) -> t.Tuple[t.Optional[str], t.Optional[str]]:
plugin_fqcn = context.get('plugin_name')
plugin_type = context.get('plugin_type')
if plugin_fqcn is None or plugin_type is None:
return None, None
# if plugin_type == 'role':
# entry_point = context.get('entry_point', 'main')
# # FIXME: use entry_point
return plugin_fqcn, plugin_type


def documented_type(text) -> str:
''' Convert any python type to a type for documentation '''

Expand Down
123 changes: 120 additions & 3 deletions src/antsibull_docs/jinja2/htmlify.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,14 @@

import typing as t

from jinja2.runtime import Context
from jinja2.utils import pass_context

from antsibull_core.logging import log

from ..semantic_helper import parse_option, parse_return_value

from .filters import extract_plugin_data
from .parser import Command, CommandSet, convert_text


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


class _Context:
j2_context: Context
counts: t.Dict[str, int]
plugin_fqcn: t.Optional[str]
plugin_type: t.Optional[str]

def __init__(self):
def __init__(self, j2_context: Context):
self.j2_context = j2_context
self.counts = {
'italic': 0,
'bold': 0,
Expand All @@ -53,6 +63,7 @@ def __init__(self):
'return-value': 0,
'ruler': 0,
}
self.plugin_fqcn, self.plugin_type = extract_plugin_data(j2_context)


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


class _OptionName(Command):
command = 'O'
parameter_count = 1
escaped_content = True

def handle(self, parameters: t.List[str], context: t.Any) -> str:
context.counts['option-name'] += 1
if context.plugin_fqcn is None or context.plugin_type is None:
raise Exception('The markup O(...) cannot be used outside a plugin or role')
text = parameters[0]
try:
plugin_fqcn, plugin_type, option_link, option, value = parse_option(
text, context.plugin_fqcn, context.plugin_type, require_plugin=False)
except ValueError as exc:
return _create_error(f'O({text})', str(exc))
if value is None:
cls = 'ansible-option'
text = f'{option}'
strong_start = '<strong>'
strong_end = '</strong>'
else:
cls = 'ansible-option-value'
text = f'{option}={value}'
strong_start = ''
strong_end = ''
if plugin_fqcn and plugin_type and plugin_fqcn.count('.') >= 2:
# TODO: handle role arguments (entrypoint!)
namespace, name, plugin = plugin_fqcn.split('.', 2)
url = f'../../{namespace}/{name}/{plugin}_{plugin_type}.html'
fragment = f'parameter-{quote(option_link.replace(".", "/"))}'
link_start = (
f'<a class="reference internal" href="{url}#{fragment}">'
'<span class="std std-ref"><span class="pre">'
)
link_end = '</span></span></a>'
else:
link_start = ''
link_end = ''
return (
f'<code class="{cls} literal notranslate">'
f'{strong_start}{link_start}{text}{link_end}{strong_end}</code>'
)


class _OptionValue(Command):
command = 'V'
parameter_count = 1
escaped_content = True

def handle(self, parameters: t.List[str], context: t.Any) -> str:
context.counts['option-value'] += 1
text = parameters[0]
return f'<code class="ansible-value literal notranslate">{html_escape(text)}</code>'


class _EnvVariable(Command):
command = 'E'
parameter_count = 1
escaped_content = True

def handle(self, parameters: t.List[str], context: t.Any) -> str:
context.counts['environment-var'] += 1
text = parameters[0]
return f'<code class="xref std std-envvar literal notranslate">{html_escape(text)}</code>'


class _RetValue(Command):
command = 'RV'
parameter_count = 1
escaped_content = True

def handle(self, parameters: t.List[str], context: t.Any) -> str:
context.counts['return-value'] += 1
if context.plugin_fqcn is None or context.plugin_type is None:
raise Exception('The markup RV(...) cannot be used outside a plugin or role')
text = parameters[0]
try:
plugin_fqcn, plugin_type, rv_link, rv, value = parse_return_value(
text, context.plugin_fqcn, context.plugin_type, require_plugin=False)
except ValueError as exc:
return _create_error(f'RV({text})', str(exc))
cls = 'ansible-return-value'
if value is None:
text = f'{rv}'
else:
text = f'{rv}={value}'
if plugin_fqcn and plugin_type and plugin_fqcn.count('.') >= 2:
namespace, name, plugin = plugin_fqcn.split('.', 2)
url = f'../../{namespace}/{name}/{plugin}_{plugin_type}.html'
fragment = f'return-{quote(rv_link.replace(".", "/"))}'
link_start = (
f'<a class="reference internal" href="{url}#{fragment}">'
'<span class="std std-ref"><span class="pre">'
)
link_end = '</span></span></a>'
else:
link_start = ''
link_end = ''
return f'<code class="{cls} literal notranslate">{link_start}{text}{link_end}</code>'


class _HorizontalLine(Command):
command = 'HORIZONTALLINE'
parameter_count = 0
Expand All @@ -178,16 +290,21 @@ def handle(self, parameters: t.List[str], context: t.Any) -> str:
_Link(),
_Ref(),
_Const(),
_OptionName(),
_OptionValue(),
_EnvVariable(),
_RetValue(),
_HorizontalLine(),
])


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

our_context = _Context()
our_context = _Context(context)

try:
text = convert_text(text, _COMMAND_SET, html_escape, our_context)
Expand Down
68 changes: 65 additions & 3 deletions src/antsibull_docs/jinja2/rstify.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,14 @@

import typing as t

from jinja2.runtime import Context
from jinja2.utils import pass_context

from antsibull_core.logging import log

from ..semantic_helper import augment_plugin_name_type

from .filters import extract_plugin_data
from .parser import Command, CommandSet, convert_text


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


class _Context:
j2_context: Context
counts: t.Dict[str, int]
plugin_fqcn: t.Optional[str]
plugin_type: t.Optional[str]

def __init__(self):
def __init__(self, j2_context: Context):
self.j2_context = j2_context
self.counts = {
'italic': 0,
'bold': 0,
Expand All @@ -74,6 +84,7 @@ def __init__(self):
'return-value': 0,
'ruler': 0,
}
self.plugin_fqcn, self.plugin_type = extract_plugin_data(j2_context)


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


class _OptionName(Command):
command = 'O'
parameter_count = 1
escaped_content = True

def handle(self, parameters: t.List[str], context: t.Any) -> str:
context.counts['option-name'] += 1
if context.plugin_fqcn is None or context.plugin_type is None:
raise Exception('The markup O(...) cannot be used outside a plugin or role')
text = augment_plugin_name_type(parameters[0], context.plugin_fqcn, context.plugin_type)
return f"\\ :ansopt:`{rst_escape(text, escape_ending_whitespace=True)}`\\ "


class _OptionValue(Command):
command = 'V'
parameter_count = 1
escaped_content = True

def handle(self, parameters: t.List[str], context: t.Any) -> str:
context.counts['option-value'] += 1
return f"\\ :ansval:`{rst_escape(parameters[0], escape_ending_whitespace=True)}`\\ "


class _EnvVariable(Command):
command = 'E'
parameter_count = 1
escaped_content = True

def handle(self, parameters: t.List[str], context: t.Any) -> str:
context.counts['environment-var'] += 1
return f"\\ :envvar:`{rst_escape(parameters[0], escape_ending_whitespace=True)}`\\ "


class _RetValue(Command):
command = 'RV'
parameter_count = 1
escaped_content = True

def handle(self, parameters: t.List[str], context: t.Any) -> str:
context.counts['return-value'] += 1
if context.plugin_fqcn is None or context.plugin_type is None:
raise Exception('The markup RV(...) cannot be used outside a plugin or role')
text = augment_plugin_name_type(parameters[0], context.plugin_fqcn, context.plugin_type)
return f"\\ :ansretval:`{rst_escape(text, escape_ending_whitespace=True)}`\\ "


class _HorizontalLine(Command):
command = 'HORIZONTALLINE'
parameter_count = 0
Expand All @@ -194,16 +251,21 @@ def handle(self, parameters: t.List[str], context: t.Any) -> str:
_Link(),
_Ref(),
_Const(),
_OptionName(),
_OptionValue(),
_EnvVariable(),
_RetValue(),
_HorizontalLine(),
])


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

our_context = _Context()
our_context = _Context(context)

try:
text = convert_text(text, _COMMAND_SET, rst_escape, our_context)
Expand Down
4 changes: 3 additions & 1 deletion src/antsibull_docs/lint_extra_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import re
import typing as t

from sphinx_antsibull_ext import roles as antsibull_roles

from .extra_docs import (
find_extra_docs,
lint_required_conditions,
Expand All @@ -33,7 +35,7 @@ def lint_optional_conditions(content: str, path: str, collection_name: str
Return a list of errors.
'''
return check_rst_content(content, filename=path)
return check_rst_content(content, filename=path, ignore_roles=list(antsibull_roles.ROLES))


def lint_collection_extra_docs_files(path_to_collection: str
Expand Down
Loading

0 comments on commit 8e6b851

Please sign in to comment.