Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert collection links from raw HTML to directive #200

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion changelogs/fragments/195-problems.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
known_issues:
- "When using Sphinx builders other than HTML, the collection links section is empty (https://github.com/ansible-community/antsibull-docs/pull/195)."
- "When using Sphinx builders other than HTML and LaTeX, the indentation for nested options and return values is missing (https://github.com/ansible-community/antsibull-docs/pull/195)."
2 changes: 2 additions & 0 deletions changelogs/fragments/200-collection-links.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
minor_changes:
- "Create collection links with a custom directive. This makes them compatible with builders other than the HTML builder (https://github.com/ansible-community/antsibull-docs/pull/200)."
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@
#}

{% macro add(collection_links, collection_communication=None, collection=None) %}
.. raw:: html
.. ansible-links::

<p class="ansible-links">
{% for link in collection_links %}
<a href="@{ link.url | escape }@" aria-role="button" target="_blank" rel="noopener external">@{ link.description | escape }@</a>
- title: @{ link.description | antsibull_to_json }@
url: @{ link.url | antsibull_to_json }@
external: true
{% endfor %}
{% if collection_communication and collection and not collection_communication.empty %}
{# WARNING: the anchor is created from Sphinx from the label and might change #}
<a href="./#communication-for-@{ collection | replace('.', '-') | escape }@" aria-role="button" target="_blank">Communication</a>
- title: Communication
ref: communication_for_@{collection}@
{% endif %}
</p>

{% endmacro %}
4 changes: 4 additions & 0 deletions src/sphinx_antsibull_ext/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

from .assets import setup_assets
from .directives import setup_directives
from .nodes import setup_nodes
from .roles import setup_roles


Expand All @@ -30,6 +31,9 @@ def setup(app):
# Add assets
setup_assets(app)

# Add nodes
setup_nodes(app)

# Add roles
setup_roles(app)

Expand Down
2 changes: 1 addition & 1 deletion src/sphinx_antsibull_ext/antsibull-minimal.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 14 additions & 2 deletions src/sphinx_antsibull_ext/css/antsibull-minimal.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,21 @@
justify-content: flex-start;
flex-wrap: wrap;

> a {
> * {
margin: 2px 4px !important;
}

> li {
list-style: none !important;

> p {
display: inline;
}
}

a {
display: block;
padding: 4px 12px;
margin: 2px 4px;

cursor: pointer;
border-radius: 3px;
Expand Down
75 changes: 74 additions & 1 deletion src/sphinx_antsibull_ext/directives.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,14 @@
Add directives for general formatting.
"""

from collections.abc import Mapping, Sequence

from antsibull_core.yaml import load_yaml_bytes
from docutils import nodes
from docutils.parsers.rst import Directive
from sphinx import addnodes

from .nodes import link_button


class _OptionTypeLine(Directive):
Expand All @@ -21,7 +27,7 @@ def run(self):
self.state.nested_parse(self.content, self.content_offset, node)
for subnode in node.children:
if not isinstance(subnode, nodes.paragraph):
raise ValueError(
raise self.error(
f"{self.name} directive's children must all be paragraphs;"
f" found {type(subnode)}"
)
Expand All @@ -33,8 +39,75 @@ def run(self):
return node.children


class _Links(Directive):
has_content = True

def _get_string(
self, entry: Mapping, index: int, key: str, exp_type, optional: bool = False
):
value = entry.get(key)
if value is None and optional:
return value
if isinstance(value, exp_type):
return value
raise self.error(
f"{index + 1}th entry in {self.name}:"
f" entry '{key}' must be {exp_type}, but found {value!r}"
)

def run(self):
self.assert_has_content()
node = nodes.bullet_list("\n".join(self.content), classes=["ansible-links"])
content = "\n".join(self.content)
try:
data = load_yaml_bytes(content.encode("utf-8"))
except Exception as exc:
raise self.error(
f"Error while parsing content of {self.name} as YAML: {exc}"
) from exc
if not isinstance(data, Sequence):
raise self.error(
f"Content of {self.name} must be a YAML list, got {data!r} - {content!r}"
)
for index, entry in enumerate(data):
if not isinstance(entry, Mapping):
raise self.error(
f"Content of {self.name} must be a YAML list of mappings:"
" item {index + 1} is not a mapping, got {entry!r}"
)
title = self._get_string(entry, index, "title", str)
url = self._get_string(entry, index, "url", str, optional=True)
ref = self._get_string(entry, index, "ref", str, optional=True)
external = (
self._get_string(entry, index, "external", bool, optional=True) or False
)
if (url is None) == (ref is None):
raise self.error(
f"{index + 1}th entry in {self.name}:"
" either one 'url' or one 'ref' must be provided"
)
if url is not None:
refnode = link_button("", title, link_external=external, refuri=url)
else:
# Due to the way that Sphinx works, we have no chance to add
# aria-role to the resulting ref node :(
options = {
"reftype": "ref",
"refdomain": "std",
"refexplicit": True,
"refwarn": True,
}
refnode = addnodes.pending_xref("", nodes.inline("", title), **options)
refnode["reftarget"] = ref
item = nodes.list_item("")
item.append(nodes.inline("", "", refnode))
node.append(item)
return [node]


DIRECTIVES = {
"ansible-option-type-line": _OptionTypeLine,
"ansible-links": _Links,
}


Expand Down
43 changes: 43 additions & 0 deletions src/sphinx_antsibull_ext/nodes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Author: Felix Fontein <felix@fontein.de>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or
# https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: 2023, Ansible Project
"""
Add nodes for general formatting.
"""

from docutils import nodes


# pylint: disable-next=too-many-ancestors
class link_button(nodes.reference): # pyre-ignore[11]
def __init__(self, *args, link_external: bool = True, **kwargs):
super().__init__(*args, **kwargs)
self["link_external"] = link_external # pyre-ignore[16]


def visit_link_button_html(self, node) -> None:
atts = {
"class": "ansible-link reference",
"href": node["refuri"],
"aria-role": "button",
"target": "_blank",
}
if node["link_external"]:
atts["rel"] = "noopener external"
atts["class"] += " external"
else:
atts["class"] += " internal"
self.body.append(self.starttag(node, "a", "", **atts))


def depart_link_button_html(self, node):
self.depart_reference(node)


def setup_nodes(app):
"""
Setup nodes for a Sphinx app object.
"""
app.add_node(link_button, html=(visit_link_button_html, depart_link_button_html))
Loading