Coverage for src/prosemark/app/materialize_node.py: 100%

36 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-09-24 18:08 +0000

1"""MaterializeNode use case for converting placeholders to actual nodes.""" 

2 

3from pathlib import Path 

4from typing import TYPE_CHECKING 

5 

6from prosemark.domain.models import BinderItem, NodeId 

7from prosemark.exceptions import PlaceholderNotFoundError 

8 

9if TYPE_CHECKING: # pragma: no cover 

10 from prosemark.ports.binder_repo import BinderRepo 

11 from prosemark.ports.clock import Clock 

12 from prosemark.ports.console_port import ConsolePort 

13 from prosemark.ports.id_generator import IdGenerator 

14 from prosemark.ports.logger import Logger 

15 from prosemark.ports.node_repo import NodeRepo 

16 

17 

18class MaterializeNode: 

19 """Convert placeholder items in the binder to actual content nodes.""" 

20 

21 def __init__( 

22 self, 

23 *, 

24 binder_repo: 'BinderRepo', 

25 node_repo: 'NodeRepo', 

26 id_generator: 'IdGenerator', 

27 clock: 'Clock', 

28 console: 'ConsolePort', 

29 logger: 'Logger', 

30 ) -> None: 

31 """Initialize the MaterializeNode use case. 

32 

33 Args: 

34 binder_repo: Repository for binder operations. 

35 node_repo: Repository for node operations. 

36 id_generator: Generator for unique node IDs. 

37 clock: Clock for timestamps. 

38 console: Console output port. 

39 logger: Logger port. 

40 

41 """ 

42 self.binder_repo = binder_repo 

43 self.node_repo = node_repo 

44 self.id_generator = id_generator 

45 self.clock = clock 

46 self.console = console 

47 self.logger = logger 

48 

49 def execute( 

50 self, 

51 *, 

52 title: str, 

53 project_path: Path | None = None, 

54 ) -> NodeId: 

55 """Materialize a placeholder into a real node. 

56 

57 Args: 

58 title: Title of the placeholder to materialize. 

59 project_path: Project directory path. 

60 

61 Returns: 

62 The ID of the newly created node. 

63 

64 Raises: 

65 PlaceholderNotFoundError: If no placeholder with the given title is found. 

66 

67 """ 

68 project_path = project_path or Path.cwd() 

69 self.logger.info('Materializing placeholder: %s', title) 

70 

71 # Load existing binder 

72 binder = self.binder_repo.load() 

73 

74 # Find the placeholder item 

75 placeholder = self._find_placeholder(binder.roots, title) 

76 if not placeholder: 

77 msg = f"Placeholder '{title}' not found" 

78 raise PlaceholderNotFoundError(msg) 

79 

80 # Check if already materialized 

81 if placeholder.node_id: # pragma: no cover 

82 self.console.print_warning(f"'{title}' is already materialized") # pragma: no cover 

83 return placeholder.node_id # pragma: no cover 

84 

85 # Generate new node ID 

86 node_id = self.id_generator.new() 

87 

88 # Create the node files 

89 self.node_repo.create(node_id, title, None) 

90 

91 # Update the placeholder with the node ID 

92 placeholder.node_id = node_id 

93 

94 # Save updated binder 

95 self.binder_repo.save(binder) 

96 

97 self.console.print_success(f'Materialized "{title}" ({node_id.value})') 

98 self.console.print_info(f'Created files: {node_id.value}.md, {node_id.value}.notes.md') 

99 self.logger.info('Placeholder materialized: %s -> %s', title, node_id.value) 

100 

101 return node_id 

102 

103 def _find_placeholder(self, items: list[BinderItem], title: str) -> BinderItem | None: 

104 """Find a placeholder item by title in the hierarchy. 

105 

106 Args: 

107 items: List of binder items to search. 

108 title: Title to search for. 

109 

110 Returns: 

111 The placeholder item if found, None otherwise. 

112 

113 """ 

114 for item in items: 

115 if item.display_title == title and not item.node_id: 

116 return item 

117 found = self._find_placeholder(item.children, title) 

118 if found: 

119 return found 

120 return None