|
8 | 8 | # from sphinx.errors import ExtensionError |
9 | 9 |
|
10 | 10 | from .nodes import ( |
| 11 | + exercise_node, |
| 12 | + exercise_enumerable_node, |
| 13 | + exercise_end_node, |
11 | 14 | solution_node, |
12 | 15 | solution_start_node, |
13 | 16 | solution_end_node, |
|
16 | 19 | logger = logging.getLogger(__name__) |
17 | 20 |
|
18 | 21 |
|
19 | | -class CheckGatedSolutions(SphinxTransform): |
| 22 | +class CheckGatedDirectives(SphinxTransform): |
20 | 23 | """ |
21 | 24 | This transform checks the structure of the gated solutions |
22 | 25 | to flag any errors in input |
@@ -117,3 +120,63 @@ def apply(self): |
117 | 120 | # Clean up Parent Node including :solution-end: |
118 | 121 | for child in parent.children[parent_start + 1 : parent_end + 1]: |
119 | 122 | parent.remove(child) |
| 123 | + |
| 124 | + |
| 125 | +class MergeGatedExercises(SphinxTransform): |
| 126 | + """ |
| 127 | + Transform Gated Exercise Directives into single unified |
| 128 | + Directives in the Sphinx Abstract Syntax Tree |
| 129 | +
|
| 130 | + Note: The CheckGatedDirectives Transform should ensure the |
| 131 | + structure of the gated directives is correct before |
| 132 | + this transform is run. |
| 133 | + """ |
| 134 | + |
| 135 | + default_priority = 10 |
| 136 | + |
| 137 | + def find_nodes(self, label, node): |
| 138 | + parent_node = node.parent |
| 139 | + parent_start, parent_end = None, None |
| 140 | + for idx1, child in enumerate(parent_node.children): |
| 141 | + if isinstance( |
| 142 | + child, (exercise_node, exercise_enumerable_node) |
| 143 | + ) and label == child.get("label"): |
| 144 | + parent_start = idx1 |
| 145 | + for idx2, child2 in enumerate(parent_node.children[parent_start:]): |
| 146 | + if isinstance(child2, exercise_end_node): |
| 147 | + parent_end = idx1 + idx2 |
| 148 | + break |
| 149 | + break |
| 150 | + return parent_start, parent_end |
| 151 | + |
| 152 | + def merge_nodes(self, node): |
| 153 | + label = node.get("label") |
| 154 | + parent_start, parent_end = self.find_nodes(label, node) |
| 155 | + if not parent_end: |
| 156 | + return |
| 157 | + parent = node.parent |
| 158 | + # Use Current Node and remove "-start" from class names and type |
| 159 | + updated_classes = [ |
| 160 | + cls.replace("-start", "") for cls in node.attributes["classes"] |
| 161 | + ] |
| 162 | + node.attributes["classes"] = updated_classes |
| 163 | + node.attributes["type"] = node.attributes["type"].replace("-start", "") |
| 164 | + # Attach content to section |
| 165 | + content = node.children[-1] |
| 166 | + for child in parent.children[parent_start + 1 : parent_end]: |
| 167 | + content += child |
| 168 | + # Clean up Parent Node including :exercise-end: |
| 169 | + for child in parent.children[parent_start + 1 : parent_end + 1]: |
| 170 | + parent.remove(child) |
| 171 | + |
| 172 | + def apply(self): |
| 173 | + # Process all matching exercise and exercise-enumerable (gated=True) |
| 174 | + # and exercise-end nodes |
| 175 | + for node in self.document.traverse(exercise_node): |
| 176 | + if node.gated: |
| 177 | + self.merge_nodes(node) |
| 178 | + node.gated = False |
| 179 | + for node in self.document.traverse(exercise_enumerable_node): |
| 180 | + if node.gated: |
| 181 | + self.merge_nodes(node) |
| 182 | + node.gated = False |
0 commit comments