From 51dcf8ea4fd4cd1d87bc126e34d74bf0d833afd9 Mon Sep 17 00:00:00 2001 From: Jason Kridner <jkridner@beagleboard.org> Date: Wed, 14 Sep 2022 11:40:19 -0400 Subject: [PATCH] _ext: add callouts support --- _ext/callouts.py | 199 +++++++++++++++++++++++++++++++++++++++++++++++ conf.py | 3 + 2 files changed, 202 insertions(+) create mode 100644 _ext/callouts.py diff --git a/_ext/callouts.py b/_ext/callouts.py new file mode 100644 index 00000000..97bb23e4 --- /dev/null +++ b/_ext/callouts.py @@ -0,0 +1,199 @@ +from docutils import nodes + +from sphinx.util.docutils import SphinxDirective +from sphinx.transforms import SphinxTransform +from docutils.nodes import Node + +# BASE_NUM = 2775 # black circles, white numbers +BASE_NUM = 2459 # white circle, black numbers + + +class CalloutIncludePostTransform(SphinxTransform): + """Code block post-processor for `literalinclude` blocks used in callouts.""" + + default_priority = 400 + + def apply(self, **kwargs) -> None: + visitor = LiteralIncludeVisitor(self.document) + self.document.walkabout(visitor) + + +class LiteralIncludeVisitor(nodes.NodeVisitor): + """Change a literal block upon visiting it.""" + + def __init__(self, document: nodes.document) -> None: + super().__init__(document) + + def unknown_visit(self, node: Node) -> None: + pass + + def unknown_departure(self, node: Node) -> None: + pass + + def visit_document(self, node: Node) -> None: + pass + + def depart_document(self, node: Node) -> None: + pass + + def visit_start_of_file(self, node: Node) -> None: + pass + + def depart_start_of_file(self, node: Node) -> None: + pass + + def visit_literal_block(self, node: nodes.literal_block) -> None: + if "<1>" in node.rawsource: + source = str(node.rawsource) + for i in range(1, 20): + source = source.replace( + f"<{i}>", chr(int(f"0x{BASE_NUM + i}", base=16)) + ) + node.rawsource = source + node[:] = [nodes.Text(source)] + + +class callout(nodes.General, nodes.Element): + """Sphinx callout node.""" + + pass + + +def visit_callout_node(self, node): + """We pass on node visit to prevent the + callout being treated as admonition.""" + pass + + +def depart_callout_node(self, node): + """Departing a callout node is a no-op, too.""" + pass + + +class annotations(nodes.Element): + """Sphinx annotations node.""" + + pass + + +def _replace_numbers(content: str): + """ + Replaces strings of the form <x> with circled unicode numbers (e.g. â‘ ) as text. + + Args: + content: Python str from a callout or annotations directive. + + Returns: The formatted content string. + """ + for i in range(1, 20): + content.replace(f"<{i}>", chr(int(f"0x{BASE_NUM + i}", base=16))) + return content + + +def _parse_recursively(self, node): + """Utility to recursively parse a node from the Sphinx AST.""" + self.state.nested_parse(self.content, self.content_offset, node) + + +class CalloutDirective(SphinxDirective): + """Code callout directive with annotations for Sphinx. + + Use this `callout` directive by wrapping either `code-block` or `literalinclude` + directives. Each line that's supposed to be equipped with an annotation should + have an inline comment of the form "# <x>" where x is an integer. + + Afterwards use the `annotations` directive to add annotations to the previously + defined code labels ("<x>") by using the syntax "<x> my annotation" to produce an + annotation "my annotation" for x. + Note that annotation lines have to be separated by a new line, i.e. + + .. annotations:: + + <1> First comment followed by a newline, + + <2> second comment after the newline. + + + Usage example: + ------------- + + .. callout:: + + .. code-block:: python + + from ray import tune + from ray.tune.search.hyperopt import HyperOptSearch + import keras + + def objective(config): # <1> + ... + + search_space = {"activation": tune.choice(["relu", "tanh"])} # <2> + algo = HyperOptSearch() + + tuner = tune.Tuner( # <3> + ... + ) + results = tuner.fit() + + .. annotations:: + + <1> Wrap a Keras model in an objective function. + + <2> Define a search space and initialize the search algorithm. + + <3> Start a Tune run that maximizes accuracy. + """ + + has_content = True + + def run(self): + self.assert_has_content() + + content = self.content + content = _replace_numbers(content) + + callout_node = callout("\n".join(content)) + _parse_recursively(self, callout_node) + + return [callout_node] + + +class AnnotationsDirective(SphinxDirective): + """Annotations directive, which is only used nested within a Callout directive.""" + + has_content = True + + def run(self): + content = self.content + content = _replace_numbers(content) + + joined_content = "\n".join(content) + annotations_node = callout(joined_content) + _parse_recursively(self, annotations_node) + + return [annotations_node] + + +def setup(app): + # Add new node types + app.add_node( + callout, + html=(visit_callout_node, depart_callout_node), + latex=(visit_callout_node, depart_callout_node), + text=(visit_callout_node, depart_callout_node), + ) + app.add_node(annotations) + + # Add new directives + app.add_directive("callout", CalloutDirective) + app.add_directive("annotations", AnnotationsDirective) + + # Add post-processor + app.add_post_transform(CalloutIncludePostTransform) + + return { + "version": "0.1", + "parallel_read_safe": True, + "parallel_write_safe": True, + } diff --git a/conf.py b/conf.py index 448c9712..8fdc7b32 100644 --- a/conf.py +++ b/conf.py @@ -23,7 +23,10 @@ author = 'BeagleBoard.org Foundation' # -- General configuration --------------------------------------------------- +sys.path.append(os.path.abspath("./_ext")) + extensions = [ + "callouts", "sphinxcontrib.rsvgconverter", "sphinx_design" ] -- GitLab