File size: 5,116 Bytes
ee037e9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
"""A directive to generate a gallery of images from structured data.

Generating a gallery of images that are all the same size is a common
pattern in documentation, and this can be cumbersome if the gallery is
generated programmatically. This directive wraps this particular use-case
in a helper-directive to generate it with a single YAML configuration file.

It currently exists for maintainers of the pydata-sphinx-theme,
but might be abstracted into a standalone package if it proves useful.
"""

from pathlib import Path
from typing import Any, ClassVar, Dict, List

from docutils import nodes
from docutils.parsers.rst import directives
from sphinx.application import Sphinx
from sphinx.util import logging
from sphinx.util.docutils import SphinxDirective
from yaml import safe_load

logger = logging.getLogger(__name__)


TEMPLATE_GRID = """
`````{{grid}} {columns}
{options}

{content}

`````
"""

GRID_CARD = """
````{{grid-item-card}} {title}
{options}

{content}
````
"""


class GalleryGridDirective(SphinxDirective):
    """A directive to show a gallery of images and links in a Bootstrap grid.

    The grid can be generated from a YAML file that contains a list of items, or
    from the content of the directive (also formatted in YAML). Use the parameter
    "class-card" to add an additional CSS class to all cards. When specifying the grid
    items, you can use all parameters from "grid-item-card" directive to customize
    individual cards + ["image", "header", "content", "title"].

    Danger:
        This directive can only be used in the context of a Myst documentation page as
        the templates use Markdown flavored formatting.
    """

    name = "gallery-grid"
    has_content = True
    required_arguments = 0
    optional_arguments = 1
    final_argument_whitespace = True
    option_spec: ClassVar[dict[str, Any]] = {
        # A class to be added to the resulting container
        "grid-columns": directives.unchanged,
        "class-container": directives.unchanged,
        "class-card": directives.unchanged,
    }

    def run(self) -> List[nodes.Node]:
        """Create the gallery grid."""
        if self.arguments:
            # If an argument is given, assume it's a path to a YAML file
            # Parse it and load it into the directive content
            path_data_rel = Path(self.arguments[0])
            path_doc, _ = self.get_source_info()
            path_doc = Path(path_doc).parent
            path_data = (path_doc / path_data_rel).resolve()
            if not path_data.exists():
                logger.info(f"Could not find grid data at {path_data}.")
                nodes.text("No grid data found at {path_data}.")
                return
            yaml_string = path_data.read_text()
        else:
            yaml_string = "\n".join(self.content)

        # Use all the element with an img-bottom key as sites to show
        # and generate a card item for each of them
        grid_items = []
        for item in safe_load(yaml_string):
            # remove parameters that are not needed for the card options
            title = item.pop("title", "")

            # build the content of the card using some extra parameters
            header = f"{item.pop('header')}  \n^^^  \n" if "header" in item else ""
            image = f"![image]({item.pop('image')})  \n" if "image" in item else ""
            content = f"{item.pop('content')}  \n" if "content" in item else ""

            # optional parameter that influence all cards
            if "class-card" in self.options:
                item["class-card"] = self.options["class-card"]

            loc_options_str = "\n".join(f":{k}: {v}" for k, v in item.items()) + "  \n"

            card = GRID_CARD.format(
                options=loc_options_str, content=header + image + content, title=title
            )
            grid_items.append(card)

        # Parse the template with Sphinx Design to create an output container
        # Prep the options for the template grid
        class_ = "gallery-directive" + f' {self.options.get("class-container", "")}'
        options = {"gutter": 2, "class-container": class_}
        options_str = "\n".join(f":{k}: {v}" for k, v in options.items())

        # Create the directive string for the grid
        grid_directive = TEMPLATE_GRID.format(
            columns=self.options.get("grid-columns", "1 2 3 4"),
            options=options_str,
            content="\n".join(grid_items),
        )

        # Parse content as a directive so Sphinx Design processes it
        container = nodes.container()
        self.state.nested_parse([grid_directive], 0, container)

        # Sphinx Design outputs a container too, so just use that
        return [container.children[0]]


def setup(app: Sphinx) -> Dict[str, Any]:
    """Add custom configuration to sphinx app.

    Args:
        app: the Sphinx application

    Returns:
        the 2 parallel parameters set to ``True``.
    """
    app.add_directive("gallery-grid", GalleryGridDirective)

    return {
        "parallel_read_safe": True,
        "parallel_write_safe": True,
    }