1+ """A directive to generate a gallery of images from structured data.
2+
3+ Generating a gallery of images that are all the same size is a common
4+ pattern in documentation, and this can be cumbersome if the gallery is
5+ generated programmatically. This directive wraps this particular use-case
6+ in a helper-directive to generate it with a single YAML configuration file.
7+
8+ It currently exists for maintainers of the pydata-sphinx-theme,
9+ but might be abstracted into a standalone package if it proves useful.
10+ """
11+ from pathlib import Path
12+ from typing import Any , Dict , List
13+
14+ from docutils import nodes
15+ from docutils .parsers .rst import directives
16+ from sphinx .application import Sphinx
17+ from sphinx .util import logging
18+ from sphinx .util .docutils import SphinxDirective
19+ from yaml import safe_load
20+
21+ logger = logging .getLogger (__name__ )
22+
23+
24+ TEMPLATE_GRID = """
25+ `````{{grid}} {grid_columns}
26+ {container_options}
27+
28+ {content}
29+
30+ `````
31+ """
32+
33+ GRID_CARD = """
34+ ````{{grid-item-card}} {title}
35+ {card_options}
36+
37+ {content}
38+ ````
39+ """
40+
41+
42+ class GalleryDirective (SphinxDirective ):
43+ """A directive to show a gallery of images and links in a grid."""
44+
45+ name = "gallery-grid"
46+ has_content = True
47+ required_arguments = 0
48+ optional_arguments = 1
49+ final_argument_whitespace = True
50+ option_spec = {
51+ # A class to be added to the resulting container
52+ "grid-columns" : directives .unchanged ,
53+ "class-container" : directives .unchanged ,
54+ "class-card" : directives .unchanged ,
55+ }
56+
57+ def run (self ) -> List [nodes .Node ]:
58+ if self .arguments :
59+ # If an argument is given, assume it's a path to a YAML file
60+ # Parse it and load it into the directive content
61+ path_data_rel = Path (self .arguments [0 ])
62+ path_doc , _ = self .get_source_info ()
63+ path_doc = Path (path_doc ).parent
64+ path_data = (path_doc / path_data_rel ).resolve ()
65+ if not path_data .exists ():
66+ logger .warn (f"Could not find grid data at { path_data } ." )
67+ nodes .text ("No grid data found at {path_data}." )
68+ return
69+ yaml_string = path_data .read_text ()
70+ else :
71+ yaml_string = "\n " .join (self .content )
72+
73+ # Read in YAML so we can generate the gallery
74+ grid_data = safe_load (yaml_string )
75+
76+ grid_items = []
77+ for item in grid_data :
78+ # Grid card parameters
79+ options = {}
80+ if "website" in item :
81+ options ["link" ] = item ["website" ]
82+
83+ if "class-card" in self .options :
84+ options ["class-card" ] = self .options ["class-card" ]
85+
86+ if "img-background" in item :
87+ options ["img-background" ] = item ["img-background" ]
88+
89+ if "img-top" in item :
90+ options ["img-top" ] = item ["img-top" ]
91+
92+ if "img-bottom" in item :
93+ options ["img-bottom" ] = item ["img-bottom" ]
94+
95+ options_str = "\n " .join (f":{ k } : { v } " for k , v in options .items ()) + "\n \n "
96+
97+ # Grid card content
98+ content_str = ""
99+ if "header" in item :
100+ content_str += f"{ item ['header' ]} \n \n ^^^\n \n "
101+
102+ if "image" in item :
103+ content_str += f"\n \n "
104+
105+ if "content" in item :
106+ content_str += f"{ item ['content' ]} \n \n "
107+
108+ if "footer" in item :
109+ content_str += f"+++\n \n { item ['footer' ]} \n \n "
110+
111+ title = item .get ("title" , "" )
112+ content_str += "\n "
113+ grid_items .append (
114+ GRID_CARD .format (
115+ card_options = options_str , content = content_str , title = title
116+ )
117+ )
118+
119+ # Parse the template with Sphinx Design to create an output
120+ container = nodes .container ()
121+ # Prep the options for the template grid
122+ container_options = {"gutter" : 2 , "class-container" : "gallery-directive" }
123+ if "class-container" in self .options :
124+ container_options [
125+ "class-container"
126+ ] += f' { self .options ["class-container" ]} '
127+ container_options_str = "\n " .join (
128+ f":{ k } : { v } " for k , v in container_options .items ()
129+ )
130+
131+ # Create the directive string for the grid
132+ grid_directive = TEMPLATE_GRID .format (
133+ grid_columns = self .options .get ("grid-columns" , "1 2 3 4" ),
134+ container_options = container_options_str ,
135+ content = "\n " .join (grid_items ),
136+ )
137+ # Parse content as a directive so Sphinx Design processes it
138+ self .state .nested_parse ([grid_directive ], 0 , container )
139+ # Sphinx Design outputs a container too, so just use that
140+ container = container .children [0 ]
141+
142+ # Add extra classes
143+ if self .options .get ("container-class" , []):
144+ container .attributes ["classes" ] += self .options .get ("class" , [])
145+ return [container ]
146+
147+
148+ def setup (app : Sphinx ) -> Dict [str , Any ]:
149+ """Add custom configuration to sphinx app.
150+
151+ Args:
152+ app: the Sphinx application
153+ Returns:
154+ the 2 parallel parameters set to ``True``.
155+ """
156+ app .add_directive ("gallery-grid" , GalleryDirective )
157+
158+ return {
159+ "parallel_read_safe" : True ,
160+ "parallel_write_safe" : True ,
161+ }
0 commit comments