Coverage for src / rtflite / pagination / processor.py: 94%

88 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-12-08 04:50 +0000

1from copy import deepcopy 

2from typing import Any 

3 

4from ..attributes import BroadcastValue 

5from .strategies.base import PageContext 

6 

7 

8class PageFeatureProcessor: 

9 """Processes page features like borders, headers, and footers for each page.""" 

10 

11 def process(self, document: Any, page: PageContext) -> PageContext: 

12 """Process a single page to apply all feature-specific logic.""" 

13 

14 # Calculate final attributes for the table body on this page 

15 # (This includes applying top/bottom borders correctly) 

16 page.final_body_attrs = self._apply_pagination_borders(document, page) 

17 

18 return page 

19 

20 def _should_show_element(self, element_location: str, page: PageContext) -> bool: 

21 """Determine if an element should be shown on a specific page.""" 

22 logic = { 

23 "all": True, 

24 "first": page.is_first_page, 

25 "last": page.is_last_page, 

26 } 

27 return logic.get(element_location, False) 

28 

29 def _apply_pagination_borders(self, document, page: PageContext) -> Any: 

30 """Apply proper borders for paginated context following r2rtf design.""" 

31 

32 # Start with a deep copy of the page's table attributes (processed/sliced) 

33 # or document's body attributes if not available 

34 base_attrs = page.table_attrs or document.rtf_body 

35 page_attrs = deepcopy(base_attrs) 

36 

37 page_df_height = page.data.height 

38 page_df_width = page.data.width 

39 page_shape = (page_df_height, page_df_width) 

40 

41 if page_df_height == 0: 

42 return page_attrs 

43 

44 # Clear border_first and border_last from being broadcast to all rows 

45 if hasattr(page_attrs, "border_first") and page_attrs.border_first: 

46 page_attrs.border_first = None 

47 

48 if hasattr(page_attrs, "border_last") and page_attrs.border_last: 

49 page_attrs.border_last = None 

50 

51 # Ensure border_top and border_bottom are properly sized for this page 

52 if not page_attrs.border_top: 

53 page_attrs.border_top = [ 

54 [""] * page_df_width for _ in range(page_df_height) 

55 ] 

56 if not page_attrs.border_bottom: 

57 page_attrs.border_bottom = [ 

58 [""] * page_df_width for _ in range(page_df_height) 

59 ] 

60 

61 # --- Logic from DocumentService.apply_pagination_borders --- 

62 

63 # 1. First Page Logic 

64 has_column_headers = ( 

65 document.rtf_column_header and len(document.rtf_column_header) > 0 

66 ) 

67 

68 # If first page, NO headers, apply PAGE border_first to top of body 

69 if ( 

70 page.is_first_page 

71 and not has_column_headers 

72 and document.rtf_page.border_first 

73 ): 

74 for col_idx in range(page_df_width): 

75 page_attrs = self._apply_border_to_cell( 

76 page_attrs, 

77 0, 

78 col_idx, 

79 "top", 

80 document.rtf_page.border_first, 

81 page_shape, 

82 ) 

83 

84 # If first page, WITH headers, apply BODY border_first to top of body 

85 if page.is_first_page and has_column_headers and document.rtf_body.border_first: 

86 self._apply_body_border_first( 

87 document, page_attrs, page_df_width, page_shape 

88 ) 

89 

90 # 2. Middle Page Logic (Non-First) 

91 # Apply BODY border_first to top of body 

92 if not page.is_first_page and document.rtf_body.border_first: 

93 self._apply_body_border_first( 

94 document, page_attrs, page_df_width, page_shape 

95 ) 

96 

97 # 3. Footnote/Source Logic 

98 has_footnote_on_page = ( 

99 document.rtf_footnote 

100 and document.rtf_footnote.text 

101 and self._should_show_element(document.rtf_page.page_footnote, page) 

102 ) 

103 has_source_on_page = ( 

104 document.rtf_source 

105 and document.rtf_source.text 

106 and self._should_show_element(document.rtf_page.page_source, page) 

107 ) 

108 

109 footnote_as_table_on_last = ( 

110 document.rtf_footnote 

111 and document.rtf_footnote.text 

112 and getattr(document.rtf_footnote, "as_table", True) 

113 and document.rtf_page.page_footnote in ("last", "all") 

114 ) 

115 source_as_table_on_last = ( 

116 document.rtf_source 

117 and document.rtf_source.text 

118 and getattr(document.rtf_source, "as_table", False) 

119 and document.rtf_page.page_source in ("last", "all") 

120 ) 

121 

122 # 4. Bottom Border Logic 

123 if not page.is_last_page: 

124 # Not last page: use BODY border_last 

125 if document.rtf_body.border_last: 

126 border_style = ( 

127 document.rtf_body.border_last[0][0] 

128 if isinstance(document.rtf_body.border_last, list) 

129 else document.rtf_body.border_last 

130 ) 

131 

132 if not (has_footnote_on_page or has_source_on_page): 

133 # Apply to last data row 

134 for col_idx in range(page_df_width): 

135 page_attrs = self._apply_border_to_cell( 

136 page_attrs, 

137 page_df_height - 1, 

138 col_idx, 

139 "bottom", 

140 border_style, 

141 page_shape, 

142 ) 

143 else: 

144 # Apply to component 

145 self._apply_footnote_source_borders( 

146 document, 

147 page, 

148 has_footnote_on_page, 

149 has_source_on_page, 

150 border_style, 

151 ) 

152 else: 

153 # Last page: use PAGE border_last 

154 if document.rtf_page.border_last: 

155 # Only if this is truly the end (not just last page of a section, 

156 # but for now we assume 1 section or last section) 

157 # The original code checked `page_info["end_row"] == total_rows - 1`. 

158 # Here we rely on `is_last_page` flag which comes from strategy. 

159 

160 if not (footnote_as_table_on_last or source_as_table_on_last): 

161 # Apply to last data row 

162 for col_idx in range(page_df_width): 

163 page_attrs = self._apply_border_to_cell( 

164 page_attrs, 

165 page_df_height - 1, 

166 col_idx, 

167 "bottom", 

168 document.rtf_page.border_last, 

169 page_shape, 

170 ) 

171 else: 

172 # Apply to component 

173 self._apply_footnote_source_borders( 

174 document, 

175 page, 

176 has_footnote_on_page, 

177 has_source_on_page, 

178 document.rtf_page.border_last, 

179 ) 

180 

181 return page_attrs 

182 

183 def _apply_body_border_first(self, document, page_attrs, page_df_width, page_shape): 

184 """Helper to apply body border_first logic.""" 

185 if isinstance(document.rtf_body.border_first, list): 

186 border_first_row = document.rtf_body.border_first[0] 

187 has_border_top = ( 

188 document.rtf_body.border_top 

189 and isinstance(document.rtf_body.border_top, list) 

190 and len(document.rtf_body.border_top[0]) > len(border_first_row) 

191 ) 

192 

193 for col_idx in range(page_df_width): 

194 if col_idx < len(border_first_row): 

195 border_style = border_first_row[col_idx] 

196 else: 

197 border_style = border_first_row[0] 

198 

199 if ( 

200 has_border_top 

201 and col_idx < len(document.rtf_body.border_top[0]) 

202 and document.rtf_body.border_top[0][col_idx] 

203 ): 

204 border_style = document.rtf_body.border_top[0][col_idx] 

205 

206 self._apply_border_to_cell( 

207 page_attrs, 0, col_idx, "top", border_style, page_shape 

208 ) 

209 else: 

210 for col_idx in range(page_df_width): 

211 self._apply_border_to_cell( 

212 page_attrs, 

213 0, 

214 col_idx, 

215 "top", 

216 document.rtf_body.border_first, 

217 page_shape, 

218 ) 

219 

220 def _apply_footnote_source_borders( 

221 self, 

222 document, 

223 page: PageContext, 

224 has_footnote: bool, 

225 has_source: bool, 

226 border_style: str, 

227 ): 

228 """Apply borders to footnote/source in the page context.""" 

229 target_component = None 

230 

231 footnote_as_table = None 

232 if has_footnote: 

233 footnote_as_table = getattr(document.rtf_footnote, "as_table", True) 

234 

235 source_as_table = None 

236 if has_source: 

237 source_as_table = getattr(document.rtf_source, "as_table", False) 

238 

239 if has_source and source_as_table: 

240 target_component = "source" 

241 elif has_footnote and footnote_as_table: 

242 target_component = "footnote" 

243 

244 if target_component: 

245 page.component_borders[target_component] = border_style 

246 

247 def _apply_border_to_cell( 

248 self, 

249 page_attrs, 

250 row_idx: int, 

251 col_idx: int, 

252 border_side: str, 

253 border_style: str, 

254 page_shape: tuple, 

255 ): 

256 """Apply specified border style to a specific cell using BroadcastValue""" 

257 border_attr = f"border_{border_side}" 

258 

259 if not hasattr(page_attrs, border_attr): 

260 return page_attrs 

261 

262 current_borders = getattr(page_attrs, border_attr) 

263 border_broadcast = BroadcastValue(value=current_borders, dimension=page_shape) 

264 border_broadcast.update_cell(row_idx, col_idx, border_style) 

265 setattr(page_attrs, border_attr, border_broadcast.value) 

266 return page_attrs