From b7c11c016e1fe202a62e5f6fa9b488154642bfa5 Mon Sep 17 00:00:00 2001 From: Jakub Kuczys Date: Wed, 4 Mar 2026 22:12:17 +0100 Subject: [PATCH] Add Sphinx extension for extracting prompt contents (#6671) --- docs/_ext/prompt_builder.py | 154 ++++++++++++++++++ docs/conf.py | 1 + .../_includes/install-and-setup-red-unix.rst | 4 + docs/install_guides/windows.rst | 4 + 4 files changed, 163 insertions(+) create mode 100644 docs/_ext/prompt_builder.py diff --git a/docs/_ext/prompt_builder.py b/docs/_ext/prompt_builder.py new file mode 100644 index 000000000..6001771dd --- /dev/null +++ b/docs/_ext/prompt_builder.py @@ -0,0 +1,154 @@ +from __future__ import annotations + +import json +import os +from typing import Any, Dict, List, Set + +from docutils import nodes +from docutils.io import StringOutput +from docutils.nodes import Element + +from sphinx.application import Sphinx +from sphinx.builders.text import TextBuilder +from sphinx.writers.text import TextWriter +from sphinx.util import logging +from sphinx.util.docutils import SphinxTranslator + +logger = logging.getLogger(__name__) + + +class PromptTranslator(SphinxTranslator): + builder: PromptBuilder + + def __init__(self, document: nodes.document, builder: PromptBuilder) -> None: + super().__init__(document, builder) + self.body = "" + self.prompts: List[Dict[str, str]] = [] + + def visit_document(self, node: Element) -> None: + pass + + def depart_document(self, node: Element) -> None: + if not self.prompts: + self.body = "" + return + if self.builder.out_suffix.endswith(".json"): + self.body = json.dumps(self.prompts, indent=4) + else: + self.body = "\n".join(prompt["content"] for prompt in self.prompts) + + def unknown_visit(self, node: Element) -> None: + pass + + def unknown_departure(self, node: Element) -> None: + pass + + def visit_prompt(self, node: Element) -> None: + self.prompts.append( + { + "language": node.attributes["language"], + "prompts": node.attributes["prompts"], + "modifiers": node.attributes["modifiers"], + "rawsource": node.rawsource, + "content": node.children[0], + } + ) + + +class PromptWriter(TextWriter): + def translate(self) -> None: + visitor = self.builder.create_translator(self.document, self.builder) + self.document.walkabout(visitor) + self.output = visitor.body + + +class prompt(nodes.literal_block): + pass + + +class PromptBuilder(TextBuilder): + """Extract prompts from documents.""" + + format = "json" + epilog = "The files with prompts are in %(outdir)s." + + out_suffix = ".json" + default_translator_class = PromptTranslator + writer: PromptWriter + + def init(self) -> None: + sphinx_prompt = __import__("sphinx-prompt") + + def run(self) -> List[prompt]: + self.assert_has_content() + rawsource = "\n".join(self.content) + language = self.options.get("language") or "text" + prompts = [ + p + for p in ( + self.options.get("prompts") or sphinx_prompt.PROMPTS.get(language, "") + ).split(",") + if p + ] + modifiers = [ + modifier for modifier in self.options.get("modifiers", "").split(",") if modifier + ] + content = rawsource + if "auto" in modifiers: + parts = [] + for line in self.content: + for p in prompts: + if line.startswith(p): + line = line[len(p) + 1 :].rstrip() + parts.append(line) + content = "\n".join(parts) + node = prompt( + rawsource, + content, + directive_content=self.content, + language=language, + prompts=self.options.get("prompts") or sphinx_prompt.PROMPTS.get(language, ""), + modifiers=modifiers, + ) + return [node] + + sphinx_prompt.PromptDirective.run = run + + def prepare_writing(self, docnames: Set[str]) -> None: + del docnames + self.writer = PromptWriter(self) + + def write_doc(self, docname: str, doctree: nodes.document) -> None: + self.writer.write(doctree, StringOutput(encoding="utf-8")) + if not self.writer.output: + # don't write empty files + return + + filename = os.path.join(self.outdir, docname.replace("/", os.path.sep) + self.out_suffix) + os.makedirs(os.path.dirname(filename), exist_ok=True) + try: + with open(filename, "w", encoding="utf-8") as f: + f.write(self.writer.output) + except OSError as err: + logger.warning("error writing file %s: %s", filename, err) + + +class JsonPromptBuilder(PromptBuilder): + name = "jsonprompt" + out_suffix = ".json" + + +class TextPromptBuilder(PromptBuilder): + name = "textprompt" + out_suffix = ".txt" + + +def setup(app: Sphinx) -> Dict[str, Any]: + app.add_builder(JsonPromptBuilder) + app.add_builder(TextPromptBuilder) + + return { + "version": "1.0", + "parallel_read_safe": True, + "parallel_write_safe": True, + } diff --git a/docs/conf.py b/docs/conf.py index c280f1b29..a0d9b6a7d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -46,6 +46,7 @@ extensions = [ "sphinxcontrib_trio", "sphinx-prompt", "deprecated_removed", + "prompt_builder", ] # Add any paths that contain templates here, relative to this directory. diff --git a/docs/install_guides/_includes/install-and-setup-red-unix.rst b/docs/install_guides/_includes/install-and-setup-red-unix.rst index 5e4a18b01..150873168 100644 --- a/docs/install_guides/_includes/install-and-setup-red-unix.rst +++ b/docs/install_guides/_includes/install-and-setup-red-unix.rst @@ -8,6 +8,7 @@ To install without additional config backend support: .. prompt:: bash :prompts: (redenv) $ + :modifiers: red-install-guide-install-normal python -m pip install -U pip wheel python -m pip install -U Red-DiscordBot @@ -16,6 +17,7 @@ Or, to install with PostgreSQL support: .. prompt:: bash :prompts: (redenv) $ + :modifiers: red-install-guide-install-postgres python -m pip install -U pip wheel python -m pip install -U "Red-DiscordBot[postgres]" @@ -29,6 +31,7 @@ After installation, set up your instance with the following command: .. prompt:: bash :prompts: (redenv) $ + :modifiers: red-install-guide-setup redbot-setup @@ -40,6 +43,7 @@ Once done setting up the instance, run the following command to run Red: .. prompt:: bash :prompts: (redenv) $ + :modifiers: red-install-guide-run redbot diff --git a/docs/install_guides/windows.rst b/docs/install_guides/windows.rst index c118608e6..ab03a4753 100644 --- a/docs/install_guides/windows.rst +++ b/docs/install_guides/windows.rst @@ -133,6 +133,7 @@ Run **one** of the following set of commands, depending on what extras you want .. prompt:: batch :prompts: (redenv) C:\\> + :modifiers: red-install-guide-install-normal python -m pip install -U pip wheel python -m pip install -U Red-DiscordBot @@ -141,6 +142,7 @@ Run **one** of the following set of commands, depending on what extras you want .. prompt:: batch :prompts: (redenv) C:\\> + :modifiers: red-install-guide-install-postgres python -m pip install -U pip wheel python -m pip install -U Red-DiscordBot[postgres] @@ -153,6 +155,7 @@ After installation, set up your instance with the following command: .. prompt:: batch :prompts: (redenv) C:\\> + :modifiers: red-install-guide-setup redbot-setup @@ -164,6 +167,7 @@ Once done setting up the instance, run the following command to run Red: .. prompt:: batch :prompts: (redenv) C:\\> + :modifiers: red-install-guide-run redbot