Coverage for pyWebLayout/layout/document_layouter.py: 77%

216 statements  

« prev     ^ index     » next       coverage.py v7.11.2, created at 2025-11-12 12:02 +0000

1from __future__ import annotations 

2 

3from typing import List, Tuple, Optional, Union 

4import numpy as np 

5 

6from pyWebLayout.concrete import Page, Line, Text 

7from pyWebLayout.concrete.image import RenderableImage 

8from pyWebLayout.concrete.functional import ButtonText, FormFieldText 

9from pyWebLayout.concrete.table import TableRenderer, TableStyle 

10from pyWebLayout.abstract import Paragraph, Word 

11from pyWebLayout.abstract.block import Image as AbstractImage, PageBreak, Table 

12from pyWebLayout.abstract.functional import Button, Form, FormField 

13from pyWebLayout.style.concrete_style import ConcreteStyleRegistry, RenderingContext, StyleResolver 

14from pyWebLayout.style import Font, Alignment 

15 

16 

17def paragraph_layouter(paragraph: Paragraph, 

18 page: Page, 

19 start_word: int = 0, 

20 pretext: Optional[Text] = None, 

21 alignment_override: Optional['Alignment'] = None) -> Tuple[bool, 

22 Optional[int], 

23 Optional[Text]]: 

24 """ 

25 Layout a paragraph of text within a given page. 

26 

27 This function extracts word spacing constraints from the style system 

28 and uses them to create properly spaced lines of text. 

29 

30 Args: 

31 paragraph: The paragraph to layout 

32 page: The page to layout the paragraph on 

33 start_word: Index of the first word to process (for continuation) 

34 pretext: Optional pretext from a previous hyphenated word 

35 alignment_override: Optional alignment to override the paragraph's default alignment 

36 

37 Returns: 

38 Tuple of: 

39 - bool: True if paragraph was completely laid out, False if page ran out of space 

40 - Optional[int]: Index of first word that didn't fit (if any) 

41 - Optional[Text]: Remaining pretext if word was hyphenated (if any) 

42 """ 

43 if not paragraph.words: 

44 return True, None, None 

45 

46 # Validate inputs 

47 if start_word >= len(paragraph.words): 

48 return True, None, None 

49 

50 # paragraph.style is already a Font object (concrete), not AbstractStyle 

51 # We need to get word spacing constraints from the Font's abstract style if available 

52 # For now, use reasonable defaults based on font size 

53 

54 if isinstance(paragraph.style, Font): 

55 # paragraph.style is already a Font (concrete style) 

56 font = paragraph.style 

57 # Use default word spacing constraints based on font size 

58 # Minimum spacing should be proportional to font size for better readability 

59 min_spacing = float(font.font_size) * 0.25 # 25% of font size 

60 max_spacing = float(font.font_size) * 0.5 # 50% of font size 

61 word_spacing_constraints = (int(min_spacing), int(max_spacing)) 

62 text_align = Alignment.LEFT # Default alignment 

63 else: 

64 # paragraph.style is an AbstractStyle, resolve it 

65 # Ensure font_size is an int (it could be a FontSize enum) 

66 from pyWebLayout.style.abstract_style import FontSize 

67 if isinstance(paragraph.style.font_size, FontSize): 67 ↛ 71line 67 didn't jump to line 71 because the condition on line 67 was always true

68 # Use a default base font size, the resolver will handle the semantic size 

69 base_font_size = 16 

70 else: 

71 base_font_size = int(paragraph.style.font_size) 

72 

73 rendering_context = RenderingContext(base_font_size=base_font_size) 

74 style_resolver = StyleResolver(rendering_context) 

75 style_registry = ConcreteStyleRegistry(style_resolver) 

76 concrete_style = style_registry.get_concrete_style(paragraph.style) 

77 font = concrete_style.create_font() 

78 word_spacing_constraints = ( 

79 int(concrete_style.word_spacing_min), 

80 int(concrete_style.word_spacing_max) 

81 ) 

82 text_align = concrete_style.text_align 

83 

84 # Apply page-level word spacing override if specified 

85 if hasattr( 85 ↛ 91line 85 didn't jump to line 91 because the condition on line 85 was never true

86 page.style, 

87 'word_spacing') and isinstance( 

88 page.style.word_spacing, 

89 int) and page.style.word_spacing > 0: 

90 # Add the page-level word spacing to both min and max constraints 

91 min_ws, max_ws = word_spacing_constraints 

92 word_spacing_constraints = ( 

93 min_ws + page.style.word_spacing, 

94 max_ws + page.style.word_spacing 

95 ) 

96 

97 # Apply alignment override if provided 

98 if alignment_override is not None: 98 ↛ 99line 98 didn't jump to line 99 because the condition on line 98 was never true

99 text_align = alignment_override 

100 

101 # Cap font size to page maximum if needed 

102 if font.font_size > page.style.max_font_size: 102 ↛ 104line 102 didn't jump to line 104 because the condition on line 102 was never true

103 # Use paragraph's font registry to create the capped font 

104 if hasattr(paragraph, 'get_or_create_font'): 

105 font = paragraph.get_or_create_font( 

106 font_path=font._font_path, 

107 font_size=page.style.max_font_size, 

108 colour=font.colour, 

109 weight=font.weight, 

110 style=font.style, 

111 decoration=font.decoration, 

112 background=font.background 

113 ) 

114 else: 

115 # Fallback to direct creation (will still use global cache) 

116 font = Font( 

117 font_path=font._font_path, 

118 font_size=page.style.max_font_size, 

119 colour=font.colour, 

120 weight=font.weight, 

121 style=font.style, 

122 decoration=font.decoration, 

123 background=font.background 

124 ) 

125 

126 # Calculate baseline-to-baseline spacing: font size + additional line spacing 

127 # This is the vertical distance between baselines of consecutive lines 

128 # Formula: baseline_spacing = font_size + line_spacing (absolute pixels) 

129 line_spacing_value = getattr(page.style, 'line_spacing', 5) 

130 # Ensure line_spacing is an int (could be Mock in tests) 

131 if not isinstance(line_spacing_value, int): 

132 line_spacing_value = 5 

133 baseline_spacing = font.font_size + line_spacing_value 

134 

135 # Get font metrics for boundary checking 

136 ascent, descent = font.font.getmetrics() 

137 

138 def create_new_line(word: Optional[Union[Word, Text]] = None, 

139 is_first_line: bool = False) -> Optional[Line]: 

140 """Helper function to create a new line, returns None if page is full.""" 

141 # Check if this line's baseline and descenders would fit on the page 

142 if not page.can_fit_line(baseline_spacing, ascent, descent): 

143 return None 

144 

145 # For the first line, position it so text starts at the top boundary 

146 # For subsequent lines, use current y_offset which tracks 

147 # baseline-to-baseline spacing 

148 if is_first_line: 148 ↛ 151line 148 didn't jump to line 151 because the condition on line 148 was never true

149 # Position line origin so that baseline (origin + ascent) is close to top 

150 # We want minimal space above the text, so origin should be at boundary 

151 y_cursor = page._current_y_offset 

152 else: 

153 y_cursor = page._current_y_offset 

154 x_cursor = page.border_size 

155 

156 # Create a temporary Text object to calculate word width 

157 if word: 

158 temp_text = Text.from_word(word, page.draw) 

159 temp_text.width 

160 else: 

161 pass 

162 

163 return Line( 

164 spacing=word_spacing_constraints, 

165 origin=(x_cursor, y_cursor), 

166 size=(page.available_width, baseline_spacing), 

167 draw=page.draw, 

168 font=font, 

169 halign=text_align 

170 ) 

171 

172 # Create initial line 

173 current_line = create_new_line() 

174 if not current_line: 

175 return False, start_word, pretext 

176 

177 page.add_child(current_line) 

178 # Note: add_child already updates _current_y_offset based on child's origin and size 

179 # No need to manually increment it here 

180 

181 # Track current position in paragraph 

182 current_pretext = pretext 

183 

184 # Process words starting from start_word 

185 for i, word in enumerate(paragraph.words[start_word:], start=start_word): 

186 # Check if this is a LinkedWord and needs special handling in concrete layer 

187 # Note: The Line.add_word method will create Text objects internally, 

188 # but we may want to create LinkText for LinkedWord instances in future 

189 # For now, the abstract layer (LinkedWord) carries the link info, 

190 # and the concrete layer (LinkText) would be created during rendering 

191 

192 success, overflow_text = current_line.add_word(word, current_pretext) 

193 

194 if success: 

195 # Word fit successfully 

196 if overflow_text is not None: 

197 # If there's overflow text, we need to start a new line with it 

198 current_pretext = overflow_text 

199 current_line = create_new_line(overflow_text) 

200 if not current_line: 

201 # If we can't create a new line, return with the current state 

202 return False, i, overflow_text 

203 page.add_child(current_line) 

204 # Note: add_child already updates _current_y_offset 

205 # Continue to the next word 

206 continue 

207 else: 

208 # No overflow, clear pretext 

209 current_pretext = None 

210 else: 

211 # Word didn't fit, need a new line 

212 current_line = create_new_line(word) 

213 if not current_line: 

214 # Page is full, return current position 

215 return False, i, overflow_text 

216 

217 # Check if the word will fit on the new line before adding it 

218 temp_text = Text.from_word(word, page.draw) 

219 if temp_text.width > current_line.size[0]: 

220 # Word is too wide for the line, we need to hyphenate it 

221 if len(word.text) >= 6: 221 ↛ 243line 221 didn't jump to line 243 because the condition on line 221 was always true

222 # Try to hyphenate the word 

223 splits = [ 

224 (Text( 

225 pair[0], 

226 word.style, 

227 page.draw, 

228 line=current_line, 

229 source=word), 

230 Text( 

231 pair[1], 

232 word.style, 

233 page.draw, 

234 line=current_line, 

235 source=word)) for pair in word.possible_hyphenation()] 

236 if len(splits) > 0: 236 ↛ 243line 236 didn't jump to line 243 because the condition on line 236 was always true

237 # Use the first hyphenation point 

238 first_part, second_part = splits[0] 

239 current_line.add_word(word, first_part) 

240 current_pretext = second_part 

241 continue 

242 

243 page.add_child(current_line) 

244 # Note: add_child already updates _current_y_offset 

245 

246 # Try to add the word to the new line 

247 success, overflow_text = current_line.add_word(word, current_pretext) 

248 

249 if not success: 249 ↛ 252line 249 didn't jump to line 252 because the condition on line 249 was never true

250 # Word still doesn't fit even on a new line 

251 # This might happen with very long words or narrow pages 

252 if overflow_text: 

253 # Word was hyphenated, continue with the overflow 

254 current_pretext = overflow_text 

255 continue 

256 else: 

257 # Word cannot be broken, skip it or handle as error 

258 # For now, we'll return indicating we couldn't process this word 

259 return False, i, None 

260 else: 

261 current_pretext = overflow_text # May be None or hyphenated remainder 

262 

263 # All words processed successfully 

264 return True, None, None 

265 

266 

267def pagebreak_layouter(page_break: PageBreak, page: Page) -> bool: 

268 """ 

269 Handle a page break element. 

270 

271 A page break signals that all subsequent content should start on a new page. 

272 This function always returns False to indicate that the current page is complete 

273 and a new page should be created for subsequent content. 

274 

275 Args: 

276 page_break: The PageBreak block 

277 page: The current page (not used, but kept for consistency) 

278 

279 Returns: 

280 bool: Always False to force creation of a new page 

281 """ 

282 # Page break always forces a new page 

283 return False 

284 

285 

286def image_layouter(image: AbstractImage, page: Page, max_width: Optional[int] = None, 

287 max_height: Optional[int] = None) -> bool: 

288 """ 

289 Layout an image within a given page. 

290 

291 This function places an image on the page, respecting size constraints 

292 and available space. Images are centered horizontally by default. 

293 

294 Args: 

295 image: The abstract Image object to layout 

296 page: The page to layout the image on 

297 max_width: Maximum width constraint (defaults to page available width) 

298 max_height: Maximum height constraint (defaults to remaining page height) 

299 

300 Returns: 

301 bool: True if image was successfully laid out, False if page ran out of space 

302 """ 

303 # Use page available width if max_width not specified 

304 if max_width is None: 

305 max_width = page.available_width 

306 

307 # Calculate available height on page 

308 available_height = page.size[1] - page._current_y_offset - page.border_size 

309 

310 # If no space available, image doesn't fit 

311 if available_height <= 0: 311 ↛ 312line 311 didn't jump to line 312 because the condition on line 311 was never true

312 return False 

313 

314 if max_height is None: 

315 max_height = available_height 

316 else: 

317 max_height = min(max_height, available_height) 

318 

319 # Calculate scaled dimensions 

320 scaled_width, scaled_height = image.calculate_scaled_dimensions( 

321 max_width, max_height) 

322 

323 # Check if image fits on current page 

324 if scaled_height is None or scaled_height > available_height: 

325 return False 

326 

327 # Create renderable image 

328 x_offset = page.border_size 

329 y_offset = page._current_y_offset 

330 

331 # Access page.draw to ensure canvas is initialized 

332 _ = page.draw 

333 

334 renderable_image = RenderableImage( 

335 image=image, 

336 canvas=page._canvas, 

337 max_width=max_width, 

338 max_height=max_height, 

339 origin=(x_offset, y_offset), 

340 size=(scaled_width or max_width, scaled_height or max_height), 

341 halign=Alignment.CENTER, 

342 valign=Alignment.TOP 

343 ) 

344 

345 # Add to page 

346 page.add_child(renderable_image) 

347 

348 return True 

349 

350 

351def table_layouter( 

352 table: Table, 

353 page: Page, 

354 style: Optional[TableStyle] = None) -> bool: 

355 """ 

356 Layout a table within a given page. 

357 

358 This function uses the TableRenderer to render the table at the current 

359 page position, advancing the page's y-offset after successful rendering. 

360 

361 Args: 

362 table: The abstract Table object to layout 

363 page: The page to layout the table on 

364 style: Optional table styling configuration 

365 

366 Returns: 

367 bool: True if table was successfully laid out, False if page ran out of space 

368 """ 

369 # Calculate available space 

370 available_width = page.available_width 

371 x_offset = page.border_size 

372 y_offset = page._current_y_offset 

373 

374 # Access page.draw to ensure canvas is initialized 

375 draw = page.draw 

376 canvas = page._canvas 

377 

378 # Create table renderer 

379 origin = (x_offset, y_offset) 

380 renderer = TableRenderer( 

381 table=table, 

382 origin=origin, 

383 available_width=available_width, 

384 draw=draw, 

385 style=style, 

386 canvas=canvas 

387 ) 

388 

389 # Check if table fits on current page 

390 table_height = renderer.size[1] 

391 available_height = page.size[1] - y_offset - page.border_size 

392 

393 if table_height > available_height: 

394 return False 

395 

396 # Render the table 

397 renderer.render() 

398 

399 # Update page y-offset 

400 page._current_y_offset = y_offset + table_height 

401 

402 return True 

403 

404 

405def button_layouter(button: Button, 

406 page: Page, 

407 font: Optional[Font] = None, 

408 padding: Tuple[int, 

409 int, 

410 int, 

411 int] = (4, 

412 8, 

413 4, 

414 8)) -> Tuple[bool, 

415 str]: 

416 """ 

417 Layout a button within a given page and register it for callback binding. 

418 

419 This function creates a ButtonText renderable, positions it on the page, 

420 and registers it in the page's callback registry using the button's html_id 

421 (if available) or an auto-generated id. 

422 

423 Args: 

424 button: The abstract Button object to layout 

425 page: The page to layout the button on 

426 font: Optional font for button text (defaults to page default) 

427 padding: Padding around button text (top, right, bottom, left) 

428 

429 Returns: 

430 Tuple of: 

431 - bool: True if button was successfully laid out, False if page ran out of space 

432 - str: The id used to register the button in the callback registry 

433 """ 

434 # Use provided font or create a default one 

435 if font is None: 

436 font = Font(font_size=14, colour=(255, 255, 255)) 

437 

438 # Calculate available space 

439 available_height = page.size[1] - page._current_y_offset - page.border_size 

440 

441 # Create ButtonText renderable 

442 button_text = ButtonText(button, font, page.draw, padding=padding) 

443 

444 # Check if button fits on current page 

445 button_height = button_text.size[1] 

446 if button_height > available_height: 

447 return False, "" 

448 

449 # Position the button 

450 x_offset = page.border_size 

451 y_offset = page._current_y_offset 

452 

453 button_text.set_origin(np.array([x_offset, y_offset])) 

454 

455 # Register in callback registry 

456 html_id = button.html_id 

457 registered_id = page.callbacks.register(button_text, html_id=html_id) 

458 

459 # Add to page 

460 page.add_child(button_text) 

461 

462 return True, registered_id 

463 

464 

465def form_field_layouter(field: FormField, page: Page, font: Optional[Font] = None, 

466 field_height: int = 24) -> Tuple[bool, str]: 

467 """ 

468 Layout a form field within a given page and register it for callback binding. 

469 

470 This function creates a FormFieldText renderable, positions it on the page, 

471 and registers it in the page's callback registry. 

472 

473 Args: 

474 field: The abstract FormField object to layout 

475 page: The page to layout the field on 

476 font: Optional font for field label (defaults to page default) 

477 field_height: Height of the input field area 

478 

479 Returns: 

480 Tuple of: 

481 - bool: True if field was successfully laid out, False if page ran out of space 

482 - str: The id used to register the field in the callback registry 

483 """ 

484 # Use provided font or create a default one 

485 if font is None: 485 ↛ 486line 485 didn't jump to line 486 because the condition on line 485 was never true

486 font = Font(font_size=12, colour=(0, 0, 0)) 

487 

488 # Calculate available space 

489 available_height = page.size[1] - page._current_y_offset - page.border_size 

490 

491 # Create FormFieldText renderable 

492 field_text = FormFieldText(field, font, page.draw, field_height=field_height) 

493 

494 # Check if field fits on current page 

495 total_field_height = field_text.size[1] 

496 if total_field_height > available_height: 496 ↛ 497line 496 didn't jump to line 497 because the condition on line 496 was never true

497 return False, "" 

498 

499 # Position the field 

500 x_offset = page.border_size 

501 y_offset = page._current_y_offset 

502 

503 field_text.set_origin(np.array([x_offset, y_offset])) 

504 

505 # Register in callback registry (use field name as html_id fallback) 

506 html_id = getattr(field, '_html_id', None) or field.name 

507 registered_id = page.callbacks.register(field_text, html_id=html_id) 

508 

509 # Add to page 

510 page.add_child(field_text) 

511 

512 return True, registered_id 

513 

514 

515def form_layouter(form: Form, page: Page, font: Optional[Font] = None, 

516 field_spacing: int = 10) -> Tuple[bool, List[str]]: 

517 """ 

518 Layout a complete form with all its fields within a given page. 

519 

520 This function creates FormFieldText renderables for all fields in the form, 

521 positions them vertically, and registers both the form and its fields in 

522 the page's callback registry. 

523 

524 Args: 

525 form: The abstract Form object to layout 

526 page: The page to layout the form on 

527 font: Optional font for field labels (defaults to page default) 

528 field_spacing: Vertical spacing between fields in pixels 

529 

530 Returns: 

531 Tuple of: 

532 - bool: True if form was successfully laid out, False if page ran out of space 

533 - List[str]: List of registered ids for all fields (empty if layout failed) 

534 """ 

535 # Use provided font or create a default one 

536 if font is None: 536 ↛ 537line 536 didn't jump to line 537 because the condition on line 536 was never true

537 font = Font(font_size=12, colour=(0, 0, 0)) 

538 

539 # Track registered field ids 

540 field_ids = [] 

541 

542 # Layout each field in the form 

543 for field_name, field in form._fields.items(): 

544 # Add spacing before each field (except the first) 

545 if field_ids: 

546 page._current_y_offset += field_spacing 

547 

548 # Layout the field 

549 success, field_id = form_field_layouter(field, page, font) 

550 

551 if not success: 551 ↛ 553line 551 didn't jump to line 553 because the condition on line 551 was never true

552 # Couldn't fit this field, return failure 

553 return False, [] 

554 

555 field_ids.append(field_id) 

556 

557 # Register the form itself (optional, for form submission) 

558 # Note: The form doesn't have a visual representation, but we can track it 

559 # for submission callbacks 

560 # form_id = page.callbacks.register(form, html_id=form.html_id) 

561 

562 return True, field_ids 

563 

564 

565class DocumentLayouter: 

566 """ 

567 Document layouter that orchestrates layout of various abstract elements. 

568 

569 Delegates to specialized layouters for different content types: 

570 - paragraph_layouter for text paragraphs 

571 - image_layouter for images 

572 - table_layouter for tables 

573 

574 This class acts as a coordinator, managing the overall document flow 

575 and page context while delegating specific layout tasks to specialized 

576 layouter functions. 

577 """ 

578 

579 def __init__(self, page: Page): 

580 """ 

581 Initialize the document layouter with a page. 

582 

583 Args: 

584 page: The page to layout content on 

585 """ 

586 self.page = page 

587 # Create a style resolver if page doesn't have one 

588 if hasattr(page, 'style_resolver'): 

589 style_resolver = page.style_resolver 

590 else: 

591 # Create a default rendering context and style resolver 

592 from pyWebLayout.style.concrete_style import RenderingContext 

593 context = RenderingContext() 

594 style_resolver = StyleResolver(context) 

595 self.style_registry = ConcreteStyleRegistry(style_resolver) 

596 

597 def layout_paragraph(self, 

598 paragraph: Paragraph, 

599 start_word: int = 0, 

600 pretext: Optional[Text] = None) -> Tuple[bool, 

601 Optional[int], 

602 Optional[Text]]: 

603 """ 

604 Layout a paragraph using the paragraph_layouter. 

605 

606 Args: 

607 paragraph: The paragraph to layout 

608 start_word: Index of the first word to process (for continuation) 

609 pretext: Optional pretext from a previous hyphenated word 

610 

611 Returns: 

612 Tuple of (success, failed_word_index, remaining_pretext) 

613 """ 

614 return paragraph_layouter(paragraph, self.page, start_word, pretext) 

615 

616 def layout_image(self, image: AbstractImage, max_width: Optional[int] = None, 

617 max_height: Optional[int] = None) -> bool: 

618 """ 

619 Layout an image using the image_layouter. 

620 

621 Args: 

622 image: The abstract Image object to layout 

623 max_width: Maximum width constraint (defaults to page available width) 

624 max_height: Maximum height constraint (defaults to remaining page height) 

625 

626 Returns: 

627 bool: True if image was successfully laid out, False if page ran out of space 

628 """ 

629 return image_layouter(image, self.page, max_width, max_height) 

630 

631 def layout_table(self, table: Table, style: Optional[TableStyle] = None) -> bool: 

632 """ 

633 Layout a table using the table_layouter. 

634 

635 Args: 

636 table: The abstract Table object to layout 

637 style: Optional table styling configuration 

638 

639 Returns: 

640 bool: True if table was successfully laid out, False if page ran out of space 

641 """ 

642 return table_layouter(table, self.page, style) 

643 

644 def layout_button(self, 

645 button: Button, 

646 font: Optional[Font] = None, 

647 padding: Tuple[int, 

648 int, 

649 int, 

650 int] = (4, 

651 8, 

652 4, 

653 8)) -> Tuple[bool, 

654 str]: 

655 """ 

656 Layout a button using the button_layouter. 

657 

658 Args: 

659 button: The abstract Button object to layout 

660 font: Optional font for button text 

661 padding: Padding around button text 

662 

663 Returns: 

664 Tuple of (success, registered_id) 

665 """ 

666 return button_layouter(button, self.page, font, padding) 

667 

668 def layout_form(self, form: Form, font: Optional[Font] = None, 

669 field_spacing: int = 10) -> Tuple[bool, List[str]]: 

670 """ 

671 Layout a form using the form_layouter. 

672 

673 Args: 

674 form: The abstract Form object to layout 

675 font: Optional font for field labels 

676 field_spacing: Vertical spacing between fields 

677 

678 Returns: 

679 Tuple of (success, list_of_field_ids) 

680 """ 

681 return form_layouter(form, self.page, font, field_spacing) 

682 

683 def layout_document( 

684 self, elements: List[Union[Paragraph, AbstractImage, Table, Button, Form]]) -> bool: 

685 """ 

686 Layout a list of abstract elements (paragraphs, images, tables, buttons, and forms). 

687 

688 This method delegates to specialized layouters based on element type: 

689 - Paragraphs are handled by layout_paragraph 

690 - Images are handled by layout_image 

691 - Tables are handled by layout_table 

692 - Buttons are handled by layout_button 

693 - Forms are handled by layout_form 

694 

695 Args: 

696 elements: List of abstract elements to layout 

697 

698 Returns: 

699 True if all elements were successfully laid out, False otherwise 

700 """ 

701 for element in elements: 

702 if isinstance(element, Paragraph): 

703 success, _, _ = self.layout_paragraph(element) 

704 if not success: 

705 return False 

706 elif isinstance(element, AbstractImage): 

707 success = self.layout_image(element) 

708 if not success: 708 ↛ 709line 708 didn't jump to line 709 because the condition on line 708 was never true

709 return False 

710 elif isinstance(element, Table): 710 ↛ 714line 710 didn't jump to line 714 because the condition on line 710 was always true

711 success = self.layout_table(element) 

712 if not success: 

713 return False 

714 elif isinstance(element, Button): 

715 success, _ = self.layout_button(element) 

716 if not success: 

717 return False 

718 elif isinstance(element, Form): 

719 success, _ = self.layout_form(element) 

720 if not success: 

721 return False 

722 # Future: elif isinstance(element, CodeBlock): use code_layouter 

723 return True