Coverage for structured_tutorials / sphinx / directives.py: 100%

33 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-21 19:08 +0100

1# Copyright (c) 2025 Mathias Ertl 

2# Licensed under the MIT License. See LICENSE file for details. 

3 

4"""Directives for Sphinx.""" 

5 

6from typing import TYPE_CHECKING 

7 

8from docutils.nodes import Node, paragraph 

9from docutils.parsers.rst.states import RSTState 

10from docutils.statemachine import StringList 

11from sphinx.environment import BuildEnvironment 

12from sphinx.util.docutils import SphinxDirective 

13 

14from structured_tutorials.sphinx.utils import TutorialWrapper, get_tutorial_path 

15 

16if TYPE_CHECKING: 

17 from sphinx.environment import _CurrentDocument 

18 

19 

20class CurrentDocumentMixin: 

21 """Mixin adding the current document property and context.""" 

22 

23 if TYPE_CHECKING: 

24 env: BuildEnvironment 

25 

26 # NOTE: sphinx 8.2.0 introduced "current_document", temp_data is deprecated and kept only for 

27 # backwards compatability: https://github.com/sphinx-doc/sphinx/pull/13151 

28 @property 

29 def current_document(self) -> "_CurrentDocument": # pragma: no cover 

30 if hasattr(self.env, "current_document"): 

31 return self.env.current_document 

32 else: 

33 return self.env.temp_data 

34 

35 

36class TutorialDirective(CurrentDocumentMixin, SphinxDirective): 

37 """Directive to specify the currently rendered tutorial.""" 

38 

39 has_content = False 

40 required_arguments = 1 

41 optional_arguments = 0 

42 

43 def run(self) -> list[Node]: 

44 tutorial_arg = self.arguments[0].strip() 

45 

46 command_text_width: int = self.config.structured_tutorial_command_text_width 

47 tutorial_path = get_tutorial_path(self.config.tutorial_root, tutorial_arg) 

48 self.current_document["tutorial"] = TutorialWrapper.from_file( 

49 tutorial_path, command_text_width=command_text_width 

50 ) 

51 

52 # NOTE: `highlighting` directive returns a custom Element for unknown reasons 

53 return [] 

54 

55 

56class PartDirective(CurrentDocumentMixin, SphinxDirective): 

57 """Directive to show a tutorial part.""" 

58 

59 required_arguments = 0 

60 optional_arguments = 1 

61 

62 def run(self) -> list[paragraph]: 

63 # Get the named ID if set. 

64 part_id = None 

65 if self.arguments: 

66 part_id = self.arguments[0].strip() 

67 

68 # Render text 

69 tutorial_wrapper: TutorialWrapper = self.current_document["tutorial"] 

70 text = tutorial_wrapper.render_part(part_id=part_id) 

71 

72 source, _lineno = self.get_source_info() 

73 

74 # Create sphinx node 

75 node = paragraph() 

76 paragraph.source = source 

77 state: RSTState = self.state 

78 state.nested_parse(StringList(text.splitlines(), source=source), 0, node) 

79 return [node]