From cf7da1843c45a4c2df7a749f7886a2d2ba0ee92a Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Mon, 15 Apr 2024 19:25:40 +0200 Subject: Adding upstream version 7.2.6. Signed-off-by: Daniel Baumann --- sphinx/ext/todo.py | 246 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 246 insertions(+) create mode 100644 sphinx/ext/todo.py (limited to 'sphinx/ext/todo.py') diff --git a/sphinx/ext/todo.py b/sphinx/ext/todo.py new file mode 100644 index 0000000..e540e7e --- /dev/null +++ b/sphinx/ext/todo.py @@ -0,0 +1,246 @@ +"""Allow todos to be inserted into your documentation. + +Inclusion of todos can be switched of by a configuration variable. +The todolist directive collects all todos of your project and lists them along +with a backlink to the original location. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, cast + +from docutils import nodes +from docutils.parsers.rst import directives +from docutils.parsers.rst.directives.admonitions import BaseAdmonition + +import sphinx +from sphinx import addnodes +from sphinx.domains import Domain +from sphinx.errors import NoUri +from sphinx.locale import _, __ +from sphinx.util import logging, texescape +from sphinx.util.docutils import SphinxDirective, new_document + +if TYPE_CHECKING: + from docutils.nodes import Element, Node + + from sphinx.application import Sphinx + from sphinx.environment import BuildEnvironment + from sphinx.util.typing import OptionSpec + from sphinx.writers.html import HTML5Translator + from sphinx.writers.latex import LaTeXTranslator + +logger = logging.getLogger(__name__) + + +class todo_node(nodes.Admonition, nodes.Element): + pass + + +class todolist(nodes.General, nodes.Element): + pass + + +class Todo(BaseAdmonition, SphinxDirective): + """ + A todo entry, displayed (if configured) in the form of an admonition. + """ + + node_class = todo_node + has_content = True + required_arguments = 0 + optional_arguments = 0 + final_argument_whitespace = False + option_spec: OptionSpec = { + 'class': directives.class_option, + 'name': directives.unchanged, + } + + def run(self) -> list[Node]: + if not self.options.get('class'): + self.options['class'] = ['admonition-todo'] + + (todo,) = super().run() + if isinstance(todo, nodes.system_message): + return [todo] + elif isinstance(todo, todo_node): + todo.insert(0, nodes.title(text=_('Todo'))) + todo['docname'] = self.env.docname + self.add_name(todo) + self.set_source_info(todo) + self.state.document.note_explicit_target(todo) + return [todo] + else: + raise RuntimeError # never reached here + + +class TodoDomain(Domain): + name = 'todo' + label = 'todo' + + @property + def todos(self) -> dict[str, list[todo_node]]: + return self.data.setdefault('todos', {}) + + def clear_doc(self, docname: str) -> None: + self.todos.pop(docname, None) + + def merge_domaindata(self, docnames: list[str], otherdata: dict) -> None: + for docname in docnames: + self.todos[docname] = otherdata['todos'][docname] + + def process_doc(self, env: BuildEnvironment, docname: str, + document: nodes.document) -> None: + todos = self.todos.setdefault(docname, []) + for todo in document.findall(todo_node): + env.app.emit('todo-defined', todo) + todos.append(todo) + + if env.config.todo_emit_warnings: + logger.warning(__("TODO entry found: %s"), todo[1].astext(), + location=todo) + + +class TodoList(SphinxDirective): + """ + A list of all todo entries. + """ + + has_content = False + required_arguments = 0 + optional_arguments = 0 + final_argument_whitespace = False + option_spec: OptionSpec = {} + + def run(self) -> list[Node]: + # Simply insert an empty todolist node which will be replaced later + # when process_todo_nodes is called + return [todolist('')] + + +class TodoListProcessor: + def __init__(self, app: Sphinx, doctree: nodes.document, docname: str) -> None: + self.builder = app.builder + self.config = app.config + self.env = app.env + self.domain = cast(TodoDomain, app.env.get_domain('todo')) + self.document = new_document('') + + self.process(doctree, docname) + + def process(self, doctree: nodes.document, docname: str) -> None: + todos: list[todo_node] = sum(self.domain.todos.values(), []) + for node in list(doctree.findall(todolist)): + if not self.config.todo_include_todos: + node.parent.remove(node) + continue + + if node.get('ids'): + content: list[Element] = [nodes.target()] + else: + content = [] + + for todo in todos: + # Create a copy of the todo node + new_todo = todo.deepcopy() + new_todo['ids'].clear() + + self.resolve_reference(new_todo, docname) + content.append(new_todo) + + todo_ref = self.create_todo_reference(todo, docname) + content.append(todo_ref) + + node.replace_self(content) + + def create_todo_reference(self, todo: todo_node, docname: str) -> nodes.paragraph: + if self.config.todo_link_only: + description = _('<>') + else: + description = (_('(The <> is located in %s, line %d.)') % + (todo.source, todo.line)) + + prefix = description[:description.find('<<')] + suffix = description[description.find('>>') + 2:] + + para = nodes.paragraph(classes=['todo-source']) + para += nodes.Text(prefix) + + # Create a reference + linktext = nodes.emphasis(_('original entry'), _('original entry')) + reference = nodes.reference('', '', linktext, internal=True) + try: + reference['refuri'] = self.builder.get_relative_uri(docname, todo['docname']) + reference['refuri'] += '#' + todo['ids'][0] + except NoUri: + # ignore if no URI can be determined, e.g. for LaTeX output + pass + + para += reference + para += nodes.Text(suffix) + + return para + + def resolve_reference(self, todo: todo_node, docname: str) -> None: + """Resolve references in the todo content.""" + for node in todo.findall(addnodes.pending_xref): + if 'refdoc' in node: + node['refdoc'] = docname + + # Note: To resolve references, it is needed to wrap it with document node + self.document += todo + self.env.resolve_references(self.document, docname, self.builder) + self.document.remove(todo) + + +def visit_todo_node(self: HTML5Translator, node: todo_node) -> None: + if self.config.todo_include_todos: + self.visit_admonition(node) + else: + raise nodes.SkipNode + + +def depart_todo_node(self: HTML5Translator, node: todo_node) -> None: + self.depart_admonition(node) + + +def latex_visit_todo_node(self: LaTeXTranslator, node: todo_node) -> None: + if self.config.todo_include_todos: + self.body.append('\n\\begin{sphinxadmonition}{note}{') + self.body.append(self.hypertarget_to(node)) + + title_node = cast(nodes.title, node[0]) + title = texescape.escape(title_node.astext(), self.config.latex_engine) + self.body.append('%s:}' % title) + node.pop(0) + else: + raise nodes.SkipNode + + +def latex_depart_todo_node(self: LaTeXTranslator, node: todo_node) -> None: + self.body.append('\\end{sphinxadmonition}\n') + + +def setup(app: Sphinx) -> dict[str, Any]: + app.add_event('todo-defined') + app.add_config_value('todo_include_todos', False, 'html') + app.add_config_value('todo_link_only', False, 'html') + app.add_config_value('todo_emit_warnings', False, 'html') + + app.add_node(todolist) + app.add_node(todo_node, + html=(visit_todo_node, depart_todo_node), + latex=(latex_visit_todo_node, latex_depart_todo_node), + text=(visit_todo_node, depart_todo_node), + man=(visit_todo_node, depart_todo_node), + texinfo=(visit_todo_node, depart_todo_node)) + + app.add_directive('todo', Todo) + app.add_directive('todolist', TodoList) + app.add_domain(TodoDomain) + app.connect('doctree-resolved', TodoListProcessor) + return { + 'version': sphinx.__display_version__, + 'env_version': 2, + 'parallel_read_safe': True, + } -- cgit v1.2.3