diff --git a/book/_config.yml b/book/_config.yml index 580aa76..1e55627 100644 --- a/book/_config.yml +++ b/book/_config.yml @@ -19,6 +19,7 @@ sphinx: - teachbooks_favourites - teachbooks_sphinx_grasple - sphinx_apa_references + - sphinx_tudelft_theme config: language: en html_js_files: diff --git a/book/_ext/sphinx_gated_directives/__init__.py b/book/_ext/sphinx_gated_directives/__init__.py index 7afe003..d213d60 100644 --- a/book/_ext/sphinx_gated_directives/__init__.py +++ b/book/_ext/sphinx_gated_directives/__init__.py @@ -30,6 +30,9 @@ from docutils import nodes from docutils.parsers.rst import Directive from docutils.parsers.rst import directives as du_directives +from dataclasses import dataclass, field +from typing import Dict, Optional + SUFFIX_START = "-start" SUFFIX_END = "-end" @@ -142,7 +145,7 @@ def make_startified_class( else: children = [children] # create a start_node and add all result nodes as its children - start_node_instance = start_node() + start_node_instance = start_node(orig_name) start_node_instance += children result = [start_node_instance] @@ -267,7 +270,7 @@ def make_endified_class( # return [] # next line should be good enough, but raises an error as not all has been implemented # maybe we need end nodes per directive type? - return [end_node()] + return [end_node(orig_name)] attrs["run"] = run new_cls_name = f"{base_cls.__name__}_End_For_{orig_name.replace(':', '_')}" @@ -333,10 +336,15 @@ def setup(app): # Node classes for start and end directives class start_node(nodes.Admonition, nodes.Element): - pass + + def __init__(self, type_of_directive: str=""): + self.type_of_directive = type_of_directive + super().__init__() class end_node(nodes.Admonition, nodes.Element): - pass + def __init__(self, type_of_directive: str=""): + self.type_of_directive = type_of_directive + super().__init__() # purge_registry def purge_registries(app: Sphinx, env: BuildEnvironment, docname: str) -> None: @@ -375,7 +383,7 @@ class CheckGatedDirectivesTransform(SphinxTransform): end = super_registry[docname]["end"] structure = "\n ".join(super_registry[docname]["msg"]) sequence = "".join(super_registry[docname]["sequence"]) - groups = re.findall("(SE)", sequence) + validate_SE_result = validate_SE(sequence) if len(start) > len(end): msg = f"The document {docname} contains more start directives than end directives:\n {structure}\nPlease ensure each start directive has a corresponding end directive." logger.error(msg) @@ -385,21 +393,23 @@ class CheckGatedDirectivesTransform(SphinxTransform): logger.error(msg) error = True # at this point, len(start) == len(end) - elif len(groups) != len(start) or len(groups) != len(end): - msg = f"The document {docname} contains nested start and end directives:\n {structure}\nThis is not allowed. Please correct the nesting." + # now check for correct nesting + elif not validate_SE_result.is_valid: + msg = f"The document {docname} contains incorrectly nested start and end directives:\n {structure}\nThis is not allowed. Please correct the nesting." logger.error(msg) error = True else: - # At this point, every start is followed by an end. + # At this point, every start is matched with an end # Now check that types match in order. types = super_registry[docname]["type"] - start_type = types[::2] - end_type = types[1::2] + start_type = [types[k] for k in validate_SE_result.pairs.keys()] + end_type = [types[v] for v in validate_SE_result.pairs.values()] for i in range(len(start_type)): if start_type[i] != end_type[i]: msg = f"The document {docname} contains mismatched start and end directives at lines {start[i]} and {end[i]}:\n {structure}\nPlease ensure that start and end directives match in type." logger.error(msg) error = True + break if error: raise Exception(f"[sphinx-gated-directives] An error has occurred when parsing gated directives in {docname}.\nPlease check warning messages above.") @@ -505,4 +515,60 @@ def findall(node: Element, *args, **kwargs) -> Iterator[Element]: # findall replaces traverse in docutils v0.18 # note a difference is that findall is an iterator impl = getattr(node, "findall", node.traverse) - return iter(impl(*args, **kwargs)) \ No newline at end of file + return iter(impl(*args, **kwargs)) + +@dataclass +class SEValidationResult: + is_valid: bool + pairs: Dict[int, int] = field(default_factory=dict) # {S_index: E_index} + error_index: Optional[int] = None # index of first error, if any + error_message: Optional[str] = None # description of the error, if any + +def validate_SE(s: str) -> SEValidationResult: + """ + Validates a string containing only 'S' (open) and 'E' (close). + Returns: + SEValidationResult with: + - is_valid: True iff all brackets match and no invalid characters occur. + - pairs: dict mapping each 'S' index to the corresponding 'E' index. + (zero-based indices in the original string) + - error_index, error_message: populated on first failure. + Errors reported: + * Invalid character (not 'S' or 'E'). + * 'E' encountered before any available 'S'. + * Unmatched 'S' remaining at the end. + The pairs dict is still returned for all successfully matched pairs up to the error. + """ + stack = [] # stack of indices of unmatched 'S' + pairs: Dict[int, int] = {} + + for i, ch in enumerate(s): + if ch == 'S': + stack.append(i) + elif ch == 'E': + if not stack: + return SEValidationResult( + is_valid=False, + pairs=pairs, + error_index=i, + error_message="Encountered 'E' before a matching 'S'." + ) + s_index = stack.pop() + pairs[s_index] = i + else: + return SEValidationResult( + is_valid=False, + pairs=pairs, + error_index=i, + error_message=f"Invalid character: {ch!r}. Only 'S' and 'E' are allowed." + ) + + if stack: + return SEValidationResult( + is_valid=False, + pairs=pairs, + error_index=len(s), + error_message=f"{len(stack)} unmatched 'S' remaining at end." + ) + + return SEValidationResult(is_valid=True, pairs=pairs) diff --git a/book/intro.md b/book/intro.md index 204b5a3..c70a559 100644 --- a/book/intro.md +++ b/book/intro.md @@ -6,6 +6,124 @@ kernelspec: # TeachBooks Manual +## Nesting inside of a start-directive + +### Different directives + +::::{prf:theorem-start} The parent theorem +:label: parent-theorem-label + +This is a parent theorem. This text is inside the parent theorem. + +:::{prf:axiom-start} A child theorem +:label: child-axiom-label + +This is a child axiom. This text is inside the child directive. +::: + +This is text after the child-start, before the child-end, inside the parent theorem. + +:::{prf:axiom-end} +::: + +Finally some text after the entire child, inside the theorem start. + +:::: + +Some text after the entire parent theorem start, but before the parent theorem end. + +:::{prf:theorem-end} +::: + +### Same directives + +::::{prf:theorem-start} The parent theorem +:label: parent-theorem-label2 + +This is a parent theorem. This text is inside the parent theorem. + +:::{prf:theorem-start} A child theorem +:label: child-theorem-label + +This is a child theorem. This text is inside the child directive. +::: + +This is text after the child-start, before the child-end, inside the parent theorem. + +:::{prf:theorem-end} +::: + +Finally some text after the entire child, inside the theorem start. + +:::: + +Some text after the entire parent theorem start, but before the parent theorem end. + +:::{prf:theorem-end} +::: + +## Nesting outside of a start-directive + +### Different directives + +Crashes still: + + + +### Same directives + +Crashes still: + + + +## Examples with code + +### Block directive with also a figure + :::{prf:theorem-start} A title :label: theorem-label @@ -32,6 +150,8 @@ print(f"{a} {b} {c}") :::{prf:theorem-end} ::: +### Figure directive with nothing with also another figure + :::{figure-start} images/nothing.svg :name: figure-label :alt: Nothing