From 735face59352265f8fddeda1e7e6a795a47678e3 Mon Sep 17 00:00:00 2001 From: Gitea Action Date: Sat, 8 Aug 2026 20:35:15 +0000 Subject: [PATCH] Update coverage badges [skip ci] --- .coveragerc | 31 + .dockerignore | 43 + .gitea/workflows/ci.yml | 177 + .gitignore | 56 + ARCHITECTURE.md | 348 + Dockerfile.ci | 80 + FONT_SWITCHING_FEATURE.md | 139 + LICENSE | 21 + MANIFEST.in | 11 + README.md | 267 + cov_info/coverage-docs.svg | 58 + cov_info/coverage-summary.txt | 1 + cov_info/coverage.json | 1 + cov_info/coverage.svg | 21 + cov_info/coverage.xml | 5760 ++++++++ cov_info/htmlcov/.gitignore | 2 + cov_info/htmlcov/class_index.html | 2218 +++ cov_info/htmlcov/coverage_html_cb_dd2e7eb5.js | 735 + cov_info/htmlcov/favicon_32_cb_c827f16f.png | Bin 0 -> 1732 bytes cov_info/htmlcov/function_index.html | 11473 ++++++++++++++++ cov_info/htmlcov/index.html | 675 + cov_info/htmlcov/keybd_closed_cb_900cfef5.png | Bin 0 -> 9004 bytes cov_info/htmlcov/status.json | 1 + cov_info/htmlcov/style_cb_4667309f.css | 391 + .../z_20e398e67121d457___init___py.html | 121 + .../z_263f2e628cef8c50___init___py.html | 116 + .../z_263f2e628cef8c50_epub_reader_py.html | 702 + ...z_263f2e628cef8c50_html_extraction_py.html | 1067 ++ .../z_40407af872b0cf37___init___py.html | 133 + .../htmlcov/z_40407af872b0cf37_base_py.html | 547 + .../htmlcov/z_40407af872b0cf37_cache_py.html | 442 + ...40407af872b0cf37_callback_registry_py.html | 377 + .../z_40407af872b0cf37_highlight_py.html | 348 + .../z_40407af872b0cf37_persistence_py.html | 158 + .../htmlcov/z_40407af872b0cf37_query_py.html | 185 + .../z_427cc3035faf7633___init___py.html | 110 + ...427cc3035faf7633_document_layouter_py.html | 836 ++ .../z_427cc3035faf7633_ereader_layout_py.html | 917 ++ ...z_427cc3035faf7633_ereader_manager_py.html | 1248 ++ .../z_427cc3035faf7633_page_buffer_py.html | 443 + ...z_427cc3035faf7633_table_optimizer_py.html | 495 + .../z_7d48e1f4c6486fa2___init___py.html | 135 + .../htmlcov/z_7d48e1f4c6486fa2_box_py.html | 139 + .../z_7d48e1f4c6486fa2_dynamic_page_py.html | 517 + .../z_7d48e1f4c6486fa2_functional_py.html | 623 + .../htmlcov/z_7d48e1f4c6486fa2_image_py.html | 379 + ...48e1f4c6486fa2_interaction_handler_py.html | 409 + .../htmlcov/z_7d48e1f4c6486fa2_page_py.html | 566 + .../htmlcov/z_7d48e1f4c6486fa2_table_py.html | 805 ++ .../htmlcov/z_7d48e1f4c6486fa2_text_py.html | 1290 ++ .../z_af715639580e2d86___init___py.html | 121 + .../htmlcov/z_af715639580e2d86_block_py.html | 1514 ++ .../z_af715639580e2d86_document_py.html | 694 + .../z_af715639580e2d86_functional_py.html | 444 + .../htmlcov/z_af715639580e2d86_inline_py.html | 556 + ...af715639580e2d86_interactive_image_py.html | 262 + .../z_ba7f6bdeb0188088___init___py.html | 122 + .../z_ba7f6bdeb0188088_abstract_style_py.html | 454 + .../z_ba7f6bdeb0188088_alignment_py.html | 124 + .../z_ba7f6bdeb0188088_concrete_style_py.html | 584 + .../htmlcov/z_ba7f6bdeb0188088_fonts_py.html | 505 + .../z_ba7f6bdeb0188088_page_style_py.html | 163 + .../z_fc521de9aff00981___init___py.html | 107 + docs/ARCHITECTURE_REVIEW.md | 595 + docs/LAYOUT_REMEDIATION_SPEC.md | 1404 ++ docs/images/README.md | 213 + docs/images/demo_08_bundled_fonts.png | Bin 0 -> 97208 bytes docs/images/example_01_page_rendering.png | Bin 0 -> 8202 bytes docs/images/example_02_text_and_layout.png | Bin 0 -> 23455 bytes docs/images/example_03_page_layouts.png | Bin 0 -> 11923 bytes docs/images/example_04_table_rendering.png | Bin 0 -> 29985 bytes .../example_05_html_table_with_images.png | Bin 0 -> 100180 bytes .../images/example_06_functional_elements.png | Bin 0 -> 18076 bytes docs/images/example_07_button_animation.gif | Bin 0 -> 21030 bytes docs/images/example_07_pressed_state.png | Bin 0 -> 16548 bytes docs/images/example_08_pagination_auto.png | Bin 0 -> 89566 bytes .../images/example_08_pagination_explicit.png | Bin 0 -> 111264 bytes docs/images/example_09_link_navigation.png | Bin 0 -> 60682 bytes docs/images/example_10_forms.png | Bin 0 -> 31342 bytes .../images/example_11_table_text_wrapping.png | Bin 0 -> 77767 bytes docs/images/example_11b_simple_wrapping.png | Bin 0 -> 26931 bytes .../example_12_optimized_table_layout.png | Bin 0 -> 126272 bytes docs/images/example_13_table_pagination.png | Bin 0 -> 112179 bytes docs/images/example_14_interactive_table.png | Bin 0 -> 24113 bytes docs/images/font_family_switching.png | Bin 0 -> 63930 bytes .../images/font_family_switching_vertical.png | Bin 0 -> 62772 bytes docs/images/functional_elements_demo.png | Bin 0 -> 18076 bytes examples/01_simple_page_rendering.py | 217 + examples/02_text_and_layout.py | 215 + examples/03_page_layouts.py | 247 + examples/04_table_rendering.py | 364 + examples/05_html_table_with_images.py | 272 + examples/06_functional_elements_demo.py | 292 + examples/07_pressed_state_demo.py | 452 + examples/08_bundled_fonts_demo.py | 227 + examples/08_pagination_demo.py | 367 + examples/09_link_navigation_demo.py | 390 + examples/10_forms_demo.py | 374 + examples/11_font_family_switching_demo.py | 240 + examples/11_table_text_wrapping_demo.py | 312 + examples/11b_simple_table_wrapping.py | 121 + examples/12_optimized_table_layout_demo.py | 254 + examples/13_table_pagination_demo.py | 291 + examples/14_interactive_table.py | 312 + examples/README.md | 310 + examples/generate_readme_font_demo.py | 252 + pyWebLayout/__init__.py | 22 + pyWebLayout/abstract/__init__.py | 22 + pyWebLayout/abstract/block.py | 1415 ++ pyWebLayout/abstract/document.py | 595 + pyWebLayout/abstract/functional.py | 345 + pyWebLayout/abstract/inline.py | 457 + pyWebLayout/abstract/interactive_image.py | 163 + pyWebLayout/assets/fonts/DEJAVU_LICENSE.txt | 187 + pyWebLayout/assets/fonts/DEJAVU_README.md | 67 + pyWebLayout/assets/fonts/DejaVuSans-Bold.ttf | Bin 0 -> 705684 bytes .../assets/fonts/DejaVuSans-BoldOblique.ttf | Bin 0 -> 643292 bytes .../assets/fonts/DejaVuSans-Oblique.ttf | Bin 0 -> 635416 bytes pyWebLayout/assets/fonts/DejaVuSans.ttf | Bin 0 -> 757076 bytes .../assets/fonts/DejaVuSansMono-Bold.ttf | Bin 0 -> 331992 bytes .../fonts/DejaVuSansMono-BoldOblique.ttf | Bin 0 -> 253580 bytes .../assets/fonts/DejaVuSansMono-Oblique.ttf | Bin 0 -> 251932 bytes pyWebLayout/assets/fonts/DejaVuSansMono.ttf | Bin 0 -> 340712 bytes pyWebLayout/assets/fonts/DejaVuSerif-Bold.ttf | Bin 0 -> 356088 bytes .../assets/fonts/DejaVuSerif-BoldItalic.ttf | Bin 0 -> 347460 bytes .../assets/fonts/DejaVuSerif-Italic.ttf | Bin 0 -> 345996 bytes pyWebLayout/assets/fonts/DejaVuSerif.ttf | Bin 0 -> 380132 bytes pyWebLayout/assets/fonts/README.md | 129 + pyWebLayout/concrete/__init__.py | 36 + pyWebLayout/concrete/box.py | 40 + pyWebLayout/concrete/dynamic_page.py | 418 + pyWebLayout/concrete/functional.py | 524 + pyWebLayout/concrete/image.py | 280 + pyWebLayout/concrete/interaction_handler.py | 310 + pyWebLayout/concrete/page.py | 467 + pyWebLayout/concrete/table.py | 706 + pyWebLayout/concrete/text.py | 1191 ++ pyWebLayout/core/__init__.py | 34 + pyWebLayout/core/base.py | 448 + pyWebLayout/core/cache.py | 343 + pyWebLayout/core/callback_registry.py | 278 + pyWebLayout/core/highlight.py | 249 + pyWebLayout/core/persistence.py | 59 + pyWebLayout/core/query.py | 86 + pyWebLayout/io/__init__.py | 8 + pyWebLayout/io/readers/__init__.py | 17 + pyWebLayout/io/readers/epub_reader.py | 603 + pyWebLayout/io/readers/html_extraction.py | 968 ++ pyWebLayout/layout/README_EREADER_API.md | 207 + pyWebLayout/layout/__init__.py | 11 + pyWebLayout/layout/document_layouter.py | 737 + pyWebLayout/layout/ereader_layout.py | 818 ++ pyWebLayout/layout/ereader_manager.py | 1149 ++ pyWebLayout/layout/page_buffer.py | 344 + pyWebLayout/layout/table_optimizer.py | 396 + pyWebLayout/style/__init__.py | 23 + pyWebLayout/style/abstract_style.py | 355 + pyWebLayout/style/alignment.py | 25 + pyWebLayout/style/concrete_style.py | 485 + pyWebLayout/style/fonts.py | 406 + pyWebLayout/style/page_style.py | 64 + pyproject.toml | 94 + scripts/debug_text_positioning.py | 76 + scripts/epub_page_renderer.py | 431 + .../epub_page_renderer_documentlayouter.py | 391 + scripts/run_coverage.py | 126 + scripts/run_coverage_gutters.py | 63 + scripts/update_coverage_badges.py | 83 + scripts/update_coverage_gutters.py | 103 + setup.cfg | 17 + setup.py | 10 + tests/__init__.py | 6 + tests/abstract/__init__.py | 0 tests/abstract/test_abstract_blocks.py | 600 + tests/abstract/test_abstract_document.py | 696 + tests/abstract/test_abstract_functional.py | 529 + tests/abstract/test_abstract_inline.py | 859 ++ tests/abstract/test_document_mixins.py | 60 + tests/abstract/test_linked_elements.py | 194 + tests/concrete/__init__.py | 0 tests/concrete/test_alignment_handlers.py | 337 + tests/concrete/test_alignment_spacing.py | 176 + tests/concrete/test_canvas_lifecycle.py | 129 + tests/concrete/test_concrete_box.py | 101 + tests/concrete/test_concrete_functional.py | 499 + tests/concrete/test_concrete_image.py | 386 + tests/concrete/test_concrete_text.py | 321 + tests/concrete/test_dynamic_page.py | 313 + tests/concrete/test_form_field_geometry.py | 140 + .../test_functional_text_centering.py | 124 + tests/concrete/test_linkedword_hyphenation.py | 140 + .../concrete/test_new_page_implementation.py | 329 + tests/concrete/test_page_geometry.py | 133 + tests/concrete/test_table_rendering.py | 652 + tests/core/__init__.py | 1 + tests/core/test_cache.py | 244 + tests/core/test_highlight.py | 352 + tests/core/test_query_system.py | 433 + tests/data/Kimi Räikkönen - Wikipedia.html | 6892 ++++++++++ .../Ambox_important.svg.webp | Bin 0 -> 4356 bytes .../Ambox_important.svg_002.webp | Bin 0 -> 1920 bytes .../Ambox_important.svg_003.webp | Bin 0 -> 3242 bytes .../Commons-logo.svg.webp | Bin 0 -> 608 bytes .../Commons-logo.svg_002.webp | Bin 0 -> 1114 bytes .../Commons-logo.svg_003.webp | Bin 0 -> 1770 bytes .../Edit-clear.svg.webp | Bin 0 -> 2012 bytes .../Edit-clear.svg_002.webp | Bin 0 -> 4902 bytes .../Edit-clear.svg_003.webp | Bin 0 -> 3410 bytes .../Flag_of_Argentina.svg.webp | Bin 0 -> 140 bytes .../Flag_of_Argentina.svg_002.webp | Bin 0 -> 200 bytes .../Flag_of_Argentina.svg_003.webp | Bin 0 -> 378 bytes .../Flag_of_Australia_(converted).svg.webp | Bin 0 -> 438 bytes .../Flag_of_Australia_(converted).svg_002.webp | Bin 0 -> 526 bytes .../Flag_of_Austria.svg.webp | Bin 0 -> 94 bytes .../Flag_of_Austria.svg_002.webp | Bin 0 -> 72 bytes .../Flag_of_Austria.svg_003.webp | Bin 0 -> 90 bytes .../Flag_of_Barbados.svg.webp | Bin 0 -> 302 bytes .../Flag_of_Belgium_(civil).svg.webp | Bin 0 -> 68 bytes .../Flag_of_Belgium_(civil).svg_002.webp | Bin 0 -> 130 bytes .../Flag_of_Belgium_(civil).svg_003.webp | Bin 0 -> 134 bytes .../Flag_of_Brazil.svg.webp | Bin 0 -> 318 bytes .../Flag_of_Brazil.svg_002.webp | Bin 0 -> 888 bytes .../Flag_of_Brazil.svg_003.webp | Bin 0 -> 568 bytes .../Flag_of_Canada_(Pantone).svg.webp | Bin 0 -> 336 bytes .../Flag_of_Canada_(Pantone).svg_002.webp | Bin 0 -> 266 bytes .../Flag_of_Finland.svg.webp | Bin 0 -> 164 bytes .../Flag_of_Finland.svg_002.webp | Bin 0 -> 164 bytes .../Flag_of_Finland.svg_003.webp | Bin 0 -> 160 bytes .../Flag_of_France.svg.webp | Bin 0 -> 138 bytes .../Flag_of_France.svg_002.webp | Bin 0 -> 72 bytes .../Flag_of_Germany.svg.webp | Bin 0 -> 56 bytes .../Flag_of_Germany.svg_002.webp | Bin 0 -> 56 bytes .../Flag_of_Germany.svg_003.webp | Bin 0 -> 54 bytes .../Flag_of_Ireland.svg.webp | Bin 0 -> 70 bytes .../Flag_of_Ireland.svg_002.webp | Bin 0 -> 88 bytes .../Flag_of_Italy.svg.webp | Bin 0 -> 122 bytes .../Flag_of_Italy.svg_002.webp | Bin 0 -> 126 bytes .../Flag_of_Italy.svg_003.webp | Bin 0 -> 72 bytes .../Flag_of_Japan.svg.webp | Bin 0 -> 260 bytes .../Flag_of_Japan.svg_002.webp | Bin 0 -> 266 bytes .../Flag_of_Mexico.svg.webp | Bin 0 -> 416 bytes .../Flag_of_Mexico.svg_002.webp | Bin 0 -> 676 bytes .../Flag_of_Monaco.svg.webp | Bin 0 -> 52 bytes .../Flag_of_Monaco.svg_002.webp | Bin 0 -> 50 bytes .../Flag_of_Norway.svg.webp | Bin 0 -> 222 bytes .../Flag_of_Norway.svg_002.webp | Bin 0 -> 218 bytes .../Flag_of_Poland.svg.webp | Bin 0 -> 70 bytes .../Flag_of_Poland.svg_002.webp | Bin 0 -> 88 bytes .../Flag_of_South_Africa_(1928-1982).svg.webp | Bin 0 -> 178 bytes ...lag_of_South_Africa_(1928-1982).svg_002.webp | Bin 0 -> 292 bytes .../Flag_of_Sweden.svg.webp | Bin 0 -> 138 bytes .../Flag_of_Sweden.svg_002.webp | Bin 0 -> 126 bytes .../Flag_of_Sweden.svg_003.webp | Bin 0 -> 68 bytes .../Flag_of_Switzerland_(Pantone).svg.webp | Bin 0 -> 130 bytes .../Flag_of_Switzerland_(Pantone).svg_002.webp | Bin 0 -> 120 bytes .../Flag_of_Venezuela.svg.webp | Bin 0 -> 246 bytes .../Flag_of_Venezuela.svg_002.webp | Bin 0 -> 334 bytes ...g_of_the_People's_Republic_of_China.svg.webp | Bin 0 -> 220 bytes ..._the_People's_Republic_of_China.svg_002.webp | Bin 0 -> 262 bytes .../Flag_of_the_United_Kingdom.svg.webp | Bin 0 -> 316 bytes .../Flag_of_the_United_Kingdom.svg_002.webp | Bin 0 -> 334 bytes .../Flag_of_the_United_States.svg.webp | Bin 0 -> 196 bytes .../Flag_of_the_United_States.svg_002.webp | Bin 0 -> 336 bytes .../Flag_of_the_United_States.svg_003.webp | Bin 0 -> 290 bytes .../OOjs_UI_icon_edit-ltr-progressive.svg.webp | Bin 0 -> 202 bytes .../Symbol_category_class.svg.webp | Bin 0 -> 632 bytes .../Symbol_category_class.svg_002.webp | Bin 0 -> 1512 bytes .../Kimi Räikkönen - Wikipedia_files/load.css | 1 + .../Kimi Räikkönen - Wikipedia_files/load.js | 23 + .../load_002.css | 1 + tests/data/test.epub | Bin 0 -> 189381 bytes tests/examples/__init__.py | 0 tests/examples/test_08_pagination_demo.py | 221 + .../examples/test_09_link_navigation_demo.py | 179 + tests/examples/test_10_forms_demo.py | 159 + tests/io_tests/__init__.py | 0 tests/io_tests/test_epub_reader.py | 1041 ++ tests/io_tests/test_html_extraction.py | 687 + .../test_html_extraction_functions.py | 524 + tests/io_tests/test_html_file_loader.py | 136 + tests/io_tests/test_html_link_end_to_end.py | 164 + .../io_tests/test_html_link_interactivity.py | 211 + tests/io_tests/test_html_links.py | 184 + .../test_inline_content_in_containers.py | 169 + tests/layout/__init__.py | 9 + tests/layout/test_backward_navigation.py | 171 + tests/layout/test_ereader_highlights.py | 168 + tests/layout/test_ereader_image_rendering.py | 545 + tests/layout/test_ereader_interaction.py | 129 + tests/layout/test_ereader_layout.py | 873 ++ tests/layout/test_ereader_manager.py | 784 ++ tests/layout/test_font_scaling.py | 262 + tests/layout/test_html_links_in_ereader.py | 148 + tests/layout/test_navigation_consistency.py | 359 + tests/layout/test_page_buffer.py | 237 + tests/layout/test_page_spanning_blocks.py | 123 + tests/layout/test_table_optimizer.py | 397 + tests/layouter/__init__.py | 3 + tests/layouter/test_document_layouter.py | 802 ++ .../test_document_layouter_integration.py | 384 + tests/mixins/__init__.py | 0 tests/mixins/font_registry_tests.py | 126 + tests/mixins/metadata_tests.py | 71 + tests/style/__init__.py | 0 tests/style/test_html_style.py | 189 + tests/style/test_new_style_system.py | 233 + tests/style/test_word_spacing_constraints.py | 149 + tests/test_callback_registry.py | 211 + tests/test_interactive_image.py | 199 + tests/utils/__init__.py | 4 + tests/utils/test_font_utilities.py | 143 + tests/utils/test_fonts.py | 168 + 312 files changed, 91102 insertions(+) create mode 100644 .coveragerc create mode 100644 .dockerignore create mode 100644 .gitea/workflows/ci.yml create mode 100644 .gitignore create mode 100644 ARCHITECTURE.md create mode 100644 Dockerfile.ci create mode 100644 FONT_SWITCHING_FEATURE.md create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 README.md create mode 100644 cov_info/coverage-docs.svg create mode 100644 cov_info/coverage-summary.txt create mode 100644 cov_info/coverage.json create mode 100644 cov_info/coverage.svg create mode 100644 cov_info/coverage.xml create mode 100644 cov_info/htmlcov/.gitignore create mode 100644 cov_info/htmlcov/class_index.html create mode 100644 cov_info/htmlcov/coverage_html_cb_dd2e7eb5.js create mode 100644 cov_info/htmlcov/favicon_32_cb_c827f16f.png create mode 100644 cov_info/htmlcov/function_index.html create mode 100644 cov_info/htmlcov/index.html create mode 100644 cov_info/htmlcov/keybd_closed_cb_900cfef5.png create mode 100644 cov_info/htmlcov/status.json create mode 100644 cov_info/htmlcov/style_cb_4667309f.css create mode 100644 cov_info/htmlcov/z_20e398e67121d457___init___py.html create mode 100644 cov_info/htmlcov/z_263f2e628cef8c50___init___py.html create mode 100644 cov_info/htmlcov/z_263f2e628cef8c50_epub_reader_py.html create mode 100644 cov_info/htmlcov/z_263f2e628cef8c50_html_extraction_py.html create mode 100644 cov_info/htmlcov/z_40407af872b0cf37___init___py.html create mode 100644 cov_info/htmlcov/z_40407af872b0cf37_base_py.html create mode 100644 cov_info/htmlcov/z_40407af872b0cf37_cache_py.html create mode 100644 cov_info/htmlcov/z_40407af872b0cf37_callback_registry_py.html create mode 100644 cov_info/htmlcov/z_40407af872b0cf37_highlight_py.html create mode 100644 cov_info/htmlcov/z_40407af872b0cf37_persistence_py.html create mode 100644 cov_info/htmlcov/z_40407af872b0cf37_query_py.html create mode 100644 cov_info/htmlcov/z_427cc3035faf7633___init___py.html create mode 100644 cov_info/htmlcov/z_427cc3035faf7633_document_layouter_py.html create mode 100644 cov_info/htmlcov/z_427cc3035faf7633_ereader_layout_py.html create mode 100644 cov_info/htmlcov/z_427cc3035faf7633_ereader_manager_py.html create mode 100644 cov_info/htmlcov/z_427cc3035faf7633_page_buffer_py.html create mode 100644 cov_info/htmlcov/z_427cc3035faf7633_table_optimizer_py.html create mode 100644 cov_info/htmlcov/z_7d48e1f4c6486fa2___init___py.html create mode 100644 cov_info/htmlcov/z_7d48e1f4c6486fa2_box_py.html create mode 100644 cov_info/htmlcov/z_7d48e1f4c6486fa2_dynamic_page_py.html create mode 100644 cov_info/htmlcov/z_7d48e1f4c6486fa2_functional_py.html create mode 100644 cov_info/htmlcov/z_7d48e1f4c6486fa2_image_py.html create mode 100644 cov_info/htmlcov/z_7d48e1f4c6486fa2_interaction_handler_py.html create mode 100644 cov_info/htmlcov/z_7d48e1f4c6486fa2_page_py.html create mode 100644 cov_info/htmlcov/z_7d48e1f4c6486fa2_table_py.html create mode 100644 cov_info/htmlcov/z_7d48e1f4c6486fa2_text_py.html create mode 100644 cov_info/htmlcov/z_af715639580e2d86___init___py.html create mode 100644 cov_info/htmlcov/z_af715639580e2d86_block_py.html create mode 100644 cov_info/htmlcov/z_af715639580e2d86_document_py.html create mode 100644 cov_info/htmlcov/z_af715639580e2d86_functional_py.html create mode 100644 cov_info/htmlcov/z_af715639580e2d86_inline_py.html create mode 100644 cov_info/htmlcov/z_af715639580e2d86_interactive_image_py.html create mode 100644 cov_info/htmlcov/z_ba7f6bdeb0188088___init___py.html create mode 100644 cov_info/htmlcov/z_ba7f6bdeb0188088_abstract_style_py.html create mode 100644 cov_info/htmlcov/z_ba7f6bdeb0188088_alignment_py.html create mode 100644 cov_info/htmlcov/z_ba7f6bdeb0188088_concrete_style_py.html create mode 100644 cov_info/htmlcov/z_ba7f6bdeb0188088_fonts_py.html create mode 100644 cov_info/htmlcov/z_ba7f6bdeb0188088_page_style_py.html create mode 100644 cov_info/htmlcov/z_fc521de9aff00981___init___py.html create mode 100644 docs/ARCHITECTURE_REVIEW.md create mode 100644 docs/LAYOUT_REMEDIATION_SPEC.md create mode 100644 docs/images/README.md create mode 100644 docs/images/demo_08_bundled_fonts.png create mode 100644 docs/images/example_01_page_rendering.png create mode 100644 docs/images/example_02_text_and_layout.png create mode 100644 docs/images/example_03_page_layouts.png create mode 100644 docs/images/example_04_table_rendering.png create mode 100644 docs/images/example_05_html_table_with_images.png create mode 100644 docs/images/example_06_functional_elements.png create mode 100644 docs/images/example_07_button_animation.gif create mode 100644 docs/images/example_07_pressed_state.png create mode 100644 docs/images/example_08_pagination_auto.png create mode 100644 docs/images/example_08_pagination_explicit.png create mode 100644 docs/images/example_09_link_navigation.png create mode 100644 docs/images/example_10_forms.png create mode 100644 docs/images/example_11_table_text_wrapping.png create mode 100644 docs/images/example_11b_simple_wrapping.png create mode 100644 docs/images/example_12_optimized_table_layout.png create mode 100644 docs/images/example_13_table_pagination.png create mode 100644 docs/images/example_14_interactive_table.png create mode 100644 docs/images/font_family_switching.png create mode 100644 docs/images/font_family_switching_vertical.png create mode 100644 docs/images/functional_elements_demo.png create mode 100644 examples/01_simple_page_rendering.py create mode 100644 examples/02_text_and_layout.py create mode 100644 examples/03_page_layouts.py create mode 100644 examples/04_table_rendering.py create mode 100644 examples/05_html_table_with_images.py create mode 100644 examples/06_functional_elements_demo.py create mode 100644 examples/07_pressed_state_demo.py create mode 100644 examples/08_bundled_fonts_demo.py create mode 100644 examples/08_pagination_demo.py create mode 100644 examples/09_link_navigation_demo.py create mode 100644 examples/10_forms_demo.py create mode 100644 examples/11_font_family_switching_demo.py create mode 100644 examples/11_table_text_wrapping_demo.py create mode 100644 examples/11b_simple_table_wrapping.py create mode 100644 examples/12_optimized_table_layout_demo.py create mode 100644 examples/13_table_pagination_demo.py create mode 100644 examples/14_interactive_table.py create mode 100644 examples/README.md create mode 100644 examples/generate_readme_font_demo.py create mode 100644 pyWebLayout/__init__.py create mode 100644 pyWebLayout/abstract/__init__.py create mode 100644 pyWebLayout/abstract/block.py create mode 100644 pyWebLayout/abstract/document.py create mode 100644 pyWebLayout/abstract/functional.py create mode 100644 pyWebLayout/abstract/inline.py create mode 100644 pyWebLayout/abstract/interactive_image.py create mode 100644 pyWebLayout/assets/fonts/DEJAVU_LICENSE.txt create mode 100644 pyWebLayout/assets/fonts/DEJAVU_README.md create mode 100644 pyWebLayout/assets/fonts/DejaVuSans-Bold.ttf create mode 100644 pyWebLayout/assets/fonts/DejaVuSans-BoldOblique.ttf create mode 100644 pyWebLayout/assets/fonts/DejaVuSans-Oblique.ttf create mode 100644 pyWebLayout/assets/fonts/DejaVuSans.ttf create mode 100644 pyWebLayout/assets/fonts/DejaVuSansMono-Bold.ttf create mode 100644 pyWebLayout/assets/fonts/DejaVuSansMono-BoldOblique.ttf create mode 100644 pyWebLayout/assets/fonts/DejaVuSansMono-Oblique.ttf create mode 100644 pyWebLayout/assets/fonts/DejaVuSansMono.ttf create mode 100644 pyWebLayout/assets/fonts/DejaVuSerif-Bold.ttf create mode 100644 pyWebLayout/assets/fonts/DejaVuSerif-BoldItalic.ttf create mode 100644 pyWebLayout/assets/fonts/DejaVuSerif-Italic.ttf create mode 100644 pyWebLayout/assets/fonts/DejaVuSerif.ttf create mode 100644 pyWebLayout/assets/fonts/README.md create mode 100644 pyWebLayout/concrete/__init__.py create mode 100644 pyWebLayout/concrete/box.py create mode 100644 pyWebLayout/concrete/dynamic_page.py create mode 100644 pyWebLayout/concrete/functional.py create mode 100644 pyWebLayout/concrete/image.py create mode 100644 pyWebLayout/concrete/interaction_handler.py create mode 100644 pyWebLayout/concrete/page.py create mode 100644 pyWebLayout/concrete/table.py create mode 100644 pyWebLayout/concrete/text.py create mode 100644 pyWebLayout/core/__init__.py create mode 100644 pyWebLayout/core/base.py create mode 100644 pyWebLayout/core/cache.py create mode 100644 pyWebLayout/core/callback_registry.py create mode 100644 pyWebLayout/core/highlight.py create mode 100644 pyWebLayout/core/persistence.py create mode 100644 pyWebLayout/core/query.py create mode 100644 pyWebLayout/io/__init__.py create mode 100644 pyWebLayout/io/readers/__init__.py create mode 100644 pyWebLayout/io/readers/epub_reader.py create mode 100644 pyWebLayout/io/readers/html_extraction.py create mode 100644 pyWebLayout/layout/README_EREADER_API.md create mode 100644 pyWebLayout/layout/__init__.py create mode 100644 pyWebLayout/layout/document_layouter.py create mode 100644 pyWebLayout/layout/ereader_layout.py create mode 100644 pyWebLayout/layout/ereader_manager.py create mode 100644 pyWebLayout/layout/page_buffer.py create mode 100644 pyWebLayout/layout/table_optimizer.py create mode 100644 pyWebLayout/style/__init__.py create mode 100644 pyWebLayout/style/abstract_style.py create mode 100644 pyWebLayout/style/alignment.py create mode 100644 pyWebLayout/style/concrete_style.py create mode 100644 pyWebLayout/style/fonts.py create mode 100644 pyWebLayout/style/page_style.py create mode 100644 pyproject.toml create mode 100644 scripts/debug_text_positioning.py create mode 100755 scripts/epub_page_renderer.py create mode 100644 scripts/epub_page_renderer_documentlayouter.py create mode 100644 scripts/run_coverage.py create mode 100644 scripts/run_coverage_gutters.py create mode 100644 scripts/update_coverage_badges.py create mode 100644 scripts/update_coverage_gutters.py create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 tests/__init__.py create mode 100644 tests/abstract/__init__.py create mode 100644 tests/abstract/test_abstract_blocks.py create mode 100644 tests/abstract/test_abstract_document.py create mode 100644 tests/abstract/test_abstract_functional.py create mode 100644 tests/abstract/test_abstract_inline.py create mode 100644 tests/abstract/test_document_mixins.py create mode 100644 tests/abstract/test_linked_elements.py create mode 100644 tests/concrete/__init__.py create mode 100644 tests/concrete/test_alignment_handlers.py create mode 100644 tests/concrete/test_alignment_spacing.py create mode 100644 tests/concrete/test_canvas_lifecycle.py create mode 100644 tests/concrete/test_concrete_box.py create mode 100644 tests/concrete/test_concrete_functional.py create mode 100644 tests/concrete/test_concrete_image.py create mode 100644 tests/concrete/test_concrete_text.py create mode 100644 tests/concrete/test_dynamic_page.py create mode 100644 tests/concrete/test_form_field_geometry.py create mode 100644 tests/concrete/test_functional_text_centering.py create mode 100644 tests/concrete/test_linkedword_hyphenation.py create mode 100644 tests/concrete/test_new_page_implementation.py create mode 100644 tests/concrete/test_page_geometry.py create mode 100644 tests/concrete/test_table_rendering.py create mode 100644 tests/core/__init__.py create mode 100644 tests/core/test_cache.py create mode 100644 tests/core/test_highlight.py create mode 100644 tests/core/test_query_system.py create mode 100644 tests/data/Kimi Räikkönen - Wikipedia.html create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Ambox_important.svg.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Ambox_important.svg_002.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Ambox_important.svg_003.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Commons-logo.svg.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Commons-logo.svg_002.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Commons-logo.svg_003.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Edit-clear.svg.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Edit-clear.svg_002.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Edit-clear.svg_003.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_Argentina.svg.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_Argentina.svg_002.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_Argentina.svg_003.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_Australia_(converted).svg.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_Australia_(converted).svg_002.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_Austria.svg.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_Austria.svg_002.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_Austria.svg_003.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_Barbados.svg.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_Belgium_(civil).svg.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_Belgium_(civil).svg_002.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_Belgium_(civil).svg_003.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_Brazil.svg.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_Brazil.svg_002.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_Brazil.svg_003.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_Canada_(Pantone).svg.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_Canada_(Pantone).svg_002.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_Finland.svg.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_Finland.svg_002.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_Finland.svg_003.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_France.svg.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_France.svg_002.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_Germany.svg.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_Germany.svg_002.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_Germany.svg_003.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_Ireland.svg.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_Ireland.svg_002.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_Italy.svg.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_Italy.svg_002.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_Italy.svg_003.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_Japan.svg.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_Japan.svg_002.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_Mexico.svg.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_Mexico.svg_002.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_Monaco.svg.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_Monaco.svg_002.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_Norway.svg.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_Norway.svg_002.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_Poland.svg.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_Poland.svg_002.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_South_Africa_(1928-1982).svg.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_South_Africa_(1928-1982).svg_002.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_Sweden.svg.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_Sweden.svg_002.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_Sweden.svg_003.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_Switzerland_(Pantone).svg.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_Switzerland_(Pantone).svg_002.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_Venezuela.svg.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_Venezuela.svg_002.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_the_People's_Republic_of_China.svg.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_the_People's_Republic_of_China.svg_002.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_the_United_Kingdom.svg.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_the_United_Kingdom.svg_002.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_the_United_States.svg.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_the_United_States.svg_002.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Flag_of_the_United_States.svg_003.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/OOjs_UI_icon_edit-ltr-progressive.svg.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Symbol_category_class.svg.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/Symbol_category_class.svg_002.webp create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/load.css create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/load.js create mode 100644 tests/data/Kimi Räikkönen - Wikipedia_files/load_002.css create mode 100644 tests/data/test.epub create mode 100644 tests/examples/__init__.py create mode 100644 tests/examples/test_08_pagination_demo.py create mode 100644 tests/examples/test_09_link_navigation_demo.py create mode 100644 tests/examples/test_10_forms_demo.py create mode 100644 tests/io_tests/__init__.py create mode 100644 tests/io_tests/test_epub_reader.py create mode 100644 tests/io_tests/test_html_extraction.py create mode 100644 tests/io_tests/test_html_extraction_functions.py create mode 100644 tests/io_tests/test_html_file_loader.py create mode 100644 tests/io_tests/test_html_link_end_to_end.py create mode 100644 tests/io_tests/test_html_link_interactivity.py create mode 100644 tests/io_tests/test_html_links.py create mode 100644 tests/io_tests/test_inline_content_in_containers.py create mode 100644 tests/layout/__init__.py create mode 100644 tests/layout/test_backward_navigation.py create mode 100644 tests/layout/test_ereader_highlights.py create mode 100644 tests/layout/test_ereader_image_rendering.py create mode 100644 tests/layout/test_ereader_interaction.py create mode 100644 tests/layout/test_ereader_layout.py create mode 100644 tests/layout/test_ereader_manager.py create mode 100644 tests/layout/test_font_scaling.py create mode 100644 tests/layout/test_html_links_in_ereader.py create mode 100644 tests/layout/test_navigation_consistency.py create mode 100644 tests/layout/test_page_buffer.py create mode 100644 tests/layout/test_page_spanning_blocks.py create mode 100644 tests/layout/test_table_optimizer.py create mode 100644 tests/layouter/__init__.py create mode 100644 tests/layouter/test_document_layouter.py create mode 100644 tests/layouter/test_document_layouter_integration.py create mode 100644 tests/mixins/__init__.py create mode 100644 tests/mixins/font_registry_tests.py create mode 100644 tests/mixins/metadata_tests.py create mode 100644 tests/style/__init__.py create mode 100644 tests/style/test_html_style.py create mode 100644 tests/style/test_new_style_system.py create mode 100644 tests/style/test_word_spacing_constraints.py create mode 100644 tests/test_callback_registry.py create mode 100644 tests/test_interactive_image.py create mode 100644 tests/utils/__init__.py create mode 100644 tests/utils/test_font_utilities.py create mode 100644 tests/utils/test_fonts.py diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..ea44045 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,31 @@ +[run] +source = pyWebLayout +branch = True +omit = + */tests/* + */test_* + setup.py + */examples/* + */__main__.py + +[report] +exclude_lines = + pragma: no cover + def __repr__ + if self.debug: + if settings.DEBUG + raise AssertionError + raise NotImplementedError + if 0: + if __name__ == .__main__.: + # Exclude docstrings + ^\s*""" + ^\s*''' + ^\s*r""" + ^\s*r''' + +[xml] +output = coverage.xml + +[html] +directory = htmlcov diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..785d586 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,43 @@ +# Dockerfile.ci copies nothing from the context, but keeping it small makes +# `docker build` fast and avoids shipping local state into the build. + +# Virtual environments +venv/ +.venv/ + +# Python cache and build output +__pycache__/ +*.py[cod] +*$py.class +*.so +build/ +dist/ +*.egg-info/ +*.egg + +# Git +.git/ + +# Test/coverage output +.coverage +coverage.json +coverage.xml +htmlcov/ +cov_info/ +.pytest_cache/ +.tox/ +.mypy_cache/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +.claude/ + +# Generated docs/images +docs/images/ + +# OS files +.DS_Store +Thumbs.db diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..884aa0f --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,177 @@ +name: Python CI + +on: + push: + branches: [ main, master, develop ] + paths-ignore: + - 'coverage*.svg' + - 'README.md' + pull_request: + branches: [ main, master, develop ] + +jobs: + test: + runs-on: linux/amd64 + container: + # Built from Dockerfile.ci at the repo root. Carries Python 3.10-3.13, + # each in its own venv at /opt/py with every dependency + # pre-installed, so a run downloads nothing. + image: gitea.tourolle.paris/dtourolle/pyweblayout-ci:latest + strategy: + matrix: + python-version: ['3.10', '3.11', '3.12', '3.13'] + fail-fast: false + + env: + # pyWebLayout is a library: it is tested on every interpreter + # pyproject.toml's requires-python claims to support. + PYBIN: /opt/py${{ matrix.python-version }}/bin + # Badges and artifacts are published once, not once per matrix leg - + # four jobs racing to force-push the same branch is not a publish + # strategy. This leg is the one that publishes. + PUBLISH: ${{ matrix.python-version == '3.13' }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install project + run: | + # --no-deps: dependencies are baked into the image. If a new one is + # added to pyproject.toml, add it to Dockerfile.ci and rebuild; + # the check below is what catches forgetting to. + $PYBIN/pip install -e . --no-deps + $PYBIN/python -c "import pyWebLayout; print('pyWebLayout', pyWebLayout.__file__)" + + - name: Verify declared dependencies are sufficient + if: env.PUBLISH == 'true' + run: | + # A clean venv with ONLY the declared runtime deps, installed from + # the index rather than from the image. If an import here fails, + # pyproject.toml is incomplete and a real `pip install pyWebLayout` + # fails the same way for a user. This is the one step that is + # allowed to reach the network. + $PYBIN/python -m venv /tmp/clean-install + /tmp/clean-install/bin/pip install --upgrade pip + /tmp/clean-install/bin/pip install . + /tmp/clean-install/bin/python -c " + import pyWebLayout.concrete, pyWebLayout.abstract + import pyWebLayout.io.readers.epub_reader + import pyWebLayout.io.readers.html_extraction + import pyWebLayout.layout.ereader_manager + print('clean install imports OK') + " + + - name: Run tests with pytest + id: pytest + continue-on-error: true + run: | + $PYBIN/python -m pytest tests/ -v \ + --cov=pyWebLayout \ + --cov-report=term-missing \ + --cov-report=json \ + --cov-report=html \ + --cov-report=xml + + - name: Check documentation coverage + id: docs + continue-on-error: true + run: | + $PYBIN/interrogate -v \ + --ignore-init-method --ignore-init-module --ignore-magic \ + --ignore-private --ignore-property-decorators --ignore-semiprivate \ + --fail-under=80 pyWebLayout/ + + - name: Lint with flake8 + run: | + # Stop the build if there are Python syntax errors or undefined names + $PYBIN/flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # Exit-zero treats all errors as warnings + $PYBIN/flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + + - name: Fail the job if tests failed + if: steps.pytest.outcome != 'success' + run: | + # pytest runs with continue-on-error so the badge steps below still + # execute; without this the job would report green on a red suite. + echo "::error::pytest failed on Python ${{ matrix.python-version }}" + exit 1 + + # ------------------------------------------------------------------ + # Badges and artifacts - publishing leg only + # ------------------------------------------------------------------ + + - name: Prepare badge directory + if: always() && env.PUBLISH == 'true' + run: | + mkdir -p cov_info + # Default to failed badges; the steps below overwrite them on success + curl -o cov_info/coverage.svg "https://img.shields.io/badge/coverage-failed-red.svg" + curl -o cov_info/coverage-docs.svg "https://img.shields.io/badge/docs-failed-red.svg" + + - name: Update test coverage badge on success + if: always() && env.PUBLISH == 'true' && steps.pytest.outcome == 'success' + run: | + if [ -f coverage.json ]; then + $PYBIN/coverage-badge -o cov_info/coverage.svg -f + echo "✅ Test coverage badge updated" + else + echo "⚠️ No coverage.json found, keeping failed badge" + fi + + - name: Update docs coverage badge on success + if: always() && env.PUBLISH == 'true' && steps.docs.outcome == 'success' + run: | + rm -f cov_info/coverage-docs.svg + $PYBIN/interrogate --generate-badge cov_info/coverage-docs.svg pyWebLayout/ + echo "✅ Docs coverage badge updated" + + - name: Generate coverage reports + if: env.PUBLISH == 'true' && steps.pytest.outcome == 'success' + run: | + $PYBIN/python -c " + import json, os + if os.path.exists('coverage.json'): + with open('coverage.json') as f: + data = json.load(f) + total = round(data['totals']['percent_covered'], 1) + with open('cov_info/coverage-summary.txt', 'w') as f: + f.write(f'{total}%') + print(f\"Test Coverage: {total}%\") + print(f\"Lines Covered: {data['totals']['covered_lines']}/{data['totals']['num_statements']}\") + else: + print('No coverage data found') + " + if [ -f coverage.json ]; then cp coverage.json cov_info/; fi + if [ -f coverage.xml ]; then cp coverage.xml cov_info/; fi + if [ -d htmlcov ]; then cp -r htmlcov cov_info/; fi + + - name: Final badge status + if: always() && env.PUBLISH == 'true' + run: | + echo "=== FINAL BADGE STATUS ===" + echo "Test outcome: ${{ steps.pytest.outcome }}" + echo "Docs outcome: ${{ steps.docs.outcome }}" + ls -la cov_info/ 2>/dev/null || echo "No cov_info directory" + + - name: Upload coverage artifacts + if: always() && env.PUBLISH == 'true' + uses: actions/upload-artifact@v3 + with: + name: coverage-reports + path: cov_info/ + + - name: Commit badges to badges branch + if: env.PUBLISH == 'true' && github.ref == 'refs/heads/master' + run: | + git config --local user.email "action@gitea.local" + git config --local user.name "Gitea Action" + + git remote set-url origin https://${{ secrets.PUSH_TOKEN }}@gitea.tourolle.paris/dtourolle/pyWebLayout.git + + # Orphan branch holding only the badges, force-pushed each time + git checkout --orphan badges + find . -maxdepth 1 -not -name '.git' -not -name 'cov_info' -exec rm -rf {} + 2>/dev/null || true + git add -f cov_info/ + git commit -m "Update coverage badges [skip ci]" + git push -f origin badges diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..be58037 --- /dev/null +++ b/.gitignore @@ -0,0 +1,56 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class +*/__pycache__ +# Distribution / packaging +dist/ +build/ +*.egg-info/ + +# Environment +venv/ +env/ +.env/ +.venv/ + +# Tests +.pytest_cache/ +.coverage +htmlcov/ + +# IDE files +.idea/ +.vscode/ +*.swp +*.swo + +# Project specific +*.png +*.jpg +*.jpeg +*.gif +*.svg + +# But allow documentation images +!docs/images/*.gif +!docs/images/*.png +!docs/images/*.jpg + +# Output directories +output/ +my_output/ +test_output/ +*_output/ +examples/output/ + +# Generated data +bookmarks/ +positions/ + +# Profiling scripts +profile_*.py + +# Debug scripts output +debug_*.png +.fish* \ No newline at end of file diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..d8f5dba --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,348 @@ +# pyWebLayout Architecture + +This document describes how pyWebLayout is organised: the layers, what lives in each, +and the rules that govern how they may depend on one another. + +## Overview + +The library turns markup (HTML/EPUB) into rendered images. That pipeline is split into +layers with a strict dependency direction: + +``` + io/readers/ parse markup into document structure + ↓ + abstract/ what the document says ─┐ + ↓ ├─ built on core/ + style/ + layout/ decide where things go │ + ↓ │ + concrete/ what the pixels are ─┘ + ↓ + PIL.Image +``` + +The central distinction is **abstract vs concrete**: + +- **Abstract** — the logical content and structure of a document. A `Paragraph` knows + it contains words in an order; it does not know how wide they are. +- **Concrete** — the spatial realisation of that content. A `Line` knows exactly which + glyphs sit at which pixel offsets on a specific canvas. + +One abstract document produces many concrete renderings: different page sizes, font +scales, and font families all re-run the concrete layer over unchanged abstract content. +That is what makes ereader features like live font scaling and reflow possible. + +## Layers + +### `core/` — shared foundations + +Everything else is built on these. `core/` depends on nothing but `style/`. + +**`core/base.py`** defines the contracts that make a class abstract or concrete: + +| Contract | Kind | Meaning | +|---|---|---| +| `Renderable` | ABC | Has `render()`; produces or draws visual output | +| `Queriable` | ABC | Has `in_object(point)`; can be hit-tested | +| `Layoutable` | ABC | Has `layout()`; arranges its own contents | +| `Interactable` | ABC | Holds a callback invoked on interaction | +| `Geometric` | mixin | `origin` and `size` as numpy arrays | +| `Hierarchical` | mixin | `parent` back-reference | +| `Styleable` | mixin | Carries a style object | +| `FontRegistry` | mixin | Deduplicates `Font` instances across a document | +| `MetadataContainer` | mixin | Key/value metadata with typed accessors | +| `BlockContainer` | mixin | Holds child `Block`s | +| `ContainerAware` | mixin | Knows the container it was added to | + +The useful shorthand: **`Renderable` and `Geometric` are the concrete markers.** An +abstract class that acquires either has crossed the line. + +**`core/query.py`** — `QueryResult` and `SelectionRange`: the result types for mapping a +pixel back to content (what was clicked, what text is selected, where it is in the +document). + +**`core/highlight.py`** — `Highlight`, `HighlightColor`, `HighlightManager`. Highlights +store both pixel bounds (for drawing) and semantic bounds (word indices, for surviving +a font change). + +**`core/cache.py`** — `UsageCache` / `SizedUsageCache`, bounded caches keyed by +`(font, string)` for text measurement and glyph rasterisation. Eviction is by usage +count with periodic aging, not LRU; the module docstring explains why at length. This +exists because a single page issues thousands of measurements over fewer than a +thousand distinct pairs, on hardware where an unbounded cache is not affordable. + +**`core/callback_registry.py`** — `CallbackRegistry`, which owns the interactive +elements registered on a page and dispatches to them. + +### `style/` — semantic styling and its resolution + +This package is itself an instance of the abstract/concrete split, applied to styling: + +- **`abstract_style.py`** — `AbstractStyle` (frozen dataclass, hashable) captures + *intent*: `FontFamily.SERIF`, `FontSize.LARGE`, `color="black"`. `AbstractStyleRegistry` + interns them so a document holds one instance per distinct style. +- **`concrete_style.py`** — `RenderingContext` (user preferences, DPI, accessibility + flags, available space) plus `StyleResolver`, which maps `AbstractStyle` + + `RenderingContext` → `ConcreteStyle` (a resolved font path, a pixel size, an RGB + tuple). `ConcreteStyleRegistry` caches the results. +- **`fonts.py`** — `Font`, the loaded PIL font plus its rendering attributes, and the + bundled DejaVu families (`BundledFont`). +- **`page_style.py`** — `PageStyle`: borders, padding, background, default alignment. +- **`alignment.py`** — the `Alignment` enum. + +`AbstractStyle` → `StyleResolver` → `ConcreteStyle` is the mechanism by which the same +document renders differently for different readers. + +### `abstract/` — document content and structure + +Layout-agnostic representations of what the document contains. + +**`abstract/block.py`** — block-level content, all deriving from `Block`: + +`Paragraph`, `Heading` (with `HeadingLevel`), `Quote`, `CodeBlock`, `HList` (with +`ListStyle`) and `ListItem`, `Table` / `TableRow` / `TableCell`, `Image` and +`LinkedImage`, `HorizontalRule`, `PageBreak`. + +**`abstract/inline.py`** — `Word`, `FormattedSpan`, `LinkedWord`, `LineBreak`. `Word` +holds text, a style, and previous/next links into the document's word sequence. It can +report whether it is a hyphenation candidate (`possible_hyphenation(language)`), but it +does not perform the split — that is a measurement decision and belongs to `Line`. + +**`abstract/document.py`** — `Document`, `Chapter`, `Book`, `MetadataType`. `Book` is +the EPUB-shaped `Document` with chapters and a table of contents. + +**`abstract/functional.py`** — `Link`, `Button`, `Form`, `FormField`, `LinkType`, +`FormFieldType`. + +**`abstract/interactive_image.py`** — `InteractiveImage`, an `Image` that is also +`Interactable` and `Queriable`. + +### `concrete/` — spatial realisation + +Objects that know their position and size and can draw themselves onto a canvas. + +**`concrete/text.py`** — the heart of text layout: +- `Text` — one renderable fragment (a whole word, or a hyphenated part). Requires an + `ImageDraw.Draw` at construction so it can measure itself immediately. +- `Line` — a sequence of `Text` objects with resolved spacing and a baseline. + `Line.add_word()` is where a `Word` becomes one or two `Text` objects: it measures, + and if the word overflows it tries dictionary hyphenation, then brute-force splitting, + before rejecting the word. +- `AlignmentHandler` and its subclasses (`LeftAlignmentHandler`, + `CenterRightAlignmentHandler`, `JustifyAlignmentHandler`) — the strategy objects that + turn a set of measured fragments plus an available width into concrete spacing and a + start position. +- Cache management: `configure_text_caches`, `clear_text_caches`, `text_cache_stats`, + `prewarm_text_caches`. + +**`concrete/page.py`** — `Page`: a fixed-size canvas holding `Renderable` children, with +a content rectangle derived from its `PageStyle`, `can_fit_line()` for the layouter to +test against, `render()` returning a `PIL.Image`, and `query_point()` / `query_range()` +for hit-testing. + +**`concrete/box.py`** — `Box`, the `Geometric` + `Renderable` + `Queriable` base for +positioned drawable objects. + +**`concrete/dynamic_page.py`** — `DynamicPage` (a `Page` subclass) and `SizeConstraints`. +Adds a two-phase measure-then-layout protocol, so a container such as a table can learn +its content's intrinsic size before committing to a size allocation. + +**`concrete/image.py`** — `RenderableImage`. + +**`concrete/table.py`** — `TableRenderer`, `TableRowRenderer`, `TableCellRenderer`, +`TableStyle`. Cells host their own nested `Page`, which is why `Page` accepts a +non-zero `origin`. + +**`concrete/functional.py`** — `LinkText`, `ButtonText`, `FormFieldText`: `Text` +subclasses that are also `Interactable`. + +**`concrete/interaction_handler.py`** — `InteractionHandler`, `InteractionStateManager`: +routing taps and presses to registered elements and tracking pressed/released state. + +### `layout/` — the abstract → concrete transformation + +This is the package the pipeline diagram calls the layout engine. + +**`layout/document_layouter.py`** — a set of layouter functions, one per content kind, +all sharing a signature shape of *(abstract element, target `Page`) → did it fit*: + +```python +paragraph_layouter(paragraph, page, start_word=0, pretext=None, alignment_override=None) + -> (complete: bool, failed_word_index: int | None, remaining_pretext: Text | None) + +image_layouter(image, page, max_width=None, max_height=None) -> bool +table_layouter(table, page, style=None) -> bool +pagebreak_layouter(page_break, page) -> bool +button_layouter(button, page, font=None, padding=...) -> (bool, str) +form_field_layouter(field, page, font=None, ...) -> ... +form_layouter(form, page, font=None, field_spacing=10) -> (bool, list[str]) +``` + +They **append to a page**, they do not return a list of lines. The three-part return of +`paragraph_layouter` is what makes pagination resumable: when a paragraph runs off the +bottom of a page, the caller learns which word failed and whether a hyphenated fragment +is pending, and can continue on the next page from exactly there. + +`DocumentLayouter` wraps a `Page` and dispatches over a list of abstract elements by +type, holding the `ConcreteStyleRegistry` for the run. + +**`layout/ereader_layout.py`** — the paginated reading model: +- `RenderingPosition` — a serialisable cursor expressed in *abstract* coordinates + (chapter, block, word, table cell, list item, pending pretext). Because it names + document structure rather than pixels, it survives font-size and page-size changes. +- `BidirectionalLayouter` — renders a page forward or backward from a position, + returning `(Page, next_position)`. +- `ChapterNavigator` / `ChapterInfo` — a table of contents built from heading structure. +- `FontScaler`, `FontFamilyOverride` — apply scale and family changes to blocks at + layout time without mutating the abstract document. + +**`layout/page_buffer.py`** — `PageBuffer` (LRU caches of rendered pages in both +directions, plus position maps) and `BufferedPageRenderer` (background rendering). + +**`layout/ereader_manager.py`** — `EreaderLayoutManager`, the top-level application +interface (page turns, font changes, chapter jumps, progress), and `BookmarkManager` +for persisting bookmarks and the last reading position. + +**`layout/table_optimizer.py`** — column width allocation for tables. + +### `io/readers/` — parsing + +**`html_extraction.py`** — `parse_html_string(html, base_font=None, document=None, +base_path=None) -> List[Block]`. Built from a `StyleContext` (a `NamedTuple` threaded +down the tree, carrying the inherited font and styling) and a table of per-tag handlers +(`paragraph_handler`, `heading_handler`, `table_handler`, …). This is the only place +that knows about HTML. + +**`epub_reader.py`** — `EPUBReader` and `read_epub(path) -> Book`: container/OPF +parsing, spine and manifest, table of contents, cover handling, and image processing +(with an e-ink processor available by default). + +## Dependency rules + +The direction of dependency is what keeps the split honest: + +``` +io/readers → abstract → core, style +layout → abstract, concrete, core, style +concrete → abstract, core, style +abstract → core, style ← must NOT import concrete +``` + +`abstract/` importing from `concrete/` is the violation to watch for. `concrete/` +importing from `abstract/` is expected and correct: a `Text` may point back at the +`Word` it came from, and a table cell renderer reads its abstract `TableCell`. + +**Where the abstract layer touches rendering today, and why:** + +- Abstract classes are constructed with `Font` objects, which carry a pixel size and a + loaded font file. This is a deliberate compromise for parsing performance (HTML + styling resolves to a `Font` once, at parse time) but it does mean the abstract layer + is not fully rendering-independent. `AbstractStyle` is the intended replacement, and + `Word` already accepts either. +- `Word.concrete` / `Word.add_concete()` ([inline.py:42](pyWebLayout/abstract/inline.py#L42), + [:134](pyWebLayout/abstract/inline.py#L134)) is a back-reference to the `Text` objects + a word became. **It is currently written and never read**, and it cannot correctly + model the relationship anyway: one `Word` becomes many `Text`s across re-layouts, and + a single slot only remembers the most recent. Treat it as vestigial. When a + word→pixels mapping is genuinely needed, build it on `Text`'s `source` back-reference + (concrete pointing at abstract), which is the safe direction — note it is currently + stored as `_source` with no public accessor, and is itself unread today. + +## Worked example + +```python +from pyWebLayout.io.readers.html_extraction import parse_html_string +from pyWebLayout.concrete.page import Page +from pyWebLayout.layout.document_layouter import DocumentLayouter + +# 1. Parse: markup -> abstract blocks +blocks = parse_html_string("

Chapter One

It was a dark and stormy night.

") +# [Heading, Paragraph] + +# 2. Lay out: abstract blocks -> concrete children appended to a Page +page = Page(size=(400, 600)) +DocumentLayouter(page).layout_document(blocks) + +# 3. Render: Page -> PIL.Image +image = page.render() + +# 4. Query: pixel -> content +result = page.query_point((50, 40)) +print(result.object_type, result.text) # e.g. "text" "Chapter" +``` + +Inspecting the intermediate concrete objects: + +```python +from pyWebLayout.concrete.text import Line + +for line in (c for c in page.children if isinstance(c, Line)): + print([t.text for t in line.text_objects]) +# ['Chapter', 'One'] +# ['It', 'was', 'a', 'dark', 'and', 'stormy', 'night.'] +``` + +For paginated reading, drive `EreaderLayoutManager` instead of building pages by hand: + +```python +from pyWebLayout.layout.ereader_manager import EreaderLayoutManager + +manager = EreaderLayoutManager(blocks, page_size=(800, 600), document_id="my-book") +page = manager.get_current_page() +page = manager.next_page() +manager.set_font_scale(1.25) # re-lays out from the same RenderingPosition +``` + +## Design principles + +**1. Abstract content is not mutated by layout.** Font scaling and family overrides +produce scaled copies at layout time rather than editing the document. This is what +allows the same `blocks` list to back a buffer of pages at several font sizes. + +**2. Positions are expressed in abstract coordinates.** `RenderingPosition` names a +chapter, block, and word — never a pixel. A reader who changes font size stays on the +same sentence. + +**3. Layouters report partial success.** Every layouter returns whether it fit, and the +text layouter also returns where it stopped. Pagination is built from this rather than +from a separate page-breaking pass. + +**4. One abstract object may become many concrete ones.** One `Word` → one or two `Text` +fragments; one `Paragraph` → many `Line`s across many `Page`s; one `Document` → an +unbounded sequence of `Page`s. Any API that assumes a one-to-one mapping will be wrong +at a hyphen or a page boundary. + +**5. Measurement is cached, not avoided.** Text width and glyph rasterisation are the +hot path. `core/cache.py` bounds their cost; `prewarm_text_caches` front-loads it for a +known document. + +## Anti-patterns + +**Concrete state stored on abstract objects.** An abstract object that remembers its +rendered width, position, or `Text` objects is wrong at the second rendering. If a +back-reference is needed, point from concrete to abstract. + +```python +# WRONG +class Word: + def __init__(self, text): + self.rendered_width = None # invalidated by any font change + +# RIGHT +text = Text(word.text, font, draw, source=word) # concrete knows its origin +``` + +**Treating `Word` as renderable.** Words are abstract; only `Text` draws. There is no +`renderable_words` anywhere in the codebase, and there should not be. + +**Assuming a layouter returns lines.** `paragraph_layouter` appends to a page and +reports what did not fit. Code that expects `List[Line]` back is working from an +outdated model. + +## Summary + +- **`core/`** — contracts and shared machinery +- **`style/`** — semantic style, and its resolution to concrete rendering parameters +- **`abstract/`** — what the document says +- **`concrete/`** — where the pixels go +- **`layout/`** — the transformation between them, and pagination on top of it +- **`io/readers/`** — markup in diff --git a/Dockerfile.ci b/Dockerfile.ci new file mode 100644 index 0000000..ff9cb0f --- /dev/null +++ b/Dockerfile.ci @@ -0,0 +1,80 @@ +# CI test image for pyWebLayout +# Build: docker build -f Dockerfile.ci -t gitea.tourolle.paris/dtourolle/pyweblayout-ci:latest . +# Push: docker push gitea.tourolle.paris/dtourolle/pyweblayout-ci:latest +# +# pyWebLayout is a library, so CI tests every interpreter pyproject.toml claims +# to support rather than just one. All four are in this image and the workflow +# matrix picks one per job; dependencies are pre-installed into each, so a CI +# run downloads nothing. +FROM ubuntu:24.04 + +ENV DEBIAN_FRONTEND=noninteractive + +# deadsnakes carries the Python versions Ubuntu 24.04 does not ship +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + gnupg \ + software-properties-common \ + && add-apt-repository -y ppa:deadsnakes/ppa \ + && apt-get update && apt-get install -y --no-install-recommends \ + # Interpreters. 3.12 is Ubuntu 24.04's own; the rest come from deadsnakes. + python3.10 python3.10-venv \ + python3.11 python3.11-venv \ + python3.12 python3.12-venv \ + python3.13 python3.13-venv \ + # Pillow needs these at runtime for font rasterisation and image IO + libfreetype6 \ + libjpeg-turbo8 \ + libopenjp2-7 \ + libtiff6 \ + zlib1g \ + # Used by the workflow itself + curl \ + git \ + nodejs \ + && rm -rf /var/lib/apt/lists/* + +# One venv per interpreter at a predictable path, /opt/py. Ubuntu marks +# its system Python externally-managed, so installing into venvs sidesteps that +# without --break-system-packages, and keeps the four dependency sets isolated. +# +# The package list is written once so versions cannot drift between +# interpreters. It mirrors pyproject.toml's runtime deps plus the test and dev +# extras; keep the two in step. +# +# Deliberately NOT installed: pyWebLayout itself. The workflow installs the +# checkout with --no-deps, so a job always tests the code under review. +# +# setuptools is pinned below 81 because that release dropped pkg_resources, +# which coverage-badge still imports at startup. Without the pin the badge step +# dies with ModuleNotFoundError. Revisit when coverage-badge stops using it. +RUN for v in 3.10 3.11 3.12 3.13; do \ + python$v -m venv /opt/py$v && \ + /opt/py$v/bin/pip install --no-cache-dir --upgrade pip wheel && \ + /opt/py$v/bin/pip install --no-cache-dir --upgrade "setuptools<81" && \ + /opt/py$v/bin/pip install --no-cache-dir \ + Pillow \ + numpy \ + pyphen \ + beautifulsoup4 \ + lxml \ + pytest \ + pytest-cov \ + flask \ + werkzeug \ + ebooklib \ + requests \ + flake8 \ + coverage-badge \ + interrogate \ + ; \ + done + +# Fail the build rather than ship an image whose dependencies do not import +RUN for v in 3.10 3.11 3.12 3.13; do \ + echo "--- python$v ---" && \ + /opt/py$v/bin/python -c \ + "import sys, PIL, numpy, pyphen, bs4, pytest, flask, ebooklib, requests; \ + print(sys.version.split()[0], 'deps OK')" \ + ; \ + done diff --git a/FONT_SWITCHING_FEATURE.md b/FONT_SWITCHING_FEATURE.md new file mode 100644 index 0000000..0d22cd3 --- /dev/null +++ b/FONT_SWITCHING_FEATURE.md @@ -0,0 +1,139 @@ +# Dynamic Font Family Switching + +The pyWebLayout ereader now supports dynamic font family switching, allowing readers to change fonts on-the-fly without losing their reading position. + +## Visual Demo + +![Font Family Switching](docs/images/font_family_switching_vertical.png) + +*The same content rendered in Sans-Serif, Serif, and Monospace fonts* + +## Features + +- **Three Bundled Font Families**: Sans-Serif (DejaVu Sans), Serif (DejaVu Serif), and Monospace (DejaVu Sans Mono) +- **Dynamic Switching**: Change fonts instantly during reading +- **Position Preservation**: Your reading position is maintained across font changes +- **Attribute Preservation**: Bold, italic, size, and color are preserved when switching families +- **Automatic Cache Management**: Intelligent cache invalidation ensures optimal performance + +## Usage + +```python +from pyWebLayout.style.fonts import BundledFont +from pyWebLayout.layout.ereader_manager import create_ereader_manager + +# Create an ereader instance +manager = create_ereader_manager(blocks, page_size=(600, 800)) + +# Switch to serif font +manager.set_font_family(BundledFont.SERIF) +page = manager.get_current_page() + +# Switch to monospace font +manager.set_font_family(BundledFont.MONOSPACE) +page = manager.get_current_page() + +# Restore original fonts +manager.set_font_family(None) +page = manager.get_current_page() + +# Query current font family +current_family = manager.get_font_family() # Returns BundledFont or None +``` + +## API Reference + +### EreaderLayoutManager Methods + +#### `set_font_family(family: Optional[BundledFont]) -> Page` + +Change the font family and re-render the current page. + +**Parameters:** +- `family`: Font family to use (`BundledFont.SANS`, `BundledFont.SERIF`, `BundledFont.MONOSPACE`, or `None` for original fonts) + +**Returns:** +- Re-rendered page with the new font family + +**Example:** +```python +# Switch to serif +page = manager.set_font_family(BundledFont.SERIF) + +# Restore original fonts +page = manager.set_font_family(None) +``` + +#### `get_font_family() -> Optional[BundledFont]` + +Get the current font family override. + +**Returns:** +- Current font family (`BundledFont.SANS`, `BundledFont.SERIF`, `BundledFont.MONOSPACE`) or `None` if using original fonts + +**Example:** +```python +family = manager.get_font_family() +if family: + print(f"Currently using: {family.value}") +else: + print("Using original fonts") +``` + +## Font Families + +### Sans-Serif (BundledFont.SANS) +- **Font**: DejaVu Sans +- **Best for**: Screen reading, modern interfaces +- **Characteristics**: Clean, legible, no decorative strokes + +### Serif (BundledFont.SERIF) +- **Font**: DejaVu Serif +- **Best for**: Long-form reading, formal documents +- **Characteristics**: Traditional, classic appearance with decorative strokes + +### Monospace (BundledFont.MONOSPACE) +- **Font**: DejaVu Sans Mono +- **Best for**: Code, technical documentation +- **Characteristics**: Fixed-width characters, uniform spacing + +## Examples + +### Complete Demo +See [examples/11_font_family_switching_demo.py](examples/11_font_family_switching_demo.py) for a full demonstration including: +- Creating ereader content +- Switching between font families +- Navigating with different fonts +- Position tracking across font changes + +### Generate README Images +Run [examples/generate_readme_font_demo.py](examples/generate_readme_font_demo.py) to create comparison images: +```bash +python examples/generate_readme_font_demo.py +``` + +## Implementation Details + +The font family switching is implemented using a **hybrid approach** that combines: + +1. **FontFamilyOverride class**: Manages font preferences at render time +2. **Font transformation pipeline**: Intercepts and transforms Font objects during rendering +3. **Intelligent caching**: Automatic cache invalidation when font family changes +4. **Backward compatibility**: Works with existing Font-based content without migration + +This approach provides: +- ✅ No breaking changes to existing code +- ✅ Instant font switching without document recreation +- ✅ Preservation of font attributes (weight, style, size, color) +- ✅ Optimal performance with intelligent buffering + +## Technical Notes + +- Font family changes invalidate the page buffer cache +- Reading position is preserved using the abstract document structure +- Background rendering adapts to the new font family automatically +- All three bundled fonts are included in the package (license: Bitstream Vera / Public Domain) + +## License + +The bundled DejaVu fonts are free and open source under the [Bitstream Vera License](pyWebLayout/assets/fonts/DEJAVU_README.md). diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..536b3ea --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Duncan Tourolle + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..9950b3d --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,11 @@ +include README.md +include LICENSE +include pyWebLayout/*.py +recursive-include pyWebLayout/abstract *.py +recursive-include pyWebLayout/concrete *.py +recursive-include pyWebLayout/style *.py +recursive-include pyWebLayout/core *.py +recursive-include pyWebLayout/typesetting *.py +recursive-include pyWebLayout/io *.py +recursive-include pyWebLayout/examples *.py +recursive-include pyWebLayout/assets *.ttf *.otf *.woff *.woff2 diff --git a/README.md b/README.md new file mode 100644 index 0000000..abe09bd --- /dev/null +++ b/README.md @@ -0,0 +1,267 @@ +# PyWebLayout + +## Project Status + + +| Badge | Description | +|-------|-------------| +| ![Test Coverage](https://gitea.tourolle.paris/dtourolle/pyWebLayout/raw/branch/badges/cov_info/coverage.svg) | **Test Coverage** - Percentage of code covered by unit tests | +| ![Documentation Coverage](https://gitea.tourolle.paris/dtourolle/pyWebLayout/raw/branch/badges/cov_info/coverage-docs.svg) | **Documentation Coverage** - Percentage of code with docstrings | +| ![License](https://img.shields.io/badge/license-MIT-blue.svg) | **License** - Project licensing information | +A Python library for HTML-like layout and rendering. +> 📋 **Note**: Badges show results from the commit referenced in the URLs. Red "error" badges indicate build failures for that specific step. +## Description + +PyWebLayout is a Python library for HTML-like layout and rendering to paginated images. It provides a flexible page rendering system with support for borders, padding, text layout, and HTML parsing. + +## Key Features + +### Page Rendering System +- 📄 **Flexible Page Layouts** - Create pages with customizable sizes, borders, and padding +- 🎨 **Styling System** - Control backgrounds, border colors, and spacing +- 📐 **Multiple Layouts** - Support for portrait, landscape, and square pages +- 🖼️ **Image Output** - Render pages to PIL Images (PNG, JPEG, etc.) + +### Text and HTML Support +- 📝 **HTML Parsing** - Parse HTML content into structured document blocks +- 🔤 **Font Support** - Multiple font sizes, weights, and styles +- 🎨 **Dynamic Font Families** - Switch between Sans, Serif, and Monospace fonts on-the-fly +- ↔️ **Text Alignment** - Left, center, right, and justified text +- 📖 **Rich Content** - Headings, paragraphs, bold, italic, and more +- 📊 **Table Rendering** - Full HTML table support with headers, borders, and styling +- 🔘 **Interactive Elements** - Buttons, forms, and links with callback support + +### Architecture +- **Abstract/Concrete Separation** - Clean separation between content structure and rendering +- **Extensible Design** - Easy to extend with custom renderables +- **Type-safe** - Comprehensive type hints throughout the codebase + +## Installation + +```bash +pip install pyWebLayout +``` + +## Quick Start + +### Basic Page Rendering + +```python +from pyWebLayout.concrete.page import Page +from pyWebLayout.style.page_style import PageStyle + +# Create a styled page +page_style = PageStyle( + border_width=2, + border_color=(200, 200, 200), + padding=(30, 30, 30, 30), # top, right, bottom, left + background_color=(255, 255, 255) +) + +page = Page(size=(600, 800), style=page_style) + +# Render to image +image = page.render() +image.save("my_page.png") +``` + +### HTML Content Parsing + +```python +from pyWebLayout.io.readers.html_extraction import parse_html_string +from pyWebLayout.style import Font + +# Parse HTML to structured blocks +html = """ +

Document Title

+

First paragraph with bold text.

+

Second paragraph with more content.

+""" + +base_font = Font(font_size=14) +blocks = parse_html_string(html, base_font=base_font) + +# blocks is a list of structured content (Paragraph, Heading, etc.) +``` + +## Visual Examples + +The library supports various page layouts and configurations: + + + + + + + + + + + + + + + + + + + + + + + +
+ Page Styles
+ Page Rendering
+ Different borders, padding, and backgrounds +
+ HTML Content
+ Text Layout
+ Parsed HTML with various text styles +
+ Page Layouts
+ Page Layouts
+ Portrait, landscape, and square formats +
+ Table Rendering
+ Table Rendering
+ HTML tables with headers and styling +
+ Interactive Elements
+ Interactive Elements
+ Buttons, forms, and callback binding +
+ 🆕 Pagination & PageBreak
+ Pagination
+ Multi-page documents with explicit and automatic breaks +
+ 🆕 Link Navigation
+ Links
+ All 4 link types: Internal, External, API, Function +
+ 🆕 Comprehensive Forms
+ Forms
+ All 14 form field types with validation +
+ 🆕 Dynamic Font Family Switching
+ Font Switching
+ Switch between Sans, Serif, and Monospace fonts instantly +
+ +## Examples + +The `examples/` directory contains working demonstrations: + +### Getting Started +- **[01_simple_page_rendering.py](examples/01_simple_page_rendering.py)** - Introduction to the Page system +- **[02_text_and_layout.py](examples/02_text_and_layout.py)** - HTML parsing and text rendering +- **[03_page_layouts.py](examples/03_page_layouts.py)** - Different page configurations +- **[04_table_rendering.py](examples/04_table_rendering.py)** - HTML table rendering with styling +- **[05_html_table_with_images.py](examples/05_html_table_with_images.py)** - Tables with embedded images +- **[06_functional_elements_demo.py](examples/06_functional_elements_demo.py)** - Interactive buttons and forms with callbacks +- **[08_bundled_fonts_demo.py](examples/08_bundled_fonts_demo.py)** - Using the bundled DejaVu font families + +### 🆕 Advanced Features (NEW) +- **[08_pagination_demo.py](examples/08_pagination_demo.py)** - Multi-page documents with PageBreak ([11 tests](tests/examples/test_08_pagination_demo.py)) +- **[09_link_navigation_demo.py](examples/09_link_navigation_demo.py)** - All link types and navigation ([10 tests](tests/examples/test_09_link_navigation_demo.py)) +- **[10_forms_demo.py](examples/10_forms_demo.py)** - All 14 form field types ([9 tests](tests/examples/test_10_forms_demo.py)) +- **[11_font_family_switching_demo.py](examples/11_font_family_switching_demo.py)** - 🆕 Dynamic font switching in ereader + +Run any example: +```bash +cd examples +python 01_simple_page_rendering.py +python 08_pagination_demo.py # NEW: Multi-page documents +``` + +**All new examples include comprehensive test coverage!** Run tests with: +```bash +python -m pytest tests/examples/ -v # 30 tests, all passing ✅ +``` + +**Coverage Impact:** The new examples fill critical documentation gaps: +- **PageBreak:** 0% → 100% (had NO examples before) +- **LinkText:** 14% → 100% (all 4 link types demonstrated) +- **FormFields:** 14% → 100% (all 14 field types demonstrated) + +See **[examples/README.md](examples/README.md)** for detailed documentation. + +## Font Family Switching (NEW ✨) + +PyWebLayout now supports dynamic font family switching in the ereader, allowing readers to change fonts on-the-fly without losing their reading position! + +### Quick Example + +```python +from pyWebLayout.style.fonts import BundledFont +from pyWebLayout.layout.ereader_manager import create_ereader_manager + +# Create an ereader +manager = create_ereader_manager(blocks, page_size=(600, 800)) + +# Switch to serif font +manager.set_font_family(BundledFont.SERIF) + +# Switch to monospace font +manager.set_font_family(BundledFont.MONOSPACE) + +# Restore original fonts +manager.set_font_family(None) + +# Query current font +current = manager.get_font_family() +``` + +### Features + +- **3 Bundled Fonts**: Sans, Serif, and Monospace (DejaVu font family) +- **Instant Switching**: Change fonts without recreating the document +- **Position Preservation**: Reading position maintained across font changes +- **Attribute Preservation**: Bold, italic, size, and color are preserved +- **Smart Caching**: Automatic cache invalidation for optimal performance + +**Learn more**: See [FONT_SWITCHING_FEATURE.md](FONT_SWITCHING_FEATURE.md) for complete documentation. + +## Documentation + +- **[ARCHITECTURE.md](ARCHITECTURE.md)** - Abstract/Concrete architecture guide +- **[FONT_SWITCHING_FEATURE.md](FONT_SWITCHING_FEATURE.md)** - 🆕 Font family switching guide +- **[examples/README.md](examples/README.md)** - Complete examples guide with tests +- **[docs/images/README.md](docs/images/README.md)** - Visual documentation index +- **[pyWebLayout/layout/README_EREADER_API.md](pyWebLayout/layout/README_EREADER_API.md)** - EbookReader API reference +- **API Reference** - See docstrings in source code + +## Continuous integration + +CI runs in a prebuilt container image rather than installing dependencies per +job. The image carries Python 3.10, 3.11, 3.12 and 3.13, each in its own venv at +`/opt/py` with every dependency installed, so a run downloads nothing +and the test matrix covers the whole range `pyproject.toml` claims to support. + +Rebuild and push the image whenever `Dockerfile.ci` changes — most often +because a dependency was added to `pyproject.toml`: + +```bash +docker build -f Dockerfile.ci -t gitea.tourolle.paris/dtourolle/pyweblayout-ci:latest . +docker push gitea.tourolle.paris/dtourolle/pyweblayout-ci:latest +``` + +To reproduce a CI job locally: + +```bash +docker run --rm -v "$PWD:/src:ro" gitea.tourolle.paris/dtourolle/pyweblayout-ci:latest bash -c ' + mkdir -p /work && cp -a /src/. /work/ && cd /work && rm -rf venv .git + /opt/py3.13/bin/pip install -e . --no-deps -q + /opt/py3.13/bin/python -m pytest tests/ -q' +``` + +The workflow is [.gitea/workflows/ci.yml](.gitea/workflows/ci.yml). Badges and +coverage artifacts are published from the 3.13 leg only. + +## License + +MIT License + +## Author + +Duncan Tourolle - duncan@tourolle.paris diff --git a/cov_info/coverage-docs.svg b/cov_info/coverage-docs.svg new file mode 100644 index 0000000..e4c12eb --- /dev/null +++ b/cov_info/coverage-docs.svg @@ -0,0 +1,58 @@ + + interrogate: 93.2% + + + + + + + + + + + interrogate + interrogate + 93.2% + 93.2% + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cov_info/coverage-summary.txt b/cov_info/coverage-summary.txt new file mode 100644 index 0000000..e9ded06 --- /dev/null +++ b/cov_info/coverage-summary.txt @@ -0,0 +1 @@ +81.5% \ No newline at end of file diff --git a/cov_info/coverage.json b/cov_info/coverage.json new file mode 100644 index 0000000..4903177 --- /dev/null +++ b/cov_info/coverage.json @@ -0,0 +1 @@ +{"meta": {"format": 3, "version": "7.15.4", "timestamp": "2026-08-08T20:34:43.914782", "branch_coverage": true, "show_contexts": false}, "files": {"pyWebLayout/__init__.py": {"executed_lines": [11], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1], "executed_branches": [], "missing_branches": [], "functions": {"": {"executed_lines": [11], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [11], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1], "start_line": 1, "executed_branches": [], "missing_branches": []}}}, "pyWebLayout/abstract/__init__.py": {"executed_lines": [8, 9, 10, 11, 13], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1], "executed_branches": [], "missing_branches": [], "functions": {"": {"executed_lines": [8, 9, 10, 11, 13], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [8, 9, 10, 11, 13], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1], "start_line": 1, "executed_branches": [], "missing_branches": []}}}, "pyWebLayout/abstract/block.py": {"executed_lines": [1, 2, 3, 4, 5, 6, 7, 8, 9, 12, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 29, 37, 44, 45, 47, 48, 50, 53, 61, 68, 69, 70, 72, 73, 100, 107, 109, 126, 135, 148, 149, 151, 153, 160, 161, 163, 173, 174, 176, 178, 179, 184, 186, 187, 188, 189, 190, 191, 194, 200, 208, 209, 210, 212, 213, 245, 246, 248, 250, 251, 253, 256, 261, 268, 269, 271, 272, 299, 300, 302, 304, 305, 310, 315, 322, 323, 324, 326, 327, 354, 355, 357, 359, 360, 362, 364, 371, 373, 380, 381, 383, 384, 386, 389, 391, 392, 393, 396, 401, 409, 410, 411, 412, 414, 415, 447, 448, 450, 452, 453, 455, 457, 458, 460, 462, 463, 467, 474, 475, 477, 490, 497, 498, 500, 501, 503, 506, 511, 519, 520, 521, 523, 524, 556, 557, 559, 561, 562, 564, 566, 567, 569, 571, 572, 577, 582, 597, 598, 599, 600, 601, 603, 604, 635, 636, 638, 640, 641, 645, 646, 648, 650, 651, 655, 656, 658, 660, 661, 665, 666, 668, 670, 671, 676, 681, 688, 689, 690, 692, 693, 725, 726, 728, 730, 731, 735, 742, 743, 745, 765, 772, 773, 775, 776, 778, 781, 786, 794, 795, 796, 797, 798, 799, 801, 802, 834, 835, 837, 839, 840, 842, 844, 845, 847, 849, 850, 854, 862, 864, 865, 866, 867, 869, 871, 884, 891, 892, 894, 901, 902, 904, 911, 912, 914, 921, 922, 923, 924, 925, 926, 928, 929, 931, 939, 944, 959, 960, 961, 962, 963, 965, 966, 1002, 1003, 1005, 1007, 1008, 1010, 1012, 1013, 1015, 1017, 1018, 1020, 1022, 1023, 1025, 1027, 1028, 1030, 1032, 1033, 1035, 1037, 1038, 1040, 1042, 1049, 1051, 1058, 1059, 1060, 1062, 1076, 1077, 1079, 1082, 1083, 1084, 1086, 1090, 1092, 1102, 1103, 1105, 1119, 1121, 1123, 1125, 1126, 1128, 1129, 1131, 1132, 1135, 1136, 1139, 1141, 1154, 1155, 1157, 1158, 1160, 1161, 1163, 1164, 1167, 1170, 1172, 1175, 1176, 1179, 1181, 1183, 1188, 1190, 1198, 1200, 1201, 1204, 1205, 1207, 1218, 1219, 1220, 1223, 1224, 1227, 1229, 1238, 1239, 1242, 1243, 1244, 1248, 1251, 1256, 1277, 1281, 1282, 1283, 1284, 1285, 1286, 1288, 1289, 1291, 1293, 1294, 1296, 1298, 1299, 1303, 1304, 1308, 1309, 1313, 1323, 1326, 1330, 1333, 1334, 1337, 1340, 1345, 1347, 1349, 1350, 1377, 1386, 1388, 1390, 1391], "summary": {"covered_lines": 396, "num_statements": 489, "percent_covered": 80.36036036036036, "percent_covered_display": "80", "missing_lines": 93, "excluded_lines": 119, "percent_statements_covered": 80.98159509202453, "percent_statements_covered_display": "81", "num_branches": 66, "num_partial_branches": 6, "covered_branches": 50, "missing_branches": 16, "percent_branches_covered": 75.75757575757575, "percent_branches_covered_display": "76"}, "missing_lines": [89, 90, 93, 96, 98, 124, 133, 146, 170, 171, 234, 235, 238, 241, 243, 288, 289, 292, 295, 297, 307, 342, 345, 346, 348, 352, 436, 437, 440, 443, 445, 465, 488, 545, 546, 549, 552, 554, 574, 624, 625, 628, 631, 633, 643, 653, 663, 673, 714, 715, 718, 721, 723, 733, 763, 823, 824, 827, 830, 832, 852, 882, 990, 993, 994, 996, 1000, 1087, 1088, 1133, 1134, 1137, 1138, 1184, 1185, 1186, 1187, 1245, 1246, 1301, 1306, 1311, 1331, 1364, 1367, 1368, 1370, 1374, 1405, 1408, 1409, 1411, 1415], "excluded_lines": [13, 30, 38, 49, 54, 62, 74, 101, 110, 127, 136, 150, 154, 164, 175, 185, 195, 201, 218, 247, 252, 257, 262, 273, 301, 306, 311, 316, 328, 356, 361, 365, 374, 385, 390, 397, 402, 420, 449, 454, 459, 464, 468, 478, 491, 502, 507, 512, 529, 558, 563, 568, 573, 578, 588, 606, 637, 642, 647, 652, 657, 662, 667, 672, 677, 682, 698, 727, 732, 736, 751, 766, 777, 782, 787, 807, 836, 841, 846, 851, 855, 872, 885, 895, 905, 915, 930, 940, 950, 973, 1004, 1009, 1014, 1019, 1024, 1029, 1034, 1039, 1043, 1052, 1066, 1093, 1106, 1144, 1191, 1252, 1262, 1290, 1295, 1300, 1305, 1310, 1314, 1341, 1346, 1351, 1378, 1387, 1392], "executed_branches": [[160, -153], [160, 161], [380, -373], [380, 381], [497, -490], [497, 498], [772, -765], [772, 773], [864, 865], [864, 866], [866, 867], [866, 869], [891, -884], [891, 892], [901, -894], [901, 902], [911, -904], [911, 912], [921, 922], [921, 923], [923, 924], [923, 925], [925, -914], [925, 926], [1058, 1059], [1058, 1060], [1076, 1077], [1076, 1079], [1082, 1083], [1082, 1086], [1086, 1090], [1154, 1155], [1154, 1157], [1161, 1163], [1161, 1167], [1175, 1176], [1175, 1179], [1183, 1188], [1200, 1201], [1200, 1204], [1205, 1207], [1218, 1219], [1223, 1224], [1223, 1229], [1238, 1239], [1242, 1243], [1242, 1248], [1330, 1333], [1333, 1334], [1333, 1337]], "missing_branches": [[170, -163], [170, 171], [345, 346], [345, 348], [993, 994], [993, 996], [1086, 1087], [1183, 1184], [1205, 1229], [1218, 1223], [1238, 1242], [1330, 1331], [1367, 1368], [1367, 1370], [1408, 1409], [1408, 1411]], "functions": {"Block.__init__": {"executed_lines": [44, 45], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [38], "start_line": 37, "executed_branches": [], "missing_branches": []}, "Block.block_type": {"executed_lines": [50], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [49], "start_line": 48, "executed_branches": [], "missing_branches": []}, "Paragraph.__init__": {"executed_lines": [68, 69, 70], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [62], "start_line": 61, "executed_branches": [], "missing_branches": []}, "Paragraph.create_and_add_to": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 5, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 5, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [89, 90, 93, 96, 98], "excluded_lines": [74], "start_line": 73, "executed_branches": [], "missing_branches": []}, "Paragraph.add_word": {"executed_lines": [107], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [101], "start_line": 100, "executed_branches": [], "missing_branches": []}, "Paragraph.create_word": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [124], "excluded_lines": [110], "start_line": 109, "executed_branches": [], "missing_branches": []}, "Paragraph.add_span": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [133], "excluded_lines": [127], "start_line": 126, "executed_branches": [], "missing_branches": []}, "Paragraph.create_span": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [146], "excluded_lines": [136], "start_line": 135, "executed_branches": [], "missing_branches": []}, "Paragraph.words": {"executed_lines": [151], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [150], "start_line": 149, "executed_branches": [], "missing_branches": []}, "Paragraph.words_iter": {"executed_lines": [160, 161], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [154], "start_line": 153, "executed_branches": [[160, -153], [160, 161]], "missing_branches": []}, "Paragraph.spans": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 2, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 2, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 2, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [170, 171], "excluded_lines": [164], "start_line": 163, "executed_branches": [], "missing_branches": [[170, -163], [170, 171]]}, "Paragraph.word_count": {"executed_lines": [176], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [175], "start_line": 174, "executed_branches": [], "missing_branches": []}, "Paragraph.__len__": {"executed_lines": [179], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 178, "executed_branches": [], "missing_branches": []}, "Heading.__init__": {"executed_lines": [208, 209, 210], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [201], "start_line": 200, "executed_branches": [], "missing_branches": []}, "Heading.create_and_add_to": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 5, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 5, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [234, 235, 238, 241, 243], "excluded_lines": [218], "start_line": 213, "executed_branches": [], "missing_branches": []}, "Heading.level": {"executed_lines": [253], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [252], "start_line": 251, "executed_branches": [], "missing_branches": []}, "Quote.__init__": {"executed_lines": [268, 269], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [262], "start_line": 261, "executed_branches": [], "missing_branches": []}, "Quote.create_and_add_to": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 5, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 5, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [288, 289, 292, 295, 297], "excluded_lines": [273], "start_line": 272, "executed_branches": [], "missing_branches": []}, "Quote.style": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [307], "excluded_lines": [306], "start_line": 305, "executed_branches": [], "missing_branches": []}, "CodeBlock.__init__": {"executed_lines": [322, 323, 324], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [316], "start_line": 315, "executed_branches": [], "missing_branches": []}, "CodeBlock.create_and_add_to": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 5, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 5, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 2, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [342, 345, 346, 348, 352], "excluded_lines": [328], "start_line": 327, "executed_branches": [], "missing_branches": [[345, 346], [345, 348]]}, "CodeBlock.language": {"executed_lines": [362], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [361], "start_line": 360, "executed_branches": [], "missing_branches": []}, "CodeBlock.add_line": {"executed_lines": [371], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [365], "start_line": 364, "executed_branches": [], "missing_branches": []}, "CodeBlock.lines": {"executed_lines": [380, 381], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [374], "start_line": 373, "executed_branches": [[380, -373], [380, 381]], "missing_branches": []}, "CodeBlock.line_count": {"executed_lines": [386], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [385], "start_line": 384, "executed_branches": [], "missing_branches": []}, "HList.__init__": {"executed_lines": [409, 410, 411, 412], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [402], "start_line": 401, "executed_branches": [], "missing_branches": []}, "HList.create_and_add_to": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 5, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 5, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [436, 437, 440, 443, 445], "excluded_lines": [420], "start_line": 415, "executed_branches": [], "missing_branches": []}, "HList.style": {"executed_lines": [455], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [454], "start_line": 453, "executed_branches": [], "missing_branches": []}, "HList.default_style": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [465], "excluded_lines": [464], "start_line": 463, "executed_branches": [], "missing_branches": []}, "HList.add_item": {"executed_lines": [474, 475], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [468], "start_line": 467, "executed_branches": [], "missing_branches": []}, "HList.create_item": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [488], "excluded_lines": [478], "start_line": 477, "executed_branches": [], "missing_branches": []}, "HList.items": {"executed_lines": [497, 498], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [491], "start_line": 490, "executed_branches": [[497, -490], [497, 498]], "missing_branches": []}, "HList.item_count": {"executed_lines": [503], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [502], "start_line": 501, "executed_branches": [], "missing_branches": []}, "ListItem.__init__": {"executed_lines": [519, 520, 521], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [512], "start_line": 511, "executed_branches": [], "missing_branches": []}, "ListItem.create_and_add_to": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 5, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 5, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [545, 546, 549, 552, 554], "excluded_lines": [529], "start_line": 524, "executed_branches": [], "missing_branches": []}, "ListItem.term": {"executed_lines": [564], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [563], "start_line": 562, "executed_branches": [], "missing_branches": []}, "ListItem.style": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [574], "excluded_lines": [573], "start_line": 572, "executed_branches": [], "missing_branches": []}, "TableCell.__init__": {"executed_lines": [597, 598, 599, 600, 601], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [588], "start_line": 582, "executed_branches": [], "missing_branches": []}, "TableCell.create_and_add_to": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 5, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 5, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [624, 625, 628, 631, 633], "excluded_lines": [606], "start_line": 604, "executed_branches": [], "missing_branches": []}, "TableCell.is_header": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [643], "excluded_lines": [642], "start_line": 641, "executed_branches": [], "missing_branches": []}, "TableCell.colspan": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [653], "excluded_lines": [652], "start_line": 651, "executed_branches": [], "missing_branches": []}, "TableCell.rowspan": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [663], "excluded_lines": [662], "start_line": 661, "executed_branches": [], "missing_branches": []}, "TableCell.style": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [673], "excluded_lines": [672], "start_line": 671, "executed_branches": [], "missing_branches": []}, "TableRow.__init__": {"executed_lines": [688, 689, 690], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [682], "start_line": 681, "executed_branches": [], "missing_branches": []}, "TableRow.create_and_add_to": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 5, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 5, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [714, 715, 718, 721, 723], "excluded_lines": [698], "start_line": 693, "executed_branches": [], "missing_branches": []}, "TableRow.style": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [733], "excluded_lines": [732], "start_line": 731, "executed_branches": [], "missing_branches": []}, "TableRow.add_cell": {"executed_lines": [742, 743], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [736], "start_line": 735, "executed_branches": [], "missing_branches": []}, "TableRow.create_cell": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [763], "excluded_lines": [751], "start_line": 745, "executed_branches": [], "missing_branches": []}, "TableRow.cells": {"executed_lines": [772, 773], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [766], "start_line": 765, "executed_branches": [[772, -765], [772, 773]], "missing_branches": []}, "TableRow.cell_count": {"executed_lines": [778], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [777], "start_line": 776, "executed_branches": [], "missing_branches": []}, "Table.__init__": {"executed_lines": [794, 795, 796, 797, 798, 799], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [787], "start_line": 786, "executed_branches": [], "missing_branches": []}, "Table.create_and_add_to": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 5, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 5, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [823, 824, 827, 830, 832], "excluded_lines": [807], "start_line": 802, "executed_branches": [], "missing_branches": []}, "Table.caption": {"executed_lines": [842], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [841], "start_line": 840, "executed_branches": [], "missing_branches": []}, "Table.style": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [852], "excluded_lines": [851], "start_line": 850, "executed_branches": [], "missing_branches": []}, "Table.add_row": {"executed_lines": [862, 864, 865, 866, 867, 869], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [855], "start_line": 854, "executed_branches": [[864, 865], [864, 866], [866, 867], [866, 869]], "missing_branches": []}, "Table.create_row": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [882], "excluded_lines": [872], "start_line": 871, "executed_branches": [], "missing_branches": []}, "Table.header_rows": {"executed_lines": [891, 892], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [885], "start_line": 884, "executed_branches": [[891, -884], [891, 892]], "missing_branches": []}, "Table.body_rows": {"executed_lines": [901, 902], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [895], "start_line": 894, "executed_branches": [[901, -894], [901, 902]], "missing_branches": []}, "Table.footer_rows": {"executed_lines": [911, 912], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [905], "start_line": 904, "executed_branches": [[911, -904], [911, 912]], "missing_branches": []}, "Table.all_rows": {"executed_lines": [921, 922, 923, 924, 925, 926], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [915], "start_line": 914, "executed_branches": [[921, 922], [921, 923], [923, 924], [923, 925], [925, -914], [925, 926]], "missing_branches": []}, "Table.row_count": {"executed_lines": [931], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [930], "start_line": 929, "executed_branches": [], "missing_branches": []}, "Image.__init__": {"executed_lines": [959, 960, 961, 962, 963], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [950], "start_line": 944, "executed_branches": [], "missing_branches": []}, "Image.create_and_add_to": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 5, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 5, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 2, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [990, 993, 994, 996, 1000], "excluded_lines": [973], "start_line": 966, "executed_branches": [], "missing_branches": [[993, 994], [993, 996]]}, "Image.source": {"executed_lines": [1010], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1009], "start_line": 1008, "executed_branches": [], "missing_branches": []}, "Image.alt_text": {"executed_lines": [1020], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1019], "start_line": 1018, "executed_branches": [], "missing_branches": []}, "Image.width": {"executed_lines": [1030], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1029], "start_line": 1028, "executed_branches": [], "missing_branches": []}, "Image.height": {"executed_lines": [1040], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1039], "start_line": 1038, "executed_branches": [], "missing_branches": []}, "Image.get_dimensions": {"executed_lines": [1049], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1043], "start_line": 1042, "executed_branches": [], "missing_branches": []}, "Image.get_aspect_ratio": {"executed_lines": [1058, 1059, 1060], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1052], "start_line": 1051, "executed_branches": [[1058, 1059], [1058, 1060]], "missing_branches": []}, "Image.calculate_scaled_dimensions": {"executed_lines": [1076, 1077, 1079, 1082, 1083, 1084, 1086, 1090], "summary": {"covered_lines": 8, "num_statements": 10, "percent_covered": 81.25, "percent_covered_display": "81", "missing_lines": 2, "excluded_lines": 1, "percent_statements_covered": 80.0, "percent_statements_covered_display": "80", "num_branches": 6, "num_partial_branches": 1, "covered_branches": 5, "missing_branches": 1, "percent_branches_covered": 83.33333333333333, "percent_branches_covered_display": "83"}, "missing_lines": [1087, 1088], "excluded_lines": [1066], "start_line": 1062, "executed_branches": [[1076, 1077], [1076, 1079], [1082, 1083], [1082, 1086], [1086, 1090]], "missing_branches": [[1086, 1087]]}, "Image._is_url": {"executed_lines": [1102, 1103], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1093], "start_line": 1092, "executed_branches": [], "missing_branches": []}, "Image._download_to_temp": {"executed_lines": [1119, 1121, 1123, 1125, 1126, 1128, 1129, 1131, 1132, 1135, 1136, 1139], "summary": {"covered_lines": 12, "num_statements": 16, "percent_covered": 75.0, "percent_covered_display": "75", "missing_lines": 4, "excluded_lines": 1, "percent_statements_covered": 75.0, "percent_statements_covered_display": "75", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [1133, 1134, 1137, 1138], "excluded_lines": [1106], "start_line": 1105, "executed_branches": [], "missing_branches": []}, "Image.load_image_data": {"executed_lines": [1154, 1155, 1157, 1158, 1160, 1161, 1163, 1164, 1167, 1170, 1172, 1175, 1176, 1179, 1181, 1183, 1188], "summary": {"covered_lines": 17, "num_statements": 21, "percent_covered": 82.75862068965517, "percent_covered_display": "83", "missing_lines": 4, "excluded_lines": 1, "percent_statements_covered": 80.95238095238095, "percent_statements_covered_display": "81", "num_branches": 8, "num_partial_branches": 1, "covered_branches": 7, "missing_branches": 1, "percent_branches_covered": 87.5, "percent_branches_covered_display": "88"}, "missing_lines": [1184, 1185, 1186, 1187], "excluded_lines": [1144], "start_line": 1141, "executed_branches": [[1154, 1155], [1154, 1157], [1161, 1163], [1161, 1167], [1175, 1176], [1175, 1179], [1183, 1188]], "missing_branches": [[1183, 1184]]}, "Image.get_image_info": {"executed_lines": [1198, 1200, 1201, 1204, 1205, 1207, 1218, 1219, 1220, 1223, 1224, 1227, 1229, 1238, 1239, 1242, 1243, 1244, 1248], "summary": {"covered_lines": 19, "num_statements": 21, "percent_covered": 84.84848484848484, "percent_covered_display": "85", "missing_lines": 2, "excluded_lines": 1, "percent_statements_covered": 90.47619047619048, "percent_statements_covered_display": "90", "num_branches": 12, "num_partial_branches": 3, "covered_branches": 9, "missing_branches": 3, "percent_branches_covered": 75.0, "percent_branches_covered_display": "75"}, "missing_lines": [1245, 1246], "excluded_lines": [1191], "start_line": 1190, "executed_branches": [[1200, 1201], [1200, 1204], [1205, 1207], [1218, 1219], [1223, 1224], [1223, 1229], [1238, 1239], [1242, 1243], [1242, 1248]], "missing_branches": [[1205, 1229], [1218, 1223], [1238, 1242]]}, "LinkedImage.__init__": {"executed_lines": [1277, 1281, 1282, 1283, 1284, 1285, 1286], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1262], "start_line": 1256, "executed_branches": [], "missing_branches": []}, "LinkedImage.location": {"executed_lines": [1291], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1290], "start_line": 1289, "executed_branches": [], "missing_branches": []}, "LinkedImage.link_type": {"executed_lines": [1296], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1295], "start_line": 1294, "executed_branches": [], "missing_branches": []}, "LinkedImage.link_callback": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [1301], "excluded_lines": [1300], "start_line": 1299, "executed_branches": [], "missing_branches": []}, "LinkedImage.params": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [1306], "excluded_lines": [1305], "start_line": 1304, "executed_branches": [], "missing_branches": []}, "LinkedImage.link_title": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [1311], "excluded_lines": [1310], "start_line": 1309, "executed_branches": [], "missing_branches": []}, "LinkedImage.execute_link": {"executed_lines": [1323, 1326, 1330, 1333, 1334, 1337], "summary": {"covered_lines": 6, "num_statements": 7, "percent_covered": 81.81818181818181, "percent_covered_display": "82", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 85.71428571428571, "percent_statements_covered_display": "86", "num_branches": 4, "num_partial_branches": 1, "covered_branches": 3, "missing_branches": 1, "percent_branches_covered": 75.0, "percent_branches_covered_display": "75"}, "missing_lines": [1331], "excluded_lines": [1314], "start_line": 1313, "executed_branches": [[1330, 1333], [1333, 1334], [1333, 1337]], "missing_branches": [[1330, 1331]]}, "HorizontalRule.__init__": {"executed_lines": [1347], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1346], "start_line": 1345, "executed_branches": [], "missing_branches": []}, "HorizontalRule.create_and_add_to": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 5, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 5, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 2, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [1364, 1367, 1368, 1370, 1374], "excluded_lines": [1351], "start_line": 1350, "executed_branches": [], "missing_branches": [[1367, 1368], [1367, 1370]]}, "PageBreak.__init__": {"executed_lines": [1388], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1387], "start_line": 1386, "executed_branches": [], "missing_branches": []}, "PageBreak.create_and_add_to": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 5, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 5, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 2, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [1405, 1408, 1409, 1411, 1415], "excluded_lines": [1392], "start_line": 1391, "executed_branches": [], "missing_branches": [[1408, 1409], [1408, 1411]]}, "": {"executed_lines": [1, 2, 3, 4, 5, 6, 7, 8, 9, 12, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 29, 37, 47, 48, 53, 61, 72, 73, 100, 109, 126, 135, 148, 149, 153, 163, 173, 174, 178, 184, 186, 187, 188, 189, 190, 191, 194, 200, 212, 213, 245, 246, 250, 251, 256, 261, 271, 272, 299, 300, 304, 305, 310, 315, 326, 327, 354, 355, 359, 360, 364, 373, 383, 384, 389, 391, 392, 393, 396, 401, 414, 415, 447, 448, 452, 453, 457, 458, 462, 463, 467, 477, 490, 500, 501, 506, 511, 523, 524, 556, 557, 561, 562, 566, 567, 571, 572, 577, 582, 603, 604, 635, 636, 640, 641, 645, 646, 650, 651, 655, 656, 660, 661, 665, 666, 670, 671, 676, 681, 692, 693, 725, 726, 730, 731, 735, 745, 765, 775, 776, 781, 786, 801, 802, 834, 835, 839, 840, 844, 845, 849, 850, 854, 871, 884, 894, 904, 914, 928, 929, 939, 944, 965, 966, 1002, 1003, 1007, 1008, 1012, 1013, 1017, 1018, 1022, 1023, 1027, 1028, 1032, 1033, 1037, 1038, 1042, 1051, 1062, 1092, 1105, 1141, 1190, 1251, 1256, 1288, 1289, 1293, 1294, 1298, 1299, 1303, 1304, 1308, 1309, 1313, 1340, 1345, 1349, 1350, 1377, 1386, 1390, 1391], "summary": {"covered_lines": 211, "num_statements": 211, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 17, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [13, 30, 54, 185, 195, 257, 311, 390, 397, 507, 578, 677, 782, 940, 1252, 1341, 1378], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"BlockType": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 12, "executed_branches": [], "missing_branches": []}, "Block": {"executed_lines": [44, 45, 50], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 2, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [38, 49], "start_line": 29, "executed_branches": [], "missing_branches": []}, "Paragraph": {"executed_lines": [68, 69, 70, 107, 151, 160, 161, 176, 179], "summary": {"covered_lines": 9, "num_statements": 19, "percent_covered": 47.82608695652174, "percent_covered_display": "48", "missing_lines": 10, "excluded_lines": 10, "percent_statements_covered": 47.36842105263158, "percent_statements_covered_display": "47", "num_branches": 4, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 2, "percent_branches_covered": 50.0, "percent_branches_covered_display": "50"}, "missing_lines": [89, 90, 93, 96, 98, 124, 133, 146, 170, 171], "excluded_lines": [62, 74, 101, 110, 127, 136, 150, 154, 164, 175], "start_line": 53, "executed_branches": [[160, -153], [160, 161]], "missing_branches": [[170, -163], [170, 171]]}, "HeadingLevel": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 184, "executed_branches": [], "missing_branches": []}, "Heading": {"executed_lines": [208, 209, 210, 248, 253], "summary": {"covered_lines": 5, "num_statements": 10, "percent_covered": 50.0, "percent_covered_display": "50", "missing_lines": 5, "excluded_lines": 4, "percent_statements_covered": 50.0, "percent_statements_covered_display": "50", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [234, 235, 238, 241, 243], "excluded_lines": [201, 218, 247, 252], "start_line": 194, "executed_branches": [], "missing_branches": []}, "Quote": {"executed_lines": [268, 269, 302], "summary": {"covered_lines": 3, "num_statements": 9, "percent_covered": 33.333333333333336, "percent_covered_display": "33", "missing_lines": 6, "excluded_lines": 4, "percent_statements_covered": 33.333333333333336, "percent_statements_covered_display": "33", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [288, 289, 292, 295, 297, 307], "excluded_lines": [262, 273, 301, 306], "start_line": 256, "executed_branches": [], "missing_branches": []}, "CodeBlock": {"executed_lines": [322, 323, 324, 357, 362, 371, 380, 381, 386], "summary": {"covered_lines": 9, "num_statements": 14, "percent_covered": 61.111111111111114, "percent_covered_display": "61", "missing_lines": 5, "excluded_lines": 7, "percent_statements_covered": 64.28571428571429, "percent_statements_covered_display": "64", "num_branches": 4, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 2, "percent_branches_covered": 50.0, "percent_branches_covered_display": "50"}, "missing_lines": [342, 345, 346, 348, 352], "excluded_lines": [316, 328, 356, 361, 365, 374, 385], "start_line": 310, "executed_branches": [[380, -373], [380, 381]], "missing_branches": [[345, 346], [345, 348]]}, "ListStyle": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 389, "executed_branches": [], "missing_branches": []}, "HList": {"executed_lines": [409, 410, 411, 412, 450, 455, 460, 474, 475, 497, 498, 503], "summary": {"covered_lines": 12, "num_statements": 19, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 7, "excluded_lines": 10, "percent_statements_covered": 63.1578947368421, "percent_statements_covered_display": "63", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [436, 437, 440, 443, 445, 465, 488], "excluded_lines": [402, 420, 449, 454, 459, 464, 468, 478, 491, 502], "start_line": 396, "executed_branches": [[497, -490], [497, 498]], "missing_branches": []}, "ListItem": {"executed_lines": [519, 520, 521, 559, 564, 569], "summary": {"covered_lines": 6, "num_statements": 12, "percent_covered": 50.0, "percent_covered_display": "50", "missing_lines": 6, "excluded_lines": 6, "percent_statements_covered": 50.0, "percent_statements_covered_display": "50", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [545, 546, 549, 552, 554, 574], "excluded_lines": [512, 529, 558, 563, 568, 573], "start_line": 506, "executed_branches": [], "missing_branches": []}, "TableCell": {"executed_lines": [597, 598, 599, 600, 601, 638, 648, 658, 668], "summary": {"covered_lines": 9, "num_statements": 18, "percent_covered": 50.0, "percent_covered_display": "50", "missing_lines": 9, "excluded_lines": 10, "percent_statements_covered": 50.0, "percent_statements_covered_display": "50", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [624, 625, 628, 631, 633, 643, 653, 663, 673], "excluded_lines": [588, 606, 637, 642, 647, 652, 657, 662, 667, 672], "start_line": 577, "executed_branches": [], "missing_branches": []}, "TableRow": {"executed_lines": [688, 689, 690, 728, 742, 743, 772, 773, 778], "summary": {"covered_lines": 9, "num_statements": 16, "percent_covered": 61.111111111111114, "percent_covered_display": "61", "missing_lines": 7, "excluded_lines": 8, "percent_statements_covered": 56.25, "percent_statements_covered_display": "56", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [714, 715, 718, 721, 723, 733, 763], "excluded_lines": [682, 698, 727, 732, 736, 751, 766, 777], "start_line": 676, "executed_branches": [[772, -765], [772, 773]], "missing_branches": []}, "Table": {"executed_lines": [794, 795, 796, 797, 798, 799, 837, 842, 847, 862, 864, 865, 866, 867, 869, 891, 892, 901, 902, 911, 912, 921, 922, 923, 924, 925, 926, 931], "summary": {"covered_lines": 28, "num_statements": 35, "percent_covered": 86.27450980392157, "percent_covered_display": "86", "missing_lines": 7, "excluded_lines": 13, "percent_statements_covered": 80.0, "percent_statements_covered_display": "80", "num_branches": 16, "num_partial_branches": 0, "covered_branches": 16, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [823, 824, 827, 830, 832, 852, 882], "excluded_lines": [787, 807, 836, 841, 846, 851, 855, 872, 885, 895, 905, 915, 930], "start_line": 781, "executed_branches": [[864, 865], [864, 866], [866, 867], [866, 869], [891, -884], [891, 892], [901, -894], [901, 902], [911, -904], [911, 912], [921, 922], [921, 923], [923, 924], [923, 925], [925, -914], [925, 926]], "missing_branches": []}, "Image": {"executed_lines": [959, 960, 961, 962, 963, 1005, 1010, 1015, 1020, 1025, 1030, 1035, 1040, 1049, 1058, 1059, 1060, 1076, 1077, 1079, 1082, 1083, 1084, 1086, 1090, 1102, 1103, 1119, 1121, 1123, 1125, 1126, 1128, 1129, 1131, 1132, 1135, 1136, 1139, 1154, 1155, 1157, 1158, 1160, 1161, 1163, 1164, 1167, 1170, 1172, 1175, 1176, 1179, 1181, 1183, 1188, 1198, 1200, 1201, 1204, 1205, 1207, 1218, 1219, 1220, 1223, 1224, 1227, 1229, 1238, 1239, 1242, 1243, 1244, 1248], "summary": {"covered_lines": 75, "num_statements": 92, "percent_covered": 80.32786885245902, "percent_covered_display": "80", "missing_lines": 17, "excluded_lines": 17, "percent_statements_covered": 81.52173913043478, "percent_statements_covered_display": "82", "num_branches": 30, "num_partial_branches": 5, "covered_branches": 23, "missing_branches": 7, "percent_branches_covered": 76.66666666666667, "percent_branches_covered_display": "77"}, "missing_lines": [990, 993, 994, 996, 1000, 1087, 1088, 1133, 1134, 1137, 1138, 1184, 1185, 1186, 1187, 1245, 1246], "excluded_lines": [950, 973, 1004, 1009, 1014, 1019, 1024, 1029, 1034, 1039, 1043, 1052, 1066, 1093, 1106, 1144, 1191], "start_line": 939, "executed_branches": [[1058, 1059], [1058, 1060], [1076, 1077], [1076, 1079], [1082, 1083], [1082, 1086], [1086, 1090], [1154, 1155], [1154, 1157], [1161, 1163], [1161, 1167], [1175, 1176], [1175, 1179], [1183, 1188], [1200, 1201], [1200, 1204], [1205, 1207], [1218, 1219], [1223, 1224], [1223, 1229], [1238, 1239], [1242, 1243], [1242, 1248]], "missing_branches": [[993, 994], [993, 996], [1086, 1087], [1183, 1184], [1205, 1229], [1218, 1223], [1238, 1242]]}, "LinkedImage": {"executed_lines": [1277, 1281, 1282, 1283, 1284, 1285, 1286, 1291, 1296, 1323, 1326, 1330, 1333, 1334, 1337], "summary": {"covered_lines": 15, "num_statements": 19, "percent_covered": 78.26086956521739, "percent_covered_display": "78", "missing_lines": 4, "excluded_lines": 7, "percent_statements_covered": 78.94736842105263, "percent_statements_covered_display": "79", "num_branches": 4, "num_partial_branches": 1, "covered_branches": 3, "missing_branches": 1, "percent_branches_covered": 75.0, "percent_branches_covered_display": "75"}, "missing_lines": [1301, 1306, 1311, 1331], "excluded_lines": [1262, 1290, 1295, 1300, 1305, 1310, 1314], "start_line": 1251, "executed_branches": [[1330, 1333], [1333, 1334], [1333, 1337]], "missing_branches": [[1330, 1331]]}, "HorizontalRule": {"executed_lines": [1347], "summary": {"covered_lines": 1, "num_statements": 6, "percent_covered": 12.5, "percent_covered_display": "12", "missing_lines": 5, "excluded_lines": 2, "percent_statements_covered": 16.666666666666668, "percent_statements_covered_display": "17", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 2, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [1364, 1367, 1368, 1370, 1374], "excluded_lines": [1346, 1351], "start_line": 1340, "executed_branches": [], "missing_branches": [[1367, 1368], [1367, 1370]]}, "PageBreak": {"executed_lines": [1388], "summary": {"covered_lines": 1, "num_statements": 6, "percent_covered": 12.5, "percent_covered_display": "12", "missing_lines": 5, "excluded_lines": 2, "percent_statements_covered": 16.666666666666668, "percent_statements_covered_display": "17", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 2, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [1405, 1408, 1409, 1411, 1415], "excluded_lines": [1387, 1392], "start_line": 1377, "executed_branches": [], "missing_branches": [[1408, 1409], [1408, 1411]]}, "": {"executed_lines": [1, 2, 3, 4, 5, 6, 7, 8, 9, 12, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 29, 37, 47, 48, 53, 61, 72, 73, 100, 109, 126, 135, 148, 149, 153, 163, 173, 174, 178, 184, 186, 187, 188, 189, 190, 191, 194, 200, 212, 213, 245, 246, 250, 251, 256, 261, 271, 272, 299, 300, 304, 305, 310, 315, 326, 327, 354, 355, 359, 360, 364, 373, 383, 384, 389, 391, 392, 393, 396, 401, 414, 415, 447, 448, 452, 453, 457, 458, 462, 463, 467, 477, 490, 500, 501, 506, 511, 523, 524, 556, 557, 561, 562, 566, 567, 571, 572, 577, 582, 603, 604, 635, 636, 640, 641, 645, 646, 650, 651, 655, 656, 660, 661, 665, 666, 670, 671, 676, 681, 692, 693, 725, 726, 730, 731, 735, 745, 765, 775, 776, 781, 786, 801, 802, 834, 835, 839, 840, 844, 845, 849, 850, 854, 871, 884, 894, 904, 914, 928, 929, 939, 944, 965, 966, 1002, 1003, 1007, 1008, 1012, 1013, 1017, 1018, 1022, 1023, 1027, 1028, 1032, 1033, 1037, 1038, 1042, 1051, 1062, 1092, 1105, 1141, 1190, 1251, 1256, 1288, 1289, 1293, 1294, 1298, 1299, 1303, 1304, 1308, 1309, 1313, 1340, 1345, 1349, 1350, 1377, 1386, 1390, 1391], "summary": {"covered_lines": 211, "num_statements": 211, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 17, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [13, 30, 54, 185, 195, 257, 311, 390, 397, 507, 578, 677, 782, 940, 1252, 1341, 1378], "start_line": 1, "executed_branches": [], "missing_branches": []}}}, "pyWebLayout/abstract/document.py": {"executed_lines": [1, 2, 3, 4, 5, 6, 7, 8, 11, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 26, 35, 48, 49, 50, 51, 52, 53, 56, 57, 58, 59, 62, 64, 75, 78, 79, 80, 82, 83, 85, 87, 88, 92, 93, 97, 104, 106, 122, 142, 164, 172, 174, 184, 186, 194, 196, 206, 208, 215, 217, 224, 226, 233, 235, 242, 244, 245, 252, 254, 255, 264, 274, 276, 277, 278, 279, 282, 284, 287, 288, 290, 297, 298, 300, 307, 309, 310, 312, 313, 314, 315, 318, 319, 321, 323, 350, 362, 364, 374, 376, 385, 387, 389, 396, 405, 420, 421, 422, 423, 424, 425, 427, 428, 430, 432, 433, 435, 437, 438, 440, 442, 443, 445, 447, 448, 452, 453, 457, 464, 466, 482, 506, 512, 523, 524, 526, 527, 529, 530, 532, 534, 541, 543, 559, 560, 561, 562, 563, 565, 572, 574, 581, 583, 590, 591, 592, 593, 595], "summary": {"covered_lines": 159, "num_statements": 194, "percent_covered": 77.82608695652173, "percent_covered_display": "78", "missing_lines": 35, "excluded_lines": 47, "percent_statements_covered": 81.95876288659794, "percent_statements_covered_display": "82", "num_branches": 36, "num_partial_branches": 4, "covered_branches": 20, "missing_branches": 16, "percent_branches_covered": 55.55555555555556, "percent_branches_covered_display": "56"}, "missing_lines": [65, 67, 73, 90, 95, 116, 117, 118, 119, 120, 136, 137, 138, 139, 140, 158, 159, 160, 262, 283, 285, 383, 391, 450, 455, 476, 477, 478, 479, 480, 496, 497, 498, 499, 500], "excluded_lines": [12, 27, 40, 84, 89, 94, 98, 107, 126, 147, 165, 175, 187, 197, 209, 218, 227, 236, 246, 256, 265, 291, 301, 333, 365, 377, 386, 390, 397, 411, 429, 434, 439, 444, 449, 454, 458, 467, 486, 507, 514, 531, 535, 548, 566, 575, 584], "executed_branches": [[62, 64], [78, 79], [78, 80], [277, -276], [277, 278], [278, 279], [278, 282], [282, 284], [284, 277], [310, 312], [310, 321], [313, 314], [313, 315], [526, -512], [526, 527], [559, 560], [591, 592], [591, 595], [592, 591], [592, 593]], "missing_branches": [[62, 65], [65, 67], [65, 75], [116, 117], [116, 118], [136, 137], [136, 138], [158, 159], [158, 160], [282, 283], [284, 285], [476, 477], [476, 478], [496, 497], [496, 498], [559, 561]], "functions": {"Document.__init__": {"executed_lines": [48, 49, 50, 51, 52, 53, 56, 57, 58, 59, 62, 64, 75, 78, 79, 80], "summary": {"covered_lines": 16, "num_statements": 19, "percent_covered": 76.0, "percent_covered_display": "76", "missing_lines": 3, "excluded_lines": 1, "percent_statements_covered": 84.21052631578948, "percent_statements_covered_display": "84", "num_branches": 6, "num_partial_branches": 1, "covered_branches": 3, "missing_branches": 3, "percent_branches_covered": 50.0, "percent_branches_covered_display": "50"}, "missing_lines": [65, 67, 73], "excluded_lines": [40], "start_line": 35, "executed_branches": [[62, 64], [78, 79], [78, 80]], "missing_branches": [[62, 65], [65, 67], [65, 75]]}, "Document.blocks": {"executed_lines": [85], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [84], "start_line": 83, "executed_branches": [], "missing_branches": []}, "Document.default_style": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [95], "excluded_lines": [94], "start_line": 93, "executed_branches": [], "missing_branches": []}, "Document.add_block": {"executed_lines": [104], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [98], "start_line": 97, "executed_branches": [], "missing_branches": []}, "Document.create_paragraph": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 5, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 5, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 2, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [116, 117, 118, 119, 120], "excluded_lines": [107], "start_line": 106, "executed_branches": [], "missing_branches": [[116, 117], [116, 118]]}, "Document.create_heading": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 5, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 5, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 2, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [136, 137, 138, 139, 140], "excluded_lines": [126], "start_line": 122, "executed_branches": [], "missing_branches": [[136, 137], [136, 138]]}, "Document.create_chapter": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 3, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 2, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [158, 159, 160], "excluded_lines": [147], "start_line": 142, "executed_branches": [], "missing_branches": [[158, 159], [158, 160]]}, "Document.add_anchor": {"executed_lines": [172], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [165], "start_line": 164, "executed_branches": [], "missing_branches": []}, "Document.get_anchor": {"executed_lines": [184], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [175], "start_line": 174, "executed_branches": [], "missing_branches": []}, "Document.add_resource": {"executed_lines": [194], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [187], "start_line": 186, "executed_branches": [], "missing_branches": []}, "Document.get_resource": {"executed_lines": [206], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [197], "start_line": 196, "executed_branches": [], "missing_branches": []}, "Document.add_stylesheet": {"executed_lines": [215], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [209], "start_line": 208, "executed_branches": [], "missing_branches": []}, "Document.add_script": {"executed_lines": [224], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [218], "start_line": 217, "executed_branches": [], "missing_branches": []}, "Document.get_title": {"executed_lines": [233], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [227], "start_line": 226, "executed_branches": [], "missing_branches": []}, "Document.set_title": {"executed_lines": [242], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [236], "start_line": 235, "executed_branches": [], "missing_branches": []}, "Document.title": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [262], "excluded_lines": [256], "start_line": 255, "executed_branches": [], "missing_branches": []}, "Document.find_blocks_by_type": {"executed_lines": [274, 276, 287, 288], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [265], "start_line": 264, "executed_branches": [], "missing_branches": []}, "Document.find_blocks_by_type._find_recursive": {"executed_lines": [277, 278, 279, 282, 284], "summary": {"covered_lines": 5, "num_statements": 7, "percent_covered": 73.33333333333333, "percent_covered_display": "73", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 71.42857142857143, "percent_statements_covered_display": "71", "num_branches": 8, "num_partial_branches": 2, "covered_branches": 6, "missing_branches": 2, "percent_branches_covered": 75.0, "percent_branches_covered_display": "75"}, "missing_lines": [283, 285], "excluded_lines": [], "start_line": 276, "executed_branches": [[277, -276], [277, 278], [278, 279], [278, 282], [282, 284], [284, 277]], "missing_branches": [[282, 283], [284, 285]]}, "Document.find_headings": {"executed_lines": [297, 298], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [291], "start_line": 290, "executed_branches": [], "missing_branches": []}, "Document.generate_table_of_contents": {"executed_lines": [307, 309, 310, 312, 313, 314, 315, 318, 319, 321], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [301], "start_line": 300, "executed_branches": [[310, 312], [310, 321], [313, 314], [313, 315]], "missing_branches": []}, "Document.get_or_create_style": {"executed_lines": [350, 362], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [333], "start_line": 323, "executed_branches": [], "missing_branches": []}, "Document.get_font_for_style": {"executed_lines": [374], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [365], "start_line": 364, "executed_branches": [], "missing_branches": []}, "Document.update_rendering_context": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [383], "excluded_lines": [377], "start_line": 376, "executed_branches": [], "missing_branches": []}, "Document.get_style_registry": {"executed_lines": [387], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [386], "start_line": 385, "executed_branches": [], "missing_branches": []}, "Document.get_concrete_style_registry": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [391], "excluded_lines": [390], "start_line": 389, "executed_branches": [], "missing_branches": []}, "Chapter.__init__": {"executed_lines": [420, 421, 422, 423, 424, 425], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [411], "start_line": 405, "executed_branches": [], "missing_branches": []}, "Chapter.title": {"executed_lines": [435], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [434], "start_line": 433, "executed_branches": [], "missing_branches": []}, "Chapter.level": {"executed_lines": [440], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [439], "start_line": 438, "executed_branches": [], "missing_branches": []}, "Chapter.blocks": {"executed_lines": [445], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [444], "start_line": 443, "executed_branches": [], "missing_branches": []}, "Chapter.style": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [455], "excluded_lines": [454], "start_line": 453, "executed_branches": [], "missing_branches": []}, "Chapter.add_block": {"executed_lines": [464], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [458], "start_line": 457, "executed_branches": [], "missing_branches": []}, "Chapter.create_paragraph": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 5, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 5, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 2, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [476, 477, 478, 479, 480], "excluded_lines": [467], "start_line": 466, "executed_branches": [], "missing_branches": [[476, 477], [476, 478]]}, "Chapter.create_heading": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 5, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 5, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 2, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [496, 497, 498, 499, 500], "excluded_lines": [486], "start_line": 482, "executed_branches": [], "missing_branches": [[496, 497], [496, 498]]}, "Book.__init__": {"executed_lines": [523, 524, 526, 527], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [514], "start_line": 512, "executed_branches": [[526, -512], [526, 527]], "missing_branches": []}, "Book.chapters": {"executed_lines": [532], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [531], "start_line": 530, "executed_branches": [], "missing_branches": []}, "Book.add_chapter": {"executed_lines": [541], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [535], "start_line": 534, "executed_branches": [], "missing_branches": []}, "Book.create_chapter": {"executed_lines": [559, 560, 561, 562, 563], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 85.71428571428571, "percent_covered_display": "86", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 1, "covered_branches": 1, "missing_branches": 1, "percent_branches_covered": 50.0, "percent_branches_covered_display": "50"}, "missing_lines": [], "excluded_lines": [548], "start_line": 543, "executed_branches": [[559, 560]], "missing_branches": [[559, 561]]}, "Book.get_author": {"executed_lines": [572], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [566], "start_line": 565, "executed_branches": [], "missing_branches": []}, "Book.set_author": {"executed_lines": [581], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [575], "start_line": 574, "executed_branches": [], "missing_branches": []}, "Book.generate_table_of_contents": {"executed_lines": [590, 591, 592, 593, 595], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [584], "start_line": 583, "executed_branches": [[591, 592], [591, 595], [592, 591], [592, 593]], "missing_branches": []}, "": {"executed_lines": [1, 2, 3, 4, 5, 6, 7, 8, 11, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 26, 35, 82, 83, 87, 88, 92, 93, 97, 106, 122, 142, 164, 174, 186, 196, 208, 217, 226, 235, 244, 245, 254, 255, 264, 290, 300, 323, 364, 376, 385, 389, 396, 405, 427, 428, 432, 433, 437, 438, 442, 443, 447, 448, 452, 453, 457, 466, 482, 506, 512, 529, 530, 534, 543, 565, 574, 583], "summary": {"covered_lines": 78, "num_statements": 78, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 4, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [12, 27, 397, 507], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"MetadataType": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 11, "executed_branches": [], "missing_branches": []}, "Document": {"executed_lines": [48, 49, 50, 51, 52, 53, 56, 57, 58, 59, 62, 64, 75, 78, 79, 80, 85, 104, 172, 184, 194, 206, 215, 224, 233, 242, 252, 274, 276, 277, 278, 279, 282, 284, 287, 288, 297, 298, 307, 309, 310, 312, 313, 314, 315, 318, 319, 321, 350, 362, 374, 387], "summary": {"covered_lines": 52, "num_statements": 75, "percent_covered": 65.65656565656566, "percent_covered_display": "66", "missing_lines": 23, "excluded_lines": 26, "percent_statements_covered": 69.33333333333333, "percent_statements_covered_display": "69", "num_branches": 24, "num_partial_branches": 3, "covered_branches": 13, "missing_branches": 11, "percent_branches_covered": 54.166666666666664, "percent_branches_covered_display": "54"}, "missing_lines": [65, 67, 73, 90, 95, 116, 117, 118, 119, 120, 136, 137, 138, 139, 140, 158, 159, 160, 262, 283, 285, 383, 391], "excluded_lines": [40, 84, 89, 94, 98, 107, 126, 147, 165, 175, 187, 197, 209, 218, 227, 236, 246, 256, 265, 291, 301, 333, 365, 377, 386, 390], "start_line": 26, "executed_branches": [[62, 64], [78, 79], [78, 80], [277, -276], [277, 278], [278, 279], [278, 282], [282, 284], [284, 277], [310, 312], [310, 321], [313, 314], [313, 315]], "missing_branches": [[62, 65], [65, 67], [65, 75], [116, 117], [116, 118], [136, 137], [136, 138], [158, 159], [158, 160], [282, 283], [284, 285]]}, "Chapter": {"executed_lines": [420, 421, 422, 423, 424, 425, 430, 435, 440, 445, 464], "summary": {"covered_lines": 11, "num_statements": 23, "percent_covered": 40.74074074074074, "percent_covered_display": "41", "missing_lines": 12, "excluded_lines": 10, "percent_statements_covered": 47.82608695652174, "percent_statements_covered_display": "48", "num_branches": 4, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 4, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [450, 455, 476, 477, 478, 479, 480, 496, 497, 498, 499, 500], "excluded_lines": [411, 429, 434, 439, 444, 449, 454, 458, 467, 486], "start_line": 396, "executed_branches": [], "missing_branches": [[476, 477], [476, 478], [496, 497], [496, 498]]}, "Book": {"executed_lines": [523, 524, 526, 527, 532, 541, 559, 560, 561, 562, 563, 572, 581, 590, 591, 592, 593, 595], "summary": {"covered_lines": 18, "num_statements": 18, "percent_covered": 96.15384615384616, "percent_covered_display": "96", "missing_lines": 0, "excluded_lines": 7, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 8, "num_partial_branches": 1, "covered_branches": 7, "missing_branches": 1, "percent_branches_covered": 87.5, "percent_branches_covered_display": "88"}, "missing_lines": [], "excluded_lines": [514, 531, 535, 548, 566, 575, 584], "start_line": 506, "executed_branches": [[526, -512], [526, 527], [559, 560], [591, 592], [591, 595], [592, 591], [592, 593]], "missing_branches": [[559, 561]]}, "": {"executed_lines": [1, 2, 3, 4, 5, 6, 7, 8, 11, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 26, 35, 82, 83, 87, 88, 92, 93, 97, 106, 122, 142, 164, 174, 186, 196, 208, 217, 226, 235, 244, 245, 254, 255, 264, 290, 300, 323, 364, 376, 385, 389, 396, 405, 427, 428, 432, 433, 437, 438, 442, 443, 447, 448, 452, 453, 457, 466, 482, 506, 512, 529, 530, 534, 543, 565, 574, 583], "summary": {"covered_lines": 78, "num_statements": 78, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 4, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [12, 27, 397, 507], "start_line": 1, "executed_branches": [], "missing_branches": []}}}, "pyWebLayout/abstract/functional.py": {"executed_lines": [1, 2, 3, 4, 7, 9, 10, 11, 12, 15, 22, 40, 41, 42, 43, 44, 45, 47, 48, 50, 52, 53, 55, 57, 58, 60, 62, 63, 65, 67, 68, 72, 85, 86, 90, 93, 99, 115, 116, 117, 118, 119, 121, 122, 124, 126, 127, 129, 131, 132, 134, 136, 137, 139, 141, 142, 144, 146, 147, 151, 161, 162, 163, 166, 172, 186, 187, 188, 189, 190, 192, 193, 195, 197, 198, 200, 202, 203, 207, 214, 215, 217, 227, 229, 236, 238, 245, 247, 248, 250, 253, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 271, 276, 294, 295, 296, 297, 298, 299, 300, 302, 303, 305, 307, 308, 310, 312, 313, 315, 317, 318, 320, 322, 323, 325, 327, 328, 330, 332, 333, 335, 337, 338, 340, 342, 343, 345], "summary": {"covered_lines": 141, "num_statements": 144, "percent_covered": 98.0, "percent_covered_display": "98", "missing_lines": 3, "excluded_lines": 39, "percent_statements_covered": 97.91666666666667, "percent_statements_covered_display": "98", "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [70, 149, 205], "excluded_lines": [8, 16, 29, 49, 54, 59, 64, 69, 73, 94, 105, 123, 128, 133, 138, 143, 148, 152, 167, 177, 194, 199, 204, 208, 218, 230, 239, 254, 272, 283, 304, 309, 314, 319, 324, 329, 334, 339, 344], "executed_branches": [[85, 86], [85, 90], [161, 162], [161, 163], [247, 248], [247, 250]], "missing_branches": [], "functions": {"Link.__init__": {"executed_lines": [40, 41, 42, 43, 44, 45], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [29], "start_line": 22, "executed_branches": [], "missing_branches": []}, "Link.location": {"executed_lines": [50], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [49], "start_line": 48, "executed_branches": [], "missing_branches": []}, "Link.link_type": {"executed_lines": [55], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [54], "start_line": 53, "executed_branches": [], "missing_branches": []}, "Link.params": {"executed_lines": [60], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [59], "start_line": 58, "executed_branches": [], "missing_branches": []}, "Link.title": {"executed_lines": [65], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [64], "start_line": 63, "executed_branches": [], "missing_branches": []}, "Link.html_id": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [70], "excluded_lines": [69], "start_line": 68, "executed_branches": [], "missing_branches": []}, "Link.execute": {"executed_lines": [85, 86, 90], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [73], "start_line": 72, "executed_branches": [[85, 86], [85, 90]], "missing_branches": []}, "Button.__init__": {"executed_lines": [115, 116, 117, 118, 119], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [105], "start_line": 99, "executed_branches": [], "missing_branches": []}, "Button.label": {"executed_lines": [129], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [128], "start_line": 127, "executed_branches": [], "missing_branches": []}, "Button.enabled": {"executed_lines": [139], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [138], "start_line": 137, "executed_branches": [], "missing_branches": []}, "Button.params": {"executed_lines": [144], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [143], "start_line": 142, "executed_branches": [], "missing_branches": []}, "Button.html_id": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [149], "excluded_lines": [148], "start_line": 147, "executed_branches": [], "missing_branches": []}, "Button.execute": {"executed_lines": [161, 162, 163], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [152], "start_line": 151, "executed_branches": [[161, 162], [161, 163]], "missing_branches": []}, "Form.__init__": {"executed_lines": [186, 187, 188, 189, 190], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [177], "start_line": 172, "executed_branches": [], "missing_branches": []}, "Form.form_id": {"executed_lines": [195], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [194], "start_line": 193, "executed_branches": [], "missing_branches": []}, "Form.action": {"executed_lines": [200], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [199], "start_line": 198, "executed_branches": [], "missing_branches": []}, "Form.html_id": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [205], "excluded_lines": [204], "start_line": 203, "executed_branches": [], "missing_branches": []}, "Form.add_field": {"executed_lines": [214, 215], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [208], "start_line": 207, "executed_branches": [], "missing_branches": []}, "Form.get_field": {"executed_lines": [227], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [218], "start_line": 217, "executed_branches": [], "missing_branches": []}, "Form.get_values": {"executed_lines": [236], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [230], "start_line": 229, "executed_branches": [], "missing_branches": []}, "Form.execute": {"executed_lines": [245, 247, 248, 250], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [239], "start_line": 238, "executed_branches": [[247, 248], [247, 250]], "missing_branches": []}, "FormField.__init__": {"executed_lines": [294, 295, 296, 297, 298, 299, 300], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [283], "start_line": 276, "executed_branches": [], "missing_branches": []}, "FormField.name": {"executed_lines": [305], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [304], "start_line": 303, "executed_branches": [], "missing_branches": []}, "FormField.field_type": {"executed_lines": [310], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [309], "start_line": 308, "executed_branches": [], "missing_branches": []}, "FormField.label": {"executed_lines": [315], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [314], "start_line": 313, "executed_branches": [], "missing_branches": []}, "FormField.value": {"executed_lines": [325], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [324], "start_line": 323, "executed_branches": [], "missing_branches": []}, "FormField.required": {"executed_lines": [330], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [329], "start_line": 328, "executed_branches": [], "missing_branches": []}, "FormField.options": {"executed_lines": [335], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [334], "start_line": 333, "executed_branches": [], "missing_branches": []}, "FormField.form": {"executed_lines": [345], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [344], "start_line": 343, "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [1, 2, 3, 4, 7, 9, 10, 11, 12, 15, 22, 47, 48, 52, 53, 57, 58, 62, 63, 67, 68, 72, 93, 99, 121, 122, 126, 127, 131, 132, 136, 137, 141, 142, 146, 147, 151, 166, 172, 192, 193, 197, 198, 202, 203, 207, 217, 229, 238, 253, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 271, 276, 302, 303, 307, 308, 312, 313, 317, 318, 322, 323, 327, 328, 332, 333, 337, 338, 342, 343], "summary": {"covered_lines": 84, "num_statements": 84, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 6, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [8, 16, 94, 167, 254, 272], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"LinkType": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 7, "executed_branches": [], "missing_branches": []}, "Link": {"executed_lines": [40, 41, 42, 43, 44, 45, 50, 55, 60, 65, 85, 86, 90], "summary": {"covered_lines": 13, "num_statements": 14, "percent_covered": 93.75, "percent_covered_display": "94", "missing_lines": 1, "excluded_lines": 7, "percent_statements_covered": 92.85714285714286, "percent_statements_covered_display": "93", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [70], "excluded_lines": [29, 49, 54, 59, 64, 69, 73], "start_line": 15, "executed_branches": [[85, 86], [85, 90]], "missing_branches": []}, "Button": {"executed_lines": [115, 116, 117, 118, 119, 124, 129, 134, 139, 144, 161, 162, 163], "summary": {"covered_lines": 13, "num_statements": 14, "percent_covered": 93.75, "percent_covered_display": "94", "missing_lines": 1, "excluded_lines": 8, "percent_statements_covered": 92.85714285714286, "percent_statements_covered_display": "93", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [149], "excluded_lines": [105, 123, 128, 133, 138, 143, 148, 152], "start_line": 93, "executed_branches": [[161, 162], [161, 163]], "missing_branches": []}, "Form": {"executed_lines": [186, 187, 188, 189, 190, 195, 200, 214, 215, 227, 236, 245, 247, 248, 250], "summary": {"covered_lines": 15, "num_statements": 16, "percent_covered": 94.44444444444444, "percent_covered_display": "94", "missing_lines": 1, "excluded_lines": 8, "percent_statements_covered": 93.75, "percent_statements_covered_display": "94", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [205], "excluded_lines": [177, 194, 199, 204, 208, 218, 230, 239], "start_line": 166, "executed_branches": [[247, 248], [247, 250]], "missing_branches": []}, "FormFieldType": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 253, "executed_branches": [], "missing_branches": []}, "FormField": {"executed_lines": [294, 295, 296, 297, 298, 299, 300, 305, 310, 315, 320, 325, 330, 335, 340, 345], "summary": {"covered_lines": 16, "num_statements": 16, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 10, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [283, 304, 309, 314, 319, 324, 329, 334, 339, 344], "start_line": 271, "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [1, 2, 3, 4, 7, 9, 10, 11, 12, 15, 22, 47, 48, 52, 53, 57, 58, 62, 63, 67, 68, 72, 93, 99, 121, 122, 126, 127, 131, 132, 136, 137, 141, 142, 146, 147, 151, 166, 172, 192, 193, 197, 198, 202, 203, 207, 217, 229, 238, 253, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 271, 276, 302, 303, 307, 308, 312, 313, 317, 318, 322, 323, 327, 328, 332, 333, 337, 338, 342, 343], "summary": {"covered_lines": 84, "num_statements": 84, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 6, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [8, 16, 94, 167, 254, 272], "start_line": 1, "executed_branches": [], "missing_branches": []}}}, "pyWebLayout/abstract/inline.py": {"executed_lines": [1, 2, 3, 4, 5, 6, 7, 10, 13, 14, 22, 25, 34, 50, 51, 52, 53, 54, 55, 56, 57, 59, 60, 83, 84, 85, 87, 91, 92, 95, 96, 98, 99, 101, 103, 104, 105, 106, 109, 112, 113, 116, 118, 119, 120, 122, 124, 125, 127, 133, 134, 137, 140, 142, 145, 147, 148, 150, 151, 153, 155, 156, 158, 160, 161, 163, 165, 166, 168, 170, 171, 173, 175, 177, 179, 189, 191, 202, 205, 208, 214, 222, 223, 224, 226, 227, 248, 249, 250, 252, 256, 257, 260, 263, 264, 266, 269, 271, 272, 274, 276, 277, 279, 281, 282, 284, 286, 297, 300, 303, 304, 307, 309, 312, 320, 341, 344, 345, 346, 347, 348, 350, 351, 353, 355, 356, 358, 360, 361, 363, 365, 366, 368, 370, 371, 373, 375, 377, 388, 399, 400, 403, 404, 407, 410, 419, 421, 423, 424, 426, 427, 429, 431, 432, 443, 446, 447, 448, 449, 450, 452, 455, 457], "summary": {"covered_lines": 163, "num_statements": 164, "percent_covered": 99.03846153846153, "percent_covered_display": "99", "missing_lines": 1, "excluded_lines": 32, "percent_statements_covered": 99.39024390243902, "percent_statements_covered_display": "99", "num_branches": 44, "num_partial_branches": 1, "covered_branches": 43, "missing_branches": 1, "percent_branches_covered": 97.72727272727273, "percent_branches_covered_display": "98"}, "missing_lines": [401], "excluded_lines": [15, 26, 41, 62, 152, 157, 162, 167, 172, 176, 180, 192, 209, 215, 232, 273, 278, 283, 287, 313, 326, 352, 357, 362, 367, 372, 376, 389, 411, 420, 428, 433], "executed_branches": [[56, -34], [56, 57], [83, 84], [83, 91], [84, 85], [84, 87], [91, 92], [91, 95], [96, 98], [96, 99], [99, 101], [99, 109], [103, 104], [103, 109], [112, 113], [112, 116], [116, 118], [116, 142], [122, 124], [122, 140], [125, 127], [125, 133], [133, 134], [133, 137], [248, 249], [248, 256], [249, 250], [249, 252], [256, 257], [256, 260], [263, 264], [263, 266], [303, 304], [303, 307], [400, 403], [403, 404], [403, 407], [446, 447], [446, 448], [448, 449], [448, 450], [450, 452], [450, 455]], "missing_branches": [[400, 401]], "functions": {"_hyphen_dict": {"executed_lines": [22], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [15], "start_line": 14, "executed_branches": [], "missing_branches": []}, "Word.__init__": {"executed_lines": [50, 51, 52, 53, 54, 55, 56, 57], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [41], "start_line": 34, "executed_branches": [[56, -34], [56, 57]], "missing_branches": []}, "Word.create_and_add_to": {"executed_lines": [83, 84, 85, 87, 91, 92, 95, 96, 98, 99, 101, 103, 104, 105, 106, 109, 112, 113, 116, 118, 119, 120, 122, 124, 125, 127, 133, 134, 137, 140, 142, 145], "summary": {"covered_lines": 32, "num_statements": 32, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 22, "num_partial_branches": 0, "covered_branches": 22, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [62], "start_line": 60, "executed_branches": [[83, 84], [83, 91], [84, 85], [84, 87], [91, 92], [91, 95], [96, 98], [96, 99], [99, 101], [99, 109], [103, 104], [103, 109], [112, 113], [112, 116], [116, 118], [116, 142], [122, 124], [122, 140], [125, 127], [125, 133], [133, 134], [133, 137]], "missing_branches": []}, "Word.add_concete": {"executed_lines": [148], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 147, "executed_branches": [], "missing_branches": []}, "Word.text": {"executed_lines": [153], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [152], "start_line": 151, "executed_branches": [], "missing_branches": []}, "Word.style": {"executed_lines": [158], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [157], "start_line": 156, "executed_branches": [], "missing_branches": []}, "Word.background": {"executed_lines": [163], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [162], "start_line": 161, "executed_branches": [], "missing_branches": []}, "Word.previous": {"executed_lines": [168], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [167], "start_line": 166, "executed_branches": [], "missing_branches": []}, "Word.next": {"executed_lines": [173], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [172], "start_line": 171, "executed_branches": [], "missing_branches": []}, "Word.add_next": {"executed_lines": [177], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [176], "start_line": 175, "executed_branches": [], "missing_branches": []}, "Word.with_style": {"executed_lines": [189], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [180], "start_line": 179, "executed_branches": [], "missing_branches": []}, "Word.possible_hyphenation": {"executed_lines": [202], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [192], "start_line": 191, "executed_branches": [], "missing_branches": []}, "FormattedSpan.__init__": {"executed_lines": [222, 223, 224], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [215], "start_line": 214, "executed_branches": [], "missing_branches": []}, "FormattedSpan.create_and_add_to": {"executed_lines": [248, 249, 250, 252, 256, 257, 260, 263, 264, 266, 269], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 8, "num_partial_branches": 0, "covered_branches": 8, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [232], "start_line": 227, "executed_branches": [[248, 249], [248, 256], [249, 250], [249, 252], [256, 257], [256, 260], [263, 264], [263, 266]], "missing_branches": []}, "FormattedSpan.style": {"executed_lines": [274], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [273], "start_line": 272, "executed_branches": [], "missing_branches": []}, "FormattedSpan.background": {"executed_lines": [279], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [278], "start_line": 277, "executed_branches": [], "missing_branches": []}, "FormattedSpan.words": {"executed_lines": [284], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [283], "start_line": 282, "executed_branches": [], "missing_branches": []}, "FormattedSpan.add_word": {"executed_lines": [297, 300, 303, 304, 307, 309], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [287], "start_line": 286, "executed_branches": [[303, 304], [303, 307]], "missing_branches": []}, "LinkedWord.__init__": {"executed_lines": [341, 344, 345, 346, 347, 348], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [326], "start_line": 320, "executed_branches": [], "missing_branches": []}, "LinkedWord.location": {"executed_lines": [353], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [352], "start_line": 351, "executed_branches": [], "missing_branches": []}, "LinkedWord.link_type": {"executed_lines": [358], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [357], "start_line": 356, "executed_branches": [], "missing_branches": []}, "LinkedWord.link_callback": {"executed_lines": [363], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [362], "start_line": 361, "executed_branches": [], "missing_branches": []}, "LinkedWord.params": {"executed_lines": [368], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [367], "start_line": 366, "executed_branches": [], "missing_branches": []}, "LinkedWord.link_title": {"executed_lines": [373], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [372], "start_line": 371, "executed_branches": [], "missing_branches": []}, "LinkedWord.with_style": {"executed_lines": [377], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [376], "start_line": 375, "executed_branches": [], "missing_branches": []}, "LinkedWord.execute_link": {"executed_lines": [399, 400, 403, 404, 407], "summary": {"covered_lines": 5, "num_statements": 6, "percent_covered": 80.0, "percent_covered_display": "80", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 83.33333333333333, "percent_statements_covered_display": "83", "num_branches": 4, "num_partial_branches": 1, "covered_branches": 3, "missing_branches": 1, "percent_branches_covered": 75.0, "percent_branches_covered_display": "75"}, "missing_lines": [401], "excluded_lines": [389], "start_line": 388, "executed_branches": [[400, 403], [403, 404], [403, 407]], "missing_branches": [[400, 401]]}, "LineBreak.__init__": {"executed_lines": [421, 423, 424], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [420], "start_line": 419, "executed_branches": [], "missing_branches": []}, "LineBreak.block_type": {"executed_lines": [429], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [428], "start_line": 427, "executed_branches": [], "missing_branches": []}, "LineBreak.create_and_add_to": {"executed_lines": [443, 446, 447, 448, 449, 450, 452, 455, 457], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [433], "start_line": 432, "executed_branches": [[446, 447], [446, 448], [448, 449], [448, 450], [450, 452], [450, 455]], "missing_branches": []}, "": {"executed_lines": [1, 2, 3, 4, 5, 6, 7, 10, 13, 14, 25, 34, 59, 60, 147, 150, 151, 155, 156, 160, 161, 165, 166, 170, 171, 175, 179, 191, 205, 208, 214, 226, 227, 271, 272, 276, 277, 281, 282, 286, 312, 320, 350, 351, 355, 356, 360, 361, 365, 366, 370, 371, 375, 388, 410, 419, 426, 427, 431, 432], "summary": {"covered_lines": 60, "num_statements": 60, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 4, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [26, 209, 313, 411], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"Word": {"executed_lines": [50, 51, 52, 53, 54, 55, 56, 57, 83, 84, 85, 87, 91, 92, 95, 96, 98, 99, 101, 103, 104, 105, 106, 109, 112, 113, 116, 118, 119, 120, 122, 124, 125, 127, 133, 134, 137, 140, 142, 145, 148, 153, 158, 163, 168, 173, 177, 189, 202], "summary": {"covered_lines": 49, "num_statements": 49, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 10, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 24, "num_partial_branches": 0, "covered_branches": 24, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [41, 62, 152, 157, 162, 167, 172, 176, 180, 192], "start_line": 25, "executed_branches": [[56, -34], [56, 57], [83, 84], [83, 91], [84, 85], [84, 87], [91, 92], [91, 95], [96, 98], [96, 99], [99, 101], [99, 109], [103, 104], [103, 109], [112, 113], [112, 116], [116, 118], [116, 142], [122, 124], [122, 140], [125, 127], [125, 133], [133, 134], [133, 137]], "missing_branches": []}, "FormattedSpan": {"executed_lines": [222, 223, 224, 248, 249, 250, 252, 256, 257, 260, 263, 264, 266, 269, 274, 279, 284, 297, 300, 303, 304, 307, 309], "summary": {"covered_lines": 23, "num_statements": 23, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 6, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 10, "num_partial_branches": 0, "covered_branches": 10, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [215, 232, 273, 278, 283, 287], "start_line": 208, "executed_branches": [[248, 249], [248, 256], [249, 250], [249, 252], [256, 257], [256, 260], [263, 264], [263, 266], [303, 304], [303, 307]], "missing_branches": []}, "LinkedWord": {"executed_lines": [341, 344, 345, 346, 347, 348, 353, 358, 363, 368, 373, 377, 399, 400, 403, 404, 407], "summary": {"covered_lines": 17, "num_statements": 18, "percent_covered": 90.9090909090909, "percent_covered_display": "91", "missing_lines": 1, "excluded_lines": 8, "percent_statements_covered": 94.44444444444444, "percent_statements_covered_display": "94", "num_branches": 4, "num_partial_branches": 1, "covered_branches": 3, "missing_branches": 1, "percent_branches_covered": 75.0, "percent_branches_covered_display": "75"}, "missing_lines": [401], "excluded_lines": [326, 352, 357, 362, 367, 372, 376, 389], "start_line": 312, "executed_branches": [[400, 403], [403, 404], [403, 407]], "missing_branches": [[400, 401]]}, "LineBreak": {"executed_lines": [421, 423, 424, 429, 443, 446, 447, 448, 449, 450, 452, 455, 457], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 3, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [420, 428, 433], "start_line": 410, "executed_branches": [[446, 447], [446, 448], [448, 449], [448, 450], [450, 452], [450, 455]], "missing_branches": []}, "": {"executed_lines": [1, 2, 3, 4, 5, 6, 7, 10, 13, 14, 22, 25, 34, 59, 60, 147, 150, 151, 155, 156, 160, 161, 165, 166, 170, 171, 175, 179, 191, 205, 208, 214, 226, 227, 271, 272, 276, 277, 281, 282, 286, 312, 320, 350, 351, 355, 356, 360, 361, 365, 366, 370, 371, 375, 388, 410, 419, 426, 427, 431, 432], "summary": {"covered_lines": 61, "num_statements": 61, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 5, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [15, 26, 209, 313, 411], "start_line": 1, "executed_branches": [], "missing_branches": []}}}, "pyWebLayout/abstract/interactive_image.py": {"executed_lines": [9, 10, 12, 13, 16, 38, 57, 65, 68, 69, 71, 84, 86, 87, 89, 91, 101, 102, 103, 105, 106, 132, 141, 143, 145, 146, 150, 152, 162, 163], "summary": {"covered_lines": 30, "num_statements": 34, "percent_covered": 80.43478260869566, "percent_covered_display": "80", "missing_lines": 4, "excluded_lines": 7, "percent_statements_covered": 88.23529411764706, "percent_statements_covered_display": "88", "num_branches": 12, "num_partial_branches": 3, "covered_branches": 7, "missing_branches": 5, "percent_branches_covered": 58.333333333333336, "percent_branches_covered_display": "58"}, "missing_lines": [142, 144, 147, 148], "excluded_lines": [1, 17, 46, 72, 92, 115, 153], "executed_branches": [[84, 86], [84, 89], [86, 87], [86, 89], [141, 143], [143, 145], [145, 146]], "missing_branches": [[141, 142], [143, 144], [145, 147], [147, 148], [147, 150]], "functions": {"InteractiveImage.__init__": {"executed_lines": [57, 65, 68, 69], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [46], "start_line": 38, "executed_branches": [], "missing_branches": []}, "InteractiveImage.interact": {"executed_lines": [84, 86, 87, 89], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [72], "start_line": 71, "executed_branches": [[84, 86], [84, 89], [86, 87], [86, 89]], "missing_branches": []}, "InteractiveImage.in_object": {"executed_lines": [101, 102, 103], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [92], "start_line": 91, "executed_branches": [], "missing_branches": []}, "InteractiveImage.create_and_add_to": {"executed_lines": [132, 141, 143, 145, 146, 150], "summary": {"covered_lines": 6, "num_statements": 10, "percent_covered": 50.0, "percent_covered_display": "50", "missing_lines": 4, "excluded_lines": 1, "percent_statements_covered": 60.0, "percent_statements_covered_display": "60", "num_branches": 8, "num_partial_branches": 3, "covered_branches": 3, "missing_branches": 5, "percent_branches_covered": 37.5, "percent_branches_covered_display": "38"}, "missing_lines": [142, 144, 147, 148], "excluded_lines": [115], "start_line": 106, "executed_branches": [[141, 143], [143, 145], [145, 146]], "missing_branches": [[141, 142], [143, 144], [145, 147], [147, 148], [147, 150]]}, "InteractiveImage.set_rendered_bounds": {"executed_lines": [162, 163], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [153], "start_line": 152, "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [9, 10, 12, 13, 16, 38, 71, 91, 105, 106, 152], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 2, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1, 17], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"InteractiveImage": {"executed_lines": [57, 65, 68, 69, 84, 86, 87, 89, 101, 102, 103, 132, 141, 143, 145, 146, 150, 162, 163], "summary": {"covered_lines": 19, "num_statements": 23, "percent_covered": 74.28571428571429, "percent_covered_display": "74", "missing_lines": 4, "excluded_lines": 5, "percent_statements_covered": 82.6086956521739, "percent_statements_covered_display": "83", "num_branches": 12, "num_partial_branches": 3, "covered_branches": 7, "missing_branches": 5, "percent_branches_covered": 58.333333333333336, "percent_branches_covered_display": "58"}, "missing_lines": [142, 144, 147, 148], "excluded_lines": [46, 72, 92, 115, 153], "start_line": 16, "executed_branches": [[84, 86], [84, 89], [86, 87], [86, 89], [141, 143], [143, 145], [145, 146]], "missing_branches": [[141, 142], [143, 144], [145, 147], [147, 148], [147, 150]]}, "": {"executed_lines": [9, 10, 12, 13, 16, 38, 71, 91, 105, 106, 152], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 2, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1, 17], "start_line": 1, "executed_branches": [], "missing_branches": []}}}, "pyWebLayout/concrete/__init__.py": {"executed_lines": [7, 15, 16, 17, 18, 19, 21], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1], "executed_branches": [], "missing_branches": [], "functions": {"": {"executed_lines": [7, 15, 16, 17, 18, 19, 21], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [7, 15, 16, 17, 18, 19, 21], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1], "start_line": 1, "executed_branches": [], "missing_branches": []}}}, "pyWebLayout/concrete/box.py": {"executed_lines": [1, 2, 3, 5, 6, 7, 10, 17, 26, 27, 28, 29, 30, 31, 33, 34, 35, 39, 40], "summary": {"covered_lines": 19, "num_statements": 19, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [11], "executed_branches": [[30, 31], [30, 33]], "missing_branches": [], "functions": {"Box.__init__": {"executed_lines": [26, 27, 28, 29, 30, 31, 33, 34, 35], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 17, "executed_branches": [[30, 31], [30, 33]], "missing_branches": []}, "Box.in_shape": {"executed_lines": [40], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 39, "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [1, 2, 3, 5, 6, 7, 10, 17, 39], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [11], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"Box": {"executed_lines": [26, 27, 28, 29, 30, 31, 33, 34, 35, 40], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 10, "executed_branches": [[30, 31], [30, 33]], "missing_branches": []}, "": {"executed_lines": [1, 2, 3, 5, 6, 7, 10, 17, 39], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [11], "start_line": 1, "executed_branches": [], "missing_branches": []}}}, "pyWebLayout/concrete/dynamic_page.py": {"executed_lines": [14, 15, 16, 17, 19, 20, 21, 24, 25, 27, 28, 29, 30, 35, 46, 57, 58, 61, 62, 63, 64, 65, 68, 69, 71, 72, 74, 76, 89, 90, 93, 94, 96, 97, 101, 102, 104, 118, 119, 122, 123, 124, 126, 127, 128, 131, 132, 134, 136, 147, 151, 153, 156, 157, 160, 161, 162, 164, 166, 168, 169, 179, 182, 185, 186, 188, 199, 203, 205, 207, 208, 210, 211, 212, 213, 214, 216, 217, 220, 222, 224, 226, 235, 238, 240, 243, 244, 246, 256, 257, 259, 261, 272, 274, 275, 277, 288, 292, 293, 295, 296, 301, 302, 304, 314, 315, 317, 319, 320, 326, 331, 345, 346, 348, 368, 370, 372, 379, 380, 382, 384, 386, 388, 389, 390, 391, 392, 393, 395, 405, 406, 407, 409, 416, 417, 418], "summary": {"covered_lines": 136, "num_statements": 178, "percent_covered": 68.3206106870229, "percent_covered_display": "68", "missing_lines": 42, "excluded_lines": 17, "percent_statements_covered": 76.40449438202248, "percent_statements_covered_display": "76", "num_branches": 84, "num_partial_branches": 19, "covered_branches": 43, "missing_branches": 41, "percent_branches_covered": 51.19047619047619, "percent_branches_covered_display": "51"}, "missing_lines": [95, 105, 107, 108, 111, 112, 114, 115, 125, 129, 148, 170, 172, 173, 174, 176, 183, 200, 227, 228, 229, 230, 232, 239, 241, 262, 263, 264, 265, 267, 269, 323, 350, 351, 352, 353, 356, 358, 360, 361, 362, 365], "excluded_lines": [1, 26, 36, 49, 73, 77, 137, 189, 247, 278, 305, 332, 373, 383, 387, 396, 410], "executed_branches": [[89, 90], [89, 93], [93, 94], [93, 101], [94, 96], [96, 97], [96, 101], [104, 118], [122, 123], [122, 124], [124, 126], [126, 127], [126, 128], [128, 131], [147, 151], [156, 157], [156, 179], [157, 160], [160, 156], [160, 161], [161, 162], [166, 168], [182, 185], [199, 203], [207, 208], [207, 235], [208, 210], [211, 212], [213, 214], [213, 226], [214, 216], [220, 213], [220, 222], [238, 240], [240, 243], [256, 257], [256, 259], [261, 272], [314, 315], [314, 326], [315, 317], [319, 320], [348, 368]], "missing_branches": [[94, 95], [104, 105], [105, 107], [105, 111], [124, 125], [128, 129], [147, 148], [157, 170], [161, 160], [166, 160], [170, 172], [170, 174], [174, 156], [174, 176], [182, 183], [199, 200], [208, 227], [211, 207], [214, 213], [227, 228], [227, 230], [230, 207], [230, 232], [238, 239], [240, 241], [261, 262], [262, 263], [262, 264], [264, 265], [264, 267], [315, 319], [319, 323], [348, 350], [350, 351], [350, 356], [351, 352], [351, 353], [358, 360], [358, 365], [360, 361], [360, 362]], "functions": {"DynamicPage.__init__": {"executed_lines": [57, 58, 61, 62, 63, 64, 65, 68, 69], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [49], "start_line": 46, "executed_branches": [], "missing_branches": []}, "DynamicPage.constraints": {"executed_lines": [74], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [73], "start_line": 72, "executed_branches": [], "missing_branches": []}, "DynamicPage.measure": {"executed_lines": [89, 90, 93, 94, 96, 97, 101, 102, 104, 118, 119, 122, 123, 124, 126, 127, 128, 131, 132, 134], "summary": {"covered_lines": 20, "num_statements": 30, "percent_covered": 68.0, "percent_covered_display": "68", "missing_lines": 10, "excluded_lines": 1, "percent_statements_covered": 66.66666666666667, "percent_statements_covered_display": "67", "num_branches": 20, "num_partial_branches": 4, "covered_branches": 14, "missing_branches": 6, "percent_branches_covered": 70.0, "percent_branches_covered_display": "70"}, "missing_lines": [95, 105, 107, 108, 111, 112, 114, 115, 125, 129], "excluded_lines": [77], "start_line": 76, "executed_branches": [[89, 90], [89, 93], [93, 94], [93, 101], [94, 96], [96, 97], [96, 101], [104, 118], [122, 123], [122, 124], [124, 126], [126, 127], [126, 128], [128, 131]], "missing_branches": [[94, 95], [104, 105], [105, 107], [105, 111], [124, 125], [128, 129]]}, "DynamicPage.get_min_width": {"executed_lines": [147, 151, 153, 156, 157, 160, 161, 162, 164, 166, 168, 169, 179, 182, 185, 186], "summary": {"covered_lines": 16, "num_statements": 23, "percent_covered": 60.97560975609756, "percent_covered_display": "61", "missing_lines": 7, "excluded_lines": 1, "percent_statements_covered": 69.56521739130434, "percent_statements_covered_display": "70", "num_branches": 18, "num_partial_branches": 5, "covered_branches": 9, "missing_branches": 9, "percent_branches_covered": 50.0, "percent_branches_covered_display": "50"}, "missing_lines": [148, 170, 172, 173, 174, 176, 183], "excluded_lines": [137], "start_line": 136, "executed_branches": [[147, 151], [156, 157], [156, 179], [157, 160], [160, 156], [160, 161], [161, 162], [166, 168], [182, 185]], "missing_branches": [[147, 148], [157, 170], [161, 160], [166, 160], [170, 172], [170, 174], [174, 156], [174, 176], [182, 183]]}, "DynamicPage.get_preferred_width": {"executed_lines": [199, 203, 205, 207, 208, 210, 211, 212, 213, 214, 216, 217, 220, 222, 224, 226, 235, 238, 240, 243, 244], "summary": {"covered_lines": 21, "num_statements": 29, "percent_covered": 64.70588235294117, "percent_covered_display": "65", "missing_lines": 8, "excluded_lines": 1, "percent_statements_covered": 72.41379310344827, "percent_statements_covered_display": "72", "num_branches": 22, "num_partial_branches": 6, "covered_branches": 12, "missing_branches": 10, "percent_branches_covered": 54.54545454545455, "percent_branches_covered_display": "55"}, "missing_lines": [200, 227, 228, 229, 230, 232, 239, 241], "excluded_lines": [189], "start_line": 188, "executed_branches": [[199, 203], [207, 208], [207, 235], [208, 210], [211, 212], [213, 214], [213, 226], [214, 216], [220, 213], [220, 222], [238, 240], [240, 243]], "missing_branches": [[199, 200], [208, 227], [211, 207], [214, 213], [227, 228], [227, 230], [230, 207], [230, 232], [238, 239], [240, 241]]}, "DynamicPage.measure_content_height": {"executed_lines": [256, 257, 259, 261, 272, 274, 275], "summary": {"covered_lines": 7, "num_statements": 13, "percent_covered": 47.61904761904762, "percent_covered_display": "48", "missing_lines": 6, "excluded_lines": 1, "percent_statements_covered": 53.84615384615385, "percent_statements_covered_display": "54", "num_branches": 8, "num_partial_branches": 1, "covered_branches": 3, "missing_branches": 5, "percent_branches_covered": 37.5, "percent_branches_covered_display": "38"}, "missing_lines": [262, 263, 264, 265, 267, 269], "excluded_lines": [247], "start_line": 246, "executed_branches": [[256, 257], [256, 259], [261, 272]], "missing_branches": [[261, 262], [262, 263], [262, 264], [264, 265], [264, 267]]}, "DynamicPage.layout": {"executed_lines": [288, 292, 293, 295, 296, 301, 302], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [278], "start_line": 277, "executed_branches": [], "missing_branches": []}, "DynamicPage.render": {"executed_lines": [314, 315, 317, 319, 320, 326], "summary": {"covered_lines": 6, "num_statements": 7, "percent_covered": 76.92307692307692, "percent_covered_display": "77", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 85.71428571428571, "percent_statements_covered_display": "86", "num_branches": 6, "num_partial_branches": 2, "covered_branches": 4, "missing_branches": 2, "percent_branches_covered": 66.66666666666667, "percent_branches_covered_display": "67"}, "missing_lines": [323], "excluded_lines": [305], "start_line": 304, "executed_branches": [[314, 315], [314, 326], [315, 317], [319, 320]], "missing_branches": [[315, 319], [319, 323]]}, "DynamicPage.render_partial": {"executed_lines": [345, 346, 348, 368, 370], "summary": {"covered_lines": 5, "num_statements": 15, "percent_covered": 24.0, "percent_covered_display": "24", "missing_lines": 10, "excluded_lines": 1, "percent_statements_covered": 33.333333333333336, "percent_statements_covered_display": "33", "num_branches": 10, "num_partial_branches": 1, "covered_branches": 1, "missing_branches": 9, "percent_branches_covered": 10.0, "percent_branches_covered_display": "10"}, "missing_lines": [350, 351, 352, 353, 356, 358, 360, 361, 362, 365], "excluded_lines": [332], "start_line": 331, "executed_branches": [[348, 368]], "missing_branches": [[348, 350], [350, 351], [350, 356], [351, 352], [351, 353], [358, 360], [358, 365], [360, 361], [360, 362]]}, "DynamicPage.has_more_content": {"executed_lines": [379, 380], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [373], "start_line": 372, "executed_branches": [], "missing_branches": []}, "DynamicPage.reset_pagination": {"executed_lines": [384], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [383], "start_line": 382, "executed_branches": [], "missing_branches": []}, "DynamicPage.invalidate_caches": {"executed_lines": [388, 389, 390, 391, 392, 393], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [387], "start_line": 386, "executed_branches": [], "missing_branches": []}, "DynamicPage.add_child": {"executed_lines": [405, 406, 407], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [396], "start_line": 395, "executed_branches": [], "missing_branches": []}, "DynamicPage.clear_children": {"executed_lines": [416, 417, 418], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [410], "start_line": 409, "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [14, 15, 16, 17, 19, 20, 21, 24, 25, 27, 28, 29, 30, 35, 46, 71, 72, 76, 136, 188, 246, 277, 304, 331, 372, 382, 386, 395, 409], "summary": {"covered_lines": 29, "num_statements": 29, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 3, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1, 26, 36], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"SizeConstraints": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 25, "executed_branches": [], "missing_branches": []}, "DynamicPage": {"executed_lines": [57, 58, 61, 62, 63, 64, 65, 68, 69, 74, 89, 90, 93, 94, 96, 97, 101, 102, 104, 118, 119, 122, 123, 124, 126, 127, 128, 131, 132, 134, 147, 151, 153, 156, 157, 160, 161, 162, 164, 166, 168, 169, 179, 182, 185, 186, 199, 203, 205, 207, 208, 210, 211, 212, 213, 214, 216, 217, 220, 222, 224, 226, 235, 238, 240, 243, 244, 256, 257, 259, 261, 272, 274, 275, 288, 292, 293, 295, 296, 301, 302, 314, 315, 317, 319, 320, 326, 345, 346, 348, 368, 370, 379, 380, 384, 388, 389, 390, 391, 392, 393, 405, 406, 407, 416, 417, 418], "summary": {"covered_lines": 107, "num_statements": 149, "percent_covered": 64.37768240343348, "percent_covered_display": "64", "missing_lines": 42, "excluded_lines": 14, "percent_statements_covered": 71.81208053691275, "percent_statements_covered_display": "72", "num_branches": 84, "num_partial_branches": 19, "covered_branches": 43, "missing_branches": 41, "percent_branches_covered": 51.19047619047619, "percent_branches_covered_display": "51"}, "missing_lines": [95, 105, 107, 108, 111, 112, 114, 115, 125, 129, 148, 170, 172, 173, 174, 176, 183, 200, 227, 228, 229, 230, 232, 239, 241, 262, 263, 264, 265, 267, 269, 323, 350, 351, 352, 353, 356, 358, 360, 361, 362, 365], "excluded_lines": [49, 73, 77, 137, 189, 247, 278, 305, 332, 373, 383, 387, 396, 410], "start_line": 35, "executed_branches": [[89, 90], [89, 93], [93, 94], [93, 101], [94, 96], [96, 97], [96, 101], [104, 118], [122, 123], [122, 124], [124, 126], [126, 127], [126, 128], [128, 131], [147, 151], [156, 157], [156, 179], [157, 160], [160, 156], [160, 161], [161, 162], [166, 168], [182, 185], [199, 203], [207, 208], [207, 235], [208, 210], [211, 212], [213, 214], [213, 226], [214, 216], [220, 213], [220, 222], [238, 240], [240, 243], [256, 257], [256, 259], [261, 272], [314, 315], [314, 326], [315, 317], [319, 320], [348, 368]], "missing_branches": [[94, 95], [104, 105], [105, 107], [105, 111], [124, 125], [128, 129], [147, 148], [157, 170], [161, 160], [166, 160], [170, 172], [170, 174], [174, 156], [174, 176], [182, 183], [199, 200], [208, 227], [211, 207], [214, 213], [227, 228], [227, 230], [230, 207], [230, 232], [238, 239], [240, 241], [261, 262], [262, 263], [262, 264], [264, 265], [264, 267], [315, 319], [319, 323], [348, 350], [350, 351], [350, 356], [351, 352], [351, 353], [358, 360], [358, 365], [360, 361], [360, 362]]}, "": {"executed_lines": [14, 15, 16, 17, 19, 20, 21, 24, 25, 27, 28, 29, 30, 35, 46, 71, 72, 76, 136, 188, 246, 277, 304, 331, 372, 382, 386, 395, 409], "summary": {"covered_lines": 29, "num_statements": 29, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 3, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1, 26, 36], "start_line": 1, "executed_branches": [], "missing_branches": []}}}, "pyWebLayout/concrete/functional.py": {"executed_lines": [1, 2, 3, 4, 6, 7, 8, 9, 12, 18, 33, 34, 35, 36, 37, 39, 40, 41, 42, 45, 48, 51, 52, 53, 54, 57, 60, 61, 63, 65, 67, 68, 70, 72, 73, 75, 77, 80, 89, 90, 94, 97, 106, 107, 108, 109, 111, 114, 115, 118, 121, 127, 143, 146, 149, 150, 151, 152, 153, 157, 160, 166, 167, 169, 171, 172, 173, 178, 179, 181, 183, 184, 186, 188, 190, 191, 193, 195, 196, 198, 207, 209, 212, 217, 219, 220, 221, 222, 227, 234, 235, 236, 240, 246, 250, 251, 255, 258, 269, 271, 274, 275, 278, 281, 283, 293, 294, 297, 301, 314, 316, 330, 334, 337, 338, 339, 344, 345, 349, 352, 354, 356, 357, 358, 363, 364, 366, 368, 369, 371, 373, 374, 376, 378, 380, 382, 388, 389, 393, 394, 395, 396, 399, 400, 403, 404, 406, 408, 411, 412, 415, 416, 419, 423, 429, 430, 433, 436, 447, 450, 452, 453, 455, 457, 467, 468, 471, 476, 490, 493, 507, 510, 524], "summary": {"covered_lines": 173, "num_statements": 190, "percent_covered": 89.1891891891892, "percent_covered_display": "89", "missing_lines": 17, "excluded_lines": 31, "percent_statements_covered": 91.05263157894737, "percent_statements_covered_display": "91", "num_branches": 32, "num_partial_branches": 7, "covered_branches": 25, "missing_branches": 7, "percent_branches_covered": 78.125, "percent_branches_covered_display": "78"}, "missing_lines": [58, 78, 92, 174, 176, 205, 210, 224, 225, 226, 229, 230, 231, 359, 361, 390, 391], "excluded_lines": [13, 20, 62, 66, 71, 76, 81, 122, 130, 170, 180, 185, 189, 194, 199, 208, 213, 284, 302, 318, 355, 365, 370, 375, 379, 383, 437, 458, 478, 495, 512], "executed_branches": [[34, 35], [34, 36], [36, 37], [36, 39], [39, 40], [39, 41], [41, 42], [57, -18], [77, -75], [90, 94], [106, 107], [106, 118], [109, 111], [109, 114], [209, -207], [217, 219], [217, 222], [222, 227], [227, 234], [411, -382], [411, 412], [415, 416], [415, 419], [450, 452], [450, 455]], "missing_branches": [[41, 45], [57, 58], [77, 78], [90, 92], [209, 210], [222, 224], [227, 229]], "functions": {"LinkText.__init__": {"executed_lines": [33, 34, 35, 36, 37, 39, 40, 41, 42, 45, 48, 51, 52, 53, 54, 57], "summary": {"covered_lines": 16, "num_statements": 17, "percent_covered": 88.88888888888889, "percent_covered_display": "89", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 94.11764705882354, "percent_statements_covered_display": "94", "num_branches": 10, "num_partial_branches": 2, "covered_branches": 8, "missing_branches": 2, "percent_branches_covered": 80.0, "percent_branches_covered_display": "80"}, "missing_lines": [58], "excluded_lines": [20], "start_line": 18, "executed_branches": [[34, 35], [34, 36], [36, 37], [36, 39], [39, 40], [39, 41], [41, 42], [57, -18]], "missing_branches": [[41, 45], [57, 58]]}, "LinkText.link": {"executed_lines": [63], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [62], "start_line": 61, "executed_branches": [], "missing_branches": []}, "LinkText.set_hovered": {"executed_lines": [67, 68], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [66], "start_line": 65, "executed_branches": [], "missing_branches": []}, "LinkText.set_pressed": {"executed_lines": [72, 73], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [71], "start_line": 70, "executed_branches": [], "missing_branches": []}, "LinkText._mark_page_dirty": {"executed_lines": [77], "summary": {"covered_lines": 1, "num_statements": 2, "percent_covered": 50.0, "percent_covered_display": "50", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 50.0, "percent_statements_covered_display": "50", "num_branches": 2, "num_partial_branches": 1, "covered_branches": 1, "missing_branches": 1, "percent_branches_covered": 50.0, "percent_branches_covered_display": "50"}, "missing_lines": [78], "excluded_lines": [76], "start_line": 75, "executed_branches": [[77, -75]], "missing_branches": [[77, 78]]}, "LinkText.render": {"executed_lines": [89, 90, 94, 97, 106, 107, 108, 109, 111, 114, 115, 118], "summary": {"covered_lines": 12, "num_statements": 13, "percent_covered": 89.47368421052632, "percent_covered_display": "89", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 92.3076923076923, "percent_statements_covered_display": "92", "num_branches": 6, "num_partial_branches": 1, "covered_branches": 5, "missing_branches": 1, "percent_branches_covered": 83.33333333333333, "percent_branches_covered_display": "83"}, "missing_lines": [92], "excluded_lines": [81], "start_line": 80, "executed_branches": [[90, 94], [106, 107], [106, 118], [109, 111], [109, 114]], "missing_branches": [[90, 92]]}, "ButtonText.__init__": {"executed_lines": [143, 146, 149, 150, 151, 152, 153, 157, 160, 166, 167], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [130], "start_line": 127, "executed_branches": [], "missing_branches": []}, "ButtonText._visual_text_height": {"executed_lines": [171, 172, 173], "summary": {"covered_lines": 3, "num_statements": 5, "percent_covered": 60.0, "percent_covered_display": "60", "missing_lines": 2, "excluded_lines": 1, "percent_statements_covered": 60.0, "percent_statements_covered_display": "60", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [174, 176], "excluded_lines": [170], "start_line": 169, "executed_branches": [], "missing_branches": []}, "ButtonText.button": {"executed_lines": [181], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [180], "start_line": 179, "executed_branches": [], "missing_branches": []}, "ButtonText.size": {"executed_lines": [186], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [185], "start_line": 184, "executed_branches": [], "missing_branches": []}, "ButtonText.set_pressed": {"executed_lines": [190, 191], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [189], "start_line": 188, "executed_branches": [], "missing_branches": []}, "ButtonText.set_hovered": {"executed_lines": [195, 196], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [194], "start_line": 193, "executed_branches": [], "missing_branches": []}, "ButtonText.set_page": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [205], "excluded_lines": [199], "start_line": 198, "executed_branches": [], "missing_branches": []}, "ButtonText._mark_page_dirty": {"executed_lines": [209], "summary": {"covered_lines": 1, "num_statements": 2, "percent_covered": 50.0, "percent_covered_display": "50", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 50.0, "percent_statements_covered_display": "50", "num_branches": 2, "num_partial_branches": 1, "covered_branches": 1, "missing_branches": 1, "percent_branches_covered": 50.0, "percent_branches_covered_display": "50"}, "missing_lines": [210], "excluded_lines": [208], "start_line": 207, "executed_branches": [[209, -207]], "missing_branches": [[209, 210]]}, "ButtonText.render": {"executed_lines": [217, 219, 220, 221, 222, 227, 234, 235, 236, 240, 246, 250, 251, 255, 258, 269, 271, 274, 275, 278, 281], "summary": {"covered_lines": 21, "num_statements": 27, "percent_covered": 75.75757575757575, "percent_covered_display": "76", "missing_lines": 6, "excluded_lines": 1, "percent_statements_covered": 77.77777777777777, "percent_statements_covered_display": "78", "num_branches": 6, "num_partial_branches": 2, "covered_branches": 4, "missing_branches": 2, "percent_branches_covered": 66.66666666666667, "percent_branches_covered_display": "67"}, "missing_lines": [224, 225, 226, 229, 230, 231], "excluded_lines": [213], "start_line": 212, "executed_branches": [[217, 219], [217, 222], [222, 227], [227, 234]], "missing_branches": [[222, 224], [227, 229]]}, "ButtonText.in_object": {"executed_lines": [293, 294, 297], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [284], "start_line": 283, "executed_branches": [], "missing_branches": []}, "FormFieldText.__init__": {"executed_lines": [330, 334, 337, 338, 339, 344, 345, 349, 352], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [318], "start_line": 316, "executed_branches": [], "missing_branches": []}, "FormFieldText._visual_label_height": {"executed_lines": [356, 357, 358], "summary": {"covered_lines": 3, "num_statements": 5, "percent_covered": 60.0, "percent_covered_display": "60", "missing_lines": 2, "excluded_lines": 1, "percent_statements_covered": 60.0, "percent_statements_covered_display": "60", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [359, 361], "excluded_lines": [355], "start_line": 354, "executed_branches": [], "missing_branches": []}, "FormFieldText.field_area_offset": {"executed_lines": [366], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [365], "start_line": 364, "executed_branches": [], "missing_branches": []}, "FormFieldText.field": {"executed_lines": [371], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [370], "start_line": 369, "executed_branches": [], "missing_branches": []}, "FormFieldText.size": {"executed_lines": [376], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [375], "start_line": 374, "executed_branches": [], "missing_branches": []}, "FormFieldText.set_focused": {"executed_lines": [380], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [379], "start_line": 378, "executed_branches": [], "missing_branches": []}, "FormFieldText.render": {"executed_lines": [388, 389, 393, 394, 395, 396, 399, 400, 403, 404, 406, 408, 411, 412, 415, 416, 419, 423, 429, 430, 433], "summary": {"covered_lines": 21, "num_statements": 23, "percent_covered": 92.5925925925926, "percent_covered_display": "93", "missing_lines": 2, "excluded_lines": 1, "percent_statements_covered": 91.30434782608695, "percent_statements_covered_display": "91", "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [390, 391], "excluded_lines": [383], "start_line": 382, "executed_branches": [[411, -382], [411, 412], [415, 416], [415, 419]], "missing_branches": []}, "FormFieldText.handle_click": {"executed_lines": [447, 450, 452, 453, 455], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [437], "start_line": 436, "executed_branches": [[450, 452], [450, 455]], "missing_branches": []}, "FormFieldText.in_object": {"executed_lines": [467, 468, 471], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [458], "start_line": 457, "executed_branches": [], "missing_branches": []}, "create_link_text": {"executed_lines": [490], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [478], "start_line": 476, "executed_branches": [], "missing_branches": []}, "create_button_text": {"executed_lines": [507], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [495], "start_line": 493, "executed_branches": [], "missing_branches": []}, "create_form_field_text": {"executed_lines": [524], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [512], "start_line": 510, "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [1, 2, 3, 4, 6, 7, 8, 9, 12, 18, 60, 61, 65, 70, 75, 80, 121, 127, 169, 178, 179, 183, 184, 188, 193, 198, 207, 212, 283, 301, 314, 316, 354, 363, 364, 368, 369, 373, 374, 378, 382, 436, 457, 476, 493, 510], "summary": {"covered_lines": 46, "num_statements": 46, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 3, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [13, 122, 302], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"LinkText": {"executed_lines": [33, 34, 35, 36, 37, 39, 40, 41, 42, 45, 48, 51, 52, 53, 54, 57, 63, 67, 68, 72, 73, 77, 89, 90, 94, 97, 106, 107, 108, 109, 111, 114, 115, 118], "summary": {"covered_lines": 34, "num_statements": 37, "percent_covered": 87.27272727272727, "percent_covered_display": "87", "missing_lines": 3, "excluded_lines": 6, "percent_statements_covered": 91.89189189189189, "percent_statements_covered_display": "92", "num_branches": 18, "num_partial_branches": 4, "covered_branches": 14, "missing_branches": 4, "percent_branches_covered": 77.77777777777777, "percent_branches_covered_display": "78"}, "missing_lines": [58, 78, 92], "excluded_lines": [20, 62, 66, 71, 76, 81], "start_line": 12, "executed_branches": [[34, 35], [34, 36], [36, 37], [36, 39], [39, 40], [39, 41], [41, 42], [57, -18], [77, -75], [90, 94], [106, 107], [106, 118], [109, 111], [109, 114]], "missing_branches": [[41, 45], [57, 58], [77, 78], [90, 92]]}, "ButtonText": {"executed_lines": [143, 146, 149, 150, 151, 152, 153, 157, 160, 166, 167, 171, 172, 173, 181, 186, 190, 191, 195, 196, 209, 217, 219, 220, 221, 222, 227, 234, 235, 236, 240, 246, 250, 251, 255, 258, 269, 271, 274, 275, 278, 281, 293, 294, 297], "summary": {"covered_lines": 45, "num_statements": 55, "percent_covered": 79.36507936507937, "percent_covered_display": "79", "missing_lines": 10, "excluded_lines": 10, "percent_statements_covered": 81.81818181818181, "percent_statements_covered_display": "82", "num_branches": 8, "num_partial_branches": 3, "covered_branches": 5, "missing_branches": 3, "percent_branches_covered": 62.5, "percent_branches_covered_display": "62"}, "missing_lines": [174, 176, 205, 210, 224, 225, 226, 229, 230, 231], "excluded_lines": [130, 170, 180, 185, 189, 194, 199, 208, 213, 284], "start_line": 121, "executed_branches": [[209, -207], [217, 219], [217, 222], [222, 227], [227, 234]], "missing_branches": [[209, 210], [222, 224], [227, 229]]}, "FormFieldText": {"executed_lines": [330, 334, 337, 338, 339, 344, 345, 349, 352, 356, 357, 358, 366, 371, 376, 380, 388, 389, 393, 394, 395, 396, 399, 400, 403, 404, 406, 408, 411, 412, 415, 416, 419, 423, 429, 430, 433, 447, 450, 452, 453, 455, 467, 468, 471], "summary": {"covered_lines": 45, "num_statements": 49, "percent_covered": 92.72727272727273, "percent_covered_display": "93", "missing_lines": 4, "excluded_lines": 9, "percent_statements_covered": 91.83673469387755, "percent_statements_covered_display": "92", "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [359, 361, 390, 391], "excluded_lines": [318, 355, 365, 370, 375, 379, 383, 437, 458], "start_line": 301, "executed_branches": [[411, -382], [411, 412], [415, 416], [415, 419], [450, 452], [450, 455]], "missing_branches": []}, "": {"executed_lines": [1, 2, 3, 4, 6, 7, 8, 9, 12, 18, 60, 61, 65, 70, 75, 80, 121, 127, 169, 178, 179, 183, 184, 188, 193, 198, 207, 212, 283, 301, 314, 316, 354, 363, 364, 368, 369, 373, 374, 378, 382, 436, 457, 476, 490, 493, 507, 510, 524], "summary": {"covered_lines": 49, "num_statements": 49, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 6, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [13, 122, 302, 478, 495, 512], "start_line": 1, "executed_branches": [], "missing_branches": []}}}, "pyWebLayout/concrete/image.py": {"executed_lines": [1, 2, 3, 4, 5, 6, 7, 10, 15, 35, 36, 37, 38, 39, 40, 41, 44, 47, 50, 51, 53, 54, 57, 60, 62, 63, 65, 67, 68, 70, 72, 73, 75, 77, 79, 81, 83, 85, 91, 94, 96, 97, 98, 100, 101, 102, 104, 105, 106, 107, 109, 110, 111, 113, 119, 123, 125, 128, 129, 132, 133, 134, 135, 137, 140, 141, 142, 143, 145, 148, 149, 153, 163, 165, 172, 173, 176, 179, 180, 183, 186, 187, 190, 193, 194, 197, 201, 204, 206, 211, 212, 213, 214, 216, 218, 222, 223, 226, 227, 229, 232, 235, 236, 237, 239, 240, 241, 242, 244, 245, 247, 248, 250, 251, 254, 255, 256, 257, 258, 261, 264, 267, 273, 275, 276, 279], "summary": {"covered_lines": 126, "num_statements": 134, "percent_covered": 92.94117647058823, "percent_covered_display": "93", "missing_lines": 8, "excluded_lines": 11, "percent_statements_covered": 94.02985074626865, "percent_statements_covered_display": "94", "num_branches": 36, "num_partial_branches": 4, "covered_branches": 32, "missing_branches": 4, "percent_branches_covered": 88.88888888888889, "percent_branches_covered_display": "89"}, "missing_lines": [88, 89, 115, 116, 117, 198, 269, 271], "excluded_lines": [11, 19, 64, 69, 74, 78, 82, 120, 166, 207, 274], "executed_branches": [[50, 51], [50, 57], [53, 54], [53, 57], [85, 91], [94, 96], [94, 98], [98, 100], [98, 113], [105, 106], [105, 109], [123, 125], [123, 163], [132, 133], [132, 134], [134, 135], [134, 137], [140, 141], [140, 142], [142, 143], [142, 145], [172, 173], [172, 176], [197, 201], [226, 227], [239, 240], [239, 250], [244, 245], [244, 247], [250, 251], [255, -206], [255, 256]], "missing_branches": [[85, 88], [197, 198], [226, -206], [250, 254]], "functions": {"RenderableImage.__init__": {"executed_lines": [35, 36, 37, 38, 39, 40, 41, 44, 47, 50, 51, 53, 54, 57, 60], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [19], "start_line": 15, "executed_branches": [[50, 51], [50, 57], [53, 54], [53, 57]], "missing_branches": []}, "RenderableImage.origin": {"executed_lines": [65], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [64], "start_line": 63, "executed_branches": [], "missing_branches": []}, "RenderableImage.size": {"executed_lines": [70], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [69], "start_line": 68, "executed_branches": [], "missing_branches": []}, "RenderableImage.width": {"executed_lines": [75], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [74], "start_line": 73, "executed_branches": [], "missing_branches": []}, "RenderableImage.set_origin": {"executed_lines": [79], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [78], "start_line": 77, "executed_branches": [], "missing_branches": []}, "RenderableImage._load_image": {"executed_lines": [83, 85, 91, 94, 96, 97, 98, 100, 101, 102, 104, 105, 106, 107, 109, 110, 111, 113], "summary": {"covered_lines": 18, "num_statements": 23, "percent_covered": 80.64516129032258, "percent_covered_display": "81", "missing_lines": 5, "excluded_lines": 1, "percent_statements_covered": 78.26086956521739, "percent_statements_covered_display": "78", "num_branches": 8, "num_partial_branches": 1, "covered_branches": 7, "missing_branches": 1, "percent_branches_covered": 87.5, "percent_branches_covered_display": "88"}, "missing_lines": [88, 89, 115, 116, 117], "excluded_lines": [82], "start_line": 81, "executed_branches": [[85, 91], [94, 96], [94, 98], [98, 100], [98, 113], [105, 106], [105, 109]], "missing_branches": [[85, 88]]}, "RenderableImage.render": {"executed_lines": [123, 125, 128, 129, 132, 133, 134, 135, 137, 140, 141, 142, 143, 145, 148, 149, 153, 163], "summary": {"covered_lines": 18, "num_statements": 18, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 10, "num_partial_branches": 0, "covered_branches": 10, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [120], "start_line": 119, "executed_branches": [[123, 125], [123, 163], [132, 133], [132, 134], [134, 135], [134, 137], [140, 141], [140, 142], [142, 143], [142, 145]], "missing_branches": []}, "RenderableImage._resize_image": {"executed_lines": [172, 173, 176, 179, 180, 183, 186, 187, 190, 193, 194, 197, 201, 204], "summary": {"covered_lines": 14, "num_statements": 15, "percent_covered": 89.47368421052632, "percent_covered_display": "89", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 93.33333333333333, "percent_statements_covered_display": "93", "num_branches": 4, "num_partial_branches": 1, "covered_branches": 3, "missing_branches": 1, "percent_branches_covered": 75.0, "percent_branches_covered_display": "75"}, "missing_lines": [198], "excluded_lines": [166], "start_line": 165, "executed_branches": [[172, 173], [172, 176], [197, 201]], "missing_branches": [[197, 198]]}, "RenderableImage._draw_error_placeholder": {"executed_lines": [211, 212, 213, 214, 216, 218, 222, 223, 226, 227, 229, 232, 235, 236, 237, 239, 240, 241, 242, 244, 245, 247, 248, 250, 251, 254, 255, 256, 257, 258, 261, 264, 267], "summary": {"covered_lines": 33, "num_statements": 35, "percent_covered": 91.11111111111111, "percent_covered_display": "91", "missing_lines": 2, "excluded_lines": 1, "percent_statements_covered": 94.28571428571429, "percent_statements_covered_display": "94", "num_branches": 10, "num_partial_branches": 2, "covered_branches": 8, "missing_branches": 2, "percent_branches_covered": 80.0, "percent_branches_covered_display": "80"}, "missing_lines": [269, 271], "excluded_lines": [207], "start_line": 206, "executed_branches": [[226, 227], [239, 240], [239, 250], [244, 245], [244, 247], [250, 251], [255, -206], [255, 256]], "missing_branches": [[226, -206], [250, 254]]}, "RenderableImage.in_object": {"executed_lines": [275, 276, 279], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [274], "start_line": 273, "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [1, 2, 3, 4, 5, 6, 7, 10, 15, 62, 63, 67, 68, 72, 73, 77, 81, 119, 165, 206, 273], "summary": {"covered_lines": 21, "num_statements": 21, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [11], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"RenderableImage": {"executed_lines": [35, 36, 37, 38, 39, 40, 41, 44, 47, 50, 51, 53, 54, 57, 60, 65, 70, 75, 79, 83, 85, 91, 94, 96, 97, 98, 100, 101, 102, 104, 105, 106, 107, 109, 110, 111, 113, 123, 125, 128, 129, 132, 133, 134, 135, 137, 140, 141, 142, 143, 145, 148, 149, 153, 163, 172, 173, 176, 179, 180, 183, 186, 187, 190, 193, 194, 197, 201, 204, 211, 212, 213, 214, 216, 218, 222, 223, 226, 227, 229, 232, 235, 236, 237, 239, 240, 241, 242, 244, 245, 247, 248, 250, 251, 254, 255, 256, 257, 258, 261, 264, 267, 275, 276, 279], "summary": {"covered_lines": 105, "num_statements": 113, "percent_covered": 91.94630872483222, "percent_covered_display": "92", "missing_lines": 8, "excluded_lines": 10, "percent_statements_covered": 92.92035398230088, "percent_statements_covered_display": "93", "num_branches": 36, "num_partial_branches": 4, "covered_branches": 32, "missing_branches": 4, "percent_branches_covered": 88.88888888888889, "percent_branches_covered_display": "89"}, "missing_lines": [88, 89, 115, 116, 117, 198, 269, 271], "excluded_lines": [19, 64, 69, 74, 78, 82, 120, 166, 207, 274], "start_line": 10, "executed_branches": [[50, 51], [50, 57], [53, 54], [53, 57], [85, 91], [94, 96], [94, 98], [98, 100], [98, 113], [105, 106], [105, 109], [123, 125], [123, 163], [132, 133], [132, 134], [134, 135], [134, 137], [140, 141], [140, 142], [142, 143], [142, 145], [172, 173], [172, 176], [197, 201], [226, 227], [239, 240], [239, 250], [244, 245], [244, 247], [250, 251], [255, -206], [255, 256]], "missing_branches": [[85, 88], [197, 198], [226, -206], [250, 254]]}, "": {"executed_lines": [1, 2, 3, 4, 5, 6, 7, 10, 15, 62, 63, 67, 68, 72, 73, 77, 81, 119, 165, 206, 273], "summary": {"covered_lines": 21, "num_statements": 21, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [11], "start_line": 1, "executed_branches": [], "missing_branches": []}}}, "pyWebLayout/concrete/interaction_handler.py": {"executed_lines": [8, 9, 10, 11, 13, 14, 17, 42, 53, 70, 87, 96, 138, 181, 189, 196, 197, 198, 200, 214, 216, 218, 220, 221, 222, 223, 227, 228, 231, 236, 237, 239, 240, 242, 244, 254, 256, 257, 259, 260, 261, 262, 263, 267, 282, 283, 286, 287, 288, 292, 293, 295, 297, 299, 301, 305, 307, 309, 310], "summary": {"covered_lines": 59, "num_statements": 99, "percent_covered": 55.39568345323741, "percent_covered_display": "55", "missing_lines": 40, "excluded_lines": 14, "percent_statements_covered": 59.5959595959596, "percent_statements_covered_display": "60", "num_branches": 40, "num_partial_branches": 8, "covered_branches": 18, "missing_branches": 22, "percent_branches_covered": 45.0, "percent_branches_covered_display": "45"}, "missing_lines": [50, 51, 61, 63, 64, 65, 67, 78, 80, 81, 82, 84, 94, 119, 120, 123, 126, 127, 128, 129, 130, 133, 134, 136, 158, 159, 162, 163, 164, 165, 166, 167, 171, 172, 176, 178, 224, 233, 265, 303], "excluded_lines": [1, 18, 43, 54, 71, 88, 101, 142, 182, 190, 201, 245, 272, 300], "executed_branches": [[216, 218], [216, 227], [218, 220], [220, 221], [228, 231], [228, 242], [231, 236], [236, 237], [256, 257], [256, 259], [260, 261], [282, 283], [282, 286], [287, 288], [292, 293], [301, 305], [305, 307], [305, 309]], "missing_branches": [[61, 63], [61, 67], [63, 64], [63, 65], [78, 80], [78, 84], [80, 81], [80, 82], [127, 128], [127, 129], [129, 130], [129, 133], [164, 165], [164, 166], [218, 224], [220, 222], [231, 233], [236, 239], [260, 265], [287, 292], [292, 295], [301, 303]], "functions": {"InteractionHandler.__init__": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 2, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 2, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [50, 51], "excluded_lines": [43], "start_line": 42, "executed_branches": [], "missing_branches": []}, "InteractionHandler.set_pressed_state": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 5, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 5, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 4, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 4, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [61, 63, 64, 65, 67], "excluded_lines": [54], "start_line": 53, "executed_branches": [], "missing_branches": [[61, 63], [61, 67], [63, 64], [63, 65]]}, "InteractionHandler.set_hovered_state": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 5, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 5, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 4, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 4, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [78, 80, 81, 82, 84], "excluded_lines": [71], "start_line": 70, "executed_branches": [], "missing_branches": [[78, 80], [78, 84], [80, 81], [80, 82]]}, "InteractionHandler.render_current_state": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [94], "excluded_lines": [88], "start_line": 87, "executed_branches": [], "missing_branches": []}, "InteractionHandler.execute_with_feedback": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 11, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 11, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 4, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 4, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [119, 120, 123, 126, 127, 128, 129, 130, 133, 134, 136], "excluded_lines": [101], "start_line": 96, "executed_branches": [], "missing_branches": [[127, 128], [127, 129], [129, 130], [129, 133]]}, "InteractionHandler.execute_async_with_feedback": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 7, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 7, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [158, 159, 162, 171, 172, 176, 178], "excluded_lines": [142], "start_line": 138, "executed_branches": [], "missing_branches": []}, "InteractionHandler.execute_async_with_feedback.execute_callback": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 5, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 5, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 2, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [163, 164, 165, 166, 167], "excluded_lines": [], "start_line": 162, "executed_branches": [], "missing_branches": [[164, 165], [164, 166]]}, "InteractionStateManager.__init__": {"executed_lines": [196, 197, 198], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [190], "start_line": 189, "executed_branches": [], "missing_branches": []}, "InteractionStateManager.update_hover": {"executed_lines": [214, 216, 218, 220, 221, 222, 223, 227, 228, 231, 236, 237, 239, 240, 242], "summary": {"covered_lines": 15, "num_statements": 17, "percent_covered": 79.3103448275862, "percent_covered_display": "79", "missing_lines": 2, "excluded_lines": 1, "percent_statements_covered": 88.23529411764706, "percent_statements_covered_display": "88", "num_branches": 12, "num_partial_branches": 4, "covered_branches": 8, "missing_branches": 4, "percent_branches_covered": 66.66666666666667, "percent_branches_covered_display": "67"}, "missing_lines": [224, 233], "excluded_lines": [201], "start_line": 200, "executed_branches": [[216, 218], [216, 227], [218, 220], [220, 221], [228, 231], [228, 242], [231, 236], [236, 237]], "missing_branches": [[218, 224], [220, 222], [231, 233], [236, 239]]}, "InteractionStateManager.handle_mouse_down": {"executed_lines": [254, 256, 257, 259, 260, 261, 262, 263], "summary": {"covered_lines": 8, "num_statements": 9, "percent_covered": 84.61538461538461, "percent_covered_display": "85", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 88.88888888888889, "percent_statements_covered_display": "89", "num_branches": 4, "num_partial_branches": 1, "covered_branches": 3, "missing_branches": 1, "percent_branches_covered": 75.0, "percent_branches_covered_display": "75"}, "missing_lines": [265], "excluded_lines": [245], "start_line": 244, "executed_branches": [[256, 257], [256, 259], [260, 261]], "missing_branches": [[260, 265]]}, "InteractionStateManager.handle_mouse_up": {"executed_lines": [282, 283, 286, 287, 288, 292, 293, 295, 297], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 86.66666666666667, "percent_covered_display": "87", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 6, "num_partial_branches": 2, "covered_branches": 4, "missing_branches": 2, "percent_branches_covered": 66.66666666666667, "percent_branches_covered_display": "67"}, "missing_lines": [], "excluded_lines": [272], "start_line": 267, "executed_branches": [[282, 283], [282, 286], [287, 288], [292, 293]], "missing_branches": [[287, 292], [292, 295]]}, "InteractionStateManager.reset": {"executed_lines": [301, 305, 307, 309, 310], "summary": {"covered_lines": 5, "num_statements": 6, "percent_covered": 80.0, "percent_covered_display": "80", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 83.33333333333333, "percent_statements_covered_display": "83", "num_branches": 4, "num_partial_branches": 1, "covered_branches": 3, "missing_branches": 1, "percent_branches_covered": 75.0, "percent_branches_covered_display": "75"}, "missing_lines": [303], "excluded_lines": [300], "start_line": 299, "executed_branches": [[301, 305], [305, 307], [305, 309]], "missing_branches": [[301, 303]]}, "": {"executed_lines": [8, 9, 10, 11, 13, 14, 17, 42, 53, 70, 87, 96, 138, 181, 189, 200, 244, 267, 299], "summary": {"covered_lines": 19, "num_statements": 19, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 3, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1, 18, 182], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"InteractionHandler": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 36, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 36, "excluded_lines": 6, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 14, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 14, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [50, 51, 61, 63, 64, 65, 67, 78, 80, 81, 82, 84, 94, 119, 120, 123, 126, 127, 128, 129, 130, 133, 134, 136, 158, 159, 162, 163, 164, 165, 166, 167, 171, 172, 176, 178], "excluded_lines": [43, 54, 71, 88, 101, 142], "start_line": 17, "executed_branches": [], "missing_branches": [[61, 63], [61, 67], [63, 64], [63, 65], [78, 80], [78, 84], [80, 81], [80, 82], [127, 128], [127, 129], [129, 130], [129, 133], [164, 165], [164, 166]]}, "InteractionStateManager": {"executed_lines": [196, 197, 198, 214, 216, 218, 220, 221, 222, 223, 227, 228, 231, 236, 237, 239, 240, 242, 254, 256, 257, 259, 260, 261, 262, 263, 282, 283, 286, 287, 288, 292, 293, 295, 297, 301, 305, 307, 309, 310], "summary": {"covered_lines": 40, "num_statements": 44, "percent_covered": 82.85714285714286, "percent_covered_display": "83", "missing_lines": 4, "excluded_lines": 5, "percent_statements_covered": 90.9090909090909, "percent_statements_covered_display": "91", "num_branches": 26, "num_partial_branches": 8, "covered_branches": 18, "missing_branches": 8, "percent_branches_covered": 69.23076923076923, "percent_branches_covered_display": "69"}, "missing_lines": [224, 233, 265, 303], "excluded_lines": [190, 201, 245, 272, 300], "start_line": 181, "executed_branches": [[216, 218], [216, 227], [218, 220], [220, 221], [228, 231], [228, 242], [231, 236], [236, 237], [256, 257], [256, 259], [260, 261], [282, 283], [282, 286], [287, 288], [292, 293], [301, 305], [305, 307], [305, 309]], "missing_branches": [[218, 224], [220, 222], [231, 233], [236, 239], [260, 265], [287, 292], [292, 295], [301, 303]]}, "": {"executed_lines": [8, 9, 10, 11, 13, 14, 17, 42, 53, 70, 87, 96, 138, 181, 189, 200, 244, 267, 299], "summary": {"covered_lines": 19, "num_statements": 19, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 3, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1, 18, 182], "start_line": 1, "executed_branches": [], "missing_branches": []}}}, "pyWebLayout/concrete/page.py": {"executed_lines": [1, 2, 3, 5, 6, 7, 8, 11, 20, 22, 33, 34, 35, 36, 37, 38, 39, 43, 45, 47, 49, 51, 57, 59, 76, 77, 80, 81, 85, 88, 90, 91, 93, 95, 96, 100, 101, 106, 111, 112, 114, 115, 117, 118, 120, 121, 123, 124, 126, 127, 132, 133, 135, 136, 141, 142, 144, 146, 147, 149, 151, 152, 154, 156, 157, 159, 161, 162, 166, 170, 174, 175, 183, 185, 186, 187, 189, 190, 203, 204, 205, 206, 208, 218, 219, 221, 222, 224, 234, 235, 236, 237, 238, 239, 241, 248, 249, 251, 253, 254, 256, 257, 259, 261, 266, 268, 269, 271, 273, 274, 276, 284, 285, 288, 291, 293, 295, 303, 306, 307, 308, 311, 312, 317, 319, 330, 333, 335, 337, 338, 339, 340, 341, 343, 346, 349, 355, 366, 367, 370, 371, 372, 380, 381, 389, 398, 399, 406, 412, 425, 426, 428, 429, 431, 435, 437, 438, 439, 441, 442, 444, 445, 446, 448, 449, 450, 452, 454, 464], "summary": {"covered_lines": 169, "num_statements": 176, "percent_covered": 94.54545454545455, "percent_covered_display": "95", "missing_lines": 7, "excluded_lines": 31, "percent_statements_covered": 96.02272727272727, "percent_statements_covered_display": "96", "num_branches": 44, "num_partial_branches": 5, "covered_branches": 39, "missing_branches": 5, "percent_branches_covered": 88.63636363636364, "percent_branches_covered_display": "89"}, "missing_lines": [98, 164, 168, 172, 272, 390, 432], "excluded_lines": [12, 24, 52, 64, 92, 97, 102, 113, 119, 125, 134, 143, 148, 153, 158, 163, 167, 171, 176, 191, 209, 225, 242, 258, 262, 277, 296, 320, 356, 414, 455], "executed_branches": [[80, 81], [80, 85], [183, 185], [183, 187], [203, 204], [203, 206], [266, -261], [266, 268], [268, 269], [268, 271], [271, 273], [273, 274], [306, 307], [306, 317], [333, 335], [333, 349], [335, 333], [335, 337], [337, 338], [337, 346], [339, 340], [339, 343], [380, 381], [380, 389], [389, 398], [398, 399], [398, 406], [431, 435], [437, 438], [437, 452], [438, 439], [439, 437], [439, 441], [441, 442], [441, 444], [444, 445], [444, 448], [448, 439], [448, 449]], "missing_branches": [[271, 272], [273, 266], [389, 390], [431, 432], [438, 437]], "functions": {"Page.__init__": {"executed_lines": [33, 34, 35, 36, 37, 38, 39, 43, 45, 47, 49], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [24], "start_line": 22, "executed_branches": [], "missing_branches": []}, "Page.free_space": {"executed_lines": [57], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [52], "start_line": 51, "executed_branches": [], "missing_branches": []}, "Page.can_fit_line": {"executed_lines": [76, 77, 80, 81, 85, 88], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [64], "start_line": 59, "executed_branches": [[80, 81], [80, 85]], "missing_branches": []}, "Page.size": {"executed_lines": [93], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [92], "start_line": 91, "executed_branches": [], "missing_branches": []}, "Page.origin": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [98], "excluded_lines": [97], "start_line": 96, "executed_branches": [], "missing_branches": []}, "Page.content_origin": {"executed_lines": [106], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [102], "start_line": 101, "executed_branches": [], "missing_branches": []}, "Page.content_rect": {"executed_lines": [114, 115], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [113], "start_line": 112, "executed_branches": [], "missing_branches": []}, "Page.remaining_height": {"executed_lines": [120, 121], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [119], "start_line": 118, "executed_branches": [], "missing_branches": []}, "Page.canvas_size": {"executed_lines": [126, 127], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [125], "start_line": 124, "executed_branches": [], "missing_branches": []}, "Page.content_size": {"executed_lines": [135, 136], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [134], "start_line": 133, "executed_branches": [], "missing_branches": []}, "Page.border_size": {"executed_lines": [144], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [143], "start_line": 142, "executed_branches": [], "missing_branches": []}, "Page.available_width": {"executed_lines": [149], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [148], "start_line": 147, "executed_branches": [], "missing_branches": []}, "Page.style": {"executed_lines": [154], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [153], "start_line": 152, "executed_branches": [], "missing_branches": []}, "Page.callbacks": {"executed_lines": [159], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [158], "start_line": 157, "executed_branches": [], "missing_branches": []}, "Page.is_dirty": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [164], "excluded_lines": [163], "start_line": 162, "executed_branches": [], "missing_branches": []}, "Page.mark_dirty": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [168], "excluded_lines": [167], "start_line": 166, "executed_branches": [], "missing_branches": []}, "Page.mark_clean": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [172], "excluded_lines": [171], "start_line": 170, "executed_branches": [], "missing_branches": []}, "Page.draw": {"executed_lines": [183, 185, 186, 187], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [176], "start_line": 175, "executed_branches": [[183, 185], [183, 187]], "missing_branches": []}, "Page.measurement_draw": {"executed_lines": [203, 204, 205, 206], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [191], "start_line": 190, "executed_branches": [[203, 204], [203, 206]], "missing_branches": []}, "Page.add_child": {"executed_lines": [218, 219, 221, 222], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [209], "start_line": 208, "executed_branches": [], "missing_branches": []}, "Page.remove_child": {"executed_lines": [234, 235, 236, 237, 238, 239], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [225], "start_line": 224, "executed_branches": [], "missing_branches": []}, "Page.clear_children": {"executed_lines": [248, 249, 251, 253, 254], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [242], "start_line": 241, "executed_branches": [], "missing_branches": []}, "Page.children": {"executed_lines": [259], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [258], "start_line": 257, "executed_branches": [], "missing_branches": []}, "Page.render_children": {"executed_lines": [266, 268, 269, 271, 273, 274], "summary": {"covered_lines": 6, "num_statements": 7, "percent_covered": 80.0, "percent_covered_display": "80", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 85.71428571428571, "percent_statements_covered_display": "86", "num_branches": 8, "num_partial_branches": 2, "covered_branches": 6, "missing_branches": 2, "percent_branches_covered": 75.0, "percent_branches_covered_display": "75"}, "missing_lines": [272], "excluded_lines": [262], "start_line": 261, "executed_branches": [[266, -261], [266, 268], [268, 269], [268, 271], [271, 273], [273, 274]], "missing_branches": [[271, 272], [273, 266]]}, "Page.render": {"executed_lines": [284, 285, 288, 291, 293], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [277], "start_line": 276, "executed_branches": [], "missing_branches": []}, "Page._create_canvas": {"executed_lines": [303, 306, 307, 308, 311, 312, 317], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [296], "start_line": 295, "executed_branches": [[306, 307], [306, 317]], "missing_branches": []}, "Page.query_point": {"executed_lines": [330, 333, 335, 337, 338, 339, 340, 341, 343, 346, 349], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 8, "num_partial_branches": 0, "covered_branches": 8, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [320], "start_line": 319, "executed_branches": [[333, 335], [333, 349], [335, 333], [335, 337], [337, 338], [337, 346], [339, 340], [339, 343]], "missing_branches": []}, "Page._make_query_result": {"executed_lines": [366, 367, 370, 371, 372, 380, 381, 389, 398, 399, 406], "summary": {"covered_lines": 11, "num_statements": 12, "percent_covered": 88.88888888888889, "percent_covered_display": "89", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 91.66666666666667, "percent_statements_covered_display": "92", "num_branches": 6, "num_partial_branches": 1, "covered_branches": 5, "missing_branches": 1, "percent_branches_covered": 83.33333333333333, "percent_branches_covered_display": "83"}, "missing_lines": [390], "excluded_lines": [356], "start_line": 355, "executed_branches": [[380, 381], [380, 389], [389, 398], [398, 399], [398, 406]], "missing_branches": [[389, 390]]}, "Page.query_range": {"executed_lines": [425, 426, 428, 429, 431, 435, 437, 438, 439, 441, 442, 444, 445, 446, 448, 449, 450, 452], "summary": {"covered_lines": 18, "num_statements": 19, "percent_covered": 90.9090909090909, "percent_covered_display": "91", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 94.73684210526316, "percent_statements_covered_display": "95", "num_branches": 14, "num_partial_branches": 2, "covered_branches": 12, "missing_branches": 2, "percent_branches_covered": 85.71428571428571, "percent_branches_covered_display": "86"}, "missing_lines": [432], "excluded_lines": [414], "start_line": 412, "executed_branches": [[431, 435], [437, 438], [437, 452], [438, 439], [439, 437], [439, 441], [441, 442], [441, 444], [444, 445], [444, 448], [448, 439], [448, 449]], "missing_branches": [[431, 432], [438, 437]]}, "Page.in_object": {"executed_lines": [464], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [455], "start_line": 454, "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [1, 2, 3, 5, 6, 7, 8, 11, 20, 22, 51, 59, 90, 91, 95, 96, 100, 101, 111, 112, 117, 118, 123, 124, 132, 133, 141, 142, 146, 147, 151, 152, 156, 157, 161, 162, 166, 170, 174, 175, 189, 190, 208, 224, 241, 256, 257, 261, 276, 295, 319, 355, 412, 454], "summary": {"covered_lines": 54, "num_statements": 54, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [12], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"Page": {"executed_lines": [33, 34, 35, 36, 37, 38, 39, 43, 45, 47, 49, 57, 76, 77, 80, 81, 85, 88, 93, 106, 114, 115, 120, 121, 126, 127, 135, 136, 144, 149, 154, 159, 183, 185, 186, 187, 203, 204, 205, 206, 218, 219, 221, 222, 234, 235, 236, 237, 238, 239, 248, 249, 251, 253, 254, 259, 266, 268, 269, 271, 273, 274, 284, 285, 288, 291, 293, 303, 306, 307, 308, 311, 312, 317, 330, 333, 335, 337, 338, 339, 340, 341, 343, 346, 349, 366, 367, 370, 371, 372, 380, 381, 389, 398, 399, 406, 425, 426, 428, 429, 431, 435, 437, 438, 439, 441, 442, 444, 445, 446, 448, 449, 450, 452, 464], "summary": {"covered_lines": 115, "num_statements": 122, "percent_covered": 92.7710843373494, "percent_covered_display": "93", "missing_lines": 7, "excluded_lines": 30, "percent_statements_covered": 94.26229508196721, "percent_statements_covered_display": "94", "num_branches": 44, "num_partial_branches": 5, "covered_branches": 39, "missing_branches": 5, "percent_branches_covered": 88.63636363636364, "percent_branches_covered_display": "89"}, "missing_lines": [98, 164, 168, 172, 272, 390, 432], "excluded_lines": [24, 52, 64, 92, 97, 102, 113, 119, 125, 134, 143, 148, 153, 158, 163, 167, 171, 176, 191, 209, 225, 242, 258, 262, 277, 296, 320, 356, 414, 455], "start_line": 11, "executed_branches": [[80, 81], [80, 85], [183, 185], [183, 187], [203, 204], [203, 206], [266, -261], [266, 268], [268, 269], [268, 271], [271, 273], [273, 274], [306, 307], [306, 317], [333, 335], [333, 349], [335, 333], [335, 337], [337, 338], [337, 346], [339, 340], [339, 343], [380, 381], [380, 389], [389, 398], [398, 399], [398, 406], [431, 435], [437, 438], [437, 452], [438, 439], [439, 437], [439, 441], [441, 442], [441, 444], [444, 445], [444, 448], [448, 439], [448, 449]], "missing_branches": [[271, 272], [273, 266], [389, 390], [431, 432], [438, 437]]}, "": {"executed_lines": [1, 2, 3, 5, 6, 7, 8, 11, 20, 22, 51, 59, 90, 91, 95, 96, 100, 101, 111, 112, 117, 118, 123, 124, 132, 133, 141, 142, 146, 147, 151, 152, 156, 157, 161, 162, 166, 170, 174, 175, 189, 190, 208, 224, 241, 256, 257, 261, 276, 295, 319, 355, 412, 454], "summary": {"covered_lines": 54, "num_statements": 54, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [12], "start_line": 1, "executed_branches": [], "missing_branches": []}}}, "pyWebLayout/concrete/table.py": {"executed_lines": [10, 11, 12, 13, 15, 16, 17, 18, 21, 22, 26, 27, 30, 33, 34, 37, 38, 41, 44, 50, 72, 73, 74, 75, 76, 77, 78, 80, 83, 84, 86, 89, 90, 91, 99, 100, 101, 102, 103, 106, 108, 110, 112, 113, 114, 116, 117, 120, 121, 122, 123, 125, 132, 133, 134, 137, 138, 141, 142, 144, 146, 148, 150, 151, 153, 158, 159, 161, 164, 167, 170, 171, 174, 175, 177, 179, 183, 193, 194, 195, 198, 199, 201, 202, 203, 211, 221, 222, 223, 225, 229, 244, 252, 254, 255, 256, 268, 272, 276, 277, 278, 280, 282, 283, 285, 289, 292, 293, 296, 323, 329, 336, 341, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 375, 377, 378, 381, 382, 383, 384, 387, 389, 390, 393, 402, 403, 405, 407, 410, 416, 435, 436, 437, 438, 439, 442, 443, 445, 448, 449, 451, 460, 462, 464, 465, 468, 475, 480, 484, 487, 491, 497, 499, 515, 516, 517, 520, 521, 522, 523, 524, 526, 528, 529, 530, 532, 534, 535, 539, 540, 547, 549, 552, 553, 555, 556, 558, 560, 562, 566, 568, 569, 572, 574, 576, 578, 594, 595, 598, 599, 602, 604, 605, 607, 609, 612, 615, 619, 622, 623, 624, 629, 632, 637, 639, 641, 643, 644, 647, 648, 649, 652, 653, 654, 655, 656, 658, 660, 662, 672, 673, 675, 677, 679, 681, 683, 684, 686, 687, 690, 691, 692, 694, 696, 698, 699, 701, 703, 704, 706], "summary": {"covered_lines": 250, "num_statements": 303, "percent_covered": 78.0246913580247, "percent_covered_display": "78", "missing_lines": 53, "excluded_lines": 19, "percent_statements_covered": 82.50825082508251, "percent_statements_covered_display": "83", "num_branches": 102, "num_partial_branches": 24, "covered_branches": 66, "missing_branches": 36, "percent_branches_covered": 64.70588235294117, "percent_branches_covered_display": "65"}, "missing_lines": [154, 162, 180, 207, 208, 214, 217, 218, 226, 231, 232, 233, 234, 235, 237, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 269, 286, 294, 299, 306, 307, 308, 310, 311, 313, 314, 315, 316, 317, 318, 324, 331, 333, 477, 536, 542, 544, 563, 610, 626, 627, 634, 635], "excluded_lines": [1, 23, 45, 60, 81, 111, 246, 337, 351, 376, 411, 424, 452, 504, 583, 642, 680, 700, 705], "executed_branches": [[83, 84], [83, 86], [122, 123], [122, 125], [141, 142], [141, 229], [142, 144], [142, 146], [146, 148], [153, 158], [159, 161], [159, 174], [161, 164], [177, 179], [177, 225], [179, 183], [194, 195], [194, 221], [201, 202], [203, 211], [221, 222], [225, 141], [229, -110], [255, 256], [268, 272], [285, 289], [292, 293], [293, 296], [323, 329], [382, 383], [382, 407], [383, 384], [387, 389], [387, 393], [464, 465], [464, 468], [475, 480], [528, 529], [528, 576], [529, 530], [529, 532], [534, 535], [534, 574], [535, 539], [540, 547], [552, 553], [552, 572], [553, 555], [553, 556], [556, 558], [562, 566], [607, 609], [607, 639], [609, 612], [622, 623], [622, 632], [624, 629], [632, 637], [647, 648], [647, 652], [652, 653], [652, 677], [653, 654], [653, 655], [655, 656], [655, 658]], "missing_branches": [[146, 225], [153, 154], [161, 162], [179, 180], [201, 214], [203, 207], [214, 217], [214, 218], [221, 177], [225, 226], [229, 231], [255, 257], [257, 258], [257, 259], [259, 260], [259, 261], [261, 262], [261, 263], [263, 264], [263, 265], [265, 266], [265, 268], [268, 269], [285, 286], [292, 299], [293, 294], [323, 324], [383, 382], [475, 477], [535, 536], [540, 542], [556, 552], [562, 563], [609, 610], [624, 626], [632, 634]], "functions": {"TableCellRenderer.__init__": {"executed_lines": [72, 73, 74, 75, 76, 77, 78], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [60], "start_line": 50, "executed_branches": [], "missing_branches": []}, "TableCellRenderer.render": {"executed_lines": [83, 84, 86, 89, 90, 91, 99, 100, 101, 102, 103, 106, 108], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [81], "start_line": 80, "executed_branches": [[83, 84], [83, 86]], "missing_branches": []}, "TableCellRenderer._render_cell_content": {"executed_lines": [112, 113, 114, 116, 117, 120, 121, 122, 123, 125, 132, 133, 134, 137, 138, 141, 142, 144, 146, 148, 150, 151, 153, 158, 159, 161, 164, 167, 170, 171, 174, 175, 177, 179, 183, 193, 194, 195, 198, 199, 201, 202, 203, 211, 221, 222, 223, 225, 229], "summary": {"covered_lines": 49, "num_statements": 64, "percent_covered": 72.91666666666667, "percent_covered_display": "73", "missing_lines": 15, "excluded_lines": 1, "percent_statements_covered": 76.5625, "percent_statements_covered_display": "77", "num_branches": 32, "num_partial_branches": 9, "covered_branches": 21, "missing_branches": 11, "percent_branches_covered": 65.625, "percent_branches_covered_display": "66"}, "missing_lines": [154, 162, 180, 207, 208, 214, 217, 218, 226, 231, 232, 233, 234, 235, 237], "excluded_lines": [111], "start_line": 110, "executed_branches": [[122, 123], [122, 125], [141, 142], [141, 229], [142, 144], [142, 146], [146, 148], [153, 158], [159, 161], [159, 174], [161, 164], [177, 179], [177, 225], [179, 183], [194, 195], [194, 221], [201, 202], [203, 211], [221, 222], [225, 141], [229, -110]], "missing_branches": [[146, 225], [153, 154], [161, 162], [179, 180], [201, 214], [203, 207], [214, 217], [214, 218], [221, 177], [225, 226], [229, 231]]}, "TableCellRenderer._render_image_in_cell": {"executed_lines": [252, 254, 255, 256, 268, 272, 276, 277, 278, 280, 282, 283, 285, 289, 292, 293, 296, 323, 329], "summary": {"covered_lines": 19, "num_statements": 47, "percent_covered": 36.231884057971016, "percent_covered_display": "36", "missing_lines": 28, "excluded_lines": 1, "percent_statements_covered": 40.42553191489362, "percent_statements_covered_display": "40", "num_branches": 22, "num_partial_branches": 6, "covered_branches": 6, "missing_branches": 16, "percent_branches_covered": 27.272727272727273, "percent_branches_covered_display": "27"}, "missing_lines": [257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 269, 286, 294, 299, 306, 307, 308, 310, 311, 313, 314, 315, 316, 317, 318, 324, 331, 333], "excluded_lines": [246], "start_line": 244, "executed_branches": [[255, 256], [268, 272], [285, 289], [292, 293], [293, 296], [323, 329]], "missing_branches": [[255, 257], [257, 258], [257, 259], [259, 260], [259, 261], [261, 262], [261, 263], [263, 264], [263, 265], [265, 266], [265, 268], [268, 269], [285, 286], [292, 299], [293, 294], [323, 324]]}, "TableRowRenderer.__init__": {"executed_lines": [364, 365, 366, 367, 368, 369, 370, 371, 372, 373], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [351], "start_line": 341, "executed_branches": [], "missing_branches": []}, "TableRowRenderer.render": {"executed_lines": [377, 378, 381, 382, 383, 384, 387, 389, 390, 393, 402, 403, 405, 407], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 95.0, "percent_covered_display": "95", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 6, "num_partial_branches": 1, "covered_branches": 5, "missing_branches": 1, "percent_branches_covered": 83.33333333333333, "percent_branches_covered_display": "83"}, "missing_lines": [], "excluded_lines": [376], "start_line": 375, "executed_branches": [[382, 383], [382, 407], [383, 384], [387, 389], [387, 393]], "missing_branches": [[383, 382]]}, "TableRenderer.__init__": {"executed_lines": [435, 436, 437, 438, 439, 442, 443, 445, 448, 449], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [424], "start_line": 416, "executed_branches": [], "missing_branches": []}, "TableRenderer._calculate_dimensions": {"executed_lines": [460, 462, 464, 465, 468, 475, 480, 484, 487, 491, 497], "summary": {"covered_lines": 11, "num_statements": 12, "percent_covered": 87.5, "percent_covered_display": "88", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 91.66666666666667, "percent_statements_covered_display": "92", "num_branches": 4, "num_partial_branches": 1, "covered_branches": 3, "missing_branches": 1, "percent_branches_covered": 75.0, "percent_branches_covered_display": "75"}, "missing_lines": [477], "excluded_lines": [452], "start_line": 451, "executed_branches": [[464, 465], [464, 468], [475, 480]], "missing_branches": [[475, 477]]}, "TableRenderer._calculate_row_height_for_section": {"executed_lines": [515, 516, 517, 520, 521, 522, 523, 524, 526, 528, 529, 530, 532, 534, 535, 539, 540, 547, 549, 552, 553, 555, 556, 558, 560, 562, 566, 568, 569, 572, 574, 576], "summary": {"covered_lines": 32, "num_statements": 36, "percent_covered": 85.18518518518519, "percent_covered_display": "85", "missing_lines": 4, "excluded_lines": 1, "percent_statements_covered": 88.88888888888889, "percent_statements_covered_display": "89", "num_branches": 18, "num_partial_branches": 4, "covered_branches": 14, "missing_branches": 4, "percent_branches_covered": 77.77777777777777, "percent_branches_covered_display": "78"}, "missing_lines": [536, 542, 544, 563], "excluded_lines": [504], "start_line": 499, "executed_branches": [[528, 529], [528, 576], [529, 530], [529, 532], [534, 535], [534, 574], [535, 539], [540, 547], [552, 553], [552, 572], [553, 555], [553, 556], [556, 558], [562, 566]], "missing_branches": [[535, 536], [540, 542], [556, 552], [562, 563]]}, "TableRenderer._estimate_wrapped_lines": {"executed_lines": [594, 595, 598, 599, 602, 604, 605, 607, 609, 612, 615, 619, 622, 623, 624, 629, 632, 637, 639], "summary": {"covered_lines": 19, "num_statements": 24, "percent_covered": 76.47058823529412, "percent_covered_display": "76", "missing_lines": 5, "excluded_lines": 1, "percent_statements_covered": 79.16666666666667, "percent_statements_covered_display": "79", "num_branches": 10, "num_partial_branches": 3, "covered_branches": 7, "missing_branches": 3, "percent_branches_covered": 70.0, "percent_branches_covered_display": "70"}, "missing_lines": [610, 626, 627, 634, 635], "excluded_lines": [583], "start_line": 578, "executed_branches": [[607, 609], [607, 639], [609, 612], [622, 623], [622, 632], [624, 629], [632, 637]], "missing_branches": [[609, 610], [624, 626], [632, 634]]}, "TableRenderer.render": {"executed_lines": [643, 644, 647, 648, 649, 652, 653, 654, 655, 656, 658, 660, 662, 672, 673, 675, 677], "summary": {"covered_lines": 17, "num_statements": 17, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 8, "num_partial_branches": 0, "covered_branches": 8, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [642], "start_line": 641, "executed_branches": [[647, 648], [647, 652], [652, 653], [652, 677], [653, 654], [653, 655], [655, 656], [655, 658]], "missing_branches": []}, "TableRenderer._render_caption": {"executed_lines": [681, 683, 684, 686, 687, 690, 691, 692, 694, 696], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [680], "start_line": 679, "executed_branches": [], "missing_branches": []}, "TableRenderer.height": {"executed_lines": [701], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [700], "start_line": 699, "executed_branches": [], "missing_branches": []}, "TableRenderer.width": {"executed_lines": [706], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [705], "start_line": 704, "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [10, 11, 12, 13, 15, 16, 17, 18, 21, 22, 26, 27, 30, 33, 34, 37, 38, 41, 44, 50, 80, 110, 244, 336, 341, 375, 410, 416, 451, 499, 578, 641, 679, 698, 699, 703, 704], "summary": {"covered_lines": 37, "num_statements": 37, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 5, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1, 23, 45, 337, 411], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"TableStyle": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 22, "executed_branches": [], "missing_branches": []}, "TableCellRenderer": {"executed_lines": [72, 73, 74, 75, 76, 77, 78, 83, 84, 86, 89, 90, 91, 99, 100, 101, 102, 103, 106, 108, 112, 113, 114, 116, 117, 120, 121, 122, 123, 125, 132, 133, 134, 137, 138, 141, 142, 144, 146, 148, 150, 151, 153, 158, 159, 161, 164, 167, 170, 171, 174, 175, 177, 179, 183, 193, 194, 195, 198, 199, 201, 202, 203, 211, 221, 222, 223, 225, 229, 252, 254, 255, 256, 268, 272, 276, 277, 278, 280, 282, 283, 285, 289, 292, 293, 296, 323, 329], "summary": {"covered_lines": 88, "num_statements": 131, "percent_covered": 62.5668449197861, "percent_covered_display": "63", "missing_lines": 43, "excluded_lines": 4, "percent_statements_covered": 67.17557251908397, "percent_statements_covered_display": "67", "num_branches": 56, "num_partial_branches": 15, "covered_branches": 29, "missing_branches": 27, "percent_branches_covered": 51.785714285714285, "percent_branches_covered_display": "52"}, "missing_lines": [154, 162, 180, 207, 208, 214, 217, 218, 226, 231, 232, 233, 234, 235, 237, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 269, 286, 294, 299, 306, 307, 308, 310, 311, 313, 314, 315, 316, 317, 318, 324, 331, 333], "excluded_lines": [60, 81, 111, 246], "start_line": 44, "executed_branches": [[83, 84], [83, 86], [122, 123], [122, 125], [141, 142], [141, 229], [142, 144], [142, 146], [146, 148], [153, 158], [159, 161], [159, 174], [161, 164], [177, 179], [177, 225], [179, 183], [194, 195], [194, 221], [201, 202], [203, 211], [221, 222], [225, 141], [229, -110], [255, 256], [268, 272], [285, 289], [292, 293], [293, 296], [323, 329]], "missing_branches": [[146, 225], [153, 154], [161, 162], [179, 180], [201, 214], [203, 207], [214, 217], [214, 218], [221, 177], [225, 226], [229, 231], [255, 257], [257, 258], [257, 259], [259, 260], [259, 261], [261, 262], [261, 263], [263, 264], [263, 265], [265, 266], [265, 268], [268, 269], [285, 286], [292, 299], [293, 294], [323, 324]]}, "TableRowRenderer": {"executed_lines": [364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 377, 378, 381, 382, 383, 384, 387, 389, 390, 393, 402, 403, 405, 407], "summary": {"covered_lines": 24, "num_statements": 24, "percent_covered": 96.66666666666667, "percent_covered_display": "97", "missing_lines": 0, "excluded_lines": 2, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 6, "num_partial_branches": 1, "covered_branches": 5, "missing_branches": 1, "percent_branches_covered": 83.33333333333333, "percent_branches_covered_display": "83"}, "missing_lines": [], "excluded_lines": [351, 376], "start_line": 336, "executed_branches": [[382, 383], [382, 407], [383, 384], [387, 389], [387, 393]], "missing_branches": [[383, 382]]}, "TableRenderer": {"executed_lines": [435, 436, 437, 438, 439, 442, 443, 445, 448, 449, 460, 462, 464, 465, 468, 475, 480, 484, 487, 491, 497, 515, 516, 517, 520, 521, 522, 523, 524, 526, 528, 529, 530, 532, 534, 535, 539, 540, 547, 549, 552, 553, 555, 556, 558, 560, 562, 566, 568, 569, 572, 574, 576, 594, 595, 598, 599, 602, 604, 605, 607, 609, 612, 615, 619, 622, 623, 624, 629, 632, 637, 639, 643, 644, 647, 648, 649, 652, 653, 654, 655, 656, 658, 660, 662, 672, 673, 675, 677, 681, 683, 684, 686, 687, 690, 691, 692, 694, 696, 701, 706], "summary": {"covered_lines": 101, "num_statements": 111, "percent_covered": 88.0794701986755, "percent_covered_display": "88", "missing_lines": 10, "excluded_lines": 8, "percent_statements_covered": 90.990990990991, "percent_statements_covered_display": "91", "num_branches": 40, "num_partial_branches": 8, "covered_branches": 32, "missing_branches": 8, "percent_branches_covered": 80.0, "percent_branches_covered_display": "80"}, "missing_lines": [477, 536, 542, 544, 563, 610, 626, 627, 634, 635], "excluded_lines": [424, 452, 504, 583, 642, 680, 700, 705], "start_line": 410, "executed_branches": [[464, 465], [464, 468], [475, 480], [528, 529], [528, 576], [529, 530], [529, 532], [534, 535], [534, 574], [535, 539], [540, 547], [552, 553], [552, 572], [553, 555], [553, 556], [556, 558], [562, 566], [607, 609], [607, 639], [609, 612], [622, 623], [622, 632], [624, 629], [632, 637], [647, 648], [647, 652], [652, 653], [652, 677], [653, 654], [653, 655], [655, 656], [655, 658]], "missing_branches": [[475, 477], [535, 536], [540, 542], [556, 552], [562, 563], [609, 610], [624, 626], [632, 634]]}, "": {"executed_lines": [10, 11, 12, 13, 15, 16, 17, 18, 21, 22, 26, 27, 30, 33, 34, 37, 38, 41, 44, 50, 80, 110, 244, 336, 341, 375, 410, 416, 451, 499, 578, 641, 679, 698, 699, 703, 704], "summary": {"covered_lines": 37, "num_statements": 37, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 5, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1, 23, 45, 337, 411], "start_line": 1, "executed_branches": [], "missing_branches": []}}}, "pyWebLayout/concrete/text.py": {"executed_lines": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17, 32, 37, 44, 47, 49, 50, 51, 54, 57, 58, 59, 68, 73, 76, 106, 113, 120, 121, 126, 127, 129, 130, 134, 135, 138, 141, 151, 246, 252, 253, 281, 284, 311, 312, 314, 315, 317, 319, 325, 327, 330, 333, 334, 336, 350, 354, 355, 356, 358, 359, 361, 362, 364, 365, 367, 368, 369, 371, 373, 376, 379, 385, 386, 387, 388, 390, 391, 393, 394, 397, 398, 400, 401, 403, 404, 405, 406, 407, 408, 409, 411, 427, 429, 430, 433, 435, 436, 437, 438, 447, 448, 449, 450, 451, 455, 458, 464, 478, 479, 480, 481, 482, 483, 484, 487, 489, 495, 496, 498, 499, 500, 501, 502, 504, 505, 506, 508, 509, 510, 512, 513, 515, 517, 518, 520, 522, 523, 525, 527, 528, 530, 532, 533, 535, 537, 538, 540, 542, 543, 546, 547, 548, 550, 552, 554, 556, 558, 571, 574, 575, 580, 583, 591, 593, 594, 597, 600, 604, 606, 627, 639, 642, 648, 658, 659, 661, 676, 679, 680, 684, 687, 688, 689, 694, 695, 696, 697, 699, 700, 701, 703, 704, 706, 707, 708, 711, 713, 714, 715, 730, 736, 770, 771, 774, 775, 776, 777, 778, 779, 780, 781, 783, 784, 785, 786, 790, 793, 794, 795, 798, 804, 806, 807, 809, 811, 812, 813, 815, 816, 823, 825, 826, 828, 838, 839, 840, 841, 843, 845, 846, 848, 850, 854, 855, 857, 859, 875, 876, 878, 880, 881, 882, 884, 887, 888, 889, 893, 910, 911, 912, 913, 917, 919, 924, 931, 939, 940, 941, 943, 945, 946, 947, 948, 949, 950, 953, 956, 957, 959, 961, 962, 963, 966, 968, 972, 978, 986, 987, 988, 990, 992, 995, 997, 998, 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1010, 1012, 1013, 1014, 1016, 1018, 1020, 1022, 1027, 1028, 1031, 1032, 1035, 1039, 1040, 1042, 1048, 1056, 1057, 1059, 1061, 1062, 1063, 1064, 1065, 1066, 1067, 1070, 1073, 1075, 1086, 1087, 1088, 1089, 1090, 1092, 1095, 1100, 1101, 1102, 1103, 1104, 1105, 1107, 1109, 1110, 1113, 1116, 1119, 1121, 1122, 1123, 1125, 1136, 1139, 1141, 1143, 1144, 1149, 1150, 1158, 1160, 1161, 1170, 1181, 1188, 1189, 1191], "summary": {"covered_lines": 375, "num_statements": 462, "percent_covered": 77.05479452054794, "percent_covered_display": "77", "missing_lines": 87, "excluded_lines": 46, "percent_statements_covered": 81.16883116883118, "percent_statements_covered_display": "81", "num_branches": 122, "num_partial_branches": 13, "covered_branches": 75, "missing_branches": 47, "percent_branches_covered": 61.47540983606557, "percent_branches_covered_display": "61"}, "missing_lines": [52, 53, 93, 94, 95, 96, 97, 98, 99, 100, 102, 103, 108, 109, 110, 122, 124, 131, 132, 136, 137, 143, 183, 184, 186, 187, 188, 190, 191, 192, 194, 195, 196, 198, 199, 200, 201, 202, 203, 205, 206, 207, 208, 209, 211, 212, 213, 214, 215, 216, 217, 220, 226, 227, 229, 231, 232, 234, 235, 237, 238, 239, 241, 243, 395, 609, 611, 612, 615, 618, 622, 624, 643, 649, 677, 685, 690, 717, 719, 722, 723, 724, 727, 852, 967, 969, 1171], "excluded_lines": [48, 79, 107, 114, 142, 155, 247, 259, 282, 292, 331, 342, 377, 392, 402, 417, 459, 471, 490, 514, 519, 524, 529, 534, 539, 544, 551, 555, 559, 584, 628, 662, 731, 752, 808, 817, 829, 847, 851, 856, 860, 879, 886, 897, 1076, 1126], "executed_branches": [[126, 127], [126, 129], [311, 312], [311, 314], [354, 355], [354, 361], [355, 356], [355, 358], [368, 369], [368, 371], [393, 394], [393, 398], [394, 397], [405, 406], [405, 409], [433, 435], [433, 447], [499, 500], [499, 502], [591, 593], [600, 604], [600, 606], [642, 648], [648, 658], [658, -627], [658, 659], [676, 679], [684, 687], [689, 694], [707, 708], [707, 713], [823, 825], [823, 826], [838, 839], [838, 840], [840, 841], [840, 843], [887, 888], [887, 889], [910, 911], [910, 917], [917, 919], [917, 939], [943, 945], [943, 953], [959, 961], [959, 995], [961, 962], [961, 995], [966, 968], [968, 972], [990, 961], [990, 992], [995, 997], [995, 1010], [1010, 1012], [1010, 1073], [1016, 1018], [1020, 1022], [1035, 1039], [1059, 1061], [1059, 1070], [1087, 1088], [1087, 1092], [1107, -1075], [1107, 1109], [1122, 1107], [1122, 1123], [1139, 1141], [1139, 1191], [1141, 1139], [1141, 1143], [1160, 1161], [1160, 1170], [1170, 1181]], "missing_branches": [[93, 94], [93, 95], [95, 96], [95, 97], [97, -76], [97, 98], [98, 99], [98, 100], [100, -76], [100, 102], [183, 184], [183, 186], [186, 187], [186, 190], [190, 191], [190, 192], [195, 196], [195, 198], [205, 206], [205, 241], [206, 207], [206, 208], [208, 209], [208, 211], [213, 214], [213, 220], [215, 216], [215, 217], [226, 227], [226, 232], [394, 395], [591, 609], [609, -583], [609, 611], [618, 622], [618, 624], [642, 643], [648, 649], [676, 677], [684, 685], [689, 690], [966, 967], [968, 969], [1016, 1073], [1020, 1073], [1035, 1073], [1170, 1171]], "functions": {"_glyph_entry_bytes": {"executed_lines": [49, 50, 51, 54], "summary": {"covered_lines": 4, "num_statements": 6, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 2, "excluded_lines": 1, "percent_statements_covered": 66.66666666666667, "percent_statements_covered_display": "67", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [52, 53], "excluded_lines": [48], "start_line": 47, "executed_branches": [], "missing_branches": []}, "configure_text_caches": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 10, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 10, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 10, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 10, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [93, 94, 95, 96, 97, 98, 99, 100, 102, 103], "excluded_lines": [79], "start_line": 76, "executed_branches": [], "missing_branches": [[93, 94], [93, 95], [95, 96], [95, 97], [97, -76], [97, 98], [98, 99], [98, 100], [100, -76], [100, 102]]}, "clear_text_caches": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 3, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [108, 109, 110], "excluded_lines": [107], "start_line": 106, "executed_branches": [], "missing_branches": []}, "_space_advance": {"executed_lines": [120, 121, 126, 127, 129, 130, 134, 135, 138], "summary": {"covered_lines": 9, "num_statements": 15, "percent_covered": 64.70588235294117, "percent_covered_display": "65", "missing_lines": 6, "excluded_lines": 1, "percent_statements_covered": 60.0, "percent_statements_covered_display": "60", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [122, 124, 131, 132, 136, 137], "excluded_lines": [114], "start_line": 113, "executed_branches": [[126, 127], [126, 129]], "missing_branches": []}, "text_cache_stats": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [143], "excluded_lines": [142], "start_line": 141, "executed_branches": [], "missing_branches": []}, "prewarm_text_caches": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 42, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 42, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 20, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 20, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [183, 184, 186, 187, 188, 190, 191, 192, 194, 195, 196, 198, 199, 200, 201, 202, 203, 205, 206, 207, 208, 209, 211, 212, 213, 214, 215, 216, 217, 220, 226, 227, 229, 231, 232, 234, 235, 237, 238, 239, 241, 243], "excluded_lines": [155], "start_line": 151, "executed_branches": [], "missing_branches": [[183, 184], [183, 186], [186, 187], [186, 190], [190, 191], [190, 192], [195, 196], [195, 198], [205, 206], [205, 241], [206, 207], [206, 208], [208, 209], [208, 211], [213, 214], [213, 220], [215, 216], [215, 217], [226, 227], [226, 232]]}, "AlignmentHandler.calculate_spacing_and_position": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [259], "start_line": 253, "executed_branches": [], "missing_branches": []}, "LeftAlignmentHandler.calculate_spacing_and_position": {"executed_lines": [311, 312, 314, 315, 317, 319, 325, 327], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [292], "start_line": 284, "executed_branches": [[311, 312], [311, 314]], "missing_branches": []}, "CenterRightAlignmentHandler.__init__": {"executed_lines": [334], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 333, "executed_branches": [], "missing_branches": []}, "CenterRightAlignmentHandler.calculate_spacing_and_position": {"executed_lines": [350, 354, 355, 356, 358, 359, 361, 362, 364, 365, 367, 368, 369, 371, 373], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [342], "start_line": 336, "executed_branches": [[354, 355], [354, 361], [355, 356], [355, 358], [368, 369], [368, 371]], "missing_branches": []}, "JustifyAlignmentHandler.__init__": {"executed_lines": [385, 386, 387, 388], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 379, "executed_branches": [], "missing_branches": []}, "JustifyAlignmentHandler._gap_spacings": {"executed_lines": [393, 394, 397, 398], "summary": {"covered_lines": 4, "num_statements": 5, "percent_covered": 77.77777777777777, "percent_covered_display": "78", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 80.0, "percent_statements_covered_display": "80", "num_branches": 4, "num_partial_branches": 1, "covered_branches": 3, "missing_branches": 1, "percent_branches_covered": 75.0, "percent_branches_covered_display": "75"}, "missing_lines": [395], "excluded_lines": [392], "start_line": 391, "executed_branches": [[393, 394], [393, 398], [394, 397]], "missing_branches": [[394, 395]]}, "JustifyAlignmentHandler._distribute": {"executed_lines": [403, 404, 405, 406, 407, 408, 409], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [402], "start_line": 401, "executed_branches": [[405, 406], [405, 409]], "missing_branches": []}, "JustifyAlignmentHandler.calculate_spacing_and_position": {"executed_lines": [427, 429, 430, 433, 435, 436, 437, 438, 447, 448, 449, 450, 451, 455], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [417], "start_line": 411, "executed_branches": [[433, 435], [433, 447]], "missing_branches": []}, "Text.__init__": {"executed_lines": [478, 479, 480, 481, 482, 483, 484, 487], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [471], "start_line": 464, "executed_branches": [], "missing_branches": []}, "Text._calculate_dimensions": {"executed_lines": [495, 496, 498, 499, 500, 501, 502, 504, 505, 506], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [490], "start_line": 489, "executed_branches": [[499, 500], [499, 502]], "missing_branches": []}, "Text.from_word": {"executed_lines": [510], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 509, "executed_branches": [], "missing_branches": []}, "Text.text": {"executed_lines": [515], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [514], "start_line": 513, "executed_branches": [], "missing_branches": []}, "Text.style": {"executed_lines": [520], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [519], "start_line": 518, "executed_branches": [], "missing_branches": []}, "Text.origin": {"executed_lines": [525], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [524], "start_line": 523, "executed_branches": [], "missing_branches": []}, "Text.line": {"executed_lines": [535], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [534], "start_line": 533, "executed_branches": [], "missing_branches": []}, "Text.width": {"executed_lines": [540], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [539], "start_line": 538, "executed_branches": [], "missing_branches": []}, "Text.size": {"executed_lines": [546, 547, 548], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [544], "start_line": 543, "executed_branches": [], "missing_branches": []}, "Text.set_origin": {"executed_lines": [552], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [551], "start_line": 550, "executed_branches": [], "missing_branches": []}, "Text.add_line": {"executed_lines": [556], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [555], "start_line": 554, "executed_branches": [], "missing_branches": []}, "Text.in_object": {"executed_lines": [571, 574, 575, 580], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [559], "start_line": 558, "executed_branches": [], "missing_branches": []}, "Text._apply_decoration": {"executed_lines": [591, 593, 594, 597, 600, 604, 606], "summary": {"covered_lines": 7, "num_statements": 14, "percent_covered": 45.45454545454545, "percent_covered_display": "45", "missing_lines": 7, "excluded_lines": 1, "percent_statements_covered": 50.0, "percent_statements_covered_display": "50", "num_branches": 8, "num_partial_branches": 1, "covered_branches": 3, "missing_branches": 5, "percent_branches_covered": 37.5, "percent_branches_covered_display": "38"}, "missing_lines": [609, 611, 612, 615, 618, 622, 624], "excluded_lines": [584], "start_line": 583, "executed_branches": [[591, 593], [600, 604], [600, 606]], "missing_branches": [[591, 609], [609, -583], [609, 611], [618, 622], [618, 624]]}, "Text.render": {"executed_lines": [639, 642, 648, 658, 659], "summary": {"covered_lines": 5, "num_statements": 7, "percent_covered": 69.23076923076923, "percent_covered_display": "69", "missing_lines": 2, "excluded_lines": 1, "percent_statements_covered": 71.42857142857143, "percent_statements_covered_display": "71", "num_branches": 6, "num_partial_branches": 2, "covered_branches": 4, "missing_branches": 2, "percent_branches_covered": 66.66666666666667, "percent_branches_covered_display": "67"}, "missing_lines": [643, 649], "excluded_lines": [628], "start_line": 627, "executed_branches": [[642, 648], [648, 658], [658, -627], [658, 659]], "missing_branches": [[642, 643], [648, 649]]}, "Text._render_from_glyph_cache": {"executed_lines": [676, 679, 680, 684, 687, 688, 689, 694, 695, 696, 697, 699, 700, 701, 703, 704, 706, 707, 708, 711, 713, 714, 715], "summary": {"covered_lines": 23, "num_statements": 32, "percent_covered": 70.0, "percent_covered_display": "70", "missing_lines": 9, "excluded_lines": 1, "percent_statements_covered": 71.875, "percent_statements_covered_display": "72", "num_branches": 8, "num_partial_branches": 3, "covered_branches": 5, "missing_branches": 3, "percent_branches_covered": 62.5, "percent_branches_covered_display": "62"}, "missing_lines": [677, 685, 690, 717, 719, 722, 723, 724, 727], "excluded_lines": [662], "start_line": 661, "executed_branches": [[676, 679], [684, 687], [689, 694], [707, 708], [707, 713]], "missing_branches": [[676, 677], [684, 685], [689, 690]]}, "Line.__init__": {"executed_lines": [770, 771, 774, 775, 776, 777, 778, 779, 780, 781, 783, 784, 785, 786, 790, 793, 794, 795, 798, 804], "summary": {"covered_lines": 20, "num_statements": 20, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [752], "start_line": 736, "executed_branches": [], "missing_branches": []}, "Line.is_paragraph_end": {"executed_lines": [813], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 812, "executed_branches": [], "missing_branches": []}, "Line.render_alignment_handler": {"executed_lines": [823, 825, 826], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [817], "start_line": 816, "executed_branches": [[823, 825], [823, 826]], "missing_branches": []}, "Line._create_alignment_handler": {"executed_lines": [838, 839, 840, 841, 843], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [829], "start_line": 828, "executed_branches": [[838, 839], [838, 840], [840, 841], [840, 843]], "missing_branches": []}, "Line.text_objects": {"executed_lines": [848], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [847], "start_line": 846, "executed_branches": [], "missing_branches": []}, "Line.set_next": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [852], "excluded_lines": [851], "start_line": 850, "executed_branches": [], "missing_branches": []}, "Line._content_width": {"executed_lines": [857], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [856], "start_line": 855, "executed_branches": [], "missing_branches": []}, "Line._push_text": {"executed_lines": [875, 876], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [860], "start_line": 859, "executed_branches": [], "missing_branches": []}, "Line._pop_text": {"executed_lines": [880, 881, 882], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [879], "start_line": 878, "executed_branches": [], "missing_branches": []}, "Line._measure": {"executed_lines": [887, 888, 889], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [886], "start_line": 884, "executed_branches": [[887, 888], [887, 889]], "missing_branches": []}, "Line.add_word": {"executed_lines": [910, 911, 912, 913, 917, 919, 924, 931, 939, 940, 941, 943, 945, 946, 947, 948, 949, 950, 953, 956, 957, 959, 961, 962, 963, 966, 968, 972, 978, 986, 987, 988, 990, 992, 995, 997, 998, 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1010, 1012, 1013, 1014, 1016, 1018, 1020, 1022, 1027, 1028, 1031, 1032, 1035, 1039, 1040, 1042, 1048, 1056, 1057, 1059, 1061, 1062, 1063, 1064, 1065, 1066, 1067, 1070, 1073], "summary": {"covered_lines": 73, "num_statements": 75, "percent_covered": 93.20388349514563, "percent_covered_display": "93", "missing_lines": 2, "excluded_lines": 1, "percent_statements_covered": 97.33333333333333, "percent_statements_covered_display": "97", "num_branches": 28, "num_partial_branches": 5, "covered_branches": 23, "missing_branches": 5, "percent_branches_covered": 82.14285714285714, "percent_branches_covered_display": "82"}, "missing_lines": [967, 969], "excluded_lines": [897], "start_line": 893, "executed_branches": [[910, 911], [910, 917], [917, 919], [917, 939], [943, 945], [943, 953], [959, 961], [959, 995], [961, 962], [961, 995], [966, 968], [968, 972], [990, 961], [990, 992], [995, 997], [995, 1010], [1010, 1012], [1010, 1073], [1016, 1018], [1020, 1022], [1035, 1039], [1059, 1061], [1059, 1070]], "missing_branches": [[966, 967], [968, 969], [1016, 1073], [1020, 1073], [1035, 1073]]}, "Line.render": {"executed_lines": [1086, 1087, 1088, 1089, 1090, 1092, 1095, 1100, 1101, 1102, 1103, 1104, 1105, 1107, 1109, 1110, 1113, 1116, 1119, 1121, 1122, 1123], "summary": {"covered_lines": 22, "num_statements": 22, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1076], "start_line": 1075, "executed_branches": [[1087, 1088], [1087, 1092], [1107, -1075], [1107, 1109], [1122, 1107], [1122, 1123]], "missing_branches": []}, "Line.query_point": {"executed_lines": [1136, 1139, 1141, 1143, 1144, 1149, 1150, 1158, 1160, 1161, 1170, 1181, 1188, 1189, 1191], "summary": {"covered_lines": 15, "num_statements": 16, "percent_covered": 91.66666666666667, "percent_covered_display": "92", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 93.75, "percent_statements_covered_display": "94", "num_branches": 8, "num_partial_branches": 1, "covered_branches": 7, "missing_branches": 1, "percent_branches_covered": 87.5, "percent_branches_covered_display": "88"}, "missing_lines": [1171], "excluded_lines": [1126], "start_line": 1125, "executed_branches": [[1139, 1141], [1139, 1191], [1141, 1139], [1141, 1143], [1160, 1161], [1160, 1170], [1170, 1181]], "missing_branches": [[1170, 1171]]}, "": {"executed_lines": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17, 32, 37, 44, 47, 57, 58, 59, 68, 73, 76, 106, 113, 141, 151, 246, 252, 253, 281, 284, 330, 333, 336, 376, 379, 390, 391, 400, 401, 411, 458, 464, 489, 508, 509, 512, 513, 517, 518, 522, 523, 527, 528, 532, 533, 537, 538, 542, 543, 550, 554, 558, 583, 627, 661, 730, 736, 806, 807, 811, 812, 815, 816, 828, 845, 846, 850, 854, 855, 859, 878, 884, 893, 1075, 1125], "summary": {"covered_lines": 90, "num_statements": 90, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 6, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [247, 282, 331, 377, 459, 731], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"AlignmentHandler": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [259], "start_line": 246, "executed_branches": [], "missing_branches": []}, "LeftAlignmentHandler": {"executed_lines": [311, 312, 314, 315, 317, 319, 325, 327], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [292], "start_line": 281, "executed_branches": [[311, 312], [311, 314]], "missing_branches": []}, "CenterRightAlignmentHandler": {"executed_lines": [334, 350, 354, 355, 356, 358, 359, 361, 362, 364, 365, 367, 368, 369, 371, 373], "summary": {"covered_lines": 16, "num_statements": 16, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [342], "start_line": 330, "executed_branches": [[354, 355], [354, 361], [355, 356], [355, 358], [368, 369], [368, 371]], "missing_branches": []}, "JustifyAlignmentHandler": {"executed_lines": [385, 386, 387, 388, 393, 394, 397, 398, 403, 404, 405, 406, 407, 408, 409, 427, 429, 430, 433, 435, 436, 437, 438, 447, 448, 449, 450, 451, 455], "summary": {"covered_lines": 29, "num_statements": 30, "percent_covered": 94.73684210526316, "percent_covered_display": "95", "missing_lines": 1, "excluded_lines": 3, "percent_statements_covered": 96.66666666666667, "percent_statements_covered_display": "97", "num_branches": 8, "num_partial_branches": 1, "covered_branches": 7, "missing_branches": 1, "percent_branches_covered": 87.5, "percent_branches_covered_display": "88"}, "missing_lines": [395], "excluded_lines": [392, 402, 417], "start_line": 376, "executed_branches": [[393, 394], [393, 398], [394, 397], [405, 406], [405, 409], [433, 435], [433, 447]], "missing_branches": [[394, 395]]}, "Text": {"executed_lines": [478, 479, 480, 481, 482, 483, 484, 487, 495, 496, 498, 499, 500, 501, 502, 504, 505, 506, 510, 515, 520, 525, 530, 535, 540, 546, 547, 548, 552, 556, 571, 574, 575, 580, 591, 593, 594, 597, 600, 604, 606, 639, 642, 648, 658, 659, 676, 679, 680, 684, 687, 688, 689, 694, 695, 696, 697, 699, 700, 701, 703, 704, 706, 707, 708, 711, 713, 714, 715], "summary": {"covered_lines": 69, "num_statements": 87, "percent_covered": 74.77477477477477, "percent_covered_display": "75", "missing_lines": 18, "excluded_lines": 15, "percent_statements_covered": 79.3103448275862, "percent_statements_covered_display": "79", "num_branches": 24, "num_partial_branches": 6, "covered_branches": 14, "missing_branches": 10, "percent_branches_covered": 58.333333333333336, "percent_branches_covered_display": "58"}, "missing_lines": [609, 611, 612, 615, 618, 622, 624, 643, 649, 677, 685, 690, 717, 719, 722, 723, 724, 727], "excluded_lines": [471, 490, 514, 519, 524, 529, 534, 539, 544, 551, 555, 559, 584, 628, 662], "start_line": 458, "executed_branches": [[499, 500], [499, 502], [591, 593], [600, 604], [600, 606], [642, 648], [648, 658], [658, -627], [658, 659], [676, 679], [684, 687], [689, 694], [707, 708], [707, 713]], "missing_branches": [[591, 609], [609, -583], [609, 611], [618, 622], [618, 624], [642, 643], [648, 649], [676, 677], [684, 685], [689, 690]]}, "Line": {"executed_lines": [770, 771, 774, 775, 776, 777, 778, 779, 780, 781, 783, 784, 785, 786, 790, 793, 794, 795, 798, 804, 809, 813, 823, 825, 826, 838, 839, 840, 841, 843, 848, 857, 875, 876, 880, 881, 882, 887, 888, 889, 910, 911, 912, 913, 917, 919, 924, 931, 939, 940, 941, 943, 945, 946, 947, 948, 949, 950, 953, 956, 957, 959, 961, 962, 963, 966, 968, 972, 978, 986, 987, 988, 990, 992, 995, 997, 998, 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1010, 1012, 1013, 1014, 1016, 1018, 1020, 1022, 1027, 1028, 1031, 1032, 1035, 1039, 1040, 1042, 1048, 1056, 1057, 1059, 1061, 1062, 1063, 1064, 1065, 1066, 1067, 1070, 1073, 1086, 1087, 1088, 1089, 1090, 1092, 1095, 1100, 1101, 1102, 1103, 1104, 1105, 1107, 1109, 1110, 1113, 1116, 1119, 1121, 1122, 1123, 1136, 1139, 1141, 1143, 1144, 1149, 1150, 1158, 1160, 1161, 1170, 1181, 1188, 1189, 1191], "summary": {"covered_lines": 150, "num_statements": 154, "percent_covered": 95.09803921568627, "percent_covered_display": "95", "missing_lines": 4, "excluded_lines": 13, "percent_statements_covered": 97.40259740259741, "percent_statements_covered_display": "97", "num_branches": 50, "num_partial_branches": 6, "covered_branches": 44, "missing_branches": 6, "percent_branches_covered": 88.0, "percent_branches_covered_display": "88"}, "missing_lines": [852, 967, 969, 1171], "excluded_lines": [752, 808, 817, 829, 847, 851, 856, 860, 879, 886, 897, 1076, 1126], "start_line": 730, "executed_branches": [[823, 825], [823, 826], [838, 839], [838, 840], [840, 841], [840, 843], [887, 888], [887, 889], [910, 911], [910, 917], [917, 919], [917, 939], [943, 945], [943, 953], [959, 961], [959, 995], [961, 962], [961, 995], [966, 968], [968, 972], [990, 961], [990, 992], [995, 997], [995, 1010], [1010, 1012], [1010, 1073], [1016, 1018], [1020, 1022], [1035, 1039], [1059, 1061], [1059, 1070], [1087, 1088], [1087, 1092], [1107, -1075], [1107, 1109], [1122, 1107], [1122, 1123], [1139, 1141], [1139, 1191], [1141, 1139], [1141, 1143], [1160, 1161], [1160, 1170], [1170, 1181]], "missing_branches": [[966, 967], [968, 969], [1016, 1073], [1020, 1073], [1035, 1073], [1170, 1171]]}, "": {"executed_lines": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17, 32, 37, 44, 47, 49, 50, 51, 54, 57, 58, 59, 68, 73, 76, 106, 113, 120, 121, 126, 127, 129, 130, 134, 135, 138, 141, 151, 246, 252, 253, 281, 284, 330, 333, 336, 376, 379, 390, 391, 400, 401, 411, 458, 464, 489, 508, 509, 512, 513, 517, 518, 522, 523, 527, 528, 532, 533, 537, 538, 542, 543, 550, 554, 558, 583, 627, 661, 730, 736, 806, 807, 811, 812, 815, 816, 828, 845, 846, 850, 854, 855, 859, 878, 884, 893, 1075, 1125], "summary": {"covered_lines": 103, "num_statements": 167, "percent_covered": 52.76381909547739, "percent_covered_display": "53", "missing_lines": 64, "excluded_lines": 12, "percent_statements_covered": 61.67664670658683, "percent_statements_covered_display": "62", "num_branches": 32, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 30, "percent_branches_covered": 6.25, "percent_branches_covered_display": "6"}, "missing_lines": [52, 53, 93, 94, 95, 96, 97, 98, 99, 100, 102, 103, 108, 109, 110, 122, 124, 131, 132, 136, 137, 143, 183, 184, 186, 187, 188, 190, 191, 192, 194, 195, 196, 198, 199, 200, 201, 202, 203, 205, 206, 207, 208, 209, 211, 212, 213, 214, 215, 216, 217, 220, 226, 227, 229, 231, 232, 234, 235, 237, 238, 239, 241, 243], "excluded_lines": [48, 79, 107, 114, 142, 155, 247, 282, 331, 377, 459, 731], "start_line": 1, "executed_branches": [[126, 127], [126, 129]], "missing_branches": [[93, 94], [93, 95], [95, 96], [95, 97], [97, -76], [97, 98], [98, 99], [98, 100], [100, -76], [100, 102], [183, 184], [183, 186], [186, 187], [186, 190], [190, 191], [190, 192], [195, 196], [195, 198], [205, 206], [205, 241], [206, 207], [206, 208], [208, 209], [208, 211], [213, 214], [213, 220], [215, 216], [215, 217], [226, 227], [226, 232]]}}}, "pyWebLayout/core/__init__.py": {"executed_lines": [8, 22], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1], "executed_branches": [], "missing_branches": [], "functions": {"": {"executed_lines": [8, 22], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [8, 22], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1], "start_line": 1, "executed_branches": [], "missing_branches": []}}}, "pyWebLayout/core/base.py": {"executed_lines": [1, 2, 3, 6, 10, 16, 24, 25, 26, 29, 35, 42, 44, 54, 56, 59, 65, 72, 74, 78, 79, 80, 88, 95, 96, 97, 99, 100, 102, 104, 105, 107, 110, 118, 119, 120, 121, 123, 124, 126, 128, 129, 133, 134, 136, 138, 139, 143, 148, 156, 157, 158, 160, 161, 163, 165, 166, 171, 182, 183, 184, 186, 217, 220, 221, 222, 223, 224, 225, 228, 233, 247, 248, 250, 263, 266, 267, 270, 282, 283, 286, 293, 294, 295, 297, 305, 307, 317, 320, 330, 331, 332, 334, 343, 345, 352, 353, 354, 356, 375, 399, 411, 412, 428, 429], "summary": {"covered_lines": 105, "num_statements": 134, "percent_covered": 71.95121951219512, "percent_covered_display": "72", "missing_lines": 29, "excluded_lines": 33, "percent_statements_covered": 78.35820895522389, "percent_statements_covered_display": "78", "num_branches": 30, "num_partial_branches": 3, "covered_branches": 13, "missing_branches": 17, "percent_branches_covered": 43.333333333333336, "percent_branches_covered_display": "43"}, "missing_lines": [7, 55, 131, 141, 145, 168, 366, 368, 369, 371, 372, 373, 386, 388, 389, 391, 392, 394, 395, 396, 423, 424, 440, 441, 443, 444, 445, 446, 448], "excluded_lines": [11, 17, 30, 36, 45, 60, 66, 75, 89, 101, 106, 111, 125, 130, 135, 140, 144, 149, 162, 167, 172, 196, 287, 298, 308, 321, 335, 346, 357, 376, 400, 413, 430], "executed_branches": [[6, 10], [54, 56], [220, 221], [220, 222], [222, 223], [222, 224], [224, 225], [224, 228], [228, 233], [228, 247], [266, 267], [266, 270], [353, 354]], "missing_branches": [[6, 7], [54, 55], [353, -345], [368, 369], [368, 371], [388, 389], [388, 391], [391, 392], [391, 394], [423, -411], [423, 424], [440, 441], [440, 443], [443, 444], [443, 445], [445, 446], [445, 448]], "functions": {"Renderable.render": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [17], "start_line": 16, "executed_branches": [], "missing_branches": []}, "Renderable.origin": {"executed_lines": [26], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 25, "executed_branches": [], "missing_branches": []}, "Interactable.__init__": {"executed_lines": [42], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [36], "start_line": 35, "executed_branches": [], "missing_branches": []}, "Interactable.interact": {"executed_lines": [54, 56], "summary": {"covered_lines": 2, "num_statements": 3, "percent_covered": 60.0, "percent_covered_display": "60", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 66.66666666666667, "percent_statements_covered_display": "67", "num_branches": 2, "num_partial_branches": 1, "covered_branches": 1, "missing_branches": 1, "percent_branches_covered": 50.0, "percent_branches_covered_display": "50"}, "missing_lines": [55], "excluded_lines": [45], "start_line": 44, "executed_branches": [[54, 56]], "missing_branches": [[54, 55]]}, "Layoutable.layout": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [66], "start_line": 65, "executed_branches": [], "missing_branches": []}, "Queriable.in_object": {"executed_lines": [78, 79, 80], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [75], "start_line": 74, "executed_branches": [], "missing_branches": []}, "Hierarchical.__init__": {"executed_lines": [96, 97], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 95, "executed_branches": [], "missing_branches": []}, "Hierarchical.parent": {"executed_lines": [107], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [106], "start_line": 105, "executed_branches": [], "missing_branches": []}, "Geometric.__init__": {"executed_lines": [119, 120, 121], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 118, "executed_branches": [], "missing_branches": []}, "Geometric.origin": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [131], "excluded_lines": [130], "start_line": 129, "executed_branches": [], "missing_branches": []}, "Geometric.size": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [141], "excluded_lines": [140], "start_line": 139, "executed_branches": [], "missing_branches": []}, "Geometric.set_origin": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [145], "excluded_lines": [144], "start_line": 143, "executed_branches": [], "missing_branches": []}, "Styleable.__init__": {"executed_lines": [157, 158], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 156, "executed_branches": [], "missing_branches": []}, "Styleable.style": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [168], "excluded_lines": [167], "start_line": 166, "executed_branches": [], "missing_branches": []}, "FontRegistry.__init__": {"executed_lines": [183, 184], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 182, "executed_branches": [], "missing_branches": []}, "FontRegistry.get_or_create_font": {"executed_lines": [217, 220, 221, 222, 223, 224, 225, 228, 233, 247, 248, 250, 263, 266, 267, 270, 282, 283], "summary": {"covered_lines": 18, "num_statements": 18, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 10, "num_partial_branches": 0, "covered_branches": 10, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [196], "start_line": 186, "executed_branches": [[220, 221], [220, 222], [222, 223], [222, 224], [224, 225], [224, 228], [228, 233], [228, 247], [266, 267], [266, 270]], "missing_branches": []}, "MetadataContainer.__init__": {"executed_lines": [294, 295], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 293, "executed_branches": [], "missing_branches": []}, "MetadataContainer.set_metadata": {"executed_lines": [305], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [298], "start_line": 297, "executed_branches": [], "missing_branches": []}, "MetadataContainer.get_metadata": {"executed_lines": [317], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [308], "start_line": 307, "executed_branches": [], "missing_branches": []}, "BlockContainer.__init__": {"executed_lines": [331, 332], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 330, "executed_branches": [], "missing_branches": []}, "BlockContainer.blocks": {"executed_lines": [343], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [335], "start_line": 334, "executed_branches": [], "missing_branches": []}, "BlockContainer.add_block": {"executed_lines": [352, 353, 354], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 80.0, "percent_covered_display": "80", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 1, "covered_branches": 1, "missing_branches": 1, "percent_branches_covered": 50.0, "percent_branches_covered_display": "50"}, "missing_lines": [], "excluded_lines": [346], "start_line": 345, "executed_branches": [[353, 354]], "missing_branches": [[353, -345]]}, "BlockContainer.create_paragraph": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 6, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 6, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 2, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [366, 368, 369, 371, 372, 373], "excluded_lines": [357], "start_line": 356, "executed_branches": [], "missing_branches": [[368, 369], [368, 371]]}, "BlockContainer.create_heading": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 8, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 8, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 4, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 4, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [386, 388, 389, 391, 392, 394, 395, 396], "excluded_lines": [376], "start_line": 375, "executed_branches": [], "missing_branches": [[388, 389], [388, 391], [391, 392], [391, 394]]}, "ContainerAware._validate_container": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 2, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 2, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 2, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [423, 424], "excluded_lines": [413], "start_line": 412, "executed_branches": [], "missing_branches": [[423, -411], [423, 424]]}, "ContainerAware._inherit_style": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 7, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 7, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 6, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 6, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [440, 441, 443, 444, 445, 446, 448], "excluded_lines": [430], "start_line": 429, "executed_branches": [], "missing_branches": [[440, 441], [440, 443], [443, 444], [443, 445], [445, 446], [445, 448]]}, "": {"executed_lines": [1, 2, 3, 6, 10, 16, 24, 25, 29, 35, 44, 59, 65, 72, 74, 88, 95, 99, 100, 104, 105, 110, 118, 123, 124, 128, 129, 133, 134, 138, 139, 143, 148, 156, 160, 161, 165, 166, 171, 182, 186, 286, 293, 297, 307, 320, 330, 334, 345, 356, 375, 399, 411, 412, 428, 429], "summary": {"covered_lines": 56, "num_statements": 57, "percent_covered": 96.61016949152543, "percent_covered_display": "97", "missing_lines": 1, "excluded_lines": 10, "percent_statements_covered": 98.24561403508773, "percent_statements_covered_display": "98", "num_branches": 2, "num_partial_branches": 1, "covered_branches": 1, "missing_branches": 1, "percent_branches_covered": 50.0, "percent_branches_covered_display": "50"}, "missing_lines": [7], "excluded_lines": [11, 30, 60, 89, 111, 149, 172, 287, 321, 400], "start_line": 1, "executed_branches": [[6, 10]], "missing_branches": [[6, 7]]}}, "classes": {"Renderable": {"executed_lines": [26], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [17], "start_line": 10, "executed_branches": [], "missing_branches": []}, "Interactable": {"executed_lines": [42, 54, 56], "summary": {"covered_lines": 3, "num_statements": 4, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 1, "excluded_lines": 2, "percent_statements_covered": 75.0, "percent_statements_covered_display": "75", "num_branches": 2, "num_partial_branches": 1, "covered_branches": 1, "missing_branches": 1, "percent_branches_covered": 50.0, "percent_branches_covered_display": "50"}, "missing_lines": [55], "excluded_lines": [36, 45], "start_line": 29, "executed_branches": [[54, 56]], "missing_branches": [[54, 55]]}, "Layoutable": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [66], "start_line": 59, "executed_branches": [], "missing_branches": []}, "Queriable": {"executed_lines": [78, 79, 80], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [75], "start_line": 72, "executed_branches": [], "missing_branches": []}, "Hierarchical": {"executed_lines": [96, 97, 102, 107], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 2, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [101, 106], "start_line": 88, "executed_branches": [], "missing_branches": []}, "Geometric": {"executed_lines": [119, 120, 121, 126, 136], "summary": {"covered_lines": 5, "num_statements": 8, "percent_covered": 62.5, "percent_covered_display": "62", "missing_lines": 3, "excluded_lines": 5, "percent_statements_covered": 62.5, "percent_statements_covered_display": "62", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [131, 141, 145], "excluded_lines": [125, 130, 135, 140, 144], "start_line": 110, "executed_branches": [], "missing_branches": []}, "Styleable": {"executed_lines": [157, 158, 163], "summary": {"covered_lines": 3, "num_statements": 4, "percent_covered": 75.0, "percent_covered_display": "75", "missing_lines": 1, "excluded_lines": 2, "percent_statements_covered": 75.0, "percent_statements_covered_display": "75", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [168], "excluded_lines": [162, 167], "start_line": 148, "executed_branches": [], "missing_branches": []}, "FontRegistry": {"executed_lines": [183, 184, 217, 220, 221, 222, 223, 224, 225, 228, 233, 247, 248, 250, 263, 266, 267, 270, 282, 283], "summary": {"covered_lines": 20, "num_statements": 20, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 10, "num_partial_branches": 0, "covered_branches": 10, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [196], "start_line": 171, "executed_branches": [[220, 221], [220, 222], [222, 223], [222, 224], [224, 225], [224, 228], [228, 233], [228, 247], [266, 267], [266, 270]], "missing_branches": []}, "MetadataContainer": {"executed_lines": [294, 295, 305, 317], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 2, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [298, 308], "start_line": 286, "executed_branches": [], "missing_branches": []}, "BlockContainer": {"executed_lines": [331, 332, 343, 352, 353, 354], "summary": {"covered_lines": 6, "num_statements": 20, "percent_covered": 25.0, "percent_covered_display": "25", "missing_lines": 14, "excluded_lines": 4, "percent_statements_covered": 30.0, "percent_statements_covered_display": "30", "num_branches": 8, "num_partial_branches": 1, "covered_branches": 1, "missing_branches": 7, "percent_branches_covered": 12.5, "percent_branches_covered_display": "12"}, "missing_lines": [366, 368, 369, 371, 372, 373, 386, 388, 389, 391, 392, 394, 395, 396], "excluded_lines": [335, 346, 357, 376], "start_line": 320, "executed_branches": [[353, 354]], "missing_branches": [[353, -345], [368, 369], [368, 371], [388, 389], [388, 391], [391, 392], [391, 394]]}, "ContainerAware": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 9, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 9, "excluded_lines": 2, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 8, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 8, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [423, 424, 440, 441, 443, 444, 445, 446, 448], "excluded_lines": [413, 430], "start_line": 399, "executed_branches": [], "missing_branches": [[423, -411], [423, 424], [440, 441], [440, 443], [443, 444], [443, 445], [445, 446], [445, 448]]}, "": {"executed_lines": [1, 2, 3, 6, 10, 16, 24, 25, 29, 35, 44, 59, 65, 72, 74, 88, 95, 99, 100, 104, 105, 110, 118, 123, 124, 128, 129, 133, 134, 138, 139, 143, 148, 156, 160, 161, 165, 166, 171, 182, 186, 286, 293, 297, 307, 320, 330, 334, 345, 356, 375, 399, 411, 412, 428, 429], "summary": {"covered_lines": 56, "num_statements": 57, "percent_covered": 96.61016949152543, "percent_covered_display": "97", "missing_lines": 1, "excluded_lines": 10, "percent_statements_covered": 98.24561403508773, "percent_statements_covered_display": "98", "num_branches": 2, "num_partial_branches": 1, "covered_branches": 1, "missing_branches": 1, "percent_branches_covered": 50.0, "percent_branches_covered_display": "50"}, "missing_lines": [7], "excluded_lines": [11, 30, 60, 89, 111, 149, 172, 287, 321, 400], "start_line": 1, "executed_branches": [[6, 10]], "missing_branches": [[6, 7]]}}}, "pyWebLayout/core/cache.py": {"executed_lines": [32, 34, 35, 37, 38, 43, 50, 54, 55, 56, 59, 71, 74, 75, 76, 77, 79, 80, 82, 83, 84, 86, 87, 88, 89, 90, 94, 97, 100, 105, 107, 108, 109, 110, 111, 112, 113, 115, 119, 120, 121, 123, 125, 126, 127, 128, 129, 130, 131, 133, 135, 136, 139, 140, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 153, 154, 155, 157, 158, 159, 162, 164, 165, 166, 167, 168, 170, 171, 172, 173, 175, 177, 178, 179, 181, 182, 183, 192, 193, 195, 196, 199, 210, 213, 214, 215, 216, 218, 219, 221, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 240, 241, 244, 246, 247, 248, 249, 251, 253, 254, 255, 258, 273, 276, 277, 278, 279, 280, 281, 282, 284, 285, 287, 288, 289, 290, 292, 293, 295, 303, 305, 307, 309, 311, 312, 313, 314, 315, 317, 318, 319, 321, 322, 323, 325, 327, 329, 330, 332, 334, 335, 336, 338, 340, 341, 342, 343], "summary": {"covered_lines": 167, "num_statements": 171, "percent_covered": 96.74418604651163, "percent_covered_display": "97", "missing_lines": 4, "excluded_lines": 20, "percent_statements_covered": 97.6608187134503, "percent_statements_covered_display": "98", "num_branches": 44, "num_partial_branches": 3, "covered_branches": 41, "missing_branches": 3, "percent_branches_covered": 93.18181818181819, "percent_branches_covered_display": "93"}, "missing_lines": [137, 160, 242, 328], "excluded_lines": [1, 60, 95, 98, 101, 106, 116, 124, 134, 163, 176, 200, 222, 245, 252, 259, 296, 326, 333, 339], "executed_branches": [[74, 75], [74, 76], [76, 77], [76, 79], [108, 109], [108, 111], [128, 129], [128, 131], [136, 139], [139, 140], [139, 142], [147, 148], [147, 153], [150, 147], [150, 151], [158, -157], [158, 159], [159, 158], [164, 165], [164, 166], [167, 168], [167, 170], [172, -162], [172, 173], [213, 214], [213, 215], [230, 231], [230, 234], [235, 236], [235, 237], [246, 247], [246, 248], [276, 277], [276, 278], [303, 305], [303, 307], [307, 309], [307, 311], [312, 313], [312, 314], [327, 329]], "missing_branches": [[136, 137], [159, 160], [327, 328]], "functions": {"_UsageRanked.__init__": {"executed_lines": [74, 75, 76, 77, 79, 80, 82, 83, 84, 86, 87, 88, 89, 90], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 71, "executed_branches": [[74, 75], [74, 76], [76, 77], [76, 79]], "missing_branches": []}, "_UsageRanked._over_budget": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [95], "start_line": 94, "executed_branches": [], "missing_branches": []}, "_UsageRanked._record_add": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [98], "start_line": 97, "executed_branches": [], "missing_branches": []}, "_UsageRanked._record_remove": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [101], "start_line": 100, "executed_branches": [], "missing_branches": []}, "_UsageRanked.get": {"executed_lines": [107, 108, 109, 110, 111, 112, 113], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [106], "start_line": 105, "executed_branches": [[108, 109], [108, 111]], "missing_branches": []}, "_UsageRanked._add_new": {"executed_lines": [119, 120, 121], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [116], "start_line": 115, "executed_branches": [], "missing_branches": []}, "_UsageRanked._remove": {"executed_lines": [125, 126, 127, 128, 129, 130, 131], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [124], "start_line": 123, "executed_branches": [[128, 129], [128, 131]], "missing_branches": []}, "_UsageRanked._evict_one": {"executed_lines": [135, 136, 139, 140, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 153, 154, 155], "summary": {"covered_lines": 17, "num_statements": 18, "percent_covered": 92.3076923076923, "percent_covered_display": "92", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 94.44444444444444, "percent_statements_covered_display": "94", "num_branches": 8, "num_partial_branches": 1, "covered_branches": 7, "missing_branches": 1, "percent_branches_covered": 87.5, "percent_branches_covered_display": "88"}, "missing_lines": [137], "excluded_lines": [134], "start_line": 133, "executed_branches": [[136, 139], [139, 140], [139, 142], [147, 148], [147, 153], [150, 147], [150, 151]], "missing_branches": [[136, 137]]}, "_UsageRanked._evict_to_budget": {"executed_lines": [158, 159], "summary": {"covered_lines": 2, "num_statements": 3, "percent_covered": 71.42857142857143, "percent_covered_display": "71", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 66.66666666666667, "percent_statements_covered_display": "67", "num_branches": 4, "num_partial_branches": 1, "covered_branches": 3, "missing_branches": 1, "percent_branches_covered": 75.0, "percent_branches_covered_display": "75"}, "missing_lines": [160], "excluded_lines": [], "start_line": 157, "executed_branches": [[158, -157], [158, 159], [159, 158]], "missing_branches": [[159, 160]]}, "_UsageRanked._maybe_age": {"executed_lines": [164, 165, 166, 167, 168, 170, 171, 172, 173], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [163], "start_line": 162, "executed_branches": [[164, 165], [164, 166], [167, 168], [167, 170], [172, -162], [172, 173]], "missing_branches": []}, "_UsageRanked.clear": {"executed_lines": [177, 178, 179], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [176], "start_line": 175, "executed_branches": [], "missing_branches": []}, "_UsageRanked._base_stats": {"executed_lines": [182, 183], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 181, "executed_branches": [], "missing_branches": []}, "_UsageRanked.__len__": {"executed_lines": [193], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 192, "executed_branches": [], "missing_branches": []}, "_UsageRanked.__contains__": {"executed_lines": [196], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 195, "executed_branches": [], "missing_branches": []}, "UsageCache.__init__": {"executed_lines": [213, 214, 215, 216], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 210, "executed_branches": [[213, 214], [213, 215]], "missing_branches": []}, "UsageCache._over_budget": {"executed_lines": [219], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 218, "executed_branches": [], "missing_branches": []}, "UsageCache.put": {"executed_lines": [229, 230, 231, 232, 233, 234, 235, 236, 237, 238], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [222], "start_line": 221, "executed_branches": [[230, 231], [230, 234], [235, 236], [235, 237]], "missing_branches": []}, "UsageCache.max_entries": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [242], "excluded_lines": [], "start_line": 241, "executed_branches": [], "missing_branches": []}, "UsageCache.resize": {"executed_lines": [246, 247, 248, 249], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [245], "start_line": 244, "executed_branches": [[246, 247], [246, 248]], "missing_branches": []}, "UsageCache.stats": {"executed_lines": [253, 254, 255], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [252], "start_line": 251, "executed_branches": [], "missing_branches": []}, "SizedUsageCache.__init__": {"executed_lines": [276, 277, 278, 279, 280, 281, 282], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 273, "executed_branches": [[276, 277], [276, 278]], "missing_branches": []}, "SizedUsageCache._over_budget": {"executed_lines": [285], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 284, "executed_branches": [], "missing_branches": []}, "SizedUsageCache._record_add": {"executed_lines": [288, 289, 290], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 287, "executed_branches": [], "missing_branches": []}, "SizedUsageCache._record_remove": {"executed_lines": [293], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 292, "executed_branches": [], "missing_branches": []}, "SizedUsageCache.put": {"executed_lines": [303, 305, 307, 309, 311, 312, 313, 314, 315], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [296], "start_line": 295, "executed_branches": [[303, 305], [303, 307], [307, 309], [307, 311], [312, 313], [312, 314]], "missing_branches": []}, "SizedUsageCache.max_bytes": {"executed_lines": [319], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 318, "executed_branches": [], "missing_branches": []}, "SizedUsageCache.total_bytes": {"executed_lines": [323], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 322, "executed_branches": [], "missing_branches": []}, "SizedUsageCache.resize": {"executed_lines": [327, 329, 330], "summary": {"covered_lines": 3, "num_statements": 4, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 75.0, "percent_statements_covered_display": "75", "num_branches": 2, "num_partial_branches": 1, "covered_branches": 1, "missing_branches": 1, "percent_branches_covered": 50.0, "percent_branches_covered_display": "50"}, "missing_lines": [328], "excluded_lines": [326], "start_line": 325, "executed_branches": [[327, 329]], "missing_branches": [[327, 328]]}, "SizedUsageCache.clear": {"executed_lines": [334, 335, 336], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [333], "start_line": 332, "executed_branches": [], "missing_branches": []}, "SizedUsageCache.stats": {"executed_lines": [340, 341, 342, 343], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [339], "start_line": 338, "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [32, 34, 35, 37, 38, 43, 50, 54, 55, 56, 59, 71, 94, 97, 100, 105, 115, 123, 133, 157, 162, 175, 181, 192, 195, 199, 210, 218, 221, 240, 241, 244, 251, 258, 273, 284, 287, 292, 295, 317, 318, 321, 322, 325, 332, 338], "summary": {"covered_lines": 46, "num_statements": 46, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 4, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1, 60, 200, 259], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"_UsageRanked": {"executed_lines": [74, 75, 76, 77, 79, 80, 82, 83, 84, 86, 87, 88, 89, 90, 107, 108, 109, 110, 111, 112, 113, 119, 120, 121, 125, 126, 127, 128, 129, 130, 131, 135, 136, 139, 140, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 153, 154, 155, 158, 159, 164, 165, 166, 167, 168, 170, 171, 172, 173, 177, 178, 179, 182, 183, 193, 196], "summary": {"covered_lines": 66, "num_statements": 68, "percent_covered": 95.74468085106383, "percent_covered_display": "96", "missing_lines": 2, "excluded_lines": 9, "percent_statements_covered": 97.05882352941177, "percent_statements_covered_display": "97", "num_branches": 26, "num_partial_branches": 2, "covered_branches": 24, "missing_branches": 2, "percent_branches_covered": 92.3076923076923, "percent_branches_covered_display": "92"}, "missing_lines": [137, 160], "excluded_lines": [95, 98, 101, 106, 116, 124, 134, 163, 176], "start_line": 59, "executed_branches": [[74, 75], [74, 76], [76, 77], [76, 79], [108, 109], [108, 111], [128, 129], [128, 131], [136, 139], [139, 140], [139, 142], [147, 148], [147, 153], [150, 147], [150, 151], [158, -157], [158, 159], [159, 158], [164, 165], [164, 166], [167, 168], [167, 170], [172, -162], [172, 173]], "missing_branches": [[136, 137], [159, 160]]}, "UsageCache": {"executed_lines": [213, 214, 215, 216, 219, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 246, 247, 248, 249, 253, 254, 255], "summary": {"covered_lines": 22, "num_statements": 23, "percent_covered": 96.7741935483871, "percent_covered_display": "97", "missing_lines": 1, "excluded_lines": 3, "percent_statements_covered": 95.65217391304348, "percent_statements_covered_display": "96", "num_branches": 8, "num_partial_branches": 0, "covered_branches": 8, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [242], "excluded_lines": [222, 245, 252], "start_line": 199, "executed_branches": [[213, 214], [213, 215], [230, 231], [230, 234], [235, 236], [235, 237], [246, 247], [246, 248]], "missing_branches": []}, "SizedUsageCache": {"executed_lines": [276, 277, 278, 279, 280, 281, 282, 285, 288, 289, 290, 293, 303, 305, 307, 309, 311, 312, 313, 314, 315, 319, 323, 327, 329, 330, 334, 335, 336, 340, 341, 342, 343], "summary": {"covered_lines": 33, "num_statements": 34, "percent_covered": 95.45454545454545, "percent_covered_display": "95", "missing_lines": 1, "excluded_lines": 4, "percent_statements_covered": 97.05882352941177, "percent_statements_covered_display": "97", "num_branches": 10, "num_partial_branches": 1, "covered_branches": 9, "missing_branches": 1, "percent_branches_covered": 90.0, "percent_branches_covered_display": "90"}, "missing_lines": [328], "excluded_lines": [296, 326, 333, 339], "start_line": 258, "executed_branches": [[276, 277], [276, 278], [303, 305], [303, 307], [307, 309], [307, 311], [312, 313], [312, 314], [327, 329]], "missing_branches": [[327, 328]]}, "": {"executed_lines": [32, 34, 35, 37, 38, 43, 50, 54, 55, 56, 59, 71, 94, 97, 100, 105, 115, 123, 133, 157, 162, 175, 181, 192, 195, 199, 210, 218, 221, 240, 241, 244, 251, 258, 273, 284, 287, 292, 295, 317, 318, 321, 322, 325, 332, 338], "summary": {"covered_lines": 46, "num_statements": 46, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 4, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1, 60, 200, 259], "start_line": 1, "executed_branches": [], "missing_branches": []}}}, "pyWebLayout/core/callback_registry.py": {"executed_lines": [11, 12, 15, 29, 31, 32, 33, 34, 36, 59, 60, 63, 64, 65, 66, 69, 71, 72, 75, 76, 77, 78, 80, 95, 97, 112, 114, 126, 128, 140, 142, 159, 160, 161, 162, 163, 165, 185, 186, 187, 188, 190, 200, 201, 203, 206, 207, 208, 209, 213, 216, 218, 219, 220, 221, 223, 230, 232, 242, 244, 255, 257, 258, 259, 260, 261, 262, 267, 269, 271, 273], "summary": {"covered_lines": 71, "num_statements": 75, "percent_covered": 92.47311827956989, "percent_covered_display": "92", "missing_lines": 4, "excluded_lines": 21, "percent_statements_covered": 94.66666666666667, "percent_statements_covered_display": "95", "num_branches": 18, "num_partial_branches": 3, "covered_branches": 15, "missing_branches": 3, "percent_branches_covered": 83.33333333333333, "percent_branches_covered_display": "83"}, "missing_lines": [210, 211, 214, 265], "excluded_lines": [1, 16, 30, 37, 81, 98, 115, 129, 143, 166, 191, 217, 224, 233, 245, 268, 272, 275, 276, 277, 278], "executed_branches": [[64, 65], [64, 66], [69, 71], [69, 75], [160, 161], [160, 163], [186, 187], [186, 188], [201, 203], [207, 208], [257, 258], [257, 259], [259, 260], [259, 261], [261, 262]], "missing_branches": [[201, 214], [207, 213], [261, 265]], "functions": {"CallbackRegistry.__init__": {"executed_lines": [31, 32, 33, 34], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [30], "start_line": 29, "executed_branches": [], "missing_branches": []}, "CallbackRegistry.register": {"executed_lines": [59, 60, 63, 64, 65, 66, 69, 71, 72, 75, 76, 77, 78], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [37], "start_line": 36, "executed_branches": [[64, 65], [64, 66], [69, 71], [69, 75]], "missing_branches": []}, "CallbackRegistry.get_by_id": {"executed_lines": [95], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [81], "start_line": 80, "executed_branches": [], "missing_branches": []}, "CallbackRegistry.get_by_type": {"executed_lines": [112], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [98], "start_line": 97, "executed_branches": [], "missing_branches": []}, "CallbackRegistry.get_all_ids": {"executed_lines": [126], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [115], "start_line": 114, "executed_branches": [], "missing_branches": []}, "CallbackRegistry.get_all_types": {"executed_lines": [140], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [129], "start_line": 128, "executed_branches": [], "missing_branches": []}, "CallbackRegistry.set_callback": {"executed_lines": [159, 160, 161, 162, 163], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [143], "start_line": 142, "executed_branches": [[160, 161], [160, 163]], "missing_branches": []}, "CallbackRegistry.set_callbacks_by_type": {"executed_lines": [185, 186, 187, 188], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [166], "start_line": 165, "executed_branches": [[186, 187], [186, 188]], "missing_branches": []}, "CallbackRegistry.unregister": {"executed_lines": [200, 201, 203, 206, 207, 208, 209, 213], "summary": {"covered_lines": 8, "num_statements": 11, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 3, "excluded_lines": 1, "percent_statements_covered": 72.72727272727273, "percent_statements_covered_display": "73", "num_branches": 4, "num_partial_branches": 2, "covered_branches": 2, "missing_branches": 2, "percent_branches_covered": 50.0, "percent_branches_covered_display": "50"}, "missing_lines": [210, 211, 214], "excluded_lines": [191], "start_line": 190, "executed_branches": [[201, 203], [207, 208]], "missing_branches": [[201, 214], [207, 213]]}, "CallbackRegistry.clear": {"executed_lines": [218, 219, 220, 221], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [217], "start_line": 216, "executed_branches": [], "missing_branches": []}, "CallbackRegistry.count": {"executed_lines": [230], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [224], "start_line": 223, "executed_branches": [], "missing_branches": []}, "CallbackRegistry.count_by_type": {"executed_lines": [242], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [233], "start_line": 232, "executed_branches": [], "missing_branches": []}, "CallbackRegistry._get_type_name": {"executed_lines": [255, 257, 258, 259, 260, 261, 262], "summary": {"covered_lines": 7, "num_statements": 8, "percent_covered": 85.71428571428571, "percent_covered_display": "86", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 87.5, "percent_statements_covered_display": "88", "num_branches": 6, "num_partial_branches": 1, "covered_branches": 5, "missing_branches": 1, "percent_branches_covered": 83.33333333333333, "percent_branches_covered_display": "83"}, "missing_lines": [265], "excluded_lines": [245], "start_line": 244, "executed_branches": [[257, 258], [257, 259], [259, 260], [259, 261], [261, 262]], "missing_branches": [[261, 265]]}, "CallbackRegistry.__len__": {"executed_lines": [269], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [268], "start_line": 267, "executed_branches": [], "missing_branches": []}, "CallbackRegistry.__contains__": {"executed_lines": [273], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [272], "start_line": 271, "executed_branches": [], "missing_branches": []}, "CallbackRegistry.__repr__": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 3, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [276, 277, 278], "start_line": 275, "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [11, 12, 15, 29, 36, 80, 97, 114, 128, 142, 165, 190, 216, 223, 232, 244, 267, 271], "summary": {"covered_lines": 18, "num_statements": 18, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 3, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1, 16, 275], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"CallbackRegistry": {"executed_lines": [31, 32, 33, 34, 59, 60, 63, 64, 65, 66, 69, 71, 72, 75, 76, 77, 78, 95, 112, 126, 140, 159, 160, 161, 162, 163, 185, 186, 187, 188, 200, 201, 203, 206, 207, 208, 209, 213, 218, 219, 220, 221, 230, 242, 255, 257, 258, 259, 260, 261, 262, 269, 273], "summary": {"covered_lines": 53, "num_statements": 57, "percent_covered": 90.66666666666667, "percent_covered_display": "91", "missing_lines": 4, "excluded_lines": 18, "percent_statements_covered": 92.98245614035088, "percent_statements_covered_display": "93", "num_branches": 18, "num_partial_branches": 3, "covered_branches": 15, "missing_branches": 3, "percent_branches_covered": 83.33333333333333, "percent_branches_covered_display": "83"}, "missing_lines": [210, 211, 214, 265], "excluded_lines": [30, 37, 81, 98, 115, 129, 143, 166, 191, 217, 224, 233, 245, 268, 272, 276, 277, 278], "start_line": 15, "executed_branches": [[64, 65], [64, 66], [69, 71], [69, 75], [160, 161], [160, 163], [186, 187], [186, 188], [201, 203], [207, 208], [257, 258], [257, 259], [259, 260], [259, 261], [261, 262]], "missing_branches": [[201, 214], [207, 213], [261, 265]]}, "": {"executed_lines": [11, 12, 15, 29, 36, 80, 97, 114, 128, 142, 165, 190, 216, 223, 232, 244, 267, 271], "summary": {"covered_lines": 18, "num_statements": 18, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 3, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1, 16, 275], "start_line": 1, "executed_branches": [], "missing_branches": []}}}, "pyWebLayout/core/highlight.py": {"executed_lines": [8, 9, 10, 11, 12, 13, 15, 17, 20, 22, 23, 24, 25, 26, 27, 28, 31, 32, 40, 43, 44, 47, 48, 49, 55, 58, 59, 60, 62, 64, 65, 67, 69, 82, 83, 85, 99, 106, 114, 115, 116, 119, 121, 128, 129, 131, 141, 142, 143, 144, 145, 147, 149, 151, 153, 155, 157, 158, 160, 171, 172, 174, 176, 177, 179, 180, 182, 184, 186, 188, 190, 195, 197, 198, 199, 209, 229, 230, 233, 234, 235, 237, 238, 240], "summary": {"covered_lines": 84, "num_statements": 87, "percent_covered": 96.96969696969697, "percent_covered_display": "97", "missing_lines": 3, "excluded_lines": 18, "percent_statements_covered": 96.55172413793103, "percent_statements_covered_display": "97", "num_branches": 12, "num_partial_branches": 0, "covered_branches": 12, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [203, 204, 206], "excluded_lines": [1, 21, 33, 63, 68, 84, 100, 107, 122, 132, 148, 152, 156, 162, 185, 189, 196, 216], "executed_branches": [[64, -62], [64, 65], [141, 142], [141, 145], [174, 176], [174, 182], [176, 174], [176, 177], [177, 176], [177, 179], [233, 234], [233, 237]], "missing_branches": [], "functions": {"Highlight.__post_init__": {"executed_lines": [64, 65], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [63], "start_line": 62, "executed_branches": [[64, -62], [64, 65]], "missing_branches": []}, "Highlight.to_dict": {"executed_lines": [69], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [68], "start_line": 67, "executed_branches": [], "missing_branches": []}, "Highlight.from_dict": {"executed_lines": [85], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [84], "start_line": 83, "executed_branches": [], "missing_branches": []}, "HighlightManager.__init__": {"executed_lines": [114, 115, 116, 119], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [107], "start_line": 106, "executed_branches": [], "missing_branches": []}, "HighlightManager.add_highlight": {"executed_lines": [128, 129], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [122], "start_line": 121, "executed_branches": [], "missing_branches": []}, "HighlightManager.remove_highlight": {"executed_lines": [141, 142, 143, 144, 145], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [132], "start_line": 131, "executed_branches": [[141, 142], [141, 145]], "missing_branches": []}, "HighlightManager.get_highlight": {"executed_lines": [149], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [148], "start_line": 147, "executed_branches": [], "missing_branches": []}, "HighlightManager.list_highlights": {"executed_lines": [153], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [152], "start_line": 151, "executed_branches": [], "missing_branches": []}, "HighlightManager.clear_all": {"executed_lines": [157, 158], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [156], "start_line": 155, "executed_branches": [], "missing_branches": []}, "HighlightManager.get_highlights_for_page": {"executed_lines": [171, 172, 174, 176, 177, 179, 180, 182], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [162], "start_line": 160, "executed_branches": [[174, 176], [174, 182], [176, 174], [176, 177], [177, 176], [177, 179]], "missing_branches": []}, "HighlightManager._get_filepath": {"executed_lines": [186], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [185], "start_line": 184, "executed_branches": [], "missing_branches": []}, "HighlightManager._save_highlights": {"executed_lines": [190], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [189], "start_line": 188, "executed_branches": [], "missing_branches": []}, "HighlightManager._load_highlights": {"executed_lines": [197, 198, 199], "summary": {"covered_lines": 3, "num_statements": 6, "percent_covered": 50.0, "percent_covered_display": "50", "missing_lines": 3, "excluded_lines": 1, "percent_statements_covered": 50.0, "percent_statements_covered_display": "50", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [203, 204, 206], "excluded_lines": [196], "start_line": 195, "executed_branches": [], "missing_branches": []}, "create_highlight_from_query_result": {"executed_lines": [229, 230, 233, 234, 235, 237, 238, 240], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [216], "start_line": 209, "executed_branches": [[233, 234], [233, 237]], "missing_branches": []}, "": {"executed_lines": [8, 9, 10, 11, 12, 13, 15, 17, 20, 22, 23, 24, 25, 26, 27, 28, 31, 32, 40, 43, 44, 47, 48, 49, 55, 58, 59, 60, 62, 67, 82, 83, 99, 106, 121, 131, 147, 151, 155, 160, 184, 188, 195, 209], "summary": {"covered_lines": 44, "num_statements": 44, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 4, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1, 21, 33, 100], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"HighlightColor": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 20, "executed_branches": [], "missing_branches": []}, "Highlight": {"executed_lines": [64, 65, 69, 85], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 3, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [63, 68, 84], "start_line": 32, "executed_branches": [[64, -62], [64, 65]], "missing_branches": []}, "HighlightManager": {"executed_lines": [114, 115, 116, 119, 128, 129, 141, 142, 143, 144, 145, 149, 153, 157, 158, 171, 172, 174, 176, 177, 179, 180, 182, 186, 190, 197, 198, 199], "summary": {"covered_lines": 28, "num_statements": 31, "percent_covered": 92.3076923076923, "percent_covered_display": "92", "missing_lines": 3, "excluded_lines": 10, "percent_statements_covered": 90.3225806451613, "percent_statements_covered_display": "90", "num_branches": 8, "num_partial_branches": 0, "covered_branches": 8, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [203, 204, 206], "excluded_lines": [107, 122, 132, 148, 152, 156, 162, 185, 189, 196], "start_line": 99, "executed_branches": [[141, 142], [141, 145], [174, 176], [174, 182], [176, 174], [176, 177], [177, 176], [177, 179]], "missing_branches": []}, "": {"executed_lines": [8, 9, 10, 11, 12, 13, 15, 17, 20, 22, 23, 24, 25, 26, 27, 28, 31, 32, 40, 43, 44, 47, 48, 49, 55, 58, 59, 60, 62, 67, 82, 83, 99, 106, 121, 131, 147, 151, 155, 160, 184, 188, 195, 209, 229, 230, 233, 234, 235, 237, 238, 240], "summary": {"covered_lines": 52, "num_statements": 52, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 5, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1, 21, 33, 100, 216], "start_line": 1, "executed_branches": [[233, 234], [233, 237]], "missing_branches": []}}}, "pyWebLayout/core/persistence.py": {"executed_lines": [10, 12, 13, 14, 15, 17, 20, 22, 23, 24, 27, 35, 36, 38, 39, 40, 41, 42, 43, 46, 53, 54, 55, 56], "summary": {"covered_lines": 24, "num_statements": 27, "percent_covered": 89.65517241379311, "percent_covered_display": "90", "missing_lines": 3, "excluded_lines": 4, "percent_statements_covered": 88.88888888888889, "percent_statements_covered_display": "89", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [57, 58, 59], "excluded_lines": [1, 21, 28, 47], "executed_branches": [[35, 36], [35, 38]], "missing_branches": [], "functions": {"ensure_dir": {"executed_lines": [22, 23, 24], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [21], "start_line": 20, "executed_branches": [], "missing_branches": []}, "read_json": {"executed_lines": [35, 36, 38, 39, 40, 41, 42, 43], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [28], "start_line": 27, "executed_branches": [[35, 36], [35, 38]], "missing_branches": []}, "write_json": {"executed_lines": [53, 54, 55, 56], "summary": {"covered_lines": 4, "num_statements": 7, "percent_covered": 57.142857142857146, "percent_covered_display": "57", "missing_lines": 3, "excluded_lines": 1, "percent_statements_covered": 57.142857142857146, "percent_statements_covered_display": "57", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [57, 58, 59], "excluded_lines": [47], "start_line": 46, "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [10, 12, 13, 14, 15, 17, 20, 27, 46], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [10, 12, 13, 14, 15, 17, 20, 22, 23, 24, 27, 35, 36, 38, 39, 40, 41, 42, 43, 46, 53, 54, 55, 56], "summary": {"covered_lines": 24, "num_statements": 27, "percent_covered": 89.65517241379311, "percent_covered_display": "90", "missing_lines": 3, "excluded_lines": 4, "percent_statements_covered": 88.88888888888889, "percent_statements_covered_display": "89", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [57, 58, 59], "excluded_lines": [1, 21, 28, 47], "start_line": 1, "executed_branches": [[35, 36], [35, 38]], "missing_branches": []}}}, "pyWebLayout/core/query.py": {"executed_lines": [9, 10, 11, 13, 17, 18, 26, 27, 30, 33, 34, 35, 38, 39, 40, 43, 44, 46, 48, 59, 60, 64, 65, 66, 68, 69, 71, 73, 74, 76, 78, 80], "summary": {"covered_lines": 32, "num_statements": 33, "percent_covered": 94.28571428571429, "percent_covered_display": "94", "missing_lines": 1, "excluded_lines": 7, "percent_statements_covered": 96.96969696969697, "percent_statements_covered_display": "97", "num_branches": 2, "num_partial_branches": 1, "covered_branches": 1, "missing_branches": 1, "percent_branches_covered": 50.0, "percent_branches_covered_display": "50"}, "missing_lines": [14], "excluded_lines": [1, 19, 47, 61, 70, 75, 79], "executed_branches": [[13, 17]], "missing_branches": [[13, 14]], "functions": {"QueryResult.to_dict": {"executed_lines": [48], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [47], "start_line": 46, "executed_branches": [], "missing_branches": []}, "SelectionRange.text": {"executed_lines": [71], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [70], "start_line": 69, "executed_branches": [], "missing_branches": []}, "SelectionRange.bounds_list": {"executed_lines": [76], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [75], "start_line": 74, "executed_branches": [], "missing_branches": []}, "SelectionRange.to_dict": {"executed_lines": [80], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [79], "start_line": 78, "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [9, 10, 11, 13, 17, 18, 26, 27, 30, 33, 34, 35, 38, 39, 40, 43, 44, 46, 59, 60, 64, 65, 66, 68, 69, 73, 74, 78], "summary": {"covered_lines": 28, "num_statements": 29, "percent_covered": 93.54838709677419, "percent_covered_display": "94", "missing_lines": 1, "excluded_lines": 3, "percent_statements_covered": 96.55172413793103, "percent_statements_covered_display": "97", "num_branches": 2, "num_partial_branches": 1, "covered_branches": 1, "missing_branches": 1, "percent_branches_covered": 50.0, "percent_branches_covered_display": "50"}, "missing_lines": [14], "excluded_lines": [1, 19, 61], "start_line": 1, "executed_branches": [[13, 17]], "missing_branches": [[13, 14]]}}, "classes": {"QueryResult": {"executed_lines": [48], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [47], "start_line": 18, "executed_branches": [], "missing_branches": []}, "SelectionRange": {"executed_lines": [71, 76, 80], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 3, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [70, 75, 79], "start_line": 60, "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [9, 10, 11, 13, 17, 18, 26, 27, 30, 33, 34, 35, 38, 39, 40, 43, 44, 46, 59, 60, 64, 65, 66, 68, 69, 73, 74, 78], "summary": {"covered_lines": 28, "num_statements": 29, "percent_covered": 93.54838709677419, "percent_covered_display": "94", "missing_lines": 1, "excluded_lines": 3, "percent_statements_covered": 96.55172413793103, "percent_statements_covered_display": "97", "num_branches": 2, "num_partial_branches": 1, "covered_branches": 1, "missing_branches": 1, "percent_branches_covered": 50.0, "percent_branches_covered_display": "50"}, "missing_lines": [14], "excluded_lines": [1, 19, 61], "start_line": 1, "executed_branches": [[13, 17]], "missing_branches": [[13, 14]]}}}, "pyWebLayout/io/__init__.py": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1], "executed_branches": [], "missing_branches": [], "functions": {"": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1], "start_line": 1, "executed_branches": [], "missing_branches": []}}}, "pyWebLayout/io/readers/__init__.py": {"executed_lines": [8, 11], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1], "executed_branches": [], "missing_branches": [], "functions": {"": {"executed_lines": [8, 11], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [8, 11], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1], "start_line": 1, "executed_branches": [], "missing_branches": []}}}, "pyWebLayout/io/readers/epub_reader.py": {"executed_lines": [8, 9, 10, 11, 12, 13, 14, 16, 17, 18, 22, 31, 55, 63, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 86, 93, 95, 96, 97, 98, 99, 102, 105, 107, 111, 112, 113, 115, 117, 118, 121, 122, 123, 124, 127, 129, 130, 131, 133, 144, 147, 148, 149, 150, 151, 152, 153, 154, 156, 160, 161, 164, 167, 170, 172, 180, 181, 185, 186, 188, 189, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 211, 214, 215, 216, 218, 222, 230, 231, 235, 236, 237, 238, 240, 242, 243, 245, 251, 259, 260, 264, 265, 266, 269, 271, 272, 273, 275, 277, 290, 298, 299, 302, 303, 307, 309, 317, 319, 320, 323, 324, 326, 329, 330, 333, 342, 345, 348, 350, 353, 354, 356, 357, 359, 360, 362, 363, 367, 368, 372, 373, 375, 376, 378, 379, 381, 383, 384, 447, 457, 458, 459, 461, 462, 464, 465, 467, 468, 469, 471, 472, 479, 480, 482, 494, 501, 502, 504, 507, 510, 512, 513, 514, 516, 517, 518, 521, 524, 528, 529, 530, 533, 534, 535, 538, 541, 544, 545, 546, 549, 550, 553, 555, 556, 559, 562, 565, 566, 570, 592, 602, 603], "summary": {"covered_lines": 210, "num_statements": 286, "percent_covered": 70.23809523809524, "percent_covered_display": "70", "missing_lines": 76, "excluded_lines": 18, "percent_statements_covered": 73.42657342657343, "percent_statements_covered_display": "73", "num_branches": 134, "num_partial_branches": 27, "covered_branches": 85, "missing_branches": 49, "percent_branches_covered": 63.43283582089552, "percent_branches_covered_display": "63"}, "missing_lines": [43, 44, 47, 50, 52, 136, 137, 138, 139, 142, 157, 182, 220, 232, 261, 282, 283, 284, 285, 286, 287, 288, 295, 304, 343, 387, 388, 391, 392, 393, 396, 398, 400, 401, 402, 407, 408, 411, 412, 415, 418, 419, 420, 421, 422, 426, 429, 430, 434, 437, 439, 440, 441, 442, 444, 445, 473, 477, 478, 485, 486, 487, 488, 489, 522, 531, 572, 573, 575, 576, 577, 578, 580, 581, 587, 589], "excluded_lines": [1, 32, 56, 65, 87, 116, 145, 173, 223, 252, 276, 310, 351, 382, 448, 495, 505, 593], "executed_branches": [[122, 123], [127, 129], [130, 131], [148, 149], [149, 150], [150, 149], [150, 151], [153, 154], [156, 160], [181, 185], [185, 186], [185, 214], [186, 185], [186, 188], [191, 192], [191, 193], [193, 194], [193, 195], [195, 196], [195, 197], [197, 198], [197, 199], [199, 200], [199, 203], [200, 201], [200, 202], [203, 204], [203, 205], [205, 206], [205, 207], [207, 208], [207, 211], [214, -172], [214, 215], [218, 214], [231, 235], [235, -222], [235, 236], [240, 242], [260, 264], [265, 266], [269, -251], [269, 271], [272, 273], [277, 290], [290, 298], [303, 307], [317, -309], [317, 319], [342, 345], [353, 354], [356, 357], [356, 359], [359, 360], [359, 362], [362, 363], [362, 367], [367, 368], [367, 372], [372, 373], [372, 375], [375, 376], [378, -350], [378, 379], [383, 384], [461, -447], [461, 462], [462, 461], [462, 464], [464, 465], [467, 468], [501, -494], [501, 502], [513, -512], [513, 514], [514, 516], [521, 513], [529, -504], [529, 530], [530, 533], [538, 541], [538, 544], [545, 546], [565, 566], [565, 570]], "missing_branches": [[43, 44], [43, 47], [122, 136], [127, 136], [130, 127], [136, 137], [136, 142], [137, 136], [137, 138], [148, 156], [149, 153], [153, 148], [156, 157], [181, 182], [218, 220], [231, 232], [240, 235], [260, 261], [265, 269], [272, 269], [277, 282], [282, 283], [282, 290], [283, 284], [283, 287], [284, 283], [284, 285], [287, 282], [287, 288], [290, 295], [303, 304], [342, 343], [353, 356], [375, 378], [383, 387], [391, 392], [391, 396], [418, 419], [418, 426], [444, -381], [444, 445], [464, 485], [467, 485], [485, 461], [485, 486], [514, 521], [521, 522], [530, 531], [545, 549]], "functions": {"default_eink_processor": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 5, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 5, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 2, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [43, 44, 47, 50, 52], "excluded_lines": [32], "start_line": 31, "executed_branches": [], "missing_branches": [[43, 44], [43, 47]]}, "EPUBReader.__init__": {"executed_lines": [75, 76, 77, 78, 79, 80, 81, 82, 83, 84], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [65], "start_line": 63, "executed_branches": [], "missing_branches": []}, "EPUBReader.read": {"executed_lines": [93, 95, 96, 97, 98, 99, 102, 105, 107, 111, 112, 113], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [87], "start_line": 86, "executed_branches": [], "missing_branches": []}, "EPUBReader._extract_epub": {"executed_lines": [117, 118, 121, 122, 123, 124, 127, 129, 130, 131, 133], "summary": {"covered_lines": 11, "num_statements": 16, "percent_covered": 53.84615384615385, "percent_covered_display": "54", "missing_lines": 5, "excluded_lines": 1, "percent_statements_covered": 68.75, "percent_statements_covered_display": "69", "num_branches": 10, "num_partial_branches": 3, "covered_branches": 3, "missing_branches": 7, "percent_branches_covered": 30.0, "percent_branches_covered_display": "30"}, "missing_lines": [136, 137, 138, 139, 142], "excluded_lines": [116], "start_line": 115, "executed_branches": [[122, 123], [127, 129], [130, 131]], "missing_branches": [[122, 136], [127, 136], [130, 127], [136, 137], [136, 142], [137, 136], [137, 138]]}, "EPUBReader._parse_package_document": {"executed_lines": [147, 148, 149, 150, 151, 152, 153, 154, 156, 160, 161, 164, 167, 170], "summary": {"covered_lines": 14, "num_statements": 15, "percent_covered": 80.0, "percent_covered_display": "80", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 93.33333333333333, "percent_statements_covered_display": "93", "num_branches": 10, "num_partial_branches": 4, "covered_branches": 6, "missing_branches": 4, "percent_branches_covered": 60.0, "percent_branches_covered_display": "60"}, "missing_lines": [157], "excluded_lines": [145], "start_line": 144, "executed_branches": [[148, 149], [149, 150], [150, 149], [150, 151], [153, 154], [156, 160]], "missing_branches": [[148, 156], [149, 153], [153, 148], [156, 157]]}, "EPUBReader._parse_metadata": {"executed_lines": [180, 181, 185, 186, 188, 189, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 211, 214, 215, 216, 218], "summary": {"covered_lines": 29, "num_statements": 31, "percent_covered": 93.22033898305085, "percent_covered_display": "93", "missing_lines": 2, "excluded_lines": 1, "percent_statements_covered": 93.54838709677419, "percent_statements_covered_display": "94", "num_branches": 28, "num_partial_branches": 2, "covered_branches": 26, "missing_branches": 2, "percent_branches_covered": 92.85714285714286, "percent_branches_covered_display": "93"}, "missing_lines": [182, 220], "excluded_lines": [173], "start_line": 172, "executed_branches": [[181, 185], [185, 186], [185, 214], [186, 185], [186, 188], [191, 192], [191, 193], [193, 194], [193, 195], [195, 196], [195, 197], [197, 198], [197, 199], [199, 200], [199, 203], [200, 201], [200, 202], [203, 204], [203, 205], [205, 206], [205, 207], [207, 208], [207, 211], [214, -172], [214, 215], [218, 214]], "missing_branches": [[181, 182], [218, 220]]}, "EPUBReader._parse_manifest": {"executed_lines": [230, 231, 235, 236, 237, 238, 240, 242, 243, 245], "summary": {"covered_lines": 10, "num_statements": 11, "percent_covered": 82.3529411764706, "percent_covered_display": "82", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 90.9090909090909, "percent_statements_covered_display": "91", "num_branches": 6, "num_partial_branches": 2, "covered_branches": 4, "missing_branches": 2, "percent_branches_covered": 66.66666666666667, "percent_branches_covered_display": "67"}, "missing_lines": [232], "excluded_lines": [223], "start_line": 222, "executed_branches": [[231, 235], [235, -222], [235, 236], [240, 242]], "missing_branches": [[231, 232], [240, 235]]}, "EPUBReader._parse_spine": {"executed_lines": [259, 260, 264, 265, 266, 269, 271, 272, 273], "summary": {"covered_lines": 9, "num_statements": 10, "percent_covered": 77.77777777777777, "percent_covered_display": "78", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 90.0, "percent_statements_covered_display": "90", "num_branches": 8, "num_partial_branches": 3, "covered_branches": 5, "missing_branches": 3, "percent_branches_covered": 62.5, "percent_branches_covered_display": "62"}, "missing_lines": [261], "excluded_lines": [252], "start_line": 251, "executed_branches": [[260, 264], [265, 266], [269, -251], [269, 271], [272, 273]], "missing_branches": [[260, 261], [265, 269], [272, 269]]}, "EPUBReader._parse_toc": {"executed_lines": [277, 290, 298, 299, 302, 303, 307], "summary": {"covered_lines": 7, "num_statements": 16, "percent_covered": 33.333333333333336, "percent_covered_display": "33", "missing_lines": 9, "excluded_lines": 1, "percent_statements_covered": 43.75, "percent_statements_covered_display": "44", "num_branches": 14, "num_partial_branches": 3, "covered_branches": 3, "missing_branches": 11, "percent_branches_covered": 21.428571428571427, "percent_branches_covered_display": "21"}, "missing_lines": [282, 283, 284, 285, 286, 287, 288, 295, 304], "excluded_lines": [276], "start_line": 275, "executed_branches": [[277, 290], [290, 298], [303, 307]], "missing_branches": [[277, 282], [282, 283], [282, 290], [283, 284], [283, 287], [284, 283], [284, 285], [287, 282], [287, 288], [290, 295], [303, 304]]}, "EPUBReader._parse_nav_points": {"executed_lines": [317, 319, 320, 323, 324, 326, 329, 330, 333, 342, 345, 348], "summary": {"covered_lines": 12, "num_statements": 13, "percent_covered": 88.23529411764706, "percent_covered_display": "88", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 92.3076923076923, "percent_statements_covered_display": "92", "num_branches": 4, "num_partial_branches": 1, "covered_branches": 3, "missing_branches": 1, "percent_branches_covered": 75.0, "percent_branches_covered_display": "75"}, "missing_lines": [343], "excluded_lines": [310], "start_line": 309, "executed_branches": [[317, -309], [317, 319], [342, 345]], "missing_branches": [[342, 343]]}, "EPUBReader._create_book": {"executed_lines": [353, 354, 356, 357, 359, 360, 362, 363, 367, 368, 372, 373, 375, 376, 378, 379], "summary": {"covered_lines": 16, "num_statements": 16, "percent_covered": 93.75, "percent_covered_display": "94", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 16, "num_partial_branches": 2, "covered_branches": 14, "missing_branches": 2, "percent_branches_covered": 87.5, "percent_branches_covered_display": "88"}, "missing_lines": [], "excluded_lines": [351], "start_line": 350, "executed_branches": [[353, 354], [356, 357], [356, 359], [359, 360], [359, 362], [362, 363], [362, 367], [367, 368], [367, 372], [372, 373], [372, 375], [375, 376], [378, -350], [378, 379]], "missing_branches": [[353, 356], [375, 378]]}, "EPUBReader._add_cover_chapter": {"executed_lines": [383, 384], "summary": {"covered_lines": 2, "num_statements": 33, "percent_covered": 7.317073170731708, "percent_covered_display": "7", "missing_lines": 31, "excluded_lines": 1, "percent_statements_covered": 6.0606060606060606, "percent_statements_covered_display": "6", "num_branches": 8, "num_partial_branches": 1, "covered_branches": 1, "missing_branches": 7, "percent_branches_covered": 12.5, "percent_branches_covered_display": "12"}, "missing_lines": [387, 388, 391, 392, 393, 396, 398, 400, 401, 402, 407, 408, 411, 412, 415, 418, 419, 420, 421, 422, 426, 429, 430, 434, 437, 439, 440, 441, 442, 444, 445], "excluded_lines": [382], "start_line": 381, "executed_branches": [[383, 384]], "missing_branches": [[383, 387], [391, 392], [391, 396], [418, 419], [418, 426], [444, -381], [444, 445]]}, "EPUBReader._process_chapter_images": {"executed_lines": [457, 458, 459, 461, 462, 464, 465, 467, 468, 469, 471, 472, 479, 480, 482], "summary": {"covered_lines": 15, "num_statements": 23, "percent_covered": 63.63636363636363, "percent_covered_display": "64", "missing_lines": 8, "excluded_lines": 1, "percent_statements_covered": 65.21739130434783, "percent_statements_covered_display": "65", "num_branches": 10, "num_partial_branches": 2, "covered_branches": 6, "missing_branches": 4, "percent_branches_covered": 60.0, "percent_branches_covered_display": "60"}, "missing_lines": [473, 477, 478, 485, 486, 487, 488, 489], "excluded_lines": [448], "start_line": 447, "executed_branches": [[461, -447], [461, 462], [462, 461], [462, 464], [464, 465], [467, 468]], "missing_branches": [[464, 485], [467, 485], [485, 461], [485, 486]]}, "EPUBReader._process_content_images": {"executed_lines": [501, 502], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [495], "start_line": 494, "executed_branches": [[501, -494], [501, 502]], "missing_branches": []}, "EPUBReader._add_chapters": {"executed_lines": [507, 510, 512, 524, 528, 529, 530, 533, 534, 535, 538, 541, 544, 545, 546, 549, 550, 553, 555, 556, 559, 562, 565, 566, 570], "summary": {"covered_lines": 25, "num_statements": 36, "percent_covered": 71.73913043478261, "percent_covered_display": "72", "missing_lines": 11, "excluded_lines": 1, "percent_statements_covered": 69.44444444444444, "percent_statements_covered_display": "69", "num_branches": 10, "num_partial_branches": 2, "covered_branches": 8, "missing_branches": 2, "percent_branches_covered": 80.0, "percent_branches_covered_display": "80"}, "missing_lines": [531, 572, 573, 575, 576, 577, 578, 580, 581, 587, 589], "excluded_lines": [505], "start_line": 504, "executed_branches": [[529, -504], [529, 530], [530, 533], [538, 541], [538, 544], [545, 546], [565, 566], [565, 570]], "missing_branches": [[530, 531], [545, 549]]}, "EPUBReader._add_chapters.add_to_toc_map": {"executed_lines": [513, 514, 516, 517, 518, 521], "summary": {"covered_lines": 6, "num_statements": 7, "percent_covered": 76.92307692307692, "percent_covered_display": "77", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 85.71428571428571, "percent_statements_covered_display": "86", "num_branches": 6, "num_partial_branches": 2, "covered_branches": 4, "missing_branches": 2, "percent_branches_covered": 66.66666666666667, "percent_branches_covered_display": "67"}, "missing_lines": [522], "excluded_lines": [], "start_line": 512, "executed_branches": [[513, -512], [513, 514], [514, 516], [521, 513]], "missing_branches": [[514, 521], [521, 522]]}, "read_epub": {"executed_lines": [602, 603], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [593], "start_line": 592, "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [8, 9, 10, 11, 12, 13, 14, 16, 17, 18, 22, 31, 55, 63, 86, 115, 144, 172, 222, 251, 275, 309, 350, 381, 447, 494, 504, 592], "summary": {"covered_lines": 28, "num_statements": 28, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 2, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1, 56], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"EPUBReader": {"executed_lines": [75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 93, 95, 96, 97, 98, 99, 102, 105, 107, 111, 112, 113, 117, 118, 121, 122, 123, 124, 127, 129, 130, 131, 133, 147, 148, 149, 150, 151, 152, 153, 154, 156, 160, 161, 164, 167, 170, 180, 181, 185, 186, 188, 189, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 211, 214, 215, 216, 218, 230, 231, 235, 236, 237, 238, 240, 242, 243, 245, 259, 260, 264, 265, 266, 269, 271, 272, 273, 277, 290, 298, 299, 302, 303, 307, 317, 319, 320, 323, 324, 326, 329, 330, 333, 342, 345, 348, 353, 354, 356, 357, 359, 360, 362, 363, 367, 368, 372, 373, 375, 376, 378, 379, 383, 384, 457, 458, 459, 461, 462, 464, 465, 467, 468, 469, 471, 472, 479, 480, 482, 501, 502, 507, 510, 512, 513, 514, 516, 517, 518, 521, 524, 528, 529, 530, 533, 534, 535, 538, 541, 544, 545, 546, 549, 550, 553, 555, 556, 559, 562, 565, 566, 570], "summary": {"covered_lines": 180, "num_statements": 251, "percent_covered": 69.19060052219321, "percent_covered_display": "69", "missing_lines": 71, "excluded_lines": 14, "percent_statements_covered": 71.71314741035856, "percent_statements_covered_display": "72", "num_branches": 132, "num_partial_branches": 27, "covered_branches": 85, "missing_branches": 47, "percent_branches_covered": 64.39393939393939, "percent_branches_covered_display": "64"}, "missing_lines": [136, 137, 138, 139, 142, 157, 182, 220, 232, 261, 282, 283, 284, 285, 286, 287, 288, 295, 304, 343, 387, 388, 391, 392, 393, 396, 398, 400, 401, 402, 407, 408, 411, 412, 415, 418, 419, 420, 421, 422, 426, 429, 430, 434, 437, 439, 440, 441, 442, 444, 445, 473, 477, 478, 485, 486, 487, 488, 489, 522, 531, 572, 573, 575, 576, 577, 578, 580, 581, 587, 589], "excluded_lines": [65, 87, 116, 145, 173, 223, 252, 276, 310, 351, 382, 448, 495, 505], "start_line": 55, "executed_branches": [[122, 123], [127, 129], [130, 131], [148, 149], [149, 150], [150, 149], [150, 151], [153, 154], [156, 160], [181, 185], [185, 186], [185, 214], [186, 185], [186, 188], [191, 192], [191, 193], [193, 194], [193, 195], [195, 196], [195, 197], [197, 198], [197, 199], [199, 200], [199, 203], [200, 201], [200, 202], [203, 204], [203, 205], [205, 206], [205, 207], [207, 208], [207, 211], [214, -172], [214, 215], [218, 214], [231, 235], [235, -222], [235, 236], [240, 242], [260, 264], [265, 266], [269, -251], [269, 271], [272, 273], [277, 290], [290, 298], [303, 307], [317, -309], [317, 319], [342, 345], [353, 354], [356, 357], [356, 359], [359, 360], [359, 362], [362, 363], [362, 367], [367, 368], [367, 372], [372, 373], [372, 375], [375, 376], [378, -350], [378, 379], [383, 384], [461, -447], [461, 462], [462, 461], [462, 464], [464, 465], [467, 468], [501, -494], [501, 502], [513, -512], [513, 514], [514, 516], [521, 513], [529, -504], [529, 530], [530, 533], [538, 541], [538, 544], [545, 546], [565, 566], [565, 570]], "missing_branches": [[122, 136], [127, 136], [130, 127], [136, 137], [136, 142], [137, 136], [137, 138], [148, 156], [149, 153], [153, 148], [156, 157], [181, 182], [218, 220], [231, 232], [240, 235], [260, 261], [265, 269], [272, 269], [277, 282], [282, 283], [282, 290], [283, 284], [283, 287], [284, 283], [284, 285], [287, 282], [287, 288], [290, 295], [303, 304], [342, 343], [353, 356], [375, 378], [383, 387], [391, 392], [391, 396], [418, 419], [418, 426], [444, -381], [444, 445], [464, 485], [467, 485], [485, 461], [485, 486], [514, 521], [521, 522], [530, 531], [545, 549]]}, "": {"executed_lines": [8, 9, 10, 11, 12, 13, 14, 16, 17, 18, 22, 31, 55, 63, 86, 115, 144, 172, 222, 251, 275, 309, 350, 381, 447, 494, 504, 592, 602, 603], "summary": {"covered_lines": 30, "num_statements": 35, "percent_covered": 81.08108108108108, "percent_covered_display": "81", "missing_lines": 5, "excluded_lines": 4, "percent_statements_covered": 85.71428571428571, "percent_statements_covered_display": "86", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 2, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [43, 44, 47, 50, 52], "excluded_lines": [1, 32, 56, 593], "start_line": 1, "executed_branches": [], "missing_branches": [[43, 44], [43, 47]]}}}, "pyWebLayout/io/readers/html_extraction.py": {"executed_lines": [9, 10, 11, 12, 13, 29, 32, 38, 39, 40, 41, 42, 43, 44, 45, 47, 49, 51, 55, 57, 59, 61, 63, 65, 67, 69, 71, 74, 90, 91, 92, 94, 96, 108, 119, 120, 123, 126, 127, 128, 133, 134, 137, 138, 139, 140, 141, 144, 146, 149, 150, 152, 155, 165, 166, 167, 168, 169, 170, 173, 192, 209, 210, 211, 212, 213, 214, 215, 216, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 231, 233, 234, 235, 236, 239, 245, 246, 247, 248, 252, 253, 254, 255, 256, 257, 259, 260, 261, 262, 263, 265, 266, 268, 270, 271, 278, 279, 280, 281, 282, 283, 284, 285, 290, 293, 296, 297, 298, 304, 315, 316, 331, 343, 356, 357, 358, 362, 365, 376, 379, 394, 395, 397, 399, 402, 405, 409, 410, 411, 413, 415, 416, 417, 419, 420, 421, 422, 423, 424, 426, 429, 432, 433, 435, 436, 437, 445, 448, 449, 450, 453, 473, 474, 475, 479, 480, 481, 486, 490, 496, 502, 512, 513, 514, 515, 516, 519, 539, 540, 542, 544, 545, 546, 547, 548, 549, 550, 551, 552, 554, 556, 557, 558, 560, 561, 562, 564, 565, 567, 568, 569, 570, 571, 572, 574, 576, 577, 580, 593, 594, 595, 603, 613, 615, 617, 621, 623, 625, 626, 627, 628, 629, 632, 633, 634, 638, 639, 640, 642, 643, 644, 645, 652, 653, 654, 655, 656, 657, 659, 662, 663, 664, 665, 666, 669, 671, 674, 676, 685, 686, 687, 688, 689, 690, 693, 695, 696, 697, 698, 701, 703, 704, 707, 708, 709, 711, 714, 724, 726, 727, 728, 729, 730, 731, 732, 733, 736, 738, 739, 740, 741, 742, 743, 744, 745, 748, 750, 751, 752, 753, 756, 758, 759, 760, 763, 766, 767, 768, 769, 770, 771, 772, 773, 774, 775, 777, 778, 779, 780, 781, 783, 786, 788, 789, 790, 791, 792, 793, 794, 795, 798, 800, 801, 802, 804, 805, 807, 810, 812, 813, 814, 816, 817, 819, 822, 824, 827, 830, 833, 835, 836, 838, 839, 842, 844, 846, 849, 850, 851, 852, 853, 854, 858, 861, 863, 866, 868, 872, 935, 950, 951, 953, 956, 958, 959, 960, 961, 962, 963, 964, 966, 968], "summary": {"covered_lines": 372, "num_statements": 400, "percent_covered": 89.47368421052632, "percent_covered_display": "89", "missing_lines": 28, "excluded_lines": 38, "percent_statements_covered": 93.0, "percent_statements_covered_display": "93", "num_branches": 208, "num_partial_branches": 24, "covered_branches": 172, "missing_branches": 36, "percent_branches_covered": 82.6923076923077, "percent_branches_covered_display": "83"}, "missing_lines": [237, 238, 240, 241, 242, 243, 249, 250, 264, 286, 287, 301, 318, 359, 403, 482, 483, 484, 485, 487, 488, 649, 717, 718, 721, 761, 855, 856], "excluded_lines": [1, 33, 48, 54, 58, 62, 66, 70, 78, 109, 156, 178, 346, 366, 380, 503, 520, 543, 583, 604, 670, 675, 694, 702, 715, 725, 737, 749, 757, 787, 799, 811, 823, 828, 834, 862, 867, 938], "executed_branches": [[90, 91], [90, 96], [91, 92], [91, 94], [127, 128], [127, 134], [138, 139], [138, 141], [166, 167], [166, 170], [167, 166], [167, 168], [219, 220], [219, 231], [221, 222], [221, 223], [223, 224], [223, 225], [225, 226], [225, 227], [227, 228], [227, 231], [231, 233], [231, 245], [234, 235], [234, 239], [239, 245], [245, 246], [245, 252], [247, 248], [252, 253], [252, 259], [254, 255], [254, 256], [256, 257], [259, 260], [259, 268], [261, 262], [261, 263], [263, 265], [265, 266], [268, 270], [268, 290], [278, 279], [278, 280], [280, 281], [280, 290], [290, 293], [290, 316], [297, 298], [316, 331], [356, 357], [356, 362], [358, 362], [399, 402], [399, 490], [402, 405], [405, 409], [405, 413], [413, 415], [415, 416], [415, 453], [417, 419], [417, 448], [419, 420], [419, 421], [421, 422], [421, 423], [423, 424], [423, 426], [435, 399], [435, 436], [436, 437], [453, 473], [453, 479], [481, 486], [486, 399], [512, 513], [512, 514], [514, 515], [514, 516], [544, 545], [544, 546], [548, -542], [548, 549], [550, 551], [550, 552], [554, 556], [554, 576], [556, 557], [556, 560], [560, 561], [560, 564], [564, 565], [564, 567], [570, 554], [570, 571], [571, 572], [571, 574], [615, 617], [615, 662], [623, 625], [623, 638], [625, 626], [625, 632], [628, 629], [632, 633], [632, 634], [638, 639], [638, 652], [639, 638], [639, 640], [640, 642], [644, 645], [654, 655], [655, 656], [655, 657], [664, 665], [664, 666], [688, 689], [688, 690], [696, 697], [696, 698], [708, 709], [708, 711], [727, 728], [727, 733], [728, 727], [728, 729], [731, 732], [739, 740], [739, 745], [740, 739], [740, 741], [743, 744], [751, 752], [751, 753], [760, 763], [766, 767], [766, 783], [767, 766], [767, 768], [768, 769], [768, 773], [771, 772], [773, 774], [777, 766], [777, 778], [780, 781], [789, 790], [789, 795], [790, 789], [790, 791], [793, 794], [804, 805], [804, 807], [816, 817], [816, 819], [842, 844], [842, 849], [851, 852], [851, 853], [853, 854], [853, 858], [958, 959], [958, 968], [959, 958], [959, 960], [962, 958], [962, 963], [963, 964], [963, 966]], "missing_branches": [[239, 240], [247, 249], [249, 250], [249, 252], [256, 259], [263, 264], [265, 268], [297, 301], [316, 318], [358, 359], [402, 403], [413, 399], [436, 435], [481, 482], [482, 399], [482, 483], [483, 482], [483, 484], [484, 482], [484, 485], [486, 487], [487, 399], [487, 488], [628, 625], [640, 649], [644, 638], [654, 659], [717, 718], [717, 721], [731, 727], [743, 739], [760, 761], [771, 766], [773, 766], [780, 777], [793, 789]], "functions": {"StyleContext.with_font": {"executed_lines": [49], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [48], "start_line": 47, "executed_branches": [], "missing_branches": []}, "StyleContext.with_background": {"executed_lines": [55], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [54], "start_line": 51, "executed_branches": [], "missing_branches": []}, "StyleContext.with_css_classes": {"executed_lines": [59], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [58], "start_line": 57, "executed_branches": [], "missing_branches": []}, "StyleContext.with_css_styles": {"executed_lines": [63], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [62], "start_line": 61, "executed_branches": [], "missing_branches": []}, "StyleContext.with_attributes": {"executed_lines": [67], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [66], "start_line": 65, "executed_branches": [], "missing_branches": []}, "StyleContext.push_element": {"executed_lines": [71], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [70], "start_line": 69, "executed_branches": [], "missing_branches": []}, "create_base_context": {"executed_lines": [90, 91, 92, 94, 96], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [78], "start_line": 74, "executed_branches": [[90, 91], [90, 96], [91, 92], [91, 94]], "missing_branches": []}, "apply_element_styling": {"executed_lines": [119, 120, 123, 126, 127, 128, 133, 134, 137, 138, 139, 140, 141, 144, 146, 149, 150, 152], "summary": {"covered_lines": 18, "num_statements": 18, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [109], "start_line": 108, "executed_branches": [[127, 128], [127, 134], [138, 139], [138, 141]], "missing_branches": []}, "parse_inline_styles": {"executed_lines": [165, 166, 167, 168, 169, 170], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [156], "start_line": 155, "executed_branches": [[166, 167], [166, 170], [167, 166], [167, 168]], "missing_branches": []}, "apply_element_font_styles": {"executed_lines": [192, 209, 210, 211, 212, 213, 214, 215, 216, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 231, 233, 234, 235, 236, 239, 245, 246, 247, 248, 252, 253, 254, 255, 256, 257, 259, 260, 261, 262, 263, 265, 266, 268, 270, 271, 278, 279, 280, 281, 282, 283, 284, 285, 290, 293, 296, 297, 298, 304, 315, 316, 331], "summary": {"covered_lines": 62, "num_statements": 75, "percent_covered": 82.11382113821138, "percent_covered_display": "82", "missing_lines": 13, "excluded_lines": 1, "percent_statements_covered": 82.66666666666667, "percent_statements_covered_display": "83", "num_branches": 48, "num_partial_branches": 7, "covered_branches": 39, "missing_branches": 9, "percent_branches_covered": 81.25, "percent_branches_covered_display": "81"}, "missing_lines": [237, 238, 240, 241, 242, 243, 249, 250, 264, 286, 287, 301, 318], "excluded_lines": [178], "start_line": 173, "executed_branches": [[219, 220], [219, 231], [221, 222], [221, 223], [223, 224], [223, 225], [225, 226], [225, 227], [227, 228], [227, 231], [231, 233], [231, 245], [234, 235], [234, 239], [239, 245], [245, 246], [245, 252], [247, 248], [252, 253], [252, 259], [254, 255], [254, 256], [256, 257], [259, 260], [259, 268], [261, 262], [261, 263], [263, 265], [265, 266], [268, 270], [268, 290], [278, 279], [278, 280], [280, 281], [280, 290], [290, 293], [290, 316], [297, 298], [316, 331]], "missing_branches": [[239, 240], [247, 249], [249, 250], [249, 252], [256, 259], [263, 264], [265, 268], [297, 301], [316, 318]]}, "apply_background_styles": {"executed_lines": [356, 357, 358, 362], "summary": {"covered_lines": 4, "num_statements": 5, "percent_covered": 77.77777777777777, "percent_covered_display": "78", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 80.0, "percent_statements_covered_display": "80", "num_branches": 4, "num_partial_branches": 1, "covered_branches": 3, "missing_branches": 1, "percent_branches_covered": 75.0, "percent_branches_covered_display": "75"}, "missing_lines": [359], "excluded_lines": [346], "start_line": 343, "executed_branches": [[356, 357], [356, 362], [358, 362]], "missing_branches": [[358, 359]]}, "extract_text_content": {"executed_lines": [376], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [366], "start_line": 365, "executed_branches": [], "missing_branches": []}, "extract_words_from_nodes": {"executed_lines": [394, 395, 397, 399, 402, 405, 409, 410, 411, 413, 415, 416, 417, 419, 420, 421, 422, 423, 424, 426, 429, 432, 433, 435, 436, 437, 445, 448, 449, 450, 453, 473, 474, 475, 479, 480, 481, 486, 490], "summary": {"covered_lines": 39, "num_statements": 46, "percent_covered": 75.60975609756098, "percent_covered_display": "76", "missing_lines": 7, "excluded_lines": 1, "percent_statements_covered": 84.78260869565217, "percent_statements_covered_display": "85", "num_branches": 36, "num_partial_branches": 5, "covered_branches": 23, "missing_branches": 13, "percent_branches_covered": 63.888888888888886, "percent_branches_covered_display": "64"}, "missing_lines": [403, 482, 483, 484, 485, 487, 488], "excluded_lines": [380], "start_line": 379, "executed_branches": [[399, 402], [399, 490], [402, 405], [405, 409], [405, 413], [413, 415], [415, 416], [415, 453], [417, 419], [417, 448], [419, 420], [419, 421], [421, 422], [421, 423], [423, 424], [423, 426], [435, 399], [435, 436], [436, 437], [453, 473], [453, 479], [481, 486], [486, 399]], "missing_branches": [[402, 403], [413, 399], [436, 435], [481, 482], [482, 399], [482, 483], [483, 482], [483, 484], [484, 482], [484, 485], [486, 487], [487, 399], [487, 488]]}, "is_inline": {"executed_lines": [512, 513, 514, 515, 516], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [503], "start_line": 502, "executed_branches": [[512, 513], [512, 514], [514, 515], [514, 516]], "missing_branches": []}, "process_block_children": {"executed_lines": [539, 540, 542, 554, 556, 557, 558, 560, 561, 562, 564, 565, 567, 568, 569, 570, 571, 572, 574, 576, 577], "summary": {"covered_lines": 21, "num_statements": 21, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 12, "num_partial_branches": 0, "covered_branches": 12, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [520], "start_line": 519, "executed_branches": [[554, 556], [554, 576], [556, 557], [556, 560], [560, 561], [560, 564], [564, 565], [564, 567], [570, 554], [570, 571], [571, 572], [571, 574]], "missing_branches": []}, "process_block_children.flush_run": {"executed_lines": [544, 545, 546, 547, 548, 549, 550, 551, 552], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [543], "start_line": 542, "executed_branches": [[544, 545], [544, 546], [548, -542], [548, 549], [550, 551], [550, 552]], "missing_branches": []}, "process_element": {"executed_lines": [593, 594, 595], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [583], "start_line": 580, "executed_branches": [], "missing_branches": []}, "paragraph_handler": {"executed_lines": [613, 615, 617, 621, 623, 625, 626, 627, 628, 629, 632, 633, 634, 638, 639, 640, 642, 643, 644, 645, 652, 653, 654, 655, 656, 657, 659, 662, 663, 664, 665, 666], "summary": {"covered_lines": 32, "num_statements": 33, "percent_covered": 91.2280701754386, "percent_covered_display": "91", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 96.96969696969697, "percent_statements_covered_display": "97", "num_branches": 24, "num_partial_branches": 4, "covered_branches": 20, "missing_branches": 4, "percent_branches_covered": 83.33333333333333, "percent_branches_covered_display": "83"}, "missing_lines": [649], "excluded_lines": [604], "start_line": 603, "executed_branches": [[615, 617], [615, 662], [623, 625], [623, 638], [625, 626], [625, 632], [628, 629], [632, 633], [632, 634], [638, 639], [638, 652], [639, 638], [639, 640], [640, 642], [644, 645], [654, 655], [655, 656], [655, 657], [664, 665], [664, 666]], "missing_branches": [[628, 625], [640, 649], [644, 638], [654, 659]]}, "div_handler": {"executed_lines": [671], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [670], "start_line": 669, "executed_branches": [], "missing_branches": []}, "heading_handler": {"executed_lines": [676, 685, 686, 687, 688, 689, 690], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [675], "start_line": 674, "executed_branches": [[688, 689], [688, 690]], "missing_branches": []}, "blockquote_handler": {"executed_lines": [695, 696, 697, 698], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [694], "start_line": 693, "executed_branches": [[696, 697], [696, 698]], "missing_branches": []}, "preformatted_handler": {"executed_lines": [703, 704, 707, 708, 709, 711], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [702], "start_line": 701, "executed_branches": [[708, 709], [708, 711]], "missing_branches": []}, "code_handler": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 3, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 2, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [717, 718, 721], "excluded_lines": [715], "start_line": 714, "executed_branches": [], "missing_branches": [[717, 718], [717, 721]]}, "unordered_list_handler": {"executed_lines": [726, 727, 728, 729, 730, 731, 732, 733], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 92.85714285714286, "percent_covered_display": "93", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 6, "num_partial_branches": 1, "covered_branches": 5, "missing_branches": 1, "percent_branches_covered": 83.33333333333333, "percent_branches_covered_display": "83"}, "missing_lines": [], "excluded_lines": [725], "start_line": 724, "executed_branches": [[727, 728], [727, 733], [728, 727], [728, 729], [731, 732]], "missing_branches": [[731, 727]]}, "ordered_list_handler": {"executed_lines": [738, 739, 740, 741, 742, 743, 744, 745], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 92.85714285714286, "percent_covered_display": "93", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 6, "num_partial_branches": 1, "covered_branches": 5, "missing_branches": 1, "percent_branches_covered": 83.33333333333333, "percent_branches_covered_display": "83"}, "missing_lines": [], "excluded_lines": [737], "start_line": 736, "executed_branches": [[739, 740], [739, 745], [740, 739], [740, 741], [743, 744]], "missing_branches": [[743, 739]]}, "list_item_handler": {"executed_lines": [750, 751, 752, 753], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [749], "start_line": 748, "executed_branches": [[751, 752], [751, 753]], "missing_branches": []}, "table_handler": {"executed_lines": [758, 759, 760, 763, 766, 767, 768, 769, 770, 771, 772, 773, 774, 775, 777, 778, 779, 780, 781, 783], "summary": {"covered_lines": 20, "num_statements": 21, "percent_covered": 86.48648648648648, "percent_covered_display": "86", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 95.23809523809524, "percent_statements_covered_display": "95", "num_branches": 16, "num_partial_branches": 4, "covered_branches": 12, "missing_branches": 4, "percent_branches_covered": 75.0, "percent_branches_covered_display": "75"}, "missing_lines": [761], "excluded_lines": [757], "start_line": 756, "executed_branches": [[760, 763], [766, 767], [766, 783], [767, 766], [767, 768], [768, 769], [768, 773], [771, 772], [773, 774], [777, 766], [777, 778], [780, 781]], "missing_branches": [[760, 761], [771, 766], [773, 766], [780, 777]]}, "table_row_handler": {"executed_lines": [788, 789, 790, 791, 792, 793, 794, 795], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 92.85714285714286, "percent_covered_display": "93", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 6, "num_partial_branches": 1, "covered_branches": 5, "missing_branches": 1, "percent_branches_covered": 83.33333333333333, "percent_branches_covered_display": "83"}, "missing_lines": [], "excluded_lines": [787], "start_line": 786, "executed_branches": [[789, 790], [789, 795], [790, 789], [790, 791], [793, 794]], "missing_branches": [[793, 789]]}, "table_cell_handler": {"executed_lines": [800, 801, 802, 804, 805, 807], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [799], "start_line": 798, "executed_branches": [[804, 805], [804, 807]], "missing_branches": []}, "table_header_cell_handler": {"executed_lines": [812, 813, 814, 816, 817, 819], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [811], "start_line": 810, "executed_branches": [[816, 817], [816, 819]], "missing_branches": []}, "horizontal_rule_handler": {"executed_lines": [824], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [823], "start_line": 822, "executed_branches": [], "missing_branches": []}, "line_break_handler": {"executed_lines": [830], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [828], "start_line": 827, "executed_branches": [], "missing_branches": []}, "image_handler": {"executed_lines": [835, 836, 838, 839, 842, 844, 846, 849, 850, 851, 852, 853, 854, 858], "summary": {"covered_lines": 14, "num_statements": 16, "percent_covered": 90.9090909090909, "percent_covered_display": "91", "missing_lines": 2, "excluded_lines": 1, "percent_statements_covered": 87.5, "percent_statements_covered_display": "88", "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [855, 856], "excluded_lines": [834], "start_line": 833, "executed_branches": [[842, 844], [842, 849], [851, 852], [851, 853], [853, 854], [853, 858]], "missing_branches": []}, "ignore_handler": {"executed_lines": [863], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [862], "start_line": 861, "executed_branches": [], "missing_branches": []}, "generic_handler": {"executed_lines": [868], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [867], "start_line": 866, "executed_branches": [], "missing_branches": []}, "parse_html_string": {"executed_lines": [950, 951, 953, 956, 958, 959, 960, 961, 962, 963, 964, 966, 968], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 8, "num_partial_branches": 0, "covered_branches": 8, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [938], "start_line": 935, "executed_branches": [[958, 959], [958, 968], [959, 958], [959, 960], [962, 958], [962, 963], [963, 964], [963, 966]], "missing_branches": []}, "": {"executed_lines": [9, 10, 11, 12, 13, 29, 32, 38, 39, 40, 41, 42, 43, 44, 45, 47, 51, 57, 61, 65, 69, 74, 108, 155, 173, 343, 365, 379, 496, 502, 519, 580, 603, 669, 674, 693, 701, 714, 724, 736, 748, 756, 786, 798, 810, 822, 827, 833, 861, 866, 872, 935], "summary": {"covered_lines": 52, "num_statements": 52, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 2, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1, 33], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"StyleContext": {"executed_lines": [49, 55, 59, 63, 67, 71], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 6, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [48, 54, 58, 62, 66, 70], "start_line": 32, "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [9, 10, 11, 12, 13, 29, 32, 38, 39, 40, 41, 42, 43, 44, 45, 47, 51, 57, 61, 65, 69, 74, 90, 91, 92, 94, 96, 108, 119, 120, 123, 126, 127, 128, 133, 134, 137, 138, 139, 140, 141, 144, 146, 149, 150, 152, 155, 165, 166, 167, 168, 169, 170, 173, 192, 209, 210, 211, 212, 213, 214, 215, 216, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 231, 233, 234, 235, 236, 239, 245, 246, 247, 248, 252, 253, 254, 255, 256, 257, 259, 260, 261, 262, 263, 265, 266, 268, 270, 271, 278, 279, 280, 281, 282, 283, 284, 285, 290, 293, 296, 297, 298, 304, 315, 316, 331, 343, 356, 357, 358, 362, 365, 376, 379, 394, 395, 397, 399, 402, 405, 409, 410, 411, 413, 415, 416, 417, 419, 420, 421, 422, 423, 424, 426, 429, 432, 433, 435, 436, 437, 445, 448, 449, 450, 453, 473, 474, 475, 479, 480, 481, 486, 490, 496, 502, 512, 513, 514, 515, 516, 519, 539, 540, 542, 544, 545, 546, 547, 548, 549, 550, 551, 552, 554, 556, 557, 558, 560, 561, 562, 564, 565, 567, 568, 569, 570, 571, 572, 574, 576, 577, 580, 593, 594, 595, 603, 613, 615, 617, 621, 623, 625, 626, 627, 628, 629, 632, 633, 634, 638, 639, 640, 642, 643, 644, 645, 652, 653, 654, 655, 656, 657, 659, 662, 663, 664, 665, 666, 669, 671, 674, 676, 685, 686, 687, 688, 689, 690, 693, 695, 696, 697, 698, 701, 703, 704, 707, 708, 709, 711, 714, 724, 726, 727, 728, 729, 730, 731, 732, 733, 736, 738, 739, 740, 741, 742, 743, 744, 745, 748, 750, 751, 752, 753, 756, 758, 759, 760, 763, 766, 767, 768, 769, 770, 771, 772, 773, 774, 775, 777, 778, 779, 780, 781, 783, 786, 788, 789, 790, 791, 792, 793, 794, 795, 798, 800, 801, 802, 804, 805, 807, 810, 812, 813, 814, 816, 817, 819, 822, 824, 827, 830, 833, 835, 836, 838, 839, 842, 844, 846, 849, 850, 851, 852, 853, 854, 858, 861, 863, 866, 868, 872, 935, 950, 951, 953, 956, 958, 959, 960, 961, 962, 963, 964, 966, 968], "summary": {"covered_lines": 366, "num_statements": 394, "percent_covered": 89.3687707641196, "percent_covered_display": "89", "missing_lines": 28, "excluded_lines": 32, "percent_statements_covered": 92.89340101522842, "percent_statements_covered_display": "93", "num_branches": 208, "num_partial_branches": 24, "covered_branches": 172, "missing_branches": 36, "percent_branches_covered": 82.6923076923077, "percent_branches_covered_display": "83"}, "missing_lines": [237, 238, 240, 241, 242, 243, 249, 250, 264, 286, 287, 301, 318, 359, 403, 482, 483, 484, 485, 487, 488, 649, 717, 718, 721, 761, 855, 856], "excluded_lines": [1, 33, 78, 109, 156, 178, 346, 366, 380, 503, 520, 543, 583, 604, 670, 675, 694, 702, 715, 725, 737, 749, 757, 787, 799, 811, 823, 828, 834, 862, 867, 938], "start_line": 1, "executed_branches": [[90, 91], [90, 96], [91, 92], [91, 94], [127, 128], [127, 134], [138, 139], [138, 141], [166, 167], [166, 170], [167, 166], [167, 168], [219, 220], [219, 231], [221, 222], [221, 223], [223, 224], [223, 225], [225, 226], [225, 227], [227, 228], [227, 231], [231, 233], [231, 245], [234, 235], [234, 239], [239, 245], [245, 246], [245, 252], [247, 248], [252, 253], [252, 259], [254, 255], [254, 256], [256, 257], [259, 260], [259, 268], [261, 262], [261, 263], [263, 265], [265, 266], [268, 270], [268, 290], [278, 279], [278, 280], [280, 281], [280, 290], [290, 293], [290, 316], [297, 298], [316, 331], [356, 357], [356, 362], [358, 362], [399, 402], [399, 490], [402, 405], [405, 409], [405, 413], [413, 415], [415, 416], [415, 453], [417, 419], [417, 448], [419, 420], [419, 421], [421, 422], [421, 423], [423, 424], [423, 426], [435, 399], [435, 436], [436, 437], [453, 473], [453, 479], [481, 486], [486, 399], [512, 513], [512, 514], [514, 515], [514, 516], [544, 545], [544, 546], [548, -542], [548, 549], [550, 551], [550, 552], [554, 556], [554, 576], [556, 557], [556, 560], [560, 561], [560, 564], [564, 565], [564, 567], [570, 554], [570, 571], [571, 572], [571, 574], [615, 617], [615, 662], [623, 625], [623, 638], [625, 626], [625, 632], [628, 629], [632, 633], [632, 634], [638, 639], [638, 652], [639, 638], [639, 640], [640, 642], [644, 645], [654, 655], [655, 656], [655, 657], [664, 665], [664, 666], [688, 689], [688, 690], [696, 697], [696, 698], [708, 709], [708, 711], [727, 728], [727, 733], [728, 727], [728, 729], [731, 732], [739, 740], [739, 745], [740, 739], [740, 741], [743, 744], [751, 752], [751, 753], [760, 763], [766, 767], [766, 783], [767, 766], [767, 768], [768, 769], [768, 773], [771, 772], [773, 774], [777, 766], [777, 778], [780, 781], [789, 790], [789, 795], [790, 789], [790, 791], [793, 794], [804, 805], [804, 807], [816, 817], [816, 819], [842, 844], [842, 849], [851, 852], [851, 853], [853, 854], [853, 858], [958, 959], [958, 968], [959, 958], [959, 960], [962, 958], [962, 963], [963, 964], [963, 966]], "missing_branches": [[239, 240], [247, 249], [249, 250], [249, 252], [256, 259], [263, 264], [265, 268], [297, 301], [316, 318], [358, 359], [402, 403], [413, 399], [436, 435], [481, 482], [482, 399], [482, 483], [483, 482], [483, 484], [484, 482], [484, 485], [486, 487], [487, 399], [487, 488], [628, 625], [640, 649], [644, 638], [654, 659], [717, 718], [717, 721], [731, 727], [743, 739], [760, 761], [771, 766], [773, 766], [780, 777], [793, 789]]}}}, "pyWebLayout/layout/__init__.py": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1], "executed_branches": [], "missing_branches": [], "functions": {"": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1], "start_line": 1, "executed_branches": [], "missing_branches": []}}}, "pyWebLayout/layout/document_layouter.py": {"executed_lines": [1, 3, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 17, 43, 44, 47, 48, 57, 58, 59, 60, 61, 63, 65, 68, 69, 70, 71, 75, 76, 78, 82, 83, 84, 85, 86, 87, 92, 95, 108, 109, 112, 139, 141, 142, 143, 146, 148, 152, 153, 158, 163, 164, 170, 180, 181, 182, 184, 189, 192, 199, 201, 203, 205, 206, 207, 209, 210, 213, 216, 219, 220, 222, 225, 226, 228, 230, 243, 245, 246, 247, 248, 250, 254, 256, 268, 274, 275, 277, 280, 299, 317, 318, 321, 324, 327, 328, 330, 333, 337, 338, 341, 342, 345, 347, 359, 361, 364, 383, 384, 385, 388, 389, 392, 393, 403, 404, 406, 407, 410, 413, 415, 418, 478, 498, 502, 505, 509, 510, 514, 515, 517, 520, 521, 524, 526, 529, 550, 554, 557, 559, 560, 563, 565, 569, 576, 579, 593, 600, 602, 603, 606, 607, 608, 609, 611, 628, 630, 643, 645, 656, 658, 682, 695, 697, 715, 716, 717, 718, 719, 720, 721, 722, 724, 725, 726, 727, 737], "summary": {"covered_lines": 178, "num_statements": 219, "percent_covered": 77.4294670846395, "percent_covered_display": "77", "missing_lines": 41, "excluded_lines": 16, "percent_statements_covered": 81.27853881278538, "percent_statements_covered_display": "81", "num_branches": 100, "num_partial_branches": 15, "covered_branches": 69, "missing_branches": 31, "percent_branches_covered": 69.0, "percent_branches_covered_display": "69"}, "missing_lines": [80, 101, 102, 114, 115, 126, 161, 259, 261, 262, 266, 296, 325, 448, 449, 452, 455, 458, 459, 460, 463, 464, 466, 469, 470, 473, 475, 499, 511, 551, 567, 680, 723, 728, 729, 730, 731, 732, 733, 734, 735], "excluded_lines": [24, 150, 281, 301, 368, 429, 480, 531, 580, 594, 617, 632, 646, 669, 684, 699], "executed_branches": [[43, 44], [43, 47], [47, 48], [47, 57], [58, 59], [58, 60], [60, 61], [60, 63], [63, 65], [63, 75], [76, 78], [95, 108], [108, 109], [108, 112], [112, 139], [141, 142], [141, 143], [152, 153], [152, 158], [158, 163], [181, 182], [181, 184], [192, 199], [192, 274], [201, 203], [201, 219], [203, 205], [203, 216], [207, 209], [207, 210], [220, 222], [220, 225], [226, 228], [226, 250], [228, 230], [243, 245], [256, 268], [274, 275], [317, 318], [317, 321], [324, 327], [327, 328], [327, 330], [337, 338], [337, 341], [406, 407], [406, 410], [498, 502], [510, 514], [550, 554], [557, 559], [557, 576], [559, 560], [559, 563], [565, 569], [602, 603], [602, 606], [715, 716], [715, 737], [716, 717], [716, 720], [718, 715], [718, 719], [720, 721], [720, 724], [722, 715], [724, 725], [726, 715], [726, 727]], "missing_branches": [[76, 80], [95, 101], [112, 114], [114, 115], [114, 126], [158, 161], [228, 250], [243, 250], [256, 259], [259, 261], [259, 266], [274, 277], [324, 325], [448, 449], [448, 452], [459, 460], [459, 463], [498, 499], [510, 511], [550, 551], [565, 567], [722, 723], [724, 728], [728, 729], [728, 732], [730, 715], [730, 731], [732, 715], [732, 733], [734, 715], [734, 735]], "functions": {"paragraph_layouter": {"executed_lines": [43, 44, 47, 48, 57, 58, 59, 60, 61, 63, 65, 68, 69, 70, 71, 75, 76, 78, 82, 83, 84, 85, 86, 87, 92, 95, 108, 109, 112, 139, 141, 142, 143, 146, 148, 180, 181, 182, 184, 189, 192, 199, 201, 203, 205, 206, 207, 209, 210, 213, 216, 219, 220, 222, 225, 226, 228, 230, 243, 245, 246, 247, 248, 250, 254, 256, 268, 274, 275, 277], "summary": {"covered_lines": 70, "num_statements": 80, "percent_covered": 83.33333333333333, "percent_covered_display": "83", "missing_lines": 10, "excluded_lines": 1, "percent_statements_covered": 87.5, "percent_statements_covered_display": "88", "num_branches": 46, "num_partial_branches": 7, "covered_branches": 35, "missing_branches": 11, "percent_branches_covered": 76.08695652173913, "percent_branches_covered_display": "76"}, "missing_lines": [80, 101, 102, 114, 115, 126, 259, 261, 262, 266], "excluded_lines": [24], "start_line": 17, "executed_branches": [[43, 44], [43, 47], [47, 48], [47, 57], [58, 59], [58, 60], [60, 61], [60, 63], [63, 65], [63, 75], [76, 78], [95, 108], [108, 109], [108, 112], [112, 139], [141, 142], [141, 143], [181, 182], [181, 184], [192, 199], [192, 274], [201, 203], [201, 219], [203, 205], [203, 216], [207, 209], [207, 210], [220, 222], [220, 225], [226, 228], [226, 250], [228, 230], [243, 245], [256, 268], [274, 275]], "missing_branches": [[76, 80], [95, 101], [112, 114], [114, 115], [114, 126], [228, 250], [243, 250], [256, 259], [259, 261], [259, 266], [274, 277]]}, "paragraph_layouter.create_new_line": {"executed_lines": [152, 153, 158, 163, 164, 170], "summary": {"covered_lines": 6, "num_statements": 7, "percent_covered": 81.81818181818181, "percent_covered_display": "82", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 85.71428571428571, "percent_statements_covered_display": "86", "num_branches": 4, "num_partial_branches": 1, "covered_branches": 3, "missing_branches": 1, "percent_branches_covered": 75.0, "percent_branches_covered_display": "75"}, "missing_lines": [161], "excluded_lines": [150], "start_line": 148, "executed_branches": [[152, 153], [152, 158], [158, 163]], "missing_branches": [[158, 161]]}, "pagebreak_layouter": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [296], "excluded_lines": [281], "start_line": 280, "executed_branches": [], "missing_branches": []}, "image_layouter": {"executed_lines": [317, 318, 321, 324, 327, 328, 330, 333, 337, 338, 341, 342, 345, 347, 359, 361], "summary": {"covered_lines": 16, "num_statements": 17, "percent_covered": 92.0, "percent_covered_display": "92", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 94.11764705882354, "percent_statements_covered_display": "94", "num_branches": 8, "num_partial_branches": 1, "covered_branches": 7, "missing_branches": 1, "percent_branches_covered": 87.5, "percent_branches_covered_display": "88"}, "missing_lines": [325], "excluded_lines": [301], "start_line": 299, "executed_branches": [[317, 318], [317, 321], [324, 327], [327, 328], [327, 330], [337, 338], [337, 341]], "missing_branches": [[324, 325]]}, "table_layouter": {"executed_lines": [383, 384, 385, 388, 389, 392, 393, 403, 404, 406, 407, 410, 413, 415], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [368], "start_line": 364, "executed_branches": [[406, 407], [406, 410]], "missing_branches": []}, "button_layouter": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 14, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 14, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 4, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 4, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [448, 449, 452, 455, 458, 459, 460, 463, 464, 466, 469, 470, 473, 475], "excluded_lines": [429], "start_line": 418, "executed_branches": [], "missing_branches": [[448, 449], [448, 452], [459, 460], [459, 463]]}, "form_field_layouter": {"executed_lines": [498, 502, 505, 509, 510, 514, 515, 517, 520, 521, 524, 526], "summary": {"covered_lines": 12, "num_statements": 14, "percent_covered": 77.77777777777777, "percent_covered_display": "78", "missing_lines": 2, "excluded_lines": 1, "percent_statements_covered": 85.71428571428571, "percent_statements_covered_display": "86", "num_branches": 4, "num_partial_branches": 2, "covered_branches": 2, "missing_branches": 2, "percent_branches_covered": 50.0, "percent_branches_covered_display": "50"}, "missing_lines": [499, 511], "excluded_lines": [480], "start_line": 478, "executed_branches": [[498, 502], [510, 514]], "missing_branches": [[498, 499], [510, 511]]}, "form_layouter": {"executed_lines": [550, 554, 557, 559, 560, 563, 565, 569, 576], "summary": {"covered_lines": 9, "num_statements": 11, "percent_covered": 78.94736842105263, "percent_covered_display": "79", "missing_lines": 2, "excluded_lines": 1, "percent_statements_covered": 81.81818181818181, "percent_statements_covered_display": "82", "num_branches": 8, "num_partial_branches": 2, "covered_branches": 6, "missing_branches": 2, "percent_branches_covered": 75.0, "percent_branches_covered_display": "75"}, "missing_lines": [551, 567], "excluded_lines": [531], "start_line": 529, "executed_branches": [[550, 554], [557, 559], [557, 576], [559, 560], [559, 563], [565, 569]], "missing_branches": [[550, 551], [565, 567]]}, "DocumentLayouter.__init__": {"executed_lines": [600, 602, 603, 606, 607, 608, 609], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [594], "start_line": 593, "executed_branches": [[602, 603], [602, 606]], "missing_branches": []}, "DocumentLayouter.layout_paragraph": {"executed_lines": [628], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [617], "start_line": 611, "executed_branches": [], "missing_branches": []}, "DocumentLayouter.layout_image": {"executed_lines": [643], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [632], "start_line": 630, "executed_branches": [], "missing_branches": []}, "DocumentLayouter.layout_table": {"executed_lines": [656], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [646], "start_line": 645, "executed_branches": [], "missing_branches": []}, "DocumentLayouter.layout_button": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [680], "excluded_lines": [669], "start_line": 658, "executed_branches": [], "missing_branches": []}, "DocumentLayouter.layout_form": {"executed_lines": [695], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [684], "start_line": 682, "executed_branches": [], "missing_branches": []}, "DocumentLayouter.layout_document": {"executed_lines": [715, 716, 717, 718, 719, 720, 721, 722, 724, 725, 726, 727, 737], "summary": {"covered_lines": 13, "num_statements": 22, "percent_covered": 56.81818181818182, "percent_covered_display": "57", "missing_lines": 9, "excluded_lines": 1, "percent_statements_covered": 59.09090909090909, "percent_statements_covered_display": "59", "num_branches": 22, "num_partial_branches": 2, "covered_branches": 12, "missing_branches": 10, "percent_branches_covered": 54.54545454545455, "percent_branches_covered_display": "55"}, "missing_lines": [723, 728, 729, 730, 731, 732, 733, 734, 735], "excluded_lines": [699], "start_line": 697, "executed_branches": [[715, 716], [715, 737], [716, 717], [716, 720], [718, 715], [718, 719], [720, 721], [720, 724], [722, 715], [724, 725], [726, 715], [726, 727]], "missing_branches": [[722, 723], [724, 728], [728, 729], [728, 732], [730, 715], [730, 731], [732, 715], [732, 733], [734, 715], [734, 735]]}, "": {"executed_lines": [1, 3, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 17, 280, 299, 364, 418, 478, 529, 579, 593, 611, 630, 645, 658, 682, 697], "summary": {"covered_lines": 27, "num_statements": 27, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [580], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"DocumentLayouter": {"executed_lines": [600, 602, 603, 606, 607, 608, 609, 628, 643, 656, 695, 715, 716, 717, 718, 719, 720, 721, 722, 724, 725, 726, 727, 737], "summary": {"covered_lines": 24, "num_statements": 34, "percent_covered": 65.51724137931035, "percent_covered_display": "66", "missing_lines": 10, "excluded_lines": 7, "percent_statements_covered": 70.58823529411765, "percent_statements_covered_display": "71", "num_branches": 24, "num_partial_branches": 2, "covered_branches": 14, "missing_branches": 10, "percent_branches_covered": 58.333333333333336, "percent_branches_covered_display": "58"}, "missing_lines": [680, 723, 728, 729, 730, 731, 732, 733, 734, 735], "excluded_lines": [594, 617, 632, 646, 669, 684, 699], "start_line": 579, "executed_branches": [[602, 603], [602, 606], [715, 716], [715, 737], [716, 717], [716, 720], [718, 715], [718, 719], [720, 721], [720, 724], [722, 715], [724, 725], [726, 715], [726, 727]], "missing_branches": [[722, 723], [724, 728], [728, 729], [728, 732], [730, 715], [730, 731], [732, 715], [732, 733], [734, 715], [734, 735]]}, "": {"executed_lines": [1, 3, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 17, 43, 44, 47, 48, 57, 58, 59, 60, 61, 63, 65, 68, 69, 70, 71, 75, 76, 78, 82, 83, 84, 85, 86, 87, 92, 95, 108, 109, 112, 139, 141, 142, 143, 146, 148, 152, 153, 158, 163, 164, 170, 180, 181, 182, 184, 189, 192, 199, 201, 203, 205, 206, 207, 209, 210, 213, 216, 219, 220, 222, 225, 226, 228, 230, 243, 245, 246, 247, 248, 250, 254, 256, 268, 274, 275, 277, 280, 299, 317, 318, 321, 324, 327, 328, 330, 333, 337, 338, 341, 342, 345, 347, 359, 361, 364, 383, 384, 385, 388, 389, 392, 393, 403, 404, 406, 407, 410, 413, 415, 418, 478, 498, 502, 505, 509, 510, 514, 515, 517, 520, 521, 524, 526, 529, 550, 554, 557, 559, 560, 563, 565, 569, 576, 579, 593, 611, 630, 645, 658, 682, 697], "summary": {"covered_lines": 154, "num_statements": 185, "percent_covered": 80.07662835249042, "percent_covered_display": "80", "missing_lines": 31, "excluded_lines": 9, "percent_statements_covered": 83.24324324324324, "percent_statements_covered_display": "83", "num_branches": 76, "num_partial_branches": 13, "covered_branches": 55, "missing_branches": 21, "percent_branches_covered": 72.36842105263158, "percent_branches_covered_display": "72"}, "missing_lines": [80, 101, 102, 114, 115, 126, 161, 259, 261, 262, 266, 296, 325, 448, 449, 452, 455, 458, 459, 460, 463, 464, 466, 469, 470, 473, 475, 499, 511, 551, 567], "excluded_lines": [24, 150, 281, 301, 368, 429, 480, 531, 580], "start_line": 1, "executed_branches": [[43, 44], [43, 47], [47, 48], [47, 57], [58, 59], [58, 60], [60, 61], [60, 63], [63, 65], [63, 75], [76, 78], [95, 108], [108, 109], [108, 112], [112, 139], [141, 142], [141, 143], [152, 153], [152, 158], [158, 163], [181, 182], [181, 184], [192, 199], [192, 274], [201, 203], [201, 219], [203, 205], [203, 216], [207, 209], [207, 210], [220, 222], [220, 225], [226, 228], [226, 250], [228, 230], [243, 245], [256, 268], [274, 275], [317, 318], [317, 321], [324, 327], [327, 328], [327, 330], [337, 338], [337, 341], [406, 407], [406, 410], [498, 502], [510, 514], [550, 554], [557, 559], [557, 576], [559, 560], [559, 563], [565, 569]], "missing_branches": [[76, 80], [95, 101], [112, 114], [114, 115], [114, 126], [158, 161], [228, 250], [243, 250], [256, 259], [259, 261], [259, 266], [274, 277], [324, 325], [448, 449], [448, 452], [459, 460], [459, 463], [498, 499], [510, 511], [550, 551], [565, 567]]}}}, "pyWebLayout/layout/ereader_layout.py": {"executed_lines": [14, 15, 16, 18, 21, 22, 23, 24, 25, 26, 27, 30, 31, 36, 37, 39, 40, 41, 42, 43, 44, 46, 55, 59, 61, 63, 64, 66, 68, 70, 72, 74, 75, 76, 78, 80, 83, 86, 92, 93, 94, 95, 98, 104, 105, 106, 107, 109, 111, 114, 115, 124, 131, 133, 134, 136, 146, 148, 155, 158, 159, 161, 163, 164, 165, 166, 167, 169, 172, 175, 177, 178, 179, 180, 182, 184, 185, 188, 190, 191, 193, 194, 195, 200, 206, 215, 254, 260, 261, 274, 275, 279, 280, 282, 284, 296, 297, 300, 301, 303, 304, 310, 316, 324, 325, 326, 327, 328, 329, 335, 342, 344, 356, 357, 360, 362, 365, 368, 371, 374, 381, 382, 383, 387, 388, 391, 393, 394, 396, 399, 400, 403, 407, 411, 413, 450, 453, 454, 455, 458, 459, 460, 461, 462, 465, 466, 467, 468, 469, 470, 471, 475, 478, 479, 481, 490, 493, 494, 495, 496, 497, 503, 504, 506, 519, 520, 522, 523, 526, 527, 529, 530, 532, 534, 536, 539, 540, 544, 545, 547, 549, 557, 558, 560, 561, 562, 563, 565, 566, 567, 569, 571, 572, 574, 575, 576, 578, 584, 585, 586, 587, 589, 590, 591, 592, 593, 595, 596, 599, 600, 603, 604, 605, 606, 608, 609, 614, 617, 618, 619, 620, 625, 626, 627, 628, 629, 635, 647, 648, 649, 651, 653, 655, 656, 659, 660, 661, 663, 683, 684, 686, 695, 704, 706, 708, 709, 710, 711, 714, 716, 719, 720, 722, 724, 730, 738, 740, 749, 750, 751, 752, 753, 755, 764, 765, 766, 767, 769, 790, 797, 799, 801, 802, 803, 807, 809, 812, 813, 814, 815, 816, 817, 818], "summary": {"covered_lines": 283, "num_statements": 304, "percent_covered": 90.31531531531532, "percent_covered_display": "90", "missing_lines": 21, "excluded_lines": 36, "percent_statements_covered": 93.09210526315789, "percent_statements_covered_display": "93", "num_branches": 140, "num_partial_branches": 16, "covered_branches": 118, "missing_branches": 22, "percent_branches_covered": 84.28571428571429, "percent_branches_covered_display": "84"}, "missing_lines": [197, 213, 225, 226, 230, 237, 238, 241, 276, 363, 472, 473, 476, 524, 537, 542, 633, 650, 652, 654, 728], "excluded_lines": [1, 32, 47, 60, 65, 69, 73, 79, 84, 99, 110, 162, 171, 176, 183, 201, 207, 216, 255, 262, 299, 311, 346, 417, 482, 510, 546, 550, 570, 641, 669, 736, 746, 761, 775, 811], "executed_branches": [[74, 75], [74, 76], [114, 115], [114, 133], [133, -109], [133, 134], [134, 133], [134, 136], [158, 133], [158, 159], [164, 165], [164, 167], [165, 166], [177, 178], [177, 180], [178, 177], [178, 179], [184, 185], [184, 188], [188, 190], [190, 191], [190, 193], [194, 188], [194, 195], [275, 279], [279, 280], [279, 282], [300, 301], [300, 303], [360, 362], [360, 399], [362, 365], [374, 381], [374, 387], [381, 382], [381, 383], [387, 388], [387, 391], [391, 393], [391, 396], [399, 400], [399, 403], [453, 454], [453, 458], [459, 460], [459, 465], [461, 462], [466, 467], [466, 475], [468, 469], [468, 470], [470, 471], [475, 478], [493, 494], [493, 503], [495, 496], [495, 497], [503, -481], [503, 504], [522, 523], [523, 526], [529, 530], [529, 532], [532, 534], [532, 536], [536, 539], [557, 558], [557, 560], [562, 563], [562, 565], [574, 575], [574, 589], [575, 576], [575, 578], [584, 585], [584, 587], [585, 586], [589, 590], [589, 595], [591, 592], [591, 593], [595, 596], [595, 608], [599, 600], [599, 606], [603, 604], [603, 605], [608, 609], [614, 617], [614, 629], [617, 614], [617, 618], [619, 620], [619, 628], [625, 626], [625, 627], [647, 648], [647, 649], [649, 651], [651, 653], [653, 655], [655, 656], [655, 659], [684, 686], [684, 695], [706, 708], [706, 714], [714, 716], [719, 720], [719, 722], [799, 801], [799, 807], [812, 813], [812, 814], [814, 815], [814, 816], [816, 817], [816, 818]], "missing_branches": [[165, 164], [188, 197], [225, 226], [225, 230], [237, 238], [237, 241], [275, 276], [362, 363], [461, 465], [470, 472], [472, 466], [472, 473], [475, 476], [522, 542], [523, 524], [536, 537], [585, 584], [608, 633], [649, 650], [651, 652], [653, 654], [714, 728]], "functions": {"RenderingPosition._key": {"executed_lines": [55], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [47], "start_line": 46, "executed_branches": [], "missing_branches": []}, "RenderingPosition.to_dict": {"executed_lines": [61], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [60], "start_line": 59, "executed_branches": [], "missing_branches": []}, "RenderingPosition.from_dict": {"executed_lines": [66], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [65], "start_line": 64, "executed_branches": [], "missing_branches": []}, "RenderingPosition.copy": {"executed_lines": [70], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [69], "start_line": 68, "executed_branches": [], "missing_branches": []}, "RenderingPosition.__eq__": {"executed_lines": [74, 75, 76], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [73], "start_line": 72, "executed_branches": [[74, 75], [74, 76]], "missing_branches": []}, "RenderingPosition.__hash__": {"executed_lines": [80], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [79], "start_line": 78, "executed_branches": [], "missing_branches": []}, "ChapterInfo.__init__": {"executed_lines": [92, 93, 94, 95], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 86, "executed_branches": [], "missing_branches": []}, "ChapterNavigator.__init__": {"executed_lines": [105, 106, 107], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 104, "executed_branches": [], "missing_branches": []}, "ChapterNavigator._build_chapter_map": {"executed_lines": [111, 114, 115, 124, 131, 133, 134, 136, 146, 148, 155, 158, 159], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 8, "num_partial_branches": 0, "covered_branches": 8, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [110], "start_line": 109, "executed_branches": [[114, 115], [114, 133], [133, -109], [133, 134], [134, 133], [134, 136], [158, 133], [158, 159]], "missing_branches": []}, "ChapterNavigator._extract_heading_text": {"executed_lines": [163, 164, 165, 166, 167], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 88.88888888888889, "percent_covered_display": "89", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 4, "num_partial_branches": 1, "covered_branches": 3, "missing_branches": 1, "percent_branches_covered": 75.0, "percent_branches_covered_display": "75"}, "missing_lines": [], "excluded_lines": [162], "start_line": 161, "executed_branches": [[164, 165], [164, 167], [165, 166]], "missing_branches": [[165, 164]]}, "ChapterNavigator.get_table_of_contents": {"executed_lines": [172], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [171], "start_line": 169, "executed_branches": [], "missing_branches": []}, "ChapterNavigator.get_chapter_position": {"executed_lines": [177, 178, 179, 180], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [176], "start_line": 175, "executed_branches": [[177, 178], [177, 180], [178, 177], [178, 179]], "missing_branches": []}, "ChapterNavigator.get_current_chapter": {"executed_lines": [184, 185, 188, 190, 191, 193, 194, 195], "summary": {"covered_lines": 8, "num_statements": 9, "percent_covered": 88.23529411764706, "percent_covered_display": "88", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 88.88888888888889, "percent_statements_covered_display": "89", "num_branches": 8, "num_partial_branches": 1, "covered_branches": 7, "missing_branches": 1, "percent_branches_covered": 87.5, "percent_branches_covered_display": "88"}, "missing_lines": [197], "excluded_lines": [183], "start_line": 182, "executed_branches": [[184, 185], [184, 188], [188, 190], [190, 191], [190, 193], [194, 188], [194, 195]], "missing_branches": [[188, 197]]}, "FontFamilyOverride.__init__": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [213], "excluded_lines": [207], "start_line": 206, "executed_branches": [], "missing_branches": []}, "FontFamilyOverride.override_font": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 6, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 6, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 4, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 4, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [225, 226, 230, 237, 238, 241], "excluded_lines": [216], "start_line": 215, "executed_branches": [], "missing_branches": [[225, 226], [225, 230], [237, 238], [237, 241]]}, "FontScaler.scale_font": {"executed_lines": [274, 275, 279, 280, 282, 284], "summary": {"covered_lines": 6, "num_statements": 7, "percent_covered": 81.81818181818181, "percent_covered_display": "82", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 85.71428571428571, "percent_statements_covered_display": "86", "num_branches": 4, "num_partial_branches": 1, "covered_branches": 3, "missing_branches": 1, "percent_branches_covered": 75.0, "percent_branches_covered_display": "75"}, "missing_lines": [276], "excluded_lines": [262], "start_line": 261, "executed_branches": [[275, 279], [279, 280], [279, 282]], "missing_branches": [[275, 276]]}, "FontScaler.scale_word_spacing": {"executed_lines": [300, 301, 303, 304], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [299], "start_line": 297, "executed_branches": [[300, 301], [300, 303]], "missing_branches": []}, "BidirectionalLayouter.__init__": {"executed_lines": [324, 325, 326, 327, 328, 329, 335, 342], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 316, "executed_branches": [], "missing_branches": []}, "BidirectionalLayouter.render_page_forward": {"executed_lines": [356, 357, 360, 362, 365, 368, 371, 374, 381, 382, 383, 387, 388, 391, 393, 394, 396, 399, 400, 403], "summary": {"covered_lines": 20, "num_statements": 21, "percent_covered": 94.28571428571429, "percent_covered_display": "94", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 95.23809523809524, "percent_statements_covered_display": "95", "num_branches": 14, "num_partial_branches": 1, "covered_branches": 13, "missing_branches": 1, "percent_branches_covered": 92.85714285714286, "percent_branches_covered_display": "93"}, "missing_lines": [363], "excluded_lines": [346], "start_line": 344, "executed_branches": [[360, 362], [360, 399], [362, 365], [374, 381], [374, 387], [381, 382], [381, 383], [387, 388], [387, 391], [391, 393], [391, 396], [399, 400], [399, 403]], "missing_branches": [[362, 363]]}, "BidirectionalLayouter.render_page_backward": {"executed_lines": [450, 453, 454, 455, 458, 459, 460, 461, 462, 465, 466, 467, 468, 469, 470, 471, 475, 478, 479], "summary": {"covered_lines": 19, "num_statements": 22, "percent_covered": 78.94736842105263, "percent_covered_display": "79", "missing_lines": 3, "excluded_lines": 1, "percent_statements_covered": 86.36363636363636, "percent_statements_covered_display": "86", "num_branches": 16, "num_partial_branches": 3, "covered_branches": 11, "missing_branches": 5, "percent_branches_covered": 68.75, "percent_branches_covered_display": "69"}, "missing_lines": [472, 473, 476], "excluded_lines": [417], "start_line": 413, "executed_branches": [[453, 454], [453, 458], [459, 460], [459, 465], [461, 462], [466, 467], [466, 475], [468, 469], [468, 470], [470, 471], [475, 478]], "missing_branches": [[461, 465], [470, 472], [472, 466], [472, 473], [475, 476]]}, "BidirectionalLayouter._backward_anchors": {"executed_lines": [490, 493, 494, 495, 496, 497, 503, 504], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [482], "start_line": 481, "executed_branches": [[493, 494], [493, 503], [495, 496], [495, 497], [503, -481], [503, 504]], "missing_branches": []}, "BidirectionalLayouter._replay_to": {"executed_lines": [519, 520, 522, 523, 526, 527, 529, 530, 532, 534, 536, 539, 540], "summary": {"covered_lines": 13, "num_statements": 16, "percent_covered": 76.92307692307692, "percent_covered_display": "77", "missing_lines": 3, "excluded_lines": 1, "percent_statements_covered": 81.25, "percent_statements_covered_display": "81", "num_branches": 10, "num_partial_branches": 3, "covered_branches": 7, "missing_branches": 3, "percent_branches_covered": 70.0, "percent_branches_covered_display": "70"}, "missing_lines": [524, 537, 542], "excluded_lines": [510], "start_line": 506, "executed_branches": [[522, 523], [523, 526], [529, 530], [529, 532], [532, 534], [532, 536], [536, 539]], "missing_branches": [[522, 542], [523, 524], [536, 537]]}, "BidirectionalLayouter._position_key": {"executed_lines": [547], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [546], "start_line": 545, "executed_branches": [], "missing_branches": []}, "BidirectionalLayouter._scale_block_fonts": {"executed_lines": [557, 558, 560, 561, 562, 563, 565, 566, 567], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [550], "start_line": 549, "executed_branches": [[557, 558], [557, 560], [562, 563], [562, 565]], "missing_branches": []}, "BidirectionalLayouter._build_scaled_block": {"executed_lines": [571, 574, 575, 576, 578, 584, 585, 586, 587, 589, 590, 591, 592, 593, 595, 596, 599, 600, 603, 604, 605, 606, 608, 609, 614, 617, 618, 619, 620, 625, 626, 627, 628, 629], "summary": {"covered_lines": 34, "num_statements": 35, "percent_covered": 95.23809523809524, "percent_covered_display": "95", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 97.14285714285714, "percent_statements_covered_display": "97", "num_branches": 28, "num_partial_branches": 2, "covered_branches": 26, "missing_branches": 2, "percent_branches_covered": 92.85714285714286, "percent_branches_covered_display": "93"}, "missing_lines": [633], "excluded_lines": [570], "start_line": 569, "executed_branches": [[574, 575], [574, 589], [575, 576], [575, 578], [584, 585], [584, 587], [585, 586], [589, 590], [589, 595], [591, 592], [591, 593], [595, 596], [595, 608], [599, 600], [599, 606], [603, 604], [603, 605], [608, 609], [614, 617], [614, 629], [617, 614], [617, 618], [619, 620], [619, 628], [625, 626], [625, 627]], "missing_branches": [[585, 584], [608, 633]]}, "BidirectionalLayouter._build_scaled_block.scale": {"executed_lines": [572], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 571, "executed_branches": [], "missing_branches": []}, "BidirectionalLayouter._layout_block_on_page": {"executed_lines": [647, 648, 649, 651, 653, 655, 656, 659, 660, 661], "summary": {"covered_lines": 10, "num_statements": 13, "percent_covered": 73.91304347826087, "percent_covered_display": "74", "missing_lines": 3, "excluded_lines": 1, "percent_statements_covered": 76.92307692307692, "percent_statements_covered_display": "77", "num_branches": 10, "num_partial_branches": 3, "covered_branches": 7, "missing_branches": 3, "percent_branches_covered": 70.0, "percent_branches_covered_display": "70"}, "missing_lines": [650, 652, 654], "excluded_lines": [641], "start_line": 635, "executed_branches": [[647, 648], [647, 649], [649, 651], [651, 653], [653, 655], [655, 656], [655, 659]], "missing_branches": [[649, 650], [651, 652], [653, 654]]}, "BidirectionalLayouter._layout_paragraph_on_page": {"executed_lines": [683, 684, 686, 695, 704, 706, 708, 709, 710, 711, 714, 716, 719, 720, 722, 724], "summary": {"covered_lines": 16, "num_statements": 17, "percent_covered": 92.0, "percent_covered_display": "92", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 94.11764705882354, "percent_statements_covered_display": "94", "num_branches": 8, "num_partial_branches": 1, "covered_branches": 7, "missing_branches": 1, "percent_branches_covered": 87.5, "percent_branches_covered_display": "88"}, "missing_lines": [728], "excluded_lines": [669], "start_line": 663, "executed_branches": [[684, 686], [684, 695], [706, 708], [706, 714], [714, 716], [719, 720], [719, 722]], "missing_branches": [[714, 728]]}, "BidirectionalLayouter._layout_heading_on_page": {"executed_lines": [738], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [736], "start_line": 730, "executed_branches": [], "missing_branches": []}, "BidirectionalLayouter._layout_table_on_page": {"executed_lines": [749, 750, 751, 752, 753], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [746], "start_line": 740, "executed_branches": [], "missing_branches": []}, "BidirectionalLayouter._layout_list_on_page": {"executed_lines": [764, 765, 766, 767], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [761], "start_line": 755, "executed_branches": [], "missing_branches": []}, "BidirectionalLayouter._layout_image_on_page": {"executed_lines": [790, 797, 799, 801, 802, 803, 807], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [775], "start_line": 769, "executed_branches": [[799, 801], [799, 807]], "missing_branches": []}, "BidirectionalLayouter._position_compare": {"executed_lines": [812, 813, 814, 815, 816, 817, 818], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [811], "start_line": 809, "executed_branches": [[812, 813], [812, 814], [814, 815], [814, 816], [816, 817], [816, 818]], "missing_branches": []}, "": {"executed_lines": [14, 15, 16, 18, 21, 22, 23, 24, 25, 26, 27, 30, 31, 36, 37, 39, 40, 41, 42, 43, 44, 46, 59, 63, 64, 68, 72, 78, 83, 86, 98, 104, 109, 161, 169, 175, 182, 200, 206, 215, 254, 260, 261, 296, 297, 310, 316, 344, 407, 411, 413, 481, 506, 544, 545, 549, 569, 635, 663, 730, 740, 755, 769, 809], "summary": {"covered_lines": 64, "num_statements": 64, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 7, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1, 32, 84, 99, 201, 255, 311], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"RenderingPosition": {"executed_lines": [55, 61, 66, 70, 74, 75, 76, 80], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 6, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [47, 60, 65, 69, 73, 79], "start_line": 31, "executed_branches": [[74, 75], [74, 76]], "missing_branches": []}, "ChapterInfo": {"executed_lines": [92, 93, 94, 95], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 83, "executed_branches": [], "missing_branches": []}, "ChapterNavigator": {"executed_lines": [105, 106, 107, 111, 114, 115, 124, 131, 133, 134, 136, 146, 148, 155, 158, 159, 163, 164, 165, 166, 167, 172, 177, 178, 179, 180, 184, 185, 188, 190, 191, 193, 194, 195], "summary": {"covered_lines": 34, "num_statements": 35, "percent_covered": 94.91525423728814, "percent_covered_display": "95", "missing_lines": 1, "excluded_lines": 5, "percent_statements_covered": 97.14285714285714, "percent_statements_covered_display": "97", "num_branches": 24, "num_partial_branches": 2, "covered_branches": 22, "missing_branches": 2, "percent_branches_covered": 91.66666666666667, "percent_branches_covered_display": "92"}, "missing_lines": [197], "excluded_lines": [110, 162, 171, 176, 183], "start_line": 98, "executed_branches": [[114, 115], [114, 133], [133, -109], [133, 134], [134, 133], [134, 136], [158, 133], [158, 159], [164, 165], [164, 167], [165, 166], [177, 178], [177, 180], [178, 177], [178, 179], [184, 185], [184, 188], [188, 190], [190, 191], [190, 193], [194, 188], [194, 195]], "missing_branches": [[165, 164], [188, 197]]}, "FontFamilyOverride": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 7, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 7, "excluded_lines": 2, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 4, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 4, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [213, 225, 226, 230, 237, 238, 241], "excluded_lines": [207, 216], "start_line": 200, "executed_branches": [], "missing_branches": [[225, 226], [225, 230], [237, 238], [237, 241]]}, "FontScaler": {"executed_lines": [274, 275, 279, 280, 282, 284, 300, 301, 303, 304], "summary": {"covered_lines": 10, "num_statements": 11, "percent_covered": 88.23529411764706, "percent_covered_display": "88", "missing_lines": 1, "excluded_lines": 2, "percent_statements_covered": 90.9090909090909, "percent_statements_covered_display": "91", "num_branches": 6, "num_partial_branches": 1, "covered_branches": 5, "missing_branches": 1, "percent_branches_covered": 83.33333333333333, "percent_branches_covered_display": "83"}, "missing_lines": [276], "excluded_lines": [262, 299], "start_line": 254, "executed_branches": [[275, 279], [279, 280], [279, 282], [300, 301], [300, 303]], "missing_branches": [[275, 276]]}, "BidirectionalLayouter": {"executed_lines": [324, 325, 326, 327, 328, 329, 335, 342, 356, 357, 360, 362, 365, 368, 371, 374, 381, 382, 383, 387, 388, 391, 393, 394, 396, 399, 400, 403, 450, 453, 454, 455, 458, 459, 460, 461, 462, 465, 466, 467, 468, 469, 470, 471, 475, 478, 479, 490, 493, 494, 495, 496, 497, 503, 504, 519, 520, 522, 523, 526, 527, 529, 530, 532, 534, 536, 539, 540, 547, 557, 558, 560, 561, 562, 563, 565, 566, 567, 571, 572, 574, 575, 576, 578, 584, 585, 586, 587, 589, 590, 591, 592, 593, 595, 596, 599, 600, 603, 604, 605, 606, 608, 609, 614, 617, 618, 619, 620, 625, 626, 627, 628, 629, 647, 648, 649, 651, 653, 655, 656, 659, 660, 661, 683, 684, 686, 695, 704, 706, 708, 709, 710, 711, 714, 716, 719, 720, 722, 724, 738, 749, 750, 751, 752, 753, 764, 765, 766, 767, 790, 797, 799, 801, 802, 803, 807, 812, 813, 814, 815, 816, 817, 818], "summary": {"covered_lines": 163, "num_statements": 175, "percent_covered": 90.3225806451613, "percent_covered_display": "90", "missing_lines": 12, "excluded_lines": 14, "percent_statements_covered": 93.14285714285714, "percent_statements_covered_display": "93", "num_branches": 104, "num_partial_branches": 13, "covered_branches": 89, "missing_branches": 15, "percent_branches_covered": 85.57692307692308, "percent_branches_covered_display": "86"}, "missing_lines": [363, 472, 473, 476, 524, 537, 542, 633, 650, 652, 654, 728], "excluded_lines": [346, 417, 482, 510, 546, 550, 570, 641, 669, 736, 746, 761, 775, 811], "start_line": 310, "executed_branches": [[360, 362], [360, 399], [362, 365], [374, 381], [374, 387], [381, 382], [381, 383], [387, 388], [387, 391], [391, 393], [391, 396], [399, 400], [399, 403], [453, 454], [453, 458], [459, 460], [459, 465], [461, 462], [466, 467], [466, 475], [468, 469], [468, 470], [470, 471], [475, 478], [493, 494], [493, 503], [495, 496], [495, 497], [503, -481], [503, 504], [522, 523], [523, 526], [529, 530], [529, 532], [532, 534], [532, 536], [536, 539], [557, 558], [557, 560], [562, 563], [562, 565], [574, 575], [574, 589], [575, 576], [575, 578], [584, 585], [584, 587], [585, 586], [589, 590], [589, 595], [591, 592], [591, 593], [595, 596], [595, 608], [599, 600], [599, 606], [603, 604], [603, 605], [608, 609], [614, 617], [614, 629], [617, 614], [617, 618], [619, 620], [619, 628], [625, 626], [625, 627], [647, 648], [647, 649], [649, 651], [651, 653], [653, 655], [655, 656], [655, 659], [684, 686], [684, 695], [706, 708], [706, 714], [714, 716], [719, 720], [719, 722], [799, 801], [799, 807], [812, 813], [812, 814], [814, 815], [814, 816], [816, 817], [816, 818]], "missing_branches": [[362, 363], [461, 465], [470, 472], [472, 466], [472, 473], [475, 476], [522, 542], [523, 524], [536, 537], [585, 584], [608, 633], [649, 650], [651, 652], [653, 654], [714, 728]]}, "": {"executed_lines": [14, 15, 16, 18, 21, 22, 23, 24, 25, 26, 27, 30, 31, 36, 37, 39, 40, 41, 42, 43, 44, 46, 59, 63, 64, 68, 72, 78, 83, 86, 98, 104, 109, 161, 169, 175, 182, 200, 206, 215, 254, 260, 261, 296, 297, 310, 316, 344, 407, 411, 413, 481, 506, 544, 545, 549, 569, 635, 663, 730, 740, 755, 769, 809], "summary": {"covered_lines": 64, "num_statements": 64, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 7, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1, 32, 84, 99, 201, 255, 311], "start_line": 1, "executed_branches": [], "missing_branches": []}}}, "pyWebLayout/layout/ereader_manager.py": {"executed_lines": [9, 10, 11, 13, 14, 15, 16, 17, 18, 19, 20, 21, 23, 24, 25, 27, 30, 35, 43, 44, 46, 47, 49, 50, 52, 54, 55, 56, 65, 67, 72, 80, 81, 83, 93, 94, 95, 96, 97, 99, 109, 111, 118, 120, 127, 129, 136, 137, 138, 139, 140, 147, 161, 182, 183, 184, 187, 188, 189, 192, 193, 194, 195, 199, 200, 203, 204, 208, 209, 212, 213, 214, 215, 218, 219, 222, 224, 227, 288, 291, 293, 296, 298, 309, 310, 313, 314, 315, 317, 319, 330, 332, 336, 340, 341, 342, 345, 352, 353, 359, 361, 363, 364, 367, 369, 370, 373, 375, 386, 387, 389, 390, 392, 403, 404, 406, 407, 410, 411, 414, 416, 420, 421, 422, 423, 429, 430, 434, 437, 438, 440, 442, 454, 455, 461, 462, 463, 466, 469, 470, 473, 475, 477, 478, 479, 483, 486, 488, 490, 491, 492, 496, 504, 506, 510, 520, 521, 522, 523, 525, 535, 536, 537, 538, 540, 550, 551, 552, 553, 555, 564, 568, 571, 574, 592, 593, 596, 600, 609, 610, 612, 614, 616, 618, 630, 631, 633, 636, 638, 640, 642, 670, 677, 679, 697, 715, 733, 752, 770, 788, 796, 798, 805, 807, 817, 818, 819, 823, 833, 835, 845, 846, 847, 848, 850, 857, 869, 886, 887, 888, 890, 892, 911, 912, 913, 915, 917, 919, 922, 923, 925, 935, 937, 939, 941, 949, 950, 953, 955, 966, 968, 969, 970, 971, 972, 973, 974, 976, 986, 988, 999, 1001, 1011, 1013, 1015, 1016, 1018, 1025, 1026, 1030, 1031, 1033, 1035, 1042, 1044, 1051, 1053, 1060, 1063, 1064, 1065, 1067, 1074, 1075, 1077, 1092, 1099, 1101, 1108, 1109, 1110, 1113, 1116, 1118, 1126, 1127, 1133, 1149], "summary": {"covered_lines": 288, "num_statements": 370, "percent_covered": 77.29257641921397, "percent_covered_display": "77", "missing_lines": 82, "excluded_lines": 66, "percent_statements_covered": 77.83783783783784, "percent_statements_covered_display": "78", "num_branches": 88, "num_partial_branches": 8, "covered_branches": 66, "missing_branches": 22, "percent_branches_covered": 75.0, "percent_branches_covered_display": "75"}, "missing_lines": [60, 61, 63, 141, 142, 144, 247, 248, 249, 251, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 271, 272, 273, 274, 275, 276, 278, 279, 280, 281, 282, 283, 285, 334, 355, 357, 409, 467, 494, 572, 597, 663, 666, 668, 691, 692, 693, 694, 695, 709, 710, 711, 712, 713, 727, 728, 729, 730, 731, 745, 747, 748, 749, 750, 764, 765, 766, 767, 768, 782, 783, 784, 785, 786, 820, 821, 1061, 1128, 1129], "excluded_lines": [1, 31, 36, 53, 66, 73, 84, 100, 112, 121, 130, 148, 169, 229, 290, 295, 299, 320, 362, 376, 393, 443, 497, 511, 526, 541, 556, 578, 615, 619, 639, 643, 671, 680, 698, 716, 734, 753, 771, 790, 799, 808, 824, 836, 851, 874, 898, 918, 926, 938, 942, 954, 967, 977, 989, 1002, 1014, 1019, 1036, 1045, 1054, 1068, 1093, 1102, 1119, 1137], "executed_branches": [[93, 94], [93, 97], [137, 138], [137, 139], [187, 188], [187, 189], [213, 214], [213, 218], [309, 310], [309, 313], [314, 315], [314, 317], [332, 336], [352, 353], [363, 364], [363, 367], [369, 370], [369, 373], [386, 387], [386, 389], [403, 404], [403, 414], [406, 407], [420, 421], [420, 429], [429, 430], [429, 440], [454, 455], [454, 466], [466, 469], [469, 470], [469, 473], [475, 477], [475, 483], [486, 488], [536, 537], [536, 538], [551, 552], [551, 553], [564, -555], [564, 568], [571, -555], [592, 593], [592, 612], [596, 600], [600, 592], [600, 609], [630, 631], [630, 636], [846, 847], [846, 848], [887, 888], [887, 890], [912, 913], [912, 915], [969, 970], [969, 974], [970, 971], [970, 972], [1015, -1013], [1015, 1016], [1025, 1026], [1025, 1030], [1060, 1063], [1108, 1109], [1108, 1110]], "missing_branches": [[257, 258], [257, 271], [259, 260], [259, 261], [261, 257], [261, 262], [263, 264], [263, 265], [272, 273], [272, 278], [279, 280], [279, 285], [281, 282], [281, 283], [332, 334], [352, 359], [406, 409], [466, 467], [486, 494], [571, 572], [596, 597], [1060, 1061]], "functions": {"BookmarkManager.__init__": {"executed_lines": [43, 44, 46, 47, 49, 50], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [36], "start_line": 35, "executed_branches": [], "missing_branches": []}, "BookmarkManager._load_bookmarks": {"executed_lines": [54, 55, 56], "summary": {"covered_lines": 3, "num_statements": 6, "percent_covered": 50.0, "percent_covered_display": "50", "missing_lines": 3, "excluded_lines": 1, "percent_statements_covered": 50.0, "percent_statements_covered_display": "50", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [60, 61, 63], "excluded_lines": [53], "start_line": 52, "executed_branches": [], "missing_branches": []}, "BookmarkManager._save_bookmarks": {"executed_lines": [67], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [66], "start_line": 65, "executed_branches": [], "missing_branches": []}, "BookmarkManager.add_bookmark": {"executed_lines": [80, 81], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [73], "start_line": 72, "executed_branches": [], "missing_branches": []}, "BookmarkManager.remove_bookmark": {"executed_lines": [93, 94, 95, 96, 97], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [84], "start_line": 83, "executed_branches": [[93, 94], [93, 97]], "missing_branches": []}, "BookmarkManager.get_bookmark": {"executed_lines": [109], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [100], "start_line": 99, "executed_branches": [], "missing_branches": []}, "BookmarkManager.list_bookmarks": {"executed_lines": [118], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [112], "start_line": 111, "executed_branches": [], "missing_branches": []}, "BookmarkManager.save_reading_position": {"executed_lines": [127], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [121], "start_line": 120, "executed_branches": [], "missing_branches": []}, "BookmarkManager.load_reading_position": {"executed_lines": [136, 137, 138, 139, 140], "summary": {"covered_lines": 5, "num_statements": 8, "percent_covered": 70.0, "percent_covered_display": "70", "missing_lines": 3, "excluded_lines": 1, "percent_statements_covered": 62.5, "percent_statements_covered_display": "62", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [141, 142, 144], "excluded_lines": [130], "start_line": 129, "executed_branches": [[137, 138], [137, 139]], "missing_branches": []}, "EreaderLayoutManager.__init__": {"executed_lines": [182, 183, 184, 187, 188, 189, 192, 193, 194, 195, 199, 200, 203, 204, 208, 209, 212, 213, 214, 215, 218, 219, 222, 224], "summary": {"covered_lines": 24, "num_statements": 24, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [169], "start_line": 161, "executed_branches": [[187, 188], [187, 189], [213, 214], [213, 218]], "missing_branches": []}, "EreaderLayoutManager.prewarm_caches": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 30, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 30, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 14, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 14, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [247, 248, 249, 251, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 271, 272, 273, 274, 275, 276, 278, 279, 280, 281, 282, 283, 285], "excluded_lines": [229], "start_line": 227, "executed_branches": [], "missing_branches": [[257, 258], [257, 271], [259, 260], [259, 261], [261, 257], [261, 262], [263, 264], [263, 265], [272, 273], [272, 278], [279, 280], [279, 285], [281, 282], [281, 283]]}, "EreaderLayoutManager.set_position_changed_callback": {"executed_lines": [291], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [290], "start_line": 288, "executed_branches": [], "missing_branches": []}, "EreaderLayoutManager.set_chapter_changed_callback": {"executed_lines": [296], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [295], "start_line": 293, "executed_branches": [], "missing_branches": []}, "EreaderLayoutManager._detect_cover": {"executed_lines": [309, 310, 313, 314, 315, 317], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [299], "start_line": 298, "executed_branches": [[309, 310], [309, 313], [314, 315], [314, 317]], "missing_branches": []}, "EreaderLayoutManager._render_cover_page": {"executed_lines": [330, 332, 336, 340, 341, 342, 345, 352, 353, 359], "summary": {"covered_lines": 10, "num_statements": 13, "percent_covered": 70.58823529411765, "percent_covered_display": "71", "missing_lines": 3, "excluded_lines": 1, "percent_statements_covered": 76.92307692307692, "percent_statements_covered_display": "77", "num_branches": 4, "num_partial_branches": 2, "covered_branches": 2, "missing_branches": 2, "percent_branches_covered": 50.0, "percent_branches_covered_display": "50"}, "missing_lines": [334, 355, 357], "excluded_lines": [320], "start_line": 319, "executed_branches": [[332, 336], [352, 353]], "missing_branches": [[332, 334], [352, 359]]}, "EreaderLayoutManager._notify_position_changed": {"executed_lines": [363, 364, 367, 369, 370, 373], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [362], "start_line": 361, "executed_branches": [[363, 364], [363, 367], [369, 370], [369, 373]], "missing_branches": []}, "EreaderLayoutManager.get_current_page": {"executed_lines": [386, 387, 389, 390], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [376], "start_line": 375, "executed_branches": [[386, 387], [386, 389]], "missing_branches": []}, "EreaderLayoutManager.next_page": {"executed_lines": [403, 404, 406, 407, 410, 411, 414, 416, 420, 421, 422, 423, 429, 430, 434, 437, 438, 440], "summary": {"covered_lines": 18, "num_statements": 19, "percent_covered": 92.5925925925926, "percent_covered_display": "93", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 94.73684210526316, "percent_statements_covered_display": "95", "num_branches": 8, "num_partial_branches": 1, "covered_branches": 7, "missing_branches": 1, "percent_branches_covered": 87.5, "percent_branches_covered_display": "88"}, "missing_lines": [409], "excluded_lines": [393], "start_line": 392, "executed_branches": [[403, 404], [403, 414], [406, 407], [420, 421], [420, 429], [429, 430], [429, 440]], "missing_branches": [[406, 409]]}, "EreaderLayoutManager.previous_page": {"executed_lines": [454, 455, 461, 462, 463, 466, 469, 470, 473, 475, 477, 478, 479, 483, 486, 488, 490, 491, 492], "summary": {"covered_lines": 19, "num_statements": 21, "percent_covered": 87.09677419354838, "percent_covered_display": "87", "missing_lines": 2, "excluded_lines": 1, "percent_statements_covered": 90.47619047619048, "percent_statements_covered_display": "90", "num_branches": 10, "num_partial_branches": 2, "covered_branches": 8, "missing_branches": 2, "percent_branches_covered": 80.0, "percent_branches_covered_display": "80"}, "missing_lines": [467, 494], "excluded_lines": [443], "start_line": 442, "executed_branches": [[454, 455], [454, 466], [466, 469], [469, 470], [469, 473], [475, 477], [475, 483], [486, 488]], "missing_branches": [[466, 467], [486, 494]]}, "EreaderLayoutManager._is_at_beginning": {"executed_lines": [504, 506], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [497], "start_line": 496, "executed_branches": [], "missing_branches": []}, "EreaderLayoutManager.jump_to_position": {"executed_lines": [520, 521, 522, 523], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [511], "start_line": 510, "executed_branches": [], "missing_branches": []}, "EreaderLayoutManager.jump_to_chapter": {"executed_lines": [535, 536, 537, 538], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [526], "start_line": 525, "executed_branches": [[536, 537], [536, 538]], "missing_branches": []}, "EreaderLayoutManager.jump_to_chapter_index": {"executed_lines": [550, 551, 552, 553], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [541], "start_line": 540, "executed_branches": [[551, 552], [551, 553]], "missing_branches": []}, "EreaderLayoutManager._add_to_history": {"executed_lines": [564, 568, 571], "summary": {"covered_lines": 3, "num_statements": 4, "percent_covered": 75.0, "percent_covered_display": "75", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 75.0, "percent_statements_covered_display": "75", "num_branches": 4, "num_partial_branches": 1, "covered_branches": 3, "missing_branches": 1, "percent_branches_covered": 75.0, "percent_branches_covered_display": "75"}, "missing_lines": [572], "excluded_lines": [556], "start_line": 555, "executed_branches": [[564, -555], [564, 568], [571, -555]], "missing_branches": [[571, 572]]}, "EreaderLayoutManager._get_from_history": {"executed_lines": [592, 593, 596, 600, 609, 610, 612], "summary": {"covered_lines": 7, "num_statements": 8, "percent_covered": 85.71428571428571, "percent_covered_display": "86", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 87.5, "percent_statements_covered_display": "88", "num_branches": 6, "num_partial_branches": 1, "covered_branches": 5, "missing_branches": 1, "percent_branches_covered": 83.33333333333333, "percent_branches_covered_display": "83"}, "missing_lines": [597], "excluded_lines": [578], "start_line": 574, "executed_branches": [[592, 593], [592, 612], [596, 600], [600, 592], [600, 609]], "missing_branches": [[596, 597]]}, "EreaderLayoutManager._clear_history": {"executed_lines": [616], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [615], "start_line": 614, "executed_branches": [], "missing_branches": []}, "EreaderLayoutManager.set_font_scale": {"executed_lines": [630, 631, 633, 636], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [619], "start_line": 618, "executed_branches": [[630, 631], [630, 636]], "missing_branches": []}, "EreaderLayoutManager.get_font_scale": {"executed_lines": [640], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [639], "start_line": 638, "executed_branches": [], "missing_branches": []}, "EreaderLayoutManager.set_font_family": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 3, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [663, 666, 668], "excluded_lines": [643], "start_line": 642, "executed_branches": [], "missing_branches": []}, "EreaderLayoutManager.get_font_family": {"executed_lines": [677], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [671], "start_line": 670, "executed_branches": [], "missing_branches": []}, "EreaderLayoutManager.increase_line_spacing": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 5, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 5, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [691, 692, 693, 694, 695], "excluded_lines": [680], "start_line": 679, "executed_branches": [], "missing_branches": []}, "EreaderLayoutManager.decrease_line_spacing": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 5, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 5, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [709, 710, 711, 712, 713], "excluded_lines": [698], "start_line": 697, "executed_branches": [], "missing_branches": []}, "EreaderLayoutManager.increase_inter_block_spacing": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 5, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 5, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [727, 728, 729, 730, 731], "excluded_lines": [716], "start_line": 715, "executed_branches": [], "missing_branches": []}, "EreaderLayoutManager.decrease_inter_block_spacing": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 5, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 5, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [745, 747, 748, 749, 750], "excluded_lines": [734], "start_line": 733, "executed_branches": [], "missing_branches": []}, "EreaderLayoutManager.increase_word_spacing": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 5, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 5, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [764, 765, 766, 767, 768], "excluded_lines": [753], "start_line": 752, "executed_branches": [], "missing_branches": []}, "EreaderLayoutManager.decrease_word_spacing": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 5, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 5, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [782, 783, 784, 785, 786], "excluded_lines": [771], "start_line": 770, "executed_branches": [], "missing_branches": []}, "EreaderLayoutManager.get_table_of_contents": {"executed_lines": [796], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [790], "start_line": 788, "executed_branches": [], "missing_branches": []}, "EreaderLayoutManager.get_current_chapter": {"executed_lines": [805], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [799], "start_line": 798, "executed_branches": [], "missing_branches": []}, "EreaderLayoutManager.add_bookmark": {"executed_lines": [817, 818, 819], "summary": {"covered_lines": 3, "num_statements": 5, "percent_covered": 60.0, "percent_covered_display": "60", "missing_lines": 2, "excluded_lines": 1, "percent_statements_covered": 60.0, "percent_statements_covered_display": "60", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [820, 821], "excluded_lines": [808], "start_line": 807, "executed_branches": [], "missing_branches": []}, "EreaderLayoutManager.remove_bookmark": {"executed_lines": [833], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [824], "start_line": 823, "executed_branches": [], "missing_branches": []}, "EreaderLayoutManager.jump_to_bookmark": {"executed_lines": [845, 846, 847, 848], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [836], "start_line": 835, "executed_branches": [[846, 847], [846, 848]], "missing_branches": []}, "EreaderLayoutManager.list_bookmarks": {"executed_lines": [857], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [851], "start_line": 850, "executed_branches": [], "missing_branches": []}, "EreaderLayoutManager.highlight_point": {"executed_lines": [886, 887, 888, 890], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [874], "start_line": 869, "executed_branches": [[887, 888], [887, 890]], "missing_branches": []}, "EreaderLayoutManager.highlight_range": {"executed_lines": [911, 912, 913, 915], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [898], "start_line": 892, "executed_branches": [[912, 913], [912, 915]], "missing_branches": []}, "EreaderLayoutManager._store_highlight": {"executed_lines": [919, 922, 923], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [918], "start_line": 917, "executed_branches": [], "missing_branches": []}, "EreaderLayoutManager.remove_highlight": {"executed_lines": [935], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [926], "start_line": 925, "executed_branches": [], "missing_branches": []}, "EreaderLayoutManager.list_highlights": {"executed_lines": [939], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [938], "start_line": 937, "executed_branches": [], "missing_branches": []}, "EreaderLayoutManager.get_highlights_for_current_page": {"executed_lines": [949, 950], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [942], "start_line": 941, "executed_branches": [], "missing_branches": []}, "EreaderLayoutManager.clear_highlights": {"executed_lines": [955], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [954], "start_line": 953, "executed_branches": [], "missing_branches": []}, "EreaderLayoutManager._interaction_state": {"executed_lines": [968, 969, 970, 971, 972, 973, 974], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [967], "start_line": 966, "executed_branches": [[969, 970], [969, 974], [970, 971], [970, 972]], "missing_branches": []}, "EreaderLayoutManager.handle_hover": {"executed_lines": [986], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [977], "start_line": 976, "executed_branches": [], "missing_branches": []}, "EreaderLayoutManager.handle_touch_down": {"executed_lines": [999], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [989], "start_line": 988, "executed_branches": [], "missing_branches": []}, "EreaderLayoutManager.handle_touch_up": {"executed_lines": [1011], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1002], "start_line": 1001, "executed_branches": [], "missing_branches": []}, "EreaderLayoutManager.reset_interaction_state": {"executed_lines": [1015, 1016], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1014], "start_line": 1013, "executed_branches": [[1015, -1013], [1015, 1016]], "missing_branches": []}, "EreaderLayoutManager.get_reading_progress": {"executed_lines": [1025, 1026, 1030, 1031, 1033], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1019], "start_line": 1018, "executed_branches": [[1025, 1026], [1025, 1030]], "missing_branches": []}, "EreaderLayoutManager.has_cover": {"executed_lines": [1042], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1036], "start_line": 1035, "executed_branches": [], "missing_branches": []}, "EreaderLayoutManager.is_on_cover": {"executed_lines": [1051], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1045], "start_line": 1044, "executed_branches": [], "missing_branches": []}, "EreaderLayoutManager.jump_to_cover": {"executed_lines": [1060, 1063, 1064, 1065], "summary": {"covered_lines": 4, "num_statements": 5, "percent_covered": 71.42857142857143, "percent_covered_display": "71", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 80.0, "percent_statements_covered_display": "80", "num_branches": 2, "num_partial_branches": 1, "covered_branches": 1, "missing_branches": 1, "percent_branches_covered": 50.0, "percent_branches_covered_display": "50"}, "missing_lines": [1061], "excluded_lines": [1054], "start_line": 1053, "executed_branches": [[1060, 1063]], "missing_branches": [[1060, 1061]]}, "EreaderLayoutManager.get_position_info": {"executed_lines": [1074, 1075, 1077], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1068], "start_line": 1067, "executed_branches": [], "missing_branches": []}, "EreaderLayoutManager.get_cache_stats": {"executed_lines": [1099], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1093], "start_line": 1092, "executed_branches": [], "missing_branches": []}, "EreaderLayoutManager.shutdown": {"executed_lines": [1108, 1109, 1110, 1113, 1116], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1102], "start_line": 1101, "executed_branches": [[1108, 1109], [1108, 1110]], "missing_branches": []}, "EreaderLayoutManager.__del__": {"executed_lines": [1126, 1127], "summary": {"covered_lines": 2, "num_statements": 4, "percent_covered": 50.0, "percent_covered_display": "50", "missing_lines": 2, "excluded_lines": 1, "percent_statements_covered": 50.0, "percent_statements_covered_display": "50", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [1128, 1129], "excluded_lines": [1119], "start_line": 1118, "executed_branches": [], "missing_branches": []}, "create_ereader_manager": {"executed_lines": [1149], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1137], "start_line": 1133, "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [9, 10, 11, 13, 14, 15, 16, 17, 18, 19, 20, 21, 23, 24, 25, 27, 30, 35, 52, 65, 72, 83, 99, 111, 120, 129, 147, 161, 227, 288, 293, 298, 319, 361, 375, 392, 442, 496, 510, 525, 540, 555, 574, 614, 618, 638, 642, 670, 679, 697, 715, 733, 752, 770, 788, 798, 807, 823, 835, 850, 869, 892, 917, 925, 937, 941, 953, 966, 976, 988, 1001, 1013, 1018, 1035, 1044, 1053, 1067, 1092, 1101, 1118, 1133], "summary": {"covered_lines": 81, "num_statements": 81, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 3, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1, 31, 148], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"BookmarkManager": {"executed_lines": [43, 44, 46, 47, 49, 50, 54, 55, 56, 67, 80, 81, 93, 94, 95, 96, 97, 109, 118, 127, 136, 137, 138, 139, 140], "summary": {"covered_lines": 25, "num_statements": 31, "percent_covered": 82.85714285714286, "percent_covered_display": "83", "missing_lines": 6, "excluded_lines": 9, "percent_statements_covered": 80.64516129032258, "percent_statements_covered_display": "81", "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [60, 61, 63, 141, 142, 144], "excluded_lines": [36, 53, 66, 73, 84, 100, 112, 121, 130], "start_line": 30, "executed_branches": [[93, 94], [93, 97], [137, 138], [137, 139]], "missing_branches": []}, "EreaderLayoutManager": {"executed_lines": [182, 183, 184, 187, 188, 189, 192, 193, 194, 195, 199, 200, 203, 204, 208, 209, 212, 213, 214, 215, 218, 219, 222, 224, 291, 296, 309, 310, 313, 314, 315, 317, 330, 332, 336, 340, 341, 342, 345, 352, 353, 359, 363, 364, 367, 369, 370, 373, 386, 387, 389, 390, 403, 404, 406, 407, 410, 411, 414, 416, 420, 421, 422, 423, 429, 430, 434, 437, 438, 440, 454, 455, 461, 462, 463, 466, 469, 470, 473, 475, 477, 478, 479, 483, 486, 488, 490, 491, 492, 504, 506, 520, 521, 522, 523, 535, 536, 537, 538, 550, 551, 552, 553, 564, 568, 571, 592, 593, 596, 600, 609, 610, 612, 616, 630, 631, 633, 636, 640, 677, 796, 805, 817, 818, 819, 833, 845, 846, 847, 848, 857, 886, 887, 888, 890, 911, 912, 913, 915, 919, 922, 923, 935, 939, 949, 950, 955, 968, 969, 970, 971, 972, 973, 974, 986, 999, 1011, 1015, 1016, 1025, 1026, 1030, 1031, 1033, 1042, 1051, 1060, 1063, 1064, 1065, 1074, 1075, 1077, 1099, 1108, 1109, 1110, 1113, 1116, 1126, 1127], "summary": {"covered_lines": 181, "num_statements": 257, "percent_covered": 71.26099706744868, "percent_covered_display": "71", "missing_lines": 76, "excluded_lines": 53, "percent_statements_covered": 70.42801556420234, "percent_statements_covered_display": "70", "num_branches": 84, "num_partial_branches": 8, "covered_branches": 62, "missing_branches": 22, "percent_branches_covered": 73.80952380952381, "percent_branches_covered_display": "74"}, "missing_lines": [247, 248, 249, 251, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 271, 272, 273, 274, 275, 276, 278, 279, 280, 281, 282, 283, 285, 334, 355, 357, 409, 467, 494, 572, 597, 663, 666, 668, 691, 692, 693, 694, 695, 709, 710, 711, 712, 713, 727, 728, 729, 730, 731, 745, 747, 748, 749, 750, 764, 765, 766, 767, 768, 782, 783, 784, 785, 786, 820, 821, 1061, 1128, 1129], "excluded_lines": [169, 229, 290, 295, 299, 320, 362, 376, 393, 443, 497, 511, 526, 541, 556, 578, 615, 619, 639, 643, 671, 680, 698, 716, 734, 753, 771, 790, 799, 808, 824, 836, 851, 874, 898, 918, 926, 938, 942, 954, 967, 977, 989, 1002, 1014, 1019, 1036, 1045, 1054, 1068, 1093, 1102, 1119], "start_line": 147, "executed_branches": [[187, 188], [187, 189], [213, 214], [213, 218], [309, 310], [309, 313], [314, 315], [314, 317], [332, 336], [352, 353], [363, 364], [363, 367], [369, 370], [369, 373], [386, 387], [386, 389], [403, 404], [403, 414], [406, 407], [420, 421], [420, 429], [429, 430], [429, 440], [454, 455], [454, 466], [466, 469], [469, 470], [469, 473], [475, 477], [475, 483], [486, 488], [536, 537], [536, 538], [551, 552], [551, 553], [564, -555], [564, 568], [571, -555], [592, 593], [592, 612], [596, 600], [600, 592], [600, 609], [630, 631], [630, 636], [846, 847], [846, 848], [887, 888], [887, 890], [912, 913], [912, 915], [969, 970], [969, 974], [970, 971], [970, 972], [1015, -1013], [1015, 1016], [1025, 1026], [1025, 1030], [1060, 1063], [1108, 1109], [1108, 1110]], "missing_branches": [[257, 258], [257, 271], [259, 260], [259, 261], [261, 257], [261, 262], [263, 264], [263, 265], [272, 273], [272, 278], [279, 280], [279, 285], [281, 282], [281, 283], [332, 334], [352, 359], [406, 409], [466, 467], [486, 494], [571, 572], [596, 597], [1060, 1061]]}, "": {"executed_lines": [9, 10, 11, 13, 14, 15, 16, 17, 18, 19, 20, 21, 23, 24, 25, 27, 30, 35, 52, 65, 72, 83, 99, 111, 120, 129, 147, 161, 227, 288, 293, 298, 319, 361, 375, 392, 442, 496, 510, 525, 540, 555, 574, 614, 618, 638, 642, 670, 679, 697, 715, 733, 752, 770, 788, 798, 807, 823, 835, 850, 869, 892, 917, 925, 937, 941, 953, 966, 976, 988, 1001, 1013, 1018, 1035, 1044, 1053, 1067, 1092, 1101, 1118, 1133, 1149], "summary": {"covered_lines": 82, "num_statements": 82, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 4, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1, 31, 148, 1137], "start_line": 1, "executed_branches": [], "missing_branches": []}}}, "pyWebLayout/layout/page_buffer.py": {"executed_lines": [30, 31, 32, 34, 35, 36, 37, 38, 41, 47, 54, 57, 58, 61, 63, 67, 68, 69, 70, 72, 87, 88, 89, 90, 92, 103, 105, 106, 107, 110, 112, 113, 114, 116, 118, 133, 136, 139, 140, 141, 143, 146, 147, 149, 150, 152, 154, 155, 156, 157, 159, 166, 167, 168, 170, 181, 183, 192, 200, 203, 209, 228, 230, 231, 232, 233, 234, 235, 237, 238, 239, 241, 254, 255, 256, 259, 260, 263, 264, 265, 268, 271, 273, 275, 291, 296, 297, 305, 308, 310, 312, 334, 336, 338, 340, 342, 344], "summary": {"covered_lines": 97, "num_statements": 110, "percent_covered": 84.78260869565217, "percent_covered_display": "85", "missing_lines": 13, "excluded_lines": 19, "percent_statements_covered": 88.18181818181819, "percent_statements_covered_display": "88", "num_branches": 28, "num_partial_branches": 2, "covered_branches": 20, "missing_branches": 8, "percent_branches_covered": 71.42857142857143, "percent_branches_covered_display": "71"}, "missing_lines": [177, 178, 179, 292, 293, 300, 301, 302, 319, 320, 323, 326, 327], "excluded_lines": [1, 42, 48, 78, 93, 124, 153, 160, 171, 182, 193, 204, 217, 243, 279, 313, 335, 339, 343], "executed_branches": [[103, 105], [103, 110], [110, 112], [110, 116], [139, 140], [139, 146], [140, 141], [140, 143], [146, -118], [146, 147], [166, -159], [166, 167], [254, 255], [254, 259], [260, 263], [260, 268], [264, 265], [264, 268], [291, 296], [297, 305]], "missing_branches": [[177, -170], [177, 178], [291, 292], [297, 300], [301, 302], [301, 305], [319, -312], [319, 320]], "functions": {"PageBuffer.__init__": {"executed_lines": [54, 57, 58, 61, 63, 67, 68, 69, 70], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [48], "start_line": 47, "executed_branches": [], "missing_branches": []}, "PageBuffer.initialize": {"executed_lines": [87, 88, 89, 90], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [78], "start_line": 72, "executed_branches": [], "missing_branches": []}, "PageBuffer.get_page": {"executed_lines": [103, 105, 106, 107, 110, 112, 113, 114, 116], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [93], "start_line": 92, "executed_branches": [[103, 105], [103, 110], [110, 112], [110, 116]], "missing_branches": []}, "PageBuffer.cache_page": {"executed_lines": [133, 136, 139, 140, 141, 143, 146, 147, 149, 150], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [124], "start_line": 118, "executed_branches": [[139, 140], [139, 146], [140, 141], [140, 143], [146, -118], [146, 147]], "missing_branches": []}, "PageBuffer.invalidate_all": {"executed_lines": [154, 155, 156, 157], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [153], "start_line": 152, "executed_branches": [], "missing_branches": []}, "PageBuffer.set_font_scale": {"executed_lines": [166, 167, 168], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [160], "start_line": 159, "executed_branches": [[166, -159], [166, 167]], "missing_branches": []}, "PageBuffer.set_font_family": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 3, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 2, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [177, 178, 179], "excluded_lines": [171], "start_line": 170, "executed_branches": [], "missing_branches": [[177, -170], [177, 178]]}, "PageBuffer.get_cache_stats": {"executed_lines": [183], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [182], "start_line": 181, "executed_branches": [], "missing_branches": []}, "PageBuffer.shutdown": {"executed_lines": [200], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [193], "start_line": 192, "executed_branches": [], "missing_branches": []}, "BufferedPageRenderer.__init__": {"executed_lines": [228, 230, 231, 232, 233, 234, 235, 237, 238, 239], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [217], "start_line": 209, "executed_branches": [], "missing_branches": []}, "BufferedPageRenderer.render_page": {"executed_lines": [254, 255, 256, 259, 260, 263, 264, 265, 268, 271, 273], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [243], "start_line": 241, "executed_branches": [[254, 255], [254, 259], [260, 263], [260, 268], [264, 265], [264, 268]], "missing_branches": []}, "BufferedPageRenderer.render_page_backward": {"executed_lines": [291, 296, 297, 305, 308, 310], "summary": {"covered_lines": 6, "num_statements": 11, "percent_covered": 47.05882352941177, "percent_covered_display": "47", "missing_lines": 5, "excluded_lines": 1, "percent_statements_covered": 54.54545454545455, "percent_statements_covered_display": "55", "num_branches": 6, "num_partial_branches": 2, "covered_branches": 2, "missing_branches": 4, "percent_branches_covered": 33.333333333333336, "percent_branches_covered_display": "33"}, "missing_lines": [292, 293, 300, 301, 302], "excluded_lines": [279], "start_line": 275, "executed_branches": [[291, 296], [297, 305]], "missing_branches": [[291, 292], [297, 300], [301, 302], [301, 305]]}, "BufferedPageRenderer.set_font_family": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 5, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 5, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 2, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [319, 320, 323, 326, 327], "excluded_lines": [313], "start_line": 312, "executed_branches": [], "missing_branches": [[319, -312], [319, 320]]}, "BufferedPageRenderer.get_font_family": {"executed_lines": [336], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [335], "start_line": 334, "executed_branches": [], "missing_branches": []}, "BufferedPageRenderer.get_cache_stats": {"executed_lines": [340], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [339], "start_line": 338, "executed_branches": [], "missing_branches": []}, "BufferedPageRenderer.shutdown": {"executed_lines": [344], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [343], "start_line": 342, "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [30, 31, 32, 34, 35, 36, 37, 38, 41, 47, 72, 92, 118, 152, 159, 170, 181, 192, 203, 209, 241, 275, 312, 334, 338, 342], "summary": {"covered_lines": 26, "num_statements": 26, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 3, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1, 42, 204], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"PageBuffer": {"executed_lines": [54, 57, 58, 61, 63, 67, 68, 69, 70, 87, 88, 89, 90, 103, 105, 106, 107, 110, 112, 113, 114, 116, 133, 136, 139, 140, 141, 143, 146, 147, 149, 150, 154, 155, 156, 157, 166, 167, 168, 183, 200], "summary": {"covered_lines": 41, "num_statements": 44, "percent_covered": 91.37931034482759, "percent_covered_display": "91", "missing_lines": 3, "excluded_lines": 9, "percent_statements_covered": 93.18181818181819, "percent_statements_covered_display": "93", "num_branches": 14, "num_partial_branches": 0, "covered_branches": 12, "missing_branches": 2, "percent_branches_covered": 85.71428571428571, "percent_branches_covered_display": "86"}, "missing_lines": [177, 178, 179], "excluded_lines": [48, 78, 93, 124, 153, 160, 171, 182, 193], "start_line": 41, "executed_branches": [[103, 105], [103, 110], [110, 112], [110, 116], [139, 140], [139, 146], [140, 141], [140, 143], [146, -118], [146, 147], [166, -159], [166, 167]], "missing_branches": [[177, -170], [177, 178]]}, "BufferedPageRenderer": {"executed_lines": [228, 230, 231, 232, 233, 234, 235, 237, 238, 239, 254, 255, 256, 259, 260, 263, 264, 265, 268, 271, 273, 291, 296, 297, 305, 308, 310, 336, 340, 344], "summary": {"covered_lines": 30, "num_statements": 40, "percent_covered": 70.37037037037037, "percent_covered_display": "70", "missing_lines": 10, "excluded_lines": 7, "percent_statements_covered": 75.0, "percent_statements_covered_display": "75", "num_branches": 14, "num_partial_branches": 2, "covered_branches": 8, "missing_branches": 6, "percent_branches_covered": 57.142857142857146, "percent_branches_covered_display": "57"}, "missing_lines": [292, 293, 300, 301, 302, 319, 320, 323, 326, 327], "excluded_lines": [217, 243, 279, 313, 335, 339, 343], "start_line": 203, "executed_branches": [[254, 255], [254, 259], [260, 263], [260, 268], [264, 265], [264, 268], [291, 296], [297, 305]], "missing_branches": [[291, 292], [297, 300], [301, 302], [301, 305], [319, -312], [319, 320]]}, "": {"executed_lines": [30, 31, 32, 34, 35, 36, 37, 38, 41, 47, 72, 92, 118, 152, 159, 170, 181, 192, 203, 209, 241, 275, 312, 334, 338, 342], "summary": {"covered_lines": 26, "num_statements": 26, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 3, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1, 42, 204], "start_line": 1, "executed_branches": [], "missing_branches": []}}}, "pyWebLayout/layout/table_optimizer.py": {"executed_lines": [8, 9, 12, 40, 42, 43, 44, 47, 48, 49, 52, 53, 54, 57, 58, 61, 62, 65, 67, 69, 75, 76, 79, 80, 81, 84, 88, 89, 90, 93, 98, 99, 102, 103, 105, 106, 109, 117, 128, 129, 130, 131, 132, 135, 136, 137, 141, 142, 145, 146, 148, 149, 151, 155, 165, 167, 170, 173, 177, 183, 186, 189, 199, 200, 201, 204, 205, 208, 219, 221, 222, 224, 226, 229, 244, 245, 248, 254, 255, 256, 257, 258, 259, 260, 262, 265, 275, 276, 278, 280, 283, 284, 287, 288, 289, 294, 295, 296, 297, 302, 318, 319, 320, 323, 324, 327, 329, 331, 334, 335, 338, 341, 342, 345, 347, 349, 351, 352, 353, 356, 357, 358, 360, 361, 364, 365, 367, 368, 369, 370, 371, 372, 376, 379, 391, 394, 396], "summary": {"covered_lines": 137, "num_statements": 151, "percent_covered": 88.12785388127854, "percent_covered_display": "88", "missing_lines": 14, "excluded_lines": 9, "percent_statements_covered": 90.72847682119205, "percent_statements_covered_display": "91", "num_branches": 68, "num_partial_branches": 8, "covered_branches": 56, "missing_branches": 12, "percent_branches_covered": 82.3529411764706, "percent_branches_covered_display": "82"}, "missing_lines": [70, 71, 72, 73, 82, 152, 168, 249, 250, 251, 290, 291, 299, 374], "excluded_lines": [1, 16, 118, 190, 209, 230, 266, 306, 380], "executed_branches": [[43, 44], [43, 47], [47, 48], [47, 52], [67, 69], [67, 109], [69, 75], [79, 80], [79, 105], [81, 84], [145, -117], [145, 146], [146, 145], [146, 148], [151, 155], [165, 167], [165, 186], [167, 170], [200, 201], [200, 204], [221, 222], [221, 226], [248, 254], [255, 256], [258, 259], [258, 262], [259, 258], [259, 260], [275, 276], [275, 278], [278, 280], [283, 284], [283, 287], [287, 288], [287, 294], [319, 320], [319, 323], [329, 331], [329, 334], [341, 342], [341, 345], [345, 347], [345, 358], [349, 351], [349, 356], [351, 352], [351, 376], [356, 357], [356, 376], [358, 360], [358, 364], [360, 361], [360, 376], [367, 368], [367, 376], [368, 369]], "missing_branches": [[69, 70], [81, 82], [151, 152], [167, 168], [248, 249], [249, 250], [249, 254], [250, 249], [250, 251], [255, 262], [278, 299], [368, 374]], "functions": {"optimize_table_layout": {"executed_lines": [40, 42, 43, 44, 47, 48, 49, 52, 53, 54, 57, 58, 61, 62, 65, 67, 69, 75, 76, 79, 80, 81, 84, 88, 89, 90, 93, 98, 99, 102, 103, 105, 106, 109], "summary": {"covered_lines": 34, "num_statements": 39, "percent_covered": 86.27450980392157, "percent_covered_display": "86", "missing_lines": 5, "excluded_lines": 1, "percent_statements_covered": 87.17948717948718, "percent_statements_covered_display": "87", "num_branches": 12, "num_partial_branches": 2, "covered_branches": 10, "missing_branches": 2, "percent_branches_covered": 83.33333333333333, "percent_branches_covered_display": "83"}, "missing_lines": [70, 71, 72, 73, 82], "excluded_lines": [16], "start_line": 12, "executed_branches": [[43, 44], [43, 47], [47, 48], [47, 52], [67, 69], [67, 109], [69, 75], [79, 80], [79, 105], [81, 84]], "missing_branches": [[69, 70], [81, 82]]}, "layout_cell_content": {"executed_lines": [128, 129, 130, 131, 132, 135, 136, 137, 141, 142, 145, 146, 148, 149, 151, 155, 165, 167, 170, 173, 177, 183, 186], "summary": {"covered_lines": 23, "num_statements": 25, "percent_covered": 88.57142857142857, "percent_covered_display": "89", "missing_lines": 2, "excluded_lines": 1, "percent_statements_covered": 92.0, "percent_statements_covered_display": "92", "num_branches": 10, "num_partial_branches": 2, "covered_branches": 8, "missing_branches": 2, "percent_branches_covered": 80.0, "percent_branches_covered_display": "80"}, "missing_lines": [152, 168], "excluded_lines": [118], "start_line": 117, "executed_branches": [[145, -117], [145, 146], [146, 145], [146, 148], [151, 155], [165, 167], [165, 186], [167, 170]], "missing_branches": [[151, 152], [167, 168]]}, "get_column_count": {"executed_lines": [199, 200, 201, 204, 205], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [190], "start_line": 189, "executed_branches": [[200, 201], [200, 204]], "missing_branches": []}, "sample_table_rows": {"executed_lines": [219, 221, 222, 224, 226], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [209], "start_line": 208, "executed_branches": [[221, 222], [221, 226]], "missing_branches": []}, "extract_html_column_widths": {"executed_lines": [244, 245, 248, 254, 255, 256, 257, 258, 259, 260, 262], "summary": {"covered_lines": 11, "num_statements": 14, "percent_covered": 65.38461538461539, "percent_covered_display": "65", "missing_lines": 3, "excluded_lines": 1, "percent_statements_covered": 78.57142857142857, "percent_statements_covered_display": "79", "num_branches": 12, "num_partial_branches": 2, "covered_branches": 6, "missing_branches": 6, "percent_branches_covered": 50.0, "percent_branches_covered_display": "50"}, "missing_lines": [249, 250, 251], "excluded_lines": [230], "start_line": 229, "executed_branches": [[248, 254], [255, 256], [258, 259], [258, 262], [259, 258], [259, 260]], "missing_branches": [[248, 249], [249, 250], [249, 254], [250, 249], [250, 251], [255, 262]]}, "parse_html_width": {"executed_lines": [275, 276, 278, 280, 283, 284, 287, 288, 289, 294, 295, 296, 297], "summary": {"covered_lines": 13, "num_statements": 16, "percent_covered": 83.33333333333333, "percent_covered_display": "83", "missing_lines": 3, "excluded_lines": 1, "percent_statements_covered": 81.25, "percent_statements_covered_display": "81", "num_branches": 8, "num_partial_branches": 1, "covered_branches": 7, "missing_branches": 1, "percent_branches_covered": 87.5, "percent_branches_covered_display": "88"}, "missing_lines": [290, 291, 299], "excluded_lines": [266], "start_line": 265, "executed_branches": [[275, 276], [275, 278], [278, 280], [283, 284], [283, 287], [287, 288], [287, 294]], "missing_branches": [[278, 299]]}, "distribute_column_widths": {"executed_lines": [318, 319, 320, 323, 324, 327, 329, 331, 334, 335, 338, 341, 342, 345, 347, 349, 351, 352, 353, 356, 357, 358, 360, 361, 364, 365, 367, 368, 369, 370, 371, 372, 376], "summary": {"covered_lines": 33, "num_statements": 34, "percent_covered": 96.42857142857143, "percent_covered_display": "96", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 97.05882352941177, "percent_statements_covered_display": "97", "num_branches": 22, "num_partial_branches": 1, "covered_branches": 21, "missing_branches": 1, "percent_branches_covered": 95.45454545454545, "percent_branches_covered_display": "95"}, "missing_lines": [374], "excluded_lines": [306], "start_line": 302, "executed_branches": [[319, 320], [319, 323], [329, 331], [329, 334], [341, 342], [341, 345], [345, 347], [345, 358], [349, 351], [349, 356], [351, 352], [351, 376], [356, 357], [356, 376], [358, 360], [358, 364], [360, 361], [360, 376], [367, 368], [367, 376], [368, 369]], "missing_branches": [[368, 374]]}, "calculate_table_overhead": {"executed_lines": [391, 394, 396], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [380], "start_line": 379, "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [8, 9, 12, 117, 189, 208, 229, 265, 302, 379], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [8, 9, 12, 40, 42, 43, 44, 47, 48, 49, 52, 53, 54, 57, 58, 61, 62, 65, 67, 69, 75, 76, 79, 80, 81, 84, 88, 89, 90, 93, 98, 99, 102, 103, 105, 106, 109, 117, 128, 129, 130, 131, 132, 135, 136, 137, 141, 142, 145, 146, 148, 149, 151, 155, 165, 167, 170, 173, 177, 183, 186, 189, 199, 200, 201, 204, 205, 208, 219, 221, 222, 224, 226, 229, 244, 245, 248, 254, 255, 256, 257, 258, 259, 260, 262, 265, 275, 276, 278, 280, 283, 284, 287, 288, 289, 294, 295, 296, 297, 302, 318, 319, 320, 323, 324, 327, 329, 331, 334, 335, 338, 341, 342, 345, 347, 349, 351, 352, 353, 356, 357, 358, 360, 361, 364, 365, 367, 368, 369, 370, 371, 372, 376, 379, 391, 394, 396], "summary": {"covered_lines": 137, "num_statements": 151, "percent_covered": 88.12785388127854, "percent_covered_display": "88", "missing_lines": 14, "excluded_lines": 9, "percent_statements_covered": 90.72847682119205, "percent_statements_covered_display": "91", "num_branches": 68, "num_partial_branches": 8, "covered_branches": 56, "missing_branches": 12, "percent_branches_covered": 82.3529411764706, "percent_branches_covered_display": "82"}, "missing_lines": [70, 71, 72, 73, 82, 152, 168, 249, 250, 251, 290, 291, 299, 374], "excluded_lines": [1, 16, 118, 190, 209, 230, 266, 306, 380], "start_line": 1, "executed_branches": [[43, 44], [43, 47], [47, 48], [47, 52], [67, 69], [67, 109], [69, 75], [79, 80], [79, 105], [81, 84], [145, -117], [145, 146], [146, 145], [146, 148], [151, 155], [165, 167], [165, 186], [167, 170], [200, 201], [200, 204], [221, 222], [221, 226], [248, 254], [255, 256], [258, 259], [258, 262], [259, 258], [259, 260], [275, 276], [275, 278], [278, 280], [283, 284], [283, 287], [287, 288], [287, 294], [319, 320], [319, 323], [329, 331], [329, 334], [341, 342], [341, 345], [345, 347], [345, 358], [349, 351], [349, 356], [351, 352], [351, 376], [356, 357], [356, 376], [358, 360], [358, 364], [360, 361], [360, 376], [367, 368], [367, 376], [368, 369]], "missing_branches": [[69, 70], [81, 82], [151, 152], [167, 168], [248, 249], [249, 250], [249, 254], [250, 249], [250, 251], [255, 262], [278, 299], [368, 374]]}}}, "pyWebLayout/style/__init__.py": {"executed_lines": [7, 11, 14, 15, 16, 18], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1], "executed_branches": [], "missing_branches": [], "functions": {"": {"executed_lines": [7, 11, 14, 15, 16, 18], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [7, 11, 14, 15, 16, 18], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1], "start_line": 1, "executed_branches": [], "missing_branches": []}}}, "pyWebLayout/style/abstract_style.py": {"executed_lines": [9, 10, 11, 12, 13, 16, 18, 19, 20, 21, 22, 25, 27, 28, 29, 30, 31, 32, 33, 36, 37, 56, 59, 60, 73, 74, 75, 76, 77, 80, 81, 85, 86, 87, 88, 89, 90, 93, 96, 98, 101, 108, 122, 123, 124, 127, 145, 146, 147, 149, 178, 188, 193, 194, 197, 205, 207, 209, 210, 213, 215, 217, 218, 219, 220, 221, 223, 224, 226, 228, 230, 231, 232, 234, 244, 246, 261, 262, 265, 266, 268, 269, 270, 272, 288, 290, 291, 293, 296, 297, 298, 301, 302, 304, 306, 308, 320, 321, 325, 326, 328, 338, 339, 342, 343, 349, 353, 355], "summary": {"covered_lines": 108, "num_statements": 135, "percent_covered": 76.1006289308176, "percent_covered_display": "76", "missing_lines": 27, "excluded_lines": 22, "percent_statements_covered": 80.0, "percent_statements_covered_display": "80", "num_branches": 24, "num_partial_branches": 7, "covered_branches": 13, "missing_branches": 11, "percent_branches_covered": 54.166666666666664, "percent_branches_covered_display": "54"}, "missing_lines": [39, 40, 41, 42, 43, 44, 46, 47, 48, 49, 50, 102, 103, 104, 106, 161, 166, 173, 174, 176, 263, 292, 322, 340, 346, 347, 351], "excluded_lines": [1, 17, 26, 38, 61, 99, 109, 150, 179, 198, 206, 216, 225, 229, 235, 250, 276, 305, 310, 329, 350, 354], "executed_branches": [[101, -98], [123, 124], [123, 127], [262, 265], [265, 266], [288, 290], [288, 296], [291, 293], [297, 298], [297, 301], [321, 325], [339, 342], [342, 343]], "missing_branches": [[39, 40], [39, 41], [41, 42], [41, 50], [101, 102], [262, 263], [265, 268], [291, 292], [321, 322], [339, 340], [342, 346]], "functions": {"FontSize.from_value": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 11, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 11, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 4, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 4, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [39, 40, 41, 42, 43, 44, 46, 47, 48, 49, 50], "excluded_lines": [38], "start_line": 37, "executed_branches": [], "missing_branches": [[39, 40], [39, 41], [41, 42], [41, 50]]}, "AbstractStyle.__post_init__": {"executed_lines": [101], "summary": {"covered_lines": 1, "num_statements": 5, "percent_covered": 28.571428571428573, "percent_covered_display": "29", "missing_lines": 4, "excluded_lines": 1, "percent_statements_covered": 20.0, "percent_statements_covered_display": "20", "num_branches": 2, "num_partial_branches": 1, "covered_branches": 1, "missing_branches": 1, "percent_branches_covered": 50.0, "percent_branches_covered_display": "50"}, "missing_lines": [102, 103, 104, 106], "excluded_lines": [99], "start_line": 98, "executed_branches": [[101, -98]], "missing_branches": [[101, 102]]}, "AbstractStyle.__hash__": {"executed_lines": [122, 123, 124, 127, 145, 146, 147], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [109], "start_line": 108, "executed_branches": [[123, 124], [123, 127]], "missing_branches": []}, "AbstractStyle.merge_with": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 5, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 5, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [161, 166, 173, 174, 176], "excluded_lines": [150], "start_line": 149, "executed_branches": [], "missing_branches": []}, "AbstractStyle.with_modifications": {"executed_lines": [188, 193, 194], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [179], "start_line": 178, "executed_branches": [], "missing_branches": []}, "AbstractStyleRegistry.__init__": {"executed_lines": [207, 209, 210, 213], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [206], "start_line": 205, "executed_branches": [], "missing_branches": []}, "AbstractStyleRegistry._create_default_style": {"executed_lines": [217, 218, 219, 220, 221], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [216], "start_line": 215, "executed_branches": [], "missing_branches": []}, "AbstractStyleRegistry.default_style": {"executed_lines": [226], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [225], "start_line": 224, "executed_branches": [], "missing_branches": []}, "AbstractStyleRegistry._generate_style_id": {"executed_lines": [230, 231, 232], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [229], "start_line": 228, "executed_branches": [], "missing_branches": []}, "AbstractStyleRegistry.get_style_id": {"executed_lines": [244], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [235], "start_line": 234, "executed_branches": [], "missing_branches": []}, "AbstractStyleRegistry.register_style": {"executed_lines": [261, 262, 265, 266, 268, 269, 270], "summary": {"covered_lines": 7, "num_statements": 8, "percent_covered": 75.0, "percent_covered_display": "75", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 87.5, "percent_statements_covered_display": "88", "num_branches": 4, "num_partial_branches": 2, "covered_branches": 2, "missing_branches": 2, "percent_branches_covered": 50.0, "percent_branches_covered_display": "50"}, "missing_lines": [263], "excluded_lines": [250], "start_line": 246, "executed_branches": [[262, 265], [265, 266]], "missing_branches": [[262, 263], [265, 268]]}, "AbstractStyleRegistry.get_or_create_style": {"executed_lines": [288, 290, 291, 293, 296, 297, 298, 301, 302], "summary": {"covered_lines": 9, "num_statements": 10, "percent_covered": 87.5, "percent_covered_display": "88", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 90.0, "percent_statements_covered_display": "90", "num_branches": 6, "num_partial_branches": 1, "covered_branches": 5, "missing_branches": 1, "percent_branches_covered": 83.33333333333333, "percent_branches_covered_display": "83"}, "missing_lines": [292], "excluded_lines": [276], "start_line": 272, "executed_branches": [[288, 290], [288, 296], [291, 293], [297, 298], [297, 301]], "missing_branches": [[291, 292]]}, "AbstractStyleRegistry.get_style_by_id": {"executed_lines": [306], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [305], "start_line": 304, "executed_branches": [], "missing_branches": []}, "AbstractStyleRegistry.create_derived_style": {"executed_lines": [320, 321, 325, 326], "summary": {"covered_lines": 4, "num_statements": 5, "percent_covered": 71.42857142857143, "percent_covered_display": "71", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 80.0, "percent_statements_covered_display": "80", "num_branches": 2, "num_partial_branches": 1, "covered_branches": 1, "missing_branches": 1, "percent_branches_covered": 50.0, "percent_branches_covered_display": "50"}, "missing_lines": [322], "excluded_lines": [310], "start_line": 308, "executed_branches": [[321, 325]], "missing_branches": [[321, 322]]}, "AbstractStyleRegistry.resolve_effective_style": {"executed_lines": [338, 339, 342, 343], "summary": {"covered_lines": 4, "num_statements": 7, "percent_covered": 54.54545454545455, "percent_covered_display": "55", "missing_lines": 3, "excluded_lines": 1, "percent_statements_covered": 57.142857142857146, "percent_statements_covered_display": "57", "num_branches": 4, "num_partial_branches": 2, "covered_branches": 2, "missing_branches": 2, "percent_branches_covered": 50.0, "percent_branches_covered_display": "50"}, "missing_lines": [340, 346, 347], "excluded_lines": [329], "start_line": 328, "executed_branches": [[339, 342], [342, 343]], "missing_branches": [[339, 340], [342, 346]]}, "AbstractStyleRegistry.get_all_styles": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [351], "excluded_lines": [350], "start_line": 349, "executed_branches": [], "missing_branches": []}, "AbstractStyleRegistry.get_style_count": {"executed_lines": [355], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [354], "start_line": 353, "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [9, 10, 11, 12, 13, 16, 18, 19, 20, 21, 22, 25, 27, 28, 29, 30, 31, 32, 33, 36, 37, 56, 59, 60, 73, 74, 75, 76, 77, 80, 81, 85, 86, 87, 88, 89, 90, 93, 96, 98, 108, 149, 178, 197, 205, 215, 223, 224, 228, 234, 246, 272, 304, 308, 328, 349, 353], "summary": {"covered_lines": 57, "num_statements": 57, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 5, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1, 17, 26, 61, 198], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"FontFamily": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 16, "executed_branches": [], "missing_branches": []}, "FontSize": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 11, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 11, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 4, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 4, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [39, 40, 41, 42, 43, 44, 46, 47, 48, 49, 50], "excluded_lines": [38], "start_line": 25, "executed_branches": [], "missing_branches": [[39, 40], [39, 41], [41, 42], [41, 50]]}, "AbstractStyle": {"executed_lines": [101, 122, 123, 124, 127, 145, 146, 147, 188, 193, 194], "summary": {"covered_lines": 11, "num_statements": 20, "percent_covered": 58.333333333333336, "percent_covered_display": "58", "missing_lines": 9, "excluded_lines": 4, "percent_statements_covered": 55.0, "percent_statements_covered_display": "55", "num_branches": 4, "num_partial_branches": 1, "covered_branches": 3, "missing_branches": 1, "percent_branches_covered": 75.0, "percent_branches_covered_display": "75"}, "missing_lines": [102, 103, 104, 106, 161, 166, 173, 174, 176], "excluded_lines": [99, 109, 150, 179], "start_line": 60, "executed_branches": [[101, -98], [123, 124], [123, 127]], "missing_branches": [[101, 102]]}, "AbstractStyleRegistry": {"executed_lines": [207, 209, 210, 213, 217, 218, 219, 220, 221, 226, 230, 231, 232, 244, 261, 262, 265, 266, 268, 269, 270, 288, 290, 291, 293, 296, 297, 298, 301, 302, 306, 320, 321, 325, 326, 338, 339, 342, 343, 355], "summary": {"covered_lines": 40, "num_statements": 47, "percent_covered": 79.36507936507937, "percent_covered_display": "79", "missing_lines": 7, "excluded_lines": 12, "percent_statements_covered": 85.1063829787234, "percent_statements_covered_display": "85", "num_branches": 16, "num_partial_branches": 6, "covered_branches": 10, "missing_branches": 6, "percent_branches_covered": 62.5, "percent_branches_covered_display": "62"}, "missing_lines": [263, 292, 322, 340, 346, 347, 351], "excluded_lines": [206, 216, 225, 229, 235, 250, 276, 305, 310, 329, 350, 354], "start_line": 197, "executed_branches": [[262, 265], [265, 266], [288, 290], [288, 296], [291, 293], [297, 298], [297, 301], [321, 325], [339, 342], [342, 343]], "missing_branches": [[262, 263], [265, 268], [291, 292], [321, 322], [339, 340], [342, 346]]}, "": {"executed_lines": [9, 10, 11, 12, 13, 16, 18, 19, 20, 21, 22, 25, 27, 28, 29, 30, 31, 32, 33, 36, 37, 56, 59, 60, 73, 74, 75, 76, 77, 80, 81, 85, 86, 87, 88, 89, 90, 93, 96, 98, 108, 149, 178, 197, 205, 215, 223, 224, 228, 234, 246, 272, 304, 308, 328, 349, 353], "summary": {"covered_lines": 57, "num_statements": 57, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 5, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1, 17, 26, 61, 198], "start_line": 1, "executed_branches": [], "missing_branches": []}}}, "pyWebLayout/style/alignment.py": {"executed_lines": [7, 10, 13, 14, 15, 16, 19, 20, 21, 23], "summary": {"covered_lines": 10, "num_statements": 11, "percent_covered": 90.9090909090909, "percent_covered_display": "91", "missing_lines": 1, "excluded_lines": 3, "percent_statements_covered": 90.9090909090909, "percent_statements_covered_display": "91", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [25], "excluded_lines": [1, 11, 24], "executed_branches": [], "missing_branches": [], "functions": {"Alignment.__str__": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [25], "excluded_lines": [24], "start_line": 23, "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [7, 10, 13, 14, 15, 16, 19, 20, 21, 23], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 2, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1, 11], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"Alignment": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [25], "excluded_lines": [24], "start_line": 10, "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [7, 10, 13, 14, 15, 16, 19, 20, 21, 23], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 2, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1, 11], "start_line": 1, "executed_branches": [], "missing_branches": []}}}, "pyWebLayout/style/concrete_style.py": {"executed_lines": [8, 9, 10, 11, 12, 15, 16, 23, 24, 25, 26, 27, 30, 31, 32, 35, 36, 37, 40, 43, 44, 53, 54, 55, 56, 59, 60, 61, 65, 66, 67, 68, 69, 70, 73, 74, 77, 79, 81, 94, 102, 109, 110, 113, 124, 145, 156, 157, 160, 161, 163, 164, 165, 167, 168, 170, 172, 174, 176, 179, 181, 182, 183, 186, 187, 188, 190, 191, 193, 196, 216, 217, 219, 221, 222, 231, 234, 236, 237, 238, 240, 250, 253, 254, 257, 259, 262, 263, 265, 267, 268, 269, 271, 272, 273, 276, 277, 278, 279, 280, 289, 298, 302, 312, 313, 331, 333, 334, 347, 350, 351, 371, 374, 375, 377, 378, 380, 381, 382, 383, 384, 395, 414, 418, 420, 423, 431, 438, 439, 441, 451, 453, 463, 466, 467, 470, 471, 473, 475, 480, 482], "summary": {"covered_lines": 141, "num_statements": 207, "percent_covered": 63.44086021505376, "percent_covered_display": "63", "missing_lines": 66, "excluded_lines": 23, "percent_statements_covered": 68.1159420289855, "percent_statements_covered_display": "68", "num_branches": 72, "num_partial_branches": 12, "covered_branches": 36, "missing_branches": 36, "percent_branches_covered": 50.0, "percent_branches_covered_display": "50"}, "missing_lines": [223, 224, 225, 226, 229, 243, 244, 245, 247, 275, 282, 283, 284, 286, 291, 292, 293, 294, 296, 300, 315, 316, 318, 319, 321, 322, 323, 326, 327, 329, 336, 337, 339, 340, 341, 342, 343, 345, 353, 354, 356, 357, 358, 359, 360, 361, 362, 364, 365, 366, 367, 369, 385, 386, 388, 389, 390, 391, 393, 403, 407, 409, 412, 416, 477, 478], "excluded_lines": [1, 17, 45, 80, 95, 103, 146, 220, 232, 261, 311, 332, 349, 373, 396, 415, 419, 424, 432, 442, 454, 476, 481], "executed_branches": [[156, 157], [156, 160], [179, 181], [179, 188], [181, 182], [181, 186], [188, 190], [188, 191], [191, 193], [191, 196], [221, 222], [234, 236], [234, 238], [238, 240], [253, 254], [253, 257], [262, 263], [262, 265], [265, 267], [267, 268], [267, 269], [269, 271], [273, 276], [276, 277], [289, 298], [312, 313], [333, 334], [350, 351], [374, 375], [374, 377], [377, 378], [377, 380], [380, 381], [381, 382], [466, 467], [466, 470]], "missing_branches": [[221, 223], [223, 224], [223, 225], [225, 226], [225, 229], [238, 243], [265, 300], [269, 286], [273, 275], [276, 282], [289, 291], [293, 294], [293, 296], [312, 315], [315, 316], [315, 321], [316, 318], [316, 319], [321, 322], [321, 329], [322, 323], [322, 326], [333, 336], [336, 337], [336, 339], [339, 340], [339, 345], [350, 353], [353, 354], [353, 356], [356, 357], [356, 369], [357, 358], [357, 364], [380, 393], [381, 388]], "functions": {"ConcreteStyle.create_font": {"executed_lines": [81], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [80], "start_line": 79, "executed_branches": [], "missing_branches": []}, "StyleResolver.__init__": {"executed_lines": [109, 110, 113, 124], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [103], "start_line": 102, "executed_branches": [], "missing_branches": []}, "StyleResolver.resolve_style": {"executed_lines": [156, 157, 160, 161, 163, 164, 165, 167, 168, 170, 172, 174, 176, 179, 181, 182, 183, 186, 187, 188, 190, 191, 193, 196, 216, 217], "summary": {"covered_lines": 26, "num_statements": 26, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 10, "num_partial_branches": 0, "covered_branches": 10, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [146], "start_line": 145, "executed_branches": [[156, 157], [156, 160], [179, 181], [179, 188], [181, 182], [181, 186], [188, 190], [188, 191], [191, 193], [191, 196]], "missing_branches": []}, "StyleResolver._resolve_font_path": {"executed_lines": [221, 222], "summary": {"covered_lines": 2, "num_statements": 7, "percent_covered": 23.076923076923077, "percent_covered_display": "23", "missing_lines": 5, "excluded_lines": 1, "percent_statements_covered": 28.571428571428573, "percent_statements_covered_display": "29", "num_branches": 6, "num_partial_branches": 1, "covered_branches": 1, "missing_branches": 5, "percent_branches_covered": 16.666666666666668, "percent_branches_covered_display": "17"}, "missing_lines": [223, 224, 225, 226, 229], "excluded_lines": [220], "start_line": 219, "executed_branches": [[221, 222]], "missing_branches": [[221, 223], [223, 224], [223, 225], [225, 226], [225, 229]]}, "StyleResolver._resolve_font_size": {"executed_lines": [234, 236, 237, 238, 240, 250, 253, 254, 257], "summary": {"covered_lines": 9, "num_statements": 13, "percent_covered": 73.6842105263158, "percent_covered_display": "74", "missing_lines": 4, "excluded_lines": 1, "percent_statements_covered": 69.23076923076923, "percent_statements_covered_display": "69", "num_branches": 6, "num_partial_branches": 1, "covered_branches": 5, "missing_branches": 1, "percent_branches_covered": 83.33333333333333, "percent_branches_covered_display": "83"}, "missing_lines": [243, 244, 245, 247], "excluded_lines": [232], "start_line": 231, "executed_branches": [[234, 236], [234, 238], [238, 240], [253, 254], [253, 257]], "missing_branches": [[238, 243]]}, "StyleResolver._resolve_color": {"executed_lines": [262, 263, 265, 267, 268, 269, 271, 272, 273, 276, 277, 278, 279, 280, 289, 298], "summary": {"covered_lines": 16, "num_statements": 27, "percent_covered": 58.13953488372093, "percent_covered_display": "58", "missing_lines": 11, "excluded_lines": 1, "percent_statements_covered": 59.25925925925926, "percent_statements_covered_display": "59", "num_branches": 16, "num_partial_branches": 5, "covered_branches": 9, "missing_branches": 7, "percent_branches_covered": 56.25, "percent_branches_covered_display": "56"}, "missing_lines": [275, 282, 283, 284, 286, 291, 292, 293, 294, 296, 300], "excluded_lines": [261], "start_line": 259, "executed_branches": [[262, 263], [262, 265], [265, 267], [267, 268], [267, 269], [269, 271], [273, 276], [276, 277], [289, 298]], "missing_branches": [[265, 300], [269, 286], [273, 275], [276, 282], [289, 291], [293, 294], [293, 296]]}, "StyleResolver._resolve_background_color": {"executed_lines": [312, 313], "summary": {"covered_lines": 2, "num_statements": 12, "percent_covered": 13.636363636363637, "percent_covered_display": "14", "missing_lines": 10, "excluded_lines": 1, "percent_statements_covered": 16.666666666666668, "percent_statements_covered_display": "17", "num_branches": 10, "num_partial_branches": 1, "covered_branches": 1, "missing_branches": 9, "percent_branches_covered": 10.0, "percent_branches_covered_display": "10"}, "missing_lines": [315, 316, 318, 319, 321, 322, 323, 326, 327, 329], "excluded_lines": [311], "start_line": 302, "executed_branches": [[312, 313]], "missing_branches": [[312, 315], [315, 316], [315, 321], [316, 318], [316, 319], [321, 322], [321, 329], [322, 323], [322, 326]]}, "StyleResolver._resolve_line_height": {"executed_lines": [333, 334], "summary": {"covered_lines": 2, "num_statements": 10, "percent_covered": 18.75, "percent_covered_display": "19", "missing_lines": 8, "excluded_lines": 1, "percent_statements_covered": 20.0, "percent_statements_covered_display": "20", "num_branches": 6, "num_partial_branches": 1, "covered_branches": 1, "missing_branches": 5, "percent_branches_covered": 16.666666666666668, "percent_branches_covered_display": "17"}, "missing_lines": [336, 337, 339, 340, 341, 342, 343, 345], "excluded_lines": [332], "start_line": 331, "executed_branches": [[333, 334]], "missing_branches": [[333, 336], [336, 337], [336, 339], [339, 340], [339, 345]]}, "StyleResolver._resolve_letter_spacing": {"executed_lines": [350, 351], "summary": {"covered_lines": 2, "num_statements": 16, "percent_covered": 12.5, "percent_covered_display": "12", "missing_lines": 14, "excluded_lines": 1, "percent_statements_covered": 12.5, "percent_statements_covered_display": "12", "num_branches": 8, "num_partial_branches": 1, "covered_branches": 1, "missing_branches": 7, "percent_branches_covered": 12.5, "percent_branches_covered_display": "12"}, "missing_lines": [353, 354, 356, 357, 358, 359, 360, 361, 362, 364, 365, 366, 367, 369], "excluded_lines": [349], "start_line": 347, "executed_branches": [[350, 351]], "missing_branches": [[350, 353], [353, 354], [353, 356], [356, 357], [356, 369], [357, 358], [357, 364]]}, "StyleResolver._resolve_word_spacing": {"executed_lines": [374, 375, 377, 378, 380, 381, 382, 383, 384], "summary": {"covered_lines": 9, "num_statements": 16, "percent_covered": 62.5, "percent_covered_display": "62", "missing_lines": 7, "excluded_lines": 1, "percent_statements_covered": 56.25, "percent_statements_covered_display": "56", "num_branches": 8, "num_partial_branches": 2, "covered_branches": 6, "missing_branches": 2, "percent_branches_covered": 75.0, "percent_branches_covered_display": "75"}, "missing_lines": [385, 386, 388, 389, 390, 391, 393], "excluded_lines": [373], "start_line": 371, "executed_branches": [[374, 375], [374, 377], [377, 378], [377, 380], [380, 381], [381, 382]], "missing_branches": [[380, 393], [381, 388]]}, "StyleResolver.update_context": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 4, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 4, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [403, 407, 409, 412], "excluded_lines": [396], "start_line": 395, "executed_branches": [], "missing_branches": []}, "StyleResolver.clear_cache": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [416], "excluded_lines": [415], "start_line": 414, "executed_branches": [], "missing_branches": []}, "StyleResolver.get_cache_size": {"executed_lines": [420], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [419], "start_line": 418, "executed_branches": [], "missing_branches": []}, "ConcreteStyleRegistry.__init__": {"executed_lines": [438, 439], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [432], "start_line": 431, "executed_branches": [], "missing_branches": []}, "ConcreteStyleRegistry.get_concrete_style": {"executed_lines": [451], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [442], "start_line": 441, "executed_branches": [], "missing_branches": []}, "ConcreteStyleRegistry.get_font": {"executed_lines": [463, 466, 467, 470, 471, 473], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [454], "start_line": 453, "executed_branches": [[466, 467], [466, 470]], "missing_branches": []}, "ConcreteStyleRegistry.clear_caches": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 2, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 2, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [477, 478], "excluded_lines": [476], "start_line": 475, "executed_branches": [], "missing_branches": []}, "ConcreteStyleRegistry.get_cache_stats": {"executed_lines": [482], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [481], "start_line": 480, "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [8, 9, 10, 11, 12, 15, 16, 23, 24, 25, 26, 27, 30, 31, 32, 35, 36, 37, 40, 43, 44, 53, 54, 55, 56, 59, 60, 61, 65, 66, 67, 68, 69, 70, 73, 74, 77, 79, 94, 102, 145, 219, 231, 259, 302, 331, 347, 371, 395, 414, 418, 423, 431, 441, 453, 475, 480], "summary": {"covered_lines": 57, "num_statements": 57, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 5, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1, 17, 45, 95, 424], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"RenderingContext": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 16, "executed_branches": [], "missing_branches": []}, "ConcreteStyle": {"executed_lines": [81], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [80], "start_line": 44, "executed_branches": [], "missing_branches": []}, "StyleResolver": {"executed_lines": [109, 110, 113, 124, 156, 157, 160, 161, 163, 164, 165, 167, 168, 170, 172, 174, 176, 179, 181, 182, 183, 186, 187, 188, 190, 191, 193, 196, 216, 217, 221, 222, 234, 236, 237, 238, 240, 250, 253, 254, 257, 262, 263, 265, 267, 268, 269, 271, 272, 273, 276, 277, 278, 279, 280, 289, 298, 312, 313, 333, 334, 350, 351, 374, 375, 377, 378, 380, 381, 382, 383, 384, 420], "summary": {"covered_lines": 73, "num_statements": 137, "percent_covered": 51.690821256038646, "percent_covered_display": "52", "missing_lines": 64, "excluded_lines": 12, "percent_statements_covered": 53.284671532846716, "percent_statements_covered_display": "53", "num_branches": 70, "num_partial_branches": 12, "covered_branches": 34, "missing_branches": 36, "percent_branches_covered": 48.57142857142857, "percent_branches_covered_display": "49"}, "missing_lines": [223, 224, 225, 226, 229, 243, 244, 245, 247, 275, 282, 283, 284, 286, 291, 292, 293, 294, 296, 300, 315, 316, 318, 319, 321, 322, 323, 326, 327, 329, 336, 337, 339, 340, 341, 342, 343, 345, 353, 354, 356, 357, 358, 359, 360, 361, 362, 364, 365, 366, 367, 369, 385, 386, 388, 389, 390, 391, 393, 403, 407, 409, 412, 416], "excluded_lines": [103, 146, 220, 232, 261, 311, 332, 349, 373, 396, 415, 419], "start_line": 94, "executed_branches": [[156, 157], [156, 160], [179, 181], [179, 188], [181, 182], [181, 186], [188, 190], [188, 191], [191, 193], [191, 196], [221, 222], [234, 236], [234, 238], [238, 240], [253, 254], [253, 257], [262, 263], [262, 265], [265, 267], [267, 268], [267, 269], [269, 271], [273, 276], [276, 277], [289, 298], [312, 313], [333, 334], [350, 351], [374, 375], [374, 377], [377, 378], [377, 380], [380, 381], [381, 382]], "missing_branches": [[221, 223], [223, 224], [223, 225], [225, 226], [225, 229], [238, 243], [265, 300], [269, 286], [273, 275], [276, 282], [289, 291], [293, 294], [293, 296], [312, 315], [315, 316], [315, 321], [316, 318], [316, 319], [321, 322], [321, 329], [322, 323], [322, 326], [333, 336], [336, 337], [336, 339], [339, 340], [339, 345], [350, 353], [353, 354], [353, 356], [356, 357], [356, 369], [357, 358], [357, 364], [380, 393], [381, 388]]}, "ConcreteStyleRegistry": {"executed_lines": [438, 439, 451, 463, 466, 467, 470, 471, 473, 482], "summary": {"covered_lines": 10, "num_statements": 12, "percent_covered": 85.71428571428571, "percent_covered_display": "86", "missing_lines": 2, "excluded_lines": 5, "percent_statements_covered": 83.33333333333333, "percent_statements_covered_display": "83", "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [477, 478], "excluded_lines": [432, 442, 454, 476, 481], "start_line": 423, "executed_branches": [[466, 467], [466, 470]], "missing_branches": []}, "": {"executed_lines": [8, 9, 10, 11, 12, 15, 16, 23, 24, 25, 26, 27, 30, 31, 32, 35, 36, 37, 40, 43, 44, 53, 54, 55, 56, 59, 60, 61, 65, 66, 67, 68, 69, 70, 73, 74, 77, 79, 94, 102, 145, 219, 231, 259, 302, 331, 347, 371, 395, 414, 418, 423, 431, 441, 453, 475, 480], "summary": {"covered_lines": 57, "num_statements": 57, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 5, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [1, 17, 45, 95, 424], "start_line": 1, "executed_branches": [], "missing_branches": []}}}, "pyWebLayout/style/fonts.py": {"executed_lines": [3, 4, 5, 6, 7, 10, 14, 17, 20, 23, 24, 25, 28, 29, 30, 33, 34, 35, 36, 39, 41, 42, 43, 46, 73, 138, 144, 169, 170, 171, 172, 173, 174, 175, 176, 177, 179, 181, 182, 232, 237, 238, 242, 244, 245, 247, 248, 249, 250, 254, 255, 256, 257, 264, 267, 268, 269, 272, 275, 276, 277, 278, 281, 282, 284, 285, 292, 294, 295, 296, 297, 308, 309, 311, 313, 314, 317, 318, 320, 322, 323, 325, 327, 328, 330, 332, 333, 335, 337, 338, 340, 342, 343, 345, 347, 348, 350, 352, 353, 355, 357, 358, 360, 362, 374, 385, 386, 388, 390, 392, 394, 396, 398, 400, 402, 404, 406], "summary": {"covered_lines": 117, "num_statements": 161, "percent_covered": 65.80310880829016, "percent_covered_display": "66", "missing_lines": 44, "excluded_lines": 23, "percent_statements_covered": 72.67080745341615, "percent_statements_covered_display": "73", "num_branches": 32, "num_partial_branches": 2, "covered_branches": 10, "missing_branches": 22, "percent_branches_covered": 31.25, "percent_branches_covered_display": "31"}, "missing_lines": [56, 57, 60, 61, 63, 64, 65, 66, 68, 69, 70, 94, 95, 96, 99, 105, 108, 110, 112, 113, 114, 115, 117, 118, 119, 120, 122, 123, 125, 127, 128, 130, 131, 132, 134, 135, 219, 220, 259, 261, 262, 289, 303, 305], "excluded_lines": [40, 47, 78, 139, 154, 192, 233, 265, 319, 324, 329, 334, 339, 344, 349, 354, 359, 363, 389, 393, 397, 401, 405], "executed_branches": [[237, 238], [237, 242], [254, 255], [268, 269], [268, 272], [275, 276], [275, 281], [282, 284], [282, 292], [294, 295]], "missing_branches": [[56, 57], [56, 60], [63, 64], [63, 68], [95, 96], [95, 99], [110, 112], [110, 118], [112, 113], [112, 114], [114, 115], [114, 117], [118, 119], [118, 120], [120, 122], [120, 127], [122, 123], [122, 125], [130, 131], [130, 134], [254, 259], [294, 303]], "functions": {"get_bundled_fonts_dir": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 11, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 11, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 4, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 4, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [56, 57, 60, 61, 63, 64, 65, 66, 68, 69, 70], "excluded_lines": [47], "start_line": 46, "executed_branches": [], "missing_branches": [[56, 57], [56, 60], [63, 64], [63, 68]]}, "get_bundled_font_path": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 25, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 25, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 16, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 16, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [94, 95, 96, 99, 105, 108, 110, 112, 113, 114, 115, 117, 118, 119, 120, 122, 123, 125, 127, 128, 130, 131, 132, 134, 135], "excluded_lines": [78], "start_line": 73, "executed_branches": [], "missing_branches": [[95, 96], [95, 99], [110, 112], [110, 118], [112, 113], [112, 114], [114, 115], [114, 117], [118, 119], [118, 120], [120, 122], [120, 127], [122, 123], [122, 125], [130, 131], [130, 134]]}, "Font.__init__": {"executed_lines": [169, 170, 171, 172, 173, 174, 175, 176, 177, 179], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [154], "start_line": 144, "executed_branches": [], "missing_branches": []}, "Font.from_family": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 2, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 2, "excluded_lines": 1, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [219, 220], "excluded_lines": [192], "start_line": 182, "executed_branches": [], "missing_branches": []}, "Font._get_bundled_font_path": {"executed_lines": [237, 238, 242, 244, 245, 247, 248, 249, 250, 254, 255, 256, 257], "summary": {"covered_lines": 13, "num_statements": 16, "percent_covered": 80.0, "percent_covered_display": "80", "missing_lines": 3, "excluded_lines": 1, "percent_statements_covered": 81.25, "percent_statements_covered_display": "81", "num_branches": 4, "num_partial_branches": 1, "covered_branches": 3, "missing_branches": 1, "percent_branches_covered": 75.0, "percent_branches_covered_display": "75"}, "missing_lines": [259, 261, 262], "excluded_lines": [233], "start_line": 232, "executed_branches": [[237, 238], [237, 242], [254, 255]], "missing_branches": [[254, 259]]}, "Font._load_font": {"executed_lines": [267, 268, 269, 272, 275, 276, 277, 278, 281, 282, 284, 285, 292, 294, 295, 296, 297, 308, 309, 311, 313, 314], "summary": {"covered_lines": 22, "num_statements": 25, "percent_covered": 87.87878787878788, "percent_covered_display": "88", "missing_lines": 3, "excluded_lines": 1, "percent_statements_covered": 88.0, "percent_statements_covered_display": "88", "num_branches": 8, "num_partial_branches": 1, "covered_branches": 7, "missing_branches": 1, "percent_branches_covered": 87.5, "percent_branches_covered_display": "88"}, "missing_lines": [289, 303, 305], "excluded_lines": [265], "start_line": 264, "executed_branches": [[268, 269], [268, 272], [275, 276], [275, 281], [282, 284], [282, 292], [294, 295]], "missing_branches": [[294, 303]]}, "Font.font": {"executed_lines": [320], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [319], "start_line": 318, "executed_branches": [], "missing_branches": []}, "Font.font_size": {"executed_lines": [325], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [324], "start_line": 323, "executed_branches": [], "missing_branches": []}, "Font.colour": {"executed_lines": [330], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [329], "start_line": 328, "executed_branches": [], "missing_branches": []}, "Font.color": {"executed_lines": [335], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [334], "start_line": 333, "executed_branches": [], "missing_branches": []}, "Font.background": {"executed_lines": [340], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [339], "start_line": 338, "executed_branches": [], "missing_branches": []}, "Font.weight": {"executed_lines": [345], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [344], "start_line": 343, "executed_branches": [], "missing_branches": []}, "Font.style": {"executed_lines": [350], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [349], "start_line": 348, "executed_branches": [], "missing_branches": []}, "Font.decoration": {"executed_lines": [355], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [354], "start_line": 353, "executed_branches": [], "missing_branches": []}, "Font.min_hyphenation_width": {"executed_lines": [360], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [359], "start_line": 358, "executed_branches": [], "missing_branches": []}, "Font._with_modified": {"executed_lines": [374, 385, 386], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [363], "start_line": 362, "executed_branches": [], "missing_branches": []}, "Font.with_size": {"executed_lines": [390], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [389], "start_line": 388, "executed_branches": [], "missing_branches": []}, "Font.with_colour": {"executed_lines": [394], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [393], "start_line": 392, "executed_branches": [], "missing_branches": []}, "Font.with_weight": {"executed_lines": [398], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [397], "start_line": 396, "executed_branches": [], "missing_branches": []}, "Font.with_style": {"executed_lines": [402], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [401], "start_line": 400, "executed_branches": [], "missing_branches": []}, "Font.with_decoration": {"executed_lines": [406], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [405], "start_line": 404, "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [3, 4, 5, 6, 7, 10, 14, 17, 20, 23, 24, 25, 28, 29, 30, 33, 34, 35, 36, 39, 41, 42, 43, 46, 73, 138, 144, 181, 182, 232, 264, 317, 318, 322, 323, 327, 328, 332, 333, 337, 338, 342, 343, 347, 348, 352, 353, 357, 358, 362, 388, 392, 396, 400, 404], "summary": {"covered_lines": 55, "num_statements": 55, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 2, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [40, 139], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"FontWeight": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 23, "executed_branches": [], "missing_branches": []}, "FontStyle": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 28, "executed_branches": [], "missing_branches": []}, "TextDecoration": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 33, "executed_branches": [], "missing_branches": []}, "BundledFont": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 39, "executed_branches": [], "missing_branches": []}, "Font": {"executed_lines": [169, 170, 171, 172, 173, 174, 175, 176, 177, 179, 237, 238, 242, 244, 245, 247, 248, 249, 250, 254, 255, 256, 257, 267, 268, 269, 272, 275, 276, 277, 278, 281, 282, 284, 285, 292, 294, 295, 296, 297, 308, 309, 311, 313, 314, 320, 325, 330, 335, 340, 345, 350, 355, 360, 374, 385, 386, 390, 394, 398, 402, 406], "summary": {"covered_lines": 62, "num_statements": 70, "percent_covered": 87.8048780487805, "percent_covered_display": "88", "missing_lines": 8, "excluded_lines": 19, "percent_statements_covered": 88.57142857142857, "percent_statements_covered_display": "89", "num_branches": 12, "num_partial_branches": 2, "covered_branches": 10, "missing_branches": 2, "percent_branches_covered": 83.33333333333333, "percent_branches_covered_display": "83"}, "missing_lines": [219, 220, 259, 261, 262, 289, 303, 305], "excluded_lines": [154, 192, 233, 265, 319, 324, 329, 334, 339, 344, 349, 354, 359, 363, 389, 393, 397, 401, 405], "start_line": 138, "executed_branches": [[237, 238], [237, 242], [254, 255], [268, 269], [268, 272], [275, 276], [275, 281], [282, 284], [282, 292], [294, 295]], "missing_branches": [[254, 259], [294, 303]]}, "": {"executed_lines": [3, 4, 5, 6, 7, 10, 14, 17, 20, 23, 24, 25, 28, 29, 30, 33, 34, 35, 36, 39, 41, 42, 43, 46, 73, 138, 144, 181, 182, 232, 264, 317, 318, 322, 323, 327, 328, 332, 333, 337, 338, 342, 343, 347, 348, 352, 353, 357, 358, 362, 388, 392, 396, 400, 404], "summary": {"covered_lines": 55, "num_statements": 91, "percent_covered": 49.549549549549546, "percent_covered_display": "50", "missing_lines": 36, "excluded_lines": 4, "percent_statements_covered": 60.43956043956044, "percent_statements_covered_display": "60", "num_branches": 20, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 20, "percent_branches_covered": 0.0, "percent_branches_covered_display": "0"}, "missing_lines": [56, 57, 60, 61, 63, 64, 65, 66, 68, 69, 70, 94, 95, 96, 99, 105, 108, 110, 112, 113, 114, 115, 117, 118, 119, 120, 122, 123, 125, 127, 128, 130, 131, 132, 134, 135], "excluded_lines": [40, 47, 78, 139], "start_line": 1, "executed_branches": [], "missing_branches": [[56, 57], [56, 60], [63, 64], [63, 68], [95, 96], [95, 99], [110, 112], [110, 118], [112, 113], [112, 114], [114, 115], [114, 117], [118, 119], [118, 120], [120, 122], [120, 127], [122, 123], [122, 125], [130, 131], [130, 134]]}}}, "pyWebLayout/style/page_style.py": {"executed_lines": [1, 2, 4, 7, 8, 15, 18, 19, 22, 23, 24, 27, 30, 33, 35, 36, 37, 39, 40, 41, 43, 44, 45, 47, 48, 49, 51, 52, 54, 56, 57, 59, 61, 62, 64], "summary": {"covered_lines": 35, "num_statements": 35, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 4, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [9, 53, 58, 63], "executed_branches": [], "missing_branches": [], "functions": {"PageStyle.padding_top": {"executed_lines": [37], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 36, "executed_branches": [], "missing_branches": []}, "PageStyle.padding_right": {"executed_lines": [41], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 40, "executed_branches": [], "missing_branches": []}, "PageStyle.padding_bottom": {"executed_lines": [45], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 44, "executed_branches": [], "missing_branches": []}, "PageStyle.padding_left": {"executed_lines": [49], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 48, "executed_branches": [], "missing_branches": []}, "PageStyle.total_horizontal_padding": {"executed_lines": [54], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [53], "start_line": 52, "executed_branches": [], "missing_branches": []}, "PageStyle.total_vertical_padding": {"executed_lines": [59], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [58], "start_line": 57, "executed_branches": [], "missing_branches": []}, "PageStyle.total_border_width": {"executed_lines": [64], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [63], "start_line": 62, "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [1, 2, 4, 7, 8, 15, 18, 19, 22, 23, 24, 27, 30, 33, 35, 36, 39, 40, 43, 44, 47, 48, 51, 52, 56, 57, 61, 62], "summary": {"covered_lines": 28, "num_statements": 28, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [9], "start_line": 1, "executed_branches": [], "missing_branches": []}}, "classes": {"PageStyle": {"executed_lines": [37, 41, 45, 49, 54, 59, 64], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 3, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [53, 58, 63], "start_line": 8, "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [1, 2, 4, 7, 8, 15, 18, 19, 22, 23, 24, 27, 30, 33, 35, 36, 39, 40, 43, 44, 47, 48, 51, 52, 56, 57, 61, 62], "summary": {"covered_lines": 28, "num_statements": 28, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100", "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0, "percent_branches_covered": 100.0, "percent_branches_covered_display": "100"}, "missing_lines": [], "excluded_lines": [9], "start_line": 1, "executed_branches": [], "missing_branches": []}}}}, "totals": {"covered_lines": 4678, "num_statements": 5525, "percent_covered": 81.54620438976653, "percent_covered_display": "82", "missing_lines": 847, "excluded_lines": 802, "percent_statements_covered": 84.66968325791855, "percent_statements_covered_display": "85", "num_branches": 1628, "num_partial_branches": 225, "covered_branches": 1155, "missing_branches": 473, "percent_branches_covered": 70.94594594594595, "percent_branches_covered_display": "71"}} \ No newline at end of file diff --git a/cov_info/coverage.svg b/cov_info/coverage.svg new file mode 100644 index 0000000..4b105c6 --- /dev/null +++ b/cov_info/coverage.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + coverage + coverage + 82% + 82% + + diff --git a/cov_info/coverage.xml b/cov_info/coverage.xml new file mode 100644 index 0000000..52d5f4e --- /dev/null +++ b/cov_info/coverage.xml @@ -0,0 +1,5760 @@ + + + + + + /workspace/dtourolle/pyWebLayout/pyWebLayout + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cov_info/htmlcov/.gitignore b/cov_info/htmlcov/.gitignore new file mode 100644 index 0000000..ccccf14 --- /dev/null +++ b/cov_info/htmlcov/.gitignore @@ -0,0 +1,2 @@ +# Created by coverage.py +* diff --git a/cov_info/htmlcov/class_index.html b/cov_info/htmlcov/class_index.html new file mode 100644 index 0000000..6a46140 --- /dev/null +++ b/cov_info/htmlcov/class_index.html @@ -0,0 +1,2218 @@ + + + + + Coverage report + + + + + +
+
+

Coverage report: + 82% +

+ +
+ +
+ + +
+
+

+ Files + Functions + Classes +

+

+ coverage.py v7.15.4, + created at 2026-08-08 20:34 +0000 +

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
   Statements Branches Total
Fileclass coveragestatementsmissingexcluded coveragebranchespartial coverage
pyWebLayout/__init__.py(no class) 100%101 100%00 100%
pyWebLayout/abstract/__init__.py(no class) 100%501 100%00 100%
pyWebLayout/abstract/block.pyBlockType 100%000 100%00 100%
pyWebLayout/abstract/block.pyBlock 100%302 100%00 100%
pyWebLayout/abstract/block.pyParagraph 47%191010 50%40 48%
pyWebLayout/abstract/block.pyHeadingLevel 100%000 100%00 100%
pyWebLayout/abstract/block.pyHeading 50%1054 100%00 50%
pyWebLayout/abstract/block.pyQuote 33%964 100%00 33%
pyWebLayout/abstract/block.pyCodeBlock 64%1457 50%40 61%
pyWebLayout/abstract/block.pyListStyle 100%000 100%00 100%
pyWebLayout/abstract/block.pyHList 63%19710 100%20 67%
pyWebLayout/abstract/block.pyListItem 50%1266 100%00 50%
pyWebLayout/abstract/block.pyTableCell 50%18910 100%00 50%
pyWebLayout/abstract/block.pyTableRow 56%1678 100%20 61%
pyWebLayout/abstract/block.pyTable 80%35713 100%160 86%
pyWebLayout/abstract/block.pyImage 82%921717 77%305 80%
pyWebLayout/abstract/block.pyLinkedImage 79%1947 75%41 78%
pyWebLayout/abstract/block.pyHorizontalRule 17%652 0%20 12%
pyWebLayout/abstract/block.pyPageBreak 17%652 0%20 12%
pyWebLayout/abstract/block.py(no class) 100%211017 100%00 100%
pyWebLayout/abstract/document.pyMetadataType 100%000 100%00 100%
pyWebLayout/abstract/document.pyDocument 69%752326 54%243 66%
pyWebLayout/abstract/document.pyChapter 48%231210 0%40 41%
pyWebLayout/abstract/document.pyBook 100%1807 88%81 96%
pyWebLayout/abstract/document.py(no class) 100%7804 100%00 100%
pyWebLayout/abstract/functional.pyLinkType 100%000 100%00 100%
pyWebLayout/abstract/functional.pyLink 93%1417 100%20 94%
pyWebLayout/abstract/functional.pyButton 93%1418 100%20 94%
pyWebLayout/abstract/functional.pyForm 94%1618 100%20 94%
pyWebLayout/abstract/functional.pyFormFieldType 100%000 100%00 100%
pyWebLayout/abstract/functional.pyFormField 100%16010 100%00 100%
pyWebLayout/abstract/functional.py(no class) 100%8406 100%00 100%
pyWebLayout/abstract/inline.pyWord 100%49010 100%240 100%
pyWebLayout/abstract/inline.pyFormattedSpan 100%2306 100%100 100%
pyWebLayout/abstract/inline.pyLinkedWord 94%1818 75%41 91%
pyWebLayout/abstract/inline.pyLineBreak 100%1303 100%60 100%
pyWebLayout/abstract/inline.py(no class) 100%6105 100%00 100%
pyWebLayout/abstract/interactive_image.pyInteractiveImage 83%2345 58%123 74%
pyWebLayout/abstract/interactive_image.py(no class) 100%1102 100%00 100%
pyWebLayout/concrete/__init__.py(no class) 100%701 100%00 100%
pyWebLayout/concrete/box.pyBox 100%1000 100%20 100%
pyWebLayout/concrete/box.py(no class) 100%901 100%00 100%
pyWebLayout/concrete/dynamic_page.pySizeConstraints 100%000 100%00 100%
pyWebLayout/concrete/dynamic_page.pyDynamicPage 72%1494214 51%8419 64%
pyWebLayout/concrete/dynamic_page.py(no class) 100%2903 100%00 100%
pyWebLayout/concrete/functional.pyLinkText 92%3736 78%184 87%
pyWebLayout/concrete/functional.pyButtonText 82%551010 62%83 79%
pyWebLayout/concrete/functional.pyFormFieldText 92%4949 100%60 93%
pyWebLayout/concrete/functional.py(no class) 100%4906 100%00 100%
pyWebLayout/concrete/image.pyRenderableImage 93%113810 89%364 92%
pyWebLayout/concrete/image.py(no class) 100%2101 100%00 100%
pyWebLayout/concrete/interaction_handler.pyInteractionHandler 0%36366 0%140 0%
pyWebLayout/concrete/interaction_handler.pyInteractionStateManager 91%4445 69%268 83%
pyWebLayout/concrete/interaction_handler.py(no class) 100%1903 100%00 100%
pyWebLayout/concrete/page.pyPage 94%122730 89%445 93%
pyWebLayout/concrete/page.py(no class) 100%5401 100%00 100%
pyWebLayout/concrete/table.pyTableStyle 100%000 100%00 100%
pyWebLayout/concrete/table.pyTableCellRenderer 67%131434 52%5615 63%
pyWebLayout/concrete/table.pyTableRowRenderer 100%2402 83%61 97%
pyWebLayout/concrete/table.pyTableRenderer 91%111108 80%408 88%
pyWebLayout/concrete/table.py(no class) 100%3705 100%00 100%
pyWebLayout/concrete/text.pyAlignmentHandler 100%001 100%00 100%
pyWebLayout/concrete/text.pyLeftAlignmentHandler 100%801 100%20 100%
pyWebLayout/concrete/text.pyCenterRightAlignmentHandler 100%1601 100%60 100%
pyWebLayout/concrete/text.pyJustifyAlignmentHandler 97%3013 88%81 95%
pyWebLayout/concrete/text.pyText 79%871815 58%246 75%
pyWebLayout/concrete/text.pyLine 97%154413 88%506 95%
pyWebLayout/concrete/text.py(no class) 62%1676412 6%320 53%
pyWebLayout/core/__init__.py(no class) 100%201 100%00 100%
pyWebLayout/core/base.pyRenderable 100%101 100%00 100%
pyWebLayout/core/base.pyInteractable 75%412 50%21 67%
pyWebLayout/core/base.pyLayoutable 100%001 100%00 100%
pyWebLayout/core/base.pyQueriable 100%301 100%00 100%
pyWebLayout/core/base.pyHierarchical 100%402 100%00 100%
pyWebLayout/core/base.pyGeometric 62%835 100%00 62%
pyWebLayout/core/base.pyStyleable 75%412 100%00 75%
pyWebLayout/core/base.pyFontRegistry 100%2001 100%100 100%
pyWebLayout/core/base.pyMetadataContainer 100%402 100%00 100%
pyWebLayout/core/base.pyBlockContainer 30%20144 12%81 25%
pyWebLayout/core/base.pyContainerAware 0%992 0%80 0%
pyWebLayout/core/base.py(no class) 98%57110 50%21 97%
pyWebLayout/core/cache.py_UsageRanked 97%6829 92%262 96%
pyWebLayout/core/cache.pyUsageCache 96%2313 100%80 97%
pyWebLayout/core/cache.pySizedUsageCache 97%3414 90%101 95%
pyWebLayout/core/cache.py(no class) 100%4604 100%00 100%
pyWebLayout/core/callback_registry.pyCallbackRegistry 93%57418 83%183 91%
pyWebLayout/core/callback_registry.py(no class) 100%1803 100%00 100%
pyWebLayout/core/highlight.pyHighlightColor 100%000 100%00 100%
pyWebLayout/core/highlight.pyHighlight 100%403 100%20 100%
pyWebLayout/core/highlight.pyHighlightManager 90%31310 100%80 92%
pyWebLayout/core/highlight.py(no class) 100%5205 100%20 100%
pyWebLayout/core/persistence.py(no class) 89%2734 100%20 90%
pyWebLayout/core/query.pyQueryResult 100%101 100%00 100%
pyWebLayout/core/query.pySelectionRange 100%303 100%00 100%
pyWebLayout/core/query.py(no class) 97%2913 50%21 94%
pyWebLayout/io/__init__.py(no class) 100%001 100%00 100%
pyWebLayout/io/readers/__init__.py(no class) 100%201 100%00 100%
pyWebLayout/io/readers/epub_reader.pyEPUBReader 72%2517114 64%13227 69%
pyWebLayout/io/readers/epub_reader.py(no class) 86%3554 0%20 81%
pyWebLayout/io/readers/html_extraction.pyStyleContext 100%606 100%00 100%
pyWebLayout/io/readers/html_extraction.py(no class) 93%3942832 83%20824 89%
pyWebLayout/layout/__init__.py(no class) 100%001 100%00 100%
pyWebLayout/layout/document_layouter.pyDocumentLayouter 71%34107 58%242 66%
pyWebLayout/layout/document_layouter.py(no class) 83%185319 72%7613 80%
pyWebLayout/layout/ereader_layout.pyRenderingPosition 100%806 100%20 100%
pyWebLayout/layout/ereader_layout.pyChapterInfo 100%400 100%00 100%
pyWebLayout/layout/ereader_layout.pyChapterNavigator 97%3515 92%242 95%
pyWebLayout/layout/ereader_layout.pyFontFamilyOverride 0%772 0%40 0%
pyWebLayout/layout/ereader_layout.pyFontScaler 91%1112 83%61 88%
pyWebLayout/layout/ereader_layout.pyBidirectionalLayouter 93%1751214 86%10413 90%
pyWebLayout/layout/ereader_layout.py(no class) 100%6407 100%00 100%
pyWebLayout/layout/ereader_manager.pyBookmarkManager 81%3169 100%40 83%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager 70%2577653 74%848 71%
pyWebLayout/layout/ereader_manager.py(no class) 100%8204 100%00 100%
pyWebLayout/layout/page_buffer.pyPageBuffer 93%4439 86%140 91%
pyWebLayout/layout/page_buffer.pyBufferedPageRenderer 75%40107 57%142 70%
pyWebLayout/layout/page_buffer.py(no class) 100%2603 100%00 100%
pyWebLayout/layout/table_optimizer.py(no class) 91%151149 82%688 88%
pyWebLayout/style/__init__.py(no class) 100%601 100%00 100%
pyWebLayout/style/abstract_style.pyFontFamily 100%000 100%00 100%
pyWebLayout/style/abstract_style.pyFontSize 0%11111 0%40 0%
pyWebLayout/style/abstract_style.pyAbstractStyle 55%2094 75%41 58%
pyWebLayout/style/abstract_style.pyAbstractStyleRegistry 85%47712 62%166 79%
pyWebLayout/style/abstract_style.py(no class) 100%5705 100%00 100%
pyWebLayout/style/alignment.pyAlignment 0%111 100%00 0%
pyWebLayout/style/alignment.py(no class) 100%1002 100%00 100%
pyWebLayout/style/concrete_style.pyRenderingContext 100%000 100%00 100%
pyWebLayout/style/concrete_style.pyConcreteStyle 100%101 100%00 100%
pyWebLayout/style/concrete_style.pyStyleResolver 53%1376412 49%7012 52%
pyWebLayout/style/concrete_style.pyConcreteStyleRegistry 83%1225 100%20 86%
pyWebLayout/style/concrete_style.py(no class) 100%5705 100%00 100%
pyWebLayout/style/fonts.pyFontWeight 100%000 100%00 100%
pyWebLayout/style/fonts.pyFontStyle 100%000 100%00 100%
pyWebLayout/style/fonts.pyTextDecoration 100%000 100%00 100%
pyWebLayout/style/fonts.pyBundledFont 100%000 100%00 100%
pyWebLayout/style/fonts.pyFont 89%70819 83%122 88%
pyWebLayout/style/fonts.py(no class) 60%91364 0%200 50%
pyWebLayout/style/page_style.pyPageStyle 100%703 100%00 100%
pyWebLayout/style/page_style.py(no class) 100%2801 100%00 100%
Total  85%5525847802 71%1628225 82%
+

+ No items found using the specified filter. +

+
+ + + diff --git a/cov_info/htmlcov/coverage_html_cb_dd2e7eb5.js b/cov_info/htmlcov/coverage_html_cb_dd2e7eb5.js new file mode 100644 index 0000000..6f87174 --- /dev/null +++ b/cov_info/htmlcov/coverage_html_cb_dd2e7eb5.js @@ -0,0 +1,735 @@ +// Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +// For details: https://github.com/coveragepy/coveragepy/blob/main/NOTICE.txt + +// Coverage.py HTML report browser code. +/*jslint browser: true, sloppy: true, vars: true, plusplus: true, maxerr: 50, indent: 4 */ +/*global coverage: true, document, window, $ */ + +coverage = {}; + +// General helpers +function debounce(callback, wait) { + let timeoutId = null; + return function(...args) { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + callback.apply(this, args); + }, wait); + }; +}; + +function checkVisible(element) { + const rect = element.getBoundingClientRect(); + const viewBottom = Math.max(document.documentElement.clientHeight, window.innerHeight); + const viewTop = 30; + return !(rect.bottom < viewTop || rect.top >= viewBottom); +} + +function on_click(sel, fn) { + const elt = document.querySelector(sel); + if (elt) { + elt.addEventListener("click", fn); + } +} + +// Helpers for table sorting +function getCellValue(row, column = 0) { + const cell = row.cells[column] // nosemgrep: eslint.detect-object-injection + if (cell.childElementCount == 1) { + var child = cell.firstElementChild; + if (child.tagName === "A") { + child = child.firstElementChild; + } + if (child instanceof HTMLDataElement && child.value) { + return child.value; + } + } + return cell.innerText || cell.textContent; +} + +function rowComparator(rowA, rowB, column = 0) { + let valueA = getCellValue(rowA, column); + let valueB = getCellValue(rowB, column); + if (!isNaN(valueA) && !isNaN(valueB)) { + return valueA - valueB; + } + return valueA.localeCompare(valueB, undefined, {numeric: true}); +} + +function sortColumn(th) { + // Get the current sorting direction of the selected header, + // clear state on other headers and then set the new sorting direction. + const currentSortOrder = th.getAttribute("aria-sort"); + [...th.parentElement.cells].forEach(header => header.setAttribute("aria-sort", "none")); + var direction; + if (currentSortOrder === "none") { + direction = th.dataset.defaultSortOrder || "ascending"; + } + else if (currentSortOrder === "ascending") { + direction = "descending"; + } + else { + direction = "ascending"; + } + th.setAttribute("aria-sort", direction); + + const column = [...th.parentElement.cells].indexOf(th) + + // Sort all rows and afterwards append them in order to move them in the DOM. + Array.from(th.closest("table").querySelectorAll("tbody tr")) + .sort((rowA, rowB) => rowComparator(rowA, rowB, column) * (direction === "ascending" ? 1 : -1)) + .forEach(tr => tr.parentElement.appendChild(tr)); + + // Save the sort order for next time. + if (th.id !== "region") { + let th_id = "file"; // Sort by file if we don't have a column id + let current_direction = direction; + const stored_list = localStorage.getItem(coverage.INDEX_SORT_STORAGE); + if (stored_list) { + ({th_id, direction} = JSON.parse(stored_list)) + } + localStorage.setItem(coverage.INDEX_SORT_STORAGE, JSON.stringify({ + "th_id": th.id, + "direction": current_direction + })); + if (th.id !== th_id || document.getElementById("region")) { + // Sort column has changed, unset sorting by function or class. + localStorage.setItem(coverage.SORTED_BY_REGION, JSON.stringify({ + "by_region": false, + "region_direction": current_direction + })); + } + } + else { + // Sort column has changed to by function or class, remember that. + localStorage.setItem(coverage.SORTED_BY_REGION, JSON.stringify({ + "by_region": true, + "region_direction": direction + })); + } +} + +// Find all the elements with data-shortcut attribute, and use them to assign a shortcut key. +coverage.assign_shortkeys = function () { + document.querySelectorAll("[data-shortcut]").forEach(element => { + document.addEventListener("keypress", event => { + if (event.target.tagName.toLowerCase() === "input") { + return; // ignore keypress from search filter + } + if (event.key === element.dataset.shortcut) { + element.click(); + } + }); + }); +}; + +// Create the events for the filter box. +coverage.wire_up_filter = function () { + // Populate the filter and hide100 inputs if there are saved values for them. + const saved_filter_value = localStorage.getItem(coverage.FILTER_STORAGE); + if (saved_filter_value) { + document.getElementById("filter").value = saved_filter_value; + } + const saved_hide100_value = localStorage.getItem(coverage.HIDE100_STORAGE); + if (saved_hide100_value) { + document.getElementById("hide100").checked = JSON.parse(saved_hide100_value); + } + + // Cache elements. + const table = document.querySelector("table.index"); + const table_body_rows = table.querySelectorAll("tbody tr"); + const no_rows = document.getElementById("no_rows"); + + const footer = table.tFoot.rows[0]; + const ratio_columns = Array.from(footer.cells).map(cell => Boolean(cell.dataset.ratio)); + + // Observe filter keyevents. + const filter_handler = (event => { + // Keep running total of each metric, first index contains number of shown rows + const totals = ratio_columns.map( + is_ratio => is_ratio ? {"numer": 0, "denom": 0} : 0 + ); + + var text = document.getElementById("filter").value; + // Store filter value + localStorage.setItem(coverage.FILTER_STORAGE, text); + const casefold = (text === text.toLowerCase()); + const hide100 = document.getElementById("hide100").checked; + // Store hide value. + localStorage.setItem(coverage.HIDE100_STORAGE, JSON.stringify(hide100)); + + // Hide / show elements. + table_body_rows.forEach(row => { + var show = false; + // Check the text filter. + for (let column = 0; column < totals.length; column++) { + cell = row.cells[column]; + if (cell.classList.contains("name")) { + var celltext = cell.textContent; + if (casefold) { + celltext = celltext.toLowerCase(); + } + if (celltext.includes(text)) { + show = true; + } + } + } + + // Check the "hide covered" filter. + if (show && hide100) { + const [numer, denom] = row.cells[row.cells.length - 1].dataset.ratio.split(" "); + show = (numer !== denom); + } + + if (!show) { + // hide + row.classList.add("hidden"); + return; + } + + // show + row.classList.remove("hidden"); + totals[0]++; + + for (let column = 0; column < totals.length; column++) { + // Accumulate dynamic totals + cell = row.cells[column] // nosemgrep: eslint.detect-object-injection + if (cell.matches(".name, .spacer")) { + continue; + } + if (ratio_columns[column] && cell.dataset.ratio) { + // Column stores a ratio + const [numer, denom] = cell.dataset.ratio.split(" "); + totals[column]["numer"] += parseInt(numer, 10); // nosemgrep: eslint.detect-object-injection + totals[column]["denom"] += parseInt(denom, 10); // nosemgrep: eslint.detect-object-injection + } + else { + totals[column] += parseInt(cell.textContent, 10); // nosemgrep: eslint.detect-object-injection + } + } + }); + + // Show placeholder if no rows will be displayed. + if (!totals[0]) { + // Show placeholder, hide table. + no_rows.style.display = "block"; + table.style.display = "none"; + return; + } + + // Hide placeholder, show table. + no_rows.style.display = null; + table.style.display = null; + + // Calculate new dynamic sum values based on visible rows. + for (let column = 0; column < totals.length; column++) { + // Get footer cell element. + const cell = footer.cells[column]; // nosemgrep: eslint.detect-object-injection + if (cell.matches(".name, .spacer")) { + continue; + } + + // Set value into dynamic footer cell element. + if (ratio_columns[column]) { + // Percentage column uses the numerator and denominator, + // and adapts to the number of decimal places. + const match = /\.([0-9]+)/.exec(cell.textContent); + const places = match ? match[1].length : 0; + const { numer, denom } = totals[column]; // nosemgrep: eslint.detect-object-injection + cell.dataset.ratio = `${numer} ${denom}`; + // Check denom to prevent NaN if filtered files contain no statements + cell.textContent = denom + ? `${(numer * 100 / denom).toFixed(places)}%` + : `${(100).toFixed(places)}%`; + } + else { + cell.textContent = totals[column]; // nosemgrep: eslint.detect-object-injection + } + } + }); + + document.getElementById("filter").addEventListener("input", debounce(filter_handler)); + document.getElementById("hide100").addEventListener("input", debounce(filter_handler)); + + // Trigger change event on setup, to force filter on page refresh + // (filter value may still be present). + document.getElementById("filter").dispatchEvent(new Event("input")); + document.getElementById("hide100").dispatchEvent(new Event("input")); +}; +coverage.FILTER_STORAGE = "COVERAGE_FILTER_VALUE"; +coverage.HIDE100_STORAGE = "COVERAGE_HIDE100_VALUE"; + +// Set up the click-to-sort columns. +coverage.wire_up_sorting = function () { + document.querySelectorAll("[data-sortable] th[aria-sort]").forEach( + th => th.addEventListener("click", e => sortColumn(e.target)) + ); + + // Look for a localStorage item containing previous sort settings: + let th_id = "file", direction = "ascending"; + const stored_list = localStorage.getItem(coverage.INDEX_SORT_STORAGE); + if (stored_list) { + ({th_id, direction} = JSON.parse(stored_list)); + } + let by_region = false, region_direction = "ascending"; + const sorted_by_region = localStorage.getItem(coverage.SORTED_BY_REGION); + if (sorted_by_region) { + ({ + by_region, + region_direction + } = JSON.parse(sorted_by_region)); + } + + const region_id = "region"; + if (by_region && document.getElementById(region_id)) { + direction = region_direction; + } + // If we are in a page that has a column with id of "region", sort on + // it if the last sort was by function or class. + let th; + if (document.getElementById(region_id)) { + th = document.getElementById(by_region ? region_id : th_id); + } + else { + th = document.getElementById(th_id); + } + th.setAttribute("aria-sort", direction === "ascending" ? "descending" : "ascending"); + th.click() +}; + +coverage.INDEX_SORT_STORAGE = "COVERAGE_INDEX_SORT_2"; +coverage.SORTED_BY_REGION = "COVERAGE_SORT_REGION"; + +// Loaded on index.html +coverage.index_ready = function () { + coverage.assign_shortkeys(); + coverage.wire_up_filter(); + coverage.wire_up_sorting(); + + on_click(".button_prev_file", coverage.to_prev_file); + on_click(".button_next_file", coverage.to_next_file); + + on_click(".button_show_hide_help", coverage.show_hide_help); +}; + +// -- pyfile stuff -- + +coverage.LINE_FILTERS_STORAGE = "COVERAGE_LINE_FILTERS"; + +coverage.pyfile_ready = function () { + // If we're directed to a particular line number, highlight the line. + var frag = location.hash; + if (frag.length > 2 && frag[1] === "t") { + document.querySelector(frag).closest(".n").classList.add("highlight"); + coverage.set_sel(parseInt(frag.substr(2), 10)); + } + else { + coverage.set_sel(0); + } + + on_click(".button_toggle_run", coverage.toggle_lines); + on_click(".button_toggle_mis", coverage.toggle_lines); + on_click(".button_toggle_exc", coverage.toggle_lines); + on_click(".button_toggle_par", coverage.toggle_lines); + + on_click(".button_next_chunk", coverage.to_next_chunk_nicely); + on_click(".button_prev_chunk", coverage.to_prev_chunk_nicely); + on_click(".button_top_of_page", coverage.to_top); + on_click(".button_first_chunk", coverage.to_first_chunk); + + on_click(".button_prev_file", coverage.to_prev_file); + on_click(".button_next_file", coverage.to_next_file); + on_click(".button_to_index", coverage.to_index); + + on_click(".button_show_hide_help", coverage.show_hide_help); + + coverage.filters = undefined; + try { + coverage.filters = localStorage.getItem(coverage.LINE_FILTERS_STORAGE); + } catch(err) {} + + if (coverage.filters) { + coverage.filters = JSON.parse(coverage.filters); + } + else { + coverage.filters = {run: false, exc: true, mis: true, par: true}; + } + + for (cls in coverage.filters) { + coverage.set_line_visibilty(cls, coverage.filters[cls]); // nosemgrep: eslint.detect-object-injection + } + + coverage.assign_shortkeys(); + coverage.init_scroll_markers(); + coverage.wire_up_sticky_header(); + + document.querySelectorAll("[id^=ctxs]").forEach( + cbox => cbox.addEventListener("click", coverage.expand_contexts) + ); + + // Rebuild scroll markers when the window height changes. + window.addEventListener("resize", coverage.build_scroll_markers); +}; + +coverage.toggle_lines = function (event) { + const btn = event.target.closest("button"); + const category = btn.value + const show = !btn.classList.contains("show_" + category); + coverage.set_line_visibilty(category, show); + coverage.build_scroll_markers(); + coverage.filters[category] = show; + try { + localStorage.setItem(coverage.LINE_FILTERS_STORAGE, JSON.stringify(coverage.filters)); + } catch(err) {} +}; + +coverage.set_line_visibilty = function (category, should_show) { + const cls = "show_" + category; + const btn = document.querySelector(".button_toggle_" + category); + if (btn) { + if (should_show) { + document.querySelectorAll("#source ." + category).forEach(e => e.classList.add(cls)); + btn.classList.add(cls); + } + else { + document.querySelectorAll("#source ." + category).forEach(e => e.classList.remove(cls)); + btn.classList.remove(cls); + } + } +}; + +// Return the nth line div. +coverage.line_elt = function (n) { + return document.getElementById("t" + n)?.closest("p"); +}; + +// Set the selection. b and e are line numbers. +coverage.set_sel = function (b, e) { + // The first line selected. + coverage.sel_begin = b; + // The next line not selected. + coverage.sel_end = (e === undefined) ? b+1 : e; +}; + +coverage.to_top = function () { + coverage.set_sel(0, 1); + coverage.scroll_window(0); +}; + +coverage.to_first_chunk = function () { + coverage.set_sel(0, 1); + coverage.to_next_chunk(); +}; + +coverage.to_prev_file = function () { + window.location = document.getElementById("prevFileLink").href; +} + +coverage.to_next_file = function () { + window.location = document.getElementById("nextFileLink").href; +} + +coverage.to_index = function () { + location.href = document.getElementById("indexLink").href; +} + +coverage.show_hide_help = function () { + const helpCheck = document.getElementById("help_panel_state") + helpCheck.checked = !helpCheck.checked; +} + +// Return a string indicating what kind of chunk this line belongs to, +// or null if not a chunk. +coverage.chunk_indicator = function (line_elt) { + const classes = line_elt?.className; + if (!classes) { + return null; + } + const match = classes.match(/\bshow_\w+\b/); + if (!match) { + return null; + } + return match[0]; +}; + +coverage.to_next_chunk = function () { + const c = coverage; + + // Find the start of the next colored chunk. + var probe = c.sel_end; + var chunk_indicator, probe_line; + while (true) { + probe_line = c.line_elt(probe); + if (!probe_line) { + return; + } + chunk_indicator = c.chunk_indicator(probe_line); + if (chunk_indicator) { + break; + } + probe++; + } + + // There's a next chunk, `probe` points to it. + var begin = probe; + + // Find the end of this chunk. + var next_indicator = chunk_indicator; + while (next_indicator === chunk_indicator) { + probe++; + probe_line = c.line_elt(probe); + next_indicator = c.chunk_indicator(probe_line); + } + c.set_sel(begin, probe); + c.show_selection(); +}; + +coverage.to_prev_chunk = function () { + const c = coverage; + + // Find the end of the prev colored chunk. + var probe = c.sel_begin-1; + var probe_line = c.line_elt(probe); + if (!probe_line) { + return; + } + var chunk_indicator = c.chunk_indicator(probe_line); + while (probe > 1 && !chunk_indicator) { + probe--; + probe_line = c.line_elt(probe); + if (!probe_line) { + return; + } + chunk_indicator = c.chunk_indicator(probe_line); + } + + // There's a prev chunk, `probe` points to its last line. + var end = probe+1; + + // Find the beginning of this chunk. + var prev_indicator = chunk_indicator; + while (prev_indicator === chunk_indicator) { + probe--; + if (probe <= 0) { + return; + } + probe_line = c.line_elt(probe); + prev_indicator = c.chunk_indicator(probe_line); + } + c.set_sel(probe+1, end); + c.show_selection(); +}; + +// Returns 0, 1, or 2: how many of the two ends of the selection are on +// the screen right now? +coverage.selection_ends_on_screen = function () { + if (coverage.sel_begin === 0) { + return 0; + } + + const begin = coverage.line_elt(coverage.sel_begin); + const end = coverage.line_elt(coverage.sel_end-1); + + return ( + (checkVisible(begin) ? 1 : 0) + + (checkVisible(end) ? 1 : 0) + ); +}; + +coverage.to_next_chunk_nicely = function () { + if (coverage.selection_ends_on_screen() === 0) { + // The selection is entirely off the screen: + // Set the top line on the screen as selection. + + // This will select the top-left of the viewport + // As this is most likely the span with the line number we take the parent + const line = document.elementFromPoint(0, 0).parentElement; + if (line.parentElement !== document.getElementById("source")) { + // The element is not a source line but the header or similar + coverage.select_line_or_chunk(1); + } + else { + // We extract the line number from the id + coverage.select_line_or_chunk(parseInt(line.id.substring(1), 10)); + } + } + coverage.to_next_chunk(); +}; + +coverage.to_prev_chunk_nicely = function () { + if (coverage.selection_ends_on_screen() === 0) { + // The selection is entirely off the screen: + // Set the lowest line on the screen as selection. + + // This will select the bottom-left of the viewport + // As this is most likely the span with the line number we take the parent + const line = document.elementFromPoint(document.documentElement.clientHeight-1, 0).parentElement; + if (line.parentElement !== document.getElementById("source")) { + // The element is not a source line but the header or similar + coverage.select_line_or_chunk(coverage.lines_len); + } + else { + // We extract the line number from the id + coverage.select_line_or_chunk(parseInt(line.id.substring(1), 10)); + } + } + coverage.to_prev_chunk(); +}; + +// Select line number lineno, or if it is in a colored chunk, select the +// entire chunk +coverage.select_line_or_chunk = function (lineno) { + var c = coverage; + var probe_line = c.line_elt(lineno); + if (!probe_line) { + return; + } + var the_indicator = c.chunk_indicator(probe_line); + if (the_indicator) { + // The line is in a highlighted chunk. + // Search backward for the first line. + var probe = lineno; + var indicator = the_indicator; + while (probe > 0 && indicator === the_indicator) { + probe--; + probe_line = c.line_elt(probe); + if (!probe_line) { + break; + } + indicator = c.chunk_indicator(probe_line); + } + var begin = probe + 1; + + // Search forward for the last line. + probe = lineno; + indicator = the_indicator; + while (indicator === the_indicator) { + probe++; + probe_line = c.line_elt(probe); + indicator = c.chunk_indicator(probe_line); + } + + coverage.set_sel(begin, probe); + } + else { + coverage.set_sel(lineno); + } +}; + +coverage.show_selection = function () { + // Highlight the lines in the chunk + document.querySelectorAll("#source .highlight").forEach(e => e.classList.remove("highlight")); + for (let probe = coverage.sel_begin; probe < coverage.sel_end; probe++) { + coverage.line_elt(probe).querySelector(".n").classList.add("highlight"); + } + + coverage.scroll_to_selection(); +}; + +coverage.scroll_to_selection = function () { + // Scroll the page if the chunk isn't fully visible. + if (coverage.selection_ends_on_screen() < 2) { + const element = coverage.line_elt(coverage.sel_begin); + coverage.scroll_window(element.offsetTop - 60); + } +}; + +coverage.scroll_window = function (to_pos) { + window.scroll({top: to_pos, behavior: "smooth"}); +}; + +coverage.init_scroll_markers = function () { + // Init some variables + coverage.lines_len = document.querySelectorAll("#source > p").length; + + // Build html + coverage.build_scroll_markers(); +}; + +coverage.build_scroll_markers = function () { + const temp_scroll_marker = document.getElementById("scroll_marker") + if (temp_scroll_marker) temp_scroll_marker.remove(); + // Don't build markers if the window has no scroll bar. + if (document.body.scrollHeight <= window.innerHeight) { + return; + } + + const marker_scale = window.innerHeight / document.body.scrollHeight; + const line_height = Math.min(Math.max(3, window.innerHeight / coverage.lines_len), 10); + + let previous_line = -99, last_mark, last_top; + + const scroll_marker = document.createElement("div"); + scroll_marker.id = "scroll_marker"; + document.getElementById("source").querySelectorAll( + "p.show_run, p.show_mis, p.show_exc, p.show_exc, p.show_par" + ).forEach(element => { + const line_top = Math.floor(element.offsetTop * marker_scale); + const line_number = parseInt(element.querySelector(".n a").id.substr(1)); + + if (line_number === previous_line + 1) { + // If this solid missed block just make previous mark higher. + last_mark.style.height = `${line_top + line_height - last_top}px`; + } + else { + // Add colored line in scroll_marker block. + last_mark = document.createElement("div"); + last_mark.id = `m${line_number}`; + last_mark.classList.add("marker"); + last_mark.style.height = `${line_height}px`; + last_mark.style.top = `${line_top}px`; + scroll_marker.append(last_mark); + last_top = line_top; + } + + previous_line = line_number; + }); + + // Append last to prevent layout calculation + document.body.append(scroll_marker); +}; + +coverage.wire_up_sticky_header = function () { + const header = document.querySelector("header"); + const header_bottom = ( + header.querySelector(".content h2").getBoundingClientRect().top - + header.getBoundingClientRect().top + ); + + function updateHeader() { + if (window.scrollY > header_bottom) { + header.classList.add("sticky"); + } + else { + header.classList.remove("sticky"); + } + } + + window.addEventListener("scroll", updateHeader); + updateHeader(); +}; + +coverage.expand_contexts = function (e) { + var ctxs = e.target.parentNode.querySelector(".ctxs"); + + if (!ctxs.classList.contains("expanded")) { + var ctxs_text = ctxs.textContent; + var width = Number(ctxs_text[0]); + ctxs.textContent = ""; + for (var i = 1; i < ctxs_text.length; i += width) { + key = ctxs_text.substring(i, i + width).trim(); + ctxs.appendChild(document.createTextNode(contexts[key])); + ctxs.appendChild(document.createElement("br")); + } + ctxs.classList.add("expanded"); + } +}; + +document.addEventListener("DOMContentLoaded", () => { + if (document.body.classList.contains("indexfile")) { + coverage.index_ready(); + } + else { + coverage.pyfile_ready(); + } +}); diff --git a/cov_info/htmlcov/favicon_32_cb_c827f16f.png b/cov_info/htmlcov/favicon_32_cb_c827f16f.png new file mode 100644 index 0000000000000000000000000000000000000000..8649f0475d8d20793b2ec431fe25a186a414cf10 GIT binary patch literal 1732 zcmV;#20QtQP)K2KOkBOVxIZChq#W-v7@TU%U6P(wycKT1hUJUToW3ke1U1ONa4 z000000000000000bb)GRa9mqwR9|UWHy;^RUrt?IT__Y0JUcxmBP0(51q1>E00030 z|NrOz)aw7%8sJzM<5^g%z7^qE`}_Ot|JUUG(NUkWzR|7K?Zo%@_v-8G-1N%N=D$;; zw;keH4dGY$`1t4M=HK_s*zm^0#KgqfwWhe3qO_HtvXYvtjgX>;-~C$L`&k>^R)9)7 zdPh2TL^pCnHC#0+_4D)M`p?qp!pq{jO_{8;$fbaflbx`Tn52n|n}8VFRTA1&ugOP< zPd{uvFjz7t*Vot1&d$l-xWCk}s;sQL&#O(Bskh6gqNJv>#iB=ypG1e3K!K4yc7!~M zfj4S*g^zZ7eP$+_Sl07Z646l;%urinP#D8a6TwRtnLIRcI!r4f@bK~9-`~;E(N?Lv zSEst7s;rcxsi~}{Nsytfz@MtUoR*iFc8!#vvx}Umhm4blk(_~MdVD-@dW&>!Nn~ro z_E~-ESVQAj6Wmn;(olz(O&_{U2*pZBc1aYjMh>Dq3z|6`jW`RDHV=t3I6yRKJ~LOX zz_z!!vbVXPqob#=pj3^VMT?x6t(irRmSKsMo1~LLkB&=#j!=M%NP35mfqim$drWb9 zYIb>no_LUwc!r^NkDzs4YHu@=ZHRzrafWDZd1EhEVq=tGX?tK$pIa)DTh#bkvh!J- z?^%@YS!U*0E8$q$_*aOTQ&)Ra64g>ep;BdcQgvlg8qQHrP*E$;P{-m=A*@axn@$bO zO-Y4JzS&EAi%YG}N?cn?YFS7ivPY=EMV6~YH;+Xxu|tefLS|Aza)Cg6us#)=JW!uH zQa?H>d^j+YHCtyjL^LulF*05|F$RG!AX_OHVI&MtA~_@=5_lU|0000rbW%=J06GH4 z^5LD8b8apw8vNh1ua1mF{{Hy)_U`NA;Nacc+sCpuHXa-V{r&yz?c(9#+}oX+NmiRW z+W-IqK1oDDR5;6GfCDCOP5}iL5fK(cB~ET81`MFgF2kGa9AjhSIk~-E-4&*tPPKdiilQJ11k_J082ZS z>@TvivP!5ZFG?t@{t+GpR3XR&@*hA_VE1|Lo8@L@)l*h(Z@=?c-NS$Fk&&61IzUU9 z*nPqBM=OBZ-6ka1SJgGAS-Us5EN)r#dUX%>wQZLa2ytPCtMKp)Ob z*xcu38Z&d5<-NBS)@jRD+*!W*cf-m_wmxDEqBf?czI%3U0J$Xik;lA`jg}VH?(S(V zE!M3;X2B8w0TnnW&6(8;_Uc)WD;Ms6PKP+s(sFgO!}B!^ES~GDt4qLPxwYB)^7)XA zZwo9zDy-B0B+jT6V=!=bo(zs_8{eBA78gT9GH$(DVhz;4VAYwz+bOIdZ-PNb|I&rl z^XG=vFLF)1{&nT2*0vMz#}7^9hXzzf&ZdKlEj{LihP;|;Ywqn35ajP?H?7t|i-Un% z&&kxee@9B{nwgv1+S-~0)E1{ob1^Wn`F2isurqThKK=3%&;`@{0{!D- z&CSj80t;uPu&FaJFtSXKH#ajgGj}=sEad7US6jP0|Db@0j)?(5@sf<7`~a9>s;wCa zm^)spe{uxGFmrJYI9cOh7s$>8Npkt-5EWB1UKc`{W{y5Ce$1+nM9Cr;);=Ju#N^62OSlJMn7omiUgP&ErsYzT~iGxcW aE(`!K@+CXylaC4j0000 + + + + Coverage report + + + + + +
+
+

Coverage report: + 82% +

+ +
+ +
+ + +
+
+

+ Files + Functions + Classes +

+

+ coverage.py v7.15.4, + created at 2026-08-08 20:34 +0000 +

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
   Statements Branches Total
Filefunction coveragestatementsmissingexcluded coveragebranchespartial coverage
pyWebLayout/__init__.py(no function) 100%101 100%00 100%
pyWebLayout/abstract/__init__.py(no function) 100%501 100%00 100%
pyWebLayout/abstract/block.pyBlock.__init__ 100%201 100%00 100%
pyWebLayout/abstract/block.pyBlock.block_type 100%101 100%00 100%
pyWebLayout/abstract/block.pyParagraph.__init__ 100%301 100%00 100%
pyWebLayout/abstract/block.pyParagraph.create_and_add_to 0%551 100%00 0%
pyWebLayout/abstract/block.pyParagraph.add_word 100%101 100%00 100%
pyWebLayout/abstract/block.pyParagraph.create_word 0%111 100%00 0%
pyWebLayout/abstract/block.pyParagraph.add_span 0%111 100%00 0%
pyWebLayout/abstract/block.pyParagraph.create_span 0%111 100%00 0%
pyWebLayout/abstract/block.pyParagraph.words 100%101 100%00 100%
pyWebLayout/abstract/block.pyParagraph.words_iter 100%201 100%20 100%
pyWebLayout/abstract/block.pyParagraph.spans 0%221 0%20 0%
pyWebLayout/abstract/block.pyParagraph.word_count 100%101 100%00 100%
pyWebLayout/abstract/block.pyParagraph.__len__ 100%100 100%00 100%
pyWebLayout/abstract/block.pyHeading.__init__ 100%301 100%00 100%
pyWebLayout/abstract/block.pyHeading.create_and_add_to 0%551 100%00 0%
pyWebLayout/abstract/block.pyHeading.level 100%101 100%00 100%
pyWebLayout/abstract/block.pyHeading.level 100%101 100%00 100%
pyWebLayout/abstract/block.pyQuote.__init__ 100%201 100%00 100%
pyWebLayout/abstract/block.pyQuote.create_and_add_to 0%551 100%00 0%
pyWebLayout/abstract/block.pyQuote.style 100%101 100%00 100%
pyWebLayout/abstract/block.pyQuote.style 0%111 100%00 0%
pyWebLayout/abstract/block.pyCodeBlock.__init__ 100%301 100%00 100%
pyWebLayout/abstract/block.pyCodeBlock.create_and_add_to 0%551 0%20 0%
pyWebLayout/abstract/block.pyCodeBlock.language 100%101 100%00 100%
pyWebLayout/abstract/block.pyCodeBlock.language 100%101 100%00 100%
pyWebLayout/abstract/block.pyCodeBlock.add_line 100%101 100%00 100%
pyWebLayout/abstract/block.pyCodeBlock.lines 100%201 100%20 100%
pyWebLayout/abstract/block.pyCodeBlock.line_count 100%101 100%00 100%
pyWebLayout/abstract/block.pyHList.__init__ 100%401 100%00 100%
pyWebLayout/abstract/block.pyHList.create_and_add_to 0%551 100%00 0%
pyWebLayout/abstract/block.pyHList.style 100%101 100%00 100%
pyWebLayout/abstract/block.pyHList.style 100%101 100%00 100%
pyWebLayout/abstract/block.pyHList.default_style 100%101 100%00 100%
pyWebLayout/abstract/block.pyHList.default_style 0%111 100%00 0%
pyWebLayout/abstract/block.pyHList.add_item 100%201 100%00 100%
pyWebLayout/abstract/block.pyHList.create_item 0%111 100%00 0%
pyWebLayout/abstract/block.pyHList.items 100%201 100%20 100%
pyWebLayout/abstract/block.pyHList.item_count 100%101 100%00 100%
pyWebLayout/abstract/block.pyListItem.__init__ 100%301 100%00 100%
pyWebLayout/abstract/block.pyListItem.create_and_add_to 0%551 100%00 0%
pyWebLayout/abstract/block.pyListItem.term 100%101 100%00 100%
pyWebLayout/abstract/block.pyListItem.term 100%101 100%00 100%
pyWebLayout/abstract/block.pyListItem.style 100%101 100%00 100%
pyWebLayout/abstract/block.pyListItem.style 0%111 100%00 0%
pyWebLayout/abstract/block.pyTableCell.__init__ 100%501 100%00 100%
pyWebLayout/abstract/block.pyTableCell.create_and_add_to 0%551 100%00 0%
pyWebLayout/abstract/block.pyTableCell.is_header 100%101 100%00 100%
pyWebLayout/abstract/block.pyTableCell.is_header 0%111 100%00 0%
pyWebLayout/abstract/block.pyTableCell.colspan 100%101 100%00 100%
pyWebLayout/abstract/block.pyTableCell.colspan 0%111 100%00 0%
pyWebLayout/abstract/block.pyTableCell.rowspan 100%101 100%00 100%
pyWebLayout/abstract/block.pyTableCell.rowspan 0%111 100%00 0%
pyWebLayout/abstract/block.pyTableCell.style 100%101 100%00 100%
pyWebLayout/abstract/block.pyTableCell.style 0%111 100%00 0%
pyWebLayout/abstract/block.pyTableRow.__init__ 100%301 100%00 100%
pyWebLayout/abstract/block.pyTableRow.create_and_add_to 0%551 100%00 0%
pyWebLayout/abstract/block.pyTableRow.style 100%101 100%00 100%
pyWebLayout/abstract/block.pyTableRow.style 0%111 100%00 0%
pyWebLayout/abstract/block.pyTableRow.add_cell 100%201 100%00 100%
pyWebLayout/abstract/block.pyTableRow.create_cell 0%111 100%00 0%
pyWebLayout/abstract/block.pyTableRow.cells 100%201 100%20 100%
pyWebLayout/abstract/block.pyTableRow.cell_count 100%101 100%00 100%
pyWebLayout/abstract/block.pyTable.__init__ 100%601 100%00 100%
pyWebLayout/abstract/block.pyTable.create_and_add_to 0%551 100%00 0%
pyWebLayout/abstract/block.pyTable.caption 100%101 100%00 100%
pyWebLayout/abstract/block.pyTable.caption 100%101 100%00 100%
pyWebLayout/abstract/block.pyTable.style 100%101 100%00 100%
pyWebLayout/abstract/block.pyTable.style 0%111 100%00 0%
pyWebLayout/abstract/block.pyTable.add_row 100%601 100%40 100%
pyWebLayout/abstract/block.pyTable.create_row 0%111 100%00 0%
pyWebLayout/abstract/block.pyTable.header_rows 100%201 100%20 100%
pyWebLayout/abstract/block.pyTable.body_rows 100%201 100%20 100%
pyWebLayout/abstract/block.pyTable.footer_rows 100%201 100%20 100%
pyWebLayout/abstract/block.pyTable.all_rows 100%601 100%60 100%
pyWebLayout/abstract/block.pyTable.row_count 100%101 100%00 100%
pyWebLayout/abstract/block.pyImage.__init__ 100%501 100%00 100%
pyWebLayout/abstract/block.pyImage.create_and_add_to 0%551 0%20 0%
pyWebLayout/abstract/block.pyImage.source 100%101 100%00 100%
pyWebLayout/abstract/block.pyImage.source 100%101 100%00 100%
pyWebLayout/abstract/block.pyImage.alt_text 100%101 100%00 100%
pyWebLayout/abstract/block.pyImage.alt_text 100%101 100%00 100%
pyWebLayout/abstract/block.pyImage.width 100%101 100%00 100%
pyWebLayout/abstract/block.pyImage.width 100%101 100%00 100%
pyWebLayout/abstract/block.pyImage.height 100%101 100%00 100%
pyWebLayout/abstract/block.pyImage.height 100%101 100%00 100%
pyWebLayout/abstract/block.pyImage.get_dimensions 100%101 100%00 100%
pyWebLayout/abstract/block.pyImage.get_aspect_ratio 100%301 100%20 100%
pyWebLayout/abstract/block.pyImage.calculate_scaled_dimensions 80%1021 83%61 81%
pyWebLayout/abstract/block.pyImage._is_url 100%201 100%00 100%
pyWebLayout/abstract/block.pyImage._download_to_temp 75%1641 100%00 75%
pyWebLayout/abstract/block.pyImage.load_image_data 81%2141 88%81 83%
pyWebLayout/abstract/block.pyImage.get_image_info 90%2121 75%123 85%
pyWebLayout/abstract/block.pyLinkedImage.__init__ 100%701 100%00 100%
pyWebLayout/abstract/block.pyLinkedImage.location 100%101 100%00 100%
pyWebLayout/abstract/block.pyLinkedImage.link_type 100%101 100%00 100%
pyWebLayout/abstract/block.pyLinkedImage.link_callback 0%111 100%00 0%
pyWebLayout/abstract/block.pyLinkedImage.params 0%111 100%00 0%
pyWebLayout/abstract/block.pyLinkedImage.link_title 0%111 100%00 0%
pyWebLayout/abstract/block.pyLinkedImage.execute_link 86%711 75%41 82%
pyWebLayout/abstract/block.pyHorizontalRule.__init__ 100%101 100%00 100%
pyWebLayout/abstract/block.pyHorizontalRule.create_and_add_to 0%551 0%20 0%
pyWebLayout/abstract/block.pyPageBreak.__init__ 100%101 100%00 100%
pyWebLayout/abstract/block.pyPageBreak.create_and_add_to 0%551 0%20 0%
pyWebLayout/abstract/block.py(no function) 100%211017 100%00 100%
pyWebLayout/abstract/document.pyDocument.__init__ 84%1931 50%61 76%
pyWebLayout/abstract/document.pyDocument.blocks 100%101 100%00 100%
pyWebLayout/abstract/document.pyDocument.default_style 0%111 100%00 0%
pyWebLayout/abstract/document.pyDocument.default_style 0%111 100%00 0%
pyWebLayout/abstract/document.pyDocument.add_block 100%101 100%00 100%
pyWebLayout/abstract/document.pyDocument.create_paragraph 0%551 0%20 0%
pyWebLayout/abstract/document.pyDocument.create_heading 0%551 0%20 0%
pyWebLayout/abstract/document.pyDocument.create_chapter 0%331 0%20 0%
pyWebLayout/abstract/document.pyDocument.add_anchor 100%101 100%00 100%
pyWebLayout/abstract/document.pyDocument.get_anchor 100%101 100%00 100%
pyWebLayout/abstract/document.pyDocument.add_resource 100%101 100%00 100%
pyWebLayout/abstract/document.pyDocument.get_resource 100%101 100%00 100%
pyWebLayout/abstract/document.pyDocument.add_stylesheet 100%101 100%00 100%
pyWebLayout/abstract/document.pyDocument.add_script 100%101 100%00 100%
pyWebLayout/abstract/document.pyDocument.get_title 100%101 100%00 100%
pyWebLayout/abstract/document.pyDocument.set_title 100%101 100%00 100%
pyWebLayout/abstract/document.pyDocument.title 100%101 100%00 100%
pyWebLayout/abstract/document.pyDocument.title 0%111 100%00 0%
pyWebLayout/abstract/document.pyDocument.find_blocks_by_type 100%401 100%00 100%
pyWebLayout/abstract/document.pyDocument.find_blocks_by_type._find_recursive 71%720 75%82 73%
pyWebLayout/abstract/document.pyDocument.find_headings 100%201 100%00 100%
pyWebLayout/abstract/document.pyDocument.generate_table_of_contents 100%1001 100%40 100%
pyWebLayout/abstract/document.pyDocument.get_or_create_style 100%201 100%00 100%
pyWebLayout/abstract/document.pyDocument.get_font_for_style 100%101 100%00 100%
pyWebLayout/abstract/document.pyDocument.update_rendering_context 0%111 100%00 0%
pyWebLayout/abstract/document.pyDocument.get_style_registry 100%101 100%00 100%
pyWebLayout/abstract/document.pyDocument.get_concrete_style_registry 0%111 100%00 0%
pyWebLayout/abstract/document.pyChapter.__init__ 100%601 100%00 100%
pyWebLayout/abstract/document.pyChapter.title 100%101 100%00 100%
pyWebLayout/abstract/document.pyChapter.title 100%101 100%00 100%
pyWebLayout/abstract/document.pyChapter.level 100%101 100%00 100%
pyWebLayout/abstract/document.pyChapter.blocks 100%101 100%00 100%
pyWebLayout/abstract/document.pyChapter.style 0%111 100%00 0%
pyWebLayout/abstract/document.pyChapter.style 0%111 100%00 0%
pyWebLayout/abstract/document.pyChapter.add_block 100%101 100%00 100%
pyWebLayout/abstract/document.pyChapter.create_paragraph 0%551 0%20 0%
pyWebLayout/abstract/document.pyChapter.create_heading 0%551 0%20 0%
pyWebLayout/abstract/document.pyBook.__init__ 100%401 100%20 100%
pyWebLayout/abstract/document.pyBook.chapters 100%101 100%00 100%
pyWebLayout/abstract/document.pyBook.add_chapter 100%101 100%00 100%
pyWebLayout/abstract/document.pyBook.create_chapter 100%501 50%21 86%
pyWebLayout/abstract/document.pyBook.get_author 100%101 100%00 100%
pyWebLayout/abstract/document.pyBook.set_author 100%101 100%00 100%
pyWebLayout/abstract/document.pyBook.generate_table_of_contents 100%501 100%40 100%
pyWebLayout/abstract/document.py(no function) 100%7804 100%00 100%
pyWebLayout/abstract/functional.pyLink.__init__ 100%601 100%00 100%
pyWebLayout/abstract/functional.pyLink.location 100%101 100%00 100%
pyWebLayout/abstract/functional.pyLink.link_type 100%101 100%00 100%
pyWebLayout/abstract/functional.pyLink.params 100%101 100%00 100%
pyWebLayout/abstract/functional.pyLink.title 100%101 100%00 100%
pyWebLayout/abstract/functional.pyLink.html_id 0%111 100%00 0%
pyWebLayout/abstract/functional.pyLink.execute 100%301 100%20 100%
pyWebLayout/abstract/functional.pyButton.__init__ 100%501 100%00 100%
pyWebLayout/abstract/functional.pyButton.label 100%101 100%00 100%
pyWebLayout/abstract/functional.pyButton.label 100%101 100%00 100%
pyWebLayout/abstract/functional.pyButton.enabled 100%101 100%00 100%
pyWebLayout/abstract/functional.pyButton.enabled 100%101 100%00 100%
pyWebLayout/abstract/functional.pyButton.params 100%101 100%00 100%
pyWebLayout/abstract/functional.pyButton.html_id 0%111 100%00 0%
pyWebLayout/abstract/functional.pyButton.execute 100%301 100%20 100%
pyWebLayout/abstract/functional.pyForm.__init__ 100%501 100%00 100%
pyWebLayout/abstract/functional.pyForm.form_id 100%101 100%00 100%
pyWebLayout/abstract/functional.pyForm.action 100%101 100%00 100%
pyWebLayout/abstract/functional.pyForm.html_id 0%111 100%00 0%
pyWebLayout/abstract/functional.pyForm.add_field 100%201 100%00 100%
pyWebLayout/abstract/functional.pyForm.get_field 100%101 100%00 100%
pyWebLayout/abstract/functional.pyForm.get_values 100%101 100%00 100%
pyWebLayout/abstract/functional.pyForm.execute 100%401 100%20 100%
pyWebLayout/abstract/functional.pyFormField.__init__ 100%701 100%00 100%
pyWebLayout/abstract/functional.pyFormField.name 100%101 100%00 100%
pyWebLayout/abstract/functional.pyFormField.field_type 100%101 100%00 100%
pyWebLayout/abstract/functional.pyFormField.label 100%101 100%00 100%
pyWebLayout/abstract/functional.pyFormField.value 100%101 100%00 100%
pyWebLayout/abstract/functional.pyFormField.value 100%101 100%00 100%
pyWebLayout/abstract/functional.pyFormField.required 100%101 100%00 100%
pyWebLayout/abstract/functional.pyFormField.options 100%101 100%00 100%
pyWebLayout/abstract/functional.pyFormField.form 100%101 100%00 100%
pyWebLayout/abstract/functional.pyFormField.form 100%101 100%00 100%
pyWebLayout/abstract/functional.py(no function) 100%8406 100%00 100%
pyWebLayout/abstract/inline.py_hyphen_dict 100%101 100%00 100%
pyWebLayout/abstract/inline.pyWord.__init__ 100%801 100%20 100%
pyWebLayout/abstract/inline.pyWord.create_and_add_to 100%3201 100%220 100%
pyWebLayout/abstract/inline.pyWord.add_concete 100%100 100%00 100%
pyWebLayout/abstract/inline.pyWord.text 100%101 100%00 100%
pyWebLayout/abstract/inline.pyWord.style 100%101 100%00 100%
pyWebLayout/abstract/inline.pyWord.background 100%101 100%00 100%
pyWebLayout/abstract/inline.pyWord.previous 100%101 100%00 100%
pyWebLayout/abstract/inline.pyWord.next 100%101 100%00 100%
pyWebLayout/abstract/inline.pyWord.add_next 100%101 100%00 100%
pyWebLayout/abstract/inline.pyWord.with_style 100%101 100%00 100%
pyWebLayout/abstract/inline.pyWord.possible_hyphenation 100%101 100%00 100%
pyWebLayout/abstract/inline.pyFormattedSpan.__init__ 100%301 100%00 100%
pyWebLayout/abstract/inline.pyFormattedSpan.create_and_add_to 100%1101 100%80 100%
pyWebLayout/abstract/inline.pyFormattedSpan.style 100%101 100%00 100%
pyWebLayout/abstract/inline.pyFormattedSpan.background 100%101 100%00 100%
pyWebLayout/abstract/inline.pyFormattedSpan.words 100%101 100%00 100%
pyWebLayout/abstract/inline.pyFormattedSpan.add_word 100%601 100%20 100%
pyWebLayout/abstract/inline.pyLinkedWord.__init__ 100%601 100%00 100%
pyWebLayout/abstract/inline.pyLinkedWord.location 100%101 100%00 100%
pyWebLayout/abstract/inline.pyLinkedWord.link_type 100%101 100%00 100%
pyWebLayout/abstract/inline.pyLinkedWord.link_callback 100%101 100%00 100%
pyWebLayout/abstract/inline.pyLinkedWord.params 100%101 100%00 100%
pyWebLayout/abstract/inline.pyLinkedWord.link_title 100%101 100%00 100%
pyWebLayout/abstract/inline.pyLinkedWord.with_style 100%101 100%00 100%
pyWebLayout/abstract/inline.pyLinkedWord.execute_link 83%611 75%41 80%
pyWebLayout/abstract/inline.pyLineBreak.__init__ 100%301 100%00 100%
pyWebLayout/abstract/inline.pyLineBreak.block_type 100%101 100%00 100%
pyWebLayout/abstract/inline.pyLineBreak.create_and_add_to 100%901 100%60 100%
pyWebLayout/abstract/inline.py(no function) 100%6004 100%00 100%
pyWebLayout/abstract/interactive_image.pyInteractiveImage.__init__ 100%401 100%00 100%
pyWebLayout/abstract/interactive_image.pyInteractiveImage.interact 100%401 100%40 100%
pyWebLayout/abstract/interactive_image.pyInteractiveImage.in_object 100%301 100%00 100%
pyWebLayout/abstract/interactive_image.pyInteractiveImage.create_and_add_to 60%1041 38%83 50%
pyWebLayout/abstract/interactive_image.pyInteractiveImage.set_rendered_bounds 100%201 100%00 100%
pyWebLayout/abstract/interactive_image.py(no function) 100%1102 100%00 100%
pyWebLayout/concrete/__init__.py(no function) 100%701 100%00 100%
pyWebLayout/concrete/box.pyBox.__init__ 100%900 100%20 100%
pyWebLayout/concrete/box.pyBox.in_shape 100%100 100%00 100%
pyWebLayout/concrete/box.py(no function) 100%901 100%00 100%
pyWebLayout/concrete/dynamic_page.pyDynamicPage.__init__ 100%901 100%00 100%
pyWebLayout/concrete/dynamic_page.pyDynamicPage.constraints 100%101 100%00 100%
pyWebLayout/concrete/dynamic_page.pyDynamicPage.measure 67%30101 70%204 68%
pyWebLayout/concrete/dynamic_page.pyDynamicPage.get_min_width 70%2371 50%185 61%
pyWebLayout/concrete/dynamic_page.pyDynamicPage.get_preferred_width 72%2981 55%226 65%
pyWebLayout/concrete/dynamic_page.pyDynamicPage.measure_content_height 54%1361 38%81 48%
pyWebLayout/concrete/dynamic_page.pyDynamicPage.layout 100%701 100%00 100%
pyWebLayout/concrete/dynamic_page.pyDynamicPage.render 86%711 67%62 77%
pyWebLayout/concrete/dynamic_page.pyDynamicPage.render_partial 33%15101 10%101 24%
pyWebLayout/concrete/dynamic_page.pyDynamicPage.has_more_content 100%201 100%00 100%
pyWebLayout/concrete/dynamic_page.pyDynamicPage.reset_pagination 100%101 100%00 100%
pyWebLayout/concrete/dynamic_page.pyDynamicPage.invalidate_caches 100%601 100%00 100%
pyWebLayout/concrete/dynamic_page.pyDynamicPage.add_child 100%301 100%00 100%
pyWebLayout/concrete/dynamic_page.pyDynamicPage.clear_children 100%301 100%00 100%
pyWebLayout/concrete/dynamic_page.py(no function) 100%2903 100%00 100%
pyWebLayout/concrete/functional.pyLinkText.__init__ 94%1711 80%102 89%
pyWebLayout/concrete/functional.pyLinkText.link 100%101 100%00 100%
pyWebLayout/concrete/functional.pyLinkText.set_hovered 100%201 100%00 100%
pyWebLayout/concrete/functional.pyLinkText.set_pressed 100%201 100%00 100%
pyWebLayout/concrete/functional.pyLinkText._mark_page_dirty 50%211 50%21 50%
pyWebLayout/concrete/functional.pyLinkText.render 92%1311 83%61 89%
pyWebLayout/concrete/functional.pyButtonText.__init__ 100%1101 100%00 100%
pyWebLayout/concrete/functional.pyButtonText._visual_text_height 60%521 100%00 60%
pyWebLayout/concrete/functional.pyButtonText.button 100%101 100%00 100%
pyWebLayout/concrete/functional.pyButtonText.size 100%101 100%00 100%
pyWebLayout/concrete/functional.pyButtonText.set_pressed 100%201 100%00 100%
pyWebLayout/concrete/functional.pyButtonText.set_hovered 100%201 100%00 100%
pyWebLayout/concrete/functional.pyButtonText.set_page 0%111 100%00 0%
pyWebLayout/concrete/functional.pyButtonText._mark_page_dirty 50%211 50%21 50%
pyWebLayout/concrete/functional.pyButtonText.render 78%2761 67%62 76%
pyWebLayout/concrete/functional.pyButtonText.in_object 100%301 100%00 100%
pyWebLayout/concrete/functional.pyFormFieldText.__init__ 100%901 100%00 100%
pyWebLayout/concrete/functional.pyFormFieldText._visual_label_height 60%521 100%00 60%
pyWebLayout/concrete/functional.pyFormFieldText.field_area_offset 100%101 100%00 100%
pyWebLayout/concrete/functional.pyFormFieldText.field 100%101 100%00 100%
pyWebLayout/concrete/functional.pyFormFieldText.size 100%101 100%00 100%
pyWebLayout/concrete/functional.pyFormFieldText.set_focused 100%101 100%00 100%
pyWebLayout/concrete/functional.pyFormFieldText.render 91%2321 100%40 93%
pyWebLayout/concrete/functional.pyFormFieldText.handle_click 100%501 100%20 100%
pyWebLayout/concrete/functional.pyFormFieldText.in_object 100%301 100%00 100%
pyWebLayout/concrete/functional.pycreate_link_text 100%101 100%00 100%
pyWebLayout/concrete/functional.pycreate_button_text 100%101 100%00 100%
pyWebLayout/concrete/functional.pycreate_form_field_text 100%101 100%00 100%
pyWebLayout/concrete/functional.py(no function) 100%4603 100%00 100%
pyWebLayout/concrete/image.pyRenderableImage.__init__ 100%1501 100%40 100%
pyWebLayout/concrete/image.pyRenderableImage.origin 100%101 100%00 100%
pyWebLayout/concrete/image.pyRenderableImage.size 100%101 100%00 100%
pyWebLayout/concrete/image.pyRenderableImage.width 100%101 100%00 100%
pyWebLayout/concrete/image.pyRenderableImage.set_origin 100%101 100%00 100%
pyWebLayout/concrete/image.pyRenderableImage._load_image 78%2351 88%81 81%
pyWebLayout/concrete/image.pyRenderableImage.render 100%1801 100%100 100%
pyWebLayout/concrete/image.pyRenderableImage._resize_image 93%1511 75%41 89%
pyWebLayout/concrete/image.pyRenderableImage._draw_error_placeholder 94%3521 80%102 91%
pyWebLayout/concrete/image.pyRenderableImage.in_object 100%301 100%00 100%
pyWebLayout/concrete/image.py(no function) 100%2101 100%00 100%
pyWebLayout/concrete/interaction_handler.pyInteractionHandler.__init__ 0%221 100%00 0%
pyWebLayout/concrete/interaction_handler.pyInteractionHandler.set_pressed_state 0%551 0%40 0%
pyWebLayout/concrete/interaction_handler.pyInteractionHandler.set_hovered_state 0%551 0%40 0%
pyWebLayout/concrete/interaction_handler.pyInteractionHandler.render_current_state 0%111 100%00 0%
pyWebLayout/concrete/interaction_handler.pyInteractionHandler.execute_with_feedback 0%11111 0%40 0%
pyWebLayout/concrete/interaction_handler.pyInteractionHandler.execute_async_with_feedback 0%771 100%00 0%
pyWebLayout/concrete/interaction_handler.pyInteractionHandler.execute_async_with_feedback.execute_callback 0%550 0%20 0%
pyWebLayout/concrete/interaction_handler.pyInteractionStateManager.__init__ 100%301 100%00 100%
pyWebLayout/concrete/interaction_handler.pyInteractionStateManager.update_hover 88%1721 67%124 79%
pyWebLayout/concrete/interaction_handler.pyInteractionStateManager.handle_mouse_down 89%911 75%41 85%
pyWebLayout/concrete/interaction_handler.pyInteractionStateManager.handle_mouse_up 100%901 67%62 87%
pyWebLayout/concrete/interaction_handler.pyInteractionStateManager.reset 83%611 75%41 80%
pyWebLayout/concrete/interaction_handler.py(no function) 100%1903 100%00 100%
pyWebLayout/concrete/page.pyPage.__init__ 100%1101 100%00 100%
pyWebLayout/concrete/page.pyPage.free_space 100%101 100%00 100%
pyWebLayout/concrete/page.pyPage.can_fit_line 100%601 100%20 100%
pyWebLayout/concrete/page.pyPage.size 100%101 100%00 100%
pyWebLayout/concrete/page.pyPage.origin 0%111 100%00 0%
pyWebLayout/concrete/page.pyPage.content_origin 100%101 100%00 100%
pyWebLayout/concrete/page.pyPage.content_rect 100%201 100%00 100%
pyWebLayout/concrete/page.pyPage.remaining_height 100%201 100%00 100%
pyWebLayout/concrete/page.pyPage.canvas_size 100%201 100%00 100%
pyWebLayout/concrete/page.pyPage.content_size 100%201 100%00 100%
pyWebLayout/concrete/page.pyPage.border_size 100%101 100%00 100%
pyWebLayout/concrete/page.pyPage.available_width 100%101 100%00 100%
pyWebLayout/concrete/page.pyPage.style 100%101 100%00 100%
pyWebLayout/concrete/page.pyPage.callbacks 100%101 100%00 100%
pyWebLayout/concrete/page.pyPage.is_dirty 0%111 100%00 0%
pyWebLayout/concrete/page.pyPage.mark_dirty 0%111 100%00 0%
pyWebLayout/concrete/page.pyPage.mark_clean 0%111 100%00 0%
pyWebLayout/concrete/page.pyPage.draw 100%401 100%20 100%
pyWebLayout/concrete/page.pyPage.measurement_draw 100%401 100%20 100%
pyWebLayout/concrete/page.pyPage.add_child 100%401 100%00 100%
pyWebLayout/concrete/page.pyPage.remove_child 100%601 100%00 100%
pyWebLayout/concrete/page.pyPage.clear_children 100%501 100%00 100%
pyWebLayout/concrete/page.pyPage.children 100%101 100%00 100%
pyWebLayout/concrete/page.pyPage.render_children 86%711 75%82 80%
pyWebLayout/concrete/page.pyPage.render 100%501 100%00 100%
pyWebLayout/concrete/page.pyPage._create_canvas 100%701 100%20 100%
pyWebLayout/concrete/page.pyPage.query_point 100%1101 100%80 100%
pyWebLayout/concrete/page.pyPage._make_query_result 92%1211 83%61 89%
pyWebLayout/concrete/page.pyPage.query_range 95%1911 86%142 91%
pyWebLayout/concrete/page.pyPage.in_object 100%101 100%00 100%
pyWebLayout/concrete/page.py(no function) 100%5401 100%00 100%
pyWebLayout/concrete/table.pyTableCellRenderer.__init__ 100%701 100%00 100%
pyWebLayout/concrete/table.pyTableCellRenderer.render 100%1301 100%20 100%
pyWebLayout/concrete/table.pyTableCellRenderer._render_cell_content 77%64151 66%329 73%
pyWebLayout/concrete/table.pyTableCellRenderer._render_image_in_cell 40%47281 27%226 36%
pyWebLayout/concrete/table.pyTableRowRenderer.__init__ 100%1001 100%00 100%
pyWebLayout/concrete/table.pyTableRowRenderer.render 100%1401 83%61 95%
pyWebLayout/concrete/table.pyTableRenderer.__init__ 100%1001 100%00 100%
pyWebLayout/concrete/table.pyTableRenderer._calculate_dimensions 92%1211 75%41 88%
pyWebLayout/concrete/table.pyTableRenderer._calculate_row_height_for_section 89%3641 78%184 85%
pyWebLayout/concrete/table.pyTableRenderer._estimate_wrapped_lines 79%2451 70%103 76%
pyWebLayout/concrete/table.pyTableRenderer.render 100%1701 100%80 100%
pyWebLayout/concrete/table.pyTableRenderer._render_caption 100%1001 100%00 100%
pyWebLayout/concrete/table.pyTableRenderer.height 100%101 100%00 100%
pyWebLayout/concrete/table.pyTableRenderer.width 100%101 100%00 100%
pyWebLayout/concrete/table.py(no function) 100%3705 100%00 100%
pyWebLayout/concrete/text.py_glyph_entry_bytes 67%621 100%00 67%
pyWebLayout/concrete/text.pyconfigure_text_caches 0%10101 0%100 0%
pyWebLayout/concrete/text.pyclear_text_caches 0%331 100%00 0%
pyWebLayout/concrete/text.py_space_advance 60%1561 100%20 65%
pyWebLayout/concrete/text.pytext_cache_stats 0%111 100%00 0%
pyWebLayout/concrete/text.pyprewarm_text_caches 0%42421 0%200 0%
pyWebLayout/concrete/text.pyAlignmentHandler.calculate_spacing_and_position 100%001 100%00 100%
pyWebLayout/concrete/text.pyLeftAlignmentHandler.calculate_spacing_and_position 100%801 100%20 100%
pyWebLayout/concrete/text.pyCenterRightAlignmentHandler.__init__ 100%100 100%00 100%
pyWebLayout/concrete/text.pyCenterRightAlignmentHandler.calculate_spacing_and_position 100%1501 100%60 100%
pyWebLayout/concrete/text.pyJustifyAlignmentHandler.__init__ 100%400 100%00 100%
pyWebLayout/concrete/text.pyJustifyAlignmentHandler._gap_spacings 80%511 75%41 78%
pyWebLayout/concrete/text.pyJustifyAlignmentHandler._distribute 100%701 100%20 100%
pyWebLayout/concrete/text.pyJustifyAlignmentHandler.calculate_spacing_and_position 100%1401 100%20 100%
pyWebLayout/concrete/text.pyText.__init__ 100%801 100%00 100%
pyWebLayout/concrete/text.pyText._calculate_dimensions 100%1001 100%20 100%
pyWebLayout/concrete/text.pyText.from_word 100%100 100%00 100%
pyWebLayout/concrete/text.pyText.text 100%101 100%00 100%
pyWebLayout/concrete/text.pyText.style 100%101 100%00 100%
pyWebLayout/concrete/text.pyText.origin 100%101 100%00 100%
pyWebLayout/concrete/text.pyText.line 100%101 100%00 100%
pyWebLayout/concrete/text.pyText.line 100%101 100%00 100%
pyWebLayout/concrete/text.pyText.width 100%101 100%00 100%
pyWebLayout/concrete/text.pyText.size 100%301 100%00 100%
pyWebLayout/concrete/text.pyText.set_origin 100%101 100%00 100%
pyWebLayout/concrete/text.pyText.add_line 100%101 100%00 100%
pyWebLayout/concrete/text.pyText.in_object 100%401 100%00 100%
pyWebLayout/concrete/text.pyText._apply_decoration 50%1471 38%81 45%
pyWebLayout/concrete/text.pyText.render 71%721 67%62 69%
pyWebLayout/concrete/text.pyText._render_from_glyph_cache 72%3291 62%83 70%
pyWebLayout/concrete/text.pyLine.__init__ 100%2001 100%00 100%
pyWebLayout/concrete/text.pyLine.is_paragraph_end 100%101 100%00 100%
pyWebLayout/concrete/text.pyLine.is_paragraph_end 100%100 100%00 100%
pyWebLayout/concrete/text.pyLine.render_alignment_handler 100%301 100%20 100%
pyWebLayout/concrete/text.pyLine._create_alignment_handler 100%501 100%40 100%
pyWebLayout/concrete/text.pyLine.text_objects 100%101 100%00 100%
pyWebLayout/concrete/text.pyLine.set_next 0%111 100%00 0%
pyWebLayout/concrete/text.pyLine._content_width 100%101 100%00 100%
pyWebLayout/concrete/text.pyLine._push_text 100%201 100%00 100%
pyWebLayout/concrete/text.pyLine._pop_text 100%301 100%00 100%
pyWebLayout/concrete/text.pyLine._measure 100%301 100%20 100%
pyWebLayout/concrete/text.pyLine.add_word 97%7521 82%285 93%
pyWebLayout/concrete/text.pyLine.render 100%2201 100%60 100%
pyWebLayout/concrete/text.pyLine.query_point 94%1611 88%81 92%
pyWebLayout/concrete/text.py(no function) 100%9006 100%00 100%
pyWebLayout/core/__init__.py(no function) 100%201 100%00 100%
pyWebLayout/core/base.pyRenderable.render 100%001 100%00 100%
pyWebLayout/core/base.pyRenderable.origin 100%100 100%00 100%
pyWebLayout/core/base.pyInteractable.__init__ 100%101 100%00 100%
pyWebLayout/core/base.pyInteractable.interact 67%311 50%21 60%
pyWebLayout/core/base.pyLayoutable.layout 100%001 100%00 100%
pyWebLayout/core/base.pyQueriable.in_object 100%301 100%00 100%
pyWebLayout/core/base.pyHierarchical.__init__ 100%200 100%00 100%
pyWebLayout/core/base.pyHierarchical.parent 100%101 100%00 100%
pyWebLayout/core/base.pyHierarchical.parent 100%101 100%00 100%
pyWebLayout/core/base.pyGeometric.__init__ 100%300 100%00 100%
pyWebLayout/core/base.pyGeometric.origin 100%101 100%00 100%
pyWebLayout/core/base.pyGeometric.origin 0%111 100%00 0%
pyWebLayout/core/base.pyGeometric.size 100%101 100%00 100%
pyWebLayout/core/base.pyGeometric.size 0%111 100%00 0%
pyWebLayout/core/base.pyGeometric.set_origin 0%111 100%00 0%
pyWebLayout/core/base.pyStyleable.__init__ 100%200 100%00 100%
pyWebLayout/core/base.pyStyleable.style 100%101 100%00 100%
pyWebLayout/core/base.pyStyleable.style 0%111 100%00 0%
pyWebLayout/core/base.pyFontRegistry.__init__ 100%200 100%00 100%
pyWebLayout/core/base.pyFontRegistry.get_or_create_font 100%1801 100%100 100%
pyWebLayout/core/base.pyMetadataContainer.__init__ 100%200 100%00 100%
pyWebLayout/core/base.pyMetadataContainer.set_metadata 100%101 100%00 100%
pyWebLayout/core/base.pyMetadataContainer.get_metadata 100%101 100%00 100%
pyWebLayout/core/base.pyBlockContainer.__init__ 100%200 100%00 100%
pyWebLayout/core/base.pyBlockContainer.blocks 100%101 100%00 100%
pyWebLayout/core/base.pyBlockContainer.add_block 100%301 50%21 80%
pyWebLayout/core/base.pyBlockContainer.create_paragraph 0%661 0%20 0%
pyWebLayout/core/base.pyBlockContainer.create_heading 0%881 0%40 0%
pyWebLayout/core/base.pyContainerAware._validate_container 0%221 0%20 0%
pyWebLayout/core/base.pyContainerAware._inherit_style 0%771 0%60 0%
pyWebLayout/core/base.py(no function) 98%57110 50%21 97%
pyWebLayout/core/cache.py_UsageRanked.__init__ 100%1400 100%40 100%
pyWebLayout/core/cache.py_UsageRanked._over_budget 100%001 100%00 100%
pyWebLayout/core/cache.py_UsageRanked._record_add 100%001 100%00 100%
pyWebLayout/core/cache.py_UsageRanked._record_remove 100%001 100%00 100%
pyWebLayout/core/cache.py_UsageRanked.get 100%701 100%20 100%
pyWebLayout/core/cache.py_UsageRanked._add_new 100%301 100%00 100%
pyWebLayout/core/cache.py_UsageRanked._remove 100%701 100%20 100%
pyWebLayout/core/cache.py_UsageRanked._evict_one 94%1811 88%81 92%
pyWebLayout/core/cache.py_UsageRanked._evict_to_budget 67%310 75%41 71%
pyWebLayout/core/cache.py_UsageRanked._maybe_age 100%901 100%60 100%
pyWebLayout/core/cache.py_UsageRanked.clear 100%301 100%00 100%
pyWebLayout/core/cache.py_UsageRanked._base_stats 100%200 100%00 100%
pyWebLayout/core/cache.py_UsageRanked.__len__ 100%100 100%00 100%
pyWebLayout/core/cache.py_UsageRanked.__contains__ 100%100 100%00 100%
pyWebLayout/core/cache.pyUsageCache.__init__ 100%400 100%20 100%
pyWebLayout/core/cache.pyUsageCache._over_budget 100%100 100%00 100%
pyWebLayout/core/cache.pyUsageCache.put 100%1001 100%40 100%
pyWebLayout/core/cache.pyUsageCache.max_entries 0%110 100%00 0%
pyWebLayout/core/cache.pyUsageCache.resize 100%401 100%20 100%
pyWebLayout/core/cache.pyUsageCache.stats 100%301 100%00 100%
pyWebLayout/core/cache.pySizedUsageCache.__init__ 100%700 100%20 100%
pyWebLayout/core/cache.pySizedUsageCache._over_budget 100%100 100%00 100%
pyWebLayout/core/cache.pySizedUsageCache._record_add 100%300 100%00 100%
pyWebLayout/core/cache.pySizedUsageCache._record_remove 100%100 100%00 100%
pyWebLayout/core/cache.pySizedUsageCache.put 100%901 100%60 100%
pyWebLayout/core/cache.pySizedUsageCache.max_bytes 100%100 100%00 100%
pyWebLayout/core/cache.pySizedUsageCache.total_bytes 100%100 100%00 100%
pyWebLayout/core/cache.pySizedUsageCache.resize 75%411 50%21 67%
pyWebLayout/core/cache.pySizedUsageCache.clear 100%301 100%00 100%
pyWebLayout/core/cache.pySizedUsageCache.stats 100%401 100%00 100%
pyWebLayout/core/cache.py(no function) 100%4604 100%00 100%
pyWebLayout/core/callback_registry.pyCallbackRegistry.__init__ 100%401 100%00 100%
pyWebLayout/core/callback_registry.pyCallbackRegistry.register 100%1301 100%40 100%
pyWebLayout/core/callback_registry.pyCallbackRegistry.get_by_id 100%101 100%00 100%
pyWebLayout/core/callback_registry.pyCallbackRegistry.get_by_type 100%101 100%00 100%
pyWebLayout/core/callback_registry.pyCallbackRegistry.get_all_ids 100%101 100%00 100%
pyWebLayout/core/callback_registry.pyCallbackRegistry.get_all_types 100%101 100%00 100%
pyWebLayout/core/callback_registry.pyCallbackRegistry.set_callback 100%501 100%20 100%
pyWebLayout/core/callback_registry.pyCallbackRegistry.set_callbacks_by_type 100%401 100%20 100%
pyWebLayout/core/callback_registry.pyCallbackRegistry.unregister 73%1131 50%42 67%
pyWebLayout/core/callback_registry.pyCallbackRegistry.clear 100%401 100%00 100%
pyWebLayout/core/callback_registry.pyCallbackRegistry.count 100%101 100%00 100%
pyWebLayout/core/callback_registry.pyCallbackRegistry.count_by_type 100%101 100%00 100%
pyWebLayout/core/callback_registry.pyCallbackRegistry._get_type_name 88%811 83%61 86%
pyWebLayout/core/callback_registry.pyCallbackRegistry.__len__ 100%101 100%00 100%
pyWebLayout/core/callback_registry.pyCallbackRegistry.__contains__ 100%101 100%00 100%
pyWebLayout/core/callback_registry.pyCallbackRegistry.__repr__ 100%003 100%00 100%
pyWebLayout/core/callback_registry.py(no function) 100%1803 100%00 100%
pyWebLayout/core/highlight.pyHighlight.__post_init__ 100%201 100%20 100%
pyWebLayout/core/highlight.pyHighlight.to_dict 100%101 100%00 100%
pyWebLayout/core/highlight.pyHighlight.from_dict 100%101 100%00 100%
pyWebLayout/core/highlight.pyHighlightManager.__init__ 100%401 100%00 100%
pyWebLayout/core/highlight.pyHighlightManager.add_highlight 100%201 100%00 100%
pyWebLayout/core/highlight.pyHighlightManager.remove_highlight 100%501 100%20 100%
pyWebLayout/core/highlight.pyHighlightManager.get_highlight 100%101 100%00 100%
pyWebLayout/core/highlight.pyHighlightManager.list_highlights 100%101 100%00 100%
pyWebLayout/core/highlight.pyHighlightManager.clear_all 100%201 100%00 100%
pyWebLayout/core/highlight.pyHighlightManager.get_highlights_for_page 100%801 100%60 100%
pyWebLayout/core/highlight.pyHighlightManager._get_filepath 100%101 100%00 100%
pyWebLayout/core/highlight.pyHighlightManager._save_highlights 100%101 100%00 100%
pyWebLayout/core/highlight.pyHighlightManager._load_highlights 50%631 100%00 50%
pyWebLayout/core/highlight.pycreate_highlight_from_query_result 100%801 100%20 100%
pyWebLayout/core/highlight.py(no function) 100%4404 100%00 100%
pyWebLayout/core/persistence.pyensure_dir 100%301 100%00 100%
pyWebLayout/core/persistence.pyread_json 100%801 100%20 100%
pyWebLayout/core/persistence.pywrite_json 57%731 100%00 57%
pyWebLayout/core/persistence.py(no function) 100%901 100%00 100%
pyWebLayout/core/query.pyQueryResult.to_dict 100%101 100%00 100%
pyWebLayout/core/query.pySelectionRange.text 100%101 100%00 100%
pyWebLayout/core/query.pySelectionRange.bounds_list 100%101 100%00 100%
pyWebLayout/core/query.pySelectionRange.to_dict 100%101 100%00 100%
pyWebLayout/core/query.py(no function) 97%2913 50%21 94%
pyWebLayout/io/__init__.py(no function) 100%001 100%00 100%
pyWebLayout/io/readers/__init__.py(no function) 100%201 100%00 100%
pyWebLayout/io/readers/epub_reader.pydefault_eink_processor 0%551 0%20 0%
pyWebLayout/io/readers/epub_reader.pyEPUBReader.__init__ 100%1001 100%00 100%
pyWebLayout/io/readers/epub_reader.pyEPUBReader.read 100%1201 100%00 100%
pyWebLayout/io/readers/epub_reader.pyEPUBReader._extract_epub 69%1651 30%103 54%
pyWebLayout/io/readers/epub_reader.pyEPUBReader._parse_package_document 93%1511 60%104 80%
pyWebLayout/io/readers/epub_reader.pyEPUBReader._parse_metadata 94%3121 93%282 93%
pyWebLayout/io/readers/epub_reader.pyEPUBReader._parse_manifest 91%1111 67%62 82%
pyWebLayout/io/readers/epub_reader.pyEPUBReader._parse_spine 90%1011 62%83 78%
pyWebLayout/io/readers/epub_reader.pyEPUBReader._parse_toc 44%1691 21%143 33%
pyWebLayout/io/readers/epub_reader.pyEPUBReader._parse_nav_points 92%1311 75%41 88%
pyWebLayout/io/readers/epub_reader.pyEPUBReader._create_book 100%1601 88%162 94%
pyWebLayout/io/readers/epub_reader.pyEPUBReader._add_cover_chapter 6%33311 12%81 7%
pyWebLayout/io/readers/epub_reader.pyEPUBReader._process_chapter_images 65%2381 60%102 64%
pyWebLayout/io/readers/epub_reader.pyEPUBReader._process_content_images 100%201 100%20 100%
pyWebLayout/io/readers/epub_reader.pyEPUBReader._add_chapters 69%36111 80%102 72%
pyWebLayout/io/readers/epub_reader.pyEPUBReader._add_chapters.add_to_toc_map 86%710 67%62 77%
pyWebLayout/io/readers/epub_reader.pyread_epub 100%201 100%00 100%
pyWebLayout/io/readers/epub_reader.py(no function) 100%2802 100%00 100%
pyWebLayout/io/readers/html_extraction.pyStyleContext.with_font 100%101 100%00 100%
pyWebLayout/io/readers/html_extraction.pyStyleContext.with_background 100%101 100%00 100%
pyWebLayout/io/readers/html_extraction.pyStyleContext.with_css_classes 100%101 100%00 100%
pyWebLayout/io/readers/html_extraction.pyStyleContext.with_css_styles 100%101 100%00 100%
pyWebLayout/io/readers/html_extraction.pyStyleContext.with_attributes 100%101 100%00 100%
pyWebLayout/io/readers/html_extraction.pyStyleContext.push_element 100%101 100%00 100%
pyWebLayout/io/readers/html_extraction.pycreate_base_context 100%501 100%40 100%
pyWebLayout/io/readers/html_extraction.pyapply_element_styling 100%1801 100%40 100%
pyWebLayout/io/readers/html_extraction.pyparse_inline_styles 100%601 100%40 100%
pyWebLayout/io/readers/html_extraction.pyapply_element_font_styles 83%75131 81%487 82%
pyWebLayout/io/readers/html_extraction.pyapply_background_styles 80%511 75%41 78%
pyWebLayout/io/readers/html_extraction.pyextract_text_content 100%101 100%00 100%
pyWebLayout/io/readers/html_extraction.pyextract_words_from_nodes 85%4671 64%365 76%
pyWebLayout/io/readers/html_extraction.pyis_inline 100%501 100%40 100%
pyWebLayout/io/readers/html_extraction.pyprocess_block_children 100%2101 100%120 100%
pyWebLayout/io/readers/html_extraction.pyprocess_block_children.flush_run 100%901 100%60 100%
pyWebLayout/io/readers/html_extraction.pyprocess_element 100%301 100%00 100%
pyWebLayout/io/readers/html_extraction.pyparagraph_handler 97%3311 83%244 91%
pyWebLayout/io/readers/html_extraction.pydiv_handler 100%101 100%00 100%
pyWebLayout/io/readers/html_extraction.pyheading_handler 100%701 100%20 100%
pyWebLayout/io/readers/html_extraction.pyblockquote_handler 100%401 100%20 100%
pyWebLayout/io/readers/html_extraction.pypreformatted_handler 100%601 100%20 100%
pyWebLayout/io/readers/html_extraction.pycode_handler 0%331 0%20 0%
pyWebLayout/io/readers/html_extraction.pyunordered_list_handler 100%801 83%61 93%
pyWebLayout/io/readers/html_extraction.pyordered_list_handler 100%801 83%61 93%
pyWebLayout/io/readers/html_extraction.pylist_item_handler 100%401 100%20 100%
pyWebLayout/io/readers/html_extraction.pytable_handler 95%2111 75%164 86%
pyWebLayout/io/readers/html_extraction.pytable_row_handler 100%801 83%61 93%
pyWebLayout/io/readers/html_extraction.pytable_cell_handler 100%601 100%20 100%
pyWebLayout/io/readers/html_extraction.pytable_header_cell_handler 100%601 100%20 100%
pyWebLayout/io/readers/html_extraction.pyhorizontal_rule_handler 100%101 100%00 100%
pyWebLayout/io/readers/html_extraction.pyline_break_handler 100%101 100%00 100%
pyWebLayout/io/readers/html_extraction.pyimage_handler 88%1621 100%60 91%
pyWebLayout/io/readers/html_extraction.pyignore_handler 100%101 100%00 100%
pyWebLayout/io/readers/html_extraction.pygeneric_handler 100%101 100%00 100%
pyWebLayout/io/readers/html_extraction.pyparse_html_string 100%1301 100%80 100%
pyWebLayout/io/readers/html_extraction.py(no function) 100%5202 100%00 100%
pyWebLayout/layout/__init__.py(no function) 100%001 100%00 100%
pyWebLayout/layout/document_layouter.pyparagraph_layouter 88%80101 76%467 83%
pyWebLayout/layout/document_layouter.pyparagraph_layouter.create_new_line 86%711 75%41 82%
pyWebLayout/layout/document_layouter.pypagebreak_layouter 0%111 100%00 0%
pyWebLayout/layout/document_layouter.pyimage_layouter 94%1711 88%81 92%
pyWebLayout/layout/document_layouter.pytable_layouter 100%1401 100%20 100%
pyWebLayout/layout/document_layouter.pybutton_layouter 0%14141 0%40 0%
pyWebLayout/layout/document_layouter.pyform_field_layouter 86%1421 50%42 78%
pyWebLayout/layout/document_layouter.pyform_layouter 82%1121 75%82 79%
pyWebLayout/layout/document_layouter.pyDocumentLayouter.__init__ 100%701 100%20 100%
pyWebLayout/layout/document_layouter.pyDocumentLayouter.layout_paragraph 100%101 100%00 100%
pyWebLayout/layout/document_layouter.pyDocumentLayouter.layout_image 100%101 100%00 100%
pyWebLayout/layout/document_layouter.pyDocumentLayouter.layout_table 100%101 100%00 100%
pyWebLayout/layout/document_layouter.pyDocumentLayouter.layout_button 0%111 100%00 0%
pyWebLayout/layout/document_layouter.pyDocumentLayouter.layout_form 100%101 100%00 100%
pyWebLayout/layout/document_layouter.pyDocumentLayouter.layout_document 59%2291 55%222 57%
pyWebLayout/layout/document_layouter.py(no function) 100%2701 100%00 100%
pyWebLayout/layout/ereader_layout.pyRenderingPosition._key 100%101 100%00 100%
pyWebLayout/layout/ereader_layout.pyRenderingPosition.to_dict 100%101 100%00 100%
pyWebLayout/layout/ereader_layout.pyRenderingPosition.from_dict 100%101 100%00 100%
pyWebLayout/layout/ereader_layout.pyRenderingPosition.copy 100%101 100%00 100%
pyWebLayout/layout/ereader_layout.pyRenderingPosition.__eq__ 100%301 100%20 100%
pyWebLayout/layout/ereader_layout.pyRenderingPosition.__hash__ 100%101 100%00 100%
pyWebLayout/layout/ereader_layout.pyChapterInfo.__init__ 100%400 100%00 100%
pyWebLayout/layout/ereader_layout.pyChapterNavigator.__init__ 100%300 100%00 100%
pyWebLayout/layout/ereader_layout.pyChapterNavigator._build_chapter_map 100%1301 100%80 100%
pyWebLayout/layout/ereader_layout.pyChapterNavigator._extract_heading_text 100%501 75%41 89%
pyWebLayout/layout/ereader_layout.pyChapterNavigator.get_table_of_contents 100%101 100%00 100%
pyWebLayout/layout/ereader_layout.pyChapterNavigator.get_chapter_position 100%401 100%40 100%
pyWebLayout/layout/ereader_layout.pyChapterNavigator.get_current_chapter 89%911 88%81 88%
pyWebLayout/layout/ereader_layout.pyFontFamilyOverride.__init__ 0%111 100%00 0%
pyWebLayout/layout/ereader_layout.pyFontFamilyOverride.override_font 0%661 0%40 0%
pyWebLayout/layout/ereader_layout.pyFontScaler.scale_font 86%711 75%41 82%
pyWebLayout/layout/ereader_layout.pyFontScaler.scale_word_spacing 100%401 100%20 100%
pyWebLayout/layout/ereader_layout.pyBidirectionalLayouter.__init__ 100%800 100%00 100%
pyWebLayout/layout/ereader_layout.pyBidirectionalLayouter.render_page_forward 95%2111 93%141 94%
pyWebLayout/layout/ereader_layout.pyBidirectionalLayouter.render_page_backward 86%2231 69%163 79%
pyWebLayout/layout/ereader_layout.pyBidirectionalLayouter._backward_anchors 100%801 100%60 100%
pyWebLayout/layout/ereader_layout.pyBidirectionalLayouter._replay_to 81%1631 70%103 77%
pyWebLayout/layout/ereader_layout.pyBidirectionalLayouter._position_key 100%101 100%00 100%
pyWebLayout/layout/ereader_layout.pyBidirectionalLayouter._scale_block_fonts 100%901 100%40 100%
pyWebLayout/layout/ereader_layout.pyBidirectionalLayouter._build_scaled_block 97%3511 93%282 95%
pyWebLayout/layout/ereader_layout.pyBidirectionalLayouter._build_scaled_block.scale 100%100 100%00 100%
pyWebLayout/layout/ereader_layout.pyBidirectionalLayouter._layout_block_on_page 77%1331 70%103 74%
pyWebLayout/layout/ereader_layout.pyBidirectionalLayouter._layout_paragraph_on_page 94%1711 88%81 92%
pyWebLayout/layout/ereader_layout.pyBidirectionalLayouter._layout_heading_on_page 100%101 100%00 100%
pyWebLayout/layout/ereader_layout.pyBidirectionalLayouter._layout_table_on_page 100%501 100%00 100%
pyWebLayout/layout/ereader_layout.pyBidirectionalLayouter._layout_list_on_page 100%401 100%00 100%
pyWebLayout/layout/ereader_layout.pyBidirectionalLayouter._layout_image_on_page 100%701 100%20 100%
pyWebLayout/layout/ereader_layout.pyBidirectionalLayouter._position_compare 100%701 100%60 100%
pyWebLayout/layout/ereader_layout.py(no function) 100%6407 100%00 100%
pyWebLayout/layout/ereader_manager.pyBookmarkManager.__init__ 100%601 100%00 100%
pyWebLayout/layout/ereader_manager.pyBookmarkManager._load_bookmarks 50%631 100%00 50%
pyWebLayout/layout/ereader_manager.pyBookmarkManager._save_bookmarks 100%101 100%00 100%
pyWebLayout/layout/ereader_manager.pyBookmarkManager.add_bookmark 100%201 100%00 100%
pyWebLayout/layout/ereader_manager.pyBookmarkManager.remove_bookmark 100%501 100%20 100%
pyWebLayout/layout/ereader_manager.pyBookmarkManager.get_bookmark 100%101 100%00 100%
pyWebLayout/layout/ereader_manager.pyBookmarkManager.list_bookmarks 100%101 100%00 100%
pyWebLayout/layout/ereader_manager.pyBookmarkManager.save_reading_position 100%101 100%00 100%
pyWebLayout/layout/ereader_manager.pyBookmarkManager.load_reading_position 62%831 100%20 70%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager.__init__ 100%2401 100%40 100%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager.prewarm_caches 0%30301 0%140 0%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager.set_position_changed_callback 100%101 100%00 100%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager.set_chapter_changed_callback 100%101 100%00 100%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager._detect_cover 100%601 100%40 100%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager._render_cover_page 77%1331 50%42 71%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager._notify_position_changed 100%601 100%40 100%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager.get_current_page 100%401 100%20 100%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager.next_page 95%1911 88%81 93%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager.previous_page 90%2121 80%102 87%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager._is_at_beginning 100%201 100%00 100%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager.jump_to_position 100%401 100%00 100%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager.jump_to_chapter 100%401 100%20 100%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager.jump_to_chapter_index 100%401 100%20 100%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager._add_to_history 75%411 75%41 75%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager._get_from_history 88%811 83%61 86%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager._clear_history 100%101 100%00 100%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager.set_font_scale 100%401 100%20 100%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager.get_font_scale 100%101 100%00 100%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager.set_font_family 0%331 100%00 0%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager.get_font_family 100%101 100%00 100%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager.increase_line_spacing 0%551 100%00 0%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager.decrease_line_spacing 0%551 100%00 0%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager.increase_inter_block_spacing 0%551 100%00 0%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager.decrease_inter_block_spacing 0%551 100%00 0%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager.increase_word_spacing 0%551 100%00 0%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager.decrease_word_spacing 0%551 100%00 0%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager.get_table_of_contents 100%101 100%00 100%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager.get_current_chapter 100%101 100%00 100%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager.add_bookmark 60%521 100%00 60%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager.remove_bookmark 100%101 100%00 100%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager.jump_to_bookmark 100%401 100%20 100%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager.list_bookmarks 100%101 100%00 100%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager.highlight_point 100%401 100%20 100%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager.highlight_range 100%401 100%20 100%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager._store_highlight 100%301 100%00 100%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager.remove_highlight 100%101 100%00 100%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager.list_highlights 100%101 100%00 100%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager.get_highlights_for_current_page 100%201 100%00 100%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager.clear_highlights 100%101 100%00 100%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager._interaction_state 100%701 100%40 100%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager.handle_hover 100%101 100%00 100%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager.handle_touch_down 100%101 100%00 100%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager.handle_touch_up 100%101 100%00 100%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager.reset_interaction_state 100%201 100%20 100%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager.get_reading_progress 100%501 100%20 100%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager.has_cover 100%101 100%00 100%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager.is_on_cover 100%101 100%00 100%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager.jump_to_cover 80%511 50%21 71%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager.get_position_info 100%301 100%00 100%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager.get_cache_stats 100%101 100%00 100%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager.shutdown 100%501 100%20 100%
pyWebLayout/layout/ereader_manager.pyEreaderLayoutManager.__del__ 50%421 100%00 50%
pyWebLayout/layout/ereader_manager.pycreate_ereader_manager 100%101 100%00 100%
pyWebLayout/layout/ereader_manager.py(no function) 100%8103 100%00 100%
pyWebLayout/layout/page_buffer.pyPageBuffer.__init__ 100%901 100%00 100%
pyWebLayout/layout/page_buffer.pyPageBuffer.initialize 100%401 100%00 100%
pyWebLayout/layout/page_buffer.pyPageBuffer.get_page 100%901 100%40 100%
pyWebLayout/layout/page_buffer.pyPageBuffer.cache_page 100%1001 100%60 100%
pyWebLayout/layout/page_buffer.pyPageBuffer.invalidate_all 100%401 100%00 100%
pyWebLayout/layout/page_buffer.pyPageBuffer.set_font_scale 100%301 100%20 100%
pyWebLayout/layout/page_buffer.pyPageBuffer.set_font_family 0%331 0%20 0%
pyWebLayout/layout/page_buffer.pyPageBuffer.get_cache_stats 100%101 100%00 100%
pyWebLayout/layout/page_buffer.pyPageBuffer.shutdown 100%101 100%00 100%
pyWebLayout/layout/page_buffer.pyBufferedPageRenderer.__init__ 100%1001 100%00 100%
pyWebLayout/layout/page_buffer.pyBufferedPageRenderer.render_page 100%1101 100%60 100%
pyWebLayout/layout/page_buffer.pyBufferedPageRenderer.render_page_backward 55%1151 33%62 47%
pyWebLayout/layout/page_buffer.pyBufferedPageRenderer.set_font_family 0%551 0%20 0%
pyWebLayout/layout/page_buffer.pyBufferedPageRenderer.get_font_family 100%101 100%00 100%
pyWebLayout/layout/page_buffer.pyBufferedPageRenderer.get_cache_stats 100%101 100%00 100%
pyWebLayout/layout/page_buffer.pyBufferedPageRenderer.shutdown 100%101 100%00 100%
pyWebLayout/layout/page_buffer.py(no function) 100%2603 100%00 100%
pyWebLayout/layout/table_optimizer.pyoptimize_table_layout 87%3951 83%122 86%
pyWebLayout/layout/table_optimizer.pylayout_cell_content 92%2521 80%102 89%
pyWebLayout/layout/table_optimizer.pyget_column_count 100%501 100%20 100%
pyWebLayout/layout/table_optimizer.pysample_table_rows 100%501 100%20 100%
pyWebLayout/layout/table_optimizer.pyextract_html_column_widths 79%1431 50%122 65%
pyWebLayout/layout/table_optimizer.pyparse_html_width 81%1631 88%81 83%
pyWebLayout/layout/table_optimizer.pydistribute_column_widths 97%3411 95%221 96%
pyWebLayout/layout/table_optimizer.pycalculate_table_overhead 100%301 100%00 100%
pyWebLayout/layout/table_optimizer.py(no function) 100%1001 100%00 100%
pyWebLayout/style/__init__.py(no function) 100%601 100%00 100%
pyWebLayout/style/abstract_style.pyFontSize.from_value 0%11111 0%40 0%
pyWebLayout/style/abstract_style.pyAbstractStyle.__post_init__ 20%541 50%21 29%
pyWebLayout/style/abstract_style.pyAbstractStyle.__hash__ 100%701 100%20 100%
pyWebLayout/style/abstract_style.pyAbstractStyle.merge_with 0%551 100%00 0%
pyWebLayout/style/abstract_style.pyAbstractStyle.with_modifications 100%301 100%00 100%
pyWebLayout/style/abstract_style.pyAbstractStyleRegistry.__init__ 100%401 100%00 100%
pyWebLayout/style/abstract_style.pyAbstractStyleRegistry._create_default_style 100%501 100%00 100%
pyWebLayout/style/abstract_style.pyAbstractStyleRegistry.default_style 100%101 100%00 100%
pyWebLayout/style/abstract_style.pyAbstractStyleRegistry._generate_style_id 100%301 100%00 100%
pyWebLayout/style/abstract_style.pyAbstractStyleRegistry.get_style_id 100%101 100%00 100%
pyWebLayout/style/abstract_style.pyAbstractStyleRegistry.register_style 88%811 50%42 75%
pyWebLayout/style/abstract_style.pyAbstractStyleRegistry.get_or_create_style 90%1011 83%61 88%
pyWebLayout/style/abstract_style.pyAbstractStyleRegistry.get_style_by_id 100%101 100%00 100%
pyWebLayout/style/abstract_style.pyAbstractStyleRegistry.create_derived_style 80%511 50%21 71%
pyWebLayout/style/abstract_style.pyAbstractStyleRegistry.resolve_effective_style 57%731 50%42 55%
pyWebLayout/style/abstract_style.pyAbstractStyleRegistry.get_all_styles 0%111 100%00 0%
pyWebLayout/style/abstract_style.pyAbstractStyleRegistry.get_style_count 100%101 100%00 100%
pyWebLayout/style/abstract_style.py(no function) 100%5705 100%00 100%
pyWebLayout/style/alignment.pyAlignment.__str__ 0%111 100%00 0%
pyWebLayout/style/alignment.py(no function) 100%1002 100%00 100%
pyWebLayout/style/concrete_style.pyConcreteStyle.create_font 100%101 100%00 100%
pyWebLayout/style/concrete_style.pyStyleResolver.__init__ 100%401 100%00 100%
pyWebLayout/style/concrete_style.pyStyleResolver.resolve_style 100%2601 100%100 100%
pyWebLayout/style/concrete_style.pyStyleResolver._resolve_font_path 29%751 17%61 23%
pyWebLayout/style/concrete_style.pyStyleResolver._resolve_font_size 69%1341 83%61 74%
pyWebLayout/style/concrete_style.pyStyleResolver._resolve_color 59%27111 56%165 58%
pyWebLayout/style/concrete_style.pyStyleResolver._resolve_background_color 17%12101 10%101 14%
pyWebLayout/style/concrete_style.pyStyleResolver._resolve_line_height 20%1081 17%61 19%
pyWebLayout/style/concrete_style.pyStyleResolver._resolve_letter_spacing 12%16141 12%81 12%
pyWebLayout/style/concrete_style.pyStyleResolver._resolve_word_spacing 56%1671 75%82 62%
pyWebLayout/style/concrete_style.pyStyleResolver.update_context 0%441 100%00 0%
pyWebLayout/style/concrete_style.pyStyleResolver.clear_cache 0%111 100%00 0%
pyWebLayout/style/concrete_style.pyStyleResolver.get_cache_size 100%101 100%00 100%
pyWebLayout/style/concrete_style.pyConcreteStyleRegistry.__init__ 100%201 100%00 100%
pyWebLayout/style/concrete_style.pyConcreteStyleRegistry.get_concrete_style 100%101 100%00 100%
pyWebLayout/style/concrete_style.pyConcreteStyleRegistry.get_font 100%601 100%20 100%
pyWebLayout/style/concrete_style.pyConcreteStyleRegistry.clear_caches 0%221 100%00 0%
pyWebLayout/style/concrete_style.pyConcreteStyleRegistry.get_cache_stats 100%101 100%00 100%
pyWebLayout/style/concrete_style.py(no function) 100%5705 100%00 100%
pyWebLayout/style/fonts.pyget_bundled_fonts_dir 0%11111 0%40 0%
pyWebLayout/style/fonts.pyget_bundled_font_path 0%25251 0%160 0%
pyWebLayout/style/fonts.pyFont.__init__ 100%1001 100%00 100%
pyWebLayout/style/fonts.pyFont.from_family 0%221 100%00 0%
pyWebLayout/style/fonts.pyFont._get_bundled_font_path 81%1631 75%41 80%
pyWebLayout/style/fonts.pyFont._load_font 88%2531 88%81 88%
pyWebLayout/style/fonts.pyFont.font 100%101 100%00 100%
pyWebLayout/style/fonts.pyFont.font_size 100%101 100%00 100%
pyWebLayout/style/fonts.pyFont.colour 100%101 100%00 100%
pyWebLayout/style/fonts.pyFont.color 100%101 100%00 100%
pyWebLayout/style/fonts.pyFont.background 100%101 100%00 100%
pyWebLayout/style/fonts.pyFont.weight 100%101 100%00 100%
pyWebLayout/style/fonts.pyFont.style 100%101 100%00 100%
pyWebLayout/style/fonts.pyFont.decoration 100%101 100%00 100%
pyWebLayout/style/fonts.pyFont.min_hyphenation_width 100%101 100%00 100%
pyWebLayout/style/fonts.pyFont._with_modified 100%301 100%00 100%
pyWebLayout/style/fonts.pyFont.with_size 100%101 100%00 100%
pyWebLayout/style/fonts.pyFont.with_colour 100%101 100%00 100%
pyWebLayout/style/fonts.pyFont.with_weight 100%101 100%00 100%
pyWebLayout/style/fonts.pyFont.with_style 100%101 100%00 100%
pyWebLayout/style/fonts.pyFont.with_decoration 100%101 100%00 100%
pyWebLayout/style/fonts.py(no function) 100%5502 100%00 100%
pyWebLayout/style/page_style.pyPageStyle.padding_top 100%100 100%00 100%
pyWebLayout/style/page_style.pyPageStyle.padding_right 100%100 100%00 100%
pyWebLayout/style/page_style.pyPageStyle.padding_bottom 100%100 100%00 100%
pyWebLayout/style/page_style.pyPageStyle.padding_left 100%100 100%00 100%
pyWebLayout/style/page_style.pyPageStyle.total_horizontal_padding 100%101 100%00 100%
pyWebLayout/style/page_style.pyPageStyle.total_vertical_padding 100%101 100%00 100%
pyWebLayout/style/page_style.pyPageStyle.total_border_width 100%101 100%00 100%
pyWebLayout/style/page_style.py(no function) 100%2801 100%00 100%
Total  85%5525847802 71%1628225 82%
+

+ No items found using the specified filter. +

+
+ + + diff --git a/cov_info/htmlcov/index.html b/cov_info/htmlcov/index.html new file mode 100644 index 0000000..bddd61f --- /dev/null +++ b/cov_info/htmlcov/index.html @@ -0,0 +1,675 @@ + + + + + Coverage report + + + + + +
+
+

Coverage report: + 82% +

+ +
+ +
+ + +
+
+

+ Files + Functions + Classes +

+

+ coverage.py v7.15.4, + created at 2026-08-08 20:34 +0000 +

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
  Statements Branches Total
File coveragestatementsmissingexcluded coveragebranchespartial coverage
pyWebLayout/__init__.py 100%101 100%00 100%
pyWebLayout/abstract/__init__.py 100%501 100%00 100%
pyWebLayout/abstract/block.py 81%48993119 76%666 80%
pyWebLayout/abstract/document.py 82%1943547 56%364 78%
pyWebLayout/abstract/functional.py 98%144339 100%60 98%
pyWebLayout/abstract/inline.py 99%164132 98%441 99%
pyWebLayout/abstract/interactive_image.py 88%3447 58%123 80%
pyWebLayout/concrete/__init__.py 100%701 100%00 100%
pyWebLayout/concrete/box.py 100%1901 100%20 100%
pyWebLayout/concrete/dynamic_page.py 76%1784217 51%8419 68%
pyWebLayout/concrete/functional.py 91%1901731 78%327 89%
pyWebLayout/concrete/image.py 94%134811 89%364 93%
pyWebLayout/concrete/interaction_handler.py 60%994014 45%408 55%
pyWebLayout/concrete/page.py 96%176731 89%445 95%
pyWebLayout/concrete/table.py 83%3035319 65%10224 78%
pyWebLayout/concrete/text.py 81%4628746 61%12213 77%
pyWebLayout/core/__init__.py 100%201 100%00 100%
pyWebLayout/core/base.py 78%1342933 43%303 72%
pyWebLayout/core/cache.py 98%171420 93%443 97%
pyWebLayout/core/callback_registry.py 95%75421 83%183 92%
pyWebLayout/core/highlight.py 97%87318 100%120 97%
pyWebLayout/core/persistence.py 89%2734 100%20 90%
pyWebLayout/core/query.py 97%3317 50%21 94%
pyWebLayout/io/__init__.py 100%001 100%00 100%
pyWebLayout/io/readers/__init__.py 100%201 100%00 100%
pyWebLayout/io/readers/epub_reader.py 73%2867618 63%13427 70%
pyWebLayout/io/readers/html_extraction.py 93%4002838 83%20824 89%
pyWebLayout/layout/__init__.py 100%001 100%00 100%
pyWebLayout/layout/document_layouter.py 81%2194116 69%10015 77%
pyWebLayout/layout/ereader_layout.py 93%3042136 84%14016 90%
pyWebLayout/layout/ereader_manager.py 78%3708266 75%888 77%
pyWebLayout/layout/page_buffer.py 88%1101319 71%282 85%
pyWebLayout/layout/table_optimizer.py 91%151149 82%688 88%
pyWebLayout/style/__init__.py 100%601 100%00 100%
pyWebLayout/style/abstract_style.py 80%1352722 54%247 76%
pyWebLayout/style/alignment.py 91%1113 100%00 91%
pyWebLayout/style/concrete_style.py 68%2076623 50%7212 63%
pyWebLayout/style/fonts.py 73%1614423 31%322 66%
pyWebLayout/style/page_style.py 100%3504 100%00 100%
Total 85%5525847802 71%1628225 82%
+

+ No items found using the specified filter. +

+
+ + + diff --git a/cov_info/htmlcov/keybd_closed_cb_900cfef5.png b/cov_info/htmlcov/keybd_closed_cb_900cfef5.png new file mode 100644 index 0000000000000000000000000000000000000000..ba119c47df81ed2bbd27a06988abf700139c4f99 GIT binary patch literal 9004 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 literal 0 HcmV?d00001 diff --git a/cov_info/htmlcov/status.json b/cov_info/htmlcov/status.json new file mode 100644 index 0000000..062742b --- /dev/null +++ b/cov_info/htmlcov/status.json @@ -0,0 +1 @@ +{"note":"This file is an internal implementation detail to speed up HTML report generation. Its format can change at any time. You might be looking for the JSON report: https://coverage.rtfd.io/cmd.html#cmd-json","format":5,"version":"7.15.4","globals":"470403f590a21f4b392b9940e1f7e190","files":{"z_20e398e67121d457___init___py":{"hash":"a599847d5e7f5ed0c0a2ae514d2ff093","index":{"url":"z_20e398e67121d457___init___py.html","file":"pyWebLayout/__init__.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":1,"n_excluded":1,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_af715639580e2d86___init___py":{"hash":"4b49f466474b4ceb4141a02173f4fd34","index":{"url":"z_af715639580e2d86___init___py.html","file":"pyWebLayout/abstract/__init__.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":5,"n_excluded":1,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_af715639580e2d86_block_py":{"hash":"be607678684b4df62abd1ed4e847fe15","index":{"url":"z_af715639580e2d86_block_py.html","file":"pyWebLayout/abstract/block.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":489,"n_excluded":119,"n_missing":93,"n_branches":66,"n_partial_branches":6,"n_missing_branches":16}}},"z_af715639580e2d86_document_py":{"hash":"14fd3cacdd2cb5154eb4280f6413267a","index":{"url":"z_af715639580e2d86_document_py.html","file":"pyWebLayout/abstract/document.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":194,"n_excluded":47,"n_missing":35,"n_branches":36,"n_partial_branches":4,"n_missing_branches":16}}},"z_af715639580e2d86_functional_py":{"hash":"601efe3a34ae28b57b68d495fff1ea05","index":{"url":"z_af715639580e2d86_functional_py.html","file":"pyWebLayout/abstract/functional.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":144,"n_excluded":39,"n_missing":3,"n_branches":6,"n_partial_branches":0,"n_missing_branches":0}}},"z_af715639580e2d86_inline_py":{"hash":"8c5706e0f86e8e160e669ff334500510","index":{"url":"z_af715639580e2d86_inline_py.html","file":"pyWebLayout/abstract/inline.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":164,"n_excluded":32,"n_missing":1,"n_branches":44,"n_partial_branches":1,"n_missing_branches":1}}},"z_af715639580e2d86_interactive_image_py":{"hash":"b99629988dcd2418767d48d2d4971f64","index":{"url":"z_af715639580e2d86_interactive_image_py.html","file":"pyWebLayout/abstract/interactive_image.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":34,"n_excluded":7,"n_missing":4,"n_branches":12,"n_partial_branches":3,"n_missing_branches":5}}},"z_7d48e1f4c6486fa2___init___py":{"hash":"f4ecf03135bea453490b21d5d557c54c","index":{"url":"z_7d48e1f4c6486fa2___init___py.html","file":"pyWebLayout/concrete/__init__.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":7,"n_excluded":1,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_7d48e1f4c6486fa2_box_py":{"hash":"424ad1e5ce6ac3b1a217fa178ecf35e6","index":{"url":"z_7d48e1f4c6486fa2_box_py.html","file":"pyWebLayout/concrete/box.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":19,"n_excluded":1,"n_missing":0,"n_branches":2,"n_partial_branches":0,"n_missing_branches":0}}},"z_7d48e1f4c6486fa2_dynamic_page_py":{"hash":"e78d9c73dbc682c5797d4cfdf6d03e2d","index":{"url":"z_7d48e1f4c6486fa2_dynamic_page_py.html","file":"pyWebLayout/concrete/dynamic_page.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":178,"n_excluded":17,"n_missing":42,"n_branches":84,"n_partial_branches":19,"n_missing_branches":41}}},"z_7d48e1f4c6486fa2_functional_py":{"hash":"3f83a2756dbb79d96067f9e61a805c15","index":{"url":"z_7d48e1f4c6486fa2_functional_py.html","file":"pyWebLayout/concrete/functional.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":190,"n_excluded":31,"n_missing":17,"n_branches":32,"n_partial_branches":7,"n_missing_branches":7}}},"z_7d48e1f4c6486fa2_image_py":{"hash":"62ef37c17c4cd0f73e05c50bcea393e8","index":{"url":"z_7d48e1f4c6486fa2_image_py.html","file":"pyWebLayout/concrete/image.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":134,"n_excluded":11,"n_missing":8,"n_branches":36,"n_partial_branches":4,"n_missing_branches":4}}},"z_7d48e1f4c6486fa2_interaction_handler_py":{"hash":"91ff3cddee846ed28c3dcd31b0bfa83a","index":{"url":"z_7d48e1f4c6486fa2_interaction_handler_py.html","file":"pyWebLayout/concrete/interaction_handler.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":99,"n_excluded":14,"n_missing":40,"n_branches":40,"n_partial_branches":8,"n_missing_branches":22}}},"z_7d48e1f4c6486fa2_page_py":{"hash":"e910b695d4cbdd4bc21fc1e50d393a13","index":{"url":"z_7d48e1f4c6486fa2_page_py.html","file":"pyWebLayout/concrete/page.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":176,"n_excluded":31,"n_missing":7,"n_branches":44,"n_partial_branches":5,"n_missing_branches":5}}},"z_7d48e1f4c6486fa2_table_py":{"hash":"c385e259e5c1357b5837fdf93bf40a7d","index":{"url":"z_7d48e1f4c6486fa2_table_py.html","file":"pyWebLayout/concrete/table.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":303,"n_excluded":19,"n_missing":53,"n_branches":102,"n_partial_branches":24,"n_missing_branches":36}}},"z_7d48e1f4c6486fa2_text_py":{"hash":"903b2f27e648982212be402ff8a1ef13","index":{"url":"z_7d48e1f4c6486fa2_text_py.html","file":"pyWebLayout/concrete/text.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":462,"n_excluded":46,"n_missing":87,"n_branches":122,"n_partial_branches":13,"n_missing_branches":47}}},"z_40407af872b0cf37___init___py":{"hash":"7ebb0be46f5002e973fb53099d5d65ed","index":{"url":"z_40407af872b0cf37___init___py.html","file":"pyWebLayout/core/__init__.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":2,"n_excluded":1,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_40407af872b0cf37_base_py":{"hash":"2542b1aa5e005f1a8c1aceb5c9345f38","index":{"url":"z_40407af872b0cf37_base_py.html","file":"pyWebLayout/core/base.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":134,"n_excluded":33,"n_missing":29,"n_branches":30,"n_partial_branches":3,"n_missing_branches":17}}},"z_40407af872b0cf37_cache_py":{"hash":"d763973a18c37176ae28a7c91e2aff3f","index":{"url":"z_40407af872b0cf37_cache_py.html","file":"pyWebLayout/core/cache.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":171,"n_excluded":20,"n_missing":4,"n_branches":44,"n_partial_branches":3,"n_missing_branches":3}}},"z_40407af872b0cf37_callback_registry_py":{"hash":"d5ef1d30746f5bfd45d08ebd6c20dfc8","index":{"url":"z_40407af872b0cf37_callback_registry_py.html","file":"pyWebLayout/core/callback_registry.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":75,"n_excluded":21,"n_missing":4,"n_branches":18,"n_partial_branches":3,"n_missing_branches":3}}},"z_40407af872b0cf37_highlight_py":{"hash":"21d0d8cfd2be6d2d98a3fd2400bb4434","index":{"url":"z_40407af872b0cf37_highlight_py.html","file":"pyWebLayout/core/highlight.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":87,"n_excluded":18,"n_missing":3,"n_branches":12,"n_partial_branches":0,"n_missing_branches":0}}},"z_40407af872b0cf37_persistence_py":{"hash":"3201c94d5f575b749e71f2f55b2d6745","index":{"url":"z_40407af872b0cf37_persistence_py.html","file":"pyWebLayout/core/persistence.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":27,"n_excluded":4,"n_missing":3,"n_branches":2,"n_partial_branches":0,"n_missing_branches":0}}},"z_40407af872b0cf37_query_py":{"hash":"e06a16739be87642799a02067c646382","index":{"url":"z_40407af872b0cf37_query_py.html","file":"pyWebLayout/core/query.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":33,"n_excluded":7,"n_missing":1,"n_branches":2,"n_partial_branches":1,"n_missing_branches":1}}},"z_fc521de9aff00981___init___py":{"hash":"545c980fc8cd9b6015723c0347c8e3d4","index":{"url":"z_fc521de9aff00981___init___py.html","file":"pyWebLayout/io/__init__.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":0,"n_excluded":1,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_263f2e628cef8c50___init___py":{"hash":"44f330033b162a5d67f3bd60249efdbb","index":{"url":"z_263f2e628cef8c50___init___py.html","file":"pyWebLayout/io/readers/__init__.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":2,"n_excluded":1,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_263f2e628cef8c50_epub_reader_py":{"hash":"9dfbcafaf724deb343fe750d91769b8b","index":{"url":"z_263f2e628cef8c50_epub_reader_py.html","file":"pyWebLayout/io/readers/epub_reader.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":286,"n_excluded":18,"n_missing":76,"n_branches":134,"n_partial_branches":27,"n_missing_branches":49}}},"z_263f2e628cef8c50_html_extraction_py":{"hash":"392010f234275a07091883cac226da80","index":{"url":"z_263f2e628cef8c50_html_extraction_py.html","file":"pyWebLayout/io/readers/html_extraction.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":400,"n_excluded":38,"n_missing":28,"n_branches":208,"n_partial_branches":24,"n_missing_branches":36}}},"z_427cc3035faf7633___init___py":{"hash":"52f267141b4d8a6235e29dfa976b14c3","index":{"url":"z_427cc3035faf7633___init___py.html","file":"pyWebLayout/layout/__init__.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":0,"n_excluded":1,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_427cc3035faf7633_document_layouter_py":{"hash":"febd84a07e6d88c655149b960f28a080","index":{"url":"z_427cc3035faf7633_document_layouter_py.html","file":"pyWebLayout/layout/document_layouter.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":219,"n_excluded":16,"n_missing":41,"n_branches":100,"n_partial_branches":15,"n_missing_branches":31}}},"z_427cc3035faf7633_ereader_layout_py":{"hash":"dae3d2db0df992758ac3fa8a0fcec9b3","index":{"url":"z_427cc3035faf7633_ereader_layout_py.html","file":"pyWebLayout/layout/ereader_layout.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":304,"n_excluded":36,"n_missing":21,"n_branches":140,"n_partial_branches":16,"n_missing_branches":22}}},"z_427cc3035faf7633_ereader_manager_py":{"hash":"f665ce23731a8cda3cd1dab5cf881d00","index":{"url":"z_427cc3035faf7633_ereader_manager_py.html","file":"pyWebLayout/layout/ereader_manager.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":370,"n_excluded":66,"n_missing":82,"n_branches":88,"n_partial_branches":8,"n_missing_branches":22}}},"z_427cc3035faf7633_page_buffer_py":{"hash":"4c84e17558534a458b0efe3242ffdd18","index":{"url":"z_427cc3035faf7633_page_buffer_py.html","file":"pyWebLayout/layout/page_buffer.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":110,"n_excluded":19,"n_missing":13,"n_branches":28,"n_partial_branches":2,"n_missing_branches":8}}},"z_427cc3035faf7633_table_optimizer_py":{"hash":"6b5abd20d5dc7f5adb130ecfbcd6300c","index":{"url":"z_427cc3035faf7633_table_optimizer_py.html","file":"pyWebLayout/layout/table_optimizer.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":151,"n_excluded":9,"n_missing":14,"n_branches":68,"n_partial_branches":8,"n_missing_branches":12}}},"z_ba7f6bdeb0188088___init___py":{"hash":"b64cac23e62f4746cb148c83c071cf49","index":{"url":"z_ba7f6bdeb0188088___init___py.html","file":"pyWebLayout/style/__init__.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":6,"n_excluded":1,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_ba7f6bdeb0188088_abstract_style_py":{"hash":"a7174f82eff85b5fb2a6444dfccaeea6","index":{"url":"z_ba7f6bdeb0188088_abstract_style_py.html","file":"pyWebLayout/style/abstract_style.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":135,"n_excluded":22,"n_missing":27,"n_branches":24,"n_partial_branches":7,"n_missing_branches":11}}},"z_ba7f6bdeb0188088_alignment_py":{"hash":"b1b75ecb291fbad4e89fa2ddb72577e6","index":{"url":"z_ba7f6bdeb0188088_alignment_py.html","file":"pyWebLayout/style/alignment.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":11,"n_excluded":3,"n_missing":1,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_ba7f6bdeb0188088_concrete_style_py":{"hash":"cdb91f82cfabc14b52fac47a36673793","index":{"url":"z_ba7f6bdeb0188088_concrete_style_py.html","file":"pyWebLayout/style/concrete_style.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":207,"n_excluded":23,"n_missing":66,"n_branches":72,"n_partial_branches":12,"n_missing_branches":36}}},"z_ba7f6bdeb0188088_fonts_py":{"hash":"f83a7cb40074e9a8653417ed0b088393","index":{"url":"z_ba7f6bdeb0188088_fonts_py.html","file":"pyWebLayout/style/fonts.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":161,"n_excluded":23,"n_missing":44,"n_branches":32,"n_partial_branches":2,"n_missing_branches":22}}},"z_ba7f6bdeb0188088_page_style_py":{"hash":"ccfabd0ab8406c8f6d317c6e5d2d4ad1","index":{"url":"z_ba7f6bdeb0188088_page_style_py.html","file":"pyWebLayout/style/page_style.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":35,"n_excluded":4,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}}}} \ No newline at end of file diff --git a/cov_info/htmlcov/style_cb_4667309f.css b/cov_info/htmlcov/style_cb_4667309f.css new file mode 100644 index 0000000..f82a185 --- /dev/null +++ b/cov_info/htmlcov/style_cb_4667309f.css @@ -0,0 +1,391 @@ +@charset "UTF-8"; +/* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */ +/* For details: https://github.com/coveragepy/coveragepy/blob/main/NOTICE.txt */ +/* Don't edit this .css file. Edit the .scss file instead! */ +html, body, h1, h2, h3, p, table, td, th { margin: 0; padding: 0; border: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } + +body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; font-size: 1em; background: #fff; color: #000; } + +@media (prefers-color-scheme: dark) { body { background: #1e1e1e; } } + +@media (prefers-color-scheme: dark) { body { color: #eee; } } + +html > body { font-size: 16px; } + +a:active, a:focus { outline: 2px dashed #007acc; } + +p { font-size: .875em; line-height: 1.4em; } + +table { border-collapse: collapse; } + +td { vertical-align: top; } + +table tr.hidden { display: none !important; } + +p#no_rows { display: none; font-size: 1.15em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; } + +a.nav { text-decoration: none; color: inherit; } + +a.nav:hover { text-decoration: underline; color: inherit; } + +.hidden { display: none; } + +header { background: #f8f8f8; width: 100%; z-index: 2; border-bottom: 1px solid #ccc; } + +@media (prefers-color-scheme: dark) { header { background: black; } } + +@media (prefers-color-scheme: dark) { header { border-color: #333; } } + +header .content { padding: 1rem 3.5rem; } + +header h2 { margin-top: .5em; font-size: 1em; } + +header h2 a.button { font-family: inherit; font-size: inherit; border: 1px solid; border-radius: .2em; background: #eee; color: inherit; text-decoration: none; padding: .1em .5em; margin: 1px calc(.1em + 1px); cursor: pointer; border-color: #ccc; } + +@media (prefers-color-scheme: dark) { header h2 a.button { background: #333; } } + +@media (prefers-color-scheme: dark) { header h2 a.button { border-color: #444; } } + +header h2 a.button.current { border: 2px solid; background: #fff; border-color: #999; cursor: default; } + +@media (prefers-color-scheme: dark) { header h2 a.button.current { background: #1e1e1e; } } + +@media (prefers-color-scheme: dark) { header h2 a.button.current { border-color: #777; } } + +header p.text { margin: .5em 0 -.5em; color: #666; font-style: italic; } + +@media (prefers-color-scheme: dark) { header p.text { color: #aaa; } } + +header.sticky { position: fixed; left: 0; right: 0; height: 2.5em; } + +header.sticky .text { display: none; } + +header.sticky h1, header.sticky h2 { font-size: 1em; margin-top: 0; display: inline-block; } + +header.sticky .content { padding: 0.5rem 3.5rem; } + +header.sticky .content p { font-size: 1em; } + +header.sticky ~ #source { padding-top: 6.5em; } + +main { position: relative; z-index: 1; } + +footer { margin: 1rem 3.5rem; } + +footer .content { padding: 0; color: #666; font-style: italic; } + +@media (prefers-color-scheme: dark) { footer .content { color: #aaa; } } + +#index { margin: 1rem 0 0 3.5rem; } + +h1 { font-size: 1.25em; display: inline-block; } + +#filter_container { float: right; margin: 0 2em 0 0; line-height: 1.66em; } + +#filter_container #filter { width: 10em; padding: 0.2em 0.5em; border: 2px solid #ccc; background: #fff; color: #000; } + +@media (prefers-color-scheme: dark) { #filter_container #filter { border-color: #444; } } + +@media (prefers-color-scheme: dark) { #filter_container #filter { background: #1e1e1e; } } + +@media (prefers-color-scheme: dark) { #filter_container #filter { color: #eee; } } + +#filter_container #filter:focus { border-color: #007acc; } + +#filter_container :disabled ~ label { color: #ccc; } + +@media (prefers-color-scheme: dark) { #filter_container :disabled ~ label { color: #444; } } + +#filter_container label { font-size: .875em; color: #666; } + +@media (prefers-color-scheme: dark) { #filter_container label { color: #aaa; } } + +header button { font-family: inherit; font-size: inherit; border: 1px solid; border-radius: .2em; background: #eee; color: inherit; text-decoration: none; padding: .1em .5em; margin: 1px calc(.1em + 1px); cursor: pointer; border-color: #ccc; } + +@media (prefers-color-scheme: dark) { header button { background: #333; } } + +@media (prefers-color-scheme: dark) { header button { border-color: #444; } } + +header button:active, header button:focus { outline: 2px dashed #007acc; } + +header button.run { background: #eeffee; } + +@media (prefers-color-scheme: dark) { header button.run { background: #373d29; } } + +header button.run.show_run { background: #dfd; border: 2px solid #00dd00; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { header button.run.show_run { background: #373d29; } } + +header button.mis { background: #ffeeee; } + +@media (prefers-color-scheme: dark) { header button.mis { background: #4b1818; } } + +header button.mis.show_mis { background: #fdd; border: 2px solid #ff0000; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { header button.mis.show_mis { background: #4b1818; } } + +header button.exc { background: #f7f7f7; } + +@media (prefers-color-scheme: dark) { header button.exc { background: #333; } } + +header button.exc.show_exc { background: #eee; border: 2px solid #808080; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { header button.exc.show_exc { background: #333; } } + +header button.par { background: #ffffd5; } + +@media (prefers-color-scheme: dark) { header button.par { background: #650; } } + +header button.par.show_par { background: #ffa; border: 2px solid #bbbb00; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { header button.par.show_par { background: #650; } } + +#help_panel, #source p .annotate.long { display: none; position: absolute; z-index: 999; background: #ffffcc; border: 1px solid #888; border-radius: .2em; color: #333; padding: .25em .5em; } + +#source p .annotate.long { white-space: normal; float: right; top: 1.75em; right: 1em; height: auto; } + +#help_panel_wrapper { float: right; position: relative; } + +#keyboard_icon { margin: 5px; } + +#help_panel_state { display: none; } + +#help_panel { top: 25px; right: 0; padding: .75em; border: 1px solid #883; color: #333; } + +#help_panel .keyhelp p { margin-top: .75em; } + +#help_panel .legend { font-style: italic; margin-bottom: 1em; } + +.indexfile #help_panel { width: 25em; } + +.pyfile #help_panel { width: 18em; } + +#help_panel_state:checked ~ #help_panel { display: block; } + +kbd { border: 1px solid black; border-color: #888 #333 #333 #888; padding: .1em .35em; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-weight: bold; background: #eee; border-radius: 3px; } + +.sep { padding: 0 .1em; } + +#source { padding: 1em 0 1em 3.5rem; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; } + +#source p { position: relative; white-space: pre; } + +#source p * { box-sizing: border-box; } + +#source p .n { float: left; text-align: right; width: 3.5rem; box-sizing: border-box; margin-left: -3.5rem; padding-right: 1em; color: #999; user-select: none; } + +@media (prefers-color-scheme: dark) { #source p .n { color: #777; } } + +#source p .n.highlight { background: #ffdd00; } + +#source p .n a { scroll-margin-top: 6em; text-decoration: none; color: #999; } + +@media (prefers-color-scheme: dark) { #source p .n a { color: #777; } } + +#source p .n a:hover { text-decoration: underline; color: #999; } + +@media (prefers-color-scheme: dark) { #source p .n a:hover { color: #777; } } + +#source p .t { display: inline-block; width: 100%; box-sizing: border-box; margin-left: -.5em; padding-left: 0.3em; border-left: 0.2em solid #fff; } + +@media (prefers-color-scheme: dark) { #source p .t { border-color: #1e1e1e; } } + +#source p .t:hover { background: #f2f2f2; } + +@media (prefers-color-scheme: dark) { #source p .t:hover { background: #282828; } } + +#source p .t:hover ~ .r .annotate.long { display: block; } + +#source p .t .com { color: #008000; font-style: italic; line-height: 1px; } + +@media (prefers-color-scheme: dark) { #source p .t .com { color: #6a9955; } } + +#source p .t .key { font-weight: bold; line-height: 1px; } + +#source p .t .str, #source p .t .fst { color: #0451a5; } + +@media (prefers-color-scheme: dark) { #source p .t .str, #source p .t .fst { color: #9cdcfe; } } + +#source p.mis .t { border-left: 0.2em solid #ff0000; } + +#source p.mis.show_mis .t { background: #fdd; } + +@media (prefers-color-scheme: dark) { #source p.mis.show_mis .t { background: #4b1818; } } + +#source p.mis.show_mis .t:hover { background: #f2d2d2; } + +@media (prefers-color-scheme: dark) { #source p.mis.show_mis .t:hover { background: #532323; } } + +#source p.mis.mis2 .t { border-left: 0.2em dotted #ff0000; } + +#source p.mis.mis2.show_mis .t { background: #ffeeee; } + +@media (prefers-color-scheme: dark) { #source p.mis.mis2.show_mis .t { background: #351b1b; } } + +#source p.mis.mis2.show_mis .t:hover { background: #f2d2d2; } + +@media (prefers-color-scheme: dark) { #source p.mis.mis2.show_mis .t:hover { background: #532323; } } + +#source p.run .t { border-left: 0.2em solid #00dd00; } + +#source p.run.show_run .t { background: #dfd; } + +@media (prefers-color-scheme: dark) { #source p.run.show_run .t { background: #373d29; } } + +#source p.run.show_run .t:hover { background: #d2f2d2; } + +@media (prefers-color-scheme: dark) { #source p.run.show_run .t:hover { background: #404633; } } + +#source p.run.run2 .t { border-left: 0.2em dotted #00dd00; } + +#source p.run.run2.show_run .t { background: #eeffee; } + +@media (prefers-color-scheme: dark) { #source p.run.run2.show_run .t { background: #2b2e24; } } + +#source p.run.run2.show_run .t:hover { background: #d2f2d2; } + +@media (prefers-color-scheme: dark) { #source p.run.run2.show_run .t:hover { background: #404633; } } + +#source p.exc .t { border-left: 0.2em solid #808080; } + +#source p.exc.show_exc .t { background: #eee; } + +@media (prefers-color-scheme: dark) { #source p.exc.show_exc .t { background: #333; } } + +#source p.exc.show_exc .t:hover { background: #e2e2e2; } + +@media (prefers-color-scheme: dark) { #source p.exc.show_exc .t:hover { background: #3c3c3c; } } + +#source p.exc.exc2 .t { border-left: 0.2em dotted #808080; } + +#source p.exc.exc2.show_exc .t { background: #f7f7f7; } + +@media (prefers-color-scheme: dark) { #source p.exc.exc2.show_exc .t { background: #292929; } } + +#source p.exc.exc2.show_exc .t:hover { background: #e2e2e2; } + +@media (prefers-color-scheme: dark) { #source p.exc.exc2.show_exc .t:hover { background: #3c3c3c; } } + +#source p.par .t { border-left: 0.2em solid #bbbb00; } + +#source p.par.show_par .t { background: #ffa; } + +@media (prefers-color-scheme: dark) { #source p.par.show_par .t { background: #650; } } + +#source p.par.show_par .t:hover { background: #f2f2a2; } + +@media (prefers-color-scheme: dark) { #source p.par.show_par .t:hover { background: #6d5d0c; } } + +#source p.par.par2 .t { border-left: 0.2em dotted #bbbb00; } + +#source p.par.par2.show_par .t { background: #ffffd5; } + +@media (prefers-color-scheme: dark) { #source p.par.par2.show_par .t { background: #423a0f; } } + +#source p.par.par2.show_par .t:hover { background: #f2f2a2; } + +@media (prefers-color-scheme: dark) { #source p.par.par2.show_par .t:hover { background: #6d5d0c; } } + +#source p .r { position: absolute; top: 0; right: 2.5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; } + +#source p .annotate { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; color: #666; padding-right: .5em; } + +@media (prefers-color-scheme: dark) { #source p .annotate { color: #ddd; } } + +#source p .annotate.short:hover ~ .long { display: block; } + +#source p .annotate.long { width: 30em; right: 2.5em; } + +#source p input { display: none; } + +#source p input ~ .r label.ctx { cursor: pointer; border-radius: .25em; } + +#source p input ~ .r label.ctx::before { content: "▶ "; } + +#source p input ~ .r label.ctx:hover { background: #e8f4ff; color: #666; } + +@media (prefers-color-scheme: dark) { #source p input ~ .r label.ctx:hover { background: #0f3a42; } } + +@media (prefers-color-scheme: dark) { #source p input ~ .r label.ctx:hover { color: #aaa; } } + +#source p input:checked ~ .r label.ctx { background: #d0e8ff; color: #666; border-radius: .75em .75em 0 0; padding: 0 .5em; margin: -.25em 0; } + +@media (prefers-color-scheme: dark) { #source p input:checked ~ .r label.ctx { background: #056; } } + +@media (prefers-color-scheme: dark) { #source p input:checked ~ .r label.ctx { color: #aaa; } } + +#source p input:checked ~ .r label.ctx::before { content: "▼ "; } + +#source p input:checked ~ .ctxs { padding: .25em .5em; overflow-y: scroll; max-height: 10.5em; } + +#source p label.ctx { color: #999; display: inline-block; padding: 0 .5em; font-size: .8333em; } + +@media (prefers-color-scheme: dark) { #source p label.ctx { color: #777; } } + +#source p .ctxs { display: block; max-height: 0; overflow-y: hidden; transition: all .2s; padding: 0 .5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; white-space: nowrap; background: #d0e8ff; border-radius: .25em; margin-right: 1.75em; text-align: right; } + +@media (prefers-color-scheme: dark) { #source p .ctxs { background: #056; } } + +#index { font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 0.875em; } + +#index table.index { margin-left: -.5em; } + +#index td, #index th { text-align: right; vertical-align: baseline; padding: .25em .5em; border-bottom: 1px solid #eee; } + +@media (prefers-color-scheme: dark) { #index td, #index th { border-color: #333; } } + +#index td.name, #index th.name { text-align: left; width: auto; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; min-width: 15em; } + +#index td.left, #index th.left { text-align: left; } + +#index td.spacer, #index th.spacer { border: none; padding: 0; } + +#index td.spacer:hover, #index th.spacer:hover { background: inherit; } + +#index th { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; font-style: italic; color: #333; border-color: #ccc; cursor: pointer; } + +@media (prefers-color-scheme: dark) { #index th { color: #ddd; } } + +@media (prefers-color-scheme: dark) { #index th { border-color: #444; } } + +#index th:hover { background: #eee; } + +@media (prefers-color-scheme: dark) { #index th:hover { background: #333; } } + +#index th .arrows { color: #666; font-size: 85%; font-family: sans-serif; font-style: normal; pointer-events: none; } + +#index th[aria-sort="ascending"], #index th[aria-sort="descending"] { white-space: nowrap; background: #eee; padding-left: .5em; } + +@media (prefers-color-scheme: dark) { #index th[aria-sort="ascending"], #index th[aria-sort="descending"] { background: #333; } } + +#index th[aria-sort="ascending"] .arrows::after { content: " ▲"; } + +#index th[aria-sort="descending"] .arrows::after { content: " ▼"; } + +#index tr.grouphead th { cursor: default; font-style: normal; border-color: #999; } + +@media (prefers-color-scheme: dark) { #index tr.grouphead th { border-color: #777; } } + +#index td.name { font-size: 1.15em; } + +#index td.name a { text-decoration: none; color: inherit; } + +#index td.name .no-noun { font-style: italic; } + +#index tr.total td, #index tr.total_dynamic td { font-weight: bold; border-bottom: none; } + +#index tr.region:hover { background: #eee; } + +@media (prefers-color-scheme: dark) { #index tr.region:hover { background: #333; } } + +#index tr.region:hover td.name { text-decoration: underline; color: inherit; } + +#scroll_marker { position: fixed; z-index: 3; right: 0; top: 0; width: 16px; height: 100%; background: #fff; border-left: 1px solid #eee; will-change: transform; } + +@media (prefers-color-scheme: dark) { #scroll_marker { background: #1e1e1e; } } + +@media (prefers-color-scheme: dark) { #scroll_marker { border-color: #333; } } + +#scroll_marker .marker { background: #ccc; position: absolute; min-height: 3px; width: 100%; } + +@media (prefers-color-scheme: dark) { #scroll_marker .marker { background: #444; } } diff --git a/cov_info/htmlcov/z_20e398e67121d457___init___py.html b/cov_info/htmlcov/z_20e398e67121d457___init___py.html new file mode 100644 index 0000000..1a012ae --- /dev/null +++ b/cov_info/htmlcov/z_20e398e67121d457___init___py.html @@ -0,0 +1,121 @@ + + + + + Coverage for pyWebLayout/__init__.py: 100% + + + + + +
+
+

+ Coverage for pyWebLayout/__init__.py: + 100% +

+ +

+ 1 statements   + + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.15.4, + created at 2026-08-08 20:34 +0000 +

+ +
+
+
+

1""" 

+

2PyWebLayout - A Python library for HTML-like layout and rendering. 

+

3 

+

4This library provides classes for rendering HTML-like content to images 

+

5using a box-based layout system. It includes support for text, tables, 

+

6and containers, as well as parsers for HTML and EPUB content. It also 

+

7supports pagination for ebook-like content with the ability to pause, 

+

8save state, and resume rendering. 

+

9""" 

+

10 

+

11__version__ = '0.1.1' 

+

12 

+

13# Core abstractions 

+

14 

+

15# Style components 

+

16 

+

17 

+

18# Abstract document model 

+

19 

+

20# Concrete implementations 

+

21 

+

22# Abstract components 

+
+ + + diff --git a/cov_info/htmlcov/z_263f2e628cef8c50___init___py.html b/cov_info/htmlcov/z_263f2e628cef8c50___init___py.html new file mode 100644 index 0000000..34517c6 --- /dev/null +++ b/cov_info/htmlcov/z_263f2e628cef8c50___init___py.html @@ -0,0 +1,116 @@ + + + + + Coverage for pyWebLayout/io/readers/__init__.py: 100% + + + + + +
+
+

+ Coverage for pyWebLayout/io/readers/__init__.py: + 100% +

+ +

+ 2 statements   + + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.15.4, + created at 2026-08-08 20:34 +0000 +

+ +
+
+
+

1""" 

+

2Readers module for pyWebLayout. 

+

3 

+

4This module provides specialized readers for different document formats. 

+

5""" 

+

6 

+

7# EPUB readers 

+

8from .epub_reader import read_epub # Legacy 

+

9 

+

10 

+

11__all__ = [ 

+

12 # HTML readers 

+

13 'read_html', 'read_html_file', 'parse_html_string', 

+

14 

+

15 # EPUB readers 

+

16 'read_epub', 

+

17] 

+
+ + + diff --git a/cov_info/htmlcov/z_263f2e628cef8c50_epub_reader_py.html b/cov_info/htmlcov/z_263f2e628cef8c50_epub_reader_py.html new file mode 100644 index 0000000..0710d4e --- /dev/null +++ b/cov_info/htmlcov/z_263f2e628cef8c50_epub_reader_py.html @@ -0,0 +1,702 @@ + + + + + Coverage for pyWebLayout/io/readers/epub_reader.py: 70% + + + + + +
+
+

+ Coverage for pyWebLayout/io/readers/epub_reader.py: + 70% +

+ +

+ 286 statements   + + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.15.4, + created at 2026-08-08 20:34 +0000 +

+ +
+
+
+

1""" 

+

2EPUB reader for pyWebLayout. 

+

3 

+

4This module provides functionality for reading EPUB documents and converting them 

+

5to pyWebLayout's abstract document model. 

+

6""" 

+

7 

+

8import os 

+

9import zipfile 

+

10import tempfile 

+

11from typing import Dict, List, Optional, Any, Callable 

+

12import xml.etree.ElementTree as ET 

+

13import urllib.parse 

+

14from PIL import Image as PILImage, ImageOps 

+

15 

+

16from pyWebLayout.abstract.document import Book, Chapter, MetadataType 

+

17from pyWebLayout.abstract.block import PageBreak 

+

18from pyWebLayout.io.readers.html_extraction import parse_html_string 

+

19 

+

20 

+

21# XML namespaces used in EPUB files 

+

22NAMESPACES = { 

+

23 'opf': 'http://www.idpf.org/2007/opf', 

+

24 'dc': 'http://purl.org/dc/elements/1.1/', 

+

25 'dcterms': 'http://purl.org/dc/terms/', 

+

26 'xhtml': 'http://www.w3.org/1999/xhtml', 

+

27 'ncx': 'http://www.daisy.org/z3986/2005/ncx/', 

+

28} 

+

29 

+

30 

+

31def default_eink_processor(img: PILImage.Image) -> PILImage.Image: 

+

32 """ 

+

33 Process image for 4-bit e-ink display using PIL only. 

+

34 Applies histogram equalization and 4-bit quantization. 

+

35 

+

36 Args: 

+

37 img: PIL Image to process 

+

38 

+

39 Returns: 

+

40 Processed PIL Image in L mode (grayscale) with 4-bit quantization 

+

41 """ 

+

42 # Convert to grayscale if needed 

+

43 if img.mode != 'L': 

+

44 img = img.convert('L') 

+

45 

+

46 # Apply histogram equalization for contrast enhancement 

+

47 img = ImageOps.equalize(img) 

+

48 

+

49 # Quantize to 4-bit (16 grayscale levels: 0, 17, 34, ..., 255) 

+

50 img = img.point(lambda x: (x // 16) * 17) 

+

51 

+

52 return img 

+

53 

+

54 

+

55class EPUBReader: 

+

56 """ 

+

57 Reader for EPUB documents. 

+

58 

+

59 This class extracts content from EPUB files and converts it to 

+

60 pyWebLayout's abstract document model. 

+

61 """ 

+

62 

+

63 def __init__(self, epub_path: str, image_processor: Optional[Callable[[ 

+

64 PILImage.Image], PILImage.Image]] = default_eink_processor): 

+

65 """ 

+

66 Initialize an EPUB reader. 

+

67 

+

68 Args: 

+

69 epub_path: Path to the EPUB file 

+

70 image_processor: Optional function to process images for display optimization. 

+

71 Defaults to default_eink_processor for 4-bit e-ink displays. 

+

72 Set to None to disable image processing. 

+

73 Custom processor should accept and return a PIL Image. 

+

74 """ 

+

75 self.epub_path = epub_path 

+

76 self.image_processor = image_processor 

+

77 self.book = Book() 

+

78 self.temp_dir = None 

+

79 self.content_dir = None 

+

80 self.metadata = {} 

+

81 self.toc = [] 

+

82 self.spine = [] 

+

83 self.manifest = {} 

+

84 self.cover_id = None # ID of the cover image in manifest 

+

85 

+

86 def read(self) -> Book: 

+

87 """ 

+

88 Read the EPUB file and convert it to a Book. 

+

89 

+

90 Returns: 

+

91 Book: The parsed book 

+

92 """ 

+

93 try: 

+

94 # Extract the EPUB file 

+

95 self.temp_dir = tempfile.mkdtemp() 

+

96 self._extract_epub() 

+

97 self._parse_package_document() 

+

98 self._parse_toc() 

+

99 self._create_book() 

+

100 

+

101 # Add chapters to the book 

+

102 self._add_chapters() 

+

103 

+

104 # Process images for e-ink display optimization 

+

105 self._process_content_images() 

+

106 

+

107 return self.book 

+

108 

+

109 finally: 

+

110 # Clean up temporary files 

+

111 if self.temp_dir: 

+

112 import shutil 

+

113 shutil.rmtree(self.temp_dir, ignore_errors=True) 

+

114 

+

115 def _extract_epub(self): 

+

116 """Extract the EPUB file to a temporary directory.""" 

+

117 with zipfile.ZipFile(self.epub_path, 'r') as zip_ref: 

+

118 zip_ref.extractall(self.temp_dir) 

+

119 

+

120 # Find the content directory (typically OEBPS or OPS) 

+

121 container_path = os.path.join(self.temp_dir, 'META-INF', 'container.xml') 

+

122 if os.path.exists(container_path): 122 ↛ 136line 122 didn't jump to line 136 because the condition on line 122 was always true

+

123 tree = ET.parse(container_path) 

+

124 root = tree.getroot() 

+

125 

+

126 # Get the path to the package document (content.opf) 

+

127 for rootfile in root.findall( 127 ↛ 136line 127 didn't jump to line 136 because the loop on line 127 didn't complete

+

128 './/{urn:oasis:names:tc:opendocument:xmlns:container}rootfile'): 

+

129 full_path = rootfile.get('full-path') 

+

130 if full_path: 130 ↛ 127line 130 didn't jump to line 127 because the condition on line 130 was always true

+

131 self.content_dir = os.path.dirname( 

+

132 os.path.join(self.temp_dir, full_path)) 

+

133 return 

+

134 

+

135 # Fallback: look for common content directories 

+

136 for content_dir in ['OEBPS', 'OPS', 'Content']: 

+

137 if os.path.exists(os.path.join(self.temp_dir, content_dir)): 

+

138 self.content_dir = os.path.join(self.temp_dir, content_dir) 

+

139 return 

+

140 

+

141 # If no content directory found, use the root 

+

142 self.content_dir = self.temp_dir 

+

143 

+

144 def _parse_package_document(self): 

+

145 """Parse the package document (content.opf).""" 

+

146 # Find the package document 

+

147 opf_path = None 

+

148 for root, dirs, files in os.walk(self.content_dir): 148 ↛ 156line 148 didn't jump to line 156 because the loop on line 148 didn't complete

+

149 for file in files: 149 ↛ 153line 149 didn't jump to line 153 because the loop on line 149 didn't complete

+

150 if file.endswith('.opf'): 

+

151 opf_path = os.path.join(root, file) 

+

152 break 

+

153 if opf_path: 153 ↛ 148line 153 didn't jump to line 148 because the condition on line 153 was always true

+

154 break 

+

155 

+

156 if not opf_path: 156 ↛ 157line 156 didn't jump to line 157 because the condition on line 156 was never true

+

157 raise ValueError("No package document (.opf) found in EPUB") 

+

158 

+

159 # Parse the package document 

+

160 tree = ET.parse(opf_path) 

+

161 root = tree.getroot() 

+

162 

+

163 # Parse metadata 

+

164 self._parse_metadata(root) 

+

165 

+

166 # Parse manifest 

+

167 self._parse_manifest(root) 

+

168 

+

169 # Parse spine 

+

170 self._parse_spine(root) 

+

171 

+

172 def _parse_metadata(self, root: ET.Element): 

+

173 """ 

+

174 Parse metadata from the package document. 

+

175 

+

176 Args: 

+

177 root: Root element of the package document 

+

178 """ 

+

179 # Find the metadata element 

+

180 metadata_elem = root.find('.//{{{0}}}metadata'.format(NAMESPACES['opf'])) 

+

181 if metadata_elem is None: 181 ↛ 182line 181 didn't jump to line 182 because the condition on line 181 was never true

+

182 return 

+

183 

+

184 # Parse DC metadata 

+

185 for elem in metadata_elem: 

+

186 if elem.tag.startswith('{{{0}}}'.format(NAMESPACES['dc'])): 

+

187 # Get the local name (without namespace) 

+

188 name = elem.tag.split('}', 1)[1] 

+

189 value = elem.text 

+

190 

+

191 if name == 'title': 

+

192 self.metadata['title'] = value 

+

193 elif name == 'creator': 

+

194 self.metadata['creator'] = value 

+

195 elif name == 'language': 

+

196 self.metadata['language'] = value 

+

197 elif name == 'description': 

+

198 self.metadata['description'] = value 

+

199 elif name == 'subject': 

+

200 if 'subjects' not in self.metadata: 

+

201 self.metadata['subjects'] = [] 

+

202 self.metadata['subjects'].append(value) 

+

203 elif name == 'date': 

+

204 self.metadata['date'] = value 

+

205 elif name == 'identifier': 

+

206 self.metadata['identifier'] = value 

+

207 elif name == 'publisher': 

+

208 self.metadata['publisher'] = value 

+

209 else: 

+

210 # Store other metadata 

+

211 self.metadata[name] = value 

+

212 

+

213 # Parse meta elements for cover reference 

+

214 for meta in metadata_elem.findall('.//{{{0}}}meta'.format(NAMESPACES['opf'])): 

+

215 name = meta.get('name') 

+

216 content = meta.get('content') 

+

217 

+

218 if name == 'cover' and content: 218 ↛ 220line 218 didn't jump to line 220 because the condition on line 218 was never true

+

219 # This is a reference to the cover image in the manifest 

+

220 self.cover_id = content 

+

221 

+

222 def _parse_manifest(self, root: ET.Element): 

+

223 """ 

+

224 Parse manifest from the package document. 

+

225 

+

226 Args: 

+

227 root: Root element of the package document 

+

228 """ 

+

229 # Find the manifest element 

+

230 manifest_elem = root.find('.//{{{0}}}manifest'.format(NAMESPACES['opf'])) 

+

231 if manifest_elem is None: 231 ↛ 232line 231 didn't jump to line 232 because the condition on line 231 was never true

+

232 return 

+

233 

+

234 # Parse items 

+

235 for item in manifest_elem.findall('.//{{{0}}}item'.format(NAMESPACES['opf'])): 

+

236 id = item.get('id') 

+

237 href = item.get('href') 

+

238 media_type = item.get('media-type') 

+

239 

+

240 if id and href: 240 ↛ 235line 240 didn't jump to line 235 because the condition on line 240 was always true

+

241 # Resolve relative path 

+

242 href = urllib.parse.unquote(href) 

+

243 path = os.path.normpath(os.path.join(self.content_dir, href)) 

+

244 

+

245 self.manifest[id] = { 

+

246 'href': href, 

+

247 'path': path, 

+

248 'media_type': media_type 

+

249 } 

+

250 

+

251 def _parse_spine(self, root: ET.Element): 

+

252 """ 

+

253 Parse spine from the package document. 

+

254 

+

255 Args: 

+

256 root: Root element of the package document 

+

257 """ 

+

258 # Find the spine element 

+

259 spine_elem = root.find('.//{{{0}}}spine'.format(NAMESPACES['opf'])) 

+

260 if spine_elem is None: 260 ↛ 261line 260 didn't jump to line 261 because the condition on line 260 was never true

+

261 return 

+

262 

+

263 # Get the toc attribute (NCX file ID) 

+

264 toc_id = spine_elem.get('toc') 

+

265 if toc_id and toc_id in self.manifest: 265 ↛ 269line 265 didn't jump to line 269 because the condition on line 265 was always true

+

266 self.toc_path = self.manifest[toc_id]['path'] 

+

267 

+

268 # Parse itemrefs 

+

269 for itemref in spine_elem.findall( 

+

270 './/{{{0}}}itemref'.format(NAMESPACES['opf'])): 

+

271 idref = itemref.get('idref') 

+

272 if idref and idref in self.manifest: 272 ↛ 269line 272 didn't jump to line 269 because the condition on line 272 was always true

+

273 self.spine.append(idref) 

+

274 

+

275 def _parse_toc(self): 

+

276 """Parse the table of contents.""" 

+

277 if not hasattr( 277 ↛ 282line 277 didn't jump to line 282 because the condition on line 277 was never true

+

278 self, 

+

279 'toc_path') or not self.toc_path or not os.path.exists( 

+

280 self.toc_path): 

+

281 # Try to find the toc.ncx file 

+

282 for root, dirs, files in os.walk(self.content_dir): 

+

283 for file in files: 

+

284 if file.endswith('.ncx'): 

+

285 self.toc_path = os.path.join(root, file) 

+

286 break 

+

287 if hasattr(self, 'toc_path') and self.toc_path: 

+

288 break 

+

289 

+

290 if not hasattr( 290 ↛ 295line 290 didn't jump to line 295 because the condition on line 290 was never true

+

291 self, 

+

292 'toc_path') or not self.toc_path or not os.path.exists( 

+

293 self.toc_path): 

+

294 # No TOC found 

+

295 return 

+

296 

+

297 # Parse the NCX file 

+

298 tree = ET.parse(self.toc_path) 

+

299 root = tree.getroot() 

+

300 

+

301 # Parse navMap 

+

302 nav_map = root.find('.//{{{0}}}navMap'.format(NAMESPACES['ncx'])) 

+

303 if nav_map is None: 303 ↛ 304line 303 didn't jump to line 304 because the condition on line 303 was never true

+

304 return 

+

305 

+

306 # Parse navPoints 

+

307 self._parse_nav_points(nav_map, []) 

+

308 

+

309 def _parse_nav_points(self, parent: ET.Element, path: List[Dict[str, Any]]): 

+

310 """ 

+

311 Recursively parse navPoints from the NCX file. 

+

312 

+

313 Args: 

+

314 parent: Parent element containing navPoints 

+

315 path: Current path in the TOC hierarchy 

+

316 """ 

+

317 for nav_point in parent.findall('.//{{{0}}}navPoint'.format(NAMESPACES['ncx'])): 

+

318 # Get navPoint attributes 

+

319 id = nav_point.get('id') 

+

320 play_order = nav_point.get('playOrder') 

+

321 

+

322 # Get navLabel 

+

323 nav_label = nav_point.find('.//{{{0}}}navLabel'.format(NAMESPACES['ncx'])) 

+

324 text_elem = nav_label.find( 

+

325 './/{{{0}}}text'.format(NAMESPACES['ncx'])) if nav_label else None 

+

326 label = text_elem.text if text_elem is not None else "" 

+

327 

+

328 # Get content 

+

329 content = nav_point.find('.//{{{0}}}content'.format(NAMESPACES['ncx'])) 

+

330 src = content.get('src') if content is not None else "" 

+

331 

+

332 # Create a TOC entry 

+

333 entry = { 

+

334 'id': id, 

+

335 'label': label, 

+

336 'src': src, 

+

337 'play_order': play_order, 

+

338 'children': [] 

+

339 } 

+

340 

+

341 # Add to TOC 

+

342 if path: 342 ↛ 343line 342 didn't jump to line 343 because the condition on line 342 was never true

+

343 path[-1]['children'].append(entry) 

+

344 else: 

+

345 self.toc.append(entry) 

+

346 

+

347 # Parse child navPoints 

+

348 self._parse_nav_points(nav_point, path + [entry]) 

+

349 

+

350 def _create_book(self): 

+

351 """Create a Book object from the parsed metadata.""" 

+

352 # Set book metadata 

+

353 if 'title' in self.metadata: 353 ↛ 356line 353 didn't jump to line 356 because the condition on line 353 was always true

+

354 self.book.set_title(self.metadata['title']) 

+

355 

+

356 if 'creator' in self.metadata: 

+

357 self.book.set_metadata(MetadataType.AUTHOR, self.metadata['creator']) 

+

358 

+

359 if 'language' in self.metadata: 

+

360 self.book.set_metadata(MetadataType.LANGUAGE, self.metadata['language']) 

+

361 

+

362 if 'description' in self.metadata: 

+

363 self.book.set_metadata( 

+

364 MetadataType.DESCRIPTION, 

+

365 self.metadata['description']) 

+

366 

+

367 if 'subjects' in self.metadata: 

+

368 self.book.set_metadata( 

+

369 MetadataType.KEYWORDS, ', '.join( 

+

370 self.metadata['subjects'])) 

+

371 

+

372 if 'date' in self.metadata: 

+

373 self.book.set_metadata(MetadataType.PUBLICATION_DATE, self.metadata['date']) 

+

374 

+

375 if 'identifier' in self.metadata: 375 ↛ 378line 375 didn't jump to line 378 because the condition on line 375 was always true

+

376 self.book.set_metadata(MetadataType.IDENTIFIER, self.metadata['identifier']) 

+

377 

+

378 if 'publisher' in self.metadata: 

+

379 self.book.set_metadata(MetadataType.PUBLISHER, self.metadata['publisher']) 

+

380 

+

381 def _add_cover_chapter(self): 

+

382 """Add a cover chapter if a cover image is available.""" 

+

383 if not self.cover_id or self.cover_id not in self.manifest: 383 ↛ 387line 383 didn't jump to line 387 because the condition on line 383 was always true

+

384 return 

+

385 

+

386 # Get the cover image path from the manifest 

+

387 cover_item = self.manifest[self.cover_id] 

+

388 cover_path = cover_item['path'] 

+

389 

+

390 # Check if the file exists 

+

391 if not os.path.exists(cover_path): 

+

392 print(f"Warning: Cover image file not found: {cover_path}") 

+

393 return 

+

394 

+

395 # Create a cover chapter 

+

396 cover_chapter = self.book.create_chapter("Cover", 0) 

+

397 

+

398 try: 

+

399 # Create an Image block for the cover 

+

400 from pyWebLayout.abstract.block import Image as AbstractImage 

+

401 from PIL import Image as PILImage 

+

402 import io 

+

403 

+

404 # Load the image into memory before the temp directory is cleaned up 

+

405 # We need to fully copy the image data to ensure it persists after temp 

+

406 # cleanup 

+

407 with open(cover_path, 'rb') as f: 

+

408 image_bytes = f.read() 

+

409 

+

410 # Create PIL image from bytes in memory 

+

411 pil_image = PILImage.open(io.BytesIO(image_bytes)) 

+

412 pil_image.load() # Force loading into memory 

+

413 

+

414 # Create a copy to ensure all data is in memory 

+

415 pil_image = pil_image.copy() 

+

416 

+

417 # Apply image processing if enabled 

+

418 if self.image_processor: 

+

419 try: 

+

420 pil_image = self.image_processor(pil_image) 

+

421 except Exception as e: 

+

422 print(f"Warning: Image processing failed for cover: {str(e)}") 

+

423 # Continue with unprocessed image 

+

424 

+

425 # Create an AbstractImage block with the cover image path 

+

426 cover_image = AbstractImage(source=cover_path, alt_text="Cover Image") 

+

427 

+

428 # Set dimensions from the loaded image 

+

429 cover_image._width = pil_image.width 

+

430 cover_image._height = pil_image.height 

+

431 

+

432 # Store the loaded PIL image in the abstract image so it persists after 

+

433 # temp cleanup 

+

434 cover_image._loaded_image = pil_image 

+

435 

+

436 # Add the image to the cover chapter 

+

437 cover_chapter.add_block(cover_image) 

+

438 

+

439 except Exception as e: 

+

440 print(f"Error creating cover chapter: {str(e)}") 

+

441 import traceback 

+

442 traceback.print_exc() 

+

443 # If we can't create the cover image, remove the chapter 

+

444 if hasattr(self.book, 'chapters') and cover_chapter in self.book.chapters: 

+

445 self.book.chapters.remove(cover_chapter) 

+

446 

+

447 def _process_chapter_images(self, chapter: Chapter): 

+

448 """ 

+

449 Load and process images in a single chapter. 

+

450 

+

451 This method loads images from disk into memory and applies image processing. 

+

452 Images must be loaded before the temporary EPUB directory is cleaned up. 

+

453 

+

454 Args: 

+

455 chapter: The chapter containing images to process 

+

456 """ 

+

457 from pyWebLayout.abstract.block import Image as AbstractImage 

+

458 from PIL import Image as PILImage 

+

459 import io 

+

460 

+

461 for block in chapter.blocks: 

+

462 if isinstance(block, AbstractImage): 

+

463 # Load image into memory if not already loaded 

+

464 if not hasattr(block, '_loaded_image') or not block._loaded_image: 464 ↛ 485line 464 didn't jump to line 485 because the condition on line 464 was always true

+

465 try: 

+

466 # Load the image from the source path 

+

467 if os.path.isfile(block.source): 467 ↛ 485line 467 didn't jump to line 485 because the condition on line 467 was always true

+

468 with open(block.source, 'rb') as f: 

+

469 image_bytes = f.read() 

+

470 # Create PIL image from bytes in memory 

+

471 pil_image = PILImage.open(io.BytesIO(image_bytes)) 

+

472 pil_image.load() # Force loading into memory 

+

473 block._loaded_image = pil_image.copy() # Create a copy to ensure it persists 

+

474 

+

475 # Set width and height on the block from the loaded image 

+

476 # This is required for layout calculations 

+

477 block._width = pil_image.width 

+

478 block._height = pil_image.height 

+

479 except Exception as e: 

+

480 print(f"Warning: Failed to load image '{block.source}': {str(e)}") 

+

481 # Continue without the image 

+

482 continue 

+

483 

+

484 # Apply image processing if enabled and image is loaded 

+

485 if self.image_processor and hasattr(block, '_loaded_image') and block._loaded_image: 

+

486 try: 

+

487 block._loaded_image = self.image_processor(block._loaded_image) 

+

488 except Exception as e: 

+

489 print( 

+

490 f"Warning: Image processing failed for image '{block.alt_text}': {str(e)}" 

+

491 ) 

+

492 # Continue with unprocessed image 

+

493 

+

494 def _process_content_images(self): 

+

495 """ 

+

496 Load all images into memory and apply image processing. 

+

497 

+

498 This must be called before the temporary EPUB directory is cleaned up, 

+

499 to ensure images are loaded from disk into memory. 

+

500 """ 

+

501 for chapter in self.book.chapters: 

+

502 self._process_chapter_images(chapter) 

+

503 

+

504 def _add_chapters(self): 

+

505 """Add chapters to the book based on the spine and TOC.""" 

+

506 # Add cover chapter first if available 

+

507 self._add_cover_chapter() 

+

508 

+

509 # Create a mapping from src to TOC entry 

+

510 toc_map = {} 

+

511 

+

512 def add_to_toc_map(entries): 

+

513 for entry in entries: 

+

514 if entry['src']: 514 ↛ 521line 514 didn't jump to line 521 because the condition on line 514 was always true

+

515 # Extract the path part of the src (remove fragment) 

+

516 src_parts = entry['src'].split('#', 1) 

+

517 path = src_parts[0] 

+

518 toc_map[path] = entry 

+

519 

+

520 # Process children 

+

521 if entry['children']: 521 ↛ 522line 521 didn't jump to line 522 because the condition on line 521 was never true

+

522 add_to_toc_map(entry['children']) 

+

523 

+

524 add_to_toc_map(self.toc) 

+

525 

+

526 # Process spine items 

+

527 # Start from chapter_index = 1 if cover was added, otherwise 0 

+

528 chapter_index = 1 if (self.cover_id and self.cover_id in self.manifest) else 0 

+

529 for i, idref in enumerate(self.spine): 

+

530 if idref not in self.manifest: 530 ↛ 531line 530 didn't jump to line 531 because the condition on line 530 was never true

+

531 continue 

+

532 

+

533 item = self.manifest[idref] 

+

534 path = item['path'] 

+

535 href = item['href'] 

+

536 

+

537 # Skip navigation files 

+

538 if (idref == 'nav' or 

+

539 item.get('media_type') == 'application/xhtml+xml' and 

+

540 ('nav' in href.lower() or 'toc' in href.lower())): 

+

541 continue 

+

542 

+

543 # Check if this item is in the TOC 

+

544 chapter_title = None 

+

545 if href in toc_map: 545 ↛ 549line 545 didn't jump to line 549 because the condition on line 545 was always true

+

546 chapter_title = toc_map[href]['label'] 

+

547 

+

548 # Create a chapter 

+

549 chapter_index += 1 

+

550 chapter = self.book.create_chapter(chapter_title, chapter_index) 

+

551 

+

552 # Parse the HTML content 

+

553 try: 

+

554 # Read the HTML file 

+

555 with open(path, 'r', encoding='utf-8') as f: 

+

556 html = f.read() 

+

557 

+

558 # Get the directory of the HTML file for resolving relative paths 

+

559 html_dir = os.path.dirname(path) 

+

560 

+

561 # Parse HTML and add blocks to chapter, passing base_path for image resolution 

+

562 blocks = parse_html_string(html, document=self.book, base_path=html_dir) 

+

563 

+

564 # Copy blocks to the chapter 

+

565 for block in blocks: 

+

566 chapter.add_block(block) 

+

567 

+

568 # Add a PageBreak after the chapter to ensure next chapter starts on new page 

+

569 # This helps maintain chapter boundaries during pagination 

+

570 chapter.add_block(PageBreak()) 

+

571 

+

572 except Exception as e: 

+

573 print(f"Error parsing chapter {i + 1}: {str(e)}") 

+

574 # Add an error message block 

+

575 from pyWebLayout.abstract.block import Paragraph 

+

576 from pyWebLayout.abstract.inline import Word 

+

577 from pyWebLayout.style import Font 

+

578 error_para = Paragraph() 

+

579 # Create a default font style for the error message 

+

580 default_font = Font() 

+

581 error_para.add_word( 

+

582 Word( 

+

583 f"Error loading chapter: {str(e)}", 

+

584 default_font 

+

585 ) 

+

586 ) 

+

587 chapter.add_block(error_para) 

+

588 # Still add PageBreak even after error 

+

589 chapter.add_block(PageBreak()) 

+

590 

+

591 

+

592def read_epub(epub_path: str) -> Book: 

+

593 """ 

+

594 Read an EPUB file and convert it to a Book. 

+

595 

+

596 Args: 

+

597 epub_path: Path to the EPUB file 

+

598 

+

599 Returns: 

+

600 Book: The parsed book 

+

601 """ 

+

602 reader = EPUBReader(epub_path) 

+

603 return reader.read() 

+
+ + + diff --git a/cov_info/htmlcov/z_263f2e628cef8c50_html_extraction_py.html b/cov_info/htmlcov/z_263f2e628cef8c50_html_extraction_py.html new file mode 100644 index 0000000..009e038 --- /dev/null +++ b/cov_info/htmlcov/z_263f2e628cef8c50_html_extraction_py.html @@ -0,0 +1,1067 @@ + + + + + Coverage for pyWebLayout/io/readers/html_extraction.py: 89% + + + + + +
+
+

+ Coverage for pyWebLayout/io/readers/html_extraction.py: + 89% +

+ +

+ 400 statements   + + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.15.4, + created at 2026-08-08 20:34 +0000 +

+ +
+
+
+

1""" 

+

2HTML extraction module for converting HTML elements to pyWebLayout abstract elements. 

+

3 

+

4This module provides handler functions for converting HTML elements into the abstract document structure 

+

5used by pyWebLayout, including paragraphs, headings, lists, tables, and inline formatting. 

+

6Each handler function has a robust signature that handles style hints, CSS classes, and attributes. 

+

7""" 

+

8 

+

9from typing import List, Dict, Any, Optional, Union, Callable, Tuple, NamedTuple 

+

10from bs4 import BeautifulSoup, Tag, NavigableString 

+

11from bs4.element import CData, Comment, Doctype, ProcessingInstruction 

+

12from pyWebLayout.abstract.inline import Word 

+

13from pyWebLayout.abstract.block import ( 

+

14 Block, 

+

15 Paragraph, 

+

16 Heading, 

+

17 HeadingLevel, 

+

18 Quote, 

+

19 CodeBlock, 

+

20 HList, 

+

21 ListItem, 

+

22 ListStyle, 

+

23 Table, 

+

24 TableRow, 

+

25 TableCell, 

+

26 HorizontalRule, 

+

27 Image, 

+

28) 

+

29from pyWebLayout.style import Font, FontWeight, FontStyle, TextDecoration 

+

30 

+

31 

+

32class StyleContext(NamedTuple): 

+

33 """ 

+

34 Immutable style context passed to handler functions. 

+

35 Contains all styling information including inherited styles, CSS hints, and element attributes. 

+

36 """ 

+

37 

+

38 font: Font 

+

39 background: Optional[Tuple[int, int, int, int]] 

+

40 css_classes: set 

+

41 css_styles: Dict[str, str] 

+

42 element_attributes: Dict[str, Any] 

+

43 parent_elements: List[str] # Stack of parent element names 

+

44 document: Optional[Any] # Reference to document for font registry 

+

45 base_path: Optional[str] = None # Base path for resolving relative URLs 

+

46 

+

47 def with_font(self, font: Font) -> "StyleContext": 

+

48 """Create new context with modified font.""" 

+

49 return self._replace(font=font) 

+

50 

+

51 def with_background( 

+

52 self, background: Optional[Tuple[int, int, int, int]] 

+

53 ) -> "StyleContext": 

+

54 """Create new context with modified background.""" 

+

55 return self._replace(background=background) 

+

56 

+

57 def with_css_classes(self, css_classes: set) -> "StyleContext": 

+

58 """Create new context with modified CSS classes.""" 

+

59 return self._replace(css_classes=css_classes) 

+

60 

+

61 def with_css_styles(self, css_styles: Dict[str, str]) -> "StyleContext": 

+

62 """Create new context with modified CSS styles.""" 

+

63 return self._replace(css_styles=css_styles) 

+

64 

+

65 def with_attributes(self, attributes: Dict[str, Any]) -> "StyleContext": 

+

66 """Create new context with modified element attributes.""" 

+

67 return self._replace(element_attributes=attributes) 

+

68 

+

69 def push_element(self, element_name: str) -> "StyleContext": 

+

70 """Create new context with element pushed onto parent stack.""" 

+

71 return self._replace(parent_elements=self.parent_elements + [element_name]) 

+

72 

+

73 

+

74def create_base_context( 

+

75 base_font: Optional[Font] = None, 

+

76 document=None, 

+

77 base_path: Optional[str] = None) -> StyleContext: 

+

78 """ 

+

79 Create a base style context with default values. 

+

80 

+

81 Args: 

+

82 base_font: Base font to use, defaults to system default 

+

83 document: Document instance for font registry 

+

84 base_path: Base directory path for resolving relative URLs 

+

85 

+

86 Returns: 

+

87 StyleContext with default values 

+

88 """ 

+

89 # Use document's font registry if available, otherwise create default font 

+

90 if base_font is None: 

+

91 if document and hasattr(document, 'get_or_create_font'): 

+

92 base_font = document.get_or_create_font() 

+

93 else: 

+

94 base_font = Font() 

+

95 

+

96 return StyleContext( 

+

97 font=base_font, 

+

98 background=None, 

+

99 css_classes=set(), 

+

100 css_styles={}, 

+

101 element_attributes={}, 

+

102 parent_elements=[], 

+

103 document=document, 

+

104 base_path=base_path, 

+

105 ) 

+

106 

+

107 

+

108def apply_element_styling(context: StyleContext, element: Tag) -> StyleContext: 

+

109 """ 

+

110 Apply element-specific styling to context based on HTML element and attributes. 

+

111 

+

112 Args: 

+

113 context: Current style context 

+

114 element: BeautifulSoup Tag object 

+

115 

+

116 Returns: 

+

117 New StyleContext with applied styling 

+

118 """ 

+

119 tag_name = element.name.lower() 

+

120 attributes = dict(element.attrs) if element.attrs else {} 

+

121 

+

122 # Start with current context 

+

123 new_context = context.with_attributes(attributes).push_element(tag_name) 

+

124 

+

125 # Apply CSS classes 

+

126 css_classes = new_context.css_classes.copy() 

+

127 if "class" in attributes: 

+

128 classes = ( 

+

129 attributes["class"].split() 

+

130 if isinstance(attributes["class"], str) 

+

131 else attributes["class"] 

+

132 ) 

+

133 css_classes.update(classes) 

+

134 new_context = new_context.with_css_classes(css_classes) 

+

135 

+

136 # Apply inline styles 

+

137 css_styles = new_context.css_styles.copy() 

+

138 if "style" in attributes: 

+

139 inline_styles = parse_inline_styles(attributes["style"]) 

+

140 css_styles.update(inline_styles) 

+

141 new_context = new_context.with_css_styles(css_styles) 

+

142 

+

143 # Apply element-specific default styles 

+

144 font = apply_element_font_styles( 

+

145 new_context.font, tag_name, css_styles, new_context) 

+

146 new_context = new_context.with_font(font) 

+

147 

+

148 # Apply background from styles 

+

149 background = apply_background_styles(new_context.background, css_styles) 

+

150 new_context = new_context.with_background(background) 

+

151 

+

152 return new_context 

+

153 

+

154 

+

155def parse_inline_styles(style_text: str) -> Dict[str, str]: 

+

156 """ 

+

157 Parse CSS inline styles into dictionary. 

+

158 

+

159 Args: 

+

160 style_text: CSS style text (e.g., "color: red; font-weight: bold;") 

+

161 

+

162 Returns: 

+

163 Dictionary of CSS property-value pairs 

+

164 """ 

+

165 styles = {} 

+

166 for declaration in style_text.split(";"): 

+

167 if ":" in declaration: 

+

168 prop, value = declaration.split(":", 1) 

+

169 styles[prop.strip().lower()] = value.strip() 

+

170 return styles 

+

171 

+

172 

+

173def apply_element_font_styles(font: Font, 

+

174 tag_name: str, 

+

175 css_styles: Dict[str, 

+

176 str], 

+

177 context: Optional[StyleContext] = None) -> Font: 

+

178 """ 

+

179 Apply font styling based on HTML element and CSS styles. 

+

180 Uses document's font registry when available to avoid creating duplicate fonts. 

+

181 

+

182 Args: 

+

183 font: Current font 

+

184 tag_name: HTML tag name 

+

185 css_styles: CSS styles dictionary 

+

186 context: Style context with document reference for font registry 

+

187 

+

188 Returns: 

+

189 Font object with applied styling (either existing or newly created) 

+

190 """ 

+

191 # Default element styles 

+

192 element_font_styles = { 

+

193 "b": {"weight": FontWeight.BOLD}, 

+

194 "strong": {"weight": FontWeight.BOLD}, 

+

195 "i": {"style": FontStyle.ITALIC}, 

+

196 "em": {"style": FontStyle.ITALIC}, 

+

197 "u": {"decoration": TextDecoration.UNDERLINE}, 

+

198 "s": {"decoration": TextDecoration.STRIKETHROUGH}, 

+

199 "del": {"decoration": TextDecoration.STRIKETHROUGH}, 

+

200 "h1": {"size": 24, "weight": FontWeight.BOLD}, 

+

201 "h2": {"size": 20, "weight": FontWeight.BOLD}, 

+

202 "h3": {"size": 18, "weight": FontWeight.BOLD}, 

+

203 "h4": {"size": 16, "weight": FontWeight.BOLD}, 

+

204 "h5": {"size": 14, "weight": FontWeight.BOLD}, 

+

205 "h6": {"size": 12, "weight": FontWeight.BOLD}, 

+

206 } 

+

207 

+

208 # Start with current font properties 

+

209 font_size = font.font_size 

+

210 colour = font.colour 

+

211 weight = font.weight 

+

212 style = font.style 

+

213 decoration = font.decoration 

+

214 background = font.background 

+

215 language = font.language 

+

216 font_path = font._font_path 

+

217 

+

218 # Apply element default styles 

+

219 if tag_name in element_font_styles: 

+

220 elem_styles = element_font_styles[tag_name] 

+

221 if "size" in elem_styles: 

+

222 font_size = elem_styles["size"] 

+

223 if "weight" in elem_styles: 

+

224 weight = elem_styles["weight"] 

+

225 if "style" in elem_styles: 

+

226 style = elem_styles["style"] 

+

227 if "decoration" in elem_styles: 

+

228 decoration = elem_styles["decoration"] 

+

229 

+

230 # Apply CSS styles (override element defaults) 

+

231 if "font-size" in css_styles: 

+

232 # Parse font-size (simplified - could be enhanced) 

+

233 size_value = css_styles["font-size"].lower() 

+

234 if size_value.endswith("px"): 

+

235 try: 

+

236 font_size = int(float(size_value[:-2])) 

+

237 except ValueError: 

+

238 pass 

+

239 elif size_value.endswith("pt"): 239 ↛ 240line 239 didn't jump to line 240 because the condition on line 239 was never true

+

240 try: 

+

241 font_size = int(float(size_value[:-2])) 

+

242 except ValueError: 

+

243 pass 

+

244 

+

245 if "font-weight" in css_styles: 

+

246 weight_value = css_styles["font-weight"].lower() 

+

247 if weight_value in ["bold", "700", "800", "900"]: 247 ↛ 249line 247 didn't jump to line 249 because the condition on line 247 was always true

+

248 weight = FontWeight.BOLD 

+

249 elif weight_value in ["normal", "400"]: 

+

250 weight = FontWeight.NORMAL 

+

251 

+

252 if "font-style" in css_styles: 

+

253 style_value = css_styles["font-style"].lower() 

+

254 if style_value == "italic": 

+

255 style = FontStyle.ITALIC 

+

256 elif style_value == "normal": 256 ↛ 259line 256 didn't jump to line 259 because the condition on line 256 was always true

+

257 style = FontStyle.NORMAL 

+

258 

+

259 if "text-decoration" in css_styles: 

+

260 decoration_value = css_styles["text-decoration"].lower() 

+

261 if "underline" in decoration_value: 

+

262 decoration = TextDecoration.UNDERLINE 

+

263 elif "line-through" in decoration_value: 263 ↛ 264line 263 didn't jump to line 264 because the condition on line 263 was never true

+

264 decoration = TextDecoration.STRIKETHROUGH 

+

265 elif "none" in decoration_value: 265 ↛ 268line 265 didn't jump to line 268 because the condition on line 265 was always true

+

266 decoration = TextDecoration.NONE 

+

267 

+

268 if "color" in css_styles: 

+

269 # Parse color (simplified - could be enhanced for hex, rgb, etc.) 

+

270 color_value = css_styles["color"].lower() 

+

271 color_map = { 

+

272 "black": (0, 0, 0), 

+

273 "white": (255, 255, 255), 

+

274 "red": (255, 0, 0), 

+

275 "green": (0, 255, 0), 

+

276 "blue": (0, 0, 255), 

+

277 } 

+

278 if color_value in color_map: 

+

279 colour = color_map[color_value] 

+

280 elif color_value.startswith("#") and len(color_value) == 7: 

+

281 try: 

+

282 r = int(color_value[1:3], 16) 

+

283 g = int(color_value[3:5], 16) 

+

284 b = int(color_value[5:7], 16) 

+

285 colour = (r, g, b) 

+

286 except ValueError: 

+

287 pass 

+

288 

+

289 # Use document's style registry if available to avoid creating duplicate styles 

+

290 if context and context.document and hasattr( 

+

291 context.document, 'get_or_create_style'): 

+

292 # Create an abstract style first 

+

293 from pyWebLayout.style.abstract_style import FontFamily, FontSize 

+

294 

+

295 # Map font properties to abstract style properties 

+

296 font_family = FontFamily.SERIF # Default - could be enhanced to detect from font_path 

+

297 if font_size: 297 ↛ 301line 297 didn't jump to line 301 because the condition on line 297 was always true

+

298 font_size_value = font_size if isinstance( 

+

299 font_size, int) else FontSize.MEDIUM 

+

300 else: 

+

301 font_size_value = FontSize.MEDIUM 

+

302 

+

303 # Create abstract style and register it 

+

304 style_id, abstract_style = context.document.get_or_create_style( 

+

305 font_family=font_family, 

+

306 font_size=font_size_value, 

+

307 font_weight=weight, 

+

308 font_style=style, 

+

309 text_decoration=decoration, 

+

310 color=colour, 

+

311 language=language 

+

312 ) 

+

313 

+

314 # Get the concrete font for this style 

+

315 return context.document.get_font_for_style(abstract_style) 

+

316 elif context and context.document and hasattr(context.document, 'get_or_create_font'): 316 ↛ 318line 316 didn't jump to line 318 because the condition on line 316 was never true

+

317 # Fallback to old font registry system 

+

318 return context.document.get_or_create_font( 

+

319 font_path=font_path, 

+

320 font_size=font_size, 

+

321 colour=colour, 

+

322 weight=weight, 

+

323 style=style, 

+

324 decoration=decoration, 

+

325 background=background, 

+

326 language=language, 

+

327 min_hyphenation_width=font.min_hyphenation_width 

+

328 ) 

+

329 else: 

+

330 # Fallback to creating new font if no document context 

+

331 return Font( 

+

332 font_path=font_path, 

+

333 font_size=font_size, 

+

334 colour=colour, 

+

335 weight=weight, 

+

336 style=style, 

+

337 decoration=decoration, 

+

338 background=background, 

+

339 language=language, 

+

340 ) 

+

341 

+

342 

+

343def apply_background_styles( 

+

344 current_background: Optional[Tuple[int, int, int, int]], css_styles: Dict[str, str] 

+

345) -> Optional[Tuple[int, int, int, int]]: 

+

346 """ 

+

347 Apply background styling from CSS. 

+

348 

+

349 Args: 

+

350 current_background: Current background color (RGBA) 

+

351 css_styles: CSS styles dictionary 

+

352 

+

353 Returns: 

+

354 New background color or None 

+

355 """ 

+

356 if "background-color" in css_styles: 

+

357 bg_value = css_styles["background-color"].lower() 

+

358 if bg_value == "transparent": 358 ↛ 359line 358 didn't jump to line 359 because the condition on line 358 was never true

+

359 return None 

+

360 # Add color parsing logic here if needed 

+

361 

+

362 return current_background 

+

363 

+

364 

+

365def extract_text_content(element: Tag, context: StyleContext) -> List[Word]: 

+

366 """ 

+

367 Extract text content from an element, handling inline formatting and links. 

+

368 

+

369 Args: 

+

370 element: BeautifulSoup Tag object 

+

371 context: Current style context 

+

372 

+

373 Returns: 

+

374 List of Word objects (including LinkedWord for hyperlinks) 

+

375 """ 

+

376 return extract_words_from_nodes(list(element.children), context) 

+

377 

+

378 

+

379def extract_words_from_nodes(nodes: List, context: StyleContext) -> List[Word]: 

+

380 """ 

+

381 Extract words from a sequence of sibling nodes. 

+

382 

+

383 Separated from extract_text_content so that a container holding a mix of 

+

384 inline and block children can hand over just the inline runs, without 

+

385 building a synthetic element to wrap them in. 

+

386 

+

387 Args: 

+

388 nodes: BeautifulSoup nodes (Tags and NavigableStrings) in document order 

+

389 context: Current style context 

+

390 

+

391 Returns: 

+

392 List of Word objects (including LinkedWord for hyperlinks) 

+

393 """ 

+

394 from pyWebLayout.abstract.inline import LinkedWord 

+

395 from pyWebLayout.abstract.functional import LinkType 

+

396 

+

397 words = [] 

+

398 

+

399 for child in nodes: 

+

400 # Comments and processing instructions are NavigableString subclasses; 

+

401 # their text is markup, not content. 

+

402 if isinstance(child, (Comment, Doctype, CData, ProcessingInstruction)): 402 ↛ 403line 402 didn't jump to line 403 because the condition on line 402 was never true

+

403 continue 

+

404 

+

405 if isinstance(child, NavigableString): 

+

406 # Plain text - split into words. Argument-less str.split() already 

+

407 # discards surrounding whitespace and never yields an empty string, so 

+

408 # it needs neither a preceding strip() nor a per-word emptiness test. 

+

409 font = context.font 

+

410 background = context.background 

+

411 words.extend([Word(word_text, font, background) 

+

412 for word_text in str(child).split()]) 

+

413 elif isinstance(child, Tag): 413 ↛ 399line 413 didn't jump to line 399 because the condition on line 413 was always true

+

414 # Special handling for <a> tags (hyperlinks) 

+

415 if child.name.lower() == "a": 

+

416 href = child.get('href', '') 

+

417 if href: 

+

418 # Determine link type based on href 

+

419 if href.startswith(('http://', 'https://')): 

+

420 link_type = LinkType.EXTERNAL 

+

421 elif href.startswith('#'): 

+

422 link_type = LinkType.INTERNAL 

+

423 elif href.startswith('javascript:') or href.startswith('api:'): 

+

424 link_type = LinkType.API 

+

425 else: 

+

426 link_type = LinkType.INTERNAL 

+

427 

+

428 # Apply link styling 

+

429 child_context = apply_element_styling(context, child) 

+

430 

+

431 # Extract text and create LinkedWord for each word 

+

432 link_text = child.get_text(strip=True) 

+

433 title = child.get('title', '') 

+

434 

+

435 for word_text in link_text.split(): 

+

436 if word_text: 436 ↛ 435line 436 didn't jump to line 435 because the condition on line 436 was always true

+

437 linked_word = LinkedWord( 

+

438 text=word_text, 

+

439 style=child_context.font, 

+

440 location=href, 

+

441 link_type=link_type, 

+

442 background=child_context.background, 

+

443 title=title if title else None 

+

444 ) 

+

445 words.append(linked_word) 

+

446 else: 

+

447 # <a> without href - treat as normal text 

+

448 child_context = apply_element_styling(context, child) 

+

449 child_words = extract_text_content(child, child_context) 

+

450 words.extend(child_words) 

+

451 

+

452 # Process other inline elements 

+

453 elif child.name.lower() in [ 

+

454 "span", 

+

455 "strong", 

+

456 "b", 

+

457 "em", 

+

458 "i", 

+

459 "u", 

+

460 "s", 

+

461 "del", 

+

462 "ins", 

+

463 "mark", 

+

464 "small", 

+

465 "sub", 

+

466 "sup", 

+

467 "code", 

+

468 "q", 

+

469 "cite", 

+

470 "abbr", 

+

471 "time", 

+

472 ]: 

+

473 child_context = apply_element_styling(context, child) 

+

474 child_words = extract_text_content(child, child_context) 

+

475 words.extend(child_words) 

+

476 else: 

+

477 # Block element - shouldn't happen in well-formed HTML but handle 

+

478 # gracefully 

+

479 child_context = apply_element_styling(context, child) 

+

480 child_result = process_element(child, child_context) 

+

481 if isinstance(child_result, list): 481 ↛ 482line 481 didn't jump to line 482 because the condition on line 481 was never true

+

482 for block in child_result: 

+

483 if isinstance(block, Paragraph): 

+

484 for _, word in block.words_iter(): 

+

485 words.append(word) 

+

486 elif isinstance(child_result, Paragraph): 486 ↛ 487line 486 didn't jump to line 487 because the condition on line 486 was never true

+

487 for _, word in child_result.words_iter(): 

+

488 words.append(word) 

+

489 

+

490 return words 

+

491 

+

492 

+

493# Tags that flow within a line of text rather than forming a block of their own. 

+

494# They carry no handler of their own: extract_words_from_nodes consumes them, 

+

495# applying their styling to the words they contain. 

+

496INLINE_TAGS = frozenset({ 

+

497 "a", "span", "strong", "b", "em", "i", "u", "s", "del", "ins", "mark", 

+

498 "small", "sub", "sup", "code", "q", "cite", "abbr", "time", 

+

499}) 

+

500 

+

501 

+

502def is_inline(node) -> bool: 

+

503 """ 

+

504 Whether a node belongs to a run of text rather than standing as its own block. 

+

505 

+

506 Args: 

+

507 node: A BeautifulSoup Tag or NavigableString 

+

508 

+

509 Returns: 

+

510 True for text and inline tags, False for block-level tags 

+

511 """ 

+

512 if isinstance(node, Tag): 

+

513 return node.name.lower() in INLINE_TAGS 

+

514 if isinstance(node, (Comment, Doctype, CData, ProcessingInstruction)): 

+

515 return False 

+

516 return isinstance(node, NavigableString) 

+

517 

+

518 

+

519def process_block_children(element: Tag, context: StyleContext) -> List[Block]: 

+

520 """ 

+

521 Process a container's children into a list of blocks. 

+

522 

+

523 Containers may hold a mix of inline and block content. Consecutive inline 

+

524 children are gathered into a run and become one Paragraph; a block child ends 

+

525 the current run and is processed by its own handler. This is the single entry 

+

526 point for every container that is not itself a paragraph - div, li, td, th, 

+

527 blockquote and the semantic containers. 

+

528 

+

529 Without this, inline tags reach process_element, whose handler for them is 

+

530 ignore_handler, and their text is silently dropped. 

+

531 

+

532 Args: 

+

533 element: The container element 

+

534 context: Current style context 

+

535 

+

536 Returns: 

+

537 Blocks in document order 

+

538 """ 

+

539 blocks: List[Block] = [] 

+

540 run: List = [] 

+

541 

+

542 def flush_run(): 

+

543 """Turn the pending inline run into a paragraph, if it holds any words.""" 

+

544 if not run: 

+

545 return 

+

546 words = extract_words_from_nodes(run, context) 

+

547 run.clear() 

+

548 if words: 

+

549 paragraph = Paragraph(context.font) 

+

550 for word in words: 

+

551 paragraph.add_word(word) 

+

552 blocks.append(paragraph) 

+

553 

+

554 for child in element.children: 

+

555 # <br> ends the current line of text and starts a new one. 

+

556 if isinstance(child, Tag) and child.name.lower() == "br": 

+

557 flush_run() 

+

558 continue 

+

559 

+

560 if is_inline(child): 

+

561 run.append(child) 

+

562 continue 

+

563 

+

564 if not isinstance(child, Tag): 

+

565 continue # comments and similar 

+

566 

+

567 flush_run() 

+

568 child_context = apply_element_styling(context, child) 

+

569 result = process_element(child, child_context) 

+

570 if result: 

+

571 if isinstance(result, list): 

+

572 blocks.extend(result) 

+

573 else: 

+

574 blocks.append(result) 

+

575 

+

576 flush_run() 

+

577 return blocks 

+

578 

+

579 

+

580def process_element( 

+

581 element: Tag, context: StyleContext 

+

582) -> Union[Block, List[Block], None]: 

+

583 """ 

+

584 Process a single HTML element using appropriate handler. 

+

585 

+

586 Args: 

+

587 element: BeautifulSoup Tag object 

+

588 context: Current style context 

+

589 

+

590 Returns: 

+

591 Block object(s) or None if element should be ignored 

+

592 """ 

+

593 tag_name = element.name.lower() 

+

594 handler = HANDLERS.get(tag_name, generic_handler) 

+

595 return handler(element, context) 

+

596 

+

597 

+

598# Handler function signatures: 

+

599# All handlers receive (element: Tag, context: StyleContext) -> 

+

600# Union[Block, List[Block], None] 

+

601 

+

602 

+

603def paragraph_handler(element: Tag, context: StyleContext) -> Union[Paragraph, List[Block], Image]: 

+

604 """ 

+

605 Handle <p> elements. 

+

606 

+

607 Special handling for paragraphs containing images: 

+

608 - If the paragraph contains only an image (common in EPUBs), return the image block 

+

609 - If the paragraph contains images mixed with text, split into separate blocks 

+

610 - Otherwise, return a normal paragraph with text content 

+

611 """ 

+

612 # Check if paragraph contains any img tags (including nested ones) 

+

613 img_tags = element.find_all('img') 

+

614 

+

615 if img_tags: 

+

616 # Paragraph contains images - need special handling 

+

617 blocks = [] 

+

618 

+

619 # Check if this is an image-only paragraph (very common in EPUBs) 

+

620 # Get text content without the img tags 

+

621 text_content = element.get_text(strip=True) 

+

622 

+

623 if not text_content or len(text_content.strip()) == 0: 

+

624 # Image-only paragraph - return just the image(s) 

+

625 for img_tag in img_tags: 

+

626 child_context = apply_element_styling(context, img_tag) 

+

627 img_block = image_handler(img_tag, child_context) 

+

628 if img_block: 628 ↛ 625line 628 didn't jump to line 625 because the condition on line 628 was always true

+

629 blocks.append(img_block) 

+

630 

+

631 # Return single image or list of images 

+

632 if len(blocks) == 1: 

+

633 return blocks[0] 

+

634 return blocks if blocks else Paragraph(context.font) 

+

635 

+

636 # Mixed content - paragraph has both text and images 

+

637 # Process children in order to preserve structure 

+

638 for child in element.children: 

+

639 if isinstance(child, Tag): 

+

640 if child.name == 'img': 640 ↛ 649line 640 didn't jump to line 649 because the condition on line 640 was always true

+

641 # Add the image as a separate block 

+

642 child_context = apply_element_styling(context, child) 

+

643 img_block = image_handler(child, child_context) 

+

644 if img_block: 644 ↛ 638line 644 didn't jump to line 638 because the condition on line 644 was always true

+

645 blocks.append(img_block) 

+

646 else: 

+

647 # Process other inline elements as part of text 

+

648 # This will be handled by extract_text_content below 

+

649 pass 

+

650 

+

651 # Also add a paragraph with the text content 

+

652 paragraph = Paragraph(context.font) 

+

653 words = extract_text_content(element, context) 

+

654 if words: 654 ↛ 659line 654 didn't jump to line 659 because the condition on line 654 was always true

+

655 for word in words: 

+

656 paragraph.add_word(word) 

+

657 blocks.insert(0, paragraph) # Text comes before images 

+

658 

+

659 return blocks if blocks else Paragraph(context.font) 

+

660 

+

661 # No images - normal paragraph handling 

+

662 paragraph = Paragraph(context.font) 

+

663 words = extract_text_content(element, context) 

+

664 for word in words: 

+

665 paragraph.add_word(word) 

+

666 return paragraph 

+

667 

+

668 

+

669def div_handler(element: Tag, context: StyleContext) -> List[Block]: 

+

670 """Handle <div> elements - treat as generic container.""" 

+

671 return process_block_children(element, context) 

+

672 

+

673 

+

674def heading_handler(element: Tag, context: StyleContext) -> Heading: 

+

675 """Handle <h1>-<h6> elements.""" 

+

676 level_map = { 

+

677 "h1": HeadingLevel.H1, 

+

678 "h2": HeadingLevel.H2, 

+

679 "h3": HeadingLevel.H3, 

+

680 "h4": HeadingLevel.H4, 

+

681 "h5": HeadingLevel.H5, 

+

682 "h6": HeadingLevel.H6, 

+

683 } 

+

684 

+

685 level = level_map.get(element.name.lower(), HeadingLevel.H1) 

+

686 heading = Heading(level, context.font) 

+

687 words = extract_text_content(element, context) 

+

688 for word in words: 

+

689 heading.add_word(word) 

+

690 return heading 

+

691 

+

692 

+

693def blockquote_handler(element: Tag, context: StyleContext) -> Quote: 

+

694 """Handle <blockquote> elements.""" 

+

695 quote = Quote(context.font) 

+

696 for block in process_block_children(element, context): 

+

697 quote.add_block(block) 

+

698 return quote 

+

699 

+

700 

+

701def preformatted_handler(element: Tag, context: StyleContext) -> CodeBlock: 

+

702 """Handle <pre> elements.""" 

+

703 language = context.element_attributes.get("data-language", "") 

+

704 code_block = CodeBlock(language) 

+

705 

+

706 # Preserve whitespace and line breaks in preformatted text 

+

707 text = element.get_text(separator="\n", strip=False) 

+

708 for line in text.split("\n"): 

+

709 code_block.add_line(line) 

+

710 

+

711 return code_block 

+

712 

+

713 

+

714def code_handler(element: Tag, context: StyleContext) -> Union[CodeBlock, None]: 

+

715 """Handle <code> elements.""" 

+

716 # If parent is <pre>, this is handled by preformatted_handler 

+

717 if context.parent_elements and context.parent_elements[-1] == "pre": 

+

718 return None # Will be handled by parent 

+

719 

+

720 # Inline code - handled during text extraction 

+

721 return None 

+

722 

+

723 

+

724def unordered_list_handler(element: Tag, context: StyleContext) -> HList: 

+

725 """Handle <ul> elements.""" 

+

726 hlist = HList(ListStyle.UNORDERED, context.font) 

+

727 for child in element.children: 

+

728 if isinstance(child, Tag) and child.name.lower() == "li": 

+

729 child_context = apply_element_styling(context, child) 

+

730 item = process_element(child, child_context) 

+

731 if item: 731 ↛ 727line 731 didn't jump to line 727 because the condition on line 731 was always true

+

732 hlist.add_item(item) 

+

733 return hlist 

+

734 

+

735 

+

736def ordered_list_handler(element: Tag, context: StyleContext) -> HList: 

+

737 """Handle <ol> elements.""" 

+

738 hlist = HList(ListStyle.ORDERED, context.font) 

+

739 for child in element.children: 

+

740 if isinstance(child, Tag) and child.name.lower() == "li": 

+

741 child_context = apply_element_styling(context, child) 

+

742 item = process_element(child, child_context) 

+

743 if item: 743 ↛ 739line 743 didn't jump to line 739 because the condition on line 743 was always true

+

744 hlist.add_item(item) 

+

745 return hlist 

+

746 

+

747 

+

748def list_item_handler(element: Tag, context: StyleContext) -> ListItem: 

+

749 """Handle <li> elements.""" 

+

750 list_item = ListItem(None, context.font) 

+

751 for block in process_block_children(element, context): 

+

752 list_item.add_block(block) 

+

753 return list_item 

+

754 

+

755 

+

756def table_handler(element: Tag, context: StyleContext) -> Table: 

+

757 """Handle <table> elements.""" 

+

758 caption = None 

+

759 caption_elem = element.find("caption") 

+

760 if caption_elem: 760 ↛ 761line 760 didn't jump to line 761 because the condition on line 760 was never true

+

761 caption = caption_elem.get_text(strip=True) 

+

762 

+

763 table = Table(caption, context.font) 

+

764 

+

765 # Process table rows 

+

766 for child in element.children: 

+

767 if isinstance(child, Tag): 

+

768 if child.name.lower() == "tr": 

+

769 child_context = apply_element_styling(context, child) 

+

770 row = process_element(child, child_context) 

+

771 if row: 771 ↛ 766line 771 didn't jump to line 766 because the condition on line 771 was always true

+

772 table.add_row(row) 

+

773 elif child.name.lower() in ["thead", "tbody", "tfoot"]: 773 ↛ 766line 773 didn't jump to line 766 because the condition on line 773 was always true

+

774 section = "header" if child.name.lower() == "thead" else "body" 

+

775 section = "footer" if child.name.lower() == "tfoot" else section 

+

776 

+

777 for row_elem in child.find_all("tr"): 

+

778 child_context = apply_element_styling(context, row_elem) 

+

779 row = process_element(row_elem, child_context) 

+

780 if row: 780 ↛ 777line 780 didn't jump to line 777 because the condition on line 780 was always true

+

781 table.add_row(row, section) 

+

782 

+

783 return table 

+

784 

+

785 

+

786def table_row_handler(element: Tag, context: StyleContext) -> TableRow: 

+

787 """Handle <tr> elements.""" 

+

788 row = TableRow(context.font) 

+

789 for child in element.children: 

+

790 if isinstance(child, Tag) and child.name.lower() in ["td", "th"]: 

+

791 child_context = apply_element_styling(context, child) 

+

792 cell = process_element(child, child_context) 

+

793 if cell: 793 ↛ 789line 793 didn't jump to line 789 because the condition on line 793 was always true

+

794 row.add_cell(cell) 

+

795 return row 

+

796 

+

797 

+

798def table_cell_handler(element: Tag, context: StyleContext) -> TableCell: 

+

799 """Handle <td> elements.""" 

+

800 colspan = int(context.element_attributes.get("colspan", 1)) 

+

801 rowspan = int(context.element_attributes.get("rowspan", 1)) 

+

802 cell = TableCell(False, colspan, rowspan, context.font) 

+

803 

+

804 for block in process_block_children(element, context): 

+

805 cell.add_block(block) 

+

806 

+

807 return cell 

+

808 

+

809 

+

810def table_header_cell_handler(element: Tag, context: StyleContext) -> TableCell: 

+

811 """Handle <th> elements.""" 

+

812 colspan = int(context.element_attributes.get("colspan", 1)) 

+

813 rowspan = int(context.element_attributes.get("rowspan", 1)) 

+

814 cell = TableCell(True, colspan, rowspan, context.font) 

+

815 

+

816 for block in process_block_children(element, context): 

+

817 cell.add_block(block) 

+

818 

+

819 return cell 

+

820 

+

821 

+

822def horizontal_rule_handler(element: Tag, context: StyleContext) -> HorizontalRule: 

+

823 """Handle <hr> elements.""" 

+

824 return HorizontalRule() 

+

825 

+

826 

+

827def line_break_handler(element: Tag, context: StyleContext) -> None: 

+

828 """Handle <br> elements.""" 

+

829 # Line breaks are typically handled at the paragraph level 

+

830 return None 

+

831 

+

832 

+

833def image_handler(element: Tag, context: StyleContext) -> Image: 

+

834 """Handle <img> elements.""" 

+

835 import os 

+

836 import urllib.parse 

+

837 

+

838 src = context.element_attributes.get("src", "") 

+

839 alt_text = context.element_attributes.get("alt", "") 

+

840 

+

841 # Resolve relative paths if base_path is provided 

+

842 if context.base_path and src and not src.startswith(('http://', 'https://', '/')): 

+

843 # Parse the src to handle URL-encoded characters 

+

844 src_decoded = urllib.parse.unquote(src) 

+

845 # Resolve relative path to absolute path 

+

846 src = os.path.normpath(os.path.join(context.base_path, src_decoded)) 

+

847 

+

848 # Parse dimensions if provided 

+

849 width = height = None 

+

850 try: 

+

851 if "width" in context.element_attributes: 

+

852 width = int(context.element_attributes["width"]) 

+

853 if "height" in context.element_attributes: 

+

854 height = int(context.element_attributes["height"]) 

+

855 except ValueError: 

+

856 pass 

+

857 

+

858 return Image(source=src, alt_text=alt_text, width=width, height=height) 

+

859 

+

860 

+

861def ignore_handler(element: Tag, context: StyleContext) -> None: 

+

862 """Handle elements that should be ignored.""" 

+

863 return None 

+

864 

+

865 

+

866def generic_handler(element: Tag, context: StyleContext) -> List[Block]: 

+

867 """Handle unknown elements as generic containers.""" 

+

868 return div_handler(element, context) 

+

869 

+

870 

+

871# Handler registry - maps HTML tag names to handler functions 

+

872HANDLERS: Dict[str, Callable[[Tag, StyleContext], Union[Block, List[Block], None]]] = { 

+

873 # Block elements 

+

874 "p": paragraph_handler, 

+

875 "div": div_handler, 

+

876 "h1": heading_handler, 

+

877 "h2": heading_handler, 

+

878 "h3": heading_handler, 

+

879 "h4": heading_handler, 

+

880 "h5": heading_handler, 

+

881 "h6": heading_handler, 

+

882 "blockquote": blockquote_handler, 

+

883 "pre": preformatted_handler, 

+

884 "code": code_handler, 

+

885 "ul": unordered_list_handler, 

+

886 "ol": ordered_list_handler, 

+

887 "li": list_item_handler, 

+

888 "table": table_handler, 

+

889 "tr": table_row_handler, 

+

890 "td": table_cell_handler, 

+

891 "th": table_header_cell_handler, 

+

892 "hr": horizontal_rule_handler, 

+

893 "br": line_break_handler, 

+

894 # Semantic elements (treated as containers) 

+

895 "section": div_handler, 

+

896 "article": div_handler, 

+

897 "aside": div_handler, 

+

898 "nav": div_handler, 

+

899 "header": div_handler, 

+

900 "footer": div_handler, 

+

901 "main": div_handler, 

+

902 "figure": div_handler, 

+

903 "figcaption": paragraph_handler, 

+

904 # Media elements 

+

905 "img": image_handler, 

+

906 # Inline elements (handled during text extraction) 

+

907 "span": ignore_handler, 

+

908 "a": ignore_handler, 

+

909 "strong": ignore_handler, 

+

910 "b": ignore_handler, 

+

911 "em": ignore_handler, 

+

912 "i": ignore_handler, 

+

913 "u": ignore_handler, 

+

914 "s": ignore_handler, 

+

915 "del": ignore_handler, 

+

916 "ins": ignore_handler, 

+

917 "mark": ignore_handler, 

+

918 "small": ignore_handler, 

+

919 "sub": ignore_handler, 

+

920 "sup": ignore_handler, 

+

921 "q": ignore_handler, 

+

922 "cite": ignore_handler, 

+

923 "abbr": ignore_handler, 

+

924 "time": ignore_handler, 

+

925 # Ignored elements 

+

926 "script": ignore_handler, 

+

927 "style": ignore_handler, 

+

928 "meta": ignore_handler, 

+

929 "link": ignore_handler, 

+

930 "head": ignore_handler, 

+

931 "title": ignore_handler, 

+

932} 

+

933 

+

934 

+

935def parse_html_string( 

+

936 html_string: str, base_font: Optional[Font] = None, document=None, base_path: Optional[str] = None 

+

937) -> List[Block]: 

+

938 """ 

+

939 Parse HTML string and return list of Block objects. 

+

940 

+

941 Args: 

+

942 html_string: HTML content to parse 

+

943 base_font: Base font for styling, defaults to system default 

+

944 document: Document instance for font registry to avoid duplicate fonts 

+

945 base_path: Base directory path for resolving relative URLs (e.g., image sources) 

+

946 

+

947 Returns: 

+

948 List of Block objects representing the document structure 

+

949 """ 

+

950 soup = BeautifulSoup(html_string, "html.parser") 

+

951 context = create_base_context(base_font, document, base_path) 

+

952 

+

953 blocks = [] 

+

954 

+

955 # Process the body if it exists, otherwise process all top-level elements 

+

956 root_element = soup.find("body") or soup 

+

957 

+

958 for element in root_element.children: 

+

959 if isinstance(element, Tag): 

+

960 element_context = apply_element_styling(context, element) 

+

961 result = process_element(element, element_context) 

+

962 if result: 

+

963 if isinstance(result, list): 

+

964 blocks.extend(result) 

+

965 else: 

+

966 blocks.append(result) 

+

967 

+

968 return blocks 

+
+ + + diff --git a/cov_info/htmlcov/z_40407af872b0cf37___init___py.html b/cov_info/htmlcov/z_40407af872b0cf37___init___py.html new file mode 100644 index 0000000..3a8561d --- /dev/null +++ b/cov_info/htmlcov/z_40407af872b0cf37___init___py.html @@ -0,0 +1,133 @@ + + + + + Coverage for pyWebLayout/core/__init__.py: 100% + + + + + +
+
+

+ Coverage for pyWebLayout/core/__init__.py: + 100% +

+ +

+ 2 statements   + + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.15.4, + created at 2026-08-08 20:34 +0000 +

+ +
+
+
+

1""" 

+

2Core functionality for the pyWebLayout library. 

+

3 

+

4This package contains the core abstractions and base classes that form the foundation 

+

5of the pyWebLayout rendering system. 

+

6""" 

+

7 

+

8from .base import ( 

+

9 Renderable, 

+

10 Interactable, 

+

11 Layoutable, 

+

12 Queriable, 

+

13 Hierarchical, 

+

14 Geometric, 

+

15 Styleable, 

+

16 FontRegistry, 

+

17 MetadataContainer, 

+

18 BlockContainer, 

+

19 ContainerAware, 

+

20) 

+

21 

+

22__all__ = [ 

+

23 'Renderable', 

+

24 'Interactable', 

+

25 'Layoutable', 

+

26 'Queriable', 

+

27 'Hierarchical', 

+

28 'Geometric', 

+

29 'Styleable', 

+

30 'FontRegistry', 

+

31 'MetadataContainer', 

+

32 'BlockContainer', 

+

33 'ContainerAware', 

+

34] 

+
+ + + diff --git a/cov_info/htmlcov/z_40407af872b0cf37_base_py.html b/cov_info/htmlcov/z_40407af872b0cf37_base_py.html new file mode 100644 index 0000000..3229f28 --- /dev/null +++ b/cov_info/htmlcov/z_40407af872b0cf37_base_py.html @@ -0,0 +1,547 @@ + + + + + Coverage for pyWebLayout/core/base.py: 72% + + + + + +
+
+

+ Coverage for pyWebLayout/core/base.py: + 72% +

+ +

+ 134 statements   + + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.15.4, + created at 2026-08-08 20:34 +0000 +

+ +
+
+
+

1from abc import ABC 

+

2from typing import Optional, Tuple, TYPE_CHECKING, Any, Dict 

+

3import numpy as np 

+

4 

+

5 

+

6if TYPE_CHECKING: 6 ↛ 7line 6 didn't jump to line 7 because the condition on line 6 was never true

+

7 from pyWebLayout.style import Font, FontWeight, FontStyle, TextDecoration 

+

8 

+

9 

+

10class Renderable(ABC): 

+

11 """ 

+

12 Abstract base class for any object that can be rendered to an image. 

+

13 All renderable objects must implement the render method. 

+

14 """ 

+

15 

+

16 def render(self): 

+

17 """ 

+

18 Render the object to an image. 

+

19 

+

20 Returns: 

+

21 PIL.Image: The rendered image 

+

22 """ 

+

23 

+

24 @property 

+

25 def origin(self): 

+

26 return self._origin 

+

27 

+

28 

+

29class Interactable(ABC): 

+

30 """ 

+

31 Abstract base class for any object that can be interacted with. 

+

32 Interactable objects must have a callback that is executed when interacted with. 

+

33 """ 

+

34 

+

35 def __init__(self, callback=None): 

+

36 """ 

+

37 Initialize an interactable object. 

+

38 

+

39 Args: 

+

40 callback: The function to call when this object is interacted with 

+

41 """ 

+

42 self._callback = callback 

+

43 

+

44 def interact(self, point: np.generic): 

+

45 """ 

+

46 Handle interaction at the given point. 

+

47 

+

48 Args: 

+

49 point: The coordinates of the interaction 

+

50 

+

51 Returns: 

+

52 The result of calling the callback function with the point 

+

53 """ 

+

54 if self._callback is None: 54 ↛ 55line 54 didn't jump to line 55 because the condition on line 54 was never true

+

55 return None 

+

56 return self._callback(point) 

+

57 

+

58 

+

59class Layoutable(ABC): 

+

60 """ 

+

61 Abstract base class for any object that can be laid out. 

+

62 Layoutable objects must implement the layout method which arranges their contents. 

+

63 """ 

+

64 

+

65 def layout(self): 

+

66 """ 

+

67 Layout the object's contents. 

+

68 This method should be called before rendering to properly arrange the object's contents. 

+

69 """ 

+

70 

+

71 

+

72class Queriable(ABC): 

+

73 

+

74 def in_object(self, point: np.generic): 

+

75 """ 

+

76 check if a point is in the object 

+

77 """ 

+

78 point_array = np.array(point) 

+

79 relative_point = point_array - self._origin 

+

80 return np.all((0 <= relative_point) & (relative_point < self.size)) 

+

81 

+

82 

+

83# ============================================================================== 

+

84# Mixins - Reusable components for common patterns 

+

85# ============================================================================== 

+

86 

+

87 

+

88class Hierarchical: 

+

89 """ 

+

90 Mixin providing parent-child relationship management. 

+

91 

+

92 Classes using this mixin can track their parent in a document hierarchy. 

+

93 """ 

+

94 

+

95 def __init__(self, *args, **kwargs): 

+

96 super().__init__(*args, **kwargs) 

+

97 self._parent: Optional[Any] = None 

+

98 

+

99 @property 

+

100 def parent(self) -> Optional[Any]: 

+

101 """Get the parent object containing this object, if any""" 

+

102 return self._parent 

+

103 

+

104 @parent.setter 

+

105 def parent(self, parent: Any): 

+

106 """Set the parent object""" 

+

107 self._parent = parent 

+

108 

+

109 

+

110class Geometric: 

+

111 """ 

+

112 Mixin providing origin and size properties for positioned elements. 

+

113 

+

114 Provides standard geometric properties for elements that have a position 

+

115 and size in 2D space. Uses numpy arrays for efficient calculations. 

+

116 """ 

+

117 

+

118 def __init__(self, *args, origin=None, size=None, **kwargs): 

+

119 super().__init__(*args, **kwargs) 

+

120 self._origin = np.array(origin) if origin is not None else np.array([0, 0]) 

+

121 self._size = np.array(size) if size is not None else np.array([0, 0]) 

+

122 

+

123 @property 

+

124 def origin(self) -> np.ndarray: 

+

125 """Get the origin (top-left corner) of the element""" 

+

126 return self._origin 

+

127 

+

128 @origin.setter 

+

129 def origin(self, origin: np.ndarray): 

+

130 """Set the origin of the element""" 

+

131 self._origin = np.array(origin) 

+

132 

+

133 @property 

+

134 def size(self) -> np.ndarray: 

+

135 """Get the size (width, height) of the element""" 

+

136 return self._size 

+

137 

+

138 @size.setter 

+

139 def size(self, size: np.ndarray): 

+

140 """Set the size of the element""" 

+

141 self._size = np.array(size) 

+

142 

+

143 def set_origin(self, origin: np.ndarray): 

+

144 """Set the origin of this element (alternative setter method)""" 

+

145 self._origin = np.array(origin) 

+

146 

+

147 

+

148class Styleable: 

+

149 """ 

+

150 Mixin providing style property management. 

+

151 

+

152 Classes using this mixin can have a style property that can be 

+

153 inherited from parents or set explicitly. 

+

154 """ 

+

155 

+

156 def __init__(self, *args, style=None, **kwargs): 

+

157 super().__init__(*args, **kwargs) 

+

158 self._style = style 

+

159 

+

160 @property 

+

161 def style(self) -> Optional[Any]: 

+

162 """Get the style for this element""" 

+

163 return self._style 

+

164 

+

165 @style.setter 

+

166 def style(self, style: Any): 

+

167 """Set the style for this element""" 

+

168 self._style = style 

+

169 

+

170 

+

171class FontRegistry: 

+

172 """ 

+

173 Mixin providing font caching and creation with parent delegation. 

+

174 

+

175 This mixin allows classes to maintain a local font registry and create/reuse 

+

176 Font objects efficiently. It supports parent delegation, where font requests 

+

177 can cascade up to a parent container if one exists. 

+

178 

+

179 Classes using this mixin should also use Hierarchical to support parent delegation. 

+

180 """ 

+

181 

+

182 def __init__(self, *args, **kwargs): 

+

183 super().__init__(*args, **kwargs) 

+

184 self._fonts: Dict[str, 'Font'] = {} 

+

185 

+

186 def get_or_create_font(self, 

+

187 font_path: Optional[str] = None, 

+

188 font_size: int = 16, 

+

189 colour: Tuple[int, int, int] = (0, 0, 0), 

+

190 weight: 'FontWeight' = None, 

+

191 style: 'FontStyle' = None, 

+

192 decoration: 'TextDecoration' = None, 

+

193 background: Optional[Tuple[int, int, int, int]] = None, 

+

194 language: str = "en_EN", 

+

195 min_hyphenation_width: Optional[int] = None) -> 'Font': 

+

196 """ 

+

197 Get or create a font with the specified properties. 

+

198 

+

199 This method will first check if a parent object has a get_or_create_font 

+

200 method and delegate to it. Otherwise, it will manage fonts locally. 

+

201 

+

202 Args: 

+

203 font_path: Path to the font file (.ttf, .otf). If None, uses default font. 

+

204 font_size: Size of the font in points. 

+

205 colour: RGB color tuple for the text. 

+

206 weight: Font weight (normal or bold). 

+

207 style: Font style (normal or italic). 

+

208 decoration: Text decoration (none, underline, or strikethrough). 

+

209 background: RGBA background color for the text. If None, transparent background. 

+

210 language: Language code for hyphenation and text processing. 

+

211 min_hyphenation_width: Minimum width in pixels required for hyphenation. 

+

212 

+

213 Returns: 

+

214 Font object (either existing or newly created) 

+

215 """ 

+

216 # Import here to avoid circular imports 

+

217 from pyWebLayout.style import Font, FontWeight, FontStyle, TextDecoration 

+

218 

+

219 # Set defaults for enum types 

+

220 if weight is None: 

+

221 weight = FontWeight.NORMAL 

+

222 if style is None: 

+

223 style = FontStyle.NORMAL 

+

224 if decoration is None: 

+

225 decoration = TextDecoration.NONE 

+

226 

+

227 # If we have a parent with font management, delegate to parent 

+

228 if hasattr( 

+

229 self, 

+

230 '_parent') and self._parent and hasattr( 

+

231 self._parent, 

+

232 'get_or_create_font'): 

+

233 return self._parent.get_or_create_font( 

+

234 font_path=font_path, 

+

235 font_size=font_size, 

+

236 colour=colour, 

+

237 weight=weight, 

+

238 style=style, 

+

239 decoration=decoration, 

+

240 background=background, 

+

241 language=language, 

+

242 min_hyphenation_width=min_hyphenation_width 

+

243 ) 

+

244 

+

245 # Otherwise manage our own fonts 

+

246 # Create a unique key for this font configuration 

+

247 bg_tuple = background if background else (255, 255, 255, 0) 

+

248 min_hyph_width = min_hyphenation_width if min_hyphenation_width is not None else font_size * 4 

+

249 

+

250 font_key = ( 

+

251 font_path, 

+

252 font_size, 

+

253 colour, 

+

254 weight.value if hasattr(weight, 'value') else weight, 

+

255 style.value if hasattr(style, 'value') else style, 

+

256 decoration.value if hasattr(decoration, 'value') else decoration, 

+

257 bg_tuple, 

+

258 language, 

+

259 min_hyph_width 

+

260 ) 

+

261 

+

262 # Convert tuple to string for dictionary key 

+

263 key_str = str(font_key) 

+

264 

+

265 # Check if we already have this font 

+

266 if key_str in self._fonts: 

+

267 return self._fonts[key_str] 

+

268 

+

269 # Create new font and store it 

+

270 new_font = Font( 

+

271 font_path=font_path, 

+

272 font_size=font_size, 

+

273 colour=colour, 

+

274 weight=weight, 

+

275 style=style, 

+

276 decoration=decoration, 

+

277 background=background, 

+

278 language=language, 

+

279 min_hyphenation_width=min_hyphenation_width 

+

280 ) 

+

281 

+

282 self._fonts[key_str] = new_font 

+

283 return new_font 

+

284 

+

285 

+

286class MetadataContainer: 

+

287 """ 

+

288 Mixin providing metadata dictionary management. 

+

289 

+

290 Allows classes to store and retrieve arbitrary metadata as key-value pairs. 

+

291 """ 

+

292 

+

293 def __init__(self, *args, **kwargs): 

+

294 super().__init__(*args, **kwargs) 

+

295 self._metadata: Dict[Any, Any] = {} 

+

296 

+

297 def set_metadata(self, key: Any, value: Any): 

+

298 """ 

+

299 Set a metadata value. 

+

300 

+

301 Args: 

+

302 key: The metadata key 

+

303 value: The metadata value 

+

304 """ 

+

305 self._metadata[key] = value 

+

306 

+

307 def get_metadata(self, key: Any) -> Optional[Any]: 

+

308 """ 

+

309 Get a metadata value. 

+

310 

+

311 Args: 

+

312 key: The metadata key 

+

313 

+

314 Returns: 

+

315 The metadata value, or None if not set 

+

316 """ 

+

317 return self._metadata.get(key) 

+

318 

+

319 

+

320class BlockContainer: 

+

321 """ 

+

322 Mixin providing block management methods. 

+

323 

+

324 Provides standard methods for managing block-level children including 

+

325 adding blocks and creating common block types. 

+

326 

+

327 Classes using this mixin should also use Styleable to support style inheritance. 

+

328 """ 

+

329 

+

330 def __init__(self, *args, **kwargs): 

+

331 super().__init__(*args, **kwargs) 

+

332 self._blocks = [] 

+

333 

+

334 def blocks(self): 

+

335 """ 

+

336 Get an iterator over the blocks in this container. 

+

337 

+

338 Can be used as blocks() for iteration or accessing the _blocks list directly. 

+

339 

+

340 Returns: 

+

341 Iterator over blocks 

+

342 """ 

+

343 return iter(self._blocks) 

+

344 

+

345 def add_block(self, block): 

+

346 """ 

+

347 Add a block to this container. 

+

348 

+

349 Args: 

+

350 block: The block to add 

+

351 """ 

+

352 self._blocks.append(block) 

+

353 if hasattr(block, 'parent'): 353 ↛ exitline 353 didn't return from function 'add_block' because the condition on line 353 was always true

+

354 block.parent = self 

+

355 

+

356 def create_paragraph(self, style=None): 

+

357 """ 

+

358 Create a new paragraph and add it to this container. 

+

359 

+

360 Args: 

+

361 style: Optional style override. If None, inherits from container 

+

362 

+

363 Returns: 

+

364 The newly created Paragraph object 

+

365 """ 

+

366 from pyWebLayout.abstract.block import Paragraph 

+

367 

+

368 if style is None and hasattr(self, '_style'): 

+

369 style = self._style 

+

370 

+

371 paragraph = Paragraph(style) 

+

372 self.add_block(paragraph) 

+

373 return paragraph 

+

374 

+

375 def create_heading(self, level=None, style=None): 

+

376 """ 

+

377 Create a new heading and add it to this container. 

+

378 

+

379 Args: 

+

380 level: The heading level (h1-h6) 

+

381 style: Optional style override. If None, inherits from container 

+

382 

+

383 Returns: 

+

384 The newly created Heading object 

+

385 """ 

+

386 from pyWebLayout.abstract.block import Heading, HeadingLevel 

+

387 

+

388 if level is None: 

+

389 level = HeadingLevel.H1 

+

390 

+

391 if style is None and hasattr(self, '_style'): 

+

392 style = self._style 

+

393 

+

394 heading = Heading(level, style) 

+

395 self.add_block(heading) 

+

396 return heading 

+

397 

+

398 

+

399class ContainerAware: 

+

400 """ 

+

401 Mixin providing support for the create_and_add_to factory pattern. 

+

402 

+

403 This is a base that can be extended to provide the create_and_add_to 

+

404 class method pattern used throughout the abstract module. 

+

405 

+

406 Note: This is a framework for future refactoring. Currently, each class 

+

407 has its own create_and_add_to implementation due to varying constructor 

+

408 signatures. This mixin provides a foundation for standardizing that pattern. 

+

409 """ 

+

410 

+

411 @classmethod 

+

412 def _validate_container(cls, container, required_method='add_block'): 

+

413 """ 

+

414 Validate that a container has the required method. 

+

415 

+

416 Args: 

+

417 container: The container to validate 

+

418 required_method: The method name to check for 

+

419 

+

420 Raises: 

+

421 AttributeError: If the container doesn't have the required method 

+

422 """ 

+

423 if not hasattr(container, required_method): 

+

424 raise AttributeError( 

+

425 f"Container {type(container).__name__} must have a '{required_method}' method" 

+

426 ) 

+

427 

+

428 @classmethod 

+

429 def _inherit_style(cls, container, style=None): 

+

430 """ 

+

431 Inherit style from container if not explicitly provided. 

+

432 

+

433 Args: 

+

434 container: The container to inherit from 

+

435 style: Optional explicit style 

+

436 

+

437 Returns: 

+

438 The style to use (explicit or inherited) 

+

439 """ 

+

440 if style is not None: 

+

441 return style 

+

442 

+

443 if hasattr(container, 'style'): 

+

444 return container.style 

+

445 elif hasattr(container, 'default_style'): 

+

446 return container.default_style 

+

447 

+

448 return None 

+
+ + + diff --git a/cov_info/htmlcov/z_40407af872b0cf37_cache_py.html b/cov_info/htmlcov/z_40407af872b0cf37_cache_py.html new file mode 100644 index 0000000..271b299 --- /dev/null +++ b/cov_info/htmlcov/z_40407af872b0cf37_cache_py.html @@ -0,0 +1,442 @@ + + + + + Coverage for pyWebLayout/core/cache.py: 97% + + + + + +
+
+

+ Coverage for pyWebLayout/core/cache.py: + 97% +

+ +

+ 171 statements   + + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.15.4, + created at 2026-08-08 20:34 +0000 +

+ +
+
+
+

1""" 

+

2Bounded usage-ranked caches for the text rendering hot path. 

+

3 

+

4Laying out and rasterising a page re-measures and re-draws the same words over and 

+

5over: a typical page issues ~2800 width measurements and ~2500 glyph rasterisations 

+

6for fewer than 1000 distinct (font, string) pairs. Caching both collapses that work, 

+

7but an unbounded cache is not an option on a memory-constrained target such as a 

+

8Raspberry Pi Zero 2, where the rasterised bitmaps reach ~19MB over a long session. 

+

9 

+

10Eviction is by **usage count**, not recency. Word frequency in prose is Zipfian and 

+

11stationary -- a small set of words ("the", "and", "of") accounts for most tokens on 

+

12every page, and that set barely shifts as the reader advances -- so the words worth 

+

13keeping are exactly the ones used most. 

+

14 

+

15Two design choices keep this from costing more than it saves, because `get` runs 

+

16once per word drawn (~2500 times per page): 

+

17 

+

18* **Counting is O(1) with no reordering.** Each entry carries its own use counter, 

+

19 bumped in place. Ranking structures that reorder on every hit (a frequency-bucket 

+

20 LFU, or an LRU's linked-list splice) were measured 3-5ms per page slower than the 

+

21 hit rate they buy is worth. 

+

22* **Eviction samples rather than sorts.** Finding the globally least-used entry 

+

23 would need a heap kept current on every hit. Instead a small random sample is 

+

24 drawn and the least-used member of it evicted, the same approximation Redis uses 

+

25 for its LFU policy. With the default sample size the evicted entry is very 

+

26 likely to be in the bottom few percent, which is all that matters here. 

+

27 

+

28Both are single-threaded by design; the rendering path holds the GIL throughout and 

+

29adding locking would cost more than it protects. 

+

30""" 

+

31 

+

32from __future__ import annotations 

+

33 

+

34import random 

+

35from typing import Any, Callable, Dict, Generic, Hashable, List, Optional, TypeVar 

+

36 

+

37K = TypeVar('K', bound=Hashable) 

+

38V = TypeVar('V') 

+

39 

+

40# Entries examined per eviction. Larger samples approximate true least-frequently-used 

+

41# more closely at linear cost; 8 puts the victim in the bottom ~15% of entries, which 

+

42# is ample when the alternative is a rasterisation that costs ~60us either way. 

+

43DEFAULT_EVICTION_SAMPLE = 8 

+

44 

+

45# Halving every entry's use count after this many insertions keeps the cache 

+

46# responsive to a change of working set. Without it, entries that were hot long ago 

+

47# retain counts a newly-hot entry cannot beat and are never evicted -- the classic 

+

48# failure of pure frequency eviction. Measured on a real access trace, a font-size 

+

49# change drove hit rate to 0% without aging and left it unchanged with it. 

+

50DEFAULT_AGING_INTERVAL = 10000 

+

51 

+

52# Index of each field in an entry. Entries are plain lists rather than tuples or 

+

53# objects so the counter can be bumped in place, without rehashing the key. 

+

54_VALUE = 0 

+

55_COUNT = 1 

+

56_SLOT = 2 

+

57 

+

58 

+

59class _UsageRanked(Generic[K, V]): 

+

60 """ 

+

61 Shared usage-count bookkeeping for the caches below. 

+

62 

+

63 Entries live in a dict for lookup and, in parallel, in a flat list that makes 

+

64 uniform random sampling possible. Each entry records its own index in that list 

+

65 so removal can swap in the tail element and stay O(1). 

+

66 

+

67 Subclasses supply the bound by implementing :meth:`_over_budget` and the 

+

68 accounting hooks :meth:`_record_add` / :meth:`_record_remove`. 

+

69 """ 

+

70 

+

71 def __init__(self, 

+

72 aging_interval: Optional[int] = DEFAULT_AGING_INTERVAL, 

+

73 eviction_sample: int = DEFAULT_EVICTION_SAMPLE): 

+

74 if aging_interval is not None and aging_interval <= 0: 

+

75 raise ValueError(f"aging_interval must be positive, got {aging_interval}") 

+

76 if eviction_sample <= 0: 

+

77 raise ValueError(f"eviction_sample must be positive, got {eviction_sample}") 

+

78 

+

79 self._aging_interval = aging_interval 

+

80 self._eviction_sample = eviction_sample 

+

81 

+

82 self._entries: Dict[K, List[Any]] = {} 

+

83 self._slots: List[K] = [] 

+

84 self._randrange = random.randrange 

+

85 

+

86 self._inserts_since_aging = 0 

+

87 self._hits = 0 

+

88 self._misses = 0 

+

89 self._evictions = 0 

+

90 self._agings = 0 

+

91 

+

92 # -- subclass hooks ---------------------------------------------------- 

+

93 

+

94 def _over_budget(self) -> bool: 

+

95 raise NotImplementedError 

+

96 

+

97 def _record_add(self, key: K, value: V): 

+

98 """Account for a value entering the cache.""" 

+

99 

+

100 def _record_remove(self, key: K): 

+

101 """Account for a value leaving the cache.""" 

+

102 

+

103 # -- core operations --------------------------------------------------- 

+

104 

+

105 def get(self, key: K) -> Optional[V]: 

+

106 """Return the cached value for `key`, or None, counting the use.""" 

+

107 entry = self._entries.get(key) 

+

108 if entry is None: 

+

109 self._misses += 1 

+

110 return None 

+

111 entry[_COUNT] += 1 

+

112 self._hits += 1 

+

113 return entry[_VALUE] 

+

114 

+

115 def _add_new(self, key: K, value: V): 

+

116 """Insert a key not currently present.""" 

+

117 # New entries start at 1 rather than 0 so that a single reuse is enough to 

+

118 # outrank an entry that has never been touched since the last aging pass. 

+

119 self._entries[key] = [value, 1, len(self._slots)] 

+

120 self._slots.append(key) 

+

121 self._record_add(key, value) 

+

122 

+

123 def _remove(self, key: K): 

+

124 """Remove a key outright, keeping the sampling list dense.""" 

+

125 entry = self._entries.pop(key) 

+

126 slot = entry[_SLOT] 

+

127 last = self._slots.pop() 

+

128 if last != key: 

+

129 self._slots[slot] = last 

+

130 self._entries[last][_SLOT] = slot 

+

131 self._record_remove(key) 

+

132 

+

133 def _evict_one(self) -> bool: 

+

134 """Evict the least-used member of a random sample. False if empty.""" 

+

135 count = len(self._slots) 

+

136 if not count: 136 ↛ 137line 136 didn't jump to line 137 because the condition on line 136 was never true

+

137 return False 

+

138 

+

139 if count <= self._eviction_sample: 

+

140 victim = min(self._slots, key=lambda k: self._entries[k][_COUNT]) 

+

141 else: 

+

142 randrange = self._randrange 

+

143 entries = self._entries 

+

144 slots = self._slots 

+

145 victim = slots[randrange(count)] 

+

146 best = entries[victim][_COUNT] 

+

147 for _ in range(self._eviction_sample - 1): 

+

148 candidate = slots[randrange(count)] 

+

149 score = entries[candidate][_COUNT] 

+

150 if score < best: 

+

151 victim, best = candidate, score 

+

152 

+

153 self._remove(victim) 

+

154 self._evictions += 1 

+

155 return True 

+

156 

+

157 def _evict_to_budget(self): 

+

158 while self._over_budget(): 

+

159 if not self._evict_one(): 159 ↛ 160line 159 didn't jump to line 160 because the condition on line 159 was never true

+

160 break 

+

161 

+

162 def _maybe_age(self): 

+

163 """Halve every use count once the aging interval has elapsed.""" 

+

164 if self._aging_interval is None: 

+

165 return 

+

166 self._inserts_since_aging += 1 

+

167 if self._inserts_since_aging < self._aging_interval: 

+

168 return 

+

169 

+

170 self._inserts_since_aging = 0 

+

171 self._agings += 1 

+

172 for entry in self._entries.values(): 

+

173 entry[_COUNT] = entry[_COUNT] // 2 or 1 

+

174 

+

175 def clear(self): 

+

176 """Drop all entries. Counters are preserved.""" 

+

177 self._entries.clear() 

+

178 self._slots.clear() 

+

179 self._inserts_since_aging = 0 

+

180 

+

181 def _base_stats(self) -> Dict[str, Any]: 

+

182 total = self._hits + self._misses 

+

183 return { 

+

184 'entries': len(self._entries), 

+

185 'hits': self._hits, 

+

186 'misses': self._misses, 

+

187 'evictions': self._evictions, 

+

188 'agings': self._agings, 

+

189 'hit_rate': (self._hits / total) if total else 0.0, 

+

190 } 

+

191 

+

192 def __len__(self) -> int: 

+

193 return len(self._entries) 

+

194 

+

195 def __contains__(self, key: object) -> bool: 

+

196 return key in self._entries 

+

197 

+

198 

+

199class UsageCache(_UsageRanked[K, V]): 

+

200 """ 

+

201 Usage-ranked cache bounded by number of entries. 

+

202 

+

203 Args: 

+

204 max_entries: Maximum number of entries to retain. Must be positive. 

+

205 aging_interval: Insertions between halving all use counts, or None to 

+

206 disable aging. See :data:`DEFAULT_AGING_INTERVAL`. 

+

207 eviction_sample: Entries sampled per eviction. 

+

208 """ 

+

209 

+

210 def __init__(self, max_entries: int, 

+

211 aging_interval: Optional[int] = DEFAULT_AGING_INTERVAL, 

+

212 eviction_sample: int = DEFAULT_EVICTION_SAMPLE): 

+

213 if max_entries <= 0: 

+

214 raise ValueError(f"max_entries must be positive, got {max_entries}") 

+

215 super().__init__(aging_interval, eviction_sample) 

+

216 self._max_entries = max_entries 

+

217 

+

218 def _over_budget(self) -> bool: 

+

219 return len(self._entries) > self._max_entries 

+

220 

+

221 def put(self, key: K, value: V, count: int = 1): 

+

222 """ 

+

223 Insert `value`, evicting the least-used entries past the bound. 

+

224 

+

225 Args: 

+

226 count: Initial use count. Pass a document-derived frequency to rank a 

+

227 preloaded entry ahead of words that have not been seen yet. 

+

228 """ 

+

229 existing = self._entries.get(key) 

+

230 if existing is not None: 

+

231 existing[_VALUE] = value 

+

232 existing[_COUNT] += 1 

+

233 return 

+

234 self._add_new(key, value) 

+

235 if count > 1: 

+

236 self._entries[key][_COUNT] = count 

+

237 self._evict_to_budget() 

+

238 self._maybe_age() 

+

239 

+

240 @property 

+

241 def max_entries(self) -> int: 

+

242 return self._max_entries 

+

243 

+

244 def resize(self, max_entries: int): 

+

245 """Change the bound, evicting immediately if the cache now overflows.""" 

+

246 if max_entries <= 0: 

+

247 raise ValueError(f"max_entries must be positive, got {max_entries}") 

+

248 self._max_entries = max_entries 

+

249 self._evict_to_budget() 

+

250 

+

251 def stats(self) -> Dict[str, Any]: 

+

252 """Hit/miss/eviction counters and current occupancy.""" 

+

253 stats = self._base_stats() 

+

254 stats['max_entries'] = self._max_entries 

+

255 return stats 

+

256 

+

257 

+

258class SizedUsageCache(_UsageRanked[K, V]): 

+

259 """ 

+

260 Usage-ranked cache bounded by the total size of its values. 

+

261 

+

262 Args: 

+

263 max_bytes: Maximum total value size to retain. Must be positive. 

+

264 sizer: Returns the size in bytes of a value. Called once per insertion. 

+

265 aging_interval: Insertions between halving all use counts, or None to 

+

266 disable aging. See :data:`DEFAULT_AGING_INTERVAL`. 

+

267 eviction_sample: Entries sampled per eviction. 

+

268 

+

269 A value larger than `max_bytes` on its own is returned to the caller but not 

+

270 retained, so that one oversized entry cannot flush the whole cache. 

+

271 """ 

+

272 

+

273 def __init__(self, max_bytes: int, sizer: Callable[[V], int], 

+

274 aging_interval: Optional[int] = DEFAULT_AGING_INTERVAL, 

+

275 eviction_sample: int = DEFAULT_EVICTION_SAMPLE): 

+

276 if max_bytes <= 0: 

+

277 raise ValueError(f"max_bytes must be positive, got {max_bytes}") 

+

278 super().__init__(aging_interval, eviction_sample) 

+

279 self._max_bytes = max_bytes 

+

280 self._sizer = sizer 

+

281 self._sizes: Dict[K, int] = {} 

+

282 self._total_bytes = 0 

+

283 

+

284 def _over_budget(self) -> bool: 

+

285 return self._total_bytes > self._max_bytes 

+

286 

+

287 def _record_add(self, key: K, value: V): 

+

288 size = self._sizer(value) 

+

289 self._sizes[key] = size 

+

290 self._total_bytes += size 

+

291 

+

292 def _record_remove(self, key: K): 

+

293 self._total_bytes -= self._sizes.pop(key) 

+

294 

+

295 def put(self, key: K, value: V, count: int = 1): 

+

296 """ 

+

297 Insert `value`, evicting the least-used entries past the bound. 

+

298 

+

299 Args: 

+

300 count: Initial use count. Pass a document-derived frequency to rank a 

+

301 preloaded entry ahead of words that have not been seen yet. 

+

302 """ 

+

303 if key in self._entries: 

+

304 # Re-measure: the replacement may be a different size. 

+

305 self._remove(key) 

+

306 

+

307 if self._sizer(value) > self._max_bytes: 

+

308 # Too large to ever retain; skip rather than flush everything for it. 

+

309 return 

+

310 

+

311 self._add_new(key, value) 

+

312 if count > 1: 

+

313 self._entries[key][_COUNT] = count 

+

314 self._evict_to_budget() 

+

315 self._maybe_age() 

+

316 

+

317 @property 

+

318 def max_bytes(self) -> int: 

+

319 return self._max_bytes 

+

320 

+

321 @property 

+

322 def total_bytes(self) -> int: 

+

323 return self._total_bytes 

+

324 

+

325 def resize(self, max_bytes: int): 

+

326 """Change the bound, evicting immediately if the cache now overflows.""" 

+

327 if max_bytes <= 0: 327 ↛ 328line 327 didn't jump to line 328 because the condition on line 327 was never true

+

328 raise ValueError(f"max_bytes must be positive, got {max_bytes}") 

+

329 self._max_bytes = max_bytes 

+

330 self._evict_to_budget() 

+

331 

+

332 def clear(self): 

+

333 """Drop all entries. Counters are preserved.""" 

+

334 super().clear() 

+

335 self._sizes.clear() 

+

336 self._total_bytes = 0 

+

337 

+

338 def stats(self) -> Dict[str, Any]: 

+

339 """Hit/miss/eviction counters and current occupancy.""" 

+

340 stats = self._base_stats() 

+

341 stats['total_bytes'] = self._total_bytes 

+

342 stats['max_bytes'] = self._max_bytes 

+

343 return stats 

+
+ + + diff --git a/cov_info/htmlcov/z_40407af872b0cf37_callback_registry_py.html b/cov_info/htmlcov/z_40407af872b0cf37_callback_registry_py.html new file mode 100644 index 0000000..3867b3c --- /dev/null +++ b/cov_info/htmlcov/z_40407af872b0cf37_callback_registry_py.html @@ -0,0 +1,377 @@ + + + + + Coverage for pyWebLayout/core/callback_registry.py: 92% + + + + + +
+
+

+ Coverage for pyWebLayout/core/callback_registry.py: + 92% +

+ +

+ 75 statements   + + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.15.4, + created at 2026-08-08 20:34 +0000 +

+ +
+
+
+

1""" 

+

2Callback Registry for managing interactable elements and their callbacks. 

+

3 

+

4This module provides a registry system for tracking interactive elements (links, buttons, forms) 

+

5and managing their callbacks. Supports multiple binding strategies: 

+

6- HTML id attributes for HTML-generated content 

+

7- Auto-generated ids for programmatic construction 

+

8- Type-based batch operations 

+

9""" 

+

10 

+

11from typing import Dict, List, Optional, Callable 

+

12from pyWebLayout.core.base import Interactable 

+

13 

+

14 

+

15class CallbackRegistry: 

+

16 """ 

+

17 Registry for managing interactable callbacks with multiple binding strategies. 

+

18 

+

19 Supports: 

+

20 - Direct references by object id 

+

21 - HTML id attributes (from parsed HTML) 

+

22 - Type-based queries (all buttons, all links, etc.) 

+

23 - Auto-generated ids for programmatic construction 

+

24 

+

25 This enables flexible callback binding for both HTML-generated content 

+

26 and manually constructed UIs. 

+

27 """ 

+

28 

+

29 def __init__(self): 

+

30 """Initialize an empty callback registry.""" 

+

31 self._by_reference: Dict[int, Interactable] = {} # id(obj) -> obj 

+

32 self._by_id: Dict[str, Interactable] = {} # HTML id or auto id -> obj 

+

33 self._by_type: Dict[str, List[Interactable]] = {} # type name -> [objs] 

+

34 self._auto_counter: int = 0 

+

35 

+

36 def register(self, obj: Interactable, html_id: Optional[str] = None) -> str: 

+

37 """ 

+

38 Register an interactable object with optional HTML id. 

+

39 

+

40 The object is always registered by reference (using Python's id()). 

+

41 If an html_id is provided, it's also registered by that id. 

+

42 If no html_id is provided, an auto-generated id is created. 

+

43 

+

44 Args: 

+

45 obj: The interactable object to register 

+

46 html_id: Optional HTML id attribute value (e.g., from <button id="save-btn">) 

+

47 

+

48 Returns: 

+

49 The id used for registration (either html_id or auto-generated) 

+

50 

+

51 Example: 

+

52 >>> button = ButtonText(...) 

+

53 >>> registry.register(button, html_id="save-btn") 

+

54 'save-btn' 

+

55 >>> registry.register(other_button) # No html_id 

+

56 'auto_button_0' 

+

57 """ 

+

58 # Always register by Python object id for direct lookups 

+

59 obj_id = id(obj) 

+

60 self._by_reference[obj_id] = obj 

+

61 

+

62 # Determine type name and register by type 

+

63 type_name = self._get_type_name(obj) 

+

64 if type_name not in self._by_type: 

+

65 self._by_type[type_name] = [] 

+

66 self._by_type[type_name].append(obj) 

+

67 

+

68 # Register by HTML id or generate auto id 

+

69 if html_id: 

+

70 # Use provided HTML id 

+

71 self._by_id[html_id] = obj 

+

72 return html_id 

+

73 else: 

+

74 # Generate automatic id 

+

75 auto_id = f"auto_{type_name}_{self._auto_counter}" 

+

76 self._auto_counter += 1 

+

77 self._by_id[auto_id] = obj 

+

78 return auto_id 

+

79 

+

80 def get_by_id(self, identifier: str) -> Optional[Interactable]: 

+

81 """ 

+

82 Get an interactable by its id (HTML id or auto-generated id). 

+

83 

+

84 Args: 

+

85 identifier: The id to lookup (e.g., "save-btn" or "auto_button_0") 

+

86 

+

87 Returns: 

+

88 The interactable object, or None if not found 

+

89 

+

90 Example: 

+

91 >>> button = registry.get_by_id("save-btn") 

+

92 >>> if button: 

+

93 ... button._callback = my_save_function 

+

94 """ 

+

95 return self._by_id.get(identifier) 

+

96 

+

97 def get_by_type(self, type_name: str) -> List[Interactable]: 

+

98 """ 

+

99 Get all interactables of a specific type. 

+

100 

+

101 Args: 

+

102 type_name: The type name (e.g., "link", "button", "form_field") 

+

103 

+

104 Returns: 

+

105 List of interactable objects of that type (may be empty) 

+

106 

+

107 Example: 

+

108 >>> all_buttons = registry.get_by_type("button") 

+

109 >>> for button in all_buttons: 

+

110 ... print(button.text) 

+

111 """ 

+

112 return self._by_type.get(type_name, []).copy() 

+

113 

+

114 def get_all_ids(self) -> List[str]: 

+

115 """ 

+

116 Get all registered ids (both HTML ids and auto-generated ids). 

+

117 

+

118 Returns: 

+

119 List of all ids in the registry 

+

120 

+

121 Example: 

+

122 >>> ids = registry.get_all_ids() 

+

123 >>> print(ids) 

+

124 ['save-btn', 'cancel-btn', 'auto_link_0', 'auto_button_1'] 

+

125 """ 

+

126 return list(self._by_id.keys()) 

+

127 

+

128 def get_all_types(self) -> List[str]: 

+

129 """ 

+

130 Get all registered type names. 

+

131 

+

132 Returns: 

+

133 List of type names that have registered objects 

+

134 

+

135 Example: 

+

136 >>> types = registry.get_all_types() 

+

137 >>> print(types) 

+

138 ['link', 'button', 'form_field'] 

+

139 """ 

+

140 return list(self._by_type.keys()) 

+

141 

+

142 def set_callback(self, identifier: str, callback: Callable) -> bool: 

+

143 """ 

+

144 Set the callback for an interactable by its id. 

+

145 

+

146 Args: 

+

147 identifier: The id of the interactable 

+

148 callback: The callback function to set 

+

149 

+

150 Returns: 

+

151 True if the interactable was found and callback set, False otherwise 

+

152 

+

153 Example: 

+

154 >>> def on_save(point): 

+

155 ... print("Save clicked!") 

+

156 >>> registry.set_callback("save-btn", on_save) 

+

157 True 

+

158 """ 

+

159 obj = self.get_by_id(identifier) 

+

160 if obj: 

+

161 obj._callback = callback 

+

162 return True 

+

163 return False 

+

164 

+

165 def set_callbacks_by_type(self, type_name: str, callback: Callable) -> int: 

+

166 """ 

+

167 Set the callback for all interactables of a specific type. 

+

168 

+

169 Useful for batch operations like setting a default click sound 

+

170 for all buttons, or a default link handler for all links. 

+

171 

+

172 Args: 

+

173 type_name: The type name (e.g., "button", "link") 

+

174 callback: The callback function to set 

+

175 

+

176 Returns: 

+

177 Number of objects that had their callback set 

+

178 

+

179 Example: 

+

180 >>> def play_click_sound(point): 

+

181 ... audio.play("click.wav") 

+

182 >>> count = registry.set_callbacks_by_type("button", play_click_sound) 

+

183 >>> print(f"Set callback for {count} buttons") 

+

184 """ 

+

185 objects = self.get_by_type(type_name) 

+

186 for obj in objects: 

+

187 obj._callback = callback 

+

188 return len(objects) 

+

189 

+

190 def unregister(self, identifier: str) -> bool: 

+

191 """ 

+

192 Unregister an interactable by its id. 

+

193 

+

194 Args: 

+

195 identifier: The id of the interactable to unregister 

+

196 

+

197 Returns: 

+

198 True if the interactable was found and unregistered, False otherwise 

+

199 """ 

+

200 obj = self._by_id.pop(identifier, None) 

+

201 if obj: 201 ↛ 214line 201 didn't jump to line 214 because the condition on line 201 was always true

+

202 # Remove from reference map 

+

203 self._by_reference.pop(id(obj), None) 

+

204 

+

205 # Remove from type map 

+

206 type_name = self._get_type_name(obj) 

+

207 if type_name in self._by_type: 207 ↛ 213line 207 didn't jump to line 213 because the condition on line 207 was always true

+

208 try: 

+

209 self._by_type[type_name].remove(obj) 

+

210 except ValueError: 

+

211 pass 

+

212 

+

213 return True 

+

214 return False 

+

215 

+

216 def clear(self): 

+

217 """Clear all registered interactables.""" 

+

218 self._by_reference.clear() 

+

219 self._by_id.clear() 

+

220 self._by_type.clear() 

+

221 self._auto_counter = 0 

+

222 

+

223 def count(self) -> int: 

+

224 """ 

+

225 Get the total number of registered interactables. 

+

226 

+

227 Returns: 

+

228 Total count of registered objects 

+

229 """ 

+

230 return len(self._by_id) 

+

231 

+

232 def count_by_type(self, type_name: str) -> int: 

+

233 """ 

+

234 Get the count of interactables of a specific type. 

+

235 

+

236 Args: 

+

237 type_name: The type name to count 

+

238 

+

239 Returns: 

+

240 Number of objects of that type 

+

241 """ 

+

242 return len(self._by_type.get(type_name, [])) 

+

243 

+

244 def _get_type_name(self, obj: Interactable) -> str: 

+

245 """ 

+

246 Get a normalized type name for an interactable object. 

+

247 

+

248 Args: 

+

249 obj: The interactable object 

+

250 

+

251 Returns: 

+

252 Type name string (e.g., "link", "button", "form_field") 

+

253 """ 

+

254 # Import here to avoid circular imports 

+

255 from pyWebLayout.concrete.functional import LinkText, ButtonText, FormFieldText 

+

256 

+

257 if isinstance(obj, LinkText): 

+

258 return "link" 

+

259 elif isinstance(obj, ButtonText): 

+

260 return "button" 

+

261 elif isinstance(obj, FormFieldText): 261 ↛ 265line 261 didn't jump to line 265 because the condition on line 261 was always true

+

262 return "form_field" 

+

263 else: 

+

264 # Fallback to class name 

+

265 return obj.__class__.__name__.lower() 

+

266 

+

267 def __len__(self) -> int: 

+

268 """Support len() to get count of registered interactables.""" 

+

269 return self.count() 

+

270 

+

271 def __contains__(self, identifier: str) -> bool: 

+

272 """Support 'in' operator to check if an id is registered.""" 

+

273 return identifier in self._by_id 

+

274 

+

275 def __repr__(self) -> str: 

+

276 """String representation showing registry statistics.""" 

+

277 type_counts = {t: len(objs) for t, objs in self._by_type.items()} 

+

278 return f"CallbackRegistry(total={self.count()}, types={type_counts})" 

+
+ + + diff --git a/cov_info/htmlcov/z_40407af872b0cf37_highlight_py.html b/cov_info/htmlcov/z_40407af872b0cf37_highlight_py.html new file mode 100644 index 0000000..322c44e --- /dev/null +++ b/cov_info/htmlcov/z_40407af872b0cf37_highlight_py.html @@ -0,0 +1,348 @@ + + + + + Coverage for pyWebLayout/core/highlight.py: 97% + + + + + +
+
+

+ Coverage for pyWebLayout/core/highlight.py: + 97% +

+ +

+ 87 statements   + + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.15.4, + created at 2026-08-08 20:34 +0000 +

+ +
+
+
+

1""" 

+

2Text highlighting system for ebook reader. 

+

3 

+

4Provides data structures and utilities for highlighting text regions, 

+

5managing highlight collections, and rendering highlights on pages. 

+

6""" 

+

7 

+

8from __future__ import annotations 

+

9import logging 

+

10from dataclasses import dataclass 

+

11from typing import List, Tuple, Optional, Dict, Any 

+

12from enum import Enum 

+

13from pathlib import Path 

+

14 

+

15from pyWebLayout.core.persistence import ensure_dir, read_json, write_json 

+

16 

+

17logger = logging.getLogger(__name__) 

+

18 

+

19 

+

20class HighlightColor(Enum): 

+

21 """Predefined highlight colors with RGBA values""" 

+

22 YELLOW = (255, 255, 0, 100) # Classic highlight yellow 

+

23 GREEN = (100, 255, 100, 100) # Green for verified/correct 

+

24 BLUE = (100, 200, 255, 100) # Blue for important 

+

25 PINK = (255, 150, 200, 100) # Pink for questions 

+

26 ORANGE = (255, 180, 100, 100) # Orange for warnings 

+

27 PURPLE = (200, 150, 255, 100) # Purple for definitions 

+

28 RED = (255, 100, 100, 100) # Red for errors/concerns 

+

29 

+

30 

+

31@dataclass 

+

32class Highlight: 

+

33 """ 

+

34 Represents a highlighted text region. 

+

35 

+

36 Highlights are stored with both pixel bounds (for rendering) and 

+

37 semantic bounds (text content, for persistence across font changes). 

+

38 """ 

+

39 # Identification 

+

40 id: str # Unique identifier 

+

41 

+

42 # Visual properties 

+

43 bounds: List[Tuple[int, int, int, int]] # List of (x, y, w, h) rectangles 

+

44 color: Tuple[int, int, int, int] # RGBA color 

+

45 

+

46 # Semantic properties (for persistence) 

+

47 text: str # The highlighted text 

+

48 start_word_index: Optional[int] = None # Word index in document (if available) 

+

49 end_word_index: Optional[int] = None 

+

50 

+

51 # Where in the document this highlight lives, as a serialized 

+

52 # RenderingPosition. `bounds` are pixel coordinates on one particular 

+

53 # rendering, so they stop matching as soon as the font scale or page size 

+

54 # changes; this survives repagination and is what page association uses. 

+

55 position: Optional[Dict[str, Any]] = None 

+

56 

+

57 # Metadata 

+

58 note: Optional[str] = None # Optional annotation 

+

59 tags: List[str] = None # Optional categorization tags 

+

60 timestamp: Optional[float] = None # When created 

+

61 

+

62 def __post_init__(self): 

+

63 """Initialize default values""" 

+

64 if self.tags is None: 

+

65 self.tags = [] 

+

66 

+

67 def to_dict(self) -> Dict[str, Any]: 

+

68 """Serialize to dictionary""" 

+

69 return { 

+

70 'id': self.id, 

+

71 'bounds': self.bounds, 

+

72 'color': self.color, 

+

73 'text': self.text, 

+

74 'start_word_index': self.start_word_index, 

+

75 'end_word_index': self.end_word_index, 

+

76 'position': self.position, 

+

77 'note': self.note, 

+

78 'tags': self.tags, 

+

79 'timestamp': self.timestamp 

+

80 } 

+

81 

+

82 @classmethod 

+

83 def from_dict(cls, data: Dict[str, Any]) -> 'Highlight': 

+

84 """Deserialize from dictionary""" 

+

85 return cls( 

+

86 id=data['id'], 

+

87 bounds=[tuple(b) for b in data['bounds']], 

+

88 color=tuple(data['color']), 

+

89 text=data['text'], 

+

90 start_word_index=data.get('start_word_index'), 

+

91 end_word_index=data.get('end_word_index'), 

+

92 position=data.get('position'), 

+

93 note=data.get('note'), 

+

94 tags=data.get('tags', []), 

+

95 timestamp=data.get('timestamp') 

+

96 ) 

+

97 

+

98 

+

99class HighlightManager: 

+

100 """ 

+

101 Manages highlights for a document. 

+

102 

+

103 Handles adding, removing, listing, and persisting highlights. 

+

104 """ 

+

105 

+

106 def __init__(self, document_id: str, highlights_dir: str = "highlights"): 

+

107 """ 

+

108 Initialize highlight manager. 

+

109 

+

110 Args: 

+

111 document_id: Unique identifier for the document 

+

112 highlights_dir: Directory to store highlight data 

+

113 """ 

+

114 self.document_id = document_id 

+

115 self.highlights_dir = ensure_dir(highlights_dir) 

+

116 self.highlights: Dict[str, Highlight] = {} # id -> Highlight 

+

117 

+

118 # Load existing highlights 

+

119 self._load_highlights() 

+

120 

+

121 def add_highlight(self, highlight: Highlight) -> None: 

+

122 """ 

+

123 Add a highlight. 

+

124 

+

125 Args: 

+

126 highlight: Highlight to add 

+

127 """ 

+

128 self.highlights[highlight.id] = highlight 

+

129 self._save_highlights() 

+

130 

+

131 def remove_highlight(self, highlight_id: str) -> bool: 

+

132 """ 

+

133 Remove a highlight by ID. 

+

134 

+

135 Args: 

+

136 highlight_id: ID of highlight to remove 

+

137 

+

138 Returns: 

+

139 True if removed, False if not found 

+

140 """ 

+

141 if highlight_id in self.highlights: 

+

142 del self.highlights[highlight_id] 

+

143 self._save_highlights() 

+

144 return True 

+

145 return False 

+

146 

+

147 def get_highlight(self, highlight_id: str) -> Optional[Highlight]: 

+

148 """Get a highlight by ID""" 

+

149 return self.highlights.get(highlight_id) 

+

150 

+

151 def list_highlights(self) -> List[Highlight]: 

+

152 """Get all highlights""" 

+

153 return list(self.highlights.values()) 

+

154 

+

155 def clear_all(self) -> None: 

+

156 """Remove all highlights""" 

+

157 self.highlights.clear() 

+

158 self._save_highlights() 

+

159 

+

160 def get_highlights_for_page( 

+

161 self, page_bounds: Tuple[int, int, int, int]) -> List[Highlight]: 

+

162 """ 

+

163 Get highlights that appear on a specific page. 

+

164 

+

165 Args: 

+

166 page_bounds: Page bounds (x, y, width, height) 

+

167 

+

168 Returns: 

+

169 List of highlights on this page 

+

170 """ 

+

171 page_x, page_y, page_w, page_h = page_bounds 

+

172 page_highlights = [] 

+

173 

+

174 for highlight in self.highlights.values(): 

+

175 # Check if any highlight bounds overlap with page 

+

176 for hx, hy, hw, hh in highlight.bounds: 

+

177 if (hx < page_x + page_w and hx + hw > page_x and 

+

178 hy < page_y + page_h and hy + hh > page_y): 

+

179 page_highlights.append(highlight) 

+

180 break 

+

181 

+

182 return page_highlights 

+

183 

+

184 def _get_filepath(self) -> Path: 

+

185 """Get filepath for this document's highlights""" 

+

186 return self.highlights_dir / f"{self.document_id}_highlights.json" 

+

187 

+

188 def _save_highlights(self) -> None: 

+

189 """Persist highlights to disk""" 

+

190 write_json(self._get_filepath(), { 

+

191 'document_id': self.document_id, 

+

192 'highlights': [h.to_dict() for h in self.highlights.values()] 

+

193 }) 

+

194 

+

195 def _load_highlights(self) -> None: 

+

196 """Load highlights from disk""" 

+

197 data = read_json(self._get_filepath(), {}) 

+

198 try: 

+

199 self.highlights = { 

+

200 h['id']: Highlight.from_dict(h) 

+

201 for h in data.get('highlights', []) 

+

202 } 

+

203 except (AttributeError, TypeError, KeyError): 

+

204 logger.warning("Highlight file %s is not in the expected shape; ignoring it", 

+

205 self._get_filepath(), exc_info=True) 

+

206 self.highlights = {} 

+

207 

+

208 

+

209def create_highlight_from_query_result( 

+

210 result, 

+

211 color: Tuple[int, int, int, int] = HighlightColor.YELLOW.value, 

+

212 note: Optional[str] = None, 

+

213 tags: Optional[List[str]] = None, 

+

214 position: Optional[Dict[str, Any]] = None 

+

215) -> Highlight: 

+

216 """ 

+

217 Create a highlight from a QueryResult. 

+

218 

+

219 Args: 

+

220 result: QueryResult from query_point or query_range 

+

221 color: RGBA color tuple 

+

222 note: Optional annotation 

+

223 tags: Optional categorization tags 

+

224 position: Serialized RenderingPosition of the page the result came from 

+

225 

+

226 Returns: 

+

227 Highlight instance 

+

228 """ 

+

229 from time import time 

+

230 import uuid 

+

231 

+

232 # Handle single result or SelectionRange 

+

233 if hasattr(result, 'results'): # SelectionRange 

+

234 bounds = result.bounds_list 

+

235 text = result.text 

+

236 else: # Single QueryResult 

+

237 bounds = [result.bounds] 

+

238 text = result.text or "" 

+

239 

+

240 return Highlight( 

+

241 id=str(uuid.uuid4()), 

+

242 bounds=bounds, 

+

243 color=color, 

+

244 text=text, 

+

245 position=position, 

+

246 note=note, 

+

247 tags=tags or [], 

+

248 timestamp=time() 

+

249 ) 

+
+ + + diff --git a/cov_info/htmlcov/z_40407af872b0cf37_persistence_py.html b/cov_info/htmlcov/z_40407af872b0cf37_persistence_py.html new file mode 100644 index 0000000..f245e8c --- /dev/null +++ b/cov_info/htmlcov/z_40407af872b0cf37_persistence_py.html @@ -0,0 +1,158 @@ + + + + + Coverage for pyWebLayout/core/persistence.py: 90% + + + + + +
+
+

+ Coverage for pyWebLayout/core/persistence.py: + 90% +

+ +

+ 27 statements   + + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.15.4, + created at 2026-08-08 20:34 +0000 +

+ +
+
+
+

1""" 

+

2Small JSON-file helpers shared by the per-document stores. 

+

3 

+

4BookmarkManager and HighlightManager both keep a JSON file per document under a 

+

5directory, and both had their own copy of "make the directory, try to read it, 

+

6swallow and print on failure". The duplication is the point of this module; the 

+

7file formats themselves stay owned by each store. 

+

8""" 

+

9 

+

10from __future__ import annotations 

+

11 

+

12import json 

+

13import logging 

+

14from pathlib import Path 

+

15from typing import Any 

+

16 

+

17logger = logging.getLogger(__name__) 

+

18 

+

19 

+

20def ensure_dir(path: str | Path) -> Path: 

+

21 """Return `path` as a Path, creating it and any missing parents.""" 

+

22 directory = Path(path) 

+

23 directory.mkdir(parents=True, exist_ok=True) 

+

24 return directory 

+

25 

+

26 

+

27def read_json(path: Path, default: Any) -> Any: 

+

28 """ 

+

29 Read JSON from `path`, returning `default` if it is missing or unreadable. 

+

30 

+

31 A corrupt store must not stop a book from opening, so failures are logged 

+

32 and swallowed. `default` is returned as given, so pass a fresh mutable if 

+

33 the caller intends to mutate it. 

+

34 """ 

+

35 if not path.exists(): 

+

36 return default 

+

37 

+

38 try: 

+

39 with open(path, 'r', encoding='utf-8') as handle: 

+

40 return json.load(handle) 

+

41 except (OSError, ValueError): 

+

42 logger.warning("Could not read %s; ignoring its contents", path, exc_info=True) 

+

43 return default 

+

44 

+

45 

+

46def write_json(path: Path, data: Any) -> bool: 

+

47 """ 

+

48 Write `data` to `path` as JSON. 

+

49 

+

50 Returns True on success. Failures are logged rather than raised: losing a 

+

51 bookmark is not a reason to take down the reader. 

+

52 """ 

+

53 try: 

+

54 with open(path, 'w', encoding='utf-8') as handle: 

+

55 json.dump(data, handle, indent=2) 

+

56 return True 

+

57 except (OSError, TypeError, ValueError): 

+

58 logger.error("Could not write %s", path, exc_info=True) 

+

59 return False 

+
+ + + diff --git a/cov_info/htmlcov/z_40407af872b0cf37_query_py.html b/cov_info/htmlcov/z_40407af872b0cf37_query_py.html new file mode 100644 index 0000000..78fcefd --- /dev/null +++ b/cov_info/htmlcov/z_40407af872b0cf37_query_py.html @@ -0,0 +1,185 @@ + + + + + Coverage for pyWebLayout/core/query.py: 94% + + + + + +
+
+

+ Coverage for pyWebLayout/core/query.py: + 94% +

+ +

+ 33 statements   + + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.15.4, + created at 2026-08-08 20:34 +0000 +

+ +
+
+
+

1""" 

+

2Query system for pixel-to-content mapping. 

+

3 

+

4This module provides data structures for querying rendered content, 

+

5enabling interactive features like link clicking, word definition lookup, 

+

6and text selection. 

+

7""" 

+

8 

+

9from __future__ import annotations 

+

10from dataclasses import dataclass 

+

11from typing import Optional, Tuple, List, Any, TYPE_CHECKING 

+

12 

+

13if TYPE_CHECKING: 13 ↛ 14line 13 didn't jump to line 14 because the condition on line 13 was never true

+

14 from pyWebLayout.core.base import Queriable 

+

15 

+

16 

+

17@dataclass 

+

18class QueryResult: 

+

19 """ 

+

20 Result of querying a point on a rendered page. 

+

21 

+

22 This encapsulates all information about what was found at a pixel location, 

+

23 including geometry, content, and interaction capabilities. 

+

24 """ 

+

25 # What was found 

+

26 object: 'Queriable' # The object at this point 

+

27 object_type: str # "link", "text", "image", "button", "word", "empty" 

+

28 

+

29 # Geometry 

+

30 bounds: Tuple[int, int, int, int] # (x, y, width, height) in page coordinates 

+

31 

+

32 # Content (for text/words) 

+

33 text: Optional[str] = None 

+

34 word_index: Optional[int] = None # Index in abstract document structure 

+

35 block_index: Optional[int] = None # Block index in document 

+

36 

+

37 # Interaction (for links/buttons) 

+

38 is_interactive: bool = False 

+

39 link_target: Optional[str] = None # URL or internal reference 

+

40 callback: Optional[Any] = None # Interaction callback 

+

41 

+

42 # Hierarchy (for debugging/traversal) 

+

43 parent_line: Optional[Any] = None 

+

44 parent_page: Optional[Any] = None 

+

45 

+

46 def to_dict(self) -> dict: 

+

47 """Convert to dictionary for serialization""" 

+

48 return { 

+

49 'object_type': self.object_type, 

+

50 'bounds': self.bounds, 

+

51 'text': self.text, 

+

52 'is_interactive': self.is_interactive, 

+

53 'link_target': self.link_target, 

+

54 'word_index': self.word_index, 

+

55 'block_index': self.block_index 

+

56 } 

+

57 

+

58 

+

59@dataclass 

+

60class SelectionRange: 

+

61 """ 

+

62 Represents a range of selected text between two points. 

+

63 """ 

+

64 start_point: Tuple[int, int] 

+

65 end_point: Tuple[int, int] 

+

66 results: List[QueryResult] # All query results in the range 

+

67 

+

68 @property 

+

69 def text(self) -> str: 

+

70 """Get concatenated text from all results""" 

+

71 return " ".join(r.text for r in self.results if r.text) 

+

72 

+

73 @property 

+

74 def bounds_list(self) -> List[Tuple[int, int, int, int]]: 

+

75 """Get list of all bounding boxes for highlighting""" 

+

76 return [r.bounds for r in self.results] 

+

77 

+

78 def to_dict(self) -> dict: 

+

79 """Convert to dictionary for serialization""" 

+

80 return { 

+

81 'start': self.start_point, 

+

82 'end': self.end_point, 

+

83 'text': self.text, 

+

84 'word_count': len(self.results), 

+

85 'bounds': self.bounds_list 

+

86 } 

+
+ + + diff --git a/cov_info/htmlcov/z_427cc3035faf7633___init___py.html b/cov_info/htmlcov/z_427cc3035faf7633___init___py.html new file mode 100644 index 0000000..0064709 --- /dev/null +++ b/cov_info/htmlcov/z_427cc3035faf7633___init___py.html @@ -0,0 +1,110 @@ + + + + + Coverage for pyWebLayout/layout/__init__.py: 100% + + + + + +
+
+

+ Coverage for pyWebLayout/layout/__init__.py: + 100% +

+ +

+ 0 statements   + + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.15.4, + created at 2026-08-08 20:34 +0000 +

+ +
+
+
+

1""" 

+

2Typesetting module for the pyWebLayout library. 

+

3 

+

4This package handles the organization and arrangement of elements for rendering, including: 

+

5- Flow layout algorithms 

+

6- Container management 

+

7- Element positioning and sizing 

+

8- Content wrapping and overflow 

+

9- Coordinate systems and transformations 

+

10- Pagination for book-like content 

+

11""" 

+
+ + + diff --git a/cov_info/htmlcov/z_427cc3035faf7633_document_layouter_py.html b/cov_info/htmlcov/z_427cc3035faf7633_document_layouter_py.html new file mode 100644 index 0000000..1c57226 --- /dev/null +++ b/cov_info/htmlcov/z_427cc3035faf7633_document_layouter_py.html @@ -0,0 +1,836 @@ + + + + + Coverage for pyWebLayout/layout/document_layouter.py: 77% + + + + + +
+
+

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

+ +

+ 219 statements   + + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.15.4, + created at 2026-08-08 20:34 +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, Heading, 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 # Alignment for text that does not specify its own. Headings are never 

+

55 # justified - stretching a two-word title across the measure is always wrong - 

+

56 # so they fall back to flush left. 

+

57 default_alignment = getattr(page.style, 'default_alignment', None) 

+

58 if not isinstance(default_alignment, Alignment): 

+

59 default_alignment = Alignment.JUSTIFY 

+

60 if isinstance(paragraph, Heading): 

+

61 default_alignment = Alignment.LEFT 

+

62 

+

63 if isinstance(paragraph.style, Font): 

+

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

+

65 font = paragraph.style 

+

66 # Use default word spacing constraints based on font size 

+

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

+

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

+

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

+

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

+

71 text_align = default_alignment 

+

72 else: 

+

73 # paragraph.style is an AbstractStyle, resolve it 

+

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

+

75 from pyWebLayout.style.abstract_style import FontSize 

+

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

+

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

+

78 base_font_size = 16 

+

79 else: 

+

80 base_font_size = int(paragraph.style.font_size) 

+

81 

+

82 rendering_context = RenderingContext(base_font_size=base_font_size) 

+

83 style_resolver = StyleResolver(rendering_context) 

+

84 style_registry = ConcreteStyleRegistry(style_resolver) 

+

85 concrete_style = style_registry.get_concrete_style(paragraph.style) 

+

86 font = concrete_style.create_font() 

+

87 word_spacing_constraints = ( 

+

88 int(concrete_style.word_spacing_min), 

+

89 int(concrete_style.word_spacing_max) 

+

90 ) 

+

91 # text_align is None when the source did not specify one. 

+

92 text_align = concrete_style.text_align or default_alignment 

+

93 

+

94 # Apply page-level word spacing override if specified 

+

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

+

96 page.style, 

+

97 'word_spacing') and isinstance( 

+

98 page.style.word_spacing, 

+

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

+

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

+

101 min_ws, max_ws = word_spacing_constraints 

+

102 word_spacing_constraints = ( 

+

103 min_ws + page.style.word_spacing, 

+

104 max_ws + page.style.word_spacing 

+

105 ) 

+

106 

+

107 # Apply alignment override if provided 

+

108 if alignment_override is not None: 

+

109 text_align = alignment_override 

+

110 

+

111 # Cap font size to page maximum if needed 

+

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

+

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

+

114 if hasattr(paragraph, 'get_or_create_font'): 

+

115 font = paragraph.get_or_create_font( 

+

116 font_path=font._font_path, 

+

117 font_size=page.style.max_font_size, 

+

118 colour=font.colour, 

+

119 weight=font.weight, 

+

120 style=font.style, 

+

121 decoration=font.decoration, 

+

122 background=font.background 

+

123 ) 

+

124 else: 

+

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

+

126 font = Font( 

+

127 font_path=font._font_path, 

+

128 font_size=page.style.max_font_size, 

+

129 colour=font.colour, 

+

130 weight=font.weight, 

+

131 style=font.style, 

+

132 decoration=font.decoration, 

+

133 background=font.background 

+

134 ) 

+

135 

+

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

+

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

+

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

+

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

+

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

+

141 if not isinstance(line_spacing_value, int): 

+

142 line_spacing_value = 5 

+

143 baseline_spacing = font.font_size + line_spacing_value 

+

144 

+

145 # Get font metrics for boundary checking 

+

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

+

147 

+

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

+

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

+

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

+

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

+

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

+

153 return None 

+

154 

+

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

+

156 # For subsequent lines, use current y_offset which tracks 

+

157 # baseline-to-baseline spacing 

+

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

+

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

+

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

+

161 y_cursor = page._current_y_offset 

+

162 else: 

+

163 y_cursor = page._current_y_offset 

+

164 x_cursor = page.content_origin[0] 

+

165 

+

166 # `word` is accepted for call-site readability only: the line that is about 

+

167 # to be created measures it when it is added, so measuring it here as well 

+

168 # only paid for a Text object that was immediately discarded. 

+

169 

+

170 return Line( 

+

171 spacing=word_spacing_constraints, 

+

172 origin=(x_cursor, y_cursor), 

+

173 size=(page.available_width, baseline_spacing), 

+

174 draw=page.measurement_draw, 

+

175 font=font, 

+

176 halign=text_align 

+

177 ) 

+

178 

+

179 # Create initial line 

+

180 current_line = create_new_line() 

+

181 if not current_line: 

+

182 return False, start_word, pretext 

+

183 

+

184 page.add_child(current_line) 

+

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

+

186 # No need to manually increment it here 

+

187 

+

188 # Track current position in paragraph 

+

189 current_pretext = pretext 

+

190 

+

191 # Process words starting from start_word 

+

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

+

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

+

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

+

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

+

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

+

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

+

198 

+

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

+

200 

+

201 if success: 

+

202 # Word fit successfully 

+

203 if overflow_text is not None: 

+

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

+

205 current_pretext = overflow_text 

+

206 current_line = create_new_line(overflow_text) 

+

207 if not current_line: 

+

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

+

209 return False, i, overflow_text 

+

210 page.add_child(current_line) 

+

211 # Note: add_child already updates _current_y_offset 

+

212 # Continue to the next word 

+

213 continue 

+

214 else: 

+

215 # No overflow, clear pretext 

+

216 current_pretext = None 

+

217 else: 

+

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

+

219 current_line = create_new_line(word) 

+

220 if not current_line: 

+

221 # Page is full, return current position 

+

222 return False, i, overflow_text 

+

223 

+

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

+

225 temp_text = Text.from_word(word, page.measurement_draw) 

+

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

+

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

+

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

+

229 # Try to hyphenate the word 

+

230 splits = [ 

+

231 (Text( 

+

232 pair[0], 

+

233 word.style, 

+

234 page.measurement_draw, 

+

235 line=current_line, 

+

236 source=word), 

+

237 Text( 

+

238 pair[1], 

+

239 word.style, 

+

240 page.measurement_draw, 

+

241 line=current_line, 

+

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

+

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

+

244 # Use the first hyphenation point 

+

245 first_part, second_part = splits[0] 

+

246 current_line.add_word(word, first_part) 

+

247 current_pretext = second_part 

+

248 continue 

+

249 

+

250 page.add_child(current_line) 

+

251 # Note: add_child already updates _current_y_offset 

+

252 

+

253 # Try to add the word to the new line 

+

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

+

255 

+

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

+

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

+

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

+

259 if overflow_text: 

+

260 # Word was hyphenated, continue with the overflow 

+

261 current_pretext = overflow_text 

+

262 continue 

+

263 else: 

+

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

+

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

+

266 return False, i, None 

+

267 else: 

+

268 current_pretext = overflow_text # May be None or hyphenated remainder 

+

269 

+

270 # All words processed successfully. The line holding the final word is the 

+

271 # end of the paragraph, so it is rendered at its natural width rather than 

+

272 # justified to the full column. A paragraph continued on the next page does 

+

273 # not reach here, so its lines stay justified - which is correct. 

+

274 if current_line is not None: 274 ↛ 277line 274 didn't jump to line 277 because the condition on line 274 was always true

+

275 current_line.is_paragraph_end = True 

+

276 

+

277 return True, None, None 

+

278 

+

279 

+

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

+

281 """ 

+

282 Handle a page break element. 

+

283 

+

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

+

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

+

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

+

287 

+

288 Args: 

+

289 page_break: The PageBreak block 

+

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

+

291 

+

292 Returns: 

+

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

+

294 """ 

+

295 # Page break always forces a new page 

+

296 return False 

+

297 

+

298 

+

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

+

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

+

301 """ 

+

302 Layout an image within a given page. 

+

303 

+

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

+

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

+

306 

+

307 Args: 

+

308 image: The abstract Image object to layout 

+

309 page: The page to layout the image on 

+

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

+

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

+

312 

+

313 Returns: 

+

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

+

315 """ 

+

316 # Use page available width if max_width not specified 

+

317 if max_width is None: 

+

318 max_width = page.available_width 

+

319 

+

320 # Calculate available height on page 

+

321 available_height = page.remaining_height 

+

322 

+

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

+

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

+

325 return False 

+

326 

+

327 if max_height is None: 

+

328 max_height = available_height 

+

329 else: 

+

330 max_height = min(max_height, available_height) 

+

331 

+

332 # Calculate scaled dimensions 

+

333 scaled_width, scaled_height = image.calculate_scaled_dimensions( 

+

334 max_width, max_height) 

+

335 

+

336 # Check if image fits on current page 

+

337 if scaled_height is None or scaled_height > available_height: 

+

338 return False 

+

339 

+

340 # Create renderable image 

+

341 x_offset = page.content_origin[0] 

+

342 y_offset = page._current_y_offset 

+

343 

+

344 # Access page.draw to ensure canvas is initialized 

+

345 _ = page.draw 

+

346 

+

347 renderable_image = RenderableImage( 

+

348 image=image, 

+

349 canvas=page._canvas, 

+

350 max_width=max_width, 

+

351 max_height=max_height, 

+

352 origin=(x_offset, y_offset), 

+

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

+

354 halign=Alignment.CENTER, 

+

355 valign=Alignment.TOP 

+

356 ) 

+

357 

+

358 # Add to page 

+

359 page.add_child(renderable_image) 

+

360 

+

361 return True 

+

362 

+

363 

+

364def table_layouter( 

+

365 table: Table, 

+

366 page: Page, 

+

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

+

368 """ 

+

369 Layout a table within a given page. 

+

370 

+

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

+

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

+

373 

+

374 Args: 

+

375 table: The abstract Table object to layout 

+

376 page: The page to layout the table on 

+

377 style: Optional table styling configuration 

+

378 

+

379 Returns: 

+

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

+

381 """ 

+

382 # Calculate available space 

+

383 available_width = page.available_width 

+

384 x_offset = page.content_origin[0] 

+

385 y_offset = page._current_y_offset 

+

386 

+

387 # Access page.draw to ensure canvas is initialized 

+

388 draw = page.draw 

+

389 canvas = page._canvas 

+

390 

+

391 # Create table renderer 

+

392 origin = (x_offset, y_offset) 

+

393 renderer = TableRenderer( 

+

394 table=table, 

+

395 origin=origin, 

+

396 available_width=available_width, 

+

397 draw=draw, 

+

398 style=style, 

+

399 canvas=canvas 

+

400 ) 

+

401 

+

402 # Check if table fits on current page 

+

403 table_height = renderer.size[1] 

+

404 available_height = page.remaining_height 

+

405 

+

406 if table_height > available_height: 

+

407 return False 

+

408 

+

409 # Render the table 

+

410 renderer.render() 

+

411 

+

412 # Update page y-offset 

+

413 page._current_y_offset = y_offset + table_height 

+

414 

+

415 return True 

+

416 

+

417 

+

418def button_layouter(button: Button, 

+

419 page: Page, 

+

420 font: Optional[Font] = None, 

+

421 padding: Tuple[int, 

+

422 int, 

+

423 int, 

+

424 int] = (4, 

+

425 8, 

+

426 4, 

+

427 8)) -> Tuple[bool, 

+

428 str]: 

+

429 """ 

+

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

+

431 

+

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

+

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

+

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

+

435 

+

436 Args: 

+

437 button: The abstract Button object to layout 

+

438 page: The page to layout the button on 

+

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

+

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

+

441 

+

442 Returns: 

+

443 Tuple of: 

+

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

+

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

+

446 """ 

+

447 # Use provided font or create a default one 

+

448 if font is None: 

+

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

+

450 

+

451 # Calculate available space 

+

452 available_height = page.remaining_height 

+

453 

+

454 # Create ButtonText renderable 

+

455 button_text = ButtonText(button, font, page.measurement_draw, padding=padding) 

+

456 

+

457 # Check if button fits on current page 

+

458 button_height = button_text.size[1] 

+

459 if button_height > available_height: 

+

460 return False, "" 

+

461 

+

462 # Position the button 

+

463 x_offset = page.content_origin[0] 

+

464 y_offset = page._current_y_offset 

+

465 

+

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

+

467 

+

468 # Register in callback registry 

+

469 html_id = button.html_id 

+

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

+

471 

+

472 # Add to page 

+

473 page.add_child(button_text) 

+

474 

+

475 return True, registered_id 

+

476 

+

477 

+

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

+

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

+

480 """ 

+

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

+

482 

+

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

+

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

+

485 

+

486 Args: 

+

487 field: The abstract FormField object to layout 

+

488 page: The page to layout the field on 

+

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

+

490 field_height: Height of the input field area 

+

491 

+

492 Returns: 

+

493 Tuple of: 

+

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

+

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

+

496 """ 

+

497 # Use provided font or create a default one 

+

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

+

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

+

500 

+

501 # Calculate available space 

+

502 available_height = page.remaining_height 

+

503 

+

504 # Create FormFieldText renderable 

+

505 field_text = FormFieldText(field, font, page.measurement_draw, 

+

506 field_height=field_height) 

+

507 

+

508 # Check if field fits on current page 

+

509 total_field_height = field_text.size[1] 

+

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

+

511 return False, "" 

+

512 

+

513 # Position the field 

+

514 x_offset = page.content_origin[0] 

+

515 y_offset = page._current_y_offset 

+

516 

+

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

+

518 

+

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

+

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

+

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

+

522 

+

523 # Add to page 

+

524 page.add_child(field_text) 

+

525 

+

526 return True, registered_id 

+

527 

+

528 

+

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

+

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

+

531 """ 

+

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

+

533 

+

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

+

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

+

536 the page's callback registry. 

+

537 

+

538 Args: 

+

539 form: The abstract Form object to layout 

+

540 page: The page to layout the form on 

+

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

+

542 field_spacing: Vertical spacing between fields in pixels 

+

543 

+

544 Returns: 

+

545 Tuple of: 

+

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

+

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

+

548 """ 

+

549 # Use provided font or create a default one 

+

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

+

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

+

552 

+

553 # Track registered field ids 

+

554 field_ids = [] 

+

555 

+

556 # Layout each field in the form 

+

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

+

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

+

559 if field_ids: 

+

560 page._current_y_offset += field_spacing 

+

561 

+

562 # Layout the field 

+

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

+

564 

+

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

+

566 # Couldn't fit this field, return failure 

+

567 return False, [] 

+

568 

+

569 field_ids.append(field_id) 

+

570 

+

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

+

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

+

573 # for submission callbacks 

+

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

+

575 

+

576 return True, field_ids 

+

577 

+

578 

+

579class DocumentLayouter: 

+

580 """ 

+

581 Document layouter that orchestrates layout of various abstract elements. 

+

582 

+

583 Delegates to specialized layouters for different content types: 

+

584 - paragraph_layouter for text paragraphs 

+

585 - image_layouter for images 

+

586 - table_layouter for tables 

+

587 

+

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

+

589 and page context while delegating specific layout tasks to specialized 

+

590 layouter functions. 

+

591 """ 

+

592 

+

593 def __init__(self, page: Page): 

+

594 """ 

+

595 Initialize the document layouter with a page. 

+

596 

+

597 Args: 

+

598 page: The page to layout content on 

+

599 """ 

+

600 self.page = page 

+

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

+

602 if hasattr(page, 'style_resolver'): 

+

603 style_resolver = page.style_resolver 

+

604 else: 

+

605 # Create a default rendering context and style resolver 

+

606 from pyWebLayout.style.concrete_style import RenderingContext 

+

607 context = RenderingContext() 

+

608 style_resolver = StyleResolver(context) 

+

609 self.style_registry = ConcreteStyleRegistry(style_resolver) 

+

610 

+

611 def layout_paragraph(self, 

+

612 paragraph: Paragraph, 

+

613 start_word: int = 0, 

+

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

+

615 Optional[int], 

+

616 Optional[Text]]: 

+

617 """ 

+

618 Layout a paragraph using the paragraph_layouter. 

+

619 

+

620 Args: 

+

621 paragraph: The paragraph to layout 

+

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

+

623 pretext: Optional pretext from a previous hyphenated word 

+

624 

+

625 Returns: 

+

626 Tuple of (success, failed_word_index, remaining_pretext) 

+

627 """ 

+

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

+

629 

+

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

+

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

+

632 """ 

+

633 Layout an image using the image_layouter. 

+

634 

+

635 Args: 

+

636 image: The abstract Image object to layout 

+

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

+

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

+

639 

+

640 Returns: 

+

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

+

642 """ 

+

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

+

644 

+

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

+

646 """ 

+

647 Layout a table using the table_layouter. 

+

648 

+

649 Args: 

+

650 table: The abstract Table object to layout 

+

651 style: Optional table styling configuration 

+

652 

+

653 Returns: 

+

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

+

655 """ 

+

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

+

657 

+

658 def layout_button(self, 

+

659 button: Button, 

+

660 font: Optional[Font] = None, 

+

661 padding: Tuple[int, 

+

662 int, 

+

663 int, 

+

664 int] = (4, 

+

665 8, 

+

666 4, 

+

667 8)) -> Tuple[bool, 

+

668 str]: 

+

669 """ 

+

670 Layout a button using the button_layouter. 

+

671 

+

672 Args: 

+

673 button: The abstract Button object to layout 

+

674 font: Optional font for button text 

+

675 padding: Padding around button text 

+

676 

+

677 Returns: 

+

678 Tuple of (success, registered_id) 

+

679 """ 

+

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

+

681 

+

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

+

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

+

684 """ 

+

685 Layout a form using the form_layouter. 

+

686 

+

687 Args: 

+

688 form: The abstract Form object to layout 

+

689 font: Optional font for field labels 

+

690 field_spacing: Vertical spacing between fields 

+

691 

+

692 Returns: 

+

693 Tuple of (success, list_of_field_ids) 

+

694 """ 

+

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

+

696 

+

697 def layout_document( 

+

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

+

699 """ 

+

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

+

701 

+

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

+

703 - Paragraphs are handled by layout_paragraph 

+

704 - Images are handled by layout_image 

+

705 - Tables are handled by layout_table 

+

706 - Buttons are handled by layout_button 

+

707 - Forms are handled by layout_form 

+

708 

+

709 Args: 

+

710 elements: List of abstract elements to layout 

+

711 

+

712 Returns: 

+

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

+

714 """ 

+

715 for element in elements: 

+

716 if isinstance(element, Paragraph): 

+

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

+

718 if not success: 

+

719 return False 

+

720 elif isinstance(element, AbstractImage): 

+

721 success = self.layout_image(element) 

+

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

+

723 return False 

+

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

+

725 success = self.layout_table(element) 

+

726 if not success: 

+

727 return False 

+

728 elif isinstance(element, Button): 

+

729 success, _ = self.layout_button(element) 

+

730 if not success: 

+

731 return False 

+

732 elif isinstance(element, Form): 

+

733 success, _ = self.layout_form(element) 

+

734 if not success: 

+

735 return False 

+

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

+

737 return True 

+
+ + + diff --git a/cov_info/htmlcov/z_427cc3035faf7633_ereader_layout_py.html b/cov_info/htmlcov/z_427cc3035faf7633_ereader_layout_py.html new file mode 100644 index 0000000..2d2dcde --- /dev/null +++ b/cov_info/htmlcov/z_427cc3035faf7633_ereader_layout_py.html @@ -0,0 +1,917 @@ + + + + + Coverage for pyWebLayout/layout/ereader_layout.py: 90% + + + + + +
+
+

+ Coverage for pyWebLayout/layout/ereader_layout.py: + 90% +

+ +

+ 304 statements   + + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.15.4, + created at 2026-08-08 20:34 +0000 +

+ +
+
+
+

1""" 

+

2Enhanced ereader layout system with position tracking, font scaling, and multi-page support. 

+

3 

+

4This module provides the core infrastructure for building high-performance ereader applications 

+

5with features like: 

+

6- Precise position tracking tied to abstract document structure 

+

7- Font scaling support 

+

8- Bidirectional page rendering (forward/backward) 

+

9- Chapter navigation based on HTML headings 

+

10- Multi-process page buffering 

+

11- Sub-second page rendering performance 

+

12""" 

+

13 

+

14from __future__ import annotations 

+

15from dataclasses import dataclass, asdict 

+

16from typing import List, Dict, Tuple, Optional, Any 

+

17 

+

18from pyWebLayout.abstract.block import ( 

+

19 Block, Paragraph, Heading, HeadingLevel, Table, TableRow, TableCell, 

+

20 HList, ListItem, Quote, Image) 

+

21from pyWebLayout.abstract.inline import Word 

+

22from pyWebLayout.concrete.page import Page 

+

23from pyWebLayout.concrete.text import Text 

+

24from pyWebLayout.style.page_style import PageStyle 

+

25from pyWebLayout.style import Font 

+

26from pyWebLayout.style.fonts import BundledFont, get_bundled_font_path, FontWeight, FontStyle 

+

27from pyWebLayout.layout.document_layouter import paragraph_layouter, image_layouter 

+

28 

+

29 

+

30@dataclass 

+

31class RenderingPosition: 

+

32 """ 

+

33 Complete state for resuming rendering at any point in a document. 

+

34 Position is tied to abstract document structure for stability across font changes. 

+

35 """ 

+

36 chapter_index: int = 0 # Which chapter (based on headings) 

+

37 block_index: int = 0 # Which block within chapter 

+

38 # Which word within block (for paragraphs) 

+

39 word_index: int = 0 

+

40 table_row: int = 0 # Which row for tables 

+

41 table_col: int = 0 # Which column for tables 

+

42 list_item_index: int = 0 # Which item for lists 

+

43 remaining_pretext: Optional[str] = None # Hyphenated word continuation 

+

44 page_y_offset: int = 0 # Vertical position on page 

+

45 

+

46 def _key(self) -> Tuple[Any, ...]: 

+

47 """ 

+

48 The fields in declaration order. 

+

49 

+

50 Copying, comparing and hashing a position all used to go through 

+

51 dataclasses.asdict, which walks the field list and deep-copies each value. 

+

52 Every field here is an immutable scalar, so that traversal bought nothing 

+

53 and these three run constantly during page navigation and buffer lookups. 

+

54 """ 

+

55 return (self.chapter_index, self.block_index, self.word_index, 

+

56 self.table_row, self.table_col, self.list_item_index, 

+

57 self.remaining_pretext, self.page_y_offset) 

+

58 

+

59 def to_dict(self) -> Dict[str, Any]: 

+

60 """Serialize position for saving to file/database""" 

+

61 return asdict(self) 

+

62 

+

63 @classmethod 

+

64 def from_dict(cls, data: Dict[str, Any]) -> 'RenderingPosition': 

+

65 """Deserialize position from saved state""" 

+

66 return cls(**data) 

+

67 

+

68 def copy(self) -> 'RenderingPosition': 

+

69 """Create a copy of this position""" 

+

70 return RenderingPosition(*self._key()) 

+

71 

+

72 def __eq__(self, other) -> bool: 

+

73 """Check if two positions are equal""" 

+

74 if not isinstance(other, RenderingPosition): 

+

75 return False 

+

76 return self._key() == other._key() 

+

77 

+

78 def __hash__(self) -> int: 

+

79 """Make position hashable for use as dict key""" 

+

80 return hash(self._key()) 

+

81 

+

82 

+

83class ChapterInfo: 

+

84 """Information about a chapter/section in the document""" 

+

85 

+

86 def __init__( 

+

87 self, 

+

88 title: str, 

+

89 level: HeadingLevel, 

+

90 position: RenderingPosition, 

+

91 block_index: int): 

+

92 self.title = title 

+

93 self.level = level 

+

94 self.position = position 

+

95 self.block_index = block_index 

+

96 

+

97 

+

98class ChapterNavigator: 

+

99 """ 

+

100 Handles chapter/section navigation based on HTML heading structure (H1-H6). 

+

101 Builds a table of contents and provides navigation capabilities. 

+

102 """ 

+

103 

+

104 def __init__(self, blocks: List[Block]): 

+

105 self.blocks = blocks 

+

106 self.chapters: List[ChapterInfo] = [] 

+

107 self._build_chapter_map() 

+

108 

+

109 def _build_chapter_map(self): 

+

110 """Scan blocks for headings and build chapter navigation map""" 

+

111 current_chapter_index = 0 

+

112 

+

113 # Check if first block is a cover image and add it to TOC 

+

114 if self.blocks and isinstance(self.blocks[0], Image): 

+

115 cover_position = RenderingPosition( 

+

116 chapter_index=0, 

+

117 block_index=0, 

+

118 word_index=0, 

+

119 table_row=0, 

+

120 table_col=0, 

+

121 list_item_index=0 

+

122 ) 

+

123 

+

124 cover_info = ChapterInfo( 

+

125 title="Cover", 

+

126 level=HeadingLevel.H1, # Treat as top-level entry 

+

127 position=cover_position, 

+

128 block_index=0 

+

129 ) 

+

130 

+

131 self.chapters.append(cover_info) 

+

132 

+

133 for block_index, block in enumerate(self.blocks): 

+

134 if isinstance(block, Heading): 

+

135 # Create position for this heading 

+

136 position = RenderingPosition( 

+

137 chapter_index=current_chapter_index, 

+

138 block_index=block_index, # Use actual block index 

+

139 word_index=0, 

+

140 table_row=0, 

+

141 table_col=0, 

+

142 list_item_index=0 

+

143 ) 

+

144 

+

145 # Extract heading text 

+

146 heading_text = self._extract_heading_text(block) 

+

147 

+

148 chapter_info = ChapterInfo( 

+

149 title=heading_text, 

+

150 level=block.level, 

+

151 position=position, 

+

152 block_index=block_index 

+

153 ) 

+

154 

+

155 self.chapters.append(chapter_info) 

+

156 

+

157 # Only increment chapter index for top-level headings (H1) 

+

158 if block.level == HeadingLevel.H1: 

+

159 current_chapter_index += 1 

+

160 

+

161 def _extract_heading_text(self, heading: Heading) -> str: 

+

162 """Extract text content from a heading block""" 

+

163 words = [] 

+

164 for position, word in heading.words_iter(): 

+

165 if isinstance(word, Word): 165 ↛ 164line 165 didn't jump to line 164 because the condition on line 165 was always true

+

166 words.append(word.text) 

+

167 return " ".join(words) 

+

168 

+

169 def get_table_of_contents( 

+

170 self) -> List[Tuple[str, HeadingLevel, RenderingPosition]]: 

+

171 """Generate table of contents from heading structure""" 

+

172 return [(chapter.title, chapter.level, chapter.position) 

+

173 for chapter in self.chapters] 

+

174 

+

175 def get_chapter_position(self, chapter_title: str) -> Optional[RenderingPosition]: 

+

176 """Get rendering position for a chapter by title""" 

+

177 for chapter in self.chapters: 

+

178 if chapter.title.lower() == chapter_title.lower(): 

+

179 return chapter.position 

+

180 return None 

+

181 

+

182 def get_current_chapter(self, position: RenderingPosition) -> Optional[ChapterInfo]: 

+

183 """Determine which chapter contains the current position""" 

+

184 if not self.chapters: 

+

185 return None 

+

186 

+

187 # Find the chapter that contains this position 

+

188 for i, chapter in enumerate(self.chapters): 188 ↛ 197line 188 didn't jump to line 197 because the loop on line 188 didn't complete

+

189 # Check if this is the last chapter or if position is before next chapter 

+

190 if i == len(self.chapters) - 1: 

+

191 return chapter 

+

192 

+

193 next_chapter = self.chapters[i + 1] 

+

194 if position.chapter_index < next_chapter.position.chapter_index: 

+

195 return chapter 

+

196 

+

197 return self.chapters[0] if self.chapters else None 

+

198 

+

199 

+

200class FontFamilyOverride: 

+

201 """ 

+

202 Manages font family preferences for ereader rendering. 

+

203 Allows dynamic font family switching without modifying source blocks. 

+

204 """ 

+

205 

+

206 def __init__(self, preferred_family: Optional[BundledFont] = None): 

+

207 """ 

+

208 Initialize font family override. 

+

209 

+

210 Args: 

+

211 preferred_family: Preferred bundled font family (None = use original fonts) 

+

212 """ 

+

213 self.preferred_family = preferred_family 

+

214 

+

215 def override_font(self, font: Font) -> Font: 

+

216 """ 

+

217 Create a new font with the preferred family while preserving other attributes. 

+

218 

+

219 Args: 

+

220 font: Original font object 

+

221 

+

222 Returns: 

+

223 Font with overridden family, or original if no override is set 

+

224 """ 

+

225 if self.preferred_family is None: 

+

226 return font 

+

227 

+

228 # Get the appropriate font path for the preferred family 

+

229 # preserving the original font's weight and style 

+

230 new_font_path = get_bundled_font_path( 

+

231 family=self.preferred_family, 

+

232 weight=font.weight, 

+

233 style=font.style 

+

234 ) 

+

235 

+

236 # If we couldn't find a matching font, fall back to original 

+

237 if new_font_path is None: 

+

238 return font 

+

239 

+

240 # Create a new font with the overridden path 

+

241 return Font( 

+

242 font_path=new_font_path, 

+

243 font_size=font.font_size, 

+

244 colour=font.colour, 

+

245 weight=font.weight, 

+

246 style=font.style, 

+

247 decoration=font.decoration, 

+

248 background=font.background, 

+

249 language=font.language, 

+

250 min_hyphenation_width=font.min_hyphenation_width 

+

251 ) 

+

252 

+

253 

+

254class FontScaler: 

+

255 """ 

+

256 Handles font scaling operations for ereader font size adjustments. 

+

257 Applies scaling at layout/render time while preserving original font objects. 

+

258 """ 

+

259 

+

260 @staticmethod 

+

261 def scale_font(font: Font, scale_factor: float, family_override: Optional[FontFamilyOverride] = None) -> Font: 

+

262 """ 

+

263 Create a scaled version of a font for layout calculations. 

+

264 

+

265 Args: 

+

266 font: Original font object 

+

267 scale_factor: Scaling factor (1.0 = no change, 2.0 = double size, etc.) 

+

268 family_override: Optional font family override 

+

269 

+

270 Returns: 

+

271 New Font object with scaled size and optional family override 

+

272 """ 

+

273 # Apply family override first if specified 

+

274 working_font = font 

+

275 if family_override is not None: 275 ↛ 276line 275 didn't jump to line 276 because the condition on line 275 was never true

+

276 working_font = family_override.override_font(font) 

+

277 

+

278 # Then apply scaling 

+

279 if scale_factor == 1.0: 

+

280 return working_font 

+

281 

+

282 scaled_size = max(1, int(working_font.font_size * scale_factor)) 

+

283 

+

284 return Font( 

+

285 font_path=working_font._font_path, 

+

286 font_size=scaled_size, 

+

287 colour=working_font.colour, 

+

288 weight=working_font.weight, 

+

289 style=working_font.style, 

+

290 decoration=working_font.decoration, 

+

291 background=working_font.background, 

+

292 language=working_font.language, 

+

293 min_hyphenation_width=working_font.min_hyphenation_width 

+

294 ) 

+

295 

+

296 @staticmethod 

+

297 def scale_word_spacing(spacing: Tuple[int, int], 

+

298 scale_factor: float) -> Tuple[int, int]: 

+

299 """Scale word spacing constraints proportionally""" 

+

300 if scale_factor == 1.0: 

+

301 return spacing 

+

302 

+

303 min_spacing, max_spacing = spacing 

+

304 return ( 

+

305 max(1, int(min_spacing * scale_factor)), 

+

306 max(2, int(max_spacing * scale_factor)) 

+

307 ) 

+

308 

+

309 

+

310class BidirectionalLayouter: 

+

311 """ 

+

312 Core layout engine supporting both forward and backward page rendering. 

+

313 Handles font scaling and maintains position state. 

+

314 """ 

+

315 

+

316 def __init__(self, 

+

317 blocks: List[Block], 

+

318 page_style: PageStyle, 

+

319 page_size: Tuple[int, 

+

320 int] = (800, 

+

321 600), 

+

322 alignment_override=None, 

+

323 font_family_override: Optional[FontFamilyOverride] = None): 

+

324 self.blocks = blocks 

+

325 self.page_style = page_style 

+

326 self.page_size = page_size 

+

327 self.chapter_navigator = ChapterNavigator(blocks) 

+

328 self.alignment_override = alignment_override 

+

329 self.font_family_override = font_family_override 

+

330 

+

331 # Maps (font_scale, end position) -> the position the page started at. 

+

332 # Filled in as pages are laid out forward, which makes "previous page" 

+

333 # exact and free for anywhere the reader has already been. Keyed by font 

+

334 # scale because changing it repaginates the document. 

+

335 self._page_chain: Dict[Tuple[float, Tuple[int, int, int]], 

+

336 RenderingPosition] = {} 

+

337 

+

338 # Scaled copies of blocks, keyed by (id(block), font_scale). Rebuilding 

+

339 # a block's words on every page render allocated a fresh Paragraph and 

+

340 # Word per word on the hot path. The original block is kept alongside 

+

341 # the copy so its id cannot be recycled while it is a live key. 

+

342 self._scaled_block_cache: Dict[Tuple[int, float], Tuple[Block, Block]] = {} 

+

343 

+

344 def render_page_forward(self, position: RenderingPosition, 

+

345 font_scale: float = 1.0) -> Tuple[Page, RenderingPosition]: 

+

346 """ 

+

347 Render a page starting from the given position, moving forward through the document. 

+

348 

+

349 Args: 

+

350 position: Starting position in document 

+

351 font_scale: Font scaling factor 

+

352 

+

353 Returns: 

+

354 Tuple of (rendered_page, next_position) 

+

355 """ 

+

356 page = Page(size=self.page_size, style=self.page_style) 

+

357 current_pos = position.copy() 

+

358 

+

359 # Start laying out blocks from the current position 

+

360 while current_pos.block_index < len(self.blocks) and page.free_space()[1] > 0: 

+

361 # Additional bounds check to prevent IndexError 

+

362 if current_pos.block_index >= len(self.blocks): 362 ↛ 363line 362 didn't jump to line 363 because the condition on line 362 was never true

+

363 break 

+

364 

+

365 block = self.blocks[current_pos.block_index] 

+

366 

+

367 # Apply font scaling to the block 

+

368 scaled_block = self._scale_block_fonts(block, font_scale) 

+

369 

+

370 # Try to fit the block on the current page 

+

371 success, new_pos = self._layout_block_on_page( 

+

372 scaled_block, page, current_pos, font_scale) 

+

373 

+

374 if not success: 

+

375 # The block did not fit in its entirety. It may still have been 

+

376 # laid out partially - a paragraph larger than one page places as 

+

377 # many lines as fit and reports the word it stopped at. Keeping 

+

378 # that resume point is what allows the next page to continue; 

+

379 # discarding it tells the caller no progress was made, which 

+

380 # dead-ends navigation on the block forever. 

+

381 if self._position_compare(new_pos, current_pos) > 0: 

+

382 current_pos = new_pos 

+

383 break 

+

384 

+

385 # Add inter-block spacing after successfully laying out a block 

+

386 # Only add if we're not at the end of the document and there's space 

+

387 if new_pos.block_index < len(self.blocks): 

+

388 page._current_y_offset += self.page_style.inter_block_spacing 

+

389 

+

390 # Ensure new position doesn't go beyond bounds 

+

391 if new_pos.block_index >= len(self.blocks): 

+

392 # We've reached the end of the document 

+

393 current_pos = new_pos 

+

394 break 

+

395 

+

396 current_pos = new_pos 

+

397 

+

398 # Remember this link in the chain so stepping back to it later is exact. 

+

399 if self._position_compare(current_pos, position) > 0: 

+

400 self._page_chain[(font_scale, self._position_key(current_pos))] = \ 

+

401 position.copy() 

+

402 

+

403 return page, current_pos 

+

404 

+

405 # How many block starts before the target to try as replay anchors before 

+

406 # settling for the best inexact answer. 

+

407 MAX_BACKWARD_ANCHORS = 4 

+

408 

+

409 # Ceiling on pages replayed from a single anchor, so a pathologically long 

+

410 # block cannot make one page turn walk an entire chapter. 

+

411 MAX_REPLAY_PAGES = 8 

+

412 

+

413 def render_page_backward(self, 

+

414 end_position: RenderingPosition, 

+

415 font_scale: float = 1.0) -> Tuple[Page, 

+

416 RenderingPosition]: 

+

417 """ 

+

418 Render the page that ends at the given position - "previous page". 

+

419 

+

420 Pagination is a pure function: laying out from a position q yields a page 

+

421 and the position where it stopped, next(q). The page before P is therefore 

+

422 the q for which next(q) == P, and it is found by *replaying* the chain 

+

423 forward from an anchor, not by guessing q. 

+

424 

+

425 The previous implementation searched instead: it estimated a block index 

+

426 and bisected on it, pinning word_index to 0. Pages routinely start 

+

427 mid-block, so the answer was frequently not in the search space at all - 

+

428 the search then exhausted its iterations and fell back to a position that 

+

429 was not the previous page, usually the start of the document. 

+

430 

+

431 Three sources are tried in order: 

+

432 

+

433 1. The recorded chain, from pages already laid out going forward. Exact, 

+

434 and the common case when the reader is paging back and forth. 

+

435 2. Replay from the start of the block containing P, then from 

+

436 progressively earlier blocks. Exact when P lies on the resulting chain. 

+

437 3. Failing an exact hit - which happens when P was reached by a jump or a 

+

438 restored bookmark rather than by reading forward, so it is on no 

+

439 natural chain - the latest page start before P. That overlaps P's page 

+

440 slightly rather than skipping content, which is the safe direction to 

+

441 be wrong in. 

+

442 

+

443 Args: 

+

444 end_position: Position where the page should end 

+

445 font_scale: Font scaling factor 

+

446 

+

447 Returns: 

+

448 Tuple of (rendered_page, start_position) 

+

449 """ 

+

450 document_start = RenderingPosition() 

+

451 

+

452 # Nothing precedes the start of the document. 

+

453 if self._position_compare(end_position, document_start) <= 0: 

+

454 page, _ = self.render_page_forward(document_start, font_scale) 

+

455 return page, document_start 

+

456 

+

457 # 1. The chain we have already walked. 

+

458 remembered = self._page_chain.get((font_scale, self._position_key(end_position))) 

+

459 if remembered is not None: 

+

460 page, actual_end = self.render_page_forward(remembered, font_scale) 

+

461 if self._position_compare(actual_end, end_position) == 0: 461 ↛ 465line 461 didn't jump to line 465 because the condition on line 461 was always true

+

462 return page, remembered 

+

463 

+

464 # 2/3. Replay from anchors, keeping the best inexact result as a fallback. 

+

465 fallback = None 

+

466 for anchor in self._backward_anchors(end_position): 

+

467 page, start, exact = self._replay_to(anchor, end_position, font_scale) 

+

468 if page is None: 

+

469 continue 

+

470 if exact: 470 ↛ 472line 470 didn't jump to line 472 because the condition on line 470 was always true

+

471 return page, start 

+

472 if fallback is None: 

+

473 fallback = (page, start) 

+

474 

+

475 if fallback is not None: 475 ↛ 476line 475 didn't jump to line 476 because the condition on line 475 was never true

+

476 return fallback 

+

477 

+

478 page, _ = self.render_page_forward(document_start, font_scale) 

+

479 return page, document_start 

+

480 

+

481 def _backward_anchors(self, target: RenderingPosition): 

+

482 """ 

+

483 Yield positions to replay from, nearest first. 

+

484 

+

485 Block starts are used as anchors because they are the coarsest positions 

+

486 that are certainly valid to lay out from. The block containing the target 

+

487 comes first: when the target is mid-block, the page before it usually 

+

488 starts in that same block or the one before. 

+

489 """ 

+

490 first_block = target.block_index if target.word_index > 0 \ 

+

491 else target.block_index - 1 

+

492 

+

493 for offset in range(self.MAX_BACKWARD_ANCHORS): 

+

494 block_index = first_block - offset 

+

495 if block_index < 0: 

+

496 break 

+

497 yield RenderingPosition( 

+

498 chapter_index=target.chapter_index, 

+

499 block_index=block_index, 

+

500 word_index=0, 

+

501 ) 

+

502 

+

503 if first_block - self.MAX_BACKWARD_ANCHORS >= 0: 

+

504 yield RenderingPosition() 

+

505 

+

506 def _replay_to(self, 

+

507 anchor: RenderingPosition, 

+

508 target: RenderingPosition, 

+

509 font_scale: float): 

+

510 """ 

+

511 Lay out pages forward from `anchor`, looking for the one ending at `target`. 

+

512 

+

513 Returns: 

+

514 (page, start, exact). `exact` is True when a page ended precisely on 

+

515 the target. When the chain steps over the target instead, the last 

+

516 page starting before it is returned with exact=False. (None, None, 

+

517 False) means the anchor yielded nothing usable. 

+

518 """ 

+

519 position = anchor 

+

520 last = (None, None) 

+

521 

+

522 for _ in range(self.MAX_REPLAY_PAGES): 522 ↛ 542line 522 didn't jump to line 542 because the loop on line 522 didn't complete

+

523 if self._position_compare(position, target) >= 0: 523 ↛ 524line 523 didn't jump to line 524 because the condition on line 523 was never true

+

524 break 

+

525 

+

526 page, next_position = self.render_page_forward(position, font_scale) 

+

527 comparison = self._position_compare(next_position, target) 

+

528 

+

529 if comparison == 0: 

+

530 return page, position, True 

+

531 

+

532 if comparison > 0: 

+

533 # Stepped over the target: this chain does not pass through it. 

+

534 return last[0], last[1], False 

+

535 

+

536 if self._position_compare(next_position, position) <= 0: 536 ↛ 537line 536 didn't jump to line 537 because the condition on line 536 was never true

+

537 break # no progress; give up on this anchor 

+

538 

+

539 last = (page, position) 

+

540 position = next_position 

+

541 

+

542 return last[0], last[1], False 

+

543 

+

544 @staticmethod 

+

545 def _position_key(position: RenderingPosition) -> Tuple[int, int, int]: 

+

546 """Hashable identity of a position, for the page chain map.""" 

+

547 return (position.chapter_index, position.block_index, position.word_index) 

+

548 

+

549 def _scale_block_fonts(self, block: Block, font_scale: float) -> Block: 

+

550 """ 

+

551 Apply font scaling and the font family override to every font in a block. 

+

552 

+

553 Returns the block unchanged when there is nothing to apply. Results are 

+

554 memoised per (block, scale) for the life of the layouter, so a page 

+

555 re-render at an unchanged scale costs a dict lookup. 

+

556 """ 

+

557 if font_scale == 1.0 and self.font_family_override is None: 

+

558 return block 

+

559 

+

560 key = (id(block), font_scale) 

+

561 cached = self._scaled_block_cache.get(key) 

+

562 if cached is not None: 

+

563 return cached[1] 

+

564 

+

565 scaled = self._build_scaled_block(block, font_scale) 

+

566 self._scaled_block_cache[key] = (block, scaled) 

+

567 return scaled 

+

568 

+

569 def _build_scaled_block(self, block: Block, font_scale: float) -> Block: 

+

570 """Construct the scaled copy of a block. See _scale_block_fonts.""" 

+

571 def scale(font: Font) -> Font: 

+

572 return FontScaler.scale_font(font, font_scale, self.font_family_override) 

+

573 

+

574 if isinstance(block, (Paragraph, Heading)): 

+

575 if isinstance(block, Heading): 

+

576 scaled_block = Heading(block.level, scale(block.style)) 

+

577 else: 

+

578 scaled_block = Paragraph(scale(block.style)) 

+

579 

+

580 # words_iter() yields (position, word) tuples. with_style() keeps 

+

581 # the concrete word class, so a LinkedWord stays linked - rebuilding 

+

582 # these as plain Words silently stripped every hyperlink in the 

+

583 # document as soon as the reader changed font size. 

+

584 for _, word in block.words_iter(): 

+

585 if isinstance(word, Word): 585 ↛ 584line 585 didn't jump to line 584 because the condition on line 585 was always true

+

586 scaled_block.add_word(word.with_style(scale(word.style))) 

+

587 return scaled_block 

+

588 

+

589 if isinstance(block, Quote): 

+

590 scaled_quote = Quote(scale(block.style) if block.style else None) 

+

591 for child in block.blocks(): 

+

592 scaled_quote.add_block(self._scale_block_fonts(child, font_scale)) 

+

593 return scaled_quote 

+

594 

+

595 if isinstance(block, HList): 

+

596 scaled_list = HList( 

+

597 block.style, 

+

598 scale(block.default_style) if block.default_style else None) 

+

599 for item in block.items(): 

+

600 scaled_item = ListItem( 

+

601 item.term, 

+

602 scale(item.style) if item.style else None) 

+

603 for child in item.blocks(): 

+

604 scaled_item.add_block(self._scale_block_fonts(child, font_scale)) 

+

605 scaled_list.add_item(scaled_item) 

+

606 return scaled_list 

+

607 

+

608 if isinstance(block, Table): 608 ↛ 633line 608 didn't jump to line 633 because the condition on line 608 was always true

+

609 scaled_table = Table( 

+

610 block.caption, 

+

611 scale(block.style) if block.style else None) 

+

612 # Rows must go back into the section they came from, or a <thead> 

+

613 # row would be re-added as a body row. 

+

614 for section, rows in (('header', block.header_rows()), 

+

615 ('body', block.body_rows()), 

+

616 ('footer', block.footer_rows())): 

+

617 for row in rows: 

+

618 scaled_row = TableRow(scale(row.style) if row.style else None) 

+

619 for cell in row.cells(): 

+

620 scaled_cell = TableCell( 

+

621 is_header=cell.is_header, 

+

622 colspan=cell.colspan, 

+

623 rowspan=cell.rowspan, 

+

624 style=scale(cell.style) if cell.style else None) 

+

625 for child in cell.blocks(): 

+

626 scaled_cell.add_block(self._scale_block_fonts(child, font_scale)) 

+

627 scaled_row.add_cell(scaled_cell) 

+

628 scaled_table.add_row(scaled_row, section) 

+

629 return scaled_table 

+

630 

+

631 # Blocks with no fonts of their own (Image, HorizontalRule, PageBreak, 

+

632 # CodeBlock - which carries raw lines, not styled words) pass through. 

+

633 return block 

+

634 

+

635 def _layout_block_on_page(self, 

+

636 block: Block, 

+

637 page: Page, 

+

638 position: RenderingPosition, 

+

639 font_scale: float) -> Tuple[bool, 

+

640 RenderingPosition]: 

+

641 """ 

+

642 Try to layout a block on the page starting from the given position. 

+

643 

+

644 Returns: 

+

645 Tuple of (success, new_position) 

+

646 """ 

+

647 if isinstance(block, Paragraph): 

+

648 return self._layout_paragraph_on_page(block, page, position, font_scale) 

+

649 elif isinstance(block, Heading): 649 ↛ 650line 649 didn't jump to line 650 because the condition on line 649 was never true

+

650 return self._layout_heading_on_page(block, page, position, font_scale) 

+

651 elif isinstance(block, Table): 651 ↛ 652line 651 didn't jump to line 652 because the condition on line 651 was never true

+

652 return self._layout_table_on_page(block, page, position, font_scale) 

+

653 elif isinstance(block, HList): 653 ↛ 654line 653 didn't jump to line 654 because the condition on line 653 was never true

+

654 return self._layout_list_on_page(block, page, position, font_scale) 

+

655 elif isinstance(block, Image): 

+

656 return self._layout_image_on_page(block, page, position, font_scale) 

+

657 else: 

+

658 # Skip unknown block types 

+

659 new_pos = position.copy() 

+

660 new_pos.block_index += 1 

+

661 return True, new_pos 

+

662 

+

663 def _layout_paragraph_on_page(self, 

+

664 paragraph: Paragraph, 

+

665 page: Page, 

+

666 position: RenderingPosition, 

+

667 font_scale: float) -> Tuple[bool, 

+

668 RenderingPosition]: 

+

669 """ 

+

670 Layout a paragraph on the page using the core paragraph_layouter. 

+

671 Integrates font scaling and position tracking with the proven layout logic. 

+

672 

+

673 Args: 

+

674 paragraph: The paragraph to layout (already scaled if font_scale != 1.0) 

+

675 page: The page to layout on 

+

676 position: Current rendering position 

+

677 font_scale: Font scaling factor (used for context, paragraph should already be scaled) 

+

678 

+

679 Returns: 

+

680 Tuple of (success, new_position) 

+

681 """ 

+

682 # Convert remaining_pretext from string to Text object if needed 

+

683 pretext_obj = None 

+

684 if position.remaining_pretext: 

+

685 # Create a Text object from the pretext string 

+

686 pretext_obj = Text( 

+

687 position.remaining_pretext, 

+

688 paragraph.style, 

+

689 page.draw, 

+

690 line=None, 

+

691 source=None 

+

692 ) 

+

693 

+

694 # Call the core paragraph layouter with alignment override if set 

+

695 success, failed_word_index, remaining_pretext = paragraph_layouter( 

+

696 paragraph, 

+

697 page, 

+

698 start_word=position.word_index, 

+

699 pretext=pretext_obj, 

+

700 alignment_override=self.alignment_override 

+

701 ) 

+

702 

+

703 # Create new position based on the result 

+

704 new_pos = position.copy() 

+

705 

+

706 if success: 

+

707 # Paragraph was fully laid out, move to next block 

+

708 new_pos.block_index += 1 

+

709 new_pos.word_index = 0 

+

710 new_pos.remaining_pretext = None 

+

711 return True, new_pos 

+

712 else: 

+

713 # Paragraph was not fully laid out 

+

714 if failed_word_index is not None: 714 ↛ 728line 714 didn't jump to line 728 because the condition on line 714 was always true

+

715 # Update position to the word that didn't fit 

+

716 new_pos.word_index = failed_word_index 

+

717 

+

718 # Convert Text object back to string if there's remaining pretext 

+

719 if remaining_pretext is not None and hasattr(remaining_pretext, 'text'): 

+

720 new_pos.remaining_pretext = remaining_pretext.text 

+

721 else: 

+

722 new_pos.remaining_pretext = None 

+

723 

+

724 return False, new_pos 

+

725 else: 

+

726 # No specific word failed, but layout wasn't successful 

+

727 # This shouldn't normally happen, but handle it gracefully 

+

728 return False, position 

+

729 

+

730 def _layout_heading_on_page(self, 

+

731 heading: Heading, 

+

732 page: Page, 

+

733 position: RenderingPosition, 

+

734 font_scale: float) -> Tuple[bool, 

+

735 RenderingPosition]: 

+

736 """Layout a heading on the page""" 

+

737 # Similar to paragraph but with heading-specific styling 

+

738 return self._layout_paragraph_on_page(heading, page, position, font_scale) 

+

739 

+

740 def _layout_table_on_page(self, 

+

741 table: Table, 

+

742 page: Page, 

+

743 position: RenderingPosition, 

+

744 font_scale: float) -> Tuple[bool, 

+

745 RenderingPosition]: 

+

746 """Layout a table on the page with column fitting and row continuation""" 

+

747 # This is a complex operation that would need full table layout logic 

+

748 # For now, skip tables 

+

749 new_pos = position.copy() 

+

750 new_pos.block_index += 1 

+

751 new_pos.table_row = 0 

+

752 new_pos.table_col = 0 

+

753 return True, new_pos 

+

754 

+

755 def _layout_list_on_page(self, 

+

756 hlist: HList, 

+

757 page: Page, 

+

758 position: RenderingPosition, 

+

759 font_scale: float) -> Tuple[bool, 

+

760 RenderingPosition]: 

+

761 """Layout a list on the page""" 

+

762 # This would need list-specific layout logic 

+

763 # For now, skip lists 

+

764 new_pos = position.copy() 

+

765 new_pos.block_index += 1 

+

766 new_pos.list_item_index = 0 

+

767 return True, new_pos 

+

768 

+

769 def _layout_image_on_page(self, 

+

770 image: Image, 

+

771 page: Page, 

+

772 position: RenderingPosition, 

+

773 font_scale: float) -> Tuple[bool, 

+

774 RenderingPosition]: 

+

775 """ 

+

776 Layout an image on the page using the image_layouter. 

+

777 

+

778 Args: 

+

779 image: The Image block to layout 

+

780 page: The page to layout on 

+

781 position: Current rendering position (should be at the start of this image block) 

+

782 font_scale: Font scaling factor (not used for images, but kept for consistency) 

+

783 

+

784 Returns: 

+

785 Tuple of (success, new_position) 

+

786 - success: True if image was laid out, False if page ran out of space 

+

787 - new_position: Updated position (next block if success, same block if failed) 

+

788 """ 

+

789 # Try to layout the image on the current page 

+

790 success = image_layouter( 

+

791 image=image, 

+

792 page=page, 

+

793 max_width=None, # Use page available width 

+

794 max_height=None # Use page available height 

+

795 ) 

+

796 

+

797 new_pos = position.copy() 

+

798 

+

799 if success: 

+

800 # Image was successfully laid out, move to next block 

+

801 new_pos.block_index += 1 

+

802 new_pos.word_index = 0 

+

803 return True, new_pos 

+

804 else: 

+

805 # Image didn't fit on current page, signal to continue on next page 

+

806 # Keep same position so it will be attempted on the next page 

+

807 return False, position 

+

808 

+

809 def _position_compare(self, pos1: RenderingPosition, 

+

810 pos2: RenderingPosition) -> int: 

+

811 """Compare two positions (-1: pos1 < pos2, 0: equal, 1: pos1 > pos2)""" 

+

812 if pos1.chapter_index != pos2.chapter_index: 

+

813 return 1 if pos1.chapter_index > pos2.chapter_index else -1 

+

814 if pos1.block_index != pos2.block_index: 

+

815 return 1 if pos1.block_index > pos2.block_index else -1 

+

816 if pos1.word_index != pos2.word_index: 

+

817 return 1 if pos1.word_index > pos2.word_index else -1 

+

818 return 0 

+
+ + + diff --git a/cov_info/htmlcov/z_427cc3035faf7633_ereader_manager_py.html b/cov_info/htmlcov/z_427cc3035faf7633_ereader_manager_py.html new file mode 100644 index 0000000..6a6ee7d --- /dev/null +++ b/cov_info/htmlcov/z_427cc3035faf7633_ereader_manager_py.html @@ -0,0 +1,1248 @@ + + + + + Coverage for pyWebLayout/layout/ereader_manager.py: 77% + + + + + +
+
+

+ Coverage for pyWebLayout/layout/ereader_manager.py: + 77% +

+ +

+ 370 statements   + + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.15.4, + created at 2026-08-08 20:34 +0000 +

+ +
+
+
+

1""" 

+

2High-performance ereader layout manager with sub-second page rendering. 

+

3 

+

4This module provides the main interface for ereader applications, combining 

+

5position tracking, font scaling, chapter navigation, and intelligent page buffering 

+

6into a unified, easy-to-use API. 

+

7""" 

+

8 

+

9from __future__ import annotations 

+

10from typing import List, Dict, Optional, Tuple, Any, Callable 

+

11import logging 

+

12 

+

13from .ereader_layout import RenderingPosition, ChapterNavigator, ChapterInfo 

+

14from .page_buffer import BufferedPageRenderer 

+

15from pyWebLayout.abstract.block import Block, HeadingLevel, Image, BlockType 

+

16from pyWebLayout.concrete.page import Page 

+

17from pyWebLayout.concrete.image import RenderableImage 

+

18from pyWebLayout.style.page_style import PageStyle 

+

19from pyWebLayout.style.fonts import BundledFont 

+

20from pyWebLayout.layout.document_layouter import image_layouter 

+

21from pyWebLayout.core.highlight import Highlight, HighlightColor, HighlightManager, \ 

+

22 create_highlight_from_query_result 

+

23from pyWebLayout.core.persistence import ensure_dir, read_json, write_json 

+

24from pyWebLayout.concrete.interaction_handler import InteractionStateManager 

+

25from PIL import Image as Image_ 

+

26 

+

27logger = logging.getLogger(__name__) 

+

28 

+

29 

+

30class BookmarkManager: 

+

31 """ 

+

32 Manages bookmarks and reading position persistence for ereader applications. 

+

33 """ 

+

34 

+

35 def __init__(self, document_id: str, bookmarks_dir: str = "bookmarks"): 

+

36 """ 

+

37 Initialize bookmark manager. 

+

38 

+

39 Args: 

+

40 document_id: Unique identifier for the document 

+

41 bookmarks_dir: Directory to store bookmark files 

+

42 """ 

+

43 self.document_id = document_id 

+

44 self.bookmarks_dir = ensure_dir(bookmarks_dir) 

+

45 

+

46 self.bookmarks_file = self.bookmarks_dir / f"{document_id}_bookmarks.json" 

+

47 self.position_file = self.bookmarks_dir / f"{document_id}_position.json" 

+

48 

+

49 self._bookmarks: Dict[str, RenderingPosition] = {} 

+

50 self._load_bookmarks() 

+

51 

+

52 def _load_bookmarks(self): 

+

53 """Load bookmarks from file""" 

+

54 data = read_json(self.bookmarks_file, {}) 

+

55 try: 

+

56 self._bookmarks = { 

+

57 name: RenderingPosition.from_dict(pos_data) 

+

58 for name, pos_data in data.items() 

+

59 } 

+

60 except (AttributeError, TypeError, KeyError): 

+

61 logger.warning("Bookmark file %s is not in the expected shape; ignoring it", 

+

62 self.bookmarks_file, exc_info=True) 

+

63 self._bookmarks = {} 

+

64 

+

65 def _save_bookmarks(self): 

+

66 """Save bookmarks to file""" 

+

67 write_json(self.bookmarks_file, { 

+

68 name: position.to_dict() 

+

69 for name, position in self._bookmarks.items() 

+

70 }) 

+

71 

+

72 def add_bookmark(self, name: str, position: RenderingPosition): 

+

73 """ 

+

74 Add a bookmark at the given position. 

+

75 

+

76 Args: 

+

77 name: Bookmark name 

+

78 position: Position to bookmark 

+

79 """ 

+

80 self._bookmarks[name] = position 

+

81 self._save_bookmarks() 

+

82 

+

83 def remove_bookmark(self, name: str) -> bool: 

+

84 """ 

+

85 Remove a bookmark. 

+

86 

+

87 Args: 

+

88 name: Bookmark name to remove 

+

89 

+

90 Returns: 

+

91 True if bookmark was removed, False if not found 

+

92 """ 

+

93 if name in self._bookmarks: 

+

94 del self._bookmarks[name] 

+

95 self._save_bookmarks() 

+

96 return True 

+

97 return False 

+

98 

+

99 def get_bookmark(self, name: str) -> Optional[RenderingPosition]: 

+

100 """ 

+

101 Get a bookmark position. 

+

102 

+

103 Args: 

+

104 name: Bookmark name 

+

105 

+

106 Returns: 

+

107 Bookmark position or None if not found 

+

108 """ 

+

109 return self._bookmarks.get(name) 

+

110 

+

111 def list_bookmarks(self) -> List[Tuple[str, RenderingPosition]]: 

+

112 """ 

+

113 Get all bookmarks. 

+

114 

+

115 Returns: 

+

116 List of (name, position) tuples 

+

117 """ 

+

118 return list(self._bookmarks.items()) 

+

119 

+

120 def save_reading_position(self, position: RenderingPosition): 

+

121 """ 

+

122 Save the current reading position. 

+

123 

+

124 Args: 

+

125 position: Current reading position 

+

126 """ 

+

127 write_json(self.position_file, position.to_dict()) 

+

128 

+

129 def load_reading_position(self) -> Optional[RenderingPosition]: 

+

130 """ 

+

131 Load the last reading position. 

+

132 

+

133 Returns: 

+

134 Last reading position or None if not found 

+

135 """ 

+

136 data = read_json(self.position_file, None) 

+

137 if data is None: 

+

138 return None 

+

139 try: 

+

140 return RenderingPosition.from_dict(data) 

+

141 except (TypeError, KeyError): 

+

142 logger.warning("Position file %s is not in the expected shape; ignoring it", 

+

143 self.position_file, exc_info=True) 

+

144 return None 

+

145 

+

146 

+

147class EreaderLayoutManager: 

+

148 """ 

+

149 High-level ereader layout manager providing a complete interface for ereader applications. 

+

150 

+

151 Features: 

+

152 - Sub-second page rendering with intelligent buffering 

+

153 - Font scaling support 

+

154 - Dynamic font family switching (Sans, Serif, Monospace) 

+

155 - Chapter navigation 

+

156 - Bookmark management 

+

157 - Position persistence 

+

158 - Progress tracking 

+

159 """ 

+

160 

+

161 def __init__(self, 

+

162 blocks: List[Block], 

+

163 page_size: Tuple[int, int], 

+

164 document_id: str = "default", 

+

165 buffer_size: int = 5, 

+

166 page_style: Optional[PageStyle] = None, 

+

167 bookmarks_dir: str = "bookmarks", 

+

168 highlights_dir: Optional[str] = None): 

+

169 """ 

+

170 Initialize the ereader layout manager. 

+

171 

+

172 Args: 

+

173 blocks: Document blocks to render 

+

174 page_size: Page size (width, height) in pixels 

+

175 document_id: Unique identifier for the document (for bookmarks/position) 

+

176 buffer_size: Number of pages to cache in each direction 

+

177 page_style: Custom page styling (uses default if None) 

+

178 bookmarks_dir: Directory to store bookmark files 

+

179 highlights_dir: Directory to store highlights. Defaults to 

+

180 bookmarks_dir, so a document's reading state lives in one place. 

+

181 """ 

+

182 self.blocks = blocks 

+

183 self.page_size = page_size 

+

184 self.document_id = document_id 

+

185 

+

186 # Initialize page style 

+

187 if page_style is None: 

+

188 page_style = PageStyle() 

+

189 self.page_style = page_style 

+

190 

+

191 # Initialize core components 

+

192 self.renderer = BufferedPageRenderer(blocks, page_style, buffer_size, page_size) 

+

193 self.chapter_navigator = ChapterNavigator(blocks) 

+

194 self.bookmark_manager = BookmarkManager(document_id, bookmarks_dir) 

+

195 self.highlight_manager = HighlightManager( 

+

196 document_id, highlights_dir if highlights_dir is not None else bookmarks_dir) 

+

197 

+

198 # Current state 

+

199 self.current_position = RenderingPosition() 

+

200 self.font_scale = 1.0 

+

201 

+

202 # Cover page handling 

+

203 self._has_cover = self._detect_cover() 

+

204 self._on_cover_page = self._has_cover # Start on cover if one exists 

+

205 

+

206 # Page position history for fast backward navigation 

+

207 # List of (position, font_scale) tuples representing the start of each page visited 

+

208 self._page_history: List[Tuple[RenderingPosition, float]] = [] 

+

209 self._max_history_size = 50 # Keep last 50 page positions 

+

210 

+

211 # Load last reading position if available 

+

212 saved_position = self.bookmark_manager.load_reading_position() 

+

213 if saved_position: 

+

214 self.current_position = saved_position 

+

215 self._on_cover_page = False # If we have a saved position, we're past the cover 

+

216 

+

217 # Pointer interaction state, rebound whenever the displayed page changes 

+

218 self._interaction_state_manager: Optional[InteractionStateManager] = None 

+

219 self._interaction_page: Optional[Page] = None 

+

220 

+

221 # Callbacks for UI updates 

+

222 self.position_changed_callback: Optional[Callable[[ 

+

223 RenderingPosition], None]] = None 

+

224 self.chapter_changed_callback: Optional[Callable[[ 

+

225 Optional[ChapterInfo]], None]] = None 

+

226 

+

227 def prewarm_caches(self, max_words: int = 2000, 

+

228 budget_bytes: Optional[int] = None) -> Tuple[int, int]: 

+

229 """ 

+

230 Preload the text caches with this document's most frequent words. 

+

231 

+

232 Counts how often each word occurs in the book and rasterises the most 

+

233 common ones ahead of time, so that the work lands at open time rather than 

+

234 on the first page turns. Entries are seeded with their document frequency, 

+

235 which is what keeps them resident under usage-ranked eviction. 

+

236 

+

237 Safe to call again after a font change; the fonts differ, so the new 

+

238 entries simply take their place in the eviction order alongside the old. 

+

239 

+

240 Args: 

+

241 max_words: Maximum distinct words to preload. 

+

242 budget_bytes: Bytes of glyph cache to fill. Defaults to half the budget. 

+

243 

+

244 Returns: 

+

245 Tuple of (words preloaded, bytes preloaded). 

+

246 """ 

+

247 from collections import Counter 

+

248 from pyWebLayout.concrete.text import prewarm_text_caches 

+

249 from .ereader_layout import FontScaler 

+

250 

+

251 override = getattr(self.renderer.layouter, 'font_family_override', None) 

+

252 

+

253 # Count by (style, text): the same word in a heading and in body text is a 

+

254 # different rasterisation, and both are worth counting separately. 

+

255 counts: Dict[Tuple[int, str], int] = Counter() 

+

256 styles: Dict[int, Any] = {} 

+

257 for block in self.blocks: 

+

258 words = getattr(block, '_words', None) 

+

259 if not words: 

+

260 continue 

+

261 for word in words: 

+

262 style = word.style 

+

263 if style is None: 

+

264 continue 

+

265 key = id(style) 

+

266 styles.setdefault(key, style) 

+

267 counts[(key, word.text)] += 1 

+

268 

+

269 # Resolve each distinct style once through the same scaling the layouter 

+

270 # applies, so the preloaded keys match what rendering will look up. 

+

271 scaled: Dict[int, Any] = {} 

+

272 for key, style in styles.items(): 

+

273 try: 

+

274 scaled[key] = FontScaler.scale_font(style, self.font_scale, override) 

+

275 except Exception: 

+

276 continue 

+

277 

+

278 entries = [] 

+

279 for (style_key, text), count in counts.items(): 

+

280 font = scaled.get(style_key) 

+

281 if font is None: 

+

282 continue 

+

283 entries.append((font.font, text, font.colour, count)) 

+

284 

+

285 return prewarm_text_caches(entries, budget_bytes=budget_bytes, 

+

286 max_words=max_words) 

+

287 

+

288 def set_position_changed_callback( 

+

289 self, callback: Callable[[RenderingPosition], None]): 

+

290 """Set callback for position changes""" 

+

291 self.position_changed_callback = callback 

+

292 

+

293 def set_chapter_changed_callback( 

+

294 self, callback: Callable[[Optional[ChapterInfo]], None]): 

+

295 """Set callback for chapter changes""" 

+

296 self.chapter_changed_callback = callback 

+

297 

+

298 def _detect_cover(self) -> bool: 

+

299 """ 

+

300 Detect if the document has a cover page. 

+

301 

+

302 A cover is detected if: 

+

303 1. The first block is an Image block, OR 

+

304 2. The document has cover metadata (future enhancement) 

+

305 

+

306 Returns: 

+

307 True if a cover page should be rendered 

+

308 """ 

+

309 if not self.blocks: 

+

310 return False 

+

311 

+

312 # Check if first block is an image - treat it as a cover 

+

313 first_block = self.blocks[0] 

+

314 if isinstance(first_block, Image): 

+

315 return True 

+

316 

+

317 return False 

+

318 

+

319 def _render_cover_page(self) -> Page: 

+

320 """ 

+

321 Render a dedicated cover page. 

+

322 

+

323 The cover page displays the first image block (if it exists) 

+

324 using the standard image layouter with maximum dimensions to fill the page. 

+

325 

+

326 Returns: 

+

327 Rendered cover page 

+

328 """ 

+

329 # Create a new page for the cover 

+

330 page = Page(self.page_size, self.page_style) 

+

331 

+

332 if not self.blocks or not isinstance(self.blocks[0], Image): 332 ↛ 334line 332 didn't jump to line 334 because the condition on line 332 was never true

+

333 # No cover image, return blank page 

+

334 return page 

+

335 

+

336 cover_image_block = self.blocks[0] 

+

337 

+

338 # Use the image layouter to render the cover image 

+

339 # Use full page dimensions (minus borders/padding) for cover 

+

340 try: 

+

341 max_width = self.page_size[0] - 2 * self.page_style.border_width 

+

342 max_height = self.page_size[1] - 2 * self.page_style.border_width 

+

343 

+

344 # Layout the image on the page 

+

345 success = image_layouter( 

+

346 image=cover_image_block, 

+

347 page=page, 

+

348 max_width=max_width, 

+

349 max_height=max_height 

+

350 ) 

+

351 

+

352 if not success: 352 ↛ 359line 352 didn't jump to line 359 because the condition on line 352 was always true

+

353 print("Warning: Failed to layout cover image") 

+

354 

+

355 except Exception as e: 

+

356 # If image loading fails, just return the blank page 

+

357 print(f"Warning: Failed to load cover image: {e}") 

+

358 

+

359 return page 

+

360 

+

361 def _notify_position_changed(self): 

+

362 """Notify UI of position change""" 

+

363 if self.position_changed_callback: 

+

364 self.position_changed_callback(self.current_position) 

+

365 

+

366 # Check if chapter changed 

+

367 current_chapter = self.chapter_navigator.get_current_chapter( 

+

368 self.current_position) 

+

369 if self.chapter_changed_callback: 

+

370 self.chapter_changed_callback(current_chapter) 

+

371 

+

372 # Auto-save reading position 

+

373 self.bookmark_manager.save_reading_position(self.current_position) 

+

374 

+

375 def get_current_page(self) -> Page: 

+

376 """ 

+

377 Get the page at the current reading position. 

+

378 

+

379 If on the cover page, returns the rendered cover. 

+

380 Otherwise, returns the regular content page. 

+

381 

+

382 Returns: 

+

383 Rendered page 

+

384 """ 

+

385 # Check if we're on the cover page 

+

386 if self._on_cover_page and self._has_cover: 

+

387 return self._render_cover_page() 

+

388 

+

389 page, _ = self.renderer.render_page(self.current_position, self.font_scale) 

+

390 return page 

+

391 

+

392 def next_page(self) -> Optional[Page]: 

+

393 """ 

+

394 Advance to the next page. 

+

395 

+

396 If currently on the cover page, advances to the first content page. 

+

397 Otherwise, advances to the next content page. 

+

398 

+

399 Returns: 

+

400 Next page or None if at end of document 

+

401 """ 

+

402 # Special case: transitioning from cover to first content page 

+

403 if self._on_cover_page and self._has_cover: 

+

404 self._on_cover_page = False 

+

405 # If first block is an image (the cover), skip it and start from block 1 

+

406 if self.blocks and isinstance(self.blocks[0], Image): 406 ↛ 409line 406 didn't jump to line 409 because the condition on line 406 was always true

+

407 self.current_position = RenderingPosition(chapter_index=0, block_index=1) 

+

408 else: 

+

409 self.current_position = RenderingPosition() 

+

410 self._notify_position_changed() 

+

411 return self.get_current_page() 

+

412 

+

413 # Save current position to history before moving forward 

+

414 self._add_to_history(self.current_position, self.font_scale) 

+

415 

+

416 page, next_position = self.renderer.render_page( 

+

417 self.current_position, self.font_scale) 

+

418 

+

419 # Check if we made progress 

+

420 if next_position != self.current_position: 

+

421 self.current_position = next_position 

+

422 self._notify_position_changed() 

+

423 return self.get_current_page() 

+

424 

+

425 # No progress. That is the correct answer only at the end of the 

+

426 # document; anywhere else a block has failed to lay out and would trap 

+

427 # the reader on this page. Skipping the block costs one block, not the 

+

428 # rest of the book. 

+

429 if self.current_position.block_index < len(self.blocks): 

+

430 logger.error( 

+

431 "Block %d made no layout progress; skipping it. This is a layout " 

+

432 "bug - the block placed nothing and reported no resume point.", 

+

433 self.current_position.block_index) 

+

434 self.current_position = RenderingPosition( 

+

435 chapter_index=self.current_position.chapter_index, 

+

436 block_index=self.current_position.block_index + 1) 

+

437 self._notify_position_changed() 

+

438 return self.get_current_page() 

+

439 

+

440 return None # At end of document 

+

441 

+

442 def previous_page(self) -> Optional[Page]: 

+

443 """ 

+

444 Go to the previous page. 

+

445 

+

446 Uses cached page history for instant navigation when available, 

+

447 falls back to iterative refinement algorithm when needed. 

+

448 Can navigate back to the cover page if it exists. 

+

449 

+

450 Returns: 

+

451 Previous page or None if at beginning of document (or on cover) 

+

452 """ 

+

453 # Special case: if at the beginning of content and there's a cover, go back to it 

+

454 if self._has_cover and self._is_at_beginning() and not self._on_cover_page: 

+

455 self._on_cover_page = True 

+

456 # Restore the canonical cover position. Being on the cover must have a 

+

457 # single representation: a fresh load sits at block 0 with the cover 

+

458 # showing, so returning to the cover has to land there too. Leaving the 

+

459 # position at the first content block saves a position that reopens past 

+

460 # the cover, silently losing it. 

+

461 self.current_position = RenderingPosition() 

+

462 self._notify_position_changed() 

+

463 return self.get_current_page() 

+

464 

+

465 # Can't go before the cover 

+

466 if self._on_cover_page: 466 ↛ 467line 466 didn't jump to line 467 because the condition on line 466 was never true

+

467 return None 

+

468 

+

469 if self._is_at_beginning(): 

+

470 return None 

+

471 

+

472 # Fast path: Check if we have this position in history 

+

473 previous_position = self._get_from_history(self.current_position, self.font_scale) 

+

474 

+

475 if previous_position is not None: 

+

476 # Cache hit! Use the cached position for instant navigation 

+

477 self.current_position = previous_position 

+

478 self._notify_position_changed() 

+

479 return self.get_current_page() 

+

480 

+

481 # Slow path: Use backward rendering to find the previous page 

+

482 # This uses the iterative refinement algorithm we just fixed 

+

483 page, start_position = self.renderer.render_page_backward( 

+

484 self.current_position, self.font_scale) 

+

485 

+

486 if start_position != self.current_position: 486 ↛ 494line 486 didn't jump to line 494 because the condition on line 486 was always true

+

487 # Save this calculated position to history for future use 

+

488 self._add_to_history(start_position, self.font_scale) 

+

489 

+

490 self.current_position = start_position 

+

491 self._notify_position_changed() 

+

492 return page 

+

493 

+

494 return None # At beginning of document 

+

495 

+

496 def _is_at_beginning(self) -> bool: 

+

497 """ 

+

498 Check if we're at the beginning of the document content. 

+

499 

+

500 If a cover exists (first block is an Image), the beginning of content 

+

501 is at block_index=1. Otherwise, it's at block_index=0. 

+

502 """ 

+

503 # Determine the first content block index 

+

504 first_content_block = 1 if (self._has_cover and self.blocks and isinstance(self.blocks[0], Image)) else 0 

+

505 

+

506 return (self.current_position.chapter_index == 0 and 

+

507 self.current_position.block_index == first_content_block and 

+

508 self.current_position.word_index == 0) 

+

509 

+

510 def jump_to_position(self, position: RenderingPosition) -> Page: 

+

511 """ 

+

512 Jump to a specific position in the document. 

+

513 

+

514 Args: 

+

515 position: Position to jump to 

+

516 

+

517 Returns: 

+

518 Page at the new position 

+

519 """ 

+

520 self.current_position = position 

+

521 self._on_cover_page = False # Jumping to a position means we're past the cover 

+

522 self._notify_position_changed() 

+

523 return self.get_current_page() 

+

524 

+

525 def jump_to_chapter(self, chapter_title: str) -> Optional[Page]: 

+

526 """ 

+

527 Jump to a specific chapter by title. 

+

528 

+

529 Args: 

+

530 chapter_title: Title of the chapter to jump to 

+

531 

+

532 Returns: 

+

533 Page at chapter start or None if chapter not found 

+

534 """ 

+

535 position = self.chapter_navigator.get_chapter_position(chapter_title) 

+

536 if position: 

+

537 return self.jump_to_position(position) 

+

538 return None 

+

539 

+

540 def jump_to_chapter_index(self, chapter_index: int) -> Optional[Page]: 

+

541 """ 

+

542 Jump to a chapter by index. 

+

543 

+

544 Args: 

+

545 chapter_index: Index of the chapter (0-based) 

+

546 

+

547 Returns: 

+

548 Page at chapter start or None if index invalid 

+

549 """ 

+

550 chapters = self.chapter_navigator.chapters 

+

551 if 0 <= chapter_index < len(chapters): 

+

552 return self.jump_to_position(chapters[chapter_index].position) 

+

553 return None 

+

554 

+

555 def _add_to_history(self, position: RenderingPosition, font_scale: float): 

+

556 """ 

+

557 Add a page position to the navigation history. 

+

558 

+

559 Args: 

+

560 position: The page start position to remember 

+

561 font_scale: The font scale at this position 

+

562 """ 

+

563 # Only add if it's different from the last entry 

+

564 if not self._page_history or \ 

+

565 self._page_history[-1][0] != position or \ 

+

566 self._page_history[-1][1] != font_scale: 

+

567 

+

568 self._page_history.append((position.copy(), font_scale)) 

+

569 

+

570 # Trim history if it exceeds max size 

+

571 if len(self._page_history) > self._max_history_size: 571 ↛ 572line 571 didn't jump to line 572 because the condition on line 571 was never true

+

572 self._page_history.pop(0) 

+

573 

+

574 def _get_from_history( 

+

575 self, 

+

576 current_position: RenderingPosition, 

+

577 current_font_scale: float) -> Optional[RenderingPosition]: 

+

578 """ 

+

579 Get the previous page position from history. 

+

580 

+

581 Searches backward through history to find the last position that 

+

582 comes before the current position at the same font scale. 

+

583 

+

584 Args: 

+

585 current_position: Current page position 

+

586 current_font_scale: Current font scale 

+

587 

+

588 Returns: 

+

589 Previous page position or None if not found in history 

+

590 """ 

+

591 # Search backward through history 

+

592 for i in range(len(self._page_history) - 1, -1, -1): 

+

593 hist_position, hist_font_scale = self._page_history[i] 

+

594 

+

595 # Must match font scale 

+

596 if hist_font_scale != current_font_scale: 596 ↛ 597line 596 didn't jump to line 597 because the condition on line 596 was never true

+

597 continue 

+

598 

+

599 # Must be before current position 

+

600 if (hist_position.chapter_index < current_position.chapter_index or 

+

601 (hist_position.chapter_index == current_position.chapter_index and 

+

602 hist_position.block_index < current_position.block_index) or 

+

603 (hist_position.chapter_index == current_position.chapter_index and 

+

604 hist_position.block_index == current_position.block_index and 

+

605 hist_position.word_index < current_position.word_index)): 

+

606 

+

607 # Found a previous position - remove it and everything after from history 

+

608 # since we're navigating backward 

+

609 self._page_history = self._page_history[:i] 

+

610 return hist_position.copy() 

+

611 

+

612 return None 

+

613 

+

614 def _clear_history(self): 

+

615 """Clear the page navigation history.""" 

+

616 self._page_history.clear() 

+

617 

+

618 def set_font_scale(self, scale: float) -> Page: 

+

619 """ 

+

620 Change the font scale and re-render current page. 

+

621 

+

622 Clears page history since font changes invalidate all cached positions. 

+

623 

+

624 Args: 

+

625 scale: Font scaling factor (1.0 = normal, 2.0 = double size, etc.) 

+

626 

+

627 Returns: 

+

628 Re-rendered page with new font scale 

+

629 """ 

+

630 if scale != self.font_scale: 

+

631 self.font_scale = scale 

+

632 # Clear history since font scale changes invalidate all cached positions 

+

633 self._clear_history() 

+

634 # The renderer will handle cache invalidation 

+

635 

+

636 return self.get_current_page() 

+

637 

+

638 def get_font_scale(self) -> float: 

+

639 """Get the current font scale""" 

+

640 return self.font_scale 

+

641 

+

642 def set_font_family(self, family: Optional[BundledFont]) -> Page: 

+

643 """ 

+

644 Change the font family and re-render current page. 

+

645 

+

646 Switches all text in the document to use the specified bundled font family 

+

647 while preserving font weights, styles, sizes, and other attributes. 

+

648 Clears page history and cache since font changes invalidate all cached positions. 

+

649 

+

650 Args: 

+

651 family: Bundled font family to use (SANS, SERIF, MONOSPACE, or None for original fonts) 

+

652 

+

653 Returns: 

+

654 Re-rendered page with new font family 

+

655 

+

656 Example: 

+

657 >>> from pyWebLayout.style.fonts import BundledFont 

+

658 >>> manager.set_font_family(BundledFont.SERIF) # Switch to serif 

+

659 >>> manager.set_font_family(BundledFont.SANS) # Switch to sans 

+

660 >>> manager.set_font_family(None) # Restore original fonts 

+

661 """ 

+

662 # Update the renderer's font family 

+

663 self.renderer.set_font_family(family) 

+

664 

+

665 # Clear history since font changes invalidate all cached positions 

+

666 self._clear_history() 

+

667 

+

668 return self.get_current_page() 

+

669 

+

670 def get_font_family(self) -> Optional[BundledFont]: 

+

671 """ 

+

672 Get the current font family override. 

+

673 

+

674 Returns: 

+

675 Current font family (SANS, SERIF, MONOSPACE) or None if using original fonts 

+

676 """ 

+

677 return self.renderer.get_font_family() 

+

678 

+

679 def increase_line_spacing(self, amount: int = 2) -> Page: 

+

680 """ 

+

681 Increase line spacing and re-render current page. 

+

682 

+

683 Clears page history since spacing changes invalidate all cached positions. 

+

684 

+

685 Args: 

+

686 amount: Pixels to add to line spacing (default: 2) 

+

687 

+

688 Returns: 

+

689 Re-rendered page with increased line spacing 

+

690 """ 

+

691 self.page_style.line_spacing += amount 

+

692 self.renderer.page_style = self.page_style # Update renderer's reference 

+

693 self.renderer.buffer.invalidate_all() # Clear cache to force re-render 

+

694 self._clear_history() # Clear position history 

+

695 return self.get_current_page() 

+

696 

+

697 def decrease_line_spacing(self, amount: int = 2) -> Page: 

+

698 """ 

+

699 Decrease line spacing and re-render current page. 

+

700 

+

701 Clears page history since spacing changes invalidate all cached positions. 

+

702 

+

703 Args: 

+

704 amount: Pixels to remove from line spacing (default: 2) 

+

705 

+

706 Returns: 

+

707 Re-rendered page with decreased line spacing 

+

708 """ 

+

709 self.page_style.line_spacing = max(0, self.page_style.line_spacing - amount) 

+

710 self.renderer.page_style = self.page_style # Update renderer's reference 

+

711 self.renderer.buffer.invalidate_all() # Clear cache to force re-render 

+

712 self._clear_history() # Clear position history 

+

713 return self.get_current_page() 

+

714 

+

715 def increase_inter_block_spacing(self, amount: int = 5) -> Page: 

+

716 """ 

+

717 Increase spacing between blocks and re-render current page. 

+

718 

+

719 Clears page history since spacing changes invalidate all cached positions. 

+

720 

+

721 Args: 

+

722 amount: Pixels to add to inter-block spacing (default: 5) 

+

723 

+

724 Returns: 

+

725 Re-rendered page with increased block spacing 

+

726 """ 

+

727 self.page_style.inter_block_spacing += amount 

+

728 self.renderer.page_style = self.page_style # Update renderer's reference 

+

729 self.renderer.buffer.invalidate_all() # Clear cache to force re-render 

+

730 self._clear_history() # Clear position history 

+

731 return self.get_current_page() 

+

732 

+

733 def decrease_inter_block_spacing(self, amount: int = 5) -> Page: 

+

734 """ 

+

735 Decrease spacing between blocks and re-render current page. 

+

736 

+

737 Clears page history since spacing changes invalidate all cached positions. 

+

738 

+

739 Args: 

+

740 amount: Pixels to remove from inter-block spacing (default: 5) 

+

741 

+

742 Returns: 

+

743 Re-rendered page with decreased block spacing 

+

744 """ 

+

745 self.page_style.inter_block_spacing = max( 

+

746 0, self.page_style.inter_block_spacing - amount) 

+

747 self.renderer.page_style = self.page_style # Update renderer's reference 

+

748 self.renderer.buffer.invalidate_all() # Clear cache to force re-render 

+

749 self._clear_history() # Clear position history 

+

750 return self.get_current_page() 

+

751 

+

752 def increase_word_spacing(self, amount: int = 2) -> Page: 

+

753 """ 

+

754 Increase spacing between words and re-render current page. 

+

755 

+

756 Clears page history since spacing changes invalidate all cached positions. 

+

757 

+

758 Args: 

+

759 amount: Pixels to add to word spacing (default: 2) 

+

760 

+

761 Returns: 

+

762 Re-rendered page with increased word spacing 

+

763 """ 

+

764 self.page_style.word_spacing += amount 

+

765 self.renderer.page_style = self.page_style # Update renderer's reference 

+

766 self.renderer.buffer.invalidate_all() # Clear cache to force re-render 

+

767 self._clear_history() # Clear position history 

+

768 return self.get_current_page() 

+

769 

+

770 def decrease_word_spacing(self, amount: int = 2) -> Page: 

+

771 """ 

+

772 Decrease spacing between words and re-render current page. 

+

773 

+

774 Clears page history since spacing changes invalidate all cached positions. 

+

775 

+

776 Args: 

+

777 amount: Pixels to remove from word spacing (default: 2) 

+

778 

+

779 Returns: 

+

780 Re-rendered page with decreased word spacing 

+

781 """ 

+

782 self.page_style.word_spacing = max(0, self.page_style.word_spacing - amount) 

+

783 self.renderer.page_style = self.page_style # Update renderer's reference 

+

784 self.renderer.buffer.invalidate_all() # Clear cache to force re-render 

+

785 self._clear_history() # Clear position history 

+

786 return self.get_current_page() 

+

787 

+

788 def get_table_of_contents( 

+

789 self) -> List[Tuple[str, HeadingLevel, RenderingPosition]]: 

+

790 """ 

+

791 Get the table of contents. 

+

792 

+

793 Returns: 

+

794 List of (title, level, position) tuples 

+

795 """ 

+

796 return self.chapter_navigator.get_table_of_contents() 

+

797 

+

798 def get_current_chapter(self) -> Optional[ChapterInfo]: 

+

799 """ 

+

800 Get information about the current chapter. 

+

801 

+

802 Returns: 

+

803 Current chapter info or None if no chapters 

+

804 """ 

+

805 return self.chapter_navigator.get_current_chapter(self.current_position) 

+

806 

+

807 def add_bookmark(self, name: str) -> bool: 

+

808 """ 

+

809 Add a bookmark at the current position. 

+

810 

+

811 Args: 

+

812 name: Bookmark name 

+

813 

+

814 Returns: 

+

815 True if bookmark was added successfully 

+

816 """ 

+

817 try: 

+

818 self.bookmark_manager.add_bookmark(name, self.current_position) 

+

819 return True 

+

820 except Exception: 

+

821 return False 

+

822 

+

823 def remove_bookmark(self, name: str) -> bool: 

+

824 """ 

+

825 Remove a bookmark. 

+

826 

+

827 Args: 

+

828 name: Bookmark name 

+

829 

+

830 Returns: 

+

831 True if bookmark was removed 

+

832 """ 

+

833 return self.bookmark_manager.remove_bookmark(name) 

+

834 

+

835 def jump_to_bookmark(self, name: str) -> Optional[Page]: 

+

836 """ 

+

837 Jump to a bookmark. 

+

838 

+

839 Args: 

+

840 name: Bookmark name 

+

841 

+

842 Returns: 

+

843 Page at bookmark position or None if bookmark not found 

+

844 """ 

+

845 position = self.bookmark_manager.get_bookmark(name) 

+

846 if position: 

+

847 return self.jump_to_position(position) 

+

848 return None 

+

849 

+

850 def list_bookmarks(self) -> List[Tuple[str, RenderingPosition]]: 

+

851 """ 

+

852 Get all bookmarks. 

+

853 

+

854 Returns: 

+

855 List of (name, position) tuples 

+

856 """ 

+

857 return self.bookmark_manager.list_bookmarks() 

+

858 

+

859 # ------------------------------------------------------------------ 

+

860 # Highlights 

+

861 # 

+

862 # A Highlight carries pixel bounds, which belong to the one rendering it 

+

863 # was taken from: change the font scale or page size and they no longer 

+

864 # describe anything. Each highlight therefore also records the 

+

865 # RenderingPosition of the page it was made on, and page association goes 

+

866 # through that rather than through the bounds. 

+

867 # ------------------------------------------------------------------ 

+

868 

+

869 def highlight_point(self, 

+

870 point: Tuple[int, int], 

+

871 color: Tuple[int, int, int, int] = HighlightColor.YELLOW.value, 

+

872 note: Optional[str] = None, 

+

873 tags: Optional[List[str]] = None) -> Optional[Highlight]: 

+

874 """ 

+

875 Highlight whatever is at a point on the current page. 

+

876 

+

877 Args: 

+

878 point: (x, y) in page coordinates, as delivered by a tap 

+

879 color: RGBA fill, e.g. one of HighlightColor 

+

880 note: Optional annotation 

+

881 tags: Optional categorization tags 

+

882 

+

883 Returns: 

+

884 The stored Highlight, or None if nothing was at that point. 

+

885 """ 

+

886 result = self.get_current_page().query_point(point) 

+

887 if result is None or result.object_type == "empty": 

+

888 return None 

+

889 

+

890 return self._store_highlight(result, color, note, tags) 

+

891 

+

892 def highlight_range(self, 

+

893 start: Tuple[int, int], 

+

894 end: Tuple[int, int], 

+

895 color: Tuple[int, int, int, int] = HighlightColor.YELLOW.value, 

+

896 note: Optional[str] = None, 

+

897 tags: Optional[List[str]] = None) -> Optional[Highlight]: 

+

898 """ 

+

899 Highlight the text between two points on the current page. 

+

900 

+

901 Args: 

+

902 start: (x, y) where the selection began 

+

903 end: (x, y) where the selection ended 

+

904 color: RGBA fill, e.g. one of HighlightColor 

+

905 note: Optional annotation 

+

906 tags: Optional categorization tags 

+

907 

+

908 Returns: 

+

909 The stored Highlight, or None if the range selected no text. 

+

910 """ 

+

911 selection = self.get_current_page().query_range(start, end) 

+

912 if not selection.results: 

+

913 return None 

+

914 

+

915 return self._store_highlight(selection, color, note, tags) 

+

916 

+

917 def _store_highlight(self, result, color, note, tags) -> Highlight: 

+

918 """Build a Highlight from a query result and persist it.""" 

+

919 highlight = create_highlight_from_query_result( 

+

920 result, color=color, note=note, tags=tags, 

+

921 position=self.current_position.to_dict()) 

+

922 self.highlight_manager.add_highlight(highlight) 

+

923 return highlight 

+

924 

+

925 def remove_highlight(self, highlight_id: str) -> bool: 

+

926 """ 

+

927 Remove a highlight. 

+

928 

+

929 Args: 

+

930 highlight_id: ID of the highlight to remove 

+

931 

+

932 Returns: 

+

933 True if it existed and was removed 

+

934 """ 

+

935 return self.highlight_manager.remove_highlight(highlight_id) 

+

936 

+

937 def list_highlights(self) -> List[Highlight]: 

+

938 """Get every highlight in this document.""" 

+

939 return self.highlight_manager.list_highlights() 

+

940 

+

941 def get_highlights_for_current_page(self) -> List[Highlight]: 

+

942 """ 

+

943 Get the highlights made on the page currently being displayed. 

+

944 

+

945 Matched on the recorded RenderingPosition, so this stays correct across 

+

946 font changes; highlights saved before the position field existed have 

+

947 no position and are never matched. 

+

948 """ 

+

949 current = self.current_position.to_dict() 

+

950 return [h for h in self.highlight_manager.list_highlights() 

+

951 if h.position == current] 

+

952 

+

953 def clear_highlights(self) -> None: 

+

954 """Remove every highlight in this document.""" 

+

955 self.highlight_manager.clear_all() 

+

956 

+

957 # ------------------------------------------------------------------ 

+

958 # Pointer interaction 

+

959 # 

+

960 # Press/hover feedback is state that belongs to one rendered page, so the 

+

961 # state machine is rebound whenever the displayed page changes. Callers get 

+

962 # a fresh frame back when something changed visually, and None when nothing 

+

963 # did - so a UI can skip a redraw it does not need. 

+

964 # ------------------------------------------------------------------ 

+

965 

+

966 def _interaction_state(self) -> InteractionStateManager: 

+

967 """The state machine for the page currently displayed.""" 

+

968 page = self.get_current_page() 

+

969 if self._interaction_page is not page: 

+

970 if self._interaction_state_manager is not None: 

+

971 self._interaction_state_manager.reset() 

+

972 self._interaction_state_manager = InteractionStateManager(page) 

+

973 self._interaction_page = page 

+

974 return self._interaction_state_manager 

+

975 

+

976 def handle_hover(self, point: Tuple[int, int]) -> Optional[Image_.Image]: 

+

977 """ 

+

978 Update hover feedback for a pointer at `point`. 

+

979 

+

980 Args: 

+

981 point: (x, y) in page coordinates 

+

982 

+

983 Returns: 

+

984 A re-rendered frame if the hover state changed, else None. 

+

985 """ 

+

986 return self._interaction_state().update_hover(point) 

+

987 

+

988 def handle_touch_down(self, point: Tuple[int, int]) -> Optional[Image_.Image]: 

+

989 """ 

+

990 Show pressed feedback for whatever interactive element is at `point`. 

+

991 

+

992 Args: 

+

993 point: (x, y) in page coordinates 

+

994 

+

995 Returns: 

+

996 A frame showing the pressed state, or None if nothing interactive 

+

997 is there. 

+

998 """ 

+

999 return self._interaction_state().handle_mouse_down(point) 

+

1000 

+

1001 def handle_touch_up(self, point: Tuple[int, int]) -> Tuple[Optional[Image_.Image], Any]: 

+

1002 """ 

+

1003 Release the pressed element and run its action. 

+

1004 

+

1005 Args: 

+

1006 point: (x, y) in page coordinates 

+

1007 

+

1008 Returns: 

+

1009 (frame, callback_result). Both are None if no element was pressed. 

+

1010 """ 

+

1011 return self._interaction_state().handle_mouse_up(point) 

+

1012 

+

1013 def reset_interaction_state(self) -> None: 

+

1014 """Clear any hover or press feedback, e.g. when the pointer leaves.""" 

+

1015 if self._interaction_state_manager is not None: 

+

1016 self._interaction_state_manager.reset() 

+

1017 

+

1018 def get_reading_progress(self) -> float: 

+

1019 """ 

+

1020 Get reading progress as a percentage. 

+

1021 

+

1022 Returns: 

+

1023 Progress from 0.0 to 1.0 

+

1024 """ 

+

1025 if not self.blocks: 

+

1026 return 0.0 

+

1027 

+

1028 # Simple progress calculation based on block index 

+

1029 # A more sophisticated version would consider word positions 

+

1030 total_blocks = len(self.blocks) 

+

1031 current_block = min(self.current_position.block_index, total_blocks - 1) 

+

1032 

+

1033 return current_block / max(1, total_blocks - 1) 

+

1034 

+

1035 def has_cover(self) -> bool: 

+

1036 """ 

+

1037 Check if the document has a cover page. 

+

1038 

+

1039 Returns: 

+

1040 True if a cover page is available 

+

1041 """ 

+

1042 return self._has_cover 

+

1043 

+

1044 def is_on_cover(self) -> bool: 

+

1045 """ 

+

1046 Check if currently viewing the cover page. 

+

1047 

+

1048 Returns: 

+

1049 True if on the cover page 

+

1050 """ 

+

1051 return self._on_cover_page 

+

1052 

+

1053 def jump_to_cover(self) -> Optional[Page]: 

+

1054 """ 

+

1055 Jump to the cover page if one exists. 

+

1056 

+

1057 Returns: 

+

1058 Cover page or None if no cover exists 

+

1059 """ 

+

1060 if not self._has_cover: 1060 ↛ 1061line 1060 didn't jump to line 1061 because the condition on line 1060 was never true

+

1061 return None 

+

1062 

+

1063 self._on_cover_page = True 

+

1064 self._notify_position_changed() 

+

1065 return self.get_current_page() 

+

1066 

+

1067 def get_position_info(self) -> Dict[str, Any]: 

+

1068 """ 

+

1069 Get detailed information about the current position. 

+

1070 

+

1071 Returns: 

+

1072 Dictionary with position details 

+

1073 """ 

+

1074 current_chapter = self.get_current_chapter() 

+

1075 font_family = self.get_font_family() 

+

1076 

+

1077 return { 

+

1078 'position': self.current_position.to_dict(), 

+

1079 'on_cover': self._on_cover_page, 

+

1080 'has_cover': self._has_cover, 

+

1081 'chapter': { 

+

1082 'title': current_chapter.title if current_chapter else None, 

+

1083 'level': current_chapter.level if current_chapter else None, 

+

1084 'index': current_chapter.block_index if current_chapter else None 

+

1085 }, 

+

1086 'progress': self.get_reading_progress(), 

+

1087 'font_scale': self.font_scale, 

+

1088 'font_family': font_family.value if font_family else None, 

+

1089 'page_size': self.page_size 

+

1090 } 

+

1091 

+

1092 def get_cache_stats(self) -> Dict[str, Any]: 

+

1093 """ 

+

1094 Get cache statistics for debugging/monitoring. 

+

1095 

+

1096 Returns: 

+

1097 Dictionary with cache statistics 

+

1098 """ 

+

1099 return self.renderer.get_cache_stats() 

+

1100 

+

1101 def shutdown(self): 

+

1102 """ 

+

1103 Shutdown the ereader manager and clean up resources. 

+

1104 Call this when the application is closing. 

+

1105 

+

1106 Idempotent: calling it twice saves the position once. 

+

1107 """ 

+

1108 if getattr(self, '_shutdown_done', False): 

+

1109 return 

+

1110 self._shutdown_done = True 

+

1111 

+

1112 # Save current position 

+

1113 self.bookmark_manager.save_reading_position(self.current_position) 

+

1114 

+

1115 # Release cached pages 

+

1116 self.renderer.shutdown() 

+

1117 

+

1118 def __del__(self): 

+

1119 """ 

+

1120 Best-effort cleanup for callers that never called shutdown(). 

+

1121 

+

1122 Finalisers run during interpreter teardown, when modules and globals 

+

1123 may already be torn down, so this must never raise and must never 

+

1124 block. Applications should call shutdown() explicitly. 

+

1125 """ 

+

1126 try: 

+

1127 self.shutdown() 

+

1128 except Exception: 

+

1129 pass 

+

1130 

+

1131 

+

1132# Convenience function for quick setup 

+

1133def create_ereader_manager(blocks: List[Block], 

+

1134 page_size: Tuple[int, int], 

+

1135 document_id: str = "default", 

+

1136 **kwargs) -> EreaderLayoutManager: 

+

1137 """ 

+

1138 Convenience function to create an ereader manager with sensible defaults. 

+

1139 

+

1140 Args: 

+

1141 blocks: Document blocks to render 

+

1142 page_size: Page size (width, height) in pixels 

+

1143 document_id: Unique identifier for the document 

+

1144 **kwargs: Additional arguments passed to EreaderLayoutManager 

+

1145 

+

1146 Returns: 

+

1147 Configured EreaderLayoutManager instance 

+

1148 """ 

+

1149 return EreaderLayoutManager(blocks, page_size, document_id, **kwargs) 

+
+ + + diff --git a/cov_info/htmlcov/z_427cc3035faf7633_page_buffer_py.html b/cov_info/htmlcov/z_427cc3035faf7633_page_buffer_py.html new file mode 100644 index 0000000..4c17091 --- /dev/null +++ b/cov_info/htmlcov/z_427cc3035faf7633_page_buffer_py.html @@ -0,0 +1,443 @@ + + + + + Coverage for pyWebLayout/layout/page_buffer.py: 85% + + + + + +
+
+

+ Coverage for pyWebLayout/layout/page_buffer.py: + 85% +

+ +

+ 110 statements   + + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.15.4, + created at 2026-08-08 20:34 +0000 +

+ +
+
+
+

1""" 

+

2Page caching for ereader navigation. 

+

3 

+

4`PageBuffer` is an LRU cache of rendered pages plus the position links between 

+

5them; `BufferedPageRenderer` wraps it around a `BidirectionalLayouter`. 

+

6 

+

7This module used to render pages ahead of time in a `ProcessPoolExecutor`. That 

+

8never worked and has been removed — see S12 in docs/LAYOUT_REMEDIATION_SPEC.md 

+

9and R1/R2 in docs/ARCHITECTURE_REVIEW.md. In short: the worker returned 

+

10`pickle.dumps(page)`, and a Page holds a live PIL canvas, which is not 

+

11picklable, so every job failed and the result was discarded. The cost — four 

+

12interpreter copies and the whole block list shipped per job — was paid in full 

+

13for no benefit. On Python 3.14, where the default start method became 

+

14`forkserver`, submitting from module-level code raised outright. 

+

15 

+

16Prefetch is not needed. Measured on the tests/data Wikipedia fixture (411 

+

17blocks) with the text caches warm, one page render costs: 

+

18 

+

19 800x600 p50 8.8 ms p95 15.4 ms 

+

20 1072x1448 p50 13.8 ms p95 56.1 ms 

+

21 

+

22A page turn is cheaper than the IPC that was meant to hide it. If a slower 

+

23target device ever changes that, the fallback is a synchronous `readahead()` 

+

24method on this class, or a single worker *thread* — layout is PIL-bound and PIL 

+

25releases the GIL — not a process pool. Making the concrete tree picklable 

+

26(Page -> Line -> Text -> Font -> FreeTypeFont) is a large surface area to 

+

27maintain for a cache. 

+

28""" 

+

29 

+

30from __future__ import annotations 

+

31from typing import Dict, Optional, List, Tuple, Any 

+

32from collections import OrderedDict 

+

33 

+

34from .ereader_layout import RenderingPosition, BidirectionalLayouter, FontFamilyOverride 

+

35from pyWebLayout.concrete.page import Page 

+

36from pyWebLayout.abstract.block import Block 

+

37from pyWebLayout.style.page_style import PageStyle 

+

38from pyWebLayout.style.fonts import BundledFont 

+

39 

+

40 

+

41class PageBuffer: 

+

42 """ 

+

43 LRU cache of rendered pages, with separate forward and backward buffers and 

+

44 the position links between adjacent pages. 

+

45 """ 

+

46 

+

47 def __init__(self, buffer_size: int = 5): 

+

48 """ 

+

49 Initialize the page buffer. 

+

50 

+

51 Args: 

+

52 buffer_size: Number of pages to cache in each direction 

+

53 """ 

+

54 self.buffer_size = buffer_size 

+

55 

+

56 # LRU caches for forward and backward pages 

+

57 self.forward_buffer: OrderedDict[RenderingPosition, Page] = OrderedDict() 

+

58 self.backward_buffer: OrderedDict[RenderingPosition, Page] = OrderedDict() 

+

59 

+

60 # Position tracking for next/previous positions 

+

61 self.position_map: Dict[RenderingPosition, 

+

62 RenderingPosition] = {} # current -> next 

+

63 self.reverse_position_map: Dict[RenderingPosition, 

+

64 RenderingPosition] = {} # current -> previous 

+

65 

+

66 # Document state 

+

67 self.blocks: Optional[List[Block]] = None 

+

68 self.page_style: Optional[PageStyle] = None 

+

69 self.current_font_scale: float = 1.0 

+

70 self.current_font_family: Optional[BundledFont] = None 

+

71 

+

72 def initialize( 

+

73 self, 

+

74 blocks: List[Block], 

+

75 page_style: PageStyle, 

+

76 font_scale: float = 1.0, 

+

77 font_family: Optional[BundledFont] = None): 

+

78 """ 

+

79 Initialize the buffer with document blocks and page style. 

+

80 

+

81 Args: 

+

82 blocks: Document blocks to render 

+

83 page_style: Page styling configuration 

+

84 font_scale: Current font scaling factor 

+

85 font_family: Optional font family override 

+

86 """ 

+

87 self.blocks = blocks 

+

88 self.page_style = page_style 

+

89 self.current_font_scale = font_scale 

+

90 self.current_font_family = font_family 

+

91 

+

92 def get_page(self, position: RenderingPosition) -> Optional[Page]: 

+

93 """ 

+

94 Get a cached page if available. 

+

95 

+

96 Args: 

+

97 position: Position to get page for 

+

98 

+

99 Returns: 

+

100 Cached page or None if not available 

+

101 """ 

+

102 # Check forward buffer first 

+

103 if position in self.forward_buffer: 

+

104 # Move to end (most recently used) 

+

105 page = self.forward_buffer.pop(position) 

+

106 self.forward_buffer[position] = page 

+

107 return page 

+

108 

+

109 # Check backward buffer 

+

110 if position in self.backward_buffer: 

+

111 # Move to end (most recently used) 

+

112 page = self.backward_buffer.pop(position) 

+

113 self.backward_buffer[position] = page 

+

114 return page 

+

115 

+

116 return None 

+

117 

+

118 def cache_page( 

+

119 self, 

+

120 position: RenderingPosition, 

+

121 page: Page, 

+

122 next_position: Optional[RenderingPosition] = None, 

+

123 is_backward: bool = False): 

+

124 """ 

+

125 Cache a rendered page with LRU eviction. 

+

126 

+

127 Args: 

+

128 position: Position of the page 

+

129 page: Rendered page to cache 

+

130 next_position: Position of the next page (for forward navigation) 

+

131 is_backward: Whether this is a backward-rendered page 

+

132 """ 

+

133 target_buffer = self.backward_buffer if is_backward else self.forward_buffer 

+

134 

+

135 # Add to cache 

+

136 target_buffer[position] = page 

+

137 

+

138 # Track position relationships 

+

139 if next_position: 

+

140 if is_backward: 

+

141 self.reverse_position_map[next_position] = position 

+

142 else: 

+

143 self.position_map[position] = next_position 

+

144 

+

145 # Evict oldest if buffer is full 

+

146 if len(target_buffer) > self.buffer_size: 

+

147 oldest_pos, _ = target_buffer.popitem(last=False) 

+

148 # Clean up position maps 

+

149 self.position_map.pop(oldest_pos, None) 

+

150 self.reverse_position_map.pop(oldest_pos, None) 

+

151 

+

152 def invalidate_all(self): 

+

153 """Clear all cached pages""" 

+

154 self.forward_buffer.clear() 

+

155 self.backward_buffer.clear() 

+

156 self.position_map.clear() 

+

157 self.reverse_position_map.clear() 

+

158 

+

159 def set_font_scale(self, font_scale: float): 

+

160 """ 

+

161 Update font scale and invalidate cache. 

+

162 

+

163 Args: 

+

164 font_scale: New font scaling factor 

+

165 """ 

+

166 if font_scale != self.current_font_scale: 

+

167 self.current_font_scale = font_scale 

+

168 self.invalidate_all() 

+

169 

+

170 def set_font_family(self, font_family: Optional[BundledFont]): 

+

171 """ 

+

172 Update font family and invalidate cache. 

+

173 

+

174 Args: 

+

175 font_family: New font family (None = use original fonts) 

+

176 """ 

+

177 if font_family != self.current_font_family: 

+

178 self.current_font_family = font_family 

+

179 self.invalidate_all() 

+

180 

+

181 def get_cache_stats(self) -> Dict[str, Any]: 

+

182 """Get cache statistics for debugging/monitoring""" 

+

183 return { 

+

184 'forward_buffer_size': len(self.forward_buffer), 

+

185 'backward_buffer_size': len(self.backward_buffer), 

+

186 'position_mappings': len(self.position_map), 

+

187 'reverse_position_mappings': len(self.reverse_position_map), 

+

188 'current_font_scale': self.current_font_scale, 

+

189 'current_font_family': self.current_font_family.value if self.current_font_family else None 

+

190 } 

+

191 

+

192 def shutdown(self): 

+

193 """ 

+

194 Release cached pages. 

+

195 

+

196 Cheap and idempotent. There is deliberately no __del__ calling this: 

+

197 blocking work in a finaliser is what deadlocked the interpreter at exit 

+

198 while the process pool existed. 

+

199 """ 

+

200 self.invalidate_all() 

+

201 

+

202 

+

203class BufferedPageRenderer: 

+

204 """ 

+

205 High-level interface for page rendering with an LRU cache in front of the 

+

206 layouter. 

+

207 """ 

+

208 

+

209 def __init__(self, 

+

210 blocks: List[Block], 

+

211 page_style: PageStyle, 

+

212 buffer_size: int = 5, 

+

213 page_size: Tuple[int, 

+

214 int] = (800, 

+

215 600), 

+

216 font_family: Optional[BundledFont] = None): 

+

217 """ 

+

218 Initialize the buffered renderer. 

+

219 

+

220 Args: 

+

221 blocks: Document blocks to render 

+

222 page_style: Page styling configuration 

+

223 buffer_size: Number of pages to cache in each direction 

+

224 page_size: Page size (width, height) in pixels 

+

225 font_family: Optional font family override 

+

226 """ 

+

227 # Create font family override if specified 

+

228 font_family_override = FontFamilyOverride(font_family) if font_family else None 

+

229 

+

230 self.layouter = BidirectionalLayouter(blocks, page_style, page_size, font_family_override=font_family_override) 

+

231 self.buffer = PageBuffer(buffer_size) 

+

232 self.buffer.initialize(blocks, page_style, font_family=font_family) 

+

233 self.page_size = page_size 

+

234 self.blocks = blocks 

+

235 self.page_style = page_style 

+

236 

+

237 self.current_position = RenderingPosition() 

+

238 self.font_scale = 1.0 

+

239 self.font_family = font_family 

+

240 

+

241 def render_page(self, position: RenderingPosition, 

+

242 font_scale: float = 1.0) -> Tuple[Page, RenderingPosition]: 

+

243 """ 

+

244 Render a page, serving it from cache when possible. 

+

245 

+

246 Args: 

+

247 position: Position to render from 

+

248 font_scale: Font scaling factor 

+

249 

+

250 Returns: 

+

251 Tuple of (rendered_page, next_position) 

+

252 """ 

+

253 # Update font scale if changed 

+

254 if font_scale != self.font_scale: 

+

255 self.font_scale = font_scale 

+

256 self.buffer.set_font_scale(font_scale) 

+

257 

+

258 # Check cache first 

+

259 cached_page = self.buffer.get_page(position) 

+

260 if cached_page: 

+

261 # Only use the cache if we also know where the next page starts; 

+

262 # otherwise fall through and compute it. 

+

263 next_pos = self.buffer.position_map.get(position) 

+

264 if next_pos is not None: 

+

265 return cached_page, next_pos 

+

266 

+

267 # Render the page directly 

+

268 page, next_pos = self.layouter.render_page_forward(position, font_scale) 

+

269 

+

270 # Cache the result 

+

271 self.buffer.cache_page(position, page, next_pos) 

+

272 

+

273 return page, next_pos 

+

274 

+

275 def render_page_backward(self, 

+

276 end_position: RenderingPosition, 

+

277 font_scale: float = 1.0) -> Tuple[Page, 

+

278 RenderingPosition]: 

+

279 """ 

+

280 Render a page ending at the given position, serving it from cache when 

+

281 possible. 

+

282 

+

283 Args: 

+

284 end_position: Position where page should end 

+

285 font_scale: Font scaling factor 

+

286 

+

287 Returns: 

+

288 Tuple of (rendered_page, start_position) 

+

289 """ 

+

290 # Update font scale if changed 

+

291 if font_scale != self.font_scale: 291 ↛ 292line 291 didn't jump to line 292 because the condition on line 291 was never true

+

292 self.font_scale = font_scale 

+

293 self.buffer.set_font_scale(font_scale) 

+

294 

+

295 # Check cache first 

+

296 cached_page = self.buffer.get_page(end_position) 

+

297 if cached_page: 297 ↛ 300line 297 didn't jump to line 300 because the condition on line 297 was never true

+

298 # Only use the cache if we also know where the previous page 

+

299 # starts; otherwise fall through and compute it. 

+

300 prev_pos = self.buffer.reverse_position_map.get(end_position) 

+

301 if prev_pos is not None: 

+

302 return cached_page, prev_pos 

+

303 

+

304 # Render the page directly 

+

305 page, start_pos = self.layouter.render_page_backward(end_position, font_scale) 

+

306 

+

307 # Cache the result 

+

308 self.buffer.cache_page(start_pos, page, end_position, is_backward=True) 

+

309 

+

310 return page, start_pos 

+

311 

+

312 def set_font_family(self, font_family: Optional[BundledFont]): 

+

313 """ 

+

314 Change the font family and invalidate cache. 

+

315 

+

316 Args: 

+

317 font_family: New font family (None = use original fonts) 

+

318 """ 

+

319 if font_family != self.font_family: 

+

320 self.font_family = font_family 

+

321 

+

322 # Update buffer 

+

323 self.buffer.set_font_family(font_family) 

+

324 

+

325 # Recreate layouter with new font family override 

+

326 font_family_override = FontFamilyOverride(font_family) if font_family else None 

+

327 self.layouter = BidirectionalLayouter( 

+

328 self.blocks, 

+

329 self.page_style, 

+

330 self.page_size, 

+

331 font_family_override=font_family_override 

+

332 ) 

+

333 

+

334 def get_font_family(self) -> Optional[BundledFont]: 

+

335 """Get the current font family override""" 

+

336 return self.font_family 

+

337 

+

338 def get_cache_stats(self) -> Dict[str, Any]: 

+

339 """Get cache statistics""" 

+

340 return self.buffer.get_cache_stats() 

+

341 

+

342 def shutdown(self): 

+

343 """Release cached pages""" 

+

344 self.buffer.shutdown() 

+
+ + + diff --git a/cov_info/htmlcov/z_427cc3035faf7633_table_optimizer_py.html b/cov_info/htmlcov/z_427cc3035faf7633_table_optimizer_py.html new file mode 100644 index 0000000..37e6c75 --- /dev/null +++ b/cov_info/htmlcov/z_427cc3035faf7633_table_optimizer_py.html @@ -0,0 +1,495 @@ + + + + + Coverage for pyWebLayout/layout/table_optimizer.py: 88% + + + + + +
+
+

+ Coverage for pyWebLayout/layout/table_optimizer.py: + 88% +

+ +

+ 151 statements   + + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.15.4, + created at 2026-08-08 20:34 +0000 +

+ +
+
+
+

1""" 

+

2Table column width optimization for pyWebLayout. 

+

3 

+

4This module provides intelligent column width distribution for tables, 

+

5ensuring optimal space usage while respecting content constraints. 

+

6""" 

+

7 

+

8from typing import List, Tuple, Optional, Dict 

+

9from pyWebLayout.abstract.block import Table, TableRow 

+

10 

+

11 

+

12def optimize_table_layout(table: Table, 

+

13 available_width: int, 

+

14 sample_size: int = 5, 

+

15 style=None) -> List[int]: 

+

16 """ 

+

17 Optimize column widths for a table. 

+

18 

+

19 Strategy: 

+

20 1. Check for HTML width overrides (colspan, width attributes) 

+

21 2. Sample first ~5 rows to estimate column requirements (performance) 

+

22 3. Calculate minimum width for each column (longest unbreakable word) 

+

23 4. Calculate preferred width for each column (no wrapping) 

+

24 5. If total preferred fits: use preferred 

+

25 6. Otherwise: distribute available space proportionally 

+

26 7. Ensure no column < min_width 

+

27 

+

28 Note: Hyphenation threshold is controlled by Font.min_hyphenation_width, 

+

29 not passed as a parameter here to avoid duplication. 

+

30 

+

31 Args: 

+

32 table: The table to optimize 

+

33 available_width: Total width available 

+

34 sample_size: Number of rows to sample for measurement (default 5) 

+

35 style: Optional table style for border/padding calculations 

+

36 

+

37 Returns: 

+

38 List of optimized column widths 

+

39 """ 

+

40 from pyWebLayout.concrete.dynamic_page import DynamicPage 

+

41 

+

42 n_cols = get_column_count(table) 

+

43 if n_cols == 0: 

+

44 return [] 

+

45 

+

46 # Account for table borders/padding overhead 

+

47 if style: 

+

48 overhead = calculate_table_overhead(n_cols, style) 

+

49 available_for_content = available_width - overhead 

+

50 else: 

+

51 # Default border overhead 

+

52 border_width = 1 

+

53 overhead = border_width * (n_cols + 1) 

+

54 available_for_content = available_width - overhead 

+

55 

+

56 # Phase 0: Check for HTML width overrides 

+

57 html_widths = extract_html_column_widths(table) 

+

58 fixed_columns = {i: width for i, width in enumerate(html_widths) if width is not None} 

+

59 

+

60 # Phase 1: Sample rows and measure constraints for each column 

+

61 min_widths = [] # Minimum without breaking words (Font handles hyphenation) 

+

62 pref_widths = [] # Preferred (no wrapping) 

+

63 

+

64 # Sample first ~5 rows from each section (header, body, footer) 

+

65 sampled_rows = sample_table_rows(table, sample_size) 

+

66 

+

67 for col_idx in range(n_cols): 

+

68 # Check if this column has HTML width override 

+

69 if col_idx in fixed_columns: 69 ↛ 70line 69 didn't jump to line 70 because the condition on line 69 was never true

+

70 fixed_width = fixed_columns[col_idx] 

+

71 min_widths.append(fixed_width) 

+

72 pref_widths.append(fixed_width) 

+

73 continue 

+

74 

+

75 col_min = 50 # Absolute minimum 

+

76 col_pref = 50 

+

77 

+

78 # Check sampled cells in this column 

+

79 for row in sampled_rows: 

+

80 cells = list(row.cells()) 

+

81 if col_idx >= len(cells): 81 ↛ 82line 81 didn't jump to line 82 because the condition on line 81 was never true

+

82 continue 

+

83 

+

84 cell = cells[col_idx] 

+

85 

+

86 # Create a DynamicPage for this cell with no padding/borders 

+

87 # (we're just measuring content, not rendering a full page) 

+

88 from pyWebLayout.style.page_style import PageStyle 

+

89 measurement_style = PageStyle(padding=(0, 0, 0, 0), border_width=0) 

+

90 cell_page = DynamicPage(style=measurement_style) 

+

91 

+

92 # Add cell content to page 

+

93 layout_cell_content(cell_page, cell) 

+

94 

+

95 # Measure minimum width (Font's min_hyphenation_width controls breaking) 

+

96 # DynamicPage returns pure content width (no padding since we set it to 0) 

+

97 # TableRenderer will add cell padding later 

+

98 cell_min = cell_page.get_min_width() 

+

99 col_min = max(col_min, cell_min) 

+

100 

+

101 # Measure preferred width (no wrapping) 

+

102 cell_pref = cell_page.get_preferred_width() 

+

103 col_pref = max(col_pref, cell_pref) 

+

104 

+

105 min_widths.append(col_min) 

+

106 pref_widths.append(col_pref) 

+

107 

+

108 # Phase 2: Distribute width (respecting fixed columns) 

+

109 return distribute_column_widths( 

+

110 min_widths, 

+

111 pref_widths, 

+

112 available_for_content, 

+

113 fixed_columns 

+

114 ) 

+

115 

+

116 

+

117def layout_cell_content(page, cell): 

+

118 """ 

+

119 Layout cell content onto a DynamicPage. 

+

120 

+

121 This adds all blocks from the cell (paragraphs, images, etc.) 

+

122 as children of the page so they can be measured. 

+

123 

+

124 Args: 

+

125 page: DynamicPage to add content to 

+

126 cell: TableCell containing blocks 

+

127 """ 

+

128 from pyWebLayout.concrete.text import Line, Text 

+

129 from pyWebLayout.style.fonts import Font 

+

130 from pyWebLayout.style import FontWeight, Alignment 

+

131 from pyWebLayout.abstract.block import Paragraph, Heading 

+

132 from PIL import Image as PILImage, ImageDraw 

+

133 

+

134 # Default font for measurement 

+

135 font_size = 12 

+

136 font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" 

+

137 font = Font(font_path=font_path, font_size=font_size) 

+

138 

+

139 # Create a minimal draw context for Text measurement 

+

140 # (Text needs this for width calculation) 

+

141 dummy_img = PILImage.new('RGB', (1, 1)) 

+

142 dummy_draw = ImageDraw.Draw(dummy_img) 

+

143 

+

144 # Get all blocks from the cell 

+

145 for block in cell.blocks(): 

+

146 if isinstance(block, (Paragraph, Heading)): 

+

147 # Get words from the block 

+

148 word_items = block.words() if callable(block.words) else block.words 

+

149 words = list(word_items) 

+

150 

+

151 if not words: 151 ↛ 152line 151 didn't jump to line 152 because the condition on line 151 was never true

+

152 continue 

+

153 

+

154 # Create a line for measurement 

+

155 line = Line( 

+

156 spacing=(3, 6), # word spacing 

+

157 origin=(0, 0), 

+

158 size=(1000, 20), # Large size for measurement 

+

159 draw=dummy_draw, 

+

160 font=font, 

+

161 halign=Alignment.LEFT 

+

162 ) 

+

163 

+

164 # Add all words to estimate width 

+

165 for word_item in words: 

+

166 # Handle word tuples (index, word_obj) 

+

167 if isinstance(word_item, tuple) and len(word_item) >= 2: 167 ↛ 168line 167 didn't jump to line 168 because the condition on line 167 was never true

+

168 word_obj = word_item[1] 

+

169 else: 

+

170 word_obj = word_item 

+

171 

+

172 # Extract text from the word 

+

173 word_text = word_obj.text if hasattr(word_obj, 'text') else str(word_obj) 

+

174 

+

175 # Create Text object for the word 

+

176 # Text constructor: (text, style, draw) 

+

177 text_obj = Text( 

+

178 text=word_text, 

+

179 style=font, # Font is the style 

+

180 draw=dummy_draw 

+

181 ) 

+

182 

+

183 line._text_objects.append(text_obj) 

+

184 

+

185 # Add line to page 

+

186 page.add_child(line) 

+

187 

+

188 

+

189def get_column_count(table: Table) -> int: 

+

190 """ 

+

191 Get the number of columns in a table. 

+

192 

+

193 Args: 

+

194 table: The table to analyze 

+

195 

+

196 Returns: 

+

197 Number of columns 

+

198 """ 

+

199 all_rows = list(table.all_rows()) 

+

200 if not all_rows: 

+

201 return 0 

+

202 

+

203 # Get from first row 

+

204 first_row = all_rows[0][1] 

+

205 return first_row.cell_count 

+

206 

+

207 

+

208def sample_table_rows(table: Table, sample_size: int) -> List[TableRow]: 

+

209 """ 

+

210 Sample first ~sample_size rows from each table section. 

+

211 

+

212 Args: 

+

213 table: The table to sample 

+

214 sample_size: Number of rows to sample per section 

+

215 

+

216 Returns: 

+

217 List of sampled rows 

+

218 """ 

+

219 sampled = [] 

+

220 

+

221 for section in ["header", "body", "footer"]: 

+

222 section_rows = [row for sec, row in table.all_rows() if sec == section] 

+

223 # Take first sample_size rows (or fewer if section is smaller) 

+

224 sampled.extend(section_rows[:sample_size]) 

+

225 

+

226 return sampled 

+

227 

+

228 

+

229def extract_html_column_widths(table: Table) -> List[Optional[int]]: 

+

230 """ 

+

231 Extract column width overrides from HTML attributes. 

+

232 

+

233 Checks for: 

+

234 - <col width="100px"> elements 

+

235 - <td width="100px"> in first row 

+

236 - <th width="100px"> in header 

+

237 

+

238 Args: 

+

239 table: The table to check 

+

240 

+

241 Returns: 

+

242 List of widths (None for auto-layout columns) 

+

243 """ 

+

244 n_cols = get_column_count(table) 

+

245 widths = [None] * n_cols 

+

246 

+

247 # Check for <col> elements with width 

+

248 if hasattr(table, 'col_widths'): 248 ↛ 249line 248 didn't jump to line 249 because the condition on line 248 was never true

+

249 for i, width in enumerate(table.col_widths): 

+

250 if width is not None: 

+

251 widths[i] = parse_html_width(width) 

+

252 

+

253 # Check first row cells for width attributes 

+

254 all_rows = list(table.all_rows()) 

+

255 if all_rows: 255 ↛ 262line 255 didn't jump to line 262 because the condition on line 255 was always true

+

256 first_row = all_rows[0][1] 

+

257 cells = list(first_row.cells()) 

+

258 for i, cell in enumerate(cells): 

+

259 if i < len(widths) and hasattr(cell, 'width') and cell.width is not None: 

+

260 widths[i] = parse_html_width(cell.width) 

+

261 

+

262 return widths 

+

263 

+

264 

+

265def parse_html_width(width_value) -> Optional[int]: 

+

266 """ 

+

267 Parse HTML width value (e.g., "100px", "20%", "100"). 

+

268 

+

269 Args: 

+

270 width_value: HTML width attribute value 

+

271 

+

272 Returns: 

+

273 Width in pixels, or None if percentage/invalid 

+

274 """ 

+

275 if isinstance(width_value, int): 

+

276 return width_value 

+

277 

+

278 if isinstance(width_value, str): 278 ↛ 299line 278 didn't jump to line 299 because the condition on line 278 was always true

+

279 # Remove whitespace 

+

280 width_value = width_value.strip() 

+

281 

+

282 # Percentage widths not supported yet 

+

283 if '%' in width_value: 

+

284 return None 

+

285 

+

286 # Parse pixel values 

+

287 if width_value.endswith('px'): 

+

288 try: 

+

289 return int(width_value[:-2]) 

+

290 except ValueError: 

+

291 return None 

+

292 

+

293 # Plain number 

+

294 try: 

+

295 return int(width_value) 

+

296 except ValueError: 

+

297 return None 

+

298 

+

299 return None 

+

300 

+

301 

+

302def distribute_column_widths(min_widths: List[int], 

+

303 pref_widths: List[int], 

+

304 available_width: int, 

+

305 fixed_columns: Dict[int, int]) -> List[int]: 

+

306 """ 

+

307 Distribute width among columns, respecting fixed column widths. 

+

308 

+

309 Args: 

+

310 min_widths: Minimum width for each column 

+

311 pref_widths: Preferred width for each column 

+

312 available_width: Total width available 

+

313 fixed_columns: Dict mapping column index to fixed width 

+

314 

+

315 Returns: 

+

316 List of final column widths 

+

317 """ 

+

318 n_cols = len(min_widths) 

+

319 if n_cols == 0: 

+

320 return [] 

+

321 

+

322 # Calculate available space for flexible columns 

+

323 fixed_total = sum(fixed_columns.values()) 

+

324 flexible_available = available_width - fixed_total 

+

325 

+

326 # Get indices of flexible columns 

+

327 flexible_cols = [i for i in range(n_cols) if i not in fixed_columns] 

+

328 

+

329 if not flexible_cols: 

+

330 # All columns fixed - return as-is 

+

331 return [fixed_columns.get(i, min_widths[i]) for i in range(n_cols)] 

+

332 

+

333 # Calculate totals for flexible columns only 

+

334 flex_min_total = sum(min_widths[i] for i in flexible_cols) 

+

335 flex_pref_total = sum(pref_widths[i] for i in flexible_cols) 

+

336 

+

337 # Distribute space among flexible columns 

+

338 widths = [0] * n_cols 

+

339 

+

340 # Set fixed columns 

+

341 for i, width in fixed_columns.items(): 

+

342 widths[i] = width 

+

343 

+

344 # Distribute to flexible columns 

+

345 if flex_pref_total <= flexible_available: 

+

346 # Preferred widths fit - distribute remaining space proportionally 

+

347 extra_space = flexible_available - flex_pref_total 

+

348 

+

349 if extra_space > 0 and flex_pref_total > 0: 

+

350 # Distribute extra space proportionally based on preferred widths 

+

351 for i in flexible_cols: 

+

352 proportion = pref_widths[i] / flex_pref_total 

+

353 widths[i] = int(pref_widths[i] + (extra_space * proportion)) 

+

354 else: 

+

355 # No extra space, just use preferred widths 

+

356 for i in flexible_cols: 

+

357 widths[i] = pref_widths[i] 

+

358 elif flex_min_total > flexible_available: 

+

359 # Can't satisfy minimum - force it anyway (graceful degradation) 

+

360 for i in flexible_cols: 

+

361 widths[i] = min_widths[i] 

+

362 else: 

+

363 # Proportional distribution between min and pref 

+

364 extra_space = flexible_available - flex_min_total 

+

365 flex_pref_over_min = flex_pref_total - flex_min_total 

+

366 

+

367 for i in flexible_cols: 

+

368 if flex_pref_over_min > 0: 368 ↛ 374line 368 didn't jump to line 374 because the condition on line 368 was always true

+

369 pref_over_min = pref_widths[i] - min_widths[i] 

+

370 proportion = pref_over_min / flex_pref_over_min 

+

371 extra = extra_space * proportion 

+

372 widths[i] = int(min_widths[i] + extra) 

+

373 else: 

+

374 widths[i] = int(min_widths[i]) 

+

375 

+

376 return widths 

+

377 

+

378 

+

379def calculate_table_overhead(n_cols: int, style) -> int: 

+

380 """ 

+

381 Calculate the pixel overhead for table borders and spacing. 

+

382 

+

383 Args: 

+

384 n_cols: Number of columns 

+

385 style: TableStyle object 

+

386 

+

387 Returns: 

+

388 Total pixel overhead 

+

389 """ 

+

390 # Border on each side of each column + outer borders 

+

391 border_overhead = style.border_width * (n_cols + 1) 

+

392 

+

393 # Cell spacing if any 

+

394 spacing_overhead = style.cell_spacing * (n_cols - 1) if n_cols > 1 else 0 

+

395 

+

396 return border_overhead + spacing_overhead 

+
+ + + diff --git a/cov_info/htmlcov/z_7d48e1f4c6486fa2___init___py.html b/cov_info/htmlcov/z_7d48e1f4c6486fa2___init___py.html new file mode 100644 index 0000000..4d93183 --- /dev/null +++ b/cov_info/htmlcov/z_7d48e1f4c6486fa2___init___py.html @@ -0,0 +1,135 @@ + + + + + Coverage for pyWebLayout/concrete/__init__.py: 100% + + + + + +
+
+

+ Coverage for pyWebLayout/concrete/__init__.py: + 100% +

+ +

+ 7 statements   + + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.15.4, + created at 2026-08-08 20:34 +0000 +

+ +
+
+
+

1""" 

+

2Concrete layer for the pyWebLayout library. 

+

3 

+

4This package contains concrete implementations that can be directly rendered. 

+

5""" 

+

6 

+

7from .text import ( 

+

8 Text, 

+

9 Line, 

+

10 configure_text_caches, 

+

11 clear_text_caches, 

+

12 text_cache_stats, 

+

13 prewarm_text_caches, 

+

14) 

+

15from .box import Box 

+

16from .image import RenderableImage 

+

17from .page import Page 

+

18from pyWebLayout.abstract.block import Table, TableRow as Row, TableCell as Cell 

+

19from .functional import LinkText, ButtonText 

+

20 

+

21__all__ = [ 

+

22 'Text', 

+

23 'Line', 

+

24 'Box', 

+

25 'RenderableImage', 

+

26 'Page', 

+

27 'Table', 

+

28 'Row', 

+

29 'Cell', 

+

30 'LinkText', 

+

31 'ButtonText', 

+

32 'configure_text_caches', 

+

33 'clear_text_caches', 

+

34 'text_cache_stats', 

+

35 'prewarm_text_caches', 

+

36] 

+
+ + + diff --git a/cov_info/htmlcov/z_7d48e1f4c6486fa2_box_py.html b/cov_info/htmlcov/z_7d48e1f4c6486fa2_box_py.html new file mode 100644 index 0000000..40d6192 --- /dev/null +++ b/cov_info/htmlcov/z_7d48e1f4c6486fa2_box_py.html @@ -0,0 +1,139 @@ + + + + + Coverage for pyWebLayout/concrete/box.py: 100% + + + + + +
+
+

+ Coverage for pyWebLayout/concrete/box.py: + 100% +

+ +

+ 19 statements   + + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.15.4, + created at 2026-08-08 20:34 +0000 +

+ +
+
+
+

1from __future__ import annotations 

+

2import numpy as np 

+

3from PIL import Image 

+

4 

+

5from pyWebLayout.core.base import Renderable, Queriable 

+

6from pyWebLayout.core import Geometric 

+

7from pyWebLayout.style import Alignment 

+

8 

+

9 

+

10class Box(Geometric, Renderable, Queriable): 

+

11 """ 

+

12 A box with geometric properties (origin and size). 

+

13 

+

14 Uses Geometric mixin for origin and size management. 

+

15 """ 

+

16 

+

17 def __init__( 

+

18 self, 

+

19 origin, 

+

20 size, 

+

21 callback=None, 

+

22 sheet: Image = None, 

+

23 mode: bool = None, 

+

24 halign=Alignment.CENTER, 

+

25 valign=Alignment.CENTER): 

+

26 super().__init__(origin=origin, size=size) 

+

27 self._end = self._origin + self._size 

+

28 self._callback = callback 

+

29 self._sheet: Image = sheet 

+

30 if self._sheet is None: 

+

31 self._mode = mode 

+

32 else: 

+

33 self._mode = sheet.mode 

+

34 self._halign = halign 

+

35 self._valign = valign 

+

36 

+

37 # origin and size properties are provided by Geometric mixin 

+

38 

+

39 def in_shape(self, point): 

+

40 return np.all((point >= self._origin) & (point < self._end), axis=-1) 

+
+ + + diff --git a/cov_info/htmlcov/z_7d48e1f4c6486fa2_dynamic_page_py.html b/cov_info/htmlcov/z_7d48e1f4c6486fa2_dynamic_page_py.html new file mode 100644 index 0000000..aa51501 --- /dev/null +++ b/cov_info/htmlcov/z_7d48e1f4c6486fa2_dynamic_page_py.html @@ -0,0 +1,517 @@ + + + + + Coverage for pyWebLayout/concrete/dynamic_page.py: 68% + + + + + +
+
+

+ Coverage for pyWebLayout/concrete/dynamic_page.py: + 68% +

+ +

+ 178 statements   + + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.15.4, + created at 2026-08-08 20:34 +0000 +

+ +
+
+
+

1""" 

+

2DynamicPage implementation for pyWebLayout. 

+

3 

+

4A DynamicPage is a page that dynamically sizes itself based on content and constraints. 

+

5Unlike a regular Page with fixed size, a DynamicPage measures its content first and 

+

6then layouts within the allocated space. 

+

7 

+

8Use cases: 

+

9- Table cells that need to fit content 

+

10- Containers that should grow with content 

+

11- Responsive layouts that adapt to constraints 

+

12""" 

+

13 

+

14from typing import Tuple, Optional, List 

+

15from dataclasses import dataclass 

+

16import numpy as np 

+

17from PIL import Image 

+

18 

+

19from pyWebLayout.concrete.page import Page 

+

20from pyWebLayout.style.page_style import PageStyle 

+

21from pyWebLayout.core.base import Renderable 

+

22 

+

23 

+

24@dataclass 

+

25class SizeConstraints: 

+

26 """Size constraints for dynamic layout.""" 

+

27 min_width: Optional[int] = None 

+

28 max_width: Optional[int] = None 

+

29 min_height: Optional[int] = None 

+

30 max_height: Optional[int] = None 

+

31 # Note: Hyphenation threshold is controlled by Font.min_hyphenation_width 

+

32 # Don't duplicate that logic here 

+

33 

+

34 

+

35class DynamicPage(Page): 

+

36 """ 

+

37 A page that dynamically sizes itself based on content and constraints. 

+

38 

+

39 The layout process has two phases: 

+

40 1. Measurement: Calculate intrinsic size needed for content 

+

41 2. Layout: Position content within allocated size 

+

42 

+

43 This allows containers (like tables) to optimize space allocation before rendering. 

+

44 """ 

+

45 

+

46 def __init__(self, 

+

47 constraints: Optional[SizeConstraints] = None, 

+

48 style: Optional[PageStyle] = None): 

+

49 """ 

+

50 Initialize a dynamic page. 

+

51 

+

52 Args: 

+

53 constraints: Optional size constraints (min/max width/height) 

+

54 style: The PageStyle defining borders, spacing, and appearance 

+

55 """ 

+

56 # Start with zero size - will be determined during measurement/layout 

+

57 super().__init__(size=(0, 0), style=style) 

+

58 self._constraints = constraints if constraints is not None else SizeConstraints() 

+

59 

+

60 # Measurement state 

+

61 self._is_measured = False 

+

62 self._intrinsic_size: Optional[Tuple[int, int]] = None 

+

63 self._min_width_cache: Optional[int] = None 

+

64 self._preferred_width_cache: Optional[int] = None 

+

65 self._content_height_cache: Optional[int] = None 

+

66 

+

67 # Pagination state 

+

68 self._render_offset = 0 # For partial rendering (pagination) 

+

69 self._is_laid_out = False 

+

70 

+

71 @property 

+

72 def constraints(self) -> SizeConstraints: 

+

73 """Get the size constraints for this page.""" 

+

74 return self._constraints 

+

75 

+

76 def measure(self, available_width: Optional[int] = None) -> Tuple[int, int]: 

+

77 """ 

+

78 Measure the intrinsic size needed for content. 

+

79 

+

80 This walks through all children and calculates how much space they need. 

+

81 The measurement respects constraints (min/max width/height). 

+

82 

+

83 Args: 

+

84 available_width: Optional width constraint for wrapping content 

+

85 

+

86 Returns: 

+

87 Tuple of (width, height) needed 

+

88 """ 

+

89 if self._is_measured and self._intrinsic_size is not None: 

+

90 return self._intrinsic_size 

+

91 

+

92 # Apply constraints to available width 

+

93 if available_width is not None: 

+

94 if self._constraints.max_width is not None: 94 ↛ 95line 94 didn't jump to line 95 because the condition on line 94 was never true

+

95 available_width = min(available_width, self._constraints.max_width) 

+

96 if self._constraints.min_width is not None: 

+

97 available_width = max(available_width, self._constraints.min_width) 

+

98 

+

99 # Measure content 

+

100 # For now, walk through children and sum their sizes 

+

101 total_width = 0 

+

102 total_height = 0 

+

103 

+

104 for child in self._children: 104 ↛ 105line 104 didn't jump to line 105 because the loop on line 104 never started

+

105 if hasattr(child, 'measure'): 

+

106 # Child is also dynamic - ask it to measure 

+

107 child_size = child.measure(available_width) 

+

108 child_width, child_height = child_size 

+

109 else: 

+

110 # Child has fixed size 

+

111 child_width = child.size[0] if hasattr(child, 'size') else 0 

+

112 child_height = child.size[1] if hasattr(child, 'size') else 0 

+

113 

+

114 total_width = max(total_width, child_width) 

+

115 total_height += child_height 

+

116 

+

117 # Add page padding/borders 

+

118 total_width += self._style.total_horizontal_padding + self._style.total_border_width 

+

119 total_height += self._style.total_vertical_padding + self._style.total_border_width 

+

120 

+

121 # Apply constraints 

+

122 if self._constraints.min_width is not None: 

+

123 total_width = max(total_width, self._constraints.min_width) 

+

124 if self._constraints.max_width is not None: 124 ↛ 125line 124 didn't jump to line 125 because the condition on line 124 was never true

+

125 total_width = min(total_width, self._constraints.max_width) 

+

126 if self._constraints.min_height is not None: 

+

127 total_height = max(total_height, self._constraints.min_height) 

+

128 if self._constraints.max_height is not None: 128 ↛ 129line 128 didn't jump to line 129 because the condition on line 128 was never true

+

129 total_height = min(total_height, self._constraints.max_height) 

+

130 

+

131 self._intrinsic_size = (total_width, total_height) 

+

132 self._is_measured = True 

+

133 

+

134 return self._intrinsic_size 

+

135 

+

136 def get_min_width(self) -> int: 

+

137 """ 

+

138 Get minimum width needed to render content. 

+

139 

+

140 This finds the widest word/element that cannot be broken, 

+

141 using Font.min_hyphenation_width for hyphenation control. 

+

142 

+

143 Returns: 

+

144 Minimum width in pixels 

+

145 """ 

+

146 # Check cache 

+

147 if self._min_width_cache is not None: 147 ↛ 148line 147 didn't jump to line 148 because the condition on line 147 was never true

+

148 return self._min_width_cache 

+

149 

+

150 # Calculate minimum width based on content 

+

151 from pyWebLayout.concrete.text import Line, Text 

+

152 

+

153 min_width = 0 

+

154 

+

155 # Walk through children and find longest unbreakable segment 

+

156 for child in self._children: 

+

157 if isinstance(child, Line): 157 ↛ 170line 157 didn't jump to line 170 because the condition on line 157 was always true

+

158 # Check all words in the line 

+

159 # Font's min_hyphenation_width already controls breaking 

+

160 for text_obj in getattr(child, '_text_objects', []): 

+

161 if isinstance(text_obj, Text) and hasattr(text_obj, '_text'): 161 ↛ 160line 161 didn't jump to line 160 because the condition on line 161 was always true

+

162 word_text = text_obj._text 

+

163 # Text stores font in _style, not _font 

+

164 font = getattr(text_obj, '_style', None) 

+

165 

+

166 if font: 166 ↛ 160line 166 didn't jump to line 160 because the condition on line 166 was always true

+

167 # Just measure the word - Font handles hyphenation rules 

+

168 word_width = int(font.font.getlength(word_text)) 

+

169 min_width = max(min_width, word_width) 

+

170 elif hasattr(child, 'get_min_width'): 

+

171 # Child supports min width calculation 

+

172 child_min = child.get_min_width() 

+

173 min_width = max(min_width, child_min) 

+

174 elif hasattr(child, 'size'): 

+

175 # Use actual width 

+

176 min_width = max(min_width, child.size[0]) 

+

177 

+

178 # Add padding/borders 

+

179 min_width += self._style.total_horizontal_padding + self._style.total_border_width 

+

180 

+

181 # Apply minimum constraint 

+

182 if self._constraints.min_width is not None: 182 ↛ 183line 182 didn't jump to line 183 because the condition on line 182 was never true

+

183 min_width = max(min_width, self._constraints.min_width) 

+

184 

+

185 self._min_width_cache = min_width 

+

186 return min_width 

+

187 

+

188 def get_preferred_width(self) -> int: 

+

189 """ 

+

190 Get preferred width (no wrapping). 

+

191 

+

192 This returns the width needed to render all content without any 

+

193 line wrapping. 

+

194 

+

195 Returns: 

+

196 Preferred width in pixels 

+

197 """ 

+

198 # Check cache 

+

199 if self._preferred_width_cache is not None: 199 ↛ 200line 199 didn't jump to line 200 because the condition on line 199 was never true

+

200 return self._preferred_width_cache 

+

201 

+

202 # Calculate preferred width (no wrapping) 

+

203 from pyWebLayout.concrete.text import Line 

+

204 

+

205 pref_width = 0 

+

206 

+

207 for child in self._children: 

+

208 if isinstance(child, Line): 208 ↛ 227line 208 didn't jump to line 227 because the condition on line 208 was always true

+

209 # Get line width without wrapping (including spacing between words) 

+

210 text_objects = getattr(child, '_text_objects', []) 

+

211 if text_objects: 211 ↛ 207line 211 didn't jump to line 207 because the condition on line 211 was always true

+

212 line_width = 0 

+

213 for i, text_obj in enumerate(text_objects): 

+

214 if hasattr(text_obj, '_text') and hasattr(text_obj, '_style'): 214 ↛ 213line 214 didn't jump to line 213 because the condition on line 214 was always true

+

215 # Text stores font in _style, not _font 

+

216 word_width = text_obj._style.font.getlength(text_obj._text) 

+

217 line_width += word_width 

+

218 

+

219 # Add spacing after word (except last word) 

+

220 if i < len(text_objects) - 1: 

+

221 # Get spacing from Line if available, otherwise use default 

+

222 spacing = getattr(child, '_spacing', (3, 6)) 

+

223 # Use minimum spacing for preferred width calculation 

+

224 line_width += spacing[0] if isinstance(spacing, tuple) else 3 

+

225 

+

226 pref_width = max(pref_width, line_width) 

+

227 elif hasattr(child, 'get_preferred_width'): 

+

228 child_pref = child.get_preferred_width() 

+

229 pref_width = max(pref_width, child_pref) 

+

230 elif hasattr(child, 'size'): 

+

231 # Use actual size 

+

232 pref_width = max(pref_width, child.size[0]) 

+

233 

+

234 # Add padding/borders 

+

235 pref_width += self._style.total_horizontal_padding + self._style.total_border_width 

+

236 

+

237 # Apply constraints 

+

238 if self._constraints.max_width is not None: 238 ↛ 239line 238 didn't jump to line 239 because the condition on line 238 was never true

+

239 pref_width = min(pref_width, self._constraints.max_width) 

+

240 if self._constraints.min_width is not None: 240 ↛ 241line 240 didn't jump to line 241 because the condition on line 240 was never true

+

241 pref_width = max(pref_width, self._constraints.min_width) 

+

242 

+

243 self._preferred_width_cache = pref_width 

+

244 return pref_width 

+

245 

+

246 def measure_content_height(self) -> int: 

+

247 """ 

+

248 Measure total height needed to render all content. 

+

249 

+

250 This is used for pagination to know how much content remains. 

+

251 

+

252 Returns: 

+

253 Total height in pixels 

+

254 """ 

+

255 # Check cache 

+

256 if self._content_height_cache is not None: 

+

257 return self._content_height_cache 

+

258 

+

259 total_height = 0 

+

260 

+

261 for child in self._children: 261 ↛ 262line 261 didn't jump to line 262 because the loop on line 261 never started

+

262 if hasattr(child, 'measure_content_height'): 

+

263 child_height = child.measure_content_height() 

+

264 elif hasattr(child, 'size'): 

+

265 child_height = child.size[1] 

+

266 else: 

+

267 child_height = 0 

+

268 

+

269 total_height += child_height 

+

270 

+

271 # Add padding/borders 

+

272 total_height += self._style.total_vertical_padding + self._style.total_border_width 

+

273 

+

274 self._content_height_cache = total_height 

+

275 return total_height 

+

276 

+

277 def layout(self, size: Tuple[int, int]): 

+

278 """ 

+

279 Layout content within the given size. 

+

280 

+

281 This is called after measurement to position children within 

+

282 the allocated space. 

+

283 

+

284 Args: 

+

285 size: The final size allocated to this page (width, height) 

+

286 """ 

+

287 # Set the page size 

+

288 self._size = size 

+

289 

+

290 # Position children sequentially 

+

291 # Use the same logic as Page but now we know our final size 

+

292 content_x = self._style.border_width + self._style.padding_left 

+

293 content_y = self._style.border_width + self._style.padding_top 

+

294 

+

295 self._current_y_offset = content_y 

+

296 self._is_first_line = True 

+

297 

+

298 # Children position themselves, we just track y_offset 

+

299 # The actual positioning happens when children render 

+

300 

+

301 self._is_laid_out = True 

+

302 self._dirty = True # Mark for re-render 

+

303 

+

304 def render(self) -> Image.Image: 

+

305 """ 

+

306 Render the page with all its children. 

+

307 

+

308 If not yet measured/laid out, use intrinsic sizing. 

+

309 

+

310 Returns: 

+

311 PIL Image containing the rendered page 

+

312 """ 

+

313 # Ensure we have a valid size 

+

314 if self._size[0] == 0 or self._size[1] == 0: 

+

315 if not self._is_measured: 315 ↛ 319line 315 didn't jump to line 319 because the condition on line 315 was always true

+

316 # Auto-measure with no constraints 

+

317 self.measure() 

+

318 

+

319 if self._intrinsic_size: 319 ↛ 323line 319 didn't jump to line 323 because the condition on line 319 was always true

+

320 self._size = self._intrinsic_size 

+

321 else: 

+

322 # Fallback to minimum size 

+

323 self._size = (100, 100) 

+

324 

+

325 # Use parent's render implementation 

+

326 return super().render() 

+

327 

+

328 # Pagination Support 

+

329 # ------------------ 

+

330 

+

331 def render_partial(self, available_height: int) -> int: 

+

332 """ 

+

333 Render as much content as fits in available_height. 

+

334 

+

335 This is used for pagination when a page needs to be split across 

+

336 multiple output pages. 

+

337 

+

338 Args: 

+

339 available_height: Height available on current page 

+

340 

+

341 Returns: 

+

342 Amount of content rendered (in pixels) 

+

343 """ 

+

344 # Calculate how many children fit in available height 

+

345 rendered_height = 0 

+

346 content_start_y = self._style.border_width + self._style.padding_top 

+

347 

+

348 for i, child in enumerate(self._children): 348 ↛ 350line 348 didn't jump to line 350 because the loop on line 348 never started

+

349 # Skip already rendered children 

+

350 if rendered_height < self._render_offset: 

+

351 if hasattr(child, 'size'): 

+

352 rendered_height += child.size[1] 

+

353 continue 

+

354 

+

355 # Check if this child fits 

+

356 child_height = child.size[1] if hasattr(child, 'size') else 0 

+

357 

+

358 if rendered_height + child_height <= available_height: 

+

359 # Child fits - render it 

+

360 if hasattr(child, 'render'): 

+

361 child.render() 

+

362 rendered_height += child_height 

+

363 else: 

+

364 # No more space 

+

365 break 

+

366 

+

367 # Update render offset for next call 

+

368 self._render_offset = rendered_height 

+

369 

+

370 return rendered_height 

+

371 

+

372 def has_more_content(self) -> bool: 

+

373 """ 

+

374 Check if there's unrendered content remaining. 

+

375 

+

376 Returns: 

+

377 True if more content needs to be rendered 

+

378 """ 

+

379 total_height = self.measure_content_height() 

+

380 return self._render_offset < total_height 

+

381 

+

382 def reset_pagination(self): 

+

383 """Reset pagination to render from beginning.""" 

+

384 self._render_offset = 0 

+

385 

+

386 def invalidate_caches(self): 

+

387 """Invalidate all measurement caches (call when children change).""" 

+

388 self._is_measured = False 

+

389 self._intrinsic_size = None 

+

390 self._min_width_cache = None 

+

391 self._preferred_width_cache = None 

+

392 self._content_height_cache = None 

+

393 self._is_laid_out = False 

+

394 

+

395 def add_child(self, child: Renderable) -> 'DynamicPage': 

+

396 """ 

+

397 Add a child and invalidate caches. 

+

398 

+

399 Args: 

+

400 child: The renderable object to add 

+

401 

+

402 Returns: 

+

403 Self for method chaining 

+

404 """ 

+

405 super().add_child(child) 

+

406 self.invalidate_caches() 

+

407 return self 

+

408 

+

409 def clear_children(self) -> 'DynamicPage': 

+

410 """ 

+

411 Remove all children and invalidate caches. 

+

412 

+

413 Returns: 

+

414 Self for method chaining 

+

415 """ 

+

416 super().clear_children() 

+

417 self.invalidate_caches() 

+

418 return self 

+
+ + + diff --git a/cov_info/htmlcov/z_7d48e1f4c6486fa2_functional_py.html b/cov_info/htmlcov/z_7d48e1f4c6486fa2_functional_py.html new file mode 100644 index 0000000..6b259f3 --- /dev/null +++ b/cov_info/htmlcov/z_7d48e1f4c6486fa2_functional_py.html @@ -0,0 +1,623 @@ + + + + + Coverage for pyWebLayout/concrete/functional.py: 89% + + + + + +
+
+

+ Coverage for pyWebLayout/concrete/functional.py: + 89% +

+ +

+ 190 statements   + + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.15.4, + created at 2026-08-08 20:34 +0000 +

+ +
+
+
+

1from __future__ import annotations 

+

2from typing import Optional, Tuple 

+

3import numpy as np 

+

4from PIL import ImageDraw 

+

5 

+

6from pyWebLayout.core.base import Interactable, Queriable 

+

7from pyWebLayout.abstract.functional import Link, Button, FormField, LinkType, FormFieldType 

+

8from pyWebLayout.style import Font, TextDecoration 

+

9from .text import Text 

+

10 

+

11 

+

12class LinkText(Text, Interactable, Queriable): 

+

13 """ 

+

14 A Text subclass that can handle Link interactions. 

+

15 Combines text rendering with clickable link functionality. 

+

16 """ 

+

17 

+

18 def __init__(self, link: Link, text: str, font: Font, draw: ImageDraw.Draw, 

+

19 source=None, line=None, page=None): 

+

20 """ 

+

21 Initialize a linkable text object. 

+

22 

+

23 Args: 

+

24 link: The abstract Link object to handle interactions 

+

25 text: The text content to render 

+

26 font: The base font style 

+

27 draw: The drawing context 

+

28 source: Optional source object 

+

29 line: Optional line container 

+

30 page: Optional parent page (for dirty flag management) 

+

31 """ 

+

32 # Create link-styled font (underlined and colored based on link type) 

+

33 link_font = font.with_decoration(TextDecoration.UNDERLINE) 

+

34 if link.link_type == LinkType.INTERNAL: 

+

35 link_font = link_font.with_colour((0, 0, 200)) # Blue for internal links 

+

36 elif link.link_type == LinkType.EXTERNAL: 

+

37 link_font = link_font.with_colour( 

+

38 (0, 0, 180)) # Darker blue for external links 

+

39 elif link.link_type == LinkType.API: 

+

40 link_font = link_font.with_colour((150, 0, 0)) # Red for API links 

+

41 elif link.link_type == LinkType.FUNCTION: 41 ↛ 45line 41 didn't jump to line 45 because the condition on line 41 was always true

+

42 link_font = link_font.with_colour((0, 120, 0)) # Green for function links 

+

43 

+

44 # Initialize Text with the styled font 

+

45 Text.__init__(self, text, link_font, draw, source, line) 

+

46 

+

47 # Initialize Interactable with the link's execute method 

+

48 Interactable.__init__(self, link.execute) 

+

49 

+

50 # Store the link object and page reference 

+

51 self._link = link 

+

52 self._page = page 

+

53 self._hovered = False 

+

54 self._pressed = False 

+

55 

+

56 # Ensure _origin is initialized as numpy array 

+

57 if not hasattr(self, '_origin') or self._origin is None: 57 ↛ 58line 57 didn't jump to line 58 because the condition on line 57 was never true

+

58 self._origin = np.array([0, 0]) 

+

59 

+

60 @property 

+

61 def link(self) -> Link: 

+

62 """Get the associated Link object""" 

+

63 return self._link 

+

64 

+

65 def set_hovered(self, hovered: bool): 

+

66 """Set the hover state for visual feedback""" 

+

67 self._hovered = hovered 

+

68 self._mark_page_dirty() 

+

69 

+

70 def set_pressed(self, pressed: bool): 

+

71 """Set the pressed state for visual feedback""" 

+

72 self._pressed = pressed 

+

73 self._mark_page_dirty() 

+

74 

+

75 def _mark_page_dirty(self): 

+

76 """Mark the parent page as dirty if available""" 

+

77 if self._page and hasattr(self._page, 'mark_dirty'): 77 ↛ 78line 77 didn't jump to line 78 because the condition on line 77 was never true

+

78 self._page.mark_dirty() 

+

79 

+

80 def render(self, next_text: Optional['Text'] = None, spacing: int = 0): 

+

81 """ 

+

82 Render the link text with optional hover and pressed effects. 

+

83 

+

84 Args: 

+

85 next_text: The next Text object in the line (if any) 

+

86 spacing: The spacing to the next text object 

+

87 """ 

+

88 # Handle mock objects in tests 

+

89 size = self.size 

+

90 if hasattr(size, '__call__'): # It's a Mock 90 ↛ 92line 90 didn't jump to line 92 because the condition on line 90 was never true

+

91 # Use default size for tests 

+

92 size = np.array([100, 20]) 

+

93 else: 

+

94 size = np.array(size) 

+

95 

+

96 # Ensure origin is a numpy array 

+

97 origin = np.array( 

+

98 self._origin) if not isinstance( 

+

99 self._origin, 

+

100 np.ndarray) else self._origin 

+

101 

+

102 # Draw background based on state (before text is rendered). 

+

103 # PIL wants a flat sequence of four scalars; handing it a list of two 

+

104 # numpy arrays raises "coordinate list must contain exactly 2 

+

105 # coordinates". 

+

106 if self._pressed or self._hovered: 

+

107 far = origin + size 

+

108 box = (int(origin[0]), int(origin[1]), int(far[0]), int(far[1])) 

+

109 if self._pressed: 

+

110 # Pressed state - stronger, darker highlight 

+

111 bg_color = (180, 180, 255, 180) 

+

112 else: 

+

113 # Hover state - subtle highlight 

+

114 bg_color = (220, 220, 255, 100) 

+

115 self._draw.rectangle(box, fill=bg_color) 

+

116 

+

117 # Call the parent Text render method with parameters 

+

118 super().render(next_text, spacing) 

+

119 

+

120 

+

121class ButtonText(Text, Interactable, Queriable): 

+

122 """ 

+

123 A Text subclass that can handle Button interactions. 

+

124 Renders text as a clickable button with visual states. 

+

125 """ 

+

126 

+

127 def __init__(self, button: Button, font: Font, draw: ImageDraw.Draw, 

+

128 padding: Tuple[int, int, int, int] = (4, 8, 4, 8), 

+

129 source=None, line=None, page=None): 

+

130 """ 

+

131 Initialize a button text object. 

+

132 

+

133 Args: 

+

134 button: The abstract Button object to handle interactions 

+

135 font: The base font style 

+

136 draw: The drawing context 

+

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

+

138 source: Optional source object 

+

139 line: Optional line container 

+

140 page: Optional parent page (for dirty flag management) 

+

141 """ 

+

142 # Initialize Text with the button label 

+

143 Text.__init__(self, button.label, font, draw, source, line) 

+

144 

+

145 # Initialize Interactable with the button's execute method 

+

146 Interactable.__init__(self, button.execute) 

+

147 

+

148 # Store button properties 

+

149 self._button = button 

+

150 self._padding = padding 

+

151 self._page = page 

+

152 self._pressed = False 

+

153 self._hovered = False 

+

154 

+

155 # Recalculate dimensions to include padding 

+

156 # Use getattr to handle mock objects in tests 

+

157 text_width = getattr( 

+

158 self, '_width', 0) if not hasattr( 

+

159 self._width, '__call__') else 0 

+

160 self._padded_width = text_width + padding[1] + padding[3] 

+

161 

+

162 # Size the button from the text's visual height (ascent + descent), not 

+

163 # from the nominal font size. The two differ by several pixels - DejaVu at 

+

164 # 14px measures 17 - so sizing by font_size leaves the button too short to 

+

165 # centre its own label in. 

+

166 self._text_height = self._visual_text_height() 

+

167 self._padded_height = self._text_height + padding[0] + padding[2] 

+

168 

+

169 def _visual_text_height(self) -> int: 

+

170 """Height of the rendered text, ascender to descender.""" 

+

171 try: 

+

172 ascent, descent = self._style.font.getmetrics() 

+

173 return int(ascent + descent) 

+

174 except (AttributeError, TypeError, ValueError): 

+

175 # Mock or unusual font object; the nominal size is the best guess. 

+

176 return int(getattr(self._style, 'font_size', 0) or 0) 

+

177 

+

178 @property 

+

179 def button(self) -> Button: 

+

180 """Get the associated Button object""" 

+

181 return self._button 

+

182 

+

183 @property 

+

184 def size(self) -> np.ndarray: 

+

185 """Get the padded size of the button""" 

+

186 return np.array([self._padded_width, self._padded_height]) 

+

187 

+

188 def set_pressed(self, pressed: bool): 

+

189 """Set the pressed state""" 

+

190 self._pressed = pressed 

+

191 self._mark_page_dirty() 

+

192 

+

193 def set_hovered(self, hovered: bool): 

+

194 """Set the hover state""" 

+

195 self._hovered = hovered 

+

196 self._mark_page_dirty() 

+

197 

+

198 def set_page(self, page): 

+

199 """ 

+

200 Set the parent page reference for dirty flag management. 

+

201 

+

202 Args: 

+

203 page: The Page object containing this element 

+

204 """ 

+

205 self._page = page 

+

206 

+

207 def _mark_page_dirty(self): 

+

208 """Mark the parent page as dirty if available""" 

+

209 if self._page and hasattr(self._page, 'mark_dirty'): 209 ↛ 210line 209 didn't jump to line 210 because the condition on line 209 was never true

+

210 self._page.mark_dirty() 

+

211 

+

212 def render(self): 

+

213 """ 

+

214 Render the button with background, border, and text. 

+

215 """ 

+

216 # Determine button colors based on state 

+

217 if not self._button.enabled: 

+

218 # Disabled button 

+

219 bg_color = (200, 200, 200) 

+

220 border_color = (150, 150, 150) 

+

221 text_color = (100, 100, 100) 

+

222 elif self._pressed: 222 ↛ 224line 222 didn't jump to line 224 because the condition on line 222 was never true

+

223 # Pressed button 

+

224 bg_color = (70, 130, 180) 

+

225 border_color = (50, 100, 150) 

+

226 text_color = (255, 255, 255) 

+

227 elif self._hovered: 227 ↛ 229line 227 didn't jump to line 229 because the condition on line 227 was never true

+

228 # Hovered button 

+

229 bg_color = (100, 160, 220) 

+

230 border_color = (70, 130, 180) 

+

231 text_color = (255, 255, 255) 

+

232 else: 

+

233 # Normal button 

+

234 bg_color = (100, 150, 200) 

+

235 border_color = (70, 120, 170) 

+

236 text_color = (255, 255, 255) 

+

237 

+

238 # Draw button background with rounded corners 

+

239 # rounded_rectangle expects [x0, y0, x1, y1] format 

+

240 button_rect = [ 

+

241 int(self._origin[0]), 

+

242 int(self._origin[1]), 

+

243 int(self._origin[0] + self.size[0]), 

+

244 int(self._origin[1] + self.size[1]) 

+

245 ] 

+

246 self._draw.rounded_rectangle(button_rect, fill=bg_color, 

+

247 outline=border_color, width=1, radius=4) 

+

248 

+

249 # Update text color and render text centered within padding 

+

250 self._style = self._style.with_colour(text_color) 

+

251 text_x = self._origin[0] + self._padding[3] # left padding 

+

252 

+

253 # Center text vertically within button 

+

254 # Get font metrics to properly center the baseline 

+

255 ascent, descent = self._style.font.getmetrics() 

+

256 

+

257 # Total button height minus top and bottom padding gives us text area height 

+

258 text_area_height = self._padded_height - self._padding[0] - self._padding[2] 

+

259 

+

260 # Centre the text's visual height (ascent + descent) within the text area. 

+

261 # text_y is the baseline, since Text renders with anchor "ls". 

+

262 # 

+

263 # top of glyphs = area_top + (area_height - (ascent + descent)) / 2 

+

264 # baseline = top of glyphs + ascent 

+

265 # 

+

266 # The previous form, area_top + area_height/2 + descent/2, is only 

+

267 # equivalent when ascent == 2 * descent. Real fonts sit nearer 4:1, so the 

+

268 # label rendered several pixels above centre, against the top edge. 

+

269 text_top = self._origin[1] + self._padding[0] \ 

+

270 + (text_area_height - (ascent + descent)) / 2 

+

271 text_y = text_top + ascent 

+

272 

+

273 # Temporarily set origin for text rendering 

+

274 original_origin = self._origin.copy() 

+

275 self._origin = np.array([text_x, text_y]) 

+

276 

+

277 # Call parent render method for the text 

+

278 super().render() 

+

279 

+

280 # Restore original origin 

+

281 self._origin = original_origin 

+

282 

+

283 def in_object(self, point) -> bool: 

+

284 """ 

+

285 Check if a point is within this button. 

+

286 

+

287 Args: 

+

288 point: The coordinates to check 

+

289 

+

290 Returns: 

+

291 True if the point is within the button bounds (including padding) 

+

292 """ 

+

293 point_array = np.array(point) 

+

294 relative_point = point_array - self._origin 

+

295 

+

296 # Check if the point is within the padded button boundaries 

+

297 return (0 <= relative_point[0] < self._padded_width and 

+

298 0 <= relative_point[1] < self._padded_height) 

+

299 

+

300 

+

301class FormFieldText(Text, Interactable, Queriable): 

+

302 """ 

+

303 A Text subclass that can handle FormField interactions. 

+

304 Renders form field labels and input areas. 

+

305 

+

306 The origin is the top-left of the whole control: label, then a gap, then the 

+

307 input box. Text itself draws from a baseline, so the label is offset down by 

+

308 its ascent when rendering; without that the glyphs would sit above the origin 

+

309 and overprint whatever is above, which for a stacked form is the previous 

+

310 field's input box. 

+

311 """ 

+

312 

+

313 # Vertical gap between the label and its input box, in pixels. 

+

314 LABEL_GAP = 5 

+

315 

+

316 def __init__(self, field: FormField, font: Font, draw: ImageDraw.Draw, 

+

317 field_height: int = 24, source=None, line=None): 

+

318 """ 

+

319 Initialize a form field text object. 

+

320 

+

321 Args: 

+

322 field: The abstract FormField object to handle interactions 

+

323 font: The base font style for the label 

+

324 draw: The drawing context 

+

325 field_height: Height of the input field area 

+

326 source: Optional source object 

+

327 line: Optional line container 

+

328 """ 

+

329 # Initialize Text with the field label 

+

330 Text.__init__(self, field.label, font, draw, source, line) 

+

331 

+

332 # Initialize Interactable - form fields don't have direct callbacks 

+

333 # but can notify of focus/value changes 

+

334 Interactable.__init__(self, None) 

+

335 

+

336 # Store field properties 

+

337 self._field = field 

+

338 self._field_height = field_height 

+

339 self._focused = False 

+

340 

+

341 # Calculate total height (label + gap + field). The label's height is its 

+

342 # ink height, ascender to descender, not the nominal font size - the two 

+

343 # differ by several pixels and the gap between label and box is only 5. 

+

344 self._label_height = self._visual_label_height() 

+

345 self._total_height = self._label_height + self.LABEL_GAP + field_height 

+

346 

+

347 # Field width should be at least as wide as the label 

+

348 # Use getattr to handle mock objects in tests 

+

349 text_width = getattr( 

+

350 self, '_width', 0) if not hasattr( 

+

351 self._width, '__call__') else 0 

+

352 self._field_width = max(text_width, 150) 

+

353 

+

354 def _visual_label_height(self) -> int: 

+

355 """Height of the rendered label, ascender to descender.""" 

+

356 try: 

+

357 ascent, descent = self._style.font.getmetrics() 

+

358 return int(ascent + descent) 

+

359 except (AttributeError, TypeError, ValueError): 

+

360 # Mock or unusual font object; the nominal size is the best guess. 

+

361 return int(getattr(self._style, 'font_size', 0) or 0) 

+

362 

+

363 @property 

+

364 def field_area_offset(self) -> int: 

+

365 """Distance from this control's origin to the top of its input box.""" 

+

366 return self._label_height + self.LABEL_GAP 

+

367 

+

368 @property 

+

369 def field(self) -> FormField: 

+

370 """Get the associated FormField object""" 

+

371 return self._field 

+

372 

+

373 @property 

+

374 def size(self) -> np.ndarray: 

+

375 """Get the total size including label and field""" 

+

376 return np.array([self._field_width, self._total_height]) 

+

377 

+

378 def set_focused(self, focused: bool): 

+

379 """Set the focus state""" 

+

380 self._focused = focused 

+

381 

+

382 def render(self): 

+

383 """ 

+

384 Render the form field with label and input area. 

+

385 """ 

+

386 # Render the label. Text draws from the baseline, so shift down by the 

+

387 # ascent to make the origin the top of the label rather than its baseline. 

+

388 try: 

+

389 label_ascent = self._style.font.getmetrics()[0] 

+

390 except (AttributeError, TypeError, ValueError): 

+

391 label_ascent = self._label_height 

+

392 

+

393 label_origin = self._origin 

+

394 self._origin = np.array([label_origin[0], label_origin[1] + label_ascent]) 

+

395 super().render() 

+

396 self._origin = label_origin 

+

397 

+

398 # Calculate field position (below the label, with the standard gap) 

+

399 field_x = self._origin[0] 

+

400 field_y = self._origin[1] + self.field_area_offset 

+

401 

+

402 # Draw field background and border 

+

403 bg_color = (255, 255, 255) 

+

404 border_color = (100, 150, 200) if self._focused else (200, 200, 200) 

+

405 

+

406 field_rect = [(field_x, field_y), 

+

407 (field_x + self._field_width, field_y + self._field_height)] 

+

408 self._draw.rectangle(field_rect, fill=bg_color, outline=border_color, width=1) 

+

409 

+

410 # Render field value if present 

+

411 if self._field.value is not None: 

+

412 value_text = str(self._field.value) 

+

413 

+

414 # For password fields, mask the text 

+

415 if self._field.field_type == FormFieldType.PASSWORD: 

+

416 value_text = "•" * len(value_text) 

+

417 

+

418 # Create a temporary Text object for the value 

+

419 value_font = self._style.with_colour((0, 0, 0)) 

+

420 

+

421 # Position value text within field (with some padding) 

+

422 # Get font metrics to properly center the baseline 

+

423 ascent, descent = value_font.font.getmetrics() 

+

424 

+

425 # Centre the value within the input box. As in ButtonText, the 

+

426 # baseline sits at the top of the glyphs plus the ascent; centring on 

+

427 # half the box height plus half the descent only works for a 2:1 

+

428 # ascent/descent ratio and otherwise rides high. 

+

429 value_x = field_x + 5 

+

430 value_y = field_y + (self._field_height - (ascent + descent)) / 2 + ascent 

+

431 

+

432 # Draw the value text 

+

433 self._draw.text((value_x, value_y), value_text, 

+

434 font=value_font.font, fill=value_font.colour, anchor="ls") 

+

435 

+

436 def handle_click(self, point) -> bool: 

+

437 """ 

+

438 Handle clicks on the form field. 

+

439 

+

440 Args: 

+

441 point: The click coordinates relative to this field 

+

442 

+

443 Returns: 

+

444 True if the field was clicked and focused 

+

445 """ 

+

446 # Calculate field area 

+

447 field_y = self.field_area_offset 

+

448 

+

449 # Check if click is within the input field area (not just the label) 

+

450 if (0 <= point[0] <= self._field_width and 

+

451 field_y <= point[1] <= field_y + self._field_height): 

+

452 self.set_focused(True) 

+

453 return True 

+

454 

+

455 return False 

+

456 

+

457 def in_object(self, point) -> bool: 

+

458 """ 

+

459 Check if a point is within this form field (including label and input area). 

+

460 

+

461 Args: 

+

462 point: The coordinates to check 

+

463 

+

464 Returns: 

+

465 True if the point is within the field bounds 

+

466 """ 

+

467 point_array = np.array(point) 

+

468 relative_point = point_array - self._origin 

+

469 

+

470 # Check if the point is within the total field area 

+

471 return (0 <= relative_point[0] < self._field_width and 

+

472 0 <= relative_point[1] < self._total_height) 

+

473 

+

474 

+

475# Factory functions for creating functional text objects 

+

476def create_link_text(link: Link, text: str, font: Font, 

+

477 draw: ImageDraw.Draw) -> LinkText: 

+

478 """ 

+

479 Factory function to create a LinkText object. 

+

480 

+

481 Args: 

+

482 link: The Link object to associate with the text 

+

483 text: The text content to display 

+

484 font: The base font style 

+

485 draw: The drawing context 

+

486 

+

487 Returns: 

+

488 A LinkText object ready for rendering and interaction 

+

489 """ 

+

490 return LinkText(link, text, font, draw) 

+

491 

+

492 

+

493def create_button_text(button: Button, font: Font, draw: ImageDraw.Draw, 

+

494 padding: Tuple[int, int, int, int] = (4, 8, 4, 8)) -> ButtonText: 

+

495 """ 

+

496 Factory function to create a ButtonText object. 

+

497 

+

498 Args: 

+

499 button: The Button object to associate with the text 

+

500 font: The base font style 

+

501 draw: The drawing context 

+

502 padding: Padding around the button text 

+

503 

+

504 Returns: 

+

505 A ButtonText object ready for rendering and interaction 

+

506 """ 

+

507 return ButtonText(button, font, draw, padding) 

+

508 

+

509 

+

510def create_form_field_text(field: FormField, font: Font, draw: ImageDraw.Draw, 

+

511 field_height: int = 24) -> FormFieldText: 

+

512 """ 

+

513 Factory function to create a FormFieldText object. 

+

514 

+

515 Args: 

+

516 field: The FormField object to associate with the text 

+

517 font: The base font style for the label 

+

518 draw: The drawing context 

+

519 field_height: Height of the input field area 

+

520 

+

521 Returns: 

+

522 A FormFieldText object ready for rendering and interaction 

+

523 """ 

+

524 return FormFieldText(field, font, draw, field_height) 

+
+ + + diff --git a/cov_info/htmlcov/z_7d48e1f4c6486fa2_image_py.html b/cov_info/htmlcov/z_7d48e1f4c6486fa2_image_py.html new file mode 100644 index 0000000..be2f220 --- /dev/null +++ b/cov_info/htmlcov/z_7d48e1f4c6486fa2_image_py.html @@ -0,0 +1,379 @@ + + + + + Coverage for pyWebLayout/concrete/image.py: 93% + + + + + +
+
+

+ Coverage for pyWebLayout/concrete/image.py: + 93% +

+ +

+ 134 statements   + + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.15.4, + created at 2026-08-08 20:34 +0000 +

+ +
+
+
+

1import os 

+

2from typing import Optional 

+

3import numpy as np 

+

4from PIL import Image as PILImage, ImageDraw, ImageFont 

+

5from pyWebLayout.core.base import Renderable, Queriable 

+

6from pyWebLayout.abstract.block import Image as AbstractImage 

+

7from pyWebLayout.style import Alignment 

+

8 

+

9 

+

10class RenderableImage(Renderable, Queriable): 

+

11 """ 

+

12 A concrete implementation for rendering Image objects. 

+

13 """ 

+

14 

+

15 def __init__(self, image: AbstractImage, canvas: PILImage.Image, 

+

16 max_width: Optional[int] = None, max_height: Optional[int] = None, 

+

17 origin=None, size=None, callback=None, sheet=None, mode=None, 

+

18 halign=Alignment.CENTER, valign=Alignment.CENTER): 

+

19 """ 

+

20 Initialize a renderable image. 

+

21 

+

22 Args: 

+

23 image: The abstract Image object to render 

+

24 draw: The PIL ImageDraw object to draw on 

+

25 max_width: Maximum width constraint for the image 

+

26 max_height: Maximum height constraint for the image 

+

27 origin: Optional origin coordinates 

+

28 size: Optional size override 

+

29 callback: Optional callback function 

+

30 sheet: Optional sheet for rendering 

+

31 mode: Optional image mode 

+

32 halign: Horizontal alignment 

+

33 valign: Vertical alignment 

+

34 """ 

+

35 super().__init__() 

+

36 self._abstract_image = image 

+

37 self._canvas = canvas 

+

38 self._pil_image = None 

+

39 self._error_message = None 

+

40 self._halign = halign 

+

41 self._valign = valign 

+

42 

+

43 # Set origin as numpy array 

+

44 self._origin = np.array(origin) if origin is not None else np.array([0, 0]) 

+

45 

+

46 # Try to load the image 

+

47 self._load_image() 

+

48 

+

49 # Calculate the size if not provided 

+

50 if size is None: 

+

51 size = image.calculate_scaled_dimensions(max_width, max_height) 

+

52 # Ensure we have valid dimensions, fallback to defaults if None 

+

53 if size[0] is None or size[1] is None: 

+

54 size = (100, 100) # Default size when image dimensions are unavailable 

+

55 

+

56 # Ensure dimensions are positive (can be negative if calculated from insufficient space) 

+

57 size = (max(1, size[0]), max(1, size[1])) 

+

58 

+

59 # Set size as numpy array 

+

60 self._size = np.array(size) 

+

61 

+

62 @property 

+

63 def origin(self) -> np.ndarray: 

+

64 """Get the origin of the image""" 

+

65 return self._origin 

+

66 

+

67 @property 

+

68 def size(self) -> np.ndarray: 

+

69 """Get the size of the image""" 

+

70 return self._size 

+

71 

+

72 @property 

+

73 def width(self) -> int: 

+

74 """Get the width of the image""" 

+

75 return self._size[0] 

+

76 

+

77 def set_origin(self, origin: np.ndarray): 

+

78 """Set the origin of this image element""" 

+

79 self._origin = origin 

+

80 

+

81 def _load_image(self): 

+

82 """Load the image from the source path""" 

+

83 try: 

+

84 # Check if the image has already been loaded into memory 

+

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

+

86 self._abstract_image, 

+

87 '_loaded_image') and self._abstract_image._loaded_image is not None: 

+

88 self._pil_image = self._abstract_image._loaded_image 

+

89 return 

+

90 

+

91 source = self._abstract_image.source 

+

92 

+

93 # Handle different types of sources 

+

94 if os.path.isfile(source): 

+

95 # Local file 

+

96 self._pil_image = PILImage.open(source) 

+

97 self._abstract_image._loaded_image = self._pil_image 

+

98 elif source.startswith(('http://', 'https://')): 

+

99 # URL - requires requests library 

+

100 try: 

+

101 import requests 

+

102 from io import BytesIO 

+

103 

+

104 response = requests.get(source, stream=True) 

+

105 if response.status_code == 200: 

+

106 self._pil_image = PILImage.open(BytesIO(response.content)) 

+

107 self._abstract_image._loaded_image = self._pil_image 

+

108 else: 

+

109 self._error_message = f"Failed to load image: HTTP status {response.status_code}" 

+

110 except ImportError: 

+

111 self._error_message = "Requests library not available for URL loading" 

+

112 else: 

+

113 self._error_message = f"Unable to load image from source: {source}" 

+

114 

+

115 except Exception as e: 

+

116 self._error_message = f"Error loading image: {str(e)}" 

+

117 self._abstract_image._error = self._error_message 

+

118 

+

119 def render(self): 

+

120 """ 

+

121 Render the image directly into the canvas using the provided draw object. 

+

122 """ 

+

123 if self._pil_image: 

+

124 # Resize the image to fit the box while maintaining aspect ratio 

+

125 resized_image = self._resize_image() 

+

126 

+

127 # Calculate position based on alignment 

+

128 img_width, img_height = resized_image.size 

+

129 box_width, box_height = self._size 

+

130 

+

131 # Horizontal alignment 

+

132 if self._halign == Alignment.LEFT: 

+

133 x_offset = 0 

+

134 elif self._halign == Alignment.RIGHT: 

+

135 x_offset = box_width - img_width 

+

136 else: # CENTER is default 

+

137 x_offset = (box_width - img_width) // 2 

+

138 

+

139 # Vertical alignment 

+

140 if self._valign == Alignment.TOP: 

+

141 y_offset = 0 

+

142 elif self._valign == Alignment.BOTTOM: 

+

143 y_offset = box_height - img_height 

+

144 else: # CENTER is default 

+

145 y_offset = (box_height - img_height) // 2 

+

146 

+

147 # Calculate final position on canvas 

+

148 final_x = int(self._origin[0] + x_offset) 

+

149 final_y = int(self._origin[1] + y_offset) 

+

150 

+

151 # Get the underlying image from the draw object to paste onto 

+

152 

+

153 self._canvas.paste( 

+

154 resized_image, 

+

155 (final_x, 

+

156 final_y, 

+

157 final_x + 

+

158 img_width, 

+

159 final_y + 

+

160 img_height)) 

+

161 else: 

+

162 # Draw error placeholder 

+

163 self._draw_error_placeholder() 

+

164 

+

165 def _resize_image(self) -> PILImage.Image: 

+

166 """ 

+

167 Resize the image to fit within the box while maintaining aspect ratio. 

+

168 

+

169 Returns: 

+

170 A resized PIL Image 

+

171 """ 

+

172 if not self._pil_image: 

+

173 return PILImage.new('RGBA', tuple(self._size), (200, 200, 200, 100)) 

+

174 

+

175 # Get the target dimensions 

+

176 target_width, target_height = self._size 

+

177 

+

178 # Ensure target dimensions are positive 

+

179 target_width = max(1, int(target_width)) 

+

180 target_height = max(1, int(target_height)) 

+

181 

+

182 # Get the original dimensions 

+

183 orig_width, orig_height = self._pil_image.size 

+

184 

+

185 # Calculate the scaling factor to maintain aspect ratio 

+

186 width_ratio = target_width / orig_width 

+

187 height_ratio = target_height / orig_height 

+

188 

+

189 # Use the smaller ratio to ensure the image fits within the box 

+

190 ratio = min(width_ratio, height_ratio) 

+

191 

+

192 # Calculate new dimensions 

+

193 new_width = max(1, int(orig_width * ratio)) 

+

194 new_height = max(1, int(orig_height * ratio)) 

+

195 

+

196 # Resize the image 

+

197 if self._pil_image.mode == 'RGBA': 197 ↛ 198line 197 didn't jump to line 198 because the condition on line 197 was never true

+

198 resized = self._pil_image.resize((new_width, new_height), PILImage.LANCZOS) 

+

199 else: 

+

200 # Convert to RGBA if needed 

+

201 resized = self._pil_image.convert('RGBA').resize( 

+

202 (new_width, new_height), PILImage.LANCZOS) 

+

203 

+

204 return resized 

+

205 

+

206 def _draw_error_placeholder(self): 

+

207 """ 

+

208 Draw a placeholder for when the image can't be loaded. 

+

209 """ 

+

210 # Calculate the rectangle coordinates with origin offset 

+

211 x1 = int(self._origin[0]) 

+

212 y1 = int(self._origin[1]) 

+

213 x2 = int(self._origin[0] + self._size[0]) 

+

214 y2 = int(self._origin[1] + self._size[1]) 

+

215 

+

216 self._draw = ImageDraw.Draw(self._canvas) 

+

217 # Draw a gray box with a border 

+

218 self._draw.rectangle([(x1, y1), (x2, y2)], fill=( 

+

219 240, 240, 240), outline=(180, 180, 180), width=2) 

+

220 

+

221 # Draw an X across the box 

+

222 self._draw.line([(x1, y1), (x2, y2)], fill=(180, 180, 180), width=2) 

+

223 self._draw.line([(x1, y2), (x2, y1)], fill=(180, 180, 180), width=2) 

+

224 

+

225 # Add error text if available 

+

226 if self._error_message: 226 ↛ exitline 226 didn't return from function '_draw_error_placeholder' because the condition on line 226 was always true

+

227 try: 

+

228 # Try to use a basic font 

+

229 font = ImageFont.load_default() 

+

230 

+

231 # Draw the error message, wrapped to fit 

+

232 error_text = "Error: " + self._error_message 

+

233 

+

234 # Simple text wrapping - split by words and add lines 

+

235 words = error_text.split() 

+

236 lines = [] 

+

237 current_line = "" 

+

238 

+

239 for word in words: 

+

240 test_line = current_line + " " + word if current_line else word 

+

241 text_bbox = self._draw.textbbox((0, 0), test_line, font=font) 

+

242 text_width = text_bbox[2] - text_bbox[0] 

+

243 

+

244 if text_width <= self._size[0] - 20: # 10px padding on each side 

+

245 current_line = test_line 

+

246 else: 

+

247 lines.append(current_line) 

+

248 current_line = word 

+

249 

+

250 if current_line: 250 ↛ 254line 250 didn't jump to line 254 because the condition on line 250 was always true

+

251 lines.append(current_line) 

+

252 

+

253 # Draw each line 

+

254 y_pos = y1 + 10 

+

255 for line in lines: 

+

256 text_bbox = self._draw.textbbox((0, 0), line, font=font) 

+

257 text_width = text_bbox[2] - text_bbox[0] 

+

258 text_height = text_bbox[3] - text_bbox[1] 

+

259 

+

260 # Center the text horizontally 

+

261 x_pos = x1 + (self._size[0] - text_width) // 2 

+

262 

+

263 # Draw the text 

+

264 self._draw.text((x_pos, y_pos), line, fill=(80, 80, 80), font=font) 

+

265 

+

266 # Move to the next line 

+

267 y_pos += text_height + 2 

+

268 

+

269 except Exception: 

+

270 # If text rendering fails, just draw a generic error indicator 

+

271 pass 

+

272 

+

273 def in_object(self, point): 

+

274 """Check if a point is within this image""" 

+

275 point_array = np.array(point) 

+

276 relative_point = point_array - self._origin 

+

277 

+

278 # Check if the point is within the image boundaries 

+

279 return (0 <= relative_point[0] < self._size[0] and 

+

280 0 <= relative_point[1] < self._size[1]) 

+
+ + + diff --git a/cov_info/htmlcov/z_7d48e1f4c6486fa2_interaction_handler_py.html b/cov_info/htmlcov/z_7d48e1f4c6486fa2_interaction_handler_py.html new file mode 100644 index 0000000..7af4705 --- /dev/null +++ b/cov_info/htmlcov/z_7d48e1f4c6486fa2_interaction_handler_py.html @@ -0,0 +1,409 @@ + + + + + Coverage for pyWebLayout/concrete/interaction_handler.py: 55% + + + + + +
+
+

+ Coverage for pyWebLayout/concrete/interaction_handler.py: + 55% +

+ +

+ 99 statements   + + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.15.4, + created at 2026-08-08 20:34 +0000 +

+ +
+
+
+

1""" 

+

2Interaction handler for managing button/link press-release lifecycle with visual feedback. 

+

3 

+

4This module provides utilities for handling interactive element states and rendering 

+

5frames at different stages of interaction (pressed, released). 

+

6""" 

+

7 

+

8from typing import Optional, Tuple, Callable, Any 

+

9from PIL import Image 

+

10import time 

+

11import numpy as np 

+

12 

+

13from pyWebLayout.concrete.functional import LinkText, ButtonText 

+

14from pyWebLayout.concrete.page import Page 

+

15 

+

16 

+

17class InteractionHandler: 

+

18 """ 

+

19 Manages the press-release lifecycle for interactive elements. 

+

20 

+

21 This class handles the timing and state management needed to show 

+

22 visual feedback when buttons or links are clicked. It can generate 

+

23 multiple rendered frames showing the pressed and released states. 

+

24 

+

25 Usage patterns: 

+

26 

+

27 Pattern A - Simple one-shot with automatic frames: 

+

28 handler = InteractionHandler(page) 

+

29 frames = handler.execute_with_feedback(button_element, point) 

+

30 # Returns: [pressed_frame, released_frame] 

+

31 # Show frames in sequence with brief delay 

+

32 

+

33 Pattern B - Manual state management for custom event loops: 

+

34 handler = InteractionHandler(page) 

+

35 handler.set_pressed_state(button_element, True) 

+

36 pressed_frame = handler.render_current_state() 

+

37 # ... show frame, wait, execute action ... 

+

38 handler.set_pressed_state(button_element, False) 

+

39 released_frame = handler.render_current_state() 

+

40 """ 

+

41 

+

42 def __init__(self, page: Page, press_duration_ms: int = 150): 

+

43 """ 

+

44 Initialize the interaction handler. 

+

45 

+

46 Args: 

+

47 page: The Page object containing the interactive elements 

+

48 press_duration_ms: How long to show the pressed state (default: 150ms) 

+

49 """ 

+

50 self._page = page 

+

51 self._press_duration_ms = press_duration_ms 

+

52 

+

53 def set_pressed_state(self, element, pressed: bool): 

+

54 """ 

+

55 Set the pressed state of an interactive element. 

+

56 

+

57 Args: 

+

58 element: A LinkText or ButtonText object 

+

59 pressed: True to show pressed, False to show released 

+

60 """ 

+

61 if isinstance(element, (LinkText, ButtonText)): 

+

62 # Ensure element has page reference for dirty flag 

+

63 if not hasattr(element, '_page') or element._page is None: 

+

64 element.set_page(self._page) 

+

65 element.set_pressed(pressed) 

+

66 else: 

+

67 raise TypeError( 

+

68 f"Element must be LinkText or ButtonText, got {type(element)}") 

+

69 

+

70 def set_hovered_state(self, element, hovered: bool): 

+

71 """ 

+

72 Set the hovered state of an interactive element. 

+

73 

+

74 Args: 

+

75 element: A LinkText or ButtonText object 

+

76 hovered: True to show hovered, False for normal 

+

77 """ 

+

78 if isinstance(element, (LinkText, ButtonText)): 

+

79 # Ensure element has page reference for dirty flag 

+

80 if not hasattr(element, '_page') or element._page is None: 

+

81 element.set_page(self._page) 

+

82 element.set_hovered(hovered) 

+

83 else: 

+

84 raise TypeError( 

+

85 f"Element must be LinkText or ButtonText, got {type(element)}") 

+

86 

+

87 def render_current_state(self) -> Image.Image: 

+

88 """ 

+

89 Render the page with current element states. 

+

90 

+

91 Returns: 

+

92 PIL Image of the rendered page 

+

93 """ 

+

94 return self._page.render() 

+

95 

+

96 def execute_with_feedback( 

+

97 self, 

+

98 element, 

+

99 point: Optional[np.ndarray] = None, 

+

100 callback: Optional[Callable] = None) -> Tuple[Image.Image, Image.Image, Any]: 

+

101 """ 

+

102 Execute an interaction with visual feedback at each stage. 

+

103 

+

104 This is the high-level "all-in-one" method that: 

+

105 1. Sets pressed state and renders 

+

106 2. Waits for press_duration_ms 

+

107 3. Executes the element's callback (or provided callback) 

+

108 4. Sets released state and renders 

+

109 

+

110 Args: 

+

111 element: A LinkText or ButtonText object 

+

112 point: Optional point where interaction occurred 

+

113 callback: Optional custom callback (overrides element's callback) 

+

114 

+

115 Returns: 

+

116 Tuple of (pressed_frame, released_frame, callback_result) 

+

117 """ 

+

118 # Step 1: Render pressed state 

+

119 self.set_pressed_state(element, True) 

+

120 pressed_frame = self.render_current_state() 

+

121 

+

122 # Step 2: Wait for visual feedback duration 

+

123 time.sleep(self._press_duration_ms / 1000.0) 

+

124 

+

125 # Step 3: Execute callback 

+

126 callback_result = None 

+

127 if callback: 

+

128 callback_result = callback(point) if point is not None else callback() 

+

129 elif hasattr(element, 'interact'): 

+

130 callback_result = element.interact(point) 

+

131 

+

132 # Step 4: Render released state 

+

133 self.set_pressed_state(element, False) 

+

134 released_frame = self.render_current_state() 

+

135 

+

136 return pressed_frame, released_frame, callback_result 

+

137 

+

138 def execute_async_with_feedback( 

+

139 self, 

+

140 element, 

+

141 point: Optional[np.ndarray] = None) -> Tuple[Image.Image, Callable, Image.Image]: 

+

142 """ 

+

143 Execute an interaction with visual feedback, returning frames immediately 

+

144 without blocking. 

+

145 

+

146 This method returns the frames and a callback to execute later, allowing 

+

147 the caller to control when the action actually happens. 

+

148 

+

149 Args: 

+

150 element: A LinkText or ButtonText object 

+

151 point: Optional point where interaction occurred 

+

152 

+

153 Returns: 

+

154 Tuple of (pressed_frame, execute_callback, released_frame) 

+

155 where execute_callback is a function that will execute the interaction 

+

156 """ 

+

157 # Render pressed state 

+

158 self.set_pressed_state(element, True) 

+

159 pressed_frame = self.render_current_state() 

+

160 

+

161 # Create callback that will execute the interaction and reset state 

+

162 def execute_callback(): 

+

163 result = None 

+

164 if hasattr(element, 'interact'): 

+

165 result = element.interact(point) 

+

166 self.set_pressed_state(element, False) 

+

167 return result 

+

168 

+

169 # Pre-render the released state (element state is still pressed) 

+

170 # We'll return this frame but the caller controls when to show it 

+

171 self.set_pressed_state(element, False) 

+

172 released_frame = self.render_current_state() 

+

173 

+

174 # Reset back to pressed for consistency 

+

175 # (caller will call execute_callback which sets to False) 

+

176 self.set_pressed_state(element, True) 

+

177 

+

178 return pressed_frame, execute_callback, released_frame 

+

179 

+

180 

+

181class InteractionStateManager: 

+

182 """ 

+

183 Manages interaction states for multiple elements on a page. 

+

184 

+

185 Useful for applications that need to track hover/press states 

+

186 across many interactive elements simultaneously. 

+

187 """ 

+

188 

+

189 def __init__(self, page: Page): 

+

190 """ 

+

191 Initialize the state manager. 

+

192 

+

193 Args: 

+

194 page: The Page object containing interactive elements 

+

195 """ 

+

196 self._page = page 

+

197 self._hovered_element = None 

+

198 self._pressed_element = None 

+

199 

+

200 def update_hover(self, point: Tuple[int, int]) -> Optional[Image.Image]: 

+

201 """ 

+

202 Update hover state based on cursor position. 

+

203 

+

204 Queries the page to find what's under the cursor and updates 

+

205 hover states accordingly. 

+

206 

+

207 Args: 

+

208 point: Cursor position (x, y) 

+

209 

+

210 Returns: 

+

211 New rendered frame if hover state changed, None otherwise 

+

212 """ 

+

213 # Query what's at this point 

+

214 result = self._page.query_point(point) 

+

215 

+

216 if not result or not result.is_interactive: 

+

217 # Nothing interactive under cursor 

+

218 if self._hovered_element: 218 ↛ 224line 218 didn't jump to line 224 because the condition on line 218 was always true

+

219 # Clear previous hover 

+

220 if isinstance(self._hovered_element, (LinkText, ButtonText)): 220 ↛ 222line 220 didn't jump to line 222 because the condition on line 220 was always true

+

221 self._hovered_element.set_hovered(False) 

+

222 self._hovered_element = None 

+

223 return self._page.render() 

+

224 return None 

+

225 

+

226 # Something interactive is under cursor 

+

227 element = result.object 

+

228 if element != self._hovered_element: 

+

229 # Hover changed 

+

230 # Clear old hover 

+

231 if self._hovered_element and isinstance( 231 ↛ 233line 231 didn't jump to line 233 because the condition on line 231 was never true

+

232 self._hovered_element, (LinkText, ButtonText)): 

+

233 self._hovered_element.set_hovered(False) 

+

234 

+

235 # Set new hover 

+

236 if isinstance(element, (LinkText, ButtonText)): 236 ↛ 239line 236 didn't jump to line 239 because the condition on line 236 was always true

+

237 element.set_hovered(True) 

+

238 

+

239 self._hovered_element = element 

+

240 return self._page.render() 

+

241 

+

242 return None 

+

243 

+

244 def handle_mouse_down(self, point: Tuple[int, int]) -> Optional[Image.Image]: 

+

245 """ 

+

246 Handle mouse button press at a point. 

+

247 

+

248 Args: 

+

249 point: Click position (x, y) 

+

250 

+

251 Returns: 

+

252 New rendered frame showing pressed state, or None if nothing interactive 

+

253 """ 

+

254 result = self._page.query_point(point) 

+

255 

+

256 if not result or not result.is_interactive: 

+

257 return None 

+

258 

+

259 element = result.object 

+

260 if isinstance(element, (LinkText, ButtonText)): 260 ↛ 265line 260 didn't jump to line 265 because the condition on line 260 was always true

+

261 element.set_pressed(True) 

+

262 self._pressed_element = element 

+

263 return self._page.render() 

+

264 

+

265 return None 

+

266 

+

267 def handle_mouse_up( 

+

268 self, 

+

269 point: Tuple[int, 

+

270 int]) -> Tuple[Optional[Image.Image], 

+

271 Any]: 

+

272 """ 

+

273 Handle mouse button release at a point. 

+

274 

+

275 Args: 

+

276 point: Release position (x, y) 

+

277 

+

278 Returns: 

+

279 Tuple of (rendered_frame, callback_result) 

+

280 Frame shows released state, result is from executing the callback 

+

281 """ 

+

282 if not self._pressed_element: 

+

283 return None, None 

+

284 

+

285 # Execute the interaction 

+

286 callback_result = None 

+

287 if hasattr(self._pressed_element, 'interact'): 287 ↛ 292line 287 didn't jump to line 292 because the condition on line 287 was always true

+

288 callback_result = self._pressed_element.interact( 

+

289 np.array(point)) 

+

290 

+

291 # Release the pressed state 

+

292 if isinstance(self._pressed_element, (LinkText, ButtonText)): 292 ↛ 295line 292 didn't jump to line 295 because the condition on line 292 was always true

+

293 self._pressed_element.set_pressed(False) 

+

294 

+

295 self._pressed_element = None 

+

296 

+

297 return self._page.render(), callback_result 

+

298 

+

299 def reset(self): 

+

300 """Reset all interaction states.""" 

+

301 if self._hovered_element and isinstance( 301 ↛ 303line 301 didn't jump to line 303 because the condition on line 301 was never true

+

302 self._hovered_element, (LinkText, ButtonText)): 

+

303 self._hovered_element.set_hovered(False) 

+

304 

+

305 if self._pressed_element and isinstance( 

+

306 self._pressed_element, (LinkText, ButtonText)): 

+

307 self._pressed_element.set_pressed(False) 

+

308 

+

309 self._hovered_element = None 

+

310 self._pressed_element = None 

+
+ + + diff --git a/cov_info/htmlcov/z_7d48e1f4c6486fa2_page_py.html b/cov_info/htmlcov/z_7d48e1f4c6486fa2_page_py.html new file mode 100644 index 0000000..0fc82fe --- /dev/null +++ b/cov_info/htmlcov/z_7d48e1f4c6486fa2_page_py.html @@ -0,0 +1,566 @@ + + + + + Coverage for pyWebLayout/concrete/page.py: 95% + + + + + +
+
+

+ Coverage for pyWebLayout/concrete/page.py: + 95% +

+ +

+ 176 statements   + + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.15.4, + created at 2026-08-08 20:34 +0000 +

+ +
+
+
+

1from typing import List, Tuple, Optional 

+

2import numpy as np 

+

3from PIL import Image, ImageDraw 

+

4 

+

5from pyWebLayout.core.base import Renderable, Queriable 

+

6from pyWebLayout.core.query import QueryResult, SelectionRange 

+

7from pyWebLayout.core.callback_registry import CallbackRegistry 

+

8from pyWebLayout.style.page_style import PageStyle 

+

9 

+

10 

+

11class Page(Renderable, Queriable): 

+

12 """ 

+

13 A page represents a canvas that can hold and render child renderable objects. 

+

14 It handles layout, rendering, and provides query capabilities to find which child 

+

15 contains a given point. 

+

16 """ 

+

17 

+

18 # Mode of the render canvas. The measurement context matches it so that text 

+

19 # width caching keys stay consistent between layout and rendering. 

+

20 _CANVAS_MODE = 'RGBA' 

+

21 

+

22 def __init__(self, size: Tuple[int, int], style: Optional[PageStyle] = None, 

+

23 origin: Tuple[int, int] = (0, 0)): 

+

24 """ 

+

25 Initialize a new page. 

+

26 

+

27 Args: 

+

28 size: The total size of the page (width, height) including borders 

+

29 style: The PageStyle defining borders, spacing, and appearance 

+

30 origin: Absolute position of the page's top-left corner. Non-zero for 

+

31 a page nested inside another surface, such as a table cell. 

+

32 """ 

+

33 self._size = size 

+

34 self._origin = origin 

+

35 self._style = style if style is not None else PageStyle() 

+

36 self._children: List[Renderable] = [] 

+

37 self._canvas: Optional[Image.Image] = None 

+

38 self._draw: Optional[ImageDraw.Draw] = None 

+

39 self._measurement_draw: Optional[ImageDraw.ImageDraw] = None 

+

40 # Initialize y_offset to start of content area 

+

41 # Position the first line so its baseline is close to the top boundary 

+

42 # For subsequent lines, baseline-to-baseline spacing is used 

+

43 self._current_y_offset = (self._origin[1] + self._style.border_width 

+

44 + self._style.padding_top) 

+

45 self._is_first_line = True # Track if we're placing the first line 

+

46 # Callback registry for managing interactable elements 

+

47 self._callbacks = CallbackRegistry() 

+

48 # Dirty flag to track if page needs re-rendering due to state changes 

+

49 self._dirty = True 

+

50 

+

51 def free_space(self) -> Tuple[int, int]: 

+

52 """ 

+

53 Get the remaining space in the content area. 

+

54 

+

55 Deprecated: use content_rect and remaining_height, which this delegates to. 

+

56 """ 

+

57 return (self.content_rect[2], self.remaining_height) 

+

58 

+

59 def can_fit_line( 

+

60 self, 

+

61 baseline_spacing: int, 

+

62 ascent: int = 0, 

+

63 descent: int = 0) -> bool: 

+

64 """ 

+

65 Check if a line with the given metrics can fit on the page. 

+

66 

+

67 Args: 

+

68 baseline_spacing: Distance from current position to next baseline 

+

69 ascent: Font ascent (height above baseline), defaults to 0 for backward compat 

+

70 descent: Font descent (height below baseline), defaults to 0 for backward compat 

+

71 

+

72 Returns: 

+

73 True if the line fits within page boundaries 

+

74 """ 

+

75 # Calculate the maximum Y position allowed (bottom boundary) 

+

76 content_y, content_h = self.content_rect[1], self.content_rect[3] 

+

77 max_y = content_y + content_h 

+

78 

+

79 # If ascent/descent not provided, use simple check (backward compatibility) 

+

80 if ascent == 0 and descent == 0: 

+

81 return (self._current_y_offset + baseline_spacing) <= max_y 

+

82 

+

83 # Calculate where the bottom of the text would be 

+

84 # Text bottom = current_y_offset + ascent + descent 

+

85 text_bottom = self._current_y_offset + ascent + descent 

+

86 

+

87 # Check if text bottom would exceed the boundary 

+

88 return text_bottom <= max_y 

+

89 

+

90 @property 

+

91 def size(self) -> Tuple[int, int]: 

+

92 """Get the total page size including borders""" 

+

93 return self._size 

+

94 

+

95 @property 

+

96 def origin(self) -> Tuple[int, int]: 

+

97 """Absolute position of the page's top-left corner""" 

+

98 return self._origin 

+

99 

+

100 @property 

+

101 def content_origin(self) -> Tuple[int, int]: 

+

102 """ 

+

103 Absolute top-left of the content box: the page origin plus its border and 

+

104 top/left padding. Layout starts here. 

+

105 """ 

+

106 return ( 

+

107 self._origin[0] + self._style.border_width + self._style.padding_left, 

+

108 self._origin[1] + self._style.border_width + self._style.padding_top, 

+

109 ) 

+

110 

+

111 @property 

+

112 def content_rect(self) -> Tuple[int, int, int, int]: 

+

113 """(x, y, width, height) of the content box, in absolute coordinates""" 

+

114 x, y = self.content_origin 

+

115 return (x, y, self.content_size[0], self.content_size[1]) 

+

116 

+

117 @property 

+

118 def remaining_height(self) -> int: 

+

119 """Content-box height still available below the current layout cursor""" 

+

120 _, y, _, h = self.content_rect 

+

121 return max(0, y + h - self._current_y_offset) 

+

122 

+

123 @property 

+

124 def canvas_size(self) -> Tuple[int, int]: 

+

125 """Get the canvas size (page size minus borders)""" 

+

126 border_reduction = self._style.total_border_width 

+

127 return ( 

+

128 self._size[0] - border_reduction, 

+

129 self._size[1] - border_reduction 

+

130 ) 

+

131 

+

132 @property 

+

133 def content_size(self) -> Tuple[int, int]: 

+

134 """Get the content area size (canvas minus padding)""" 

+

135 canvas_w, canvas_h = self.canvas_size 

+

136 return ( 

+

137 canvas_w - self._style.total_horizontal_padding, 

+

138 canvas_h - self._style.total_vertical_padding 

+

139 ) 

+

140 

+

141 @property 

+

142 def border_size(self) -> int: 

+

143 """Get the border width""" 

+

144 return self._style.border_width 

+

145 

+

146 @property 

+

147 def available_width(self) -> int: 

+

148 """Get the available width for content (content area width)""" 

+

149 return self.content_size[0] 

+

150 

+

151 @property 

+

152 def style(self) -> PageStyle: 

+

153 """Get the page style""" 

+

154 return self._style 

+

155 

+

156 @property 

+

157 def callbacks(self) -> CallbackRegistry: 

+

158 """Get the callback registry for managing interactable elements""" 

+

159 return self._callbacks 

+

160 

+

161 @property 

+

162 def is_dirty(self) -> bool: 

+

163 """Check if the page needs re-rendering due to state changes""" 

+

164 return self._dirty 

+

165 

+

166 def mark_dirty(self): 

+

167 """Mark the page as needing re-rendering""" 

+

168 self._dirty = True 

+

169 

+

170 def mark_clean(self): 

+

171 """Mark the page as clean (up-to-date render)""" 

+

172 self._dirty = False 

+

173 

+

174 @property 

+

175 def draw(self) -> Optional[ImageDraw.Draw]: 

+

176 """ 

+

177 Get the ImageDraw object bound to this page's render canvas. 

+

178 

+

179 Rebuilt whenever the canvas has been invalidated: a draw context 

+

180 outlives the image it was created from, so checking only _draw would 

+

181 hand back a context pointing at a discarded canvas. 

+

182 """ 

+

183 if self._draw is None or self._canvas is None: 

+

184 # Initialize canvas and draw context if not already done 

+

185 self._canvas = self._create_canvas() 

+

186 self._draw = ImageDraw.Draw(self._canvas) 

+

187 return self._draw 

+

188 

+

189 @property 

+

190 def measurement_draw(self) -> ImageDraw.ImageDraw: 

+

191 """ 

+

192 A scratch draw context for text metrics during layout. 

+

193 

+

194 Layout asks for text widths constantly, but has no reason to touch the 

+

195 render canvas - and the canvas is invalidated on every add_child, so 

+

196 measuring through `draw` would allocate a full-page image per line. 

+

197 This context is 1x1 and never invalidated. 

+

198 

+

199 Its mode matches the render canvas because Text keys its width cache on 

+

200 the draw mode; a mismatch would double every cache entry. Children built 

+

201 against it are re-bound to the real canvas by render_children. 

+

202 """ 

+

203 if self._measurement_draw is None: 

+

204 scratch = Image.new(self._CANVAS_MODE, (1, 1)) 

+

205 self._measurement_draw = ImageDraw.Draw(scratch) 

+

206 return self._measurement_draw 

+

207 

+

208 def add_child(self, child: Renderable) -> 'Page': 

+

209 """ 

+

210 Add a child renderable object to this page. 

+

211 

+

212 Args: 

+

213 child: The renderable object to add 

+

214 

+

215 Returns: 

+

216 Self for method chaining 

+

217 """ 

+

218 self._children.append(child) 

+

219 self._current_y_offset = child.origin[1] + child.size[1] 

+

220 # Invalidate the canvas when children change 

+

221 self._canvas = None 

+

222 return self 

+

223 

+

224 def remove_child(self, child: Renderable) -> bool: 

+

225 """ 

+

226 Remove a child from the page. 

+

227 

+

228 Args: 

+

229 child: The child to remove 

+

230 

+

231 Returns: 

+

232 True if the child was found and removed, False otherwise 

+

233 """ 

+

234 try: 

+

235 self._children.remove(child) 

+

236 self._canvas = None 

+

237 return True 

+

238 except ValueError: 

+

239 return False 

+

240 

+

241 def clear_children(self) -> 'Page': 

+

242 """ 

+

243 Remove all children from the page. 

+

244 

+

245 Returns: 

+

246 Self for method chaining 

+

247 """ 

+

248 self._children.clear() 

+

249 self._canvas = None 

+

250 # Clear callback registry when clearing children 

+

251 self._callbacks.clear() 

+

252 # Reset y_offset to start of content area (after border and padding) 

+

253 self._current_y_offset = self.content_origin[1] 

+

254 return self 

+

255 

+

256 @property 

+

257 def children(self) -> List[Renderable]: 

+

258 """Get a copy of the children list""" 

+

259 return self._children.copy() 

+

260 

+

261 def render_children(self): 

+

262 """ 

+

263 Call render on all children in the list. 

+

264 Children draw directly onto the page's canvas via the shared ImageDraw object. 

+

265 """ 

+

266 for child in self._children: 

+

267 # Synchronize draw context for Line objects before rendering 

+

268 if hasattr(child, '_draw'): 

+

269 child._draw = self._draw 

+

270 # Synchronize canvas for Image objects before rendering 

+

271 if hasattr(child, '_canvas'): 271 ↛ 272line 271 didn't jump to line 272 because the condition on line 271 was never true

+

272 child._canvas = self._canvas 

+

273 if hasattr(child, 'render'): 273 ↛ 266line 273 didn't jump to line 266 because the condition on line 273 was always true

+

274 child.render() 

+

275 

+

276 def render(self) -> Image.Image: 

+

277 """ 

+

278 Render the page with all its children. 

+

279 

+

280 Returns: 

+

281 PIL Image containing the rendered page 

+

282 """ 

+

283 # Create the base canvas and draw object 

+

284 self._canvas = self._create_canvas() 

+

285 self._draw = ImageDraw.Draw(self._canvas) 

+

286 

+

287 # Render all children - they draw directly onto the canvas 

+

288 self.render_children() 

+

289 

+

290 # Mark as clean after rendering 

+

291 self._dirty = False 

+

292 

+

293 return self._canvas 

+

294 

+

295 def _create_canvas(self) -> Image.Image: 

+

296 """ 

+

297 Create the base canvas with background and borders. 

+

298 

+

299 Returns: 

+

300 PIL Image with background and borders applied 

+

301 """ 

+

302 # Create base image 

+

303 canvas = Image.new(self._CANVAS_MODE, self._size, (*self._style.background_color, 255)) 

+

304 

+

305 # Draw borders if needed 

+

306 if self._style.border_width > 0: 

+

307 draw = ImageDraw.Draw(canvas) 

+

308 border_color = (*self._style.border_color, 255) 

+

309 

+

310 # Draw border rectangle inside the content area 

+

311 border_offset = self._style.border_width 

+

312 draw.rectangle([ 

+

313 (border_offset, border_offset), 

+

314 (self._size[0] - border_offset - 1, self._size[1] - border_offset - 1) 

+

315 ], outline=border_color) 

+

316 

+

317 return canvas 

+

318 

+

319 def query_point(self, point: Tuple[int, int]) -> Optional[QueryResult]: 

+

320 """ 

+

321 Query a point to find the deepest object at that location. 

+

322 Traverses children and uses Queriable.in_object() for hit-testing. 

+

323 

+

324 Args: 

+

325 point: The (x, y) coordinates to query 

+

326 

+

327 Returns: 

+

328 QueryResult with metadata about what was found, or None if nothing hit 

+

329 """ 

+

330 point_array = np.array(point) 

+

331 

+

332 # Check each child (in reverse order so topmost child is found first) 

+

333 for child in reversed(self._children): 

+

334 # Use Queriable mixin's in_object() for hit-testing 

+

335 if isinstance(child, Queriable) and child.in_object(point_array): 

+

336 # If child can also query (has children of its own), recurse 

+

337 if hasattr(child, 'query_point'): 

+

338 result = child.query_point(point) 

+

339 if result: 

+

340 result.parent_page = self 

+

341 return result 

+

342 # If child's query returned None, continue to next child 

+

343 continue 

+

344 

+

345 # Otherwise, package this child as the result 

+

346 return self._make_query_result(child, point) 

+

347 

+

348 # Nothing hit - return empty result 

+

349 return QueryResult( 

+

350 object=self, 

+

351 object_type="empty", 

+

352 bounds=(int(point[0]), int(point[1]), 0, 0) 

+

353 ) 

+

354 

+

355 def _make_query_result(self, obj, point: Tuple[int, int]) -> QueryResult: 

+

356 """ 

+

357 Package an object into a QueryResult with metadata. 

+

358 

+

359 Args: 

+

360 obj: The object to package 

+

361 point: The query point 

+

362 

+

363 Returns: 

+

364 QueryResult with extracted metadata 

+

365 """ 

+

366 from .text import Text 

+

367 from .functional import LinkText, ButtonText 

+

368 

+

369 # Extract bounds 

+

370 origin = getattr(obj, '_origin', np.array([0, 0])) 

+

371 size = getattr(obj, 'size', np.array([0, 0])) 

+

372 bounds = ( 

+

373 int(origin[0]), 

+

374 int(origin[1]), 

+

375 int(size[0]) if hasattr(size, '__getitem__') else 0, 

+

376 int(size[1]) if hasattr(size, '__getitem__') else 0 

+

377 ) 

+

378 

+

379 # Determine type and extract metadata 

+

380 if isinstance(obj, LinkText): 

+

381 return QueryResult( 

+

382 object=obj, 

+

383 object_type="link", 

+

384 bounds=bounds, 

+

385 text=obj._text, 

+

386 is_interactive=True, 

+

387 link_target=obj._link.location if hasattr(obj, '_link') else None 

+

388 ) 

+

389 elif isinstance(obj, ButtonText): 389 ↛ 390line 389 didn't jump to line 390 because the condition on line 389 was never true

+

390 return QueryResult( 

+

391 object=obj, 

+

392 object_type="button", 

+

393 bounds=bounds, 

+

394 text=obj._text, 

+

395 is_interactive=True, 

+

396 callback=obj._callback if hasattr(obj, '_callback') else None 

+

397 ) 

+

398 elif isinstance(obj, Text): 

+

399 return QueryResult( 

+

400 object=obj, 

+

401 object_type="text", 

+

402 bounds=bounds, 

+

403 text=obj._text if hasattr(obj, '_text') else None 

+

404 ) 

+

405 else: 

+

406 return QueryResult( 

+

407 object=obj, 

+

408 object_type="unknown", 

+

409 bounds=bounds 

+

410 ) 

+

411 

+

412 def query_range(self, start: Tuple[int, int], 

+

413 end: Tuple[int, int]) -> SelectionRange: 

+

414 """ 

+

415 Query all text objects between two points (for text selection). 

+

416 Uses Queriable.in_object() to determine which objects are in range. 

+

417 

+

418 Args: 

+

419 start: Starting (x, y) point 

+

420 end: Ending (x, y) point 

+

421 

+

422 Returns: 

+

423 SelectionRange with all text objects between the points 

+

424 """ 

+

425 results = [] 

+

426 in_selection = False 

+

427 

+

428 start_result = self.query_point(start) 

+

429 end_result = self.query_point(end) 

+

430 

+

431 if not start_result or not end_result: 431 ↛ 432line 431 didn't jump to line 432 because the condition on line 431 was never true

+

432 return SelectionRange(start, end, []) 

+

433 

+

434 # Walk through all children (Lines) and their text objects 

+

435 from .text import Line, Text 

+

436 

+

437 for child in self._children: 

+

438 if isinstance(child, Line) and hasattr(child, '_text_objects'): 438 ↛ 437line 438 didn't jump to line 437 because the condition on line 438 was always true

+

439 for text_obj in child._text_objects: 

+

440 # Check if this text is the start or is between start and end 

+

441 if text_obj == start_result.object: 

+

442 in_selection = True 

+

443 

+

444 if in_selection and isinstance(text_obj, Text): 

+

445 result = self._make_query_result(text_obj, start) 

+

446 results.append(result) 

+

447 

+

448 if text_obj == end_result.object: 

+

449 in_selection = False 

+

450 break 

+

451 

+

452 return SelectionRange(start, end, results) 

+

453 

+

454 def in_object(self, point: Tuple[int, int]) -> bool: 

+

455 """ 

+

456 Check if a point is within this page's bounds. 

+

457 

+

458 Args: 

+

459 point: The (x, y) coordinates to check 

+

460 

+

461 Returns: 

+

462 True if the point is within the page bounds 

+

463 """ 

+

464 return ( 

+

465 self._origin[0] <= point[0] < self._origin[0] + self._size[0] and 

+

466 self._origin[1] <= point[1] < self._origin[1] + self._size[1] 

+

467 ) 

+
+ + + diff --git a/cov_info/htmlcov/z_7d48e1f4c6486fa2_table_py.html b/cov_info/htmlcov/z_7d48e1f4c6486fa2_table_py.html new file mode 100644 index 0000000..5213c84 --- /dev/null +++ b/cov_info/htmlcov/z_7d48e1f4c6486fa2_table_py.html @@ -0,0 +1,805 @@ + + + + + Coverage for pyWebLayout/concrete/table.py: 78% + + + + + +
+
+

+ Coverage for pyWebLayout/concrete/table.py: + 78% +

+ +

+ 303 statements   + + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.15.4, + created at 2026-08-08 20:34 +0000 +

+ +
+
+
+

1""" 

+

2Concrete table rendering implementation for pyWebLayout. 

+

3 

+

4This module provides the concrete rendering classes for tables, including: 

+

5- TableRenderer: Main table rendering with borders and spacing 

+

6- TableRowRenderer: Individual row rendering 

+

7- TableCellRenderer: Cell rendering with support for nested content (text, images, links) 

+

8""" 

+

9 

+

10from __future__ import annotations 

+

11from typing import Tuple, List, Optional, Dict 

+

12from PIL import Image, ImageDraw 

+

13from dataclasses import dataclass 

+

14 

+

15from pyWebLayout.core.base import Renderable 

+

16from pyWebLayout.concrete.box import Box 

+

17from pyWebLayout.abstract.block import Table, TableRow, TableCell, Paragraph, Heading, Image as AbstractImage 

+

18from pyWebLayout.abstract.interactive_image import InteractiveImage 

+

19 

+

20 

+

21@dataclass 

+

22class TableStyle: 

+

23 """Styling configuration for table rendering.""" 

+

24 

+

25 # Border configuration 

+

26 border_width: int = 1 

+

27 border_color: Tuple[int, int, int] = (0, 0, 0) 

+

28 

+

29 # Cell padding 

+

30 cell_padding: Tuple[int, int, int, int] = (5, 5, 5, 5) # top, right, bottom, left 

+

31 

+

32 # Header styling 

+

33 header_bg_color: Tuple[int, int, int] = (240, 240, 240) 

+

34 header_text_bold: bool = True 

+

35 

+

36 # Cell background 

+

37 cell_bg_color: Tuple[int, int, int] = (255, 255, 255) 

+

38 alternate_row_color: Optional[Tuple[int, int, int]] = (250, 250, 250) 

+

39 

+

40 # Spacing 

+

41 cell_spacing: int = 0 # Space between cells (for separated borders model) 

+

42 

+

43 

+

44class TableCellRenderer(Box): 

+

45 """ 

+

46 Renders a single table cell with its content. 

+

47 Supports paragraphs, headings, images, and links within cells. 

+

48 """ 

+

49 

+

50 def __init__(self, 

+

51 cell: TableCell, 

+

52 origin: Tuple[int, 

+

53 int], 

+

54 size: Tuple[int, 

+

55 int], 

+

56 draw: ImageDraw.Draw, 

+

57 style: TableStyle, 

+

58 is_header_section: bool = False, 

+

59 canvas: Optional[Image.Image] = None): 

+

60 """ 

+

61 Initialize a table cell renderer. 

+

62 

+

63 Args: 

+

64 cell: The abstract TableCell to render 

+

65 origin: Top-left position of the cell 

+

66 size: Width and height of the cell 

+

67 draw: PIL ImageDraw object for rendering 

+

68 style: Table styling configuration 

+

69 is_header_section: Whether this cell is in the header section 

+

70 canvas: Optional PIL Image for pasting images (required for image rendering) 

+

71 """ 

+

72 super().__init__(origin, size) 

+

73 self._cell = cell 

+

74 self._draw = draw 

+

75 self._style = style 

+

76 self._is_header_section = is_header_section or cell.is_header 

+

77 self._canvas = canvas 

+

78 self._children: List[Renderable] = [] 

+

79 

+

80 def render(self) -> Image.Image: 

+

81 """Render the table cell.""" 

+

82 # Determine background color 

+

83 if self._is_header_section: 

+

84 bg_color = self._style.header_bg_color 

+

85 else: 

+

86 bg_color = self._style.cell_bg_color 

+

87 

+

88 # Draw cell background 

+

89 x, y = self._origin 

+

90 w, h = self._size 

+

91 self._draw.rectangle( 

+

92 [x, y, x + w, y + h], 

+

93 fill=bg_color, 

+

94 outline=self._style.border_color, 

+

95 width=self._style.border_width 

+

96 ) 

+

97 

+

98 # Calculate content area (inside padding) 

+

99 padding = self._style.cell_padding 

+

100 content_x = x + padding[3] # left padding 

+

101 content_y = y + padding[0] # top padding 

+

102 content_width = w - (padding[1] + padding[3]) # minus left and right padding 

+

103 content_height = h - (padding[0] + padding[2]) # minus top and bottom padding 

+

104 

+

105 # Render cell content (text) 

+

106 self._render_cell_content(content_x, content_y, content_width, content_height) 

+

107 

+

108 return None # Cell rendering is done directly on the page 

+

109 

+

110 def _render_cell_content(self, x: int, y: int, width: int, height: int): 

+

111 """Render the content inside the cell (text and images) with line wrapping.""" 

+

112 from pyWebLayout.concrete.text import Line, Text 

+

113 from pyWebLayout.style.fonts import Font 

+

114 from pyWebLayout.style import FontWeight, Alignment 

+

115 

+

116 current_y = y + 2 

+

117 available_height = height - 4 # Account for top/bottom padding 

+

118 

+

119 # Create font for the cell 

+

120 font_size = 12 

+

121 font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" 

+

122 if self._is_header_section and self._style.header_text_bold: 

+

123 font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf" 

+

124 

+

125 font = Font( 

+

126 font_path=font_path, 

+

127 font_size=font_size, 

+

128 weight=FontWeight.BOLD if self._is_header_section and self._style.header_text_bold else FontWeight.NORMAL 

+

129 ) 

+

130 

+

131 # Word spacing constraints (min, max) 

+

132 min_spacing = int(font_size * 0.25) 

+

133 max_spacing = int(font_size * 0.5) 

+

134 word_spacing = (min_spacing, max_spacing) 

+

135 

+

136 # Line height (baseline spacing) 

+

137 line_height = font_size + 4 

+

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

+

139 

+

140 # Render each block in the cell 

+

141 for block in self._cell.blocks(): 

+

142 if isinstance(block, AbstractImage): 

+

143 # Render image 

+

144 current_y = self._render_image_in_cell( 

+

145 block, x, current_y, width, height - (current_y - y)) 

+

146 elif isinstance(block, (Paragraph, Heading)): 146 ↛ 225line 146 didn't jump to line 225 because the condition on line 146 was always true

+

147 # Get words from the block 

+

148 from pyWebLayout.abstract.inline import Word as AbstractWord 

+

149 

+

150 word_items = block.words() if callable(block.words) else block.words 

+

151 words = list(word_items) 

+

152 

+

153 if not words: 153 ↛ 154line 153 didn't jump to line 154 because the condition on line 153 was never true

+

154 continue 

+

155 

+

156 # Create new Word objects with the table cell's font 

+

157 # The words from the paragraph may have AbstractStyle, but we need Font objects 

+

158 wrapped_words = [] 

+

159 for word_item in words: 

+

160 # Handle word tuples (index, word_obj) 

+

161 if isinstance(word_item, tuple) and len(word_item) >= 2: 161 ↛ 162line 161 didn't jump to line 162 because the condition on line 161 was never true

+

162 word_obj = word_item[1] 

+

163 else: 

+

164 word_obj = word_item 

+

165 

+

166 # Extract text from the word 

+

167 word_text = word_obj.text if hasattr(word_obj, 'text') else str(word_obj) 

+

168 

+

169 # Create a new Word with the cell's Font 

+

170 new_word = AbstractWord(word_text, font) 

+

171 wrapped_words.append(new_word) 

+

172 

+

173 # Layout words using Line objects with wrapping 

+

174 word_index = 0 

+

175 pretext = None 

+

176 

+

177 while word_index < len(wrapped_words): 

+

178 # Check if we have space for another line 

+

179 if current_y + ascent + descent > y + available_height: 179 ↛ 180line 179 didn't jump to line 180 because the condition on line 179 was never true

+

180 break # No more space in cell 

+

181 

+

182 # Create a new line 

+

183 line = Line( 

+

184 spacing=word_spacing, 

+

185 origin=(x + 2, current_y), 

+

186 size=(width - 4, line_height), 

+

187 draw=self._draw, 

+

188 font=font, 

+

189 halign=Alignment.LEFT 

+

190 ) 

+

191 

+

192 # Add words to this line until it's full 

+

193 line_has_content = False 

+

194 while word_index < len(wrapped_words): 

+

195 word = wrapped_words[word_index] 

+

196 

+

197 # Try to add word to line 

+

198 success, overflow = line.add_word(word, pretext) 

+

199 pretext = None # Clear pretext after use 

+

200 

+

201 if success: 201 ↛ 214line 201 didn't jump to line 214 because the condition on line 201 was always true

+

202 line_has_content = True 

+

203 if overflow: 203 ↛ 207line 203 didn't jump to line 207 because the condition on line 203 was never true

+

204 # Word was hyphenated, carry over to next line 

+

205 # DON'T increment word_index - we need to add the overflow 

+

206 # to the next line with the same word 

+

207 pretext = overflow 

+

208 break # Move to next line 

+

209 else: 

+

210 # Word fit completely, move to next word 

+

211 word_index += 1 

+

212 else: 

+

213 # Word doesn't fit on this line 

+

214 if not line_has_content: 

+

215 # Even first word doesn't fit, force it anyway and advance 

+

216 # This prevents infinite loops with words that truly can't fit 

+

217 word_index += 1 

+

218 break 

+

219 

+

220 # Render the line if it has content 

+

221 if line_has_content or len(line.text_objects) > 0: 221 ↛ 177line 221 didn't jump to line 177 because the condition on line 221 was always true

+

222 line.render() 

+

223 current_y += line_height 

+

224 

+

225 if current_y > y + height - 10: # Don't overflow cell 225 ↛ 226line 225 didn't jump to line 226 because the condition on line 225 was never true

+

226 break 

+

227 

+

228 # If no structured content, try to get any text representation 

+

229 if current_y == y + 2 and hasattr(self._cell, '_text_content'): 229 ↛ 231line 229 didn't jump to line 231 because the condition on line 229 was never true

+

230 # Use simple text rendering for fallback case 

+

231 from PIL import ImageFont 

+

232 try: 

+

233 pil_font = ImageFont.truetype(font_path, font_size) 

+

234 except BaseException: 

+

235 pil_font = ImageFont.load_default() 

+

236 

+

237 self._draw.text( 

+

238 (x + 2, current_y), 

+

239 self._cell._text_content, 

+

240 fill=(0, 0, 0), 

+

241 font=pil_font 

+

242 ) 

+

243 

+

244 def _render_image_in_cell(self, image_block: AbstractImage, x: int, y: int, 

+

245 max_width: int, max_height: int) -> int: 

+

246 """ 

+

247 Render an image block inside a table cell. 

+

248 

+

249 Returns: 

+

250 The new Y position after the image 

+

251 """ 

+

252 try: 

+

253 # Get the image path from the block 

+

254 image_path = None 

+

255 if hasattr(image_block, 'source'): 255 ↛ 257line 255 didn't jump to line 257 because the condition on line 255 was always true

+

256 image_path = image_block.source 

+

257 elif hasattr(image_block, '_source'): 

+

258 image_path = image_block._source 

+

259 elif hasattr(image_block, 'path'): 

+

260 image_path = image_block.path 

+

261 elif hasattr(image_block, 'src'): 

+

262 image_path = image_block.src 

+

263 elif hasattr(image_block, '_path'): 

+

264 image_path = image_block._path 

+

265 elif hasattr(image_block, '_src'): 

+

266 image_path = image_block._src 

+

267 

+

268 if not image_path: 268 ↛ 269line 268 didn't jump to line 269 because the condition on line 268 was never true

+

269 return y + 20 # Skip if no image path 

+

270 

+

271 # Load and resize image to fit in cell 

+

272 img = Image.open(image_path) 

+

273 

+

274 # Calculate scaling to fit within max dimensions 

+

275 # Use more of the cell space for images 

+

276 img_width, img_height = img.size 

+

277 scale_w = max_width / img_width if img_width > max_width else 1 

+

278 scale_h = (max_height - 10) / \ 

+

279 img_height if img_height > (max_height - 10) else 1 

+

280 scale = min(scale_w, scale_h, 1.0) # Don't upscale 

+

281 

+

282 new_width = int(img_width * scale) 

+

283 new_height = int(img_height * scale) 

+

284 

+

285 if scale < 1.0: 285 ↛ 286line 285 didn't jump to line 286 because the condition on line 285 was never true

+

286 img = img.resize((new_width, new_height), Image.Resampling.LANCZOS) 

+

287 

+

288 # Center image horizontally in cell 

+

289 img_x = x + (max_width - new_width) // 2 

+

290 

+

291 # Paste the image onto the canvas if available 

+

292 if self._canvas is not None: 292 ↛ 299line 292 didn't jump to line 299 because the condition on line 292 was always true

+

293 if img.mode == 'RGBA': 293 ↛ 294line 293 didn't jump to line 294 because the condition on line 293 was never true

+

294 self._canvas.paste(img, (img_x, y), img) 

+

295 else: 

+

296 self._canvas.paste(img, (img_x, y)) 

+

297 else: 

+

298 # Fallback: draw a placeholder if no canvas provided 

+

299 self._draw.rectangle( 

+

300 [img_x, y, img_x + new_width, y + new_height], 

+

301 fill=(200, 200, 200), 

+

302 outline=(150, 150, 150) 

+

303 ) 

+

304 

+

305 # Draw image indicator text 

+

306 from PIL import ImageFont 

+

307 try: 

+

308 small_font = ImageFont.truetype( 

+

309 "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 9) 

+

310 except BaseException: 

+

311 small_font = ImageFont.load_default() 

+

312 

+

313 text = f"[Image: {new_width}x{new_height}]" 

+

314 bbox = self._draw.textbbox((0, 0), text, font=small_font) 

+

315 text_width = bbox[2] - bbox[0] 

+

316 text_x = img_x + (new_width - text_width) // 2 

+

317 text_y = y + (new_height - 12) // 2 

+

318 self._draw.text( 

+

319 (text_x, text_y), text, fill=( 

+

320 100, 100, 100), font=small_font) 

+

321 

+

322 # Set bounds on InteractiveImage objects for tap detection 

+

323 if isinstance(image_block, InteractiveImage): 323 ↛ 324line 323 didn't jump to line 324 because the condition on line 323 was never true

+

324 image_block.set_rendered_bounds( 

+

325 origin=(img_x, y), 

+

326 size=(new_width, new_height) 

+

327 ) 

+

328 

+

329 return y + new_height + 5 # Add some spacing after image 

+

330 

+

331 except Exception: 

+

332 # If image loading fails, just return current position 

+

333 return y + 20 

+

334 

+

335 

+

336class TableRowRenderer(Box): 

+

337 """ 

+

338 Renders a single table row containing multiple cells. 

+

339 """ 

+

340 

+

341 def __init__(self, 

+

342 row: TableRow, 

+

343 origin: Tuple[int, 

+

344 int], 

+

345 column_widths: List[int], 

+

346 row_height: int, 

+

347 draw: ImageDraw.Draw, 

+

348 style: TableStyle, 

+

349 is_header_section: bool = False, 

+

350 canvas: Optional[Image.Image] = None): 

+

351 """ 

+

352 Initialize a table row renderer. 

+

353 

+

354 Args: 

+

355 row: The abstract TableRow to render 

+

356 origin: Top-left position of the row 

+

357 column_widths: List of widths for each column 

+

358 row_height: Height of this row 

+

359 draw: PIL ImageDraw object for rendering 

+

360 style: Table styling configuration 

+

361 is_header_section: Whether this row is in the header section 

+

362 canvas: Optional PIL Image for pasting images 

+

363 """ 

+

364 width = sum(column_widths) + style.border_width * (len(column_widths) + 1) 

+

365 super().__init__(origin, (width, row_height)) 

+

366 self._row = row 

+

367 self._column_widths = column_widths 

+

368 self._row_height = row_height 

+

369 self._draw = draw 

+

370 self._style = style 

+

371 self._is_header_section = is_header_section 

+

372 self._canvas = canvas 

+

373 self._cell_renderers: List[TableCellRenderer] = [] 

+

374 

+

375 def render(self) -> Image.Image: 

+

376 """Render the table row by rendering each cell.""" 

+

377 x, y = self._origin 

+

378 current_x = x 

+

379 

+

380 # Render each cell 

+

381 cells = list(self._row.cells()) 

+

382 for i, cell in enumerate(cells): 

+

383 if i < len(self._column_widths): 383 ↛ 382line 383 didn't jump to line 382 because the condition on line 383 was always true

+

384 cell_width = self._column_widths[i] 

+

385 

+

386 # Handle colspan 

+

387 if cell.colspan > 1 and i + cell.colspan <= len(self._column_widths): 

+

388 # Sum up widths for spanned columns 

+

389 cell_width = sum(self._column_widths[i:i + cell.colspan]) 

+

390 cell_width += self._style.border_width * (cell.colspan - 1) 

+

391 

+

392 # Create and render cell 

+

393 cell_renderer = TableCellRenderer( 

+

394 cell, 

+

395 (current_x, y), 

+

396 (cell_width, self._row_height), 

+

397 self._draw, 

+

398 self._style, 

+

399 self._is_header_section, 

+

400 self._canvas 

+

401 ) 

+

402 cell_renderer.render() 

+

403 self._cell_renderers.append(cell_renderer) 

+

404 

+

405 current_x += cell_width + self._style.border_width 

+

406 

+

407 return None # Row rendering is done directly on the page 

+

408 

+

409 

+

410class TableRenderer(Box): 

+

411 """ 

+

412 Main table renderer that orchestrates the rendering of an entire table. 

+

413 Handles layout calculation, row/cell placement, and overall table structure. 

+

414 """ 

+

415 

+

416 def __init__(self, 

+

417 table: Table, 

+

418 origin: Tuple[int, 

+

419 int], 

+

420 available_width: int, 

+

421 draw: ImageDraw.Draw, 

+

422 style: Optional[TableStyle] = None, 

+

423 canvas: Optional[Image.Image] = None): 

+

424 """ 

+

425 Initialize a table renderer. 

+

426 

+

427 Args: 

+

428 table: The abstract Table to render 

+

429 origin: Top-left position where the table should be rendered 

+

430 available_width: Maximum width available for the table 

+

431 draw: PIL ImageDraw object for rendering 

+

432 style: Optional table styling configuration 

+

433 canvas: Optional PIL Image for pasting images 

+

434 """ 

+

435 self._table = table 

+

436 self._draw = draw 

+

437 self._style = style or TableStyle() 

+

438 self._available_width = available_width 

+

439 self._canvas = canvas 

+

440 

+

441 # Calculate table dimensions 

+

442 self._column_widths, self._row_heights = self._calculate_dimensions() 

+

443 total_width = sum(self._column_widths) + \ 

+

444 self._style.border_width * (len(self._column_widths) + 1) 

+

445 total_height = sum(self._row_heights.values()) + \ 

+

446 self._style.border_width * (len(self._row_heights) + 1) 

+

447 

+

448 super().__init__(origin, (total_width, total_height)) 

+

449 self._row_renderers: List[TableRowRenderer] = [] 

+

450 

+

451 def _calculate_dimensions(self) -> Tuple[List[int], Dict[str, int]]: 

+

452 """ 

+

453 Calculate column widths and row heights for the table. 

+

454 

+

455 Uses the table optimizer for intelligent column width distribution. 

+

456 

+

457 Returns: 

+

458 Tuple of (column_widths, row_heights_dict) 

+

459 """ 

+

460 from pyWebLayout.layout.table_optimizer import optimize_table_layout 

+

461 

+

462 all_rows = list(self._table.all_rows()) 

+

463 

+

464 if not all_rows: 

+

465 return ([100], {"header": 30, "body": 30, "footer": 30}) 

+

466 

+

467 # Use optimizer for column widths! 

+

468 column_widths = optimize_table_layout( 

+

469 self._table, 

+

470 self._available_width, 

+

471 sample_size=5, 

+

472 style=self._style 

+

473 ) 

+

474 

+

475 if not column_widths: 475 ↛ 477line 475 didn't jump to line 477 because the condition on line 475 was never true

+

476 # Fallback if table is empty 

+

477 column_widths = [100] 

+

478 

+

479 # Calculate row heights dynamically based on optimized column widths 

+

480 header_height = self._calculate_row_height_for_section( 

+

481 all_rows, "header", column_widths) if any( 

+

482 1 for section, _ in all_rows if section == "header") else 0 

+

483 

+

484 body_height = self._calculate_row_height_for_section( 

+

485 all_rows, "body", column_widths) 

+

486 

+

487 footer_height = self._calculate_row_height_for_section( 

+

488 all_rows, "footer", column_widths) if any( 

+

489 1 for section, _ in all_rows if section == "footer") else 0 

+

490 

+

491 row_heights = { 

+

492 "header": header_height, 

+

493 "body": body_height, 

+

494 "footer": footer_height 

+

495 } 

+

496 

+

497 return (column_widths, row_heights) 

+

498 

+

499 def _calculate_row_height_for_section( 

+

500 self, 

+

501 all_rows: List, 

+

502 section: str, 

+

503 column_widths: List[int]) -> int: 

+

504 """ 

+

505 Calculate the maximum required height for rows in a specific section. 

+

506 

+

507 Args: 

+

508 all_rows: List of all rows in the table 

+

509 section: Section name ('header', 'body', or 'footer') 

+

510 column_widths: List of column widths 

+

511 

+

512 Returns: 

+

513 Maximum height needed for rows in this section 

+

514 """ 

+

515 from pyWebLayout.concrete.text import Text 

+

516 from pyWebLayout.style.fonts import Font 

+

517 from pyWebLayout.abstract.inline import Word as AbstractWord 

+

518 

+

519 # Font configuration 

+

520 font_size = 12 

+

521 line_height = font_size + 4 

+

522 padding = self._style.cell_padding 

+

523 vertical_padding = padding[0] + padding[2] # top + bottom 

+

524 horizontal_padding = padding[1] + padding[3] # left + right 

+

525 

+

526 max_height = 40 # Minimum height 

+

527 

+

528 for row_section, row in all_rows: 

+

529 if row_section != section: 

+

530 continue 

+

531 

+

532 row_max_height = 40 # Minimum for this row 

+

533 

+

534 for cell_idx, cell in enumerate(row.cells()): 

+

535 if cell_idx >= len(column_widths): 535 ↛ 536line 535 didn't jump to line 536 because the condition on line 535 was never true

+

536 continue 

+

537 

+

538 # Get cell width (accounting for colspan) 

+

539 cell_width = column_widths[cell_idx] 

+

540 if cell.colspan > 1 and cell_idx + \ 540 ↛ 542line 540 didn't jump to line 542 because the condition on line 540 was never true

+

541 cell.colspan <= len(column_widths): 

+

542 cell_width = sum( 

+

543 column_widths[cell_idx:cell_idx + cell.colspan]) 

+

544 cell_width += self._style.border_width * (cell.colspan - 1) 

+

545 

+

546 # Calculate content width (minus padding) 

+

547 content_width = cell_width - horizontal_padding - 4 # Extra margin 

+

548 

+

549 cell_height = vertical_padding + 4 # Base height with padding 

+

550 

+

551 # Analyze each block in the cell 

+

552 for block in cell.blocks(): 

+

553 if isinstance(block, AbstractImage): 

+

554 # Images need more space 

+

555 cell_height = max(cell_height, 120) 

+

556 elif isinstance(block, (Paragraph, Heading)): 556 ↛ 552line 556 didn't jump to line 552 because the condition on line 556 was always true

+

557 # Calculate text wrapping height 

+

558 word_items = block.words() if callable( 

+

559 block.words) else block.words 

+

560 words = list(word_items) 

+

561 

+

562 if not words: 562 ↛ 563line 562 didn't jump to line 563 because the condition on line 562 was never true

+

563 continue 

+

564 

+

565 # Simulate text wrapping to count lines 

+

566 lines_needed = self._estimate_wrapped_lines( 

+

567 words, content_width, font_size) 

+

568 text_height = lines_needed * line_height 

+

569 cell_height = max( 

+

570 cell_height, text_height + vertical_padding + 4) 

+

571 

+

572 row_max_height = max(row_max_height, cell_height) 

+

573 

+

574 max_height = max(max_height, row_max_height) 

+

575 

+

576 return max_height 

+

577 

+

578 def _estimate_wrapped_lines( 

+

579 self, 

+

580 words: List, 

+

581 available_width: int, 

+

582 font_size: int) -> int: 

+

583 """ 

+

584 Estimate how many lines are needed to render the given words. 

+

585 

+

586 Args: 

+

587 words: List of word objects 

+

588 available_width: Available width for text 

+

589 font_size: Font size in pixels 

+

590 

+

591 Returns: 

+

592 Number of lines needed 

+

593 """ 

+

594 from pyWebLayout.concrete.text import Text 

+

595 from pyWebLayout.style.fonts import Font 

+

596 

+

597 # Create a temporary font for measurement 

+

598 font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" 

+

599 font = Font(font_path=font_path, font_size=font_size) 

+

600 

+

601 # Word spacing (approximate) 

+

602 word_spacing = int(font_size * 0.25) 

+

603 

+

604 lines = 1 

+

605 current_line_width = 0 

+

606 

+

607 for word_item in words: 

+

608 # Handle word tuples (index, word_obj) 

+

609 if isinstance(word_item, tuple) and len(word_item) >= 2: 609 ↛ 610line 609 didn't jump to line 610 because the condition on line 609 was never true

+

610 word_obj = word_item[1] 

+

611 else: 

+

612 word_obj = word_item 

+

613 

+

614 # Extract text from the word 

+

615 word_text = word_obj.text if hasattr( 

+

616 word_obj, 'text') else str(word_obj) 

+

617 

+

618 # Measure word width 

+

619 word_width = font.font.getlength(word_text) 

+

620 

+

621 # Check if word fits on current line 

+

622 if current_line_width > 0: # Not first word on line 

+

623 needed_width = current_line_width + word_spacing + word_width 

+

624 if needed_width > available_width: 624 ↛ 626line 624 didn't jump to line 626 because the condition on line 624 was never true

+

625 # Need new line 

+

626 lines += 1 

+

627 current_line_width = word_width 

+

628 else: 

+

629 current_line_width = needed_width 

+

630 else: 

+

631 # First word on line 

+

632 if word_width > available_width: 632 ↛ 634line 632 didn't jump to line 634 because the condition on line 632 was never true

+

633 # Word needs to be hyphenated, assume it takes 1 line 

+

634 lines += 1 

+

635 current_line_width = 0 

+

636 else: 

+

637 current_line_width = word_width 

+

638 

+

639 return lines 

+

640 

+

641 def render(self) -> Image.Image: 

+

642 """Render the complete table.""" 

+

643 x, y = self._origin 

+

644 current_y = y 

+

645 

+

646 # Render caption if present 

+

647 if self._table.caption: 

+

648 current_y = self._render_caption(x, current_y) 

+

649 current_y += 10 # Space after caption 

+

650 

+

651 # Render header rows 

+

652 for section, row in self._table.all_rows(): 

+

653 if section == "header": 

+

654 row_height = self._row_heights["header"] 

+

655 elif section == "footer": 

+

656 row_height = self._row_heights["footer"] 

+

657 else: 

+

658 row_height = self._row_heights["body"] 

+

659 

+

660 is_header = (section == "header") 

+

661 

+

662 row_renderer = TableRowRenderer( 

+

663 row, 

+

664 (x, current_y), 

+

665 self._column_widths, 

+

666 row_height, 

+

667 self._draw, 

+

668 self._style, 

+

669 is_header, 

+

670 self._canvas 

+

671 ) 

+

672 row_renderer.render() 

+

673 self._row_renderers.append(row_renderer) 

+

674 

+

675 current_y += row_height + self._style.border_width 

+

676 

+

677 return None # Table rendering is done directly on the page 

+

678 

+

679 def _render_caption(self, x: int, y: int) -> int: 

+

680 """Render the table caption and return the new Y position.""" 

+

681 from PIL import ImageFont 

+

682 

+

683 try: 

+

684 font = ImageFont.truetype( 

+

685 "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 13) 

+

686 except BaseException: 

+

687 font = ImageFont.load_default() 

+

688 

+

689 # Center the caption 

+

690 bbox = self._draw.textbbox((0, 0), self._table.caption, font=font) 

+

691 text_width = bbox[2] - bbox[0] 

+

692 caption_x = x + (self._size[0] - text_width) // 2 

+

693 

+

694 self._draw.text((caption_x, y), self._table.caption, fill=(0, 0, 0), font=font) 

+

695 

+

696 return y + 20 # Caption height 

+

697 

+

698 @property 

+

699 def height(self) -> int: 

+

700 """Get the total height of the rendered table.""" 

+

701 return int(self._size[1]) 

+

702 

+

703 @property 

+

704 def width(self) -> int: 

+

705 """Get the total width of the rendered table.""" 

+

706 return int(self._size[0]) 

+
+ + + diff --git a/cov_info/htmlcov/z_7d48e1f4c6486fa2_text_py.html b/cov_info/htmlcov/z_7d48e1f4c6486fa2_text_py.html new file mode 100644 index 0000000..28a4bc7 --- /dev/null +++ b/cov_info/htmlcov/z_7d48e1f4c6486fa2_text_py.html @@ -0,0 +1,1290 @@ + + + + + Coverage for pyWebLayout/concrete/text.py: 77% + + + + + +
+
+

+ Coverage for pyWebLayout/concrete/text.py: + 77% +

+ +

+ 462 statements   + + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.15.4, + created at 2026-08-08 20:34 +0000 +

+ +
+
+
+

1from __future__ import annotations 

+

2from pyWebLayout.core.base import Renderable, Queriable 

+

3from pyWebLayout.core.query import QueryResult 

+

4from .box import Box 

+

5from pyWebLayout.style import Alignment, Font, TextDecoration 

+

6from pyWebLayout.abstract import Word 

+

7from pyWebLayout.abstract.inline import LinkedWord 

+

8from pyWebLayout.abstract.functional import Link 

+

9from pyWebLayout.core.cache import UsageCache, SizedUsageCache 

+

10from PIL import ImageDraw, ImageFont 

+

11from typing import Tuple, List, Optional, Any, Dict 

+

12import logging 

+

13import math 

+

14import numpy as np 

+

15from abc import ABC, abstractmethod 

+

16 

+

17logger = logging.getLogger(__name__) 

+

18 

+

19 

+

20# --------------------------------------------------------------------------- 

+

21# Text rendering caches 

+

22# 

+

23# A page re-measures and re-rasterises the same words constantly: measured over a 

+

24# novel at 1404x1872, a page issues ~2800 width measurements and ~2500 glyph 

+

25# rasterisations for fewer than 1000 distinct (font, string) pairs. Caching both 

+

26# turns a ~225ms page into a ~30ms page. Both caches are bounded so that a long 

+

27# reading session cannot grow without limit on a memory-constrained device. 

+

28# --------------------------------------------------------------------------- 

+

29 

+

30# Word widths are small floats; 8192 entries costs well under 1MB and comfortably 

+

31# spans the working set of several chapters at a couple of font sizes. 

+

32DEFAULT_WIDTH_CACHE_ENTRIES = 8192 

+

33 

+

34# Glyph bitmaps are the expensive ones: ~700 bytes each on average at 1404x1872, 

+

35# so an unbounded cache reaches ~12MB after 40 pages. 4MB holds several pages' 

+

36# worth of distinct words while leaving headroom on a 512MB Pi Zero 2. 

+

37DEFAULT_GLYPH_CACHE_BYTES = 4 * 1024 * 1024 

+

38 

+

39# PIL rasterises text at sub-pixel horizontal offsets, so a cache keyed only on 

+

40# (font, string) would quantise every word to a whole pixel. Bucketing the 

+

41# sub-pixel phase keeps that error negligible at the cost of more entries. 2 steps 

+

42# holds the mean error to ~3.6/255 -- a fifth of one step of a 16-level e-ink 

+

43# panel -- while keeping the cache four times smaller than 4 steps would. 

+

44DEFAULT_GLYPH_SUBPIXEL_STEPS = 2 

+

45 

+

46 

+

47def _glyph_entry_bytes(entry: Tuple[Any, Tuple[int, int]]) -> int: 

+

48 """Approximate footprint of a cached (mask, offset) pair, in bytes.""" 

+

49 mask = entry[0] 

+

50 try: 

+

51 width, height = mask.size 

+

52 except (AttributeError, TypeError, ValueError): 

+

53 return 0 

+

54 return width * height 

+

55 

+

56 

+

57_width_cache: UsageCache = UsageCache(DEFAULT_WIDTH_CACHE_ENTRIES) 

+

58_glyph_cache: SizedUsageCache = SizedUsageCache(DEFAULT_GLYPH_CACHE_BYTES, _glyph_entry_bytes) 

+

59_glyph_subpixel_steps: int = DEFAULT_GLYPH_SUBPIXEL_STEPS 

+

60 

+

61# Every Line asks its font for the advance width of a space. That single 

+

62# FreeTypeFont.getlength(" ") call costs ~18us -- two orders of magnitude more 

+

63# than getmetrics() -- because PIL shapes the string from scratch each time, and 

+

64# it lands once per line created, which dominates the cost of laying a line out. 

+

65# There are only ever a handful of distinct fonts in play, so memoise per font 

+

66# object. Values are wrapped in a 1-tuple because None is itself a legitimate 

+

67# result (fonts that cannot report a length) and must not read as a cache miss. 

+

68_space_advance_cache: Dict[Any, Tuple[Optional[int]]] = {} 

+

69 

+

70# Set to False the first time the fast rasterisation path is found to be 

+

71# unavailable (e.g. a PIL build without the private ImageDraw internals it uses), 

+

72# after which every Text falls back to ImageDraw.text(). 

+

73_glyph_fast_path_available: bool = True 

+

74 

+

75 

+

76def configure_text_caches(width_entries: Optional[int] = None, 

+

77 glyph_bytes: Optional[int] = None, 

+

78 subpixel_steps: Optional[int] = None): 

+

79 """ 

+

80 Tune the text rendering caches. 

+

81 

+

82 Memory-constrained targets should shrink these; a desktop rendering many font 

+

83 sizes may benefit from raising them. 

+

84 

+

85 Args: 

+

86 width_entries: Maximum cached word-width measurements. 

+

87 glyph_bytes: Maximum total size of cached glyph bitmaps, in bytes. 

+

88 subpixel_steps: Sub-pixel phase buckets per axis. 1 disables sub-pixel 

+

89 positioning entirely (smallest cache, slightly softer text). 

+

90 """ 

+

91 global _glyph_subpixel_steps 

+

92 

+

93 if width_entries is not None: 

+

94 _width_cache.resize(width_entries) 

+

95 if glyph_bytes is not None: 

+

96 _glyph_cache.resize(glyph_bytes) 

+

97 if subpixel_steps is not None: 

+

98 if subpixel_steps <= 0: 

+

99 raise ValueError(f"subpixel_steps must be positive, got {subpixel_steps}") 

+

100 if subpixel_steps != _glyph_subpixel_steps: 

+

101 # Cached entries embed the phase bucket in their key. 

+

102 _glyph_cache.clear() 

+

103 _glyph_subpixel_steps = subpixel_steps 

+

104 

+

105 

+

106def clear_text_caches(): 

+

107 """Drop all cached widths and glyph bitmaps.""" 

+

108 _width_cache.clear() 

+

109 _glyph_cache.clear() 

+

110 _space_advance_cache.clear() 

+

111 

+

112 

+

113def _space_advance(font) -> Optional[int]: 

+

114 """ 

+

115 The font's own advance width for a space, in whole pixels. 

+

116 

+

117 None when the font cannot report one, which is the signal for callers to fall 

+

118 back to their configured spacing range. 

+

119 """ 

+

120 try: 

+

121 cached = _space_advance_cache.get(font) 

+

122 except TypeError: 

+

123 # Unhashable font object; measure without caching. 

+

124 cached = None 

+

125 else: 

+

126 if cached is not None: 

+

127 return cached[0] 

+

128 

+

129 try: 

+

130 value = int(round(font.getlength(" "))) 

+

131 except (AttributeError, TypeError, ValueError): 

+

132 value = None 

+

133 

+

134 try: 

+

135 _space_advance_cache[font] = (value,) 

+

136 except TypeError: 

+

137 pass 

+

138 return value 

+

139 

+

140 

+

141def text_cache_stats() -> Dict[str, Any]: 

+

142 """Occupancy and hit rates for both text caches, for tuning and diagnostics.""" 

+

143 return { 

+

144 'width': _width_cache.stats(), 

+

145 'glyph': _glyph_cache.stats(), 

+

146 'glyph_subpixel_steps': _glyph_subpixel_steps, 

+

147 'glyph_fast_path': _glyph_fast_path_available, 

+

148 } 

+

149 

+

150 

+

151def prewarm_text_caches(entries, 

+

152 draw: Optional[ImageDraw.ImageDraw] = None, 

+

153 budget_bytes: Optional[int] = None, 

+

154 max_words: Optional[int] = None) -> Tuple[int, int]: 

+

155 """ 

+

156 Preload the caches with a document's most frequent words. 

+

157 

+

158 A document states its own access distribution up front: the words it uses most 

+

159 are the words every page will draw. Rasterising them once at open time moves 

+

160 that work off the page-turn path, and seeding each entry with its document 

+

161 frequency puts it in the right place in the eviction order immediately, rather 

+

162 than after the cache has learned it. 

+

163 

+

164 This depends on eviction ranking by use count. Under recency eviction the 

+

165 preloaded entries would be discarded by the first page of unfamiliar text; under 

+

166 usage ranking a word occurring 4000 times outranks anything met while scanning 

+

167 and stays resident. Measured over a 50-page trace, preloading cut misses by 27% 

+

168 with usage ranking against 12% with recency. 

+

169 

+

170 Args: 

+

171 entries: Iterable of ``(font, text, colour, frequency)``, where `font` is a 

+

172 PIL font object, `colour` the fill the text will be drawn in, and 

+

173 `frequency` the number of times the word occurs in the document. 

+

174 draw: An ImageDraw sharing the page's mode, used to resolve ink and font 

+

175 mode. A scratch RGBA context is used if omitted. 

+

176 budget_bytes: Cap on bytes to preload. Defaults to half the glyph budget so 

+

177 that live rendering keeps room to cache what preloading missed. 

+

178 max_words: Cap on distinct words to preload, before sub-pixel variants. 

+

179 

+

180 Returns: 

+

181 Tuple of (words preloaded, bytes preloaded). 

+

182 """ 

+

183 if not _glyph_fast_path_available: 

+

184 return 0, 0 

+

185 

+

186 if draw is None: 

+

187 from PIL import Image 

+

188 draw = ImageDraw.Draw(Image.new('RGBA', (1, 1))) 

+

189 

+

190 if budget_bytes is None: 

+

191 budget_bytes = _glyph_cache.max_bytes // 2 

+

192 budget_bytes = min(budget_bytes, _glyph_cache.max_bytes) 

+

193 

+

194 ranked = sorted(entries, key=lambda e: -e[3]) 

+

195 if max_words is not None: 

+

196 ranked = ranked[:max_words] 

+

197 

+

198 steps = _glyph_subpixel_steps 

+

199 mode = draw.fontmode 

+

200 draw_mode = draw.mode 

+

201 ink_cache: Dict[Any, Any] = {} 

+

202 words = 0 

+

203 used = 0 

+

204 

+

205 for font, text, colour, frequency in ranked: 

+

206 if frequency <= 1 or used >= budget_bytes: 

+

207 break 

+

208 if not isinstance(font, ImageFont.FreeTypeFont): 

+

209 continue 

+

210 

+

211 try: 

+

212 ink = ink_cache.get(colour) 

+

213 if ink is None: 

+

214 ink, _ = draw._getink(colour) 

+

215 if ink is None: 

+

216 continue 

+

217 ink_cache[colour] = ink 

+

218 

+

219 # Measuring is cheap and every layout pass needs it. 

+

220 _width_cache.put((font, text, draw_mode), 

+

221 draw.textlength(text, font=font), count=frequency) 

+

222 

+

223 # Words land on arbitrary sub-pixel offsets, so cover every horizontal 

+

224 # phase. Baselines are whole pixels, so only phase 0 is needed 

+

225 # vertically. 

+

226 for x_bucket in range(steps): 

+

227 entry = font.getmask2(text, mode, anchor="ls", ink=ink, 

+

228 start=(x_bucket / steps, 0.0)) 

+

229 _glyph_cache.put((font, text, mode, ink, x_bucket, 0), entry, 

+

230 count=frequency) 

+

231 used += _glyph_entry_bytes(entry) 

+

232 words += 1 

+

233 

+

234 except AttributeError: 

+

235 logger.warning("Glyph cache unavailable for this Pillow build; " 

+

236 "skipping prewarm.", exc_info=True) 

+

237 return words, used 

+

238 except (TypeError, ValueError): 

+

239 continue 

+

240 

+

241 logger.debug("Prewarmed %d words (%.2fMB) into the text caches", 

+

242 words, used / 1e6) 

+

243 return words, used 

+

244 

+

245 

+

246class AlignmentHandler(ABC): 

+

247 """ 

+

248 Abstract base class for text alignment handlers. 

+

249 Each handler implements a specific alignment strategy. 

+

250 """ 

+

251 

+

252 @abstractmethod 

+

253 def calculate_spacing_and_position(self, text_objects: List['Text'], 

+

254 available_width: int, min_spacing: int, 

+

255 max_spacing: int, 

+

256 natural_spacing: Optional[int] = None, 

+

257 total_width: Optional[float] = None 

+

258 ) -> Tuple[int, int, bool]: 

+

259 """ 

+

260 Calculate the spacing between words and starting position for the line. 

+

261 

+

262 Args: 

+

263 text_objects: List of Text objects in the line 

+

264 available_width: Total width available for the line 

+

265 min_spacing: Minimum spacing between words 

+

266 max_spacing: Maximum spacing between words 

+

267 natural_spacing: The font's own space width. Ragged alignments use it 

+

268 as a constant gap; justification ignores it. Defaults to 

+

269 min_spacing when not supplied. 

+

270 total_width: The summed width of `text_objects`, when the caller 

+

271 already knows it. Purely an optimisation: a line asks its handler 

+

272 to re-measure once per candidate word, and summing the whole line 

+

273 each time makes filling a line quadratic in its word count. Omit 

+

274 it and the sum is taken here as before. 

+

275 

+

276 Returns: 

+

277 Tuple of (spacing_between_words, starting_x_position, overflow) 

+

278 """ 

+

279 

+

280 

+

281class LeftAlignmentHandler(AlignmentHandler): 

+

282 """Handler for left-aligned text.""" 

+

283 

+

284 def calculate_spacing_and_position(self, 

+

285 text_objects: List['Text'], 

+

286 available_width: int, 

+

287 min_spacing: int, 

+

288 max_spacing: int, 

+

289 natural_spacing: Optional[int] = None, 

+

290 total_width: Optional[float] = None 

+

291 ) -> Tuple[int, int, bool]: 

+

292 """ 

+

293 Calculate spacing and position for left-aligned text objects. 

+

294 

+

295 Left-aligned text uses a constant word space and leaves whatever is left 

+

296 over as a ragged right edge. It must not spread the residual space across 

+

297 the gaps: that stretches each line by a different amount, which reads as 

+

298 badly-set justified text rather than as ragged-right. 

+

299 

+

300 Args: 

+

301 text_objects (List[Text]): A list of text objects to be laid out. 

+

302 available_width (int): The total width available for layout. 

+

303 min_spacing (int): Minimum spacing between text objects. 

+

304 max_spacing (int): Maximum spacing between text objects. 

+

305 natural_spacing (Optional[int]): The font's own space width. 

+

306 

+

307 Returns: 

+

308 Tuple[int, int, bool]: Spacing, start position, and overflow flag. 

+

309 """ 

+

310 # Handle single word case 

+

311 if len(text_objects) <= 1: 

+

312 return 0, 0, False 

+

313 

+

314 spacing = min_spacing if natural_spacing is None else natural_spacing 

+

315 spacing = max(min_spacing, min(max_spacing, int(spacing))) 

+

316 

+

317 text_length = (sum([text.width for text in text_objects]) 

+

318 if total_width is None else total_width) 

+

319 num_gaps = len(text_objects) - 1 

+

320 

+

321 # The spacing is constant whether or not the content fits: tightening a 

+

322 # full line here would make it differ from its neighbours, which is the 

+

323 # variation this alignment is supposed to avoid. Report the overflow and 

+

324 # let line breaking move the offending word instead. 

+

325 overflow = text_length + (spacing * num_gaps) > available_width 

+

326 

+

327 return spacing, 0, overflow 

+

328 

+

329 

+

330class CenterRightAlignmentHandler(AlignmentHandler): 

+

331 """Handler for center and right-aligned text.""" 

+

332 

+

333 def __init__(self, alignment: Alignment): 

+

334 self._alignment = alignment 

+

335 

+

336 def calculate_spacing_and_position(self, text_objects: List['Text'], 

+

337 available_width: int, min_spacing: int, 

+

338 max_spacing: int, 

+

339 natural_spacing: Optional[int] = None, 

+

340 total_width: Optional[float] = None 

+

341 ) -> Tuple[int, int, bool]: 

+

342 """ 

+

343 Centre/right alignment: constant word space, line shifted as a block. 

+

344 

+

345 Like left alignment, the residual space must not be spread across the 

+

346 gaps - it belongs in the margin. The start position is then derived from 

+

347 the same spacing that will actually be used, so the line lands where it 

+

348 was measured to land. 

+

349 """ 

+

350 word_length = (sum([word.width for word in text_objects]) 

+

351 if total_width is None else total_width) 

+

352 

+

353 # Handle single word case 

+

354 if len(text_objects) <= 1: 

+

355 if self._alignment == Alignment.CENTER: 

+

356 start_position = (available_width - word_length) // 2 

+

357 else: # RIGHT 

+

358 start_position = available_width - word_length 

+

359 return 0, max(0, int(start_position)), False 

+

360 

+

361 spacing = min_spacing if natural_spacing is None else natural_spacing 

+

362 spacing = max(min_spacing, min(max_spacing, int(spacing))) 

+

363 

+

364 num_gaps = len(text_objects) - 1 

+

365 overflow = word_length + (spacing * num_gaps) > available_width 

+

366 

+

367 content_length = word_length + num_gaps * spacing 

+

368 if self._alignment == Alignment.CENTER: 

+

369 start_position = (available_width - content_length) // 2 

+

370 else: 

+

371 start_position = available_width - content_length 

+

372 

+

373 return spacing, max(0, int(start_position)), overflow 

+

374 

+

375 

+

376class JustifyAlignmentHandler(AlignmentHandler): 

+

377 """Handler for justified text with full justification.""" 

+

378 

+

379 def __init__(self): 

+

380 # The per-gap spacings are described by a plan rather than stored outright, 

+

381 # and materialised on demand by the _gap_spacings property below. Fitting a 

+

382 # line calls this handler once per candidate word and only ever looks at the 

+

383 # first gap; building the whole list on each of those probes made adding n 

+

384 # words to a line O(n^2). Only render() reads the full list. 

+

385 self._gap_uniform: Optional[int] = None 

+

386 self._gap_residual: int = 0 

+

387 self._gap_count: int = 0 

+

388 self._gap_cache: Optional[List[int]] = [] 

+

389 

+

390 @property 

+

391 def _gap_spacings(self) -> List[int]: 

+

392 """The spacing to apply at each gap, left to right.""" 

+

393 if self._gap_cache is None: 

+

394 if self._gap_uniform is not None: 394 ↛ 395line 394 didn't jump to line 395 because the condition on line 394 was never true

+

395 self._gap_cache = [self._gap_uniform] * self._gap_count 

+

396 else: 

+

397 self._gap_cache = self._distribute(self._gap_residual, self._gap_count) 

+

398 return self._gap_cache 

+

399 

+

400 @staticmethod 

+

401 def _distribute(total: int, num_gaps: int) -> List[int]: 

+

402 """Split `total` pixels across `num_gaps` gaps by cumulative rounding.""" 

+

403 gaps = [] 

+

404 placed = 0 

+

405 for i in range(1, num_gaps + 1): 

+

406 cumulative = int(round(total * i / num_gaps)) 

+

407 gaps.append(cumulative - placed) 

+

408 placed = cumulative 

+

409 return gaps 

+

410 

+

411 def calculate_spacing_and_position(self, text_objects: List['Text'], 

+

412 available_width: int, min_spacing: int, 

+

413 max_spacing: int, 

+

414 natural_spacing: Optional[int] = None, 

+

415 total_width: Optional[float] = None 

+

416 ) -> Tuple[int, int, bool]: 

+

417 """ 

+

418 Justified alignment distributes space to fill the entire line width. 

+

419 

+

420 natural_spacing is ignored: filling the measure is the whole point. 

+

421 

+

422 For justified text, we ALWAYS try to fill the entire width by distributing 

+

423 space between words, regardless of max_spacing constraints. The only limit 

+

424 is min_spacing to ensure readability. 

+

425 """ 

+

426 

+

427 word_length = (sum([word.width for word in text_objects]) 

+

428 if total_width is None else total_width) 

+

429 residual_space = available_width - word_length 

+

430 num_gaps = max(1, len(text_objects) - 1) 

+

431 

+

432 # Check if we have enough space for minimum spacing 

+

433 if residual_space // num_gaps < min_spacing: 

+

434 # Not enough space - this is overflow 

+

435 self._gap_uniform = min_spacing 

+

436 self._gap_count = num_gaps 

+

437 self._gap_cache = None 

+

438 return min_spacing, 0, True 

+

439 

+

440 # Distribute the residual by cumulative rounding rather than by taking a 

+

441 # floor per gap and scattering the remainder. Word widths are fractional, 

+

442 # so flooring each gap loses part of a pixel and truncating the remainder 

+

443 # loses up to another - the line then stops one or two pixels short of the 

+

444 # margin, and by a different amount on each line, which is visible as a 

+

445 # ragged right edge on otherwise justified text. Rounding the running 

+

446 # total makes the gaps sum to the residual exactly. 

+

447 total = int(round(residual_space)) 

+

448 self._gap_uniform = None 

+

449 self._gap_residual = total 

+

450 self._gap_count = num_gaps 

+

451 self._gap_cache = None 

+

452 

+

453 # The first gap is the whole of the plan that fitting needs, and it falls 

+

454 # out of the same cumulative rounding as _distribute would give it. 

+

455 return int(round(total / num_gaps)), 0, False 

+

456 

+

457 

+

458class Text(Renderable, Queriable): 

+

459 """ 

+

460 Concrete implementation for rendering text. 

+

461 This class handles the visual representation of text fragments. 

+

462 """ 

+

463 

+

464 def __init__( 

+

465 self, 

+

466 text: str, 

+

467 style: Font, 

+

468 draw: ImageDraw.Draw, 

+

469 source: Optional[Word] = None, 

+

470 line: Optional[Line] = None): 

+

471 """ 

+

472 Initialize a Text object. 

+

473 

+

474 Args: 

+

475 text: The text content to render 

+

476 style: The font style to use for rendering 

+

477 """ 

+

478 super().__init__() 

+

479 self._text = text 

+

480 self._style = style 

+

481 self._line = line 

+

482 self._source = source 

+

483 self._origin = np.array([0, 0]) 

+

484 self._draw = draw 

+

485 

+

486 # Calculate dimensions 

+

487 self._calculate_dimensions() 

+

488 

+

489 def _calculate_dimensions(self): 

+

490 """Calculate the width and height of the text based on the font metrics""" 

+

491 # Measuring a word costs a FreeType shaping pass, and the same words recur 

+

492 # constantly within a document, so results are cached per (font, string). 

+

493 # The draw's image mode is part of the key because PIL derives advance 

+

494 # widths differently for bilevel ("1") targets. 

+

495 font = self._style.font 

+

496 key = (font, self._text, self._draw.mode) 

+

497 

+

498 width = _width_cache.get(key) 

+

499 if width is None: 

+

500 width = self._draw.textlength(self._text, font=font) 

+

501 _width_cache.put(key, width) 

+

502 self._width = width 

+

503 

+

504 ascent, descent = font.getmetrics() 

+

505 self._ascent = ascent 

+

506 self._middle_y = ascent - descent / 2 

+

507 

+

508 @classmethod 

+

509 def from_word(cls, word: Word, draw: ImageDraw.Draw): 

+

510 return cls(word.text, word.style, draw) 

+

511 

+

512 @property 

+

513 def text(self) -> str: 

+

514 """Get the text content""" 

+

515 return self._text 

+

516 

+

517 @property 

+

518 def style(self) -> Font: 

+

519 """Get the text style""" 

+

520 return self._style 

+

521 

+

522 @property 

+

523 def origin(self) -> np.ndarray: 

+

524 """Get the origin of the text""" 

+

525 return self._origin 

+

526 

+

527 @property 

+

528 def line(self) -> Optional[Line]: 

+

529 """Get the line containing this text""" 

+

530 return self._line 

+

531 

+

532 @line.setter 

+

533 def line(self, line): 

+

534 """Set the line containing this text""" 

+

535 self._line = line 

+

536 

+

537 @property 

+

538 def width(self) -> int: 

+

539 """Get the width of the text""" 

+

540 return self._width 

+

541 

+

542 @property 

+

543 def size(self) -> int: 

+

544 """Get the width and height of the text""" 

+

545 # Return actual rendered height (ascent + descent) not just font_size 

+

546 ascent, descent = self._style.font.getmetrics() 

+

547 actual_height = ascent + descent 

+

548 return np.array((self._width, actual_height)) 

+

549 

+

550 def set_origin(self, origin: np.generic): 

+

551 """Set the origin (left baseline ("ls")) of this text element""" 

+

552 self._origin = origin 

+

553 

+

554 def add_line(self, line): 

+

555 """Add this text to a line""" 

+

556 self._line = line 

+

557 

+

558 def in_object(self, point: np.generic): 

+

559 """ 

+

560 Check if a point is in the text object. 

+

561 

+

562 Override Queriable.in_object() because Text uses baseline-anchored positioning. 

+

563 The origin is at the baseline (anchor="ls"), not the top-left corner. 

+

564 

+

565 Args: 

+

566 point: The coordinates to check 

+

567 

+

568 Returns: 

+

569 True if the point is within the text bounds 

+

570 """ 

+

571 point_array = np.array(point) 

+

572 

+

573 # Text origin is at baseline, so visual top is origin[1] - ascent 

+

574 visual_top = self._origin[1] - self._ascent 

+

575 visual_bottom = self._origin[1] + (self.size[1] - self._ascent) 

+

576 

+

577 # Check if point is within bounds 

+

578 # X: origin[0] to origin[0] + width 

+

579 # Y: visual_top to visual_bottom 

+

580 return (self._origin[0] <= point_array[0] < self._origin[0] + self.size[0] and 

+

581 visual_top <= point_array[1] < visual_bottom) 

+

582 

+

583 def _apply_decoration(self, next_text: Optional['Text'] = None, spacing: int = 0): 

+

584 """ 

+

585 Apply text decoration (underline or strikethrough). 

+

586 

+

587 Args: 

+

588 next_text: The next Text object in the line (if any) 

+

589 spacing: The spacing to the next text object 

+

590 """ 

+

591 if self._style.decoration == TextDecoration.UNDERLINE: 591 ↛ 609line 591 didn't jump to line 609 because the condition on line 591 was always true

+

592 # Draw underline at about 90% of the height 

+

593 y_position = self._origin[1] - 0.1 * self._style.font_size 

+

594 line_width = max(1, int(self._style.font_size / 15)) 

+

595 

+

596 # Determine end x-coordinate 

+

597 end_x = self._origin[0] + self._width 

+

598 

+

599 # If next text also has underline decoration, extend to connect them 

+

600 if (next_text is not None and 

+

601 next_text.style.decoration == TextDecoration.UNDERLINE and 

+

602 next_text.style.colour == self._style.colour): 

+

603 # Extend the underline through the spacing to connect with next word 

+

604 end_x += spacing 

+

605 

+

606 self._draw.line([(self._origin[0], y_position), (end_x, y_position)], 

+

607 fill=self._style.colour, width=line_width) 

+

608 

+

609 elif self._style.decoration == TextDecoration.STRIKETHROUGH: 

+

610 # Draw strikethrough at about 50% of the height 

+

611 y_position = self._origin[1] + self._middle_y 

+

612 line_width = max(1, int(self._style.font_size / 15)) 

+

613 

+

614 # Determine end x-coordinate 

+

615 end_x = self._origin[0] + self._width 

+

616 

+

617 # If next text also has strikethrough decoration, extend to connect them 

+

618 if (next_text is not None and 

+

619 next_text.style.decoration == TextDecoration.STRIKETHROUGH and 

+

620 next_text.style.colour == self._style.colour): 

+

621 # Extend the strikethrough through the spacing to connect with next word 

+

622 end_x += spacing 

+

623 

+

624 self._draw.line([(self._origin[0], y_position), (end_x, y_position)], 

+

625 fill=self._style.colour, width=line_width) 

+

626 

+

627 def render(self, next_text: Optional['Text'] = None, spacing: int = 0): 

+

628 """ 

+

629 Render the text to an image. 

+

630 

+

631 Args: 

+

632 next_text: The next Text object in the line (if any) 

+

633 spacing: The spacing to the next text object 

+

634 

+

635 Returns: 

+

636 A PIL Image containing the rendered text 

+

637 """ 

+

638 

+

639 style = self._style 

+

640 

+

641 # Draw the text background if specified 

+

642 if style.background and style.background[3] > 0: # If alpha > 0 642 ↛ 643line 642 didn't jump to line 643 because the condition on line 642 was never true

+

643 self._draw.rectangle([tuple(self._origin), tuple(self._origin + self.size)], 

+

644 fill=style.background) 

+

645 

+

646 # Draw the text using baseline as anchor point ("ls" = left-baseline) 

+

647 # This ensures the origin represents the baseline, not the top-left 

+

648 if not self._render_from_glyph_cache(style): 648 ↛ 649line 648 didn't jump to line 649 because the condition on line 648 was never true

+

649 self._draw.text( 

+

650 (self.origin[0], 

+

651 self._origin[1]), 

+

652 self._text, 

+

653 font=style.font, 

+

654 fill=style.colour, 

+

655 anchor="ls") 

+

656 

+

657 # Apply any text decorations with knowledge of next text 

+

658 if style.decoration != TextDecoration.NONE: 

+

659 self._apply_decoration(next_text, spacing) 

+

660 

+

661 def _render_from_glyph_cache(self, style) -> bool: 

+

662 """ 

+

663 Blit this word from the cached glyph bitmap. 

+

664 

+

665 Rasterising a word is the single most expensive step in drawing a page, and 

+

666 the same words recur constantly, so the bitmap PIL would produce is cached 

+

667 and blitted directly. This reproduces what ImageDraw.text() does internally 

+

668 (getmask2 followed by draw_bitmap) minus the per-call setup. 

+

669 

+

670 Returns: 

+

671 True if the word was drawn. False means the caller must fall back to 

+

672 ImageDraw.text(). 

+

673 """ 

+

674 global _glyph_fast_path_available 

+

675 

+

676 if not _glyph_fast_path_available: 676 ↛ 677line 676 didn't jump to line 677 because the condition on line 676 was never true

+

677 return False 

+

678 

+

679 draw = self._draw 

+

680 font = style.font 

+

681 

+

682 # Bitmap and other non-FreeType fonts do not expose getmask2's anchor and 

+

683 # sub-pixel arguments; let PIL handle them. 

+

684 if not isinstance(font, ImageFont.FreeTypeFont): 684 ↛ 685line 684 didn't jump to line 685 because the condition on line 684 was never true

+

685 return False 

+

686 

+

687 try: 

+

688 ink, _ = draw._getink(style.colour) 

+

689 if ink is None: 689 ↛ 690line 689 didn't jump to line 690 because the condition on line 689 was never true

+

690 return False 

+

691 

+

692 # floor() rather than modf() so the fraction is always in [0, 1), 

+

693 # keeping bucket indices non-negative for negative coordinates. 

+

694 x = float(self._origin[0]) 

+

695 y = float(self._origin[1]) 

+

696 x_whole = math.floor(x) 

+

697 y_whole = math.floor(y) 

+

698 

+

699 steps = _glyph_subpixel_steps 

+

700 x_bucket = int((x - x_whole) * steps) 

+

701 y_bucket = int((y - y_whole) * steps) 

+

702 

+

703 mode = draw.fontmode 

+

704 key = (font, self._text, mode, ink, x_bucket, y_bucket) 

+

705 

+

706 entry = _glyph_cache.get(key) 

+

707 if entry is None: 

+

708 entry = font.getmask2( 

+

709 self._text, mode, anchor="ls", ink=ink, 

+

710 start=(x_bucket / steps, y_bucket / steps)) 

+

711 _glyph_cache.put(key, entry) 

+

712 

+

713 mask, offset = entry 

+

714 draw.draw.draw_bitmap((x_whole + offset[0], y_whole + offset[1]), mask, ink) 

+

715 return True 

+

716 

+

717 except AttributeError: 

+

718 # A PIL build without the internals this path relies on. Stop trying. 

+

719 logger.warning( 

+

720 "Glyph cache unavailable for this Pillow build; falling back to " 

+

721 "ImageDraw.text() for all text rendering.", exc_info=True) 

+

722 _glyph_fast_path_available = False 

+

723 return False 

+

724 except (TypeError, ValueError): 

+

725 # This particular colour/mode combination is not supported by the fast 

+

726 # path (e.g. an ink PIL cannot resolve). Others may still be. 

+

727 return False 

+

728 

+

729 

+

730class Line(Box): 

+

731 """ 

+

732 A line of text consisting of Text objects with consistent spacing. 

+

733 Each Text represents a word or word fragment that can be rendered. 

+

734 """ 

+

735 

+

736 def __init__(self, 

+

737 spacing: Tuple[int, 

+

738 int], 

+

739 origin, 

+

740 size, 

+

741 draw: ImageDraw.Draw, 

+

742 font: Optional[Font] = None, 

+

743 callback=None, 

+

744 sheet=None, 

+

745 mode=None, 

+

746 halign=Alignment.CENTER, 

+

747 valign=Alignment.CENTER, 

+

748 previous=None, 

+

749 min_word_length_for_brute_force: int = 8, 

+

750 min_chars_before_hyphen: int = 2, 

+

751 min_chars_after_hyphen: int = 2): 

+

752 """ 

+

753 Initialize a new line. 

+

754 

+

755 Args: 

+

756 spacing: A tuple of (min_spacing, max_spacing) between words 

+

757 origin: The top-left position of the line 

+

758 size: The width and height of the line 

+

759 font: The default font to use for text in this line 

+

760 callback: Optional callback function 

+

761 sheet: Optional image sheet 

+

762 mode: Optional image mode 

+

763 halign: Horizontal alignment of text within the line 

+

764 valign: Vertical alignment of text within the line 

+

765 previous: Reference to the previous line 

+

766 min_word_length_for_brute_force: Minimum word length to attempt brute force hyphenation (default: 8) 

+

767 min_chars_before_hyphen: Minimum characters before hyphen in any split (default: 2) 

+

768 min_chars_after_hyphen: Minimum characters after hyphen in any split (default: 2) 

+

769 """ 

+

770 super().__init__(origin, size, callback, sheet, mode, halign, valign) 

+

771 self._text_objects: List['Text'] = [] # Store Text objects directly 

+

772 # Prefix sums of the widths in _text_objects, kept in step by _push_text / 

+

773 # _pop_text. Element 0 is the empty sum. See _push_text for the rationale. 

+

774 self._width_prefix: List[float] = [0.0] 

+

775 self._spacing = spacing # (min_spacing, max_spacing) 

+

776 self._font = font if font else Font() # Use default font if none provided 

+

777 self._current_width = 0 # Track the current width used 

+

778 self._words: List['Word'] = [] 

+

779 self._previous = previous 

+

780 self._next = None 

+

781 ascent, descent = self._font.font.getmetrics() 

+

782 # Store baseline as offset from line origin (top), not absolute position 

+

783 self._baseline = ascent 

+

784 self._draw = draw 

+

785 self._spacing_render = (spacing[0] + spacing[1]) // 2 

+

786 self._position_render = 0 

+

787 

+

788 # The font's own space advance. Ragged alignments use this as their 

+

789 # constant word gap rather than stretching to fill the measure. 

+

790 self._natural_spacing = _space_advance(self._font.font) 

+

791 

+

792 # Hyphenation configuration parameters 

+

793 self._min_word_length_for_brute_force = min_word_length_for_brute_force 

+

794 self._min_chars_before_hyphen = min_chars_before_hyphen 

+

795 self._min_chars_after_hyphen = min_chars_after_hyphen 

+

796 

+

797 # Create the appropriate alignment handler 

+

798 self._alignment_handler = self._create_alignment_handler(halign) 

+

799 

+

800 # Set on the final line of a paragraph. Justification stretches a line to 

+

801 # fill the column, which is wrong for the last line - a three-word tail 

+

802 # would be spread across the full measure. The last line takes its 

+

803 # natural width instead, as in every other typesetting system. 

+

804 self._is_paragraph_end = False 

+

805 

+

806 @property 

+

807 def is_paragraph_end(self) -> bool: 

+

808 """Whether this is the final line of its paragraph""" 

+

809 return self._is_paragraph_end 

+

810 

+

811 @is_paragraph_end.setter 

+

812 def is_paragraph_end(self, value: bool): 

+

813 self._is_paragraph_end = value 

+

814 

+

815 @property 

+

816 def render_alignment_handler(self) -> AlignmentHandler: 

+

817 """ 

+

818 The handler used to position text when rendering. 

+

819 

+

820 This differs from the fitting handler only for the last line of a 

+

821 justified paragraph, which is rendered flush left. 

+

822 """ 

+

823 if self._is_paragraph_end and isinstance( 

+

824 self._alignment_handler, JustifyAlignmentHandler): 

+

825 return LeftAlignmentHandler() 

+

826 return self._alignment_handler 

+

827 

+

828 def _create_alignment_handler(self, alignment: Alignment) -> AlignmentHandler: 

+

829 """ 

+

830 Create the appropriate alignment handler based on the alignment type. 

+

831 

+

832 Args: 

+

833 alignment: The alignment type 

+

834 

+

835 Returns: 

+

836 The appropriate alignment handler instance 

+

837 """ 

+

838 if alignment == Alignment.LEFT: 

+

839 return LeftAlignmentHandler() 

+

840 elif alignment == Alignment.JUSTIFY: 

+

841 return JustifyAlignmentHandler() 

+

842 else: # CENTER or RIGHT 

+

843 return CenterRightAlignmentHandler(alignment) 

+

844 

+

845 @property 

+

846 def text_objects(self) -> List[Text]: 

+

847 """Get the list of Text objects in this line""" 

+

848 return self._text_objects 

+

849 

+

850 def set_next(self, line: Line): 

+

851 """Set the next line in sequence""" 

+

852 self._next = line 

+

853 

+

854 @property 

+

855 def _content_width(self) -> float: 

+

856 """Summed width of the line's current contents.""" 

+

857 return self._width_prefix[-1] 

+

858 

+

859 def _push_text(self, text: 'Text'): 

+

860 """ 

+

861 Append a Text to the line, keeping the running width sum in step. 

+

862 

+

863 Fitting a word is a trial: the candidate is pushed, measured, and popped 

+

864 again if it did not fit, so the line's contents churn far more often than 

+

865 they grow. Tracking the sum here rather than re-adding every width on each 

+

866 measurement is what keeps filling a line linear in its word count. 

+

867 

+

868 The sum is kept as a prefix list rather than as one accumulator that is 

+

869 added to and subtracted from. Widths are floats, so `(total + w) - w` need 

+

870 not give back `total` exactly, and a drift of one ulp is enough to flip an 

+

871 overflow decision on a line that ends flush. Truncating a prefix list 

+

872 restores the earlier total bit for bit, and each entry is built by the same 

+

873 left-to-right addition sum() would perform. 

+

874 """ 

+

875 self._text_objects.append(text) 

+

876 self._width_prefix.append(self._width_prefix[-1] + text.width) 

+

877 

+

878 def _pop_text(self) -> 'Text': 

+

879 """Remove the last Text from the line, keeping the width sum in step.""" 

+

880 text = self._text_objects.pop() 

+

881 self._width_prefix.pop() 

+

882 return text 

+

883 

+

884 def _measure(self, handler: Optional[AlignmentHandler] = None 

+

885 ) -> Tuple[int, int, bool]: 

+

886 """Ask an alignment handler to place the line's current contents.""" 

+

887 if handler is None: 

+

888 handler = self._alignment_handler 

+

889 return handler.calculate_spacing_and_position( 

+

890 self._text_objects, self._size[0], self._spacing[0], self._spacing[1], 

+

891 self._natural_spacing, self._content_width) 

+

892 

+

893 def add_word(self, 

+

894 word: 'Word', 

+

895 part: Optional[Text] = None) -> Tuple[bool, 

+

896 Optional['Text']]: 

+

897 """ 

+

898 Add a word to this line using intelligent word fitting strategies. 

+

899 

+

900 Args: 

+

901 word: The word to add to the line 

+

902 part: Optional pretext from a previous hyphenated word 

+

903 

+

904 Returns: 

+

905 Tuple of (success, overflow_text): 

+

906 - success: True if word/part was added, False if it couldn't fit 

+

907 - overflow_text: Remaining text if word was hyphenated, None otherwise 

+

908 """ 

+

909 # First, add any pretext from previous hyphenation 

+

910 if part is not None: 

+

911 self._push_text(part) 

+

912 self._words.append(word) 

+

913 part.add_line(self) 

+

914 

+

915 # Try to add the full word - create LinkText for LinkedWord, regular Text 

+

916 # otherwise 

+

917 if isinstance(word, LinkedWord): 

+

918 # Import here to avoid circular dependency 

+

919 from .functional import LinkText 

+

920 # Create a LinkText which includes the link functionality 

+

921 # LinkText constructor needs: (link, text, font, draw, source, line) 

+

922 # But LinkedWord itself contains the link properties 

+

923 # We'll create a Link object from the LinkedWord properties 

+

924 link = Link( 

+

925 location=word.location, 

+

926 link_type=word.link_type, 

+

927 callback=word.link_callback, 

+

928 params=word.params, 

+

929 title=word.link_title 

+

930 ) 

+

931 text = LinkText( 

+

932 link, 

+

933 word.text, 

+

934 word.style, 

+

935 self._draw, 

+

936 source=word, 

+

937 line=self) 

+

938 else: 

+

939 text = Text.from_word(word, self._draw) 

+

940 self._push_text(text) 

+

941 spacing, position, overflow = self._measure() 

+

942 

+

943 if not overflow: 

+

944 # Word fits! Add it completely 

+

945 self._words.append(word) 

+

946 word.add_concete(text) 

+

947 text.add_line(self) 

+

948 self._position_render = position 

+

949 self._spacing_render = spacing 

+

950 return True, None 

+

951 

+

952 # Word doesn't fit, remove it and try hyphenation 

+

953 self._pop_text() 

+

954 

+

955 # Step 1: Try pyphen hyphenation 

+

956 pyphen_splits = word.possible_hyphenation() 

+

957 valid_splits = [] 

+

958 

+

959 if pyphen_splits: 

+

960 # Create Text objects for each possible split and check if they fit 

+

961 for pair in pyphen_splits: 

+

962 first_part_text = pair[0] + "-" 

+

963 second_part_text = pair[1] 

+

964 

+

965 # Validate minimum character requirements 

+

966 if len(pair[0]) < self._min_chars_before_hyphen: 966 ↛ 967line 966 didn't jump to line 967 because the condition on line 966 was never true

+

967 continue 

+

968 if len(pair[1]) < self._min_chars_after_hyphen: 968 ↛ 969line 968 didn't jump to line 969 because the condition on line 968 was never true

+

969 continue 

+

970 

+

971 # Create Text objects 

+

972 first_text = Text( 

+

973 first_part_text, 

+

974 word.style, 

+

975 self._draw, 

+

976 line=self, 

+

977 source=word) 

+

978 second_text = Text( 

+

979 second_part_text, 

+

980 word.style, 

+

981 self._draw, 

+

982 line=self, 

+

983 source=word) 

+

984 

+

985 # Check if first part fits 

+

986 self._push_text(first_text) 

+

987 spacing, position, overflow = self._measure() 

+

988 self._pop_text() 

+

989 

+

990 if not overflow: 

+

991 # This split fits! Add it to valid options 

+

992 valid_splits.append((first_text, second_text, spacing, position)) 

+

993 

+

994 # Step 2: If we have valid pyphen splits, choose the best one 

+

995 if valid_splits: 

+

996 # Select the split with the best (minimum) spacing 

+

997 best_split = min(valid_splits, key=lambda x: x[2]) 

+

998 first_text, second_text, spacing, position = best_split 

+

999 

+

1000 # Apply the split 

+

1001 self._push_text(first_text) 

+

1002 first_text.line = self 

+

1003 word.add_concete((first_text, second_text)) 

+

1004 self._spacing_render = spacing 

+

1005 self._position_render = position 

+

1006 self._words.append(word) 

+

1007 return True, second_text 

+

1008 

+

1009 # Step 3: Try brute force hyphenation (only for long words) 

+

1010 if len(word.text) >= self._min_word_length_for_brute_force: 

+

1011 # Calculate available space for the word 

+

1012 word_length = self._content_width 

+

1013 spacing_length = self._spacing[0] * max(0, len(self._text_objects) - 1) 

+

1014 remaining = self._size[0] - word_length - spacing_length 

+

1015 

+

1016 if remaining > 0: 1016 ↛ 1073line 1016 didn't jump to line 1073 because the condition on line 1016 was always true

+

1017 # Create a hyphenated version to measure 

+

1018 test_text = Text(word.text + "-", word.style, self._draw) 

+

1019 

+

1020 if test_text.width > 0: 1020 ↛ 1073line 1020 didn't jump to line 1073 because the condition on line 1020 was always true

+

1021 # Calculate what fraction of the hyphenated word fits 

+

1022 fraction = remaining / test_text.width 

+

1023 

+

1024 # Convert fraction to character position 

+

1025 # We need at least min_chars_before_hyphen and leave at least 

+

1026 # min_chars_after_hyphen 

+

1027 max_split_pos = len(word.text) - self._min_chars_after_hyphen 

+

1028 min_split_pos = self._min_chars_before_hyphen 

+

1029 

+

1030 # Calculate ideal split position based on available space 

+

1031 ideal_split = int(fraction * len(word.text)) 

+

1032 split_pos = max(min_split_pos, min(ideal_split, max_split_pos)) 

+

1033 

+

1034 # Ensure we meet minimum requirements 

+

1035 if (split_pos >= self._min_chars_before_hyphen and 1035 ↛ 1073line 1035 didn't jump to line 1073 because the condition on line 1035 was always true

+

1036 len(word.text) - split_pos >= self._min_chars_after_hyphen): 

+

1037 

+

1038 # Create the split 

+

1039 first_part_text = word.text[:split_pos] + "-" 

+

1040 second_part_text = word.text[split_pos:] 

+

1041 

+

1042 first_text = Text( 

+

1043 first_part_text, 

+

1044 word.style, 

+

1045 self._draw, 

+

1046 line=self, 

+

1047 source=word) 

+

1048 second_text = Text( 

+

1049 second_part_text, 

+

1050 word.style, 

+

1051 self._draw, 

+

1052 line=self, 

+

1053 source=word) 

+

1054 

+

1055 # Verify the first part actually fits 

+

1056 self._push_text(first_text) 

+

1057 spacing, position, overflow = self._measure() 

+

1058 

+

1059 if not overflow: 

+

1060 # Brute force split works! 

+

1061 first_text.line = self 

+

1062 second_text.line = self 

+

1063 word.add_concete((first_text, second_text)) 

+

1064 self._spacing_render = spacing 

+

1065 self._position_render = position 

+

1066 self._words.append(word) 

+

1067 return True, second_text 

+

1068 else: 

+

1069 # Doesn't fit, remove it 

+

1070 self._pop_text() 

+

1071 

+

1072 # Step 4: Word cannot be hyphenated or split, move to next line 

+

1073 return False, None 

+

1074 

+

1075 def render(self): 

+

1076 """ 

+

1077 Render the line with all its text objects using the alignment handler system. 

+

1078 

+

1079 Returns: 

+

1080 A PIL Image containing the rendered line 

+

1081 """ 

+

1082 # Recalculate spacing and position for current text objects to ensure 

+

1083 # accuracy. Word fitting used the paragraph's alignment; rendering uses 

+

1084 # render_alignment_handler, which differs only for the last line of a 

+

1085 # justified paragraph. 

+

1086 handler = self.render_alignment_handler 

+

1087 if len(self._text_objects) > 0: 

+

1088 spacing, position, overflow = self._measure(handler) 

+

1089 self._spacing_render = spacing 

+

1090 self._position_render = position 

+

1091 

+

1092 y_cursor = self._origin[1] + self._baseline 

+

1093 

+

1094 # Start x_cursor at line origin plus any alignment offset 

+

1095 x_cursor = self._origin[0] + self._position_render 

+

1096 

+

1097 # Everything the loop needs that does not vary per word is resolved once. 

+

1098 # Only justified lines carry per-gap spacings; every other alignment uses 

+

1099 # the single spacing figured above. 

+

1100 texts = self._text_objects 

+

1101 last = len(texts) - 1 

+

1102 draw = self._draw 

+

1103 default_spacing = self._spacing_render 

+

1104 gaps = handler._gap_spacings if isinstance(handler, JustifyAlignmentHandler) else () 

+

1105 gap_count = len(gaps) 

+

1106 

+

1107 for i, text in enumerate(texts): 

+

1108 # Update text draw context to current draw context 

+

1109 text._draw = draw 

+

1110 text.set_origin(np.array([x_cursor, y_cursor])) 

+

1111 

+

1112 # Determine next text object for continuous decoration 

+

1113 next_text = texts[i + 1] if i < last else None 

+

1114 

+

1115 # Get the spacing for this specific gap (variable for justified text) 

+

1116 current_spacing = gaps[i] if i < gap_count else default_spacing 

+

1117 

+

1118 # Render with next text information for continuous underline/strikethrough 

+

1119 text.render(next_text, current_spacing) 

+

1120 # Add text width, then spacing only if there are more words 

+

1121 x_cursor += text.width 

+

1122 if i < last: 

+

1123 x_cursor += current_spacing 

+

1124 

+

1125 def query_point(self, point: Tuple[int, int]) -> Optional['QueryResult']: 

+

1126 """ 

+

1127 Find which Text object contains the given point. 

+

1128 Uses Queriable.in_object() mixin for hit-testing. 

+

1129 

+

1130 Args: 

+

1131 point: (x, y) coordinates to query 

+

1132 

+

1133 Returns: 

+

1134 QueryResult from the text object at that point, or None 

+

1135 """ 

+

1136 point_array = np.array(point) 

+

1137 

+

1138 # Check each text object in this line 

+

1139 for text_obj in self._text_objects: 

+

1140 # Use Queriable mixin's in_object() for hit-testing 

+

1141 if isinstance(text_obj, Queriable) and text_obj.in_object(point_array): 

+

1142 # Extract metadata based on text type 

+

1143 origin = text_obj._origin 

+

1144 size = text_obj.size 

+

1145 

+

1146 # Text origin is at baseline (anchor="ls"), so visual top is origin[1] - ascent 

+

1147 # Bounds should be (x, visual_top, width, height) for proper 

+

1148 # highlighting 

+

1149 visual_top = int(origin[1] - text_obj._ascent) 

+

1150 bounds = ( 

+

1151 int(origin[0]), 

+

1152 visual_top, 

+

1153 int(size[0]) if hasattr(size, '__getitem__') else 0, 

+

1154 int(size[1]) if hasattr(size, '__getitem__') else 0 

+

1155 ) 

+

1156 

+

1157 # Import here to avoid circular dependency 

+

1158 from .functional import LinkText, ButtonText 

+

1159 

+

1160 if isinstance(text_obj, LinkText): 

+

1161 result = QueryResult( 

+

1162 object=text_obj, 

+

1163 object_type="link", 

+

1164 bounds=bounds, 

+

1165 text=text_obj._text, 

+

1166 is_interactive=True, 

+

1167 link_target=text_obj._link.location if hasattr( 

+

1168 text_obj, 

+

1169 '_link') else None) 

+

1170 elif isinstance(text_obj, ButtonText): 1170 ↛ 1171line 1170 didn't jump to line 1171 because the condition on line 1170 was never true

+

1171 result = QueryResult( 

+

1172 object=text_obj, 

+

1173 object_type="button", 

+

1174 bounds=bounds, 

+

1175 text=text_obj._text, 

+

1176 is_interactive=True, 

+

1177 callback=text_obj._callback if hasattr( 

+

1178 text_obj, 

+

1179 '_callback') else None) 

+

1180 else: 

+

1181 result = QueryResult( 

+

1182 object=text_obj, 

+

1183 object_type="text", 

+

1184 bounds=bounds, 

+

1185 text=text_obj._text if hasattr(text_obj, '_text') else None 

+

1186 ) 

+

1187 

+

1188 result.parent_line = self 

+

1189 return result 

+

1190 

+

1191 return None 

+
+ + + diff --git a/cov_info/htmlcov/z_af715639580e2d86___init___py.html b/cov_info/htmlcov/z_af715639580e2d86___init___py.html new file mode 100644 index 0000000..06a7d14 --- /dev/null +++ b/cov_info/htmlcov/z_af715639580e2d86___init___py.html @@ -0,0 +1,121 @@ + + + + + Coverage for pyWebLayout/abstract/__init__.py: 100% + + + + + +
+
+

+ Coverage for pyWebLayout/abstract/__init__.py: + 100% +

+ +

+ 5 statements   + + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.15.4, + created at 2026-08-08 20:34 +0000 +

+ +
+
+
+

1""" 

+

2Abstract layer for the pyWebLayout library. 

+

3 

+

4This package contains abstract representations of document elements that are 

+

5independent of rendering specifics. 

+

6""" 

+

7 

+

8from .inline import Word, FormattedSpan 

+

9from .block import Paragraph, Heading, Image, HeadingLevel 

+

10from .document import Document 

+

11from .functional import LinkType 

+

12 

+

13__all__ = [ 

+

14 'Word', 

+

15 'FormattedSpan', 

+

16 'Paragraph', 

+

17 'Heading', 

+

18 'Image', 

+

19 'HeadingLevel', 

+

20 'Document', 

+

21 'LinkType', 

+

22] 

+
+ + + diff --git a/cov_info/htmlcov/z_af715639580e2d86_block_py.html b/cov_info/htmlcov/z_af715639580e2d86_block_py.html new file mode 100644 index 0000000..43757b7 --- /dev/null +++ b/cov_info/htmlcov/z_af715639580e2d86_block_py.html @@ -0,0 +1,1514 @@ + + + + + Coverage for pyWebLayout/abstract/block.py: 80% + + + + + +
+
+

+ Coverage for pyWebLayout/abstract/block.py: + 80% +

+ +

+ 489 statements   + + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.15.4, + created at 2026-08-08 20:34 +0000 +

+ +
+
+
+

1from typing import List, Iterator, Tuple, Dict, Optional, Any 

+

2from enum import Enum 

+

3import os 

+

4import tempfile 

+

5import urllib.request 

+

6import urllib.parse 

+

7from PIL import Image as PILImage 

+

8from .inline import Word, FormattedSpan 

+

9from ..core import Hierarchical, Styleable, FontRegistry, ContainerAware, BlockContainer 

+

10 

+

11 

+

12class BlockType(Enum): 

+

13 """Enumeration of different block types for classification purposes""" 

+

14 PARAGRAPH = 1 

+

15 HEADING = 2 

+

16 QUOTE = 3 

+

17 CODE_BLOCK = 4 

+

18 LIST = 5 

+

19 LIST_ITEM = 6 

+

20 TABLE = 7 

+

21 TABLE_ROW = 8 

+

22 TABLE_CELL = 9 

+

23 HORIZONTAL_RULE = 10 

+

24 LINE_BREAK = 11 

+

25 IMAGE = 12 

+

26 PAGE_BREAK = 13 

+

27 

+

28 

+

29class Block(Hierarchical): 

+

30 """ 

+

31 Base class for all block-level elements. 

+

32 Block elements typically represent visual blocks of content that stack vertically. 

+

33 

+

34 Uses Hierarchical mixin for parent-child relationship management. 

+

35 """ 

+

36 

+

37 def __init__(self, block_type: BlockType): 

+

38 """ 

+

39 Initialize a block element. 

+

40 

+

41 Args: 

+

42 block_type: The type of block this element represents 

+

43 """ 

+

44 super().__init__() 

+

45 self._block_type = block_type 

+

46 

+

47 @property 

+

48 def block_type(self) -> BlockType: 

+

49 """Get the type of this block element""" 

+

50 return self._block_type 

+

51 

+

52 

+

53class Paragraph(Styleable, FontRegistry, ContainerAware, Block): 

+

54 """ 

+

55 A paragraph is a block-level element that contains a sequence of words. 

+

56 

+

57 Uses Styleable mixin for style property management. 

+

58 Uses FontRegistry mixin for font caching with parent delegation. 

+

59 """ 

+

60 

+

61 def __init__(self, style=None): 

+

62 """ 

+

63 Initialize an empty paragraph 

+

64 

+

65 Args: 

+

66 style: Optional default style for words in this paragraph 

+

67 """ 

+

68 super().__init__(style=style, block_type=BlockType.PARAGRAPH) 

+

69 self._words: List[Word] = [] 

+

70 self._spans: List[FormattedSpan] = [] 

+

71 

+

72 @classmethod 

+

73 def create_and_add_to(cls, container, style=None) -> 'Paragraph': 

+

74 """ 

+

75 Create a new Paragraph and add it to a container, inheriting style from 

+

76 the container if not explicitly provided. 

+

77 

+

78 Args: 

+

79 container: The container to add the paragraph to (must have add_block method and style property) 

+

80 style: Optional style override. If None, inherits from container 

+

81 

+

82 Returns: 

+

83 The newly created Paragraph object 

+

84 

+

85 Raises: 

+

86 AttributeError: If the container doesn't have the required add_block method 

+

87 """ 

+

88 # Validate container and inherit style using ContainerAware utilities 

+

89 cls._validate_container(container) 

+

90 style = cls._inherit_style(container, style) 

+

91 

+

92 # Create the new paragraph 

+

93 paragraph = cls(style) 

+

94 

+

95 # Add the paragraph to the container 

+

96 container.add_block(paragraph) 

+

97 

+

98 return paragraph 

+

99 

+

100 def add_word(self, word: Word): 

+

101 """ 

+

102 Add a word to this paragraph. 

+

103 

+

104 Args: 

+

105 word: The Word object to add 

+

106 """ 

+

107 self._words.append(word) 

+

108 

+

109 def create_word(self, text: str, style=None, background=None) -> Word: 

+

110 """ 

+

111 Create a new word and add it to this paragraph, inheriting paragraph's style if not specified. 

+

112 

+

113 This is a convenience method that uses Word.create_and_add_to() to create words 

+

114 that automatically inherit styling from this paragraph. 

+

115 

+

116 Args: 

+

117 text: The text content of the word 

+

118 style: Optional Font style override. If None, attempts to inherit from paragraph 

+

119 background: Optional background color override 

+

120 

+

121 Returns: 

+

122 The newly created Word object 

+

123 """ 

+

124 return Word.create_and_add_to(text, self, style, background) 

+

125 

+

126 def add_span(self, span: FormattedSpan): 

+

127 """ 

+

128 Add a formatted span to this paragraph. 

+

129 

+

130 Args: 

+

131 span: The FormattedSpan object to add 

+

132 """ 

+

133 self._spans.append(span) 

+

134 

+

135 def create_span(self, style=None, background=None) -> FormattedSpan: 

+

136 """ 

+

137 Create a new formatted span with inherited style. 

+

138 

+

139 Args: 

+

140 style: Optional Font style override. If None, inherits from paragraph 

+

141 background: Optional background color override 

+

142 

+

143 Returns: 

+

144 The newly created FormattedSpan object 

+

145 """ 

+

146 return FormattedSpan.create_and_add_to(self, style, background) 

+

147 

+

148 @property 

+

149 def words(self) -> List[Word]: 

+

150 """Get the list of words in this paragraph""" 

+

151 return self._words 

+

152 

+

153 def words_iter(self) -> Iterator[Tuple[int, Word]]: 

+

154 """ 

+

155 Iterate over the words in this paragraph. 

+

156 

+

157 Yields: 

+

158 Tuples of (index, word) for each word in the paragraph 

+

159 """ 

+

160 for i, word in enumerate(self._words): 

+

161 yield i, word 

+

162 

+

163 def spans(self) -> Iterator[FormattedSpan]: 

+

164 """ 

+

165 Iterate over the formatted spans in this paragraph. 

+

166 

+

167 Yields: 

+

168 Each FormattedSpan in the paragraph 

+

169 """ 

+

170 for span in self._spans: 

+

171 yield span 

+

172 

+

173 @property 

+

174 def word_count(self) -> int: 

+

175 """Get the number of words in this paragraph""" 

+

176 return len(self._words) 

+

177 

+

178 def __len__(self): 

+

179 return self.word_count 

+

180 

+

181 # get_or_create_font() is provided by FontRegistry mixin 

+

182 

+

183 

+

184class HeadingLevel(Enum): 

+

185 """Enumeration representing HTML heading levels (h1-h6)""" 

+

186 H1 = 1 

+

187 H2 = 2 

+

188 H3 = 3 

+

189 H4 = 4 

+

190 H5 = 5 

+

191 H6 = 6 

+

192 

+

193 

+

194class Heading(Paragraph): 

+

195 """ 

+

196 A heading element (h1, h2, h3, etc.) that contains text with a specific heading level. 

+

197 Headings inherit from Paragraph as they contain words but have additional properties. 

+

198 """ 

+

199 

+

200 def __init__(self, level: HeadingLevel = HeadingLevel.H1, style=None): 

+

201 """ 

+

202 Initialize a heading element. 

+

203 

+

204 Args: 

+

205 level: The heading level (h1-h6) 

+

206 style: Optional default style for words in this heading 

+

207 """ 

+

208 super().__init__(style) 

+

209 self._block_type = BlockType.HEADING 

+

210 self._level = level 

+

211 

+

212 @classmethod 

+

213 def create_and_add_to( 

+

214 cls, 

+

215 container, 

+

216 level: HeadingLevel = HeadingLevel.H1, 

+

217 style=None) -> 'Heading': 

+

218 """ 

+

219 Create a new Heading and add it to a container, inheriting style from 

+

220 the container if not explicitly provided. 

+

221 

+

222 Args: 

+

223 container: The container to add the heading to (must have add_block method and style property) 

+

224 level: The heading level (h1-h6) 

+

225 style: Optional style override. If None, inherits from container 

+

226 

+

227 Returns: 

+

228 The newly created Heading object 

+

229 

+

230 Raises: 

+

231 AttributeError: If the container doesn't have the required add_block method 

+

232 """ 

+

233 # Validate container and inherit style using ContainerAware utilities 

+

234 cls._validate_container(container) 

+

235 style = cls._inherit_style(container, style) 

+

236 

+

237 # Create the new heading 

+

238 heading = cls(level, style) 

+

239 

+

240 # Add the heading to the container 

+

241 container.add_block(heading) 

+

242 

+

243 return heading 

+

244 

+

245 @property 

+

246 def level(self) -> HeadingLevel: 

+

247 """Get the heading level""" 

+

248 return self._level 

+

249 

+

250 @level.setter 

+

251 def level(self, level: HeadingLevel): 

+

252 """Set the heading level""" 

+

253 self._level = level 

+

254 

+

255 

+

256class Quote(BlockContainer, ContainerAware, Block): 

+

257 """ 

+

258 A blockquote element that can contain other block elements. 

+

259 """ 

+

260 

+

261 def __init__(self, style=None): 

+

262 """ 

+

263 Initialize an empty blockquote 

+

264 

+

265 Args: 

+

266 style: Optional default style for child blocks 

+

267 """ 

+

268 super().__init__(BlockType.QUOTE) 

+

269 self._style = style 

+

270 

+

271 @classmethod 

+

272 def create_and_add_to(cls, container, style=None) -> 'Quote': 

+

273 """ 

+

274 Create a new Quote and add it to a container, inheriting style from 

+

275 the container if not explicitly provided. 

+

276 

+

277 Args: 

+

278 container: The container to add the quote to (must have add_block method and style property) 

+

279 style: Optional style override. If None, inherits from container 

+

280 

+

281 Returns: 

+

282 The newly created Quote object 

+

283 

+

284 Raises: 

+

285 AttributeError: If the container doesn't have the required add_block method 

+

286 """ 

+

287 # Validate container and inherit style using ContainerAware utilities 

+

288 cls._validate_container(container) 

+

289 style = cls._inherit_style(container, style) 

+

290 

+

291 # Create the new quote 

+

292 quote = cls(style) 

+

293 

+

294 # Add the quote to the container 

+

295 container.add_block(quote) 

+

296 

+

297 return quote 

+

298 

+

299 @property 

+

300 def style(self): 

+

301 """Get the default style for this quote""" 

+

302 return self._style 

+

303 

+

304 @style.setter 

+

305 def style(self, style): 

+

306 """Set the default style for this quote""" 

+

307 self._style = style 

+

308 

+

309 

+

310class CodeBlock(Block): 

+

311 """ 

+

312 A code block element containing pre-formatted text with syntax highlighting. 

+

313 """ 

+

314 

+

315 def __init__(self, language: str = ""): 

+

316 """ 

+

317 Initialize a code block. 

+

318 

+

319 Args: 

+

320 language: The programming language for syntax highlighting 

+

321 """ 

+

322 super().__init__(BlockType.CODE_BLOCK) 

+

323 self._language = language 

+

324 self._lines: List[str] = [] 

+

325 

+

326 @classmethod 

+

327 def create_and_add_to(cls, container, language: str = "") -> 'CodeBlock': 

+

328 """ 

+

329 Create a new CodeBlock and add it to a container. 

+

330 

+

331 Args: 

+

332 container: The container to add the code block to (must have add_block method) 

+

333 language: The programming language for syntax highlighting 

+

334 

+

335 Returns: 

+

336 The newly created CodeBlock object 

+

337 

+

338 Raises: 

+

339 AttributeError: If the container doesn't have the required add_block method 

+

340 """ 

+

341 # Create the new code block 

+

342 code_block = cls(language) 

+

343 

+

344 # Add the code block to the container 

+

345 if hasattr(container, 'add_block'): 

+

346 container.add_block(code_block) 

+

347 else: 

+

348 raise AttributeError( 

+

349 f"Container {type(container).__name__} must have an 'add_block' method" 

+

350 ) 

+

351 

+

352 return code_block 

+

353 

+

354 @property 

+

355 def language(self) -> str: 

+

356 """Get the programming language""" 

+

357 return self._language 

+

358 

+

359 @language.setter 

+

360 def language(self, language: str): 

+

361 """Set the programming language""" 

+

362 self._language = language 

+

363 

+

364 def add_line(self, line: str): 

+

365 """ 

+

366 Add a line of code to this code block. 

+

367 

+

368 Args: 

+

369 line: The line of code to add 

+

370 """ 

+

371 self._lines.append(line) 

+

372 

+

373 def lines(self) -> Iterator[Tuple[int, str]]: 

+

374 """ 

+

375 Iterate over the lines in this code block. 

+

376 

+

377 Yields: 

+

378 Tuples of (line_number, line_text) for each line 

+

379 """ 

+

380 for i, line in enumerate(self._lines): 

+

381 yield i, line 

+

382 

+

383 @property 

+

384 def line_count(self) -> int: 

+

385 """Get the number of lines in this code block""" 

+

386 return len(self._lines) 

+

387 

+

388 

+

389class ListStyle(Enum): 

+

390 """Enumeration of list styles""" 

+

391 UNORDERED = 1 # <ul> 

+

392 ORDERED = 2 # <ol> 

+

393 DEFINITION = 3 # <dl> 

+

394 

+

395 

+

396class HList(ContainerAware, Block): 

+

397 """ 

+

398 An HTML list element (ul, ol, dl). 

+

399 """ 

+

400 

+

401 def __init__(self, style: ListStyle = ListStyle.UNORDERED, default_style=None): 

+

402 """ 

+

403 Initialize a list. 

+

404 

+

405 Args: 

+

406 style: The style of list (unordered, ordered, definition) 

+

407 default_style: Optional default style for child items 

+

408 """ 

+

409 super().__init__(BlockType.LIST) 

+

410 self._style = style 

+

411 self._items: List[ListItem] = [] 

+

412 self._default_style = default_style 

+

413 

+

414 @classmethod 

+

415 def create_and_add_to( 

+

416 cls, 

+

417 container, 

+

418 style: ListStyle = ListStyle.UNORDERED, 

+

419 default_style=None) -> 'HList': 

+

420 """ 

+

421 Create a new HList and add it to a container, inheriting style from 

+

422 the container if not explicitly provided. 

+

423 

+

424 Args: 

+

425 container: The container to add the list to (must have add_block method) 

+

426 style: The style of list (unordered, ordered, definition) 

+

427 default_style: Optional default style for child items. If None, inherits from container 

+

428 

+

429 Returns: 

+

430 The newly created HList object 

+

431 

+

432 Raises: 

+

433 AttributeError: If the container doesn't have the required add_block method 

+

434 """ 

+

435 # Validate container and inherit style using ContainerAware utilities 

+

436 cls._validate_container(container) 

+

437 default_style = cls._inherit_style(container, default_style) 

+

438 

+

439 # Create the new list 

+

440 hlist = cls(style, default_style) 

+

441 

+

442 # Add the list to the container 

+

443 container.add_block(hlist) 

+

444 

+

445 return hlist 

+

446 

+

447 @property 

+

448 def style(self) -> ListStyle: 

+

449 """Get the list style""" 

+

450 return self._style 

+

451 

+

452 @style.setter 

+

453 def style(self, style: ListStyle): 

+

454 """Set the list style""" 

+

455 self._style = style 

+

456 

+

457 @property 

+

458 def default_style(self): 

+

459 """Get the default style for list items""" 

+

460 return self._default_style 

+

461 

+

462 @default_style.setter 

+

463 def default_style(self, style): 

+

464 """Set the default style for list items""" 

+

465 self._default_style = style 

+

466 

+

467 def add_item(self, item: 'ListItem'): 

+

468 """ 

+

469 Add an item to this list. 

+

470 

+

471 Args: 

+

472 item: The ListItem to add 

+

473 """ 

+

474 self._items.append(item) 

+

475 item.parent = self 

+

476 

+

477 def create_item(self, term: Optional[str] = None, style=None) -> 'ListItem': 

+

478 """ 

+

479 Create a new list item and add it to this list. 

+

480 

+

481 Args: 

+

482 term: Optional term for definition lists 

+

483 style: Optional style override. If None, inherits from list 

+

484 

+

485 Returns: 

+

486 The newly created ListItem object 

+

487 """ 

+

488 return ListItem.create_and_add_to(self, term, style) 

+

489 

+

490 def items(self) -> Iterator['ListItem']: 

+

491 """ 

+

492 Iterate over the items in this list. 

+

493 

+

494 Yields: 

+

495 Each ListItem in the list 

+

496 """ 

+

497 for item in self._items: 

+

498 yield item 

+

499 

+

500 @property 

+

501 def item_count(self) -> int: 

+

502 """Get the number of items in this list""" 

+

503 return len(self._items) 

+

504 

+

505 

+

506class ListItem(BlockContainer, ContainerAware, Block): 

+

507 """ 

+

508 A list item element that can contain other block elements. 

+

509 """ 

+

510 

+

511 def __init__(self, term: Optional[str] = None, style=None): 

+

512 """ 

+

513 Initialize a list item. 

+

514 

+

515 Args: 

+

516 term: Optional term for definition lists (dt element) 

+

517 style: Optional default style for child blocks 

+

518 """ 

+

519 super().__init__(BlockType.LIST_ITEM) 

+

520 self._term = term 

+

521 self._style = style 

+

522 

+

523 @classmethod 

+

524 def create_and_add_to( 

+

525 cls, 

+

526 container, 

+

527 term: Optional[str] = None, 

+

528 style=None) -> 'ListItem': 

+

529 """ 

+

530 Create a new ListItem and add it to a container, inheriting style from 

+

531 the container if not explicitly provided. 

+

532 

+

533 Args: 

+

534 container: The container to add the list item to (must have add_item method) 

+

535 term: Optional term for definition lists (dt element) 

+

536 style: Optional style override. If None, inherits from container 

+

537 

+

538 Returns: 

+

539 The newly created ListItem object 

+

540 

+

541 Raises: 

+

542 AttributeError: If the container doesn't have the required add_item method 

+

543 """ 

+

544 # Validate container and inherit style using ContainerAware utilities 

+

545 cls._validate_container(container, required_method='add_item') 

+

546 style = cls._inherit_style(container, style) 

+

547 

+

548 # Create the new list item 

+

549 item = cls(term, style) 

+

550 

+

551 # Add the list item to the container 

+

552 container.add_item(item) 

+

553 

+

554 return item 

+

555 

+

556 @property 

+

557 def term(self) -> Optional[str]: 

+

558 """Get the definition term (for definition lists)""" 

+

559 return self._term 

+

560 

+

561 @term.setter 

+

562 def term(self, term: str): 

+

563 """Set the definition term""" 

+

564 self._term = term 

+

565 

+

566 @property 

+

567 def style(self): 

+

568 """Get the default style for this list item""" 

+

569 return self._style 

+

570 

+

571 @style.setter 

+

572 def style(self, style): 

+

573 """Set the default style for this list item""" 

+

574 self._style = style 

+

575 

+

576 

+

577class TableCell(BlockContainer, ContainerAware, Block): 

+

578 """ 

+

579 A table cell element that can contain other block elements. 

+

580 """ 

+

581 

+

582 def __init__( 

+

583 self, 

+

584 is_header: bool = False, 

+

585 colspan: int = 1, 

+

586 rowspan: int = 1, 

+

587 style=None): 

+

588 """ 

+

589 Initialize a table cell. 

+

590 

+

591 Args: 

+

592 is_header: Whether this cell is a header cell (th) or data cell (td) 

+

593 colspan: Number of columns this cell spans 

+

594 rowspan: Number of rows this cell spans 

+

595 style: Optional default style for child blocks 

+

596 """ 

+

597 super().__init__(BlockType.TABLE_CELL) 

+

598 self._is_header = is_header 

+

599 self._colspan = colspan 

+

600 self._rowspan = rowspan 

+

601 self._style = style 

+

602 

+

603 @classmethod 

+

604 def create_and_add_to(cls, container, is_header: bool = False, colspan: int = 1, 

+

605 rowspan: int = 1, style=None) -> 'TableCell': 

+

606 """ 

+

607 Create a new TableCell and add it to a container, inheriting style from 

+

608 the container if not explicitly provided. 

+

609 

+

610 Args: 

+

611 container: The container to add the cell to (must have add_cell method) 

+

612 is_header: Whether this cell is a header cell (th) or data cell (td) 

+

613 colspan: Number of columns this cell spans 

+

614 rowspan: Number of rows this cell spans 

+

615 style: Optional style override. If None, inherits from container 

+

616 

+

617 Returns: 

+

618 The newly created TableCell object 

+

619 

+

620 Raises: 

+

621 AttributeError: If the container doesn't have the required add_cell method 

+

622 """ 

+

623 # Validate container and inherit style using ContainerAware utilities 

+

624 cls._validate_container(container, required_method='add_cell') 

+

625 style = cls._inherit_style(container, style) 

+

626 

+

627 # Create the new table cell 

+

628 cell = cls(is_header, colspan, rowspan, style) 

+

629 

+

630 # Add the cell to the container 

+

631 container.add_cell(cell) 

+

632 

+

633 return cell 

+

634 

+

635 @property 

+

636 def is_header(self) -> bool: 

+

637 """Check if this is a header cell""" 

+

638 return self._is_header 

+

639 

+

640 @is_header.setter 

+

641 def is_header(self, is_header: bool): 

+

642 """Set whether this is a header cell""" 

+

643 self._is_header = is_header 

+

644 

+

645 @property 

+

646 def colspan(self) -> int: 

+

647 """Get the column span""" 

+

648 return self._colspan 

+

649 

+

650 @colspan.setter 

+

651 def colspan(self, colspan: int): 

+

652 """Set the column span""" 

+

653 self._colspan = max(1, colspan) # Ensure minimum of 1 

+

654 

+

655 @property 

+

656 def rowspan(self) -> int: 

+

657 """Get the row span""" 

+

658 return self._rowspan 

+

659 

+

660 @rowspan.setter 

+

661 def rowspan(self, rowspan: int): 

+

662 """Set the row span""" 

+

663 self._rowspan = max(1, rowspan) # Ensure minimum of 1 

+

664 

+

665 @property 

+

666 def style(self): 

+

667 """Get the default style for this table cell""" 

+

668 return self._style 

+

669 

+

670 @style.setter 

+

671 def style(self, style): 

+

672 """Set the default style for this table cell""" 

+

673 self._style = style 

+

674 

+

675 

+

676class TableRow(ContainerAware, Block): 

+

677 """ 

+

678 A table row element containing table cells. 

+

679 """ 

+

680 

+

681 def __init__(self, style=None): 

+

682 """ 

+

683 Initialize an empty table row 

+

684 

+

685 Args: 

+

686 style: Optional default style for child cells 

+

687 """ 

+

688 super().__init__(BlockType.TABLE_ROW) 

+

689 self._cells: List[TableCell] = [] 

+

690 self._style = style 

+

691 

+

692 @classmethod 

+

693 def create_and_add_to( 

+

694 cls, 

+

695 container, 

+

696 section: str = "body", 

+

697 style=None) -> 'TableRow': 

+

698 """ 

+

699 Create a new TableRow and add it to a container, inheriting style from 

+

700 the container if not explicitly provided. 

+

701 

+

702 Args: 

+

703 container: The container to add the row to (must have add_row method) 

+

704 section: The section to add the row to ("header", "body", or "footer") 

+

705 style: Optional style override. If None, inherits from container 

+

706 

+

707 Returns: 

+

708 The newly created TableRow object 

+

709 

+

710 Raises: 

+

711 AttributeError: If the container doesn't have the required add_row method 

+

712 """ 

+

713 # Validate container and inherit style using ContainerAware utilities 

+

714 cls._validate_container(container, required_method='add_row') 

+

715 style = cls._inherit_style(container, style) 

+

716 

+

717 # Create the new table row 

+

718 row = cls(style) 

+

719 

+

720 # Add the row to the container 

+

721 container.add_row(row, section) 

+

722 

+

723 return row 

+

724 

+

725 @property 

+

726 def style(self): 

+

727 """Get the default style for this table row""" 

+

728 return self._style 

+

729 

+

730 @style.setter 

+

731 def style(self, style): 

+

732 """Set the default style for this table row""" 

+

733 self._style = style 

+

734 

+

735 def add_cell(self, cell: TableCell): 

+

736 """ 

+

737 Add a cell to this row. 

+

738 

+

739 Args: 

+

740 cell: The TableCell to add 

+

741 """ 

+

742 self._cells.append(cell) 

+

743 cell.parent = self 

+

744 

+

745 def create_cell( 

+

746 self, 

+

747 is_header: bool = False, 

+

748 colspan: int = 1, 

+

749 rowspan: int = 1, 

+

750 style=None) -> TableCell: 

+

751 """ 

+

752 Create a new table cell and add it to this row. 

+

753 

+

754 Args: 

+

755 is_header: Whether this cell is a header cell 

+

756 colspan: Number of columns this cell spans 

+

757 rowspan: Number of rows this cell spans 

+

758 style: Optional style override. If None, inherits from row 

+

759 

+

760 Returns: 

+

761 The newly created TableCell object 

+

762 """ 

+

763 return TableCell.create_and_add_to(self, is_header, colspan, rowspan, style) 

+

764 

+

765 def cells(self) -> Iterator[TableCell]: 

+

766 """ 

+

767 Iterate over the cells in this row. 

+

768 

+

769 Yields: 

+

770 Each TableCell in the row 

+

771 """ 

+

772 for cell in self._cells: 

+

773 yield cell 

+

774 

+

775 @property 

+

776 def cell_count(self) -> int: 

+

777 """Get the number of cells in this row""" 

+

778 return len(self._cells) 

+

779 

+

780 

+

781class Table(ContainerAware, Block): 

+

782 """ 

+

783 A table element containing rows and cells. 

+

784 """ 

+

785 

+

786 def __init__(self, caption: Optional[str] = None, style=None): 

+

787 """ 

+

788 Initialize a table. 

+

789 

+

790 Args: 

+

791 caption: Optional caption for the table 

+

792 style: Optional default style for child rows 

+

793 """ 

+

794 super().__init__(BlockType.TABLE) 

+

795 self._caption = caption 

+

796 self._rows: List[TableRow] = [] 

+

797 self._header_rows: List[TableRow] = [] 

+

798 self._footer_rows: List[TableRow] = [] 

+

799 self._style = style 

+

800 

+

801 @classmethod 

+

802 def create_and_add_to( 

+

803 cls, 

+

804 container, 

+

805 caption: Optional[str] = None, 

+

806 style=None) -> 'Table': 

+

807 """ 

+

808 Create a new Table and add it to a container, inheriting style from 

+

809 the container if not explicitly provided. 

+

810 

+

811 Args: 

+

812 container: The container to add the table to (must have add_block method) 

+

813 caption: Optional caption for the table 

+

814 style: Optional style override. If None, inherits from container 

+

815 

+

816 Returns: 

+

817 The newly created Table object 

+

818 

+

819 Raises: 

+

820 AttributeError: If the container doesn't have the required add_block method 

+

821 """ 

+

822 # Validate container and inherit style using ContainerAware utilities 

+

823 cls._validate_container(container) 

+

824 style = cls._inherit_style(container, style) 

+

825 

+

826 # Create the new table 

+

827 table = cls(caption, style) 

+

828 

+

829 # Add the table to the container 

+

830 container.add_block(table) 

+

831 

+

832 return table 

+

833 

+

834 @property 

+

835 def caption(self) -> Optional[str]: 

+

836 """Get the table caption""" 

+

837 return self._caption 

+

838 

+

839 @caption.setter 

+

840 def caption(self, caption: Optional[str]): 

+

841 """Set the table caption""" 

+

842 self._caption = caption 

+

843 

+

844 @property 

+

845 def style(self): 

+

846 """Get the default style for this table""" 

+

847 return self._style 

+

848 

+

849 @style.setter 

+

850 def style(self, style): 

+

851 """Set the default style for this table""" 

+

852 self._style = style 

+

853 

+

854 def add_row(self, row: TableRow, section: str = "body"): 

+

855 """ 

+

856 Add a row to this table. 

+

857 

+

858 Args: 

+

859 row: The TableRow to add 

+

860 section: The section to add the row to ("header", "body", or "footer") 

+

861 """ 

+

862 row.parent = self 

+

863 

+

864 if section.lower() == "header": 

+

865 self._header_rows.append(row) 

+

866 elif section.lower() == "footer": 

+

867 self._footer_rows.append(row) 

+

868 else: # Default to body 

+

869 self._rows.append(row) 

+

870 

+

871 def create_row(self, section: str = "body", style=None) -> TableRow: 

+

872 """ 

+

873 Create a new table row and add it to this table. 

+

874 

+

875 Args: 

+

876 section: The section to add the row to ("header", "body", or "footer") 

+

877 style: Optional style override. If None, inherits from table 

+

878 

+

879 Returns: 

+

880 The newly created TableRow object 

+

881 """ 

+

882 return TableRow.create_and_add_to(self, section, style) 

+

883 

+

884 def header_rows(self) -> Iterator[TableRow]: 

+

885 """ 

+

886 Iterate over the header rows in this table. 

+

887 

+

888 Yields: 

+

889 Each TableRow in the header section 

+

890 """ 

+

891 for row in self._header_rows: 

+

892 yield row 

+

893 

+

894 def body_rows(self) -> Iterator[TableRow]: 

+

895 """ 

+

896 Iterate over the body rows in this table. 

+

897 

+

898 Yields: 

+

899 Each TableRow in the body section 

+

900 """ 

+

901 for row in self._rows: 

+

902 yield row 

+

903 

+

904 def footer_rows(self) -> Iterator[TableRow]: 

+

905 """ 

+

906 Iterate over the footer rows in this table. 

+

907 

+

908 Yields: 

+

909 Each TableRow in the footer section 

+

910 """ 

+

911 for row in self._footer_rows: 

+

912 yield row 

+

913 

+

914 def all_rows(self) -> Iterator[Tuple[str, TableRow]]: 

+

915 """ 

+

916 Iterate over all rows in this table with their section labels. 

+

917 

+

918 Yields: 

+

919 Tuples of (section, row) for each row in the table 

+

920 """ 

+

921 for row in self._header_rows: 

+

922 yield ("header", row) 

+

923 for row in self._rows: 

+

924 yield ("body", row) 

+

925 for row in self._footer_rows: 

+

926 yield ("footer", row) 

+

927 

+

928 @property 

+

929 def row_count(self) -> Dict[str, int]: 

+

930 """Get the row counts by section""" 

+

931 return { 

+

932 "header": len(self._header_rows), 

+

933 "body": len(self._rows), 

+

934 "footer": len(self._footer_rows), 

+

935 "total": len(self._header_rows) + len(self._rows) + len(self._footer_rows) 

+

936 } 

+

937 

+

938 

+

939class Image(Block): 

+

940 """ 

+

941 An image element with source, dimensions, and alternative text. 

+

942 """ 

+

943 

+

944 def __init__( 

+

945 self, 

+

946 source: str = "", 

+

947 alt_text: str = "", 

+

948 width: Optional[int] = None, 

+

949 height: Optional[int] = None): 

+

950 """ 

+

951 Initialize an image element. 

+

952 

+

953 Args: 

+

954 source: The image source URL or path 

+

955 alt_text: Alternative text for accessibility 

+

956 width: Optional image width in pixels 

+

957 height: Optional image height in pixels 

+

958 """ 

+

959 super().__init__(BlockType.IMAGE) 

+

960 self._source = source 

+

961 self._alt_text = alt_text 

+

962 self._width = width 

+

963 self._height = height 

+

964 

+

965 @classmethod 

+

966 def create_and_add_to( 

+

967 cls, 

+

968 container, 

+

969 source: str = "", 

+

970 alt_text: str = "", 

+

971 width: Optional[int] = None, 

+

972 height: Optional[int] = None) -> 'Image': 

+

973 """ 

+

974 Create a new Image and add it to a container. 

+

975 

+

976 Args: 

+

977 container: The container to add the image to (must have add_block method) 

+

978 source: The image source URL or path 

+

979 alt_text: Alternative text for accessibility 

+

980 width: Optional image width in pixels 

+

981 height: Optional image height in pixels 

+

982 

+

983 Returns: 

+

984 The newly created Image object 

+

985 

+

986 Raises: 

+

987 AttributeError: If the container doesn't have the required add_block method 

+

988 """ 

+

989 # Create the new image 

+

990 image = cls(source, alt_text, width, height) 

+

991 

+

992 # Add the image to the container 

+

993 if hasattr(container, 'add_block'): 

+

994 container.add_block(image) 

+

995 else: 

+

996 raise AttributeError( 

+

997 f"Container {type(container).__name__} must have an 'add_block' method" 

+

998 ) 

+

999 

+

1000 return image 

+

1001 

+

1002 @property 

+

1003 def source(self) -> str: 

+

1004 """Get the image source""" 

+

1005 return self._source 

+

1006 

+

1007 @source.setter 

+

1008 def source(self, source: str): 

+

1009 """Set the image source""" 

+

1010 self._source = source 

+

1011 

+

1012 @property 

+

1013 def alt_text(self) -> str: 

+

1014 """Get the alternative text""" 

+

1015 return self._alt_text 

+

1016 

+

1017 @alt_text.setter 

+

1018 def alt_text(self, alt_text: str): 

+

1019 """Set the alternative text""" 

+

1020 self._alt_text = alt_text 

+

1021 

+

1022 @property 

+

1023 def width(self) -> Optional[int]: 

+

1024 """Get the image width""" 

+

1025 return self._width 

+

1026 

+

1027 @width.setter 

+

1028 def width(self, width: Optional[int]): 

+

1029 """Set the image width""" 

+

1030 self._width = width 

+

1031 

+

1032 @property 

+

1033 def height(self) -> Optional[int]: 

+

1034 """Get the image height""" 

+

1035 return self._height 

+

1036 

+

1037 @height.setter 

+

1038 def height(self, height: Optional[int]): 

+

1039 """Set the image height""" 

+

1040 self._height = height 

+

1041 

+

1042 def get_dimensions(self) -> Tuple[Optional[int], Optional[int]]: 

+

1043 """ 

+

1044 Get the image dimensions as a tuple. 

+

1045 

+

1046 Returns: 

+

1047 Tuple of (width, height) 

+

1048 """ 

+

1049 return (self._width, self._height) 

+

1050 

+

1051 def get_aspect_ratio(self) -> Optional[float]: 

+

1052 """ 

+

1053 Calculate the aspect ratio of the image. 

+

1054 

+

1055 Returns: 

+

1056 The aspect ratio (width/height) or None if either dimension is missing 

+

1057 """ 

+

1058 if self._width is not None and self._height is not None and self._height > 0: 

+

1059 return self._width / self._height 

+

1060 return None 

+

1061 

+

1062 def calculate_scaled_dimensions(self, 

+

1063 max_width: Optional[int] = None, 

+

1064 max_height: Optional[int] = None) -> Tuple[Optional[int], 

+

1065 Optional[int]]: 

+

1066 """ 

+

1067 Calculate scaled dimensions that fit within the given constraints. 

+

1068 

+

1069 Args: 

+

1070 max_width: Maximum allowed width 

+

1071 max_height: Maximum allowed height 

+

1072 

+

1073 Returns: 

+

1074 Tuple of (scaled_width, scaled_height) 

+

1075 """ 

+

1076 if self._width is None or self._height is None: 

+

1077 return (self._width, self._height) 

+

1078 

+

1079 width, height = self._width, self._height 

+

1080 

+

1081 # Scale down if needed 

+

1082 if max_width is not None and width > max_width: 

+

1083 height = int(height * max_width / width) 

+

1084 width = max_width 

+

1085 

+

1086 if max_height is not None and height > max_height: 1086 ↛ 1087line 1086 didn't jump to line 1087 because the condition on line 1086 was never true

+

1087 width = int(width * max_height / height) 

+

1088 height = max_height 

+

1089 

+

1090 return (width, height) 

+

1091 

+

1092 def _is_url(self, source: str) -> bool: 

+

1093 """ 

+

1094 Check if the source is a URL. 

+

1095 

+

1096 Args: 

+

1097 source: The source string to check 

+

1098 

+

1099 Returns: 

+

1100 True if the source appears to be a URL, False otherwise 

+

1101 """ 

+

1102 parsed = urllib.parse.urlparse(source) 

+

1103 return bool(parsed.scheme and parsed.netloc) 

+

1104 

+

1105 def _download_to_temp(self, url: str) -> str: 

+

1106 """ 

+

1107 Download an image from a URL to a temporary file. 

+

1108 

+

1109 Args: 

+

1110 url: The URL to download from 

+

1111 

+

1112 Returns: 

+

1113 Path to the temporary file 

+

1114 

+

1115 Raises: 

+

1116 urllib.error.URLError: If the download fails 

+

1117 """ 

+

1118 # Create a temporary file 

+

1119 temp_fd, temp_path = tempfile.mkstemp(suffix='.tmp') 

+

1120 

+

1121 try: 

+

1122 # Download the image 

+

1123 with urllib.request.urlopen(url) as response: 

+

1124 # Write the response data to the temporary file 

+

1125 with os.fdopen(temp_fd, 'wb') as temp_file: 

+

1126 temp_file.write(response.read()) 

+

1127 

+

1128 return temp_path 

+

1129 except BaseException: 

+

1130 # Clean up the temporary file if download fails 

+

1131 try: 

+

1132 os.close(temp_fd) 

+

1133 except BaseException: 

+

1134 pass 

+

1135 try: 

+

1136 os.unlink(temp_path) 

+

1137 except BaseException: 

+

1138 pass 

+

1139 raise 

+

1140 

+

1141 def load_image_data(self, 

+

1142 auto_update_dimensions: bool = True) -> Tuple[Optional[str], 

+

1143 Optional[PILImage.Image]]: 

+

1144 """ 

+

1145 Load image data using PIL, handling both local files and URLs. 

+

1146 

+

1147 Args: 

+

1148 auto_update_dimensions: If True, automatically update width and height from the loaded image 

+

1149 

+

1150 Returns: 

+

1151 Tuple of (file_path, PIL_Image_object). For URLs, file_path is the temporary file path. 

+

1152 Returns (None, None) if loading fails. 

+

1153 """ 

+

1154 if not self._source: 

+

1155 return None, None 

+

1156 

+

1157 file_path = None 

+

1158 temp_file = None 

+

1159 

+

1160 try: 

+

1161 if self._is_url(self._source): 

+

1162 # Download to temporary file 

+

1163 temp_file = self._download_to_temp(self._source) 

+

1164 file_path = temp_file 

+

1165 else: 

+

1166 # Use local file path 

+

1167 file_path = self._source 

+

1168 

+

1169 # Open with PIL 

+

1170 with PILImage.open(file_path) as img: 

+

1171 # Load the image data 

+

1172 img.load() 

+

1173 

+

1174 # Update dimensions if requested 

+

1175 if auto_update_dimensions: 

+

1176 self._width, self._height = img.size 

+

1177 

+

1178 # Return a copy to avoid issues with the context manager 

+

1179 return file_path, img.copy() 

+

1180 

+

1181 except Exception: 

+

1182 # Clean up temporary file on error 

+

1183 if temp_file and os.path.exists(temp_file): 1183 ↛ 1184line 1183 didn't jump to line 1184 because the condition on line 1183 was never true

+

1184 try: 

+

1185 os.unlink(temp_file) 

+

1186 except BaseException: 

+

1187 pass 

+

1188 return None, None 

+

1189 

+

1190 def get_image_info(self) -> Dict[str, Any]: 

+

1191 """ 

+

1192 Get detailed information about the image using PIL. 

+

1193 

+

1194 Returns: 

+

1195 Dictionary containing image information including format, mode, size, etc. 

+

1196 Returns empty dict if image cannot be loaded. 

+

1197 """ 

+

1198 file_path, img = self.load_image_data(auto_update_dimensions=False) 

+

1199 

+

1200 if img is None: 

+

1201 return {} 

+

1202 

+

1203 # Try to determine format from the image, file extension, or source 

+

1204 img_format = img.format 

+

1205 if img_format is None: 1205 ↛ 1229line 1205 didn't jump to line 1229 because the condition on line 1205 was always true

+

1206 # Try to determine format from file extension 

+

1207 format_map = { 

+

1208 '.jpg': 'JPEG', 

+

1209 '.jpeg': 'JPEG', 

+

1210 '.png': 'PNG', 

+

1211 '.gif': 'GIF', 

+

1212 '.bmp': 'BMP', 

+

1213 '.tiff': 'TIFF', 

+

1214 '.tif': 'TIFF' 

+

1215 } 

+

1216 

+

1217 # First try the actual file path if available 

+

1218 if file_path: 1218 ↛ 1223line 1218 didn't jump to line 1223 because the condition on line 1218 was always true

+

1219 ext = os.path.splitext(file_path)[1].lower() 

+

1220 img_format = format_map.get(ext) 

+

1221 

+

1222 # If still no format and we have a URL source, try the original URL 

+

1223 if img_format is None and self._is_url(self._source): 

+

1224 ext = os.path.splitext( 

+

1225 urllib.parse.urlparse( 

+

1226 self._source).path)[1].lower() 

+

1227 img_format = format_map.get(ext) 

+

1228 

+

1229 info = { 

+

1230 'format': img_format, 

+

1231 'mode': img.mode, 

+

1232 'size': img.size, 

+

1233 'width': img.width, 

+

1234 'height': img.height, 

+

1235 } 

+

1236 

+

1237 # Add additional info if available 

+

1238 if hasattr(img, 'info'): 1238 ↛ 1242line 1238 didn't jump to line 1242 because the condition on line 1238 was always true

+

1239 info['info'] = img.info 

+

1240 

+

1241 # Clean up temporary file if it was created 

+

1242 if file_path and self._is_url(self._source): 

+

1243 try: 

+

1244 os.unlink(file_path) 

+

1245 except BaseException: 

+

1246 pass 

+

1247 

+

1248 return info 

+

1249 

+

1250 

+

1251class LinkedImage(Image): 

+

1252 """ 

+

1253 An Image that is also a Link - clickable images that navigate or trigger callbacks. 

+

1254 """ 

+

1255 

+

1256 def __init__(self, source: str, alt_text: str, location: str, 

+

1257 width: Optional[int] = None, height: Optional[int] = None, 

+

1258 link_type=None, 

+

1259 callback: Optional[Any] = None, 

+

1260 params: Optional[Dict[str, Any]] = None, 

+

1261 title: Optional[str] = None): 

+

1262 """ 

+

1263 Initialize a linked image. 

+

1264 

+

1265 Args: 

+

1266 source: The image source URL or path 

+

1267 alt_text: Alternative text for accessibility 

+

1268 location: The link target (URL, bookmark, etc.) 

+

1269 width: Optional image width in pixels 

+

1270 height: Optional image height in pixels 

+

1271 link_type: Type of link (INTERNAL, EXTERNAL, etc.) 

+

1272 callback: Optional callback for link activation 

+

1273 params: Parameters for the link 

+

1274 title: Tooltip/title for the link 

+

1275 """ 

+

1276 # Initialize Image 

+

1277 super().__init__(source, alt_text, width, height) 

+

1278 

+

1279 # Store link properties 

+

1280 # Import here to avoid circular imports at module level 

+

1281 from pyWebLayout.abstract.functional import LinkType 

+

1282 self._location = location 

+

1283 self._link_type = link_type or LinkType.EXTERNAL 

+

1284 self._callback = callback 

+

1285 self._params = params or {} 

+

1286 self._link_title = title 

+

1287 

+

1288 @property 

+

1289 def location(self) -> str: 

+

1290 """Get the link target location""" 

+

1291 return self._location 

+

1292 

+

1293 @property 

+

1294 def link_type(self): 

+

1295 """Get the type of link""" 

+

1296 return self._link_type 

+

1297 

+

1298 @property 

+

1299 def link_callback(self) -> Optional[Any]: 

+

1300 """Get the link callback""" 

+

1301 return self._callback 

+

1302 

+

1303 @property 

+

1304 def params(self) -> Dict[str, Any]: 

+

1305 """Get the link parameters""" 

+

1306 return self._params 

+

1307 

+

1308 @property 

+

1309 def link_title(self) -> Optional[str]: 

+

1310 """Get the link title/tooltip""" 

+

1311 return self._link_title 

+

1312 

+

1313 def execute_link(self, context: Optional[Dict[str, Any]] = None) -> Any: 

+

1314 """ 

+

1315 Execute the link action. 

+

1316 

+

1317 Args: 

+

1318 context: Optional context dict (e.g., {'alt_text': image.alt_text}) 

+

1319 

+

1320 Returns: 

+

1321 The result of the link execution 

+

1322 """ 

+

1323 from pyWebLayout.abstract.functional import LinkType 

+

1324 

+

1325 # Add image info to context 

+

1326 full_context = { 

+

1327 **self._params, 

+

1328 'alt_text': self._alt_text, 

+

1329 'source': self._source} 

+

1330 if context: 1330 ↛ 1331line 1330 didn't jump to line 1331 because the condition on line 1330 was never true

+

1331 full_context.update(context) 

+

1332 

+

1333 if self._link_type in (LinkType.API, LinkType.FUNCTION) and self._callback: 

+

1334 return self._callback(self._location, **full_context) 

+

1335 else: 

+

1336 # For INTERNAL and EXTERNAL links, return the location 

+

1337 return self._location 

+

1338 

+

1339 

+

1340class HorizontalRule(Block): 

+

1341 """ 

+

1342 A horizontal rule element (hr tag). 

+

1343 """ 

+

1344 

+

1345 def __init__(self): 

+

1346 """Initialize a horizontal rule element.""" 

+

1347 super().__init__(BlockType.HORIZONTAL_RULE) 

+

1348 

+

1349 @classmethod 

+

1350 def create_and_add_to(cls, container) -> 'HorizontalRule': 

+

1351 """ 

+

1352 Create a new HorizontalRule and add it to a container. 

+

1353 

+

1354 Args: 

+

1355 container: The container to add the horizontal rule to (must have add_block method) 

+

1356 

+

1357 Returns: 

+

1358 The newly created HorizontalRule object 

+

1359 

+

1360 Raises: 

+

1361 AttributeError: If the container doesn't have the required add_block method 

+

1362 """ 

+

1363 # Create the new horizontal rule 

+

1364 hr = cls() 

+

1365 

+

1366 # Add the horizontal rule to the container 

+

1367 if hasattr(container, 'add_block'): 

+

1368 container.add_block(hr) 

+

1369 else: 

+

1370 raise AttributeError( 

+

1371 f"Container {type(container).__name__} must have an 'add_block' method" 

+

1372 ) 

+

1373 

+

1374 return hr 

+

1375 

+

1376 

+

1377class PageBreak(Block): 

+

1378 """ 

+

1379 A page break element that forces content to start on a new page. 

+

1380 

+

1381 When encountered during layout, this block signals that all subsequent 

+

1382 content should be placed on a new page, even if the current page has 

+

1383 available space. 

+

1384 """ 

+

1385 

+

1386 def __init__(self): 

+

1387 """Initialize a page break element.""" 

+

1388 super().__init__(BlockType.PAGE_BREAK) 

+

1389 

+

1390 @classmethod 

+

1391 def create_and_add_to(cls, container) -> 'PageBreak': 

+

1392 """ 

+

1393 Create a new PageBreak and add it to a container. 

+

1394 

+

1395 Args: 

+

1396 container: The container to add the page break to (must have add_block method) 

+

1397 

+

1398 Returns: 

+

1399 The newly created PageBreak object 

+

1400 

+

1401 Raises: 

+

1402 AttributeError: If the container doesn't have the required add_block method 

+

1403 """ 

+

1404 # Create the new page break 

+

1405 page_break = cls() 

+

1406 

+

1407 # Add the page break to the container 

+

1408 if hasattr(container, 'add_block'): 

+

1409 container.add_block(page_break) 

+

1410 else: 

+

1411 raise AttributeError( 

+

1412 f"Container {type(container).__name__} must have an 'add_block' method" 

+

1413 ) 

+

1414 

+

1415 return page_break 

+
+ + + diff --git a/cov_info/htmlcov/z_af715639580e2d86_document_py.html b/cov_info/htmlcov/z_af715639580e2d86_document_py.html new file mode 100644 index 0000000..abbc1d6 --- /dev/null +++ b/cov_info/htmlcov/z_af715639580e2d86_document_py.html @@ -0,0 +1,694 @@ + + + + + Coverage for pyWebLayout/abstract/document.py: 78% + + + + + +
+
+

+ Coverage for pyWebLayout/abstract/document.py: + 78% +

+ +

+ 194 statements   + + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.15.4, + created at 2026-08-08 20:34 +0000 +

+ +
+
+
+

1from __future__ import annotations 

+

2from typing import List, Dict, Optional, Tuple, Union, Any 

+

3from enum import Enum 

+

4from .block import Block, BlockType, Heading, HeadingLevel, Paragraph 

+

5from ..style import Font, FontWeight, FontStyle, TextDecoration 

+

6from ..style.abstract_style import AbstractStyle, AbstractStyleRegistry, FontFamily, FontSize 

+

7from ..style.concrete_style import ConcreteStyleRegistry, RenderingContext, StyleResolver 

+

8from ..core import FontRegistry, MetadataContainer 

+

9 

+

10 

+

11class MetadataType(Enum): 

+

12 """Types of metadata that can be associated with a document""" 

+

13 TITLE = 1 

+

14 AUTHOR = 2 

+

15 DESCRIPTION = 3 

+

16 KEYWORDS = 4 

+

17 LANGUAGE = 5 

+

18 PUBLICATION_DATE = 6 

+

19 MODIFIED_DATE = 7 

+

20 PUBLISHER = 8 

+

21 IDENTIFIER = 9 

+

22 COVER_IMAGE = 10 

+

23 CUSTOM = 100 

+

24 

+

25 

+

26class Document(FontRegistry, MetadataContainer): 

+

27 """ 

+

28 Abstract representation of a complete document like an HTML page or an ebook. 

+

29 This class manages the logical structure of the document without rendering concerns. 

+

30 

+

31 Uses FontRegistry mixin for font caching. 

+

32 Uses MetadataContainer mixin for metadata management. 

+

33 """ 

+

34 

+

35 def __init__( 

+

36 self, 

+

37 title: Optional[str] = None, 

+

38 language: str = "en-US", 

+

39 default_style=None): 

+

40 """ 

+

41 Initialize a new document. 

+

42 

+

43 Args: 

+

44 title: The document title 

+

45 language: The document language code 

+

46 default_style: Optional default style for child blocks 

+

47 """ 

+

48 super().__init__() 

+

49 self._blocks: List[Block] = [] 

+

50 self._anchors: Dict[str, Block] = {} # Named anchors for navigation 

+

51 self._resources: Dict[str, Any] = {} # External resources like images 

+

52 self._stylesheets: List[Dict[str, Any]] = [] # CSS stylesheets 

+

53 self._scripts: List[str] = [] # JavaScript code 

+

54 

+

55 # Style management with new abstract/concrete system 

+

56 self._abstract_style_registry = AbstractStyleRegistry() 

+

57 self._rendering_context = RenderingContext(default_language=language) 

+

58 self._style_resolver = StyleResolver(self._rendering_context) 

+

59 self._concrete_style_registry = ConcreteStyleRegistry(self._style_resolver) 

+

60 

+

61 # Set default style 

+

62 if default_style is None: 62 ↛ 65line 62 didn't jump to line 65 because the condition on line 62 was always true

+

63 # Create a default abstract style 

+

64 default_style = self._abstract_style_registry.default_style 

+

65 elif isinstance(default_style, Font): 

+

66 # Convert Font to AbstractStyle for backward compatibility 

+

67 default_style = AbstractStyle( 

+

68 font_family=FontFamily.SERIF, # Default assumption 

+

69 font_size=default_style.font_size, 

+

70 color=default_style.colour, 

+

71 language=default_style.language 

+

72 ) 

+

73 style_id, default_style = self._abstract_style_registry.get_or_create_style( 

+

74 default_style) 

+

75 self._default_style = default_style 

+

76 

+

77 # Set basic metadata 

+

78 if title: 

+

79 self.set_metadata(MetadataType.TITLE, title) 

+

80 self.set_metadata(MetadataType.LANGUAGE, language) 

+

81 

+

82 @property 

+

83 def blocks(self) -> List[Block]: 

+

84 """Get the top-level blocks in this document""" 

+

85 return self._blocks 

+

86 

+

87 @property 

+

88 def default_style(self): 

+

89 """Get the default style for this document""" 

+

90 return self._default_style 

+

91 

+

92 @default_style.setter 

+

93 def default_style(self, style): 

+

94 """Set the default style for this document""" 

+

95 self._default_style = style 

+

96 

+

97 def add_block(self, block: Block): 

+

98 """ 

+

99 Add a block to this document. 

+

100 

+

101 Args: 

+

102 block: The block to add 

+

103 """ 

+

104 self._blocks.append(block) 

+

105 

+

106 def create_paragraph(self, style=None) -> Paragraph: 

+

107 """ 

+

108 Create a new paragraph and add it to this document. 

+

109 

+

110 Args: 

+

111 style: Optional style override. If None, inherits from document 

+

112 

+

113 Returns: 

+

114 The newly created Paragraph object 

+

115 """ 

+

116 if style is None: 

+

117 style = self._default_style 

+

118 paragraph = Paragraph(style) 

+

119 self.add_block(paragraph) 

+

120 return paragraph 

+

121 

+

122 def create_heading( 

+

123 self, 

+

124 level: HeadingLevel = HeadingLevel.H1, 

+

125 style=None) -> Heading: 

+

126 """ 

+

127 Create a new heading and add it to this document. 

+

128 

+

129 Args: 

+

130 level: The heading level 

+

131 style: Optional style override. If None, inherits from document 

+

132 

+

133 Returns: 

+

134 The newly created Heading object 

+

135 """ 

+

136 if style is None: 

+

137 style = self._default_style 

+

138 heading = Heading(level, style) 

+

139 self.add_block(heading) 

+

140 return heading 

+

141 

+

142 def create_chapter( 

+

143 self, 

+

144 title: Optional[str] = None, 

+

145 level: int = 1, 

+

146 style=None) -> 'Chapter': 

+

147 """ 

+

148 Create a new chapter with inherited style. 

+

149 

+

150 Args: 

+

151 title: The chapter title 

+

152 level: The chapter level 

+

153 style: Optional style override. If None, inherits from document 

+

154 

+

155 Returns: 

+

156 The newly created Chapter object 

+

157 """ 

+

158 if style is None: 

+

159 style = self._default_style 

+

160 return Chapter(title, level, style) 

+

161 

+

162 # set_metadata() and get_metadata() are provided by MetadataContainer mixin 

+

163 

+

164 def add_anchor(self, name: str, target: Block): 

+

165 """ 

+

166 Add a named anchor to this document. 

+

167 

+

168 Args: 

+

169 name: The anchor name 

+

170 target: The target block 

+

171 """ 

+

172 self._anchors[name] = target 

+

173 

+

174 def get_anchor(self, name: str) -> Optional[Block]: 

+

175 """ 

+

176 Get a named anchor from this document. 

+

177 

+

178 Args: 

+

179 name: The anchor name 

+

180 

+

181 Returns: 

+

182 The target block, or None if not found 

+

183 """ 

+

184 return self._anchors.get(name) 

+

185 

+

186 def add_resource(self, name: str, resource: Any): 

+

187 """ 

+

188 Add a resource to this document. 

+

189 

+

190 Args: 

+

191 name: The resource name 

+

192 resource: The resource data 

+

193 """ 

+

194 self._resources[name] = resource 

+

195 

+

196 def get_resource(self, name: str) -> Optional[Any]: 

+

197 """ 

+

198 Get a resource from this document. 

+

199 

+

200 Args: 

+

201 name: The resource name 

+

202 

+

203 Returns: 

+

204 The resource data, or None if not found 

+

205 """ 

+

206 return self._resources.get(name) 

+

207 

+

208 def add_stylesheet(self, stylesheet: Dict[str, Any]): 

+

209 """ 

+

210 Add a stylesheet to this document. 

+

211 

+

212 Args: 

+

213 stylesheet: The stylesheet data 

+

214 """ 

+

215 self._stylesheets.append(stylesheet) 

+

216 

+

217 def add_script(self, script: str): 

+

218 """ 

+

219 Add a script to this document. 

+

220 

+

221 Args: 

+

222 script: The script code 

+

223 """ 

+

224 self._scripts.append(script) 

+

225 

+

226 def get_title(self) -> Optional[str]: 

+

227 """ 

+

228 Get the document title. 

+

229 

+

230 Returns: 

+

231 The document title, or None if not set 

+

232 """ 

+

233 return self.get_metadata(MetadataType.TITLE) 

+

234 

+

235 def set_title(self, title: str): 

+

236 """ 

+

237 Set the document title. 

+

238 

+

239 Args: 

+

240 title: The document title 

+

241 """ 

+

242 self.set_metadata(MetadataType.TITLE, title) 

+

243 

+

244 @property 

+

245 def title(self) -> Optional[str]: 

+

246 """ 

+

247 Get the document title as a property. 

+

248 

+

249 Returns: 

+

250 The document title, or None if not set 

+

251 """ 

+

252 return self.get_title() 

+

253 

+

254 @title.setter 

+

255 def title(self, title: str): 

+

256 """ 

+

257 Set the document title as a property. 

+

258 

+

259 Args: 

+

260 title: The document title 

+

261 """ 

+

262 self.set_title(title) 

+

263 

+

264 def find_blocks_by_type(self, block_type: BlockType) -> List[Block]: 

+

265 """ 

+

266 Find all blocks of a specific type. 

+

267 

+

268 Args: 

+

269 block_type: The type of blocks to find 

+

270 

+

271 Returns: 

+

272 A list of matching blocks 

+

273 """ 

+

274 result = [] 

+

275 

+

276 def _find_recursive(blocks: List[Block]): 

+

277 for block in blocks: 

+

278 if block.block_type == block_type: 

+

279 result.append(block) 

+

280 

+

281 # Check for child blocks based on block type 

+

282 if hasattr(block, '_blocks'): 282 ↛ 283line 282 didn't jump to line 283 because the condition on line 282 was never true

+

283 _find_recursive(block._blocks) 

+

284 elif hasattr(block, '_items') and isinstance(block._items, list): 284 ↛ 285line 284 didn't jump to line 285 because the condition on line 284 was never true

+

285 _find_recursive(block._items) 

+

286 

+

287 _find_recursive(self._blocks) 

+

288 return result 

+

289 

+

290 def find_headings(self) -> List[Heading]: 

+

291 """ 

+

292 Find all headings in the document. 

+

293 

+

294 Returns: 

+

295 A list of heading blocks 

+

296 """ 

+

297 blocks = self.find_blocks_by_type(BlockType.HEADING) 

+

298 return [block for block in blocks if isinstance(block, Heading)] 

+

299 

+

300 def generate_table_of_contents(self) -> List[Tuple[int, str, Block]]: 

+

301 """ 

+

302 Generate a table of contents from headings. 

+

303 

+

304 Returns: 

+

305 A list of tuples containing (level, title, heading_block) 

+

306 """ 

+

307 headings = self.find_headings() 

+

308 

+

309 toc = [] 

+

310 for heading in headings: 

+

311 # Extract text from the heading 

+

312 title = "" 

+

313 for _, word in heading.words_iter(): 

+

314 title += word.text + " " 

+

315 title = title.strip() 

+

316 

+

317 # Add to TOC 

+

318 level = heading.level.value # Get numeric value from HeadingLevel enum 

+

319 toc.append((level, title, heading)) 

+

320 

+

321 return toc 

+

322 

+

323 def get_or_create_style(self, 

+

324 font_family: FontFamily = FontFamily.SERIF, 

+

325 font_size: Union[FontSize, int] = FontSize.MEDIUM, 

+

326 font_weight: FontWeight = FontWeight.NORMAL, 

+

327 font_style: FontStyle = FontStyle.NORMAL, 

+

328 text_decoration: TextDecoration = TextDecoration.NONE, 

+

329 color: Union[str, Tuple[int, int, int]] = "black", 

+

330 background_color: Optional[Union[str, Tuple[int, int, int, int]]] = None, 

+

331 language: str = "en-US", 

+

332 **kwargs) -> Tuple[str, AbstractStyle]: 

+

333 """ 

+

334 Get or create an abstract style with the specified properties. 

+

335 

+

336 Args: 

+

337 font_family: Semantic font family 

+

338 font_size: Font size (semantic or numeric) 

+

339 font_weight: Font weight 

+

340 font_style: Font style 

+

341 text_decoration: Text decoration 

+

342 color: Text color (name or RGB tuple) 

+

343 background_color: Background color 

+

344 language: Language code 

+

345 **kwargs: Additional style properties 

+

346 

+

347 Returns: 

+

348 Tuple of (style_id, AbstractStyle) 

+

349 """ 

+

350 abstract_style = AbstractStyle( 

+

351 font_family=font_family, 

+

352 font_size=font_size, 

+

353 font_weight=font_weight, 

+

354 font_style=font_style, 

+

355 text_decoration=text_decoration, 

+

356 color=color, 

+

357 background_color=background_color, 

+

358 language=language, 

+

359 **kwargs 

+

360 ) 

+

361 

+

362 return self._abstract_style_registry.get_or_create_style(abstract_style) 

+

363 

+

364 def get_font_for_style(self, abstract_style: AbstractStyle) -> Font: 

+

365 """ 

+

366 Get a Font object for an AbstractStyle (for rendering). 

+

367 

+

368 Args: 

+

369 abstract_style: The abstract style to get a font for 

+

370 

+

371 Returns: 

+

372 Font object ready for rendering 

+

373 """ 

+

374 return self._concrete_style_registry.get_font(abstract_style) 

+

375 

+

376 def update_rendering_context(self, **kwargs): 

+

377 """ 

+

378 Update the rendering context (user preferences, device settings, etc.). 

+

379 

+

380 Args: 

+

381 **kwargs: Context properties to update (base_font_size, font_scale_factor, etc.) 

+

382 """ 

+

383 self._style_resolver.update_context(**kwargs) 

+

384 

+

385 def get_style_registry(self) -> AbstractStyleRegistry: 

+

386 """Get the abstract style registry for this document.""" 

+

387 return self._abstract_style_registry 

+

388 

+

389 def get_concrete_style_registry(self) -> ConcreteStyleRegistry: 

+

390 """Get the concrete style registry for this document.""" 

+

391 return self._concrete_style_registry 

+

392 

+

393 # get_or_create_font() is provided by FontRegistry mixin 

+

394 

+

395 

+

396class Chapter(FontRegistry, MetadataContainer): 

+

397 """ 

+

398 Represents a chapter or section in a document. 

+

399 A chapter contains a sequence of blocks and has metadata. 

+

400 

+

401 Uses FontRegistry mixin for font caching with parent delegation. 

+

402 Uses MetadataContainer mixin for metadata management. 

+

403 """ 

+

404 

+

405 def __init__( 

+

406 self, 

+

407 title: Optional[str] = None, 

+

408 level: int = 1, 

+

409 style=None, 

+

410 parent=None): 

+

411 """ 

+

412 Initialize a new chapter. 

+

413 

+

414 Args: 

+

415 title: The chapter title 

+

416 level: The chapter level (1 = top level, 2 = subsection, etc.) 

+

417 style: Optional default style for child blocks 

+

418 parent: Parent container (e.g., Document or Book) 

+

419 """ 

+

420 super().__init__() 

+

421 self._title = title 

+

422 self._level = level 

+

423 self._blocks: List[Block] = [] 

+

424 self._style = style 

+

425 self._parent = parent 

+

426 

+

427 @property 

+

428 def title(self) -> Optional[str]: 

+

429 """Get the chapter title""" 

+

430 return self._title 

+

431 

+

432 @title.setter 

+

433 def title(self, title: str): 

+

434 """Set the chapter title""" 

+

435 self._title = title 

+

436 

+

437 @property 

+

438 def level(self) -> int: 

+

439 """Get the chapter level""" 

+

440 return self._level 

+

441 

+

442 @property 

+

443 def blocks(self) -> List[Block]: 

+

444 """Get the blocks in this chapter""" 

+

445 return self._blocks 

+

446 

+

447 @property 

+

448 def style(self): 

+

449 """Get the default style for this chapter""" 

+

450 return self._style 

+

451 

+

452 @style.setter 

+

453 def style(self, style): 

+

454 """Set the default style for this chapter""" 

+

455 self._style = style 

+

456 

+

457 def add_block(self, block: Block): 

+

458 """ 

+

459 Add a block to this chapter. 

+

460 

+

461 Args: 

+

462 block: The block to add 

+

463 """ 

+

464 self._blocks.append(block) 

+

465 

+

466 def create_paragraph(self, style=None) -> Paragraph: 

+

467 """ 

+

468 Create a new paragraph and add it to this chapter. 

+

469 

+

470 Args: 

+

471 style: Optional style override. If None, inherits from chapter 

+

472 

+

473 Returns: 

+

474 The newly created Paragraph object 

+

475 """ 

+

476 if style is None: 

+

477 style = self._style 

+

478 paragraph = Paragraph(style) 

+

479 self.add_block(paragraph) 

+

480 return paragraph 

+

481 

+

482 def create_heading( 

+

483 self, 

+

484 level: HeadingLevel = HeadingLevel.H1, 

+

485 style=None) -> Heading: 

+

486 """ 

+

487 Create a new heading and add it to this chapter. 

+

488 

+

489 Args: 

+

490 level: The heading level 

+

491 style: Optional style override. If None, inherits from chapter 

+

492 

+

493 Returns: 

+

494 The newly created Heading object 

+

495 """ 

+

496 if style is None: 

+

497 style = self._style 

+

498 heading = Heading(level, style) 

+

499 self.add_block(heading) 

+

500 return heading 

+

501 

+

502 # set_metadata() and get_metadata() are provided by MetadataContainer mixin 

+

503 # get_or_create_font() is provided by FontRegistry mixin 

+

504 

+

505 

+

506class Book(Document): 

+

507 """ 

+

508 Abstract representation of an ebook. 

+

509 A book is a document that contains chapters. 

+

510 """ 

+

511 

+

512 def __init__(self, title: Optional[str] = None, author: Optional[str] = None, 

+

513 language: str = "en-US", default_style=None): 

+

514 """ 

+

515 Initialize a new book. 

+

516 

+

517 Args: 

+

518 title: The book title 

+

519 author: The book author 

+

520 language: The book language code 

+

521 default_style: Optional default style for child chapters and blocks 

+

522 """ 

+

523 super().__init__(title, language, default_style) 

+

524 self._chapters: List[Chapter] = [] 

+

525 

+

526 if author: 

+

527 self.set_metadata(MetadataType.AUTHOR, author) 

+

528 

+

529 @property 

+

530 def chapters(self) -> List[Chapter]: 

+

531 """Get the chapters in this book""" 

+

532 return self._chapters 

+

533 

+

534 def add_chapter(self, chapter: Chapter): 

+

535 """ 

+

536 Add a chapter to this book. 

+

537 

+

538 Args: 

+

539 chapter: The chapter to add 

+

540 """ 

+

541 self._chapters.append(chapter) 

+

542 

+

543 def create_chapter( 

+

544 self, 

+

545 title: Optional[str] = None, 

+

546 level: int = 1, 

+

547 style=None) -> Chapter: 

+

548 """ 

+

549 Create and add a new chapter with inherited style. 

+

550 

+

551 Args: 

+

552 title: The chapter title 

+

553 level: The chapter level 

+

554 style: Optional style override. If None, inherits from book 

+

555 

+

556 Returns: 

+

557 The new chapter 

+

558 """ 

+

559 if style is None: 559 ↛ 561line 559 didn't jump to line 561 because the condition on line 559 was always true

+

560 style = self._default_style 

+

561 chapter = Chapter(title, level, style) 

+

562 self.add_chapter(chapter) 

+

563 return chapter 

+

564 

+

565 def get_author(self) -> Optional[str]: 

+

566 """ 

+

567 Get the book author. 

+

568 

+

569 Returns: 

+

570 The book author, or None if not set 

+

571 """ 

+

572 return self.get_metadata(MetadataType.AUTHOR) 

+

573 

+

574 def set_author(self, author: str): 

+

575 """ 

+

576 Set the book author. 

+

577 

+

578 Args: 

+

579 author: The book author 

+

580 """ 

+

581 self.set_metadata(MetadataType.AUTHOR, author) 

+

582 

+

583 def generate_table_of_contents(self) -> List[Tuple[int, str, Chapter]]: 

+

584 """ 

+

585 Generate a table of contents from chapters. 

+

586 

+

587 Returns: 

+

588 A list of tuples containing (level, title, chapter) 

+

589 """ 

+

590 toc = [] 

+

591 for chapter in self._chapters: 

+

592 if chapter.title: 

+

593 toc.append((chapter.level, chapter.title, chapter)) 

+

594 

+

595 return toc 

+
+ + + diff --git a/cov_info/htmlcov/z_af715639580e2d86_functional_py.html b/cov_info/htmlcov/z_af715639580e2d86_functional_py.html new file mode 100644 index 0000000..7c78573 --- /dev/null +++ b/cov_info/htmlcov/z_af715639580e2d86_functional_py.html @@ -0,0 +1,444 @@ + + + + + Coverage for pyWebLayout/abstract/functional.py: 98% + + + + + +
+
+

+ Coverage for pyWebLayout/abstract/functional.py: + 98% +

+ +

+ 144 statements   + + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.15.4, + created at 2026-08-08 20:34 +0000 +

+ +
+
+
+

1from __future__ import annotations 

+

2from enum import Enum 

+

3from typing import Callable, Dict, Any, Optional, List, Tuple 

+

4from pyWebLayout.core.base import Interactable 

+

5 

+

6 

+

7class LinkType(Enum): 

+

8 """Enumeration of different types of links for classification purposes""" 

+

9 INTERNAL = 1 # Links within the same document (e.g., chapter references, bookmarks) 

+

10 EXTERNAL = 2 # Links to external resources (e.g., websites, other documents) 

+

11 API = 3 # Links that trigger API calls (e.g., for settings management) 

+

12 FUNCTION = 4 # Links that execute a specific function 

+

13 

+

14 

+

15class Link(Interactable): 

+

16 """ 

+

17 A link that can navigate to a location or execute a function. 

+

18 Links can be used for navigation within a document, to external resources, 

+

19 or to trigger API calls for functionality like settings management. 

+

20 """ 

+

21 

+

22 def __init__(self, 

+

23 location: str, 

+

24 link_type: LinkType = LinkType.INTERNAL, 

+

25 callback: Optional[Callable] = None, 

+

26 params: Optional[Dict[str, Any]] = None, 

+

27 title: Optional[str] = None, 

+

28 html_id: Optional[str] = None): 

+

29 """ 

+

30 Initialize a link. 

+

31 

+

32 Args: 

+

33 location: The target location or identifier for this link 

+

34 link_type: The type of link (internal, external, API, function) 

+

35 callback: Optional callback function to execute when the link is activated 

+

36 params: Optional parameters to pass to the callback or API 

+

37 title: Optional title/tooltip for the link 

+

38 html_id: Optional HTML id attribute (from <a id="...">) for callback binding 

+

39 """ 

+

40 super().__init__(callback) 

+

41 self._location = location 

+

42 self._link_type = link_type 

+

43 self._params = params or {} 

+

44 self._title = title 

+

45 self._html_id = html_id 

+

46 

+

47 @property 

+

48 def location(self) -> str: 

+

49 """Get the target location of this link""" 

+

50 return self._location 

+

51 

+

52 @property 

+

53 def link_type(self) -> LinkType: 

+

54 """Get the type of this link""" 

+

55 return self._link_type 

+

56 

+

57 @property 

+

58 def params(self) -> Dict[str, Any]: 

+

59 """Get the parameters for this link""" 

+

60 return self._params 

+

61 

+

62 @property 

+

63 def title(self) -> Optional[str]: 

+

64 """Get the title/tooltip for this link""" 

+

65 return self._title 

+

66 

+

67 @property 

+

68 def html_id(self) -> Optional[str]: 

+

69 """Get the HTML id attribute for callback binding""" 

+

70 return self._html_id 

+

71 

+

72 def execute(self, point=None) -> Any: 

+

73 """ 

+

74 Execute the link action based on its type. 

+

75 

+

76 For internal and external links, returns the location. 

+

77 For API and function links, executes the callback with the provided parameters. 

+

78 

+

79 Args: 

+

80 point: Optional interaction point passed from the interact() method 

+

81 

+

82 Returns: 

+

83 The result of the link execution, which depends on the link type. 

+

84 """ 

+

85 if self._link_type in (LinkType.API, LinkType.FUNCTION) and self._callback: 

+

86 return self._callback(self._location, point, **self._params) 

+

87 else: 

+

88 # For INTERNAL and EXTERNAL links, return the location 

+

89 # The renderer/browser will handle the navigation 

+

90 return self._location 

+

91 

+

92 

+

93class Button(Interactable): 

+

94 """ 

+

95 A button that can be clicked to execute an action. 

+

96 Buttons are similar to function links but are rendered differently. 

+

97 """ 

+

98 

+

99 def __init__(self, 

+

100 label: str, 

+

101 callback: Callable, 

+

102 params: Optional[Dict[str, Any]] = None, 

+

103 enabled: bool = True, 

+

104 html_id: Optional[str] = None): 

+

105 """ 

+

106 Initialize a button. 

+

107 

+

108 Args: 

+

109 label: The text label for the button 

+

110 callback: The function to execute when the button is clicked 

+

111 params: Optional parameters to pass to the callback 

+

112 enabled: Whether the button is initially enabled 

+

113 html_id: Optional HTML id attribute (from <button id="...">) for callback binding 

+

114 """ 

+

115 super().__init__(callback) 

+

116 self._label = label 

+

117 self._params = params or {} 

+

118 self._enabled = enabled 

+

119 self._html_id = html_id 

+

120 

+

121 @property 

+

122 def label(self) -> str: 

+

123 """Get the button label""" 

+

124 return self._label 

+

125 

+

126 @label.setter 

+

127 def label(self, label: str): 

+

128 """Set the button label""" 

+

129 self._label = label 

+

130 

+

131 @property 

+

132 def enabled(self) -> bool: 

+

133 """Check if the button is enabled""" 

+

134 return self._enabled 

+

135 

+

136 @enabled.setter 

+

137 def enabled(self, enabled: bool): 

+

138 """Enable or disable the button""" 

+

139 self._enabled = enabled 

+

140 

+

141 @property 

+

142 def params(self) -> Dict[str, Any]: 

+

143 """Get the button parameters""" 

+

144 return self._params 

+

145 

+

146 @property 

+

147 def html_id(self) -> Optional[str]: 

+

148 """Get the HTML id attribute for callback binding""" 

+

149 return self._html_id 

+

150 

+

151 def execute(self, point=None) -> Any: 

+

152 """ 

+

153 Execute the button's callback function if the button is enabled. 

+

154 

+

155 Args: 

+

156 point: Optional interaction point passed from the interact() method 

+

157 

+

158 Returns: 

+

159 The result of the callback function, or None if the button is disabled. 

+

160 """ 

+

161 if self._enabled and self._callback: 

+

162 return self._callback(point, **self._params) 

+

163 return None 

+

164 

+

165 

+

166class Form(Interactable): 

+

167 """ 

+

168 A form that can contain input fields and be submitted. 

+

169 Forms can be used for user input and settings configuration. 

+

170 """ 

+

171 

+

172 def __init__(self, 

+

173 form_id: str, 

+

174 action: Optional[str] = None, 

+

175 callback: Optional[Callable] = None, 

+

176 html_id: Optional[str] = None): 

+

177 """ 

+

178 Initialize a form. 

+

179 

+

180 Args: 

+

181 form_id: The unique identifier for this form 

+

182 action: The action URL or endpoint for form submission 

+

183 callback: Optional callback function to execute on form submission 

+

184 html_id: Optional HTML id attribute (from <form id="...">) for callback binding 

+

185 """ 

+

186 super().__init__(callback) 

+

187 self._form_id = form_id 

+

188 self._action = action 

+

189 self._fields: Dict[str, FormField] = {} 

+

190 self._html_id = html_id 

+

191 

+

192 @property 

+

193 def form_id(self) -> str: 

+

194 """Get the form ID""" 

+

195 return self._form_id 

+

196 

+

197 @property 

+

198 def action(self) -> Optional[str]: 

+

199 """Get the form action""" 

+

200 return self._action 

+

201 

+

202 @property 

+

203 def html_id(self) -> Optional[str]: 

+

204 """Get the HTML id attribute for callback binding""" 

+

205 return self._html_id 

+

206 

+

207 def add_field(self, field: FormField): 

+

208 """ 

+

209 Add a field to this form. 

+

210 

+

211 Args: 

+

212 field: The FormField to add 

+

213 """ 

+

214 self._fields[field.name] = field 

+

215 field.form = self 

+

216 

+

217 def get_field(self, name: str) -> Optional[FormField]: 

+

218 """ 

+

219 Get a field by name. 

+

220 

+

221 Args: 

+

222 name: The name of the field to get 

+

223 

+

224 Returns: 

+

225 The FormField with the specified name, or None if not found 

+

226 """ 

+

227 return self._fields.get(name) 

+

228 

+

229 def get_values(self) -> Dict[str, Any]: 

+

230 """ 

+

231 Get the current values of all fields in this form. 

+

232 

+

233 Returns: 

+

234 A dictionary mapping field names to their current values 

+

235 """ 

+

236 return {name: field.value for name, field in self._fields.items()} 

+

237 

+

238 def execute(self) -> Any: 

+

239 """ 

+

240 Submit the form, executing the callback with the form values. 

+

241 

+

242 Returns: 

+

243 The result of the callback function, or the form values if no callback is provided. 

+

244 """ 

+

245 values = self.get_values() 

+

246 

+

247 if self._callback: 

+

248 return self._callback(self._form_id, values) 

+

249 

+

250 return values 

+

251 

+

252 

+

253class FormFieldType(Enum): 

+

254 """Enumeration of different types of form fields""" 

+

255 TEXT = 1 

+

256 PASSWORD = 2 

+

257 CHECKBOX = 3 

+

258 RADIO = 4 

+

259 SELECT = 5 

+

260 TEXTAREA = 6 

+

261 NUMBER = 7 

+

262 DATE = 8 

+

263 TIME = 9 

+

264 EMAIL = 10 

+

265 URL = 11 

+

266 COLOR = 12 

+

267 RANGE = 13 

+

268 HIDDEN = 14 

+

269 

+

270 

+

271class FormField: 

+

272 """ 

+

273 A field in a form that can accept user input. 

+

274 """ 

+

275 

+

276 def __init__(self, 

+

277 name: str, 

+

278 field_type: FormFieldType, 

+

279 label: Optional[str] = None, 

+

280 value: Any = None, 

+

281 required: bool = False, 

+

282 options: Optional[List[Tuple[str, str]]] = None): 

+

283 """ 

+

284 Initialize a form field. 

+

285 

+

286 Args: 

+

287 name: The name of this field 

+

288 field_type: The type of this field 

+

289 label: Optional label for this field 

+

290 value: Initial value for this field 

+

291 required: Whether this field is required 

+

292 options: Options for select, radio, or checkbox fields (list of (value, label) tuples) 

+

293 """ 

+

294 self._name = name 

+

295 self._field_type = field_type 

+

296 self._label = label or name 

+

297 self._value = value 

+

298 self._required = required 

+

299 self._options = options or [] 

+

300 self._form: Optional[Form] = None 

+

301 

+

302 @property 

+

303 def name(self) -> str: 

+

304 """Get the field name""" 

+

305 return self._name 

+

306 

+

307 @property 

+

308 def field_type(self) -> FormFieldType: 

+

309 """Get the field type""" 

+

310 return self._field_type 

+

311 

+

312 @property 

+

313 def label(self) -> str: 

+

314 """Get the field label""" 

+

315 return self._label 

+

316 

+

317 @property 

+

318 def value(self) -> Any: 

+

319 """Get the current field value""" 

+

320 return self._value 

+

321 

+

322 @value.setter 

+

323 def value(self, value: Any): 

+

324 """Set the field value""" 

+

325 self._value = value 

+

326 

+

327 @property 

+

328 def required(self) -> bool: 

+

329 """Check if the field is required""" 

+

330 return self._required 

+

331 

+

332 @property 

+

333 def options(self) -> List[Tuple[str, str]]: 

+

334 """Get the field options""" 

+

335 return self._options 

+

336 

+

337 @property 

+

338 def form(self) -> Optional[Form]: 

+

339 """Get the form containing this field""" 

+

340 return self._form 

+

341 

+

342 @form.setter 

+

343 def form(self, form: Form): 

+

344 """Set the form containing this field""" 

+

345 self._form = form 

+
+ + + diff --git a/cov_info/htmlcov/z_af715639580e2d86_inline_py.html b/cov_info/htmlcov/z_af715639580e2d86_inline_py.html new file mode 100644 index 0000000..be61f69 --- /dev/null +++ b/cov_info/htmlcov/z_af715639580e2d86_inline_py.html @@ -0,0 +1,556 @@ + + + + + Coverage for pyWebLayout/abstract/inline.py: 99% + + + + + +
+
+

+ Coverage for pyWebLayout/abstract/inline.py: + 99% +

+ +

+ 164 statements   + + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.15.4, + created at 2026-08-08 20:34 +0000 +

+ +
+
+
+

1from __future__ import annotations 

+

2from pyWebLayout.core import Hierarchical 

+

3from pyWebLayout.style import Font 

+

4from pyWebLayout.style.abstract_style import AbstractStyle 

+

5from typing import Tuple, Union, List, Optional, Dict, Any, Callable 

+

6from functools import lru_cache 

+

7import pyphen 

+

8 

+

9# Import LinkType for type hints (imported at module level to avoid F821 linting error) 

+

10from pyWebLayout.abstract.functional import LinkType 

+

11 

+

12 

+

13@lru_cache(maxsize=16) 

+

14def _hyphen_dict(language: Optional[str]) -> pyphen.Pyphen: 

+

15 """ 

+

16 The pyphen dictionary for a language, reused across words. 

+

17 

+

18 Pyphen caches the parsed dictionary file itself, but rebuilding the wrapper 

+

19 per word still costs about 40% of a hyphenation call, and hyphenation is 

+

20 attempted for every word that overflows its line. 

+

21 """ 

+

22 return pyphen.Pyphen(lang=language) 

+

23 

+

24 

+

25class Word: 

+

26 """ 

+

27 An abstract representation of a word in a document. Words can be split across 

+

28 lines or pages during rendering. This class manages the logical representation 

+

29 of a word without any rendering specifics. 

+

30 

+

31 Now uses AbstractStyle objects for memory efficiency and proper style management. 

+

32 """ 

+

33 

+

34 def __init__(self, 

+

35 text: str, 

+

36 style: Union[Font, 

+

37 AbstractStyle], 

+

38 background=None, 

+

39 previous: Union['Word', 

+

40 None] = None): 

+

41 """ 

+

42 Initialize a new Word. 

+

43 

+

44 Args: 

+

45 text: The text content of the word 

+

46 style: AbstractStyle object or Font object (for backward compatibility) 

+

47 background: Optional background color override 

+

48 previous: Reference to the previous word in sequence 

+

49 """ 

+

50 self._text = text 

+

51 self._style = style 

+

52 self._background = background 

+

53 self._previous = previous 

+

54 self._next = None 

+

55 self.concrete = None 

+

56 if previous: 

+

57 previous.add_next(self) 

+

58 

+

59 @classmethod 

+

60 def create_and_add_to(cls, text: str, container, style: Optional[Font] = None, 

+

61 background=None) -> 'Word': 

+

62 """ 

+

63 Create a new Word and add it to a container, inheriting style and language 

+

64 from the container if not explicitly provided. 

+

65 

+

66 This method provides a convenient way to create words that automatically 

+

67 inherit styling from their container (Paragraph, FormattedSpan, etc.) 

+

68 without copying string values - using object references instead. 

+

69 

+

70 Args: 

+

71 text: The text content of the word 

+

72 container: The container to add the word to (must have add_word method and style property) 

+

73 style: Optional Font style override. If None, inherits from container 

+

74 background: Optional background color override. If None, inherits from container 

+

75 

+

76 Returns: 

+

77 The newly created Word object 

+

78 

+

79 Raises: 

+

80 AttributeError: If the container doesn't have the required add_word method or style property 

+

81 """ 

+

82 # Inherit style from container if not provided 

+

83 if style is None: 

+

84 if hasattr(container, 'style'): 

+

85 style = container.style 

+

86 else: 

+

87 raise AttributeError( 

+

88 f"Container {type(container).__name__} must have a 'style' property") 

+

89 

+

90 # Inherit background from container if not provided 

+

91 if background is None and hasattr(container, 'background'): 

+

92 background = container.background 

+

93 

+

94 # Determine the previous word for proper linking 

+

95 previous = None 

+

96 if hasattr(container, '_words') and container._words: 

+

97 # Container has a _words list (like FormattedSpan) 

+

98 previous = container._words[-1] 

+

99 elif hasattr(container, 'words'): 

+

100 # Container has a words() method (like Paragraph) 

+

101 try: 

+

102 # Get the last word from the iterator 

+

103 for _, word in container.words(): 

+

104 previous = word 

+

105 except (StopIteration, TypeError): 

+

106 previous = None 

+

107 

+

108 # Create the new word 

+

109 word = cls(text, style, background, previous) 

+

110 

+

111 # Link the previous word to this new one 

+

112 if previous: 

+

113 previous.add_next(word) 

+

114 

+

115 # Add the word to the container 

+

116 if hasattr(container, 'add_word'): 

+

117 # Check if add_word expects a Word object or text string 

+

118 import inspect 

+

119 sig = inspect.signature(container.add_word) 

+

120 params = list(sig.parameters.keys()) 

+

121 

+

122 if len(params) > 0: 

+

123 # Peek at the parameter name to guess the expected type 

+

124 param_name = params[0] 

+

125 if param_name in ['word', 'word_obj', 'word_object']: 

+

126 # Expects a Word object 

+

127 container.add_word(word) 

+

128 else: 

+

129 # Might expect text string (like FormattedSpan.add_word) 

+

130 # In this case, we can't use the container's add_word as it would create 

+

131 # a duplicate Word. We need to add directly to the container's word 

+

132 # list. 

+

133 if hasattr(container, '_words'): 

+

134 container._words.append(word) 

+

135 else: 

+

136 # Fallback: try calling with the Word object anyway 

+

137 container.add_word(word) 

+

138 else: 

+

139 # No parameters, shouldn't happen with add_word methods 

+

140 container.add_word(word) 

+

141 else: 

+

142 raise AttributeError( 

+

143 f"Container {type(container).__name__} must have an 'add_word' method") 

+

144 

+

145 return word 

+

146 

+

147 def add_concete(self, text: Union[Any, Tuple[Any, Any]]): 

+

148 self.concrete = text 

+

149 

+

150 @property 

+

151 def text(self) -> str: 

+

152 """Get the text content of the word""" 

+

153 return self._text 

+

154 

+

155 @property 

+

156 def style(self) -> Font: 

+

157 """Get the font style of the word""" 

+

158 return self._style 

+

159 

+

160 @property 

+

161 def background(self): 

+

162 """Get the background color of the word""" 

+

163 return self._background 

+

164 

+

165 @property 

+

166 def previous(self) -> Union['Word', None]: 

+

167 """Get the previous word in sequence""" 

+

168 return self._previous 

+

169 

+

170 @property 

+

171 def next(self) -> Union['Word', None]: 

+

172 """Get the next word in sequence""" 

+

173 return self._next 

+

174 

+

175 def add_next(self, next_word: 'Word'): 

+

176 """Set the next word in sequence""" 

+

177 self._next = next_word 

+

178 

+

179 def with_style(self, style: Font) -> 'Word': 

+

180 """ 

+

181 Return a copy of this word carrying a different font. 

+

182 

+

183 Subclasses that hold extra state must override this, or that state is 

+

184 silently dropped when a caller restyles the word. Sequence links 

+

185 (previous/next) are deliberately not copied: the copy belongs to a 

+

186 different word chain, which the new container rebuilds as words are 

+

187 added to it. 

+

188 """ 

+

189 return Word(self._text, style, self._background) 

+

190 

+

191 def possible_hyphenation(self, language: str = None) -> bool: 

+

192 """ 

+

193 Hyphenate the word and store the parts. 

+

194 

+

195 Args: 

+

196 language: Language code for hyphenation. If None, uses the style's language. 

+

197 

+

198 Returns: 

+

199 bool: True if the word was hyphenated, False otherwise. 

+

200 """ 

+

201 

+

202 return list(_hyphen_dict(self._style.language).iterate(self._text)) 

+

203 

+

204 

+

205... 

+

206 

+

207 

+

208class FormattedSpan: 

+

209 """ 

+

210 A run of words with consistent formatting. 

+

211 This represents a sequence of words that share the same style attributes. 

+

212 """ 

+

213 

+

214 def __init__(self, style: Font, background=None): 

+

215 """ 

+

216 Initialize a new formatted span. 

+

217 

+

218 Args: 

+

219 style: Font style information for all words in this span 

+

220 background: Optional background color override 

+

221 """ 

+

222 self._style = style 

+

223 self._background = background if background else style.background 

+

224 self._words: List[Word] = [] 

+

225 

+

226 @classmethod 

+

227 def create_and_add_to( 

+

228 cls, 

+

229 container, 

+

230 style: Optional[Font] = None, 

+

231 background=None) -> 'FormattedSpan': 

+

232 """ 

+

233 Create a new FormattedSpan and add it to a container, inheriting style from 

+

234 the container if not explicitly provided. 

+

235 

+

236 Args: 

+

237 container: The container to add the span to (must have add_span method and style property) 

+

238 style: Optional Font style override. If None, inherits from container 

+

239 background: Optional background color override 

+

240 

+

241 Returns: 

+

242 The newly created FormattedSpan object 

+

243 

+

244 Raises: 

+

245 AttributeError: If the container doesn't have the required add_span method or style property 

+

246 """ 

+

247 # Inherit style from container if not provided 

+

248 if style is None: 

+

249 if hasattr(container, 'style'): 

+

250 style = container.style 

+

251 else: 

+

252 raise AttributeError( 

+

253 f"Container {type(container).__name__} must have a 'style' property") 

+

254 

+

255 # Inherit background from container if not provided 

+

256 if background is None and hasattr(container, 'background'): 

+

257 background = container.background 

+

258 

+

259 # Create the new span 

+

260 span = cls(style, background) 

+

261 

+

262 # Add the span to the container 

+

263 if hasattr(container, 'add_span'): 

+

264 container.add_span(span) 

+

265 else: 

+

266 raise AttributeError( 

+

267 f"Container {type(container).__name__} must have an 'add_span' method") 

+

268 

+

269 return span 

+

270 

+

271 @property 

+

272 def style(self) -> Font: 

+

273 """Get the font style of this span""" 

+

274 return self._style 

+

275 

+

276 @property 

+

277 def background(self): 

+

278 """Get the background color of this span""" 

+

279 return self._background 

+

280 

+

281 @property 

+

282 def words(self) -> List[Word]: 

+

283 """Get the list of words in this span""" 

+

284 return self._words 

+

285 

+

286 def add_word(self, text: str) -> Word: 

+

287 """ 

+

288 Create and add a new word to this span. 

+

289 

+

290 Args: 

+

291 text: The text content of the word 

+

292 

+

293 Returns: 

+

294 The newly created Word object 

+

295 """ 

+

296 # Get the previous word if any 

+

297 previous = self._words[-1] if self._words else None 

+

298 

+

299 # Create the new word 

+

300 word = Word(text, self._style, self._background, previous) 

+

301 

+

302 # Link the previous word to this new one 

+

303 if previous: 

+

304 previous.add_next(word) 

+

305 

+

306 # Add the word to our list 

+

307 self._words.append(word) 

+

308 

+

309 return word 

+

310 

+

311 

+

312class LinkedWord(Word): 

+

313 """ 

+

314 A Word that is also a Link - combines text content with hyperlink functionality. 

+

315 

+

316 When a word is part of a hyperlink, it becomes clickable and can trigger 

+

317 navigation or callbacks. Multiple words can share the same link destination. 

+

318 """ 

+

319 

+

320 def __init__(self, text: str, style: Union[Font, 'AbstractStyle'], 

+

321 location: str, link_type: Optional['LinkType'] = None, 

+

322 callback: Optional[Callable] = None, 

+

323 background=None, previous: Optional[Word] = None, 

+

324 params: Optional[Dict[str, Any]] = None, 

+

325 title: Optional[str] = None): 

+

326 """ 

+

327 Initialize a linked word. 

+

328 

+

329 Args: 

+

330 text: The text content of the word 

+

331 style: The font style 

+

332 location: The link target (URL, bookmark, etc.) 

+

333 link_type: Type of link (INTERNAL, EXTERNAL, etc.) 

+

334 callback: Optional callback for link activation 

+

335 background: Optional background color 

+

336 previous: Previous word in sequence 

+

337 params: Parameters for the link 

+

338 title: Tooltip/title for the link 

+

339 """ 

+

340 # Initialize Word first 

+

341 super().__init__(text, style, background, previous) 

+

342 

+

343 # Store link properties 

+

344 self._location = location 

+

345 self._link_type = link_type or LinkType.EXTERNAL 

+

346 self._callback = callback 

+

347 self._params = params or {} 

+

348 self._title = title 

+

349 

+

350 @property 

+

351 def location(self) -> str: 

+

352 """Get the link target location""" 

+

353 return self._location 

+

354 

+

355 @property 

+

356 def link_type(self): 

+

357 """Get the type of link""" 

+

358 return self._link_type 

+

359 

+

360 @property 

+

361 def link_callback(self) -> Optional[Callable]: 

+

362 """Get the link callback (distinct from word callback)""" 

+

363 return self._callback 

+

364 

+

365 @property 

+

366 def params(self) -> Dict[str, Any]: 

+

367 """Get the link parameters""" 

+

368 return self._params 

+

369 

+

370 @property 

+

371 def link_title(self) -> Optional[str]: 

+

372 """Get the link title/tooltip""" 

+

373 return self._title 

+

374 

+

375 def with_style(self, style: Font) -> 'LinkedWord': 

+

376 """Return a copy carrying a different font, keeping the link intact.""" 

+

377 return LinkedWord( 

+

378 self._text, 

+

379 style, 

+

380 self._location, 

+

381 link_type=self._link_type, 

+

382 callback=self._callback, 

+

383 background=self._background, 

+

384 params=dict(self._params), 

+

385 title=self._title, 

+

386 ) 

+

387 

+

388 def execute_link(self, context: Optional[Dict[str, Any]] = None) -> Any: 

+

389 """ 

+

390 Execute the link action. 

+

391 

+

392 Args: 

+

393 context: Optional context dict (e.g., {'text': word.text}) 

+

394 

+

395 Returns: 

+

396 The result of the link execution 

+

397 """ 

+

398 # Add word text to context 

+

399 full_context = {**self._params, 'text': self._text} 

+

400 if context: 400 ↛ 401line 400 didn't jump to line 401 because the condition on line 400 was never true

+

401 full_context.update(context) 

+

402 

+

403 if self._link_type in (LinkType.API, LinkType.FUNCTION) and self._callback: 

+

404 return self._callback(self._location, **full_context) 

+

405 else: 

+

406 # For INTERNAL and EXTERNAL links, return the location 

+

407 return self._location 

+

408 

+

409 

+

410class LineBreak(Hierarchical): 

+

411 """ 

+

412 A line break element that forces a new line within text content. 

+

413 While this is an inline element that can occur within paragraphs, 

+

414 it has block-like properties for consistency with the abstract model. 

+

415 

+

416 Uses Hierarchical mixin for parent-child relationship management. 

+

417 """ 

+

418 

+

419 def __init__(self): 

+

420 """Initialize a line break element.""" 

+

421 super().__init__() 

+

422 # Import here to avoid circular imports 

+

423 from .block import BlockType 

+

424 self._block_type = BlockType.LINE_BREAK 

+

425 

+

426 @property 

+

427 def block_type(self): 

+

428 """Get the block type for this line break""" 

+

429 return self._block_type 

+

430 

+

431 @classmethod 

+

432 def create_and_add_to(cls, container) -> 'LineBreak': 

+

433 """ 

+

434 Create a new LineBreak and add it to a container. 

+

435 

+

436 Args: 

+

437 container: The container to add the line break to 

+

438 

+

439 Returns: 

+

440 The newly created LineBreak object 

+

441 """ 

+

442 # Create the new line break 

+

443 line_break = cls() 

+

444 

+

445 # Add the line break to the container if it has an appropriate method 

+

446 if hasattr(container, 'add_line_break'): 

+

447 container.add_line_break(line_break) 

+

448 elif hasattr(container, 'add_element'): 

+

449 container.add_element(line_break) 

+

450 elif hasattr(container, 'add_word'): 

+

451 # Some containers might treat line breaks like words 

+

452 container.add_word(line_break) 

+

453 else: 

+

454 # Set parent relationship manually 

+

455 line_break.parent = container 

+

456 

+

457 return line_break 

+
+ + + diff --git a/cov_info/htmlcov/z_af715639580e2d86_interactive_image_py.html b/cov_info/htmlcov/z_af715639580e2d86_interactive_image_py.html new file mode 100644 index 0000000..6be5b25 --- /dev/null +++ b/cov_info/htmlcov/z_af715639580e2d86_interactive_image_py.html @@ -0,0 +1,262 @@ + + + + + Coverage for pyWebLayout/abstract/interactive_image.py: 80% + + + + + +
+
+

+ Coverage for pyWebLayout/abstract/interactive_image.py: + 80% +

+ +

+ 34 statements   + + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.15.4, + created at 2026-08-08 20:34 +0000 +

+ +
+
+
+

1""" 

+

2Interactive and queryable image for pyWebLayout. 

+

3 

+

4Provides an InteractiveImage class that combines Image with Interactable 

+

5and Queriable capabilities, allowing images to respond to tap events with 

+

6proper bounding box detection. 

+

7""" 

+

8 

+

9from typing import Optional, Callable, Tuple 

+

10import numpy as np 

+

11 

+

12from .block import Image 

+

13from ..core.base import Interactable, Queriable 

+

14 

+

15 

+

16class InteractiveImage(Image, Interactable, Queriable): 

+

17 """ 

+

18 An image that can be interacted with and queried for hit detection. 

+

19 

+

20 This combines pyWebLayout's Image block with Interactable and Queriable 

+

21 capabilities, allowing the image to: 

+

22 - Have a callback that fires when tapped 

+

23 - Know its rendered position (origin) 

+

24 - Detect if a point is within its bounds 

+

25 

+

26 Example: 

+

27 >>> img = InteractiveImage( 

+

28 ... source="cover.png", 

+

29 ... alt_text="Book Title", 

+

30 ... callback=lambda point: "/path/to/book.epub" 

+

31 ... ) 

+

32 >>> # After rendering, origin is set automatically 

+

33 >>> # Check if tap is inside 

+

34 >>> result = img.interact((120, 250)) 

+

35 >>> # Returns "/path/to/book.epub" if inside, None if outside 

+

36 """ 

+

37 

+

38 def __init__( 

+

39 self, 

+

40 source: str = "", 

+

41 alt_text: str = "", 

+

42 width: Optional[int] = None, 

+

43 height: Optional[int] = None, 

+

44 callback: Optional[Callable] = None 

+

45 ): 

+

46 """ 

+

47 Initialize an interactive image. 

+

48 

+

49 Args: 

+

50 source: The image source URL or path 

+

51 alt_text: Alternative text for accessibility 

+

52 width: Optional image width in pixels 

+

53 height: Optional image height in pixels 

+

54 callback: Function to call when image is tapped (receives point coordinates) 

+

55 """ 

+

56 # Initialize Image 

+

57 Image.__init__( 

+

58 self, 

+

59 source=source, 

+

60 alt_text=alt_text, 

+

61 width=width, 

+

62 height=height) 

+

63 

+

64 # Initialize Interactable 

+

65 Interactable.__init__(self, callback=callback) 

+

66 

+

67 # Initialize position tracking 

+

68 self._origin = np.array([0, 0]) # Will be set during rendering 

+

69 self.size = (width or 0, height or 0) # Will be updated during rendering 

+

70 

+

71 def interact(self, point: np.generic) -> Optional[any]: 

+

72 """ 

+

73 Handle interaction at the given point. 

+

74 

+

75 Only triggers the callback if the point is within the image bounds. 

+

76 

+

77 Args: 

+

78 point: The coordinates of the interaction (x, y) 

+

79 

+

80 Returns: 

+

81 The result of the callback if point is inside, None otherwise 

+

82 """ 

+

83 # Check if point is inside this image 

+

84 if self.in_object(point): 

+

85 # Point is inside, trigger callback 

+

86 if self._callback is not None: 

+

87 return self._callback(point) 

+

88 

+

89 return None 

+

90 

+

91 def in_object(self, point: np.generic) -> bool: 

+

92 """ 

+

93 Check if a point is within the image bounds. 

+

94 

+

95 Args: 

+

96 point: The coordinates to check (x, y) 

+

97 

+

98 Returns: 

+

99 True if point is inside the image, False otherwise 

+

100 """ 

+

101 point_array = np.array(point) 

+

102 relative_point = point_array - self._origin 

+

103 return np.all((0 <= relative_point) & (relative_point < self.size)) 

+

104 

+

105 @classmethod 

+

106 def create_and_add_to( 

+

107 cls, 

+

108 parent, 

+

109 source: str, 

+

110 alt_text: str = "", 

+

111 width: Optional[int] = None, 

+

112 height: Optional[int] = None, 

+

113 callback: Optional[Callable] = None 

+

114 ) -> 'InteractiveImage': 

+

115 """ 

+

116 Create an interactive image and add it to a parent block. 

+

117 

+

118 This is a convenience method that mimics the Image.create_and_add_to API 

+

119 but creates an InteractiveImage instead. 

+

120 

+

121 Args: 

+

122 parent: Parent block to add this image to 

+

123 source: The image source URL or path 

+

124 alt_text: Alternative text for accessibility 

+

125 width: Optional image width in pixels 

+

126 height: Optional image height in pixels 

+

127 callback: Function to call when image is tapped 

+

128 

+

129 Returns: 

+

130 The created InteractiveImage instance 

+

131 """ 

+

132 img = cls( 

+

133 source=source, 

+

134 alt_text=alt_text, 

+

135 width=width, 

+

136 height=height, 

+

137 callback=callback 

+

138 ) 

+

139 

+

140 # Add to parent using its add_block method 

+

141 if hasattr(parent, 'add_block'): 141 ↛ 142line 141 didn't jump to line 142 because the condition on line 141 was never true

+

142 parent.add_block(img) 

+

143 elif hasattr(parent, 'add_child'): 143 ↛ 144line 143 didn't jump to line 144 because the condition on line 143 was never true

+

144 parent.add_child(img) 

+

145 elif hasattr(parent, '_children'): 145 ↛ 147line 145 didn't jump to line 147 because the condition on line 145 was always true

+

146 parent._children.append(img) 

+

147 elif hasattr(parent, '_blocks'): 

+

148 parent._blocks.append(img) 

+

149 

+

150 return img 

+

151 

+

152 def set_rendered_bounds(self, origin: Tuple[int, int], size: Tuple[int, int]): 

+

153 """ 

+

154 Set the rendered position and size of this image. 

+

155 

+

156 This should be called by the renderer after it places the image. 

+

157 

+

158 Args: 

+

159 origin: (x, y) coordinates of top-left corner 

+

160 size: (width, height) of the rendered image 

+

161 """ 

+

162 self._origin = np.array(origin) 

+

163 self.size = size 

+
+ + + diff --git a/cov_info/htmlcov/z_ba7f6bdeb0188088___init___py.html b/cov_info/htmlcov/z_ba7f6bdeb0188088___init___py.html new file mode 100644 index 0000000..a56cf4a --- /dev/null +++ b/cov_info/htmlcov/z_ba7f6bdeb0188088___init___py.html @@ -0,0 +1,122 @@ + + + + + Coverage for pyWebLayout/style/__init__.py: 100% + + + + + +
+
+

+ Coverage for pyWebLayout/style/__init__.py: + 100% +

+ +

+ 6 statements   + + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.15.4, + created at 2026-08-08 20:34 +0000 +

+ +
+
+
+

1""" 

+

2Style system for the pyWebLayout library. 

+

3 

+

4This module provides the core styling components used throughout the library. 

+

5""" 

+

6 

+

7from .fonts import ( 

+

8 Font, FontWeight, FontStyle, TextDecoration, 

+

9 BundledFont, get_bundled_font_path, get_bundled_fonts_dir 

+

10) 

+

11from .abstract_style import ( 

+

12 AbstractStyle, AbstractStyleRegistry, FontFamily, FontSize 

+

13) 

+

14from .concrete_style import ConcreteStyle 

+

15from .page_style import PageStyle 

+

16from .alignment import Alignment 

+

17 

+

18__all__ = [ 

+

19 "Font", "FontWeight", "FontStyle", "TextDecoration", 

+

20 "BundledFont", "get_bundled_font_path", "get_bundled_fonts_dir", 

+

21 "AbstractStyle", "AbstractStyleRegistry", "FontFamily", "FontSize", 

+

22 "ConcreteStyle", "PageStyle", "Alignment" 

+

23] 

+
+ + + diff --git a/cov_info/htmlcov/z_ba7f6bdeb0188088_abstract_style_py.html b/cov_info/htmlcov/z_ba7f6bdeb0188088_abstract_style_py.html new file mode 100644 index 0000000..168c2ec --- /dev/null +++ b/cov_info/htmlcov/z_ba7f6bdeb0188088_abstract_style_py.html @@ -0,0 +1,454 @@ + + + + + Coverage for pyWebLayout/style/abstract_style.py: 76% + + + + + +
+
+

+ Coverage for pyWebLayout/style/abstract_style.py: + 76% +

+ +

+ 135 statements   + + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.15.4, + created at 2026-08-08 20:34 +0000 +

+ +
+
+
+

1""" 

+

2Abstract style system for storing document styling intent. 

+

3 

+

4This module defines styles in terms of semantic meaning rather than concrete 

+

5rendering parameters, allowing for flexible interpretation by different 

+

6rendering systems and user preferences. 

+

7""" 

+

8 

+

9from .alignment import Alignment 

+

10from typing import Dict, Optional, Tuple, Union 

+

11from dataclasses import dataclass 

+

12from enum import Enum 

+

13from .fonts import FontWeight, FontStyle, TextDecoration 

+

14 

+

15 

+

16class FontFamily(Enum): 

+

17 """Semantic font family categories""" 

+

18 SERIF = "serif" 

+

19 SANS_SERIF = "sans-serif" 

+

20 MONOSPACE = "monospace" 

+

21 CURSIVE = "cursive" 

+

22 FANTASY = "fantasy" 

+

23 

+

24 

+

25class FontSize(Enum): 

+

26 """Semantic font sizes""" 

+

27 XX_SMALL = "xx-small" 

+

28 X_SMALL = "x-small" 

+

29 SMALL = "small" 

+

30 MEDIUM = "medium" 

+

31 LARGE = "large" 

+

32 X_LARGE = "x-large" 

+

33 XX_LARGE = "xx-large" 

+

34 

+

35 # Allow numeric values as well 

+

36 @classmethod 

+

37 def from_value(cls, value: Union[str, int, float]) -> Union['FontSize', int]: 

+

38 """Convert a value to FontSize enum or return numeric value""" 

+

39 if isinstance(value, (int, float)): 

+

40 return int(value) 

+

41 if isinstance(value, str): 

+

42 try: 

+

43 return cls(value) 

+

44 except ValueError: 

+

45 # Try to parse as number 

+

46 try: 

+

47 return int(float(value)) 

+

48 except ValueError: 

+

49 return cls.MEDIUM 

+

50 return cls.MEDIUM 

+

51 

+

52 

+

53# Import Alignment from the centralized location 

+

54 

+

55# Use Alignment for text alignment 

+

56TextAlign = Alignment 

+

57 

+

58 

+

59@dataclass(frozen=True) 

+

60class AbstractStyle: 

+

61 """ 

+

62 Abstract representation of text styling that captures semantic intent 

+

63 rather than concrete rendering parameters. 

+

64 

+

65 This allows the same document to be rendered differently based on 

+

66 user preferences, device capabilities, or accessibility requirements. 

+

67 

+

68 Being frozen=True makes this class hashable and immutable, which is 

+

69 perfect for use as dictionary keys and preventing accidental modification. 

+

70 """ 

+

71 

+

72 # Font properties (semantic) 

+

73 font_family: FontFamily = FontFamily.SERIF 

+

74 font_size: Union[FontSize, int] = FontSize.MEDIUM 

+

75 font_weight: FontWeight = FontWeight.NORMAL 

+

76 font_style: FontStyle = FontStyle.NORMAL 

+

77 text_decoration: TextDecoration = TextDecoration.NONE 

+

78 

+

79 # Color (as semantic names or RGB) 

+

80 color: Union[str, Tuple[int, int, int]] = "black" 

+

81 background_color: Optional[Union[str, Tuple[int, int, int, int]]] = None 

+

82 

+

83 # Text properties 

+

84 # None means "not specified": the page's default_alignment applies. 

+

85 text_align: Optional[TextAlign] = None 

+

86 line_height: Optional[Union[str, float]] = None # "normal", "1.2", 1.5, etc. 

+

87 letter_spacing: Optional[Union[str, float]] = None # "normal", "0.1em", etc. 

+

88 word_spacing: Optional[Union[str, float]] = None 

+

89 word_spacing_min: Optional[Union[str, float]] = None # Minimum allowed word spacing 

+

90 word_spacing_max: Optional[Union[str, float]] = None # Maximum allowed word spacing 

+

91 

+

92 # Language and locale 

+

93 language: str = "en-US" 

+

94 

+

95 # Hierarchy properties 

+

96 parent_style_id: Optional[str] = None 

+

97 

+

98 def __post_init__(self): 

+

99 """Validate and normalize values after creation""" 

+

100 # Normalize font_size if it's a string that could be a number 

+

101 if isinstance(self.font_size, str): 101 ↛ 102line 101 didn't jump to line 102 because the condition on line 101 was never true

+

102 try: 

+

103 object.__setattr__(self, 'font_size', int(float(self.font_size))) 

+

104 except ValueError: 

+

105 # Keep as is if it's a semantic size name 

+

106 pass 

+

107 

+

108 def __hash__(self) -> int: 

+

109 """ 

+

110 Custom hash implementation to ensure consistent hashing. 

+

111 

+

112 Since this is a frozen dataclass, it should be hashable by default, 

+

113 but we provide a custom implementation to ensure all fields are 

+

114 properly considered and to handle the Union types correctly. 

+

115 

+

116 The result is memoised on first use. Styles are used as dictionary keys 

+

117 throughout parsing and style resolution, and five of the fields are enum 

+

118 members whose own __hash__ is a Python-level call, so rebuilding the 

+

119 15-tuple on every lookup was a measurable share of document parsing. The 

+

120 class is frozen, so the value cannot go stale. 

+

121 """ 

+

122 cached = self.__dict__.get('_hash_cache') 

+

123 if cached is not None: 

+

124 return cached 

+

125 

+

126 # Convert all values to hashable forms 

+

127 hashable_values = ( 

+

128 self.font_family, 

+

129 self.font_size if isinstance(self.font_size, int) else self.font_size, 

+

130 self.font_weight, 

+

131 self.font_style, 

+

132 self.text_decoration, 

+

133 self.color if isinstance(self.color, (str, tuple)) else str(self.color), 

+

134 self.background_color, 

+

135 self.text_align, 

+

136 self.line_height, 

+

137 self.letter_spacing, 

+

138 self.word_spacing, 

+

139 self.word_spacing_min, 

+

140 self.word_spacing_max, 

+

141 self.language, 

+

142 self.parent_style_id 

+

143 ) 

+

144 

+

145 result = hash(hashable_values) 

+

146 object.__setattr__(self, '_hash_cache', result) 

+

147 return result 

+

148 

+

149 def merge_with(self, other: 'AbstractStyle') -> 'AbstractStyle': 

+

150 """ 

+

151 Create a new AbstractStyle by merging this one with another. 

+

152 The other style's properties take precedence. 

+

153 

+

154 Args: 

+

155 other: AbstractStyle to merge with this one 

+

156 

+

157 Returns: 

+

158 New AbstractStyle with merged values 

+

159 """ 

+

160 # Get all fields from both styles 

+

161 current_dict = { 

+

162 field.name: getattr(self, field.name) 

+

163 for field in self.__dataclass_fields__.values() 

+

164 } 

+

165 

+

166 other_dict = { 

+

167 field.name: getattr(other, field.name) 

+

168 for field in other.__dataclass_fields__.values() 

+

169 if getattr(other, field.name) != field.default 

+

170 } 

+

171 

+

172 # Merge dictionaries (other takes precedence) 

+

173 merged_dict = current_dict.copy() 

+

174 merged_dict.update(other_dict) 

+

175 

+

176 return AbstractStyle(**merged_dict) 

+

177 

+

178 def with_modifications(self, **kwargs) -> 'AbstractStyle': 

+

179 """ 

+

180 Create a new AbstractStyle with specified modifications. 

+

181 

+

182 Args: 

+

183 **kwargs: Properties to modify 

+

184 

+

185 Returns: 

+

186 New AbstractStyle with modifications applied 

+

187 """ 

+

188 current_dict = { 

+

189 field.name: getattr(self, field.name) 

+

190 for field in self.__dataclass_fields__.values() 

+

191 } 

+

192 

+

193 current_dict.update(kwargs) 

+

194 return AbstractStyle(**current_dict) 

+

195 

+

196 

+

197class AbstractStyleRegistry: 

+

198 """ 

+

199 Registry for managing abstract document styles. 

+

200 

+

201 This registry stores the semantic styling intent and provides 

+

202 deduplication and inheritance capabilities using hashable AbstractStyle objects. 

+

203 """ 

+

204 

+

205 def __init__(self): 

+

206 """Initialize an empty abstract style registry.""" 

+

207 self._styles: Dict[str, AbstractStyle] = {} 

+

208 # Reverse mapping using hashable styles 

+

209 self._style_to_id: Dict[AbstractStyle, str] = {} 

+

210 self._next_id = 1 

+

211 

+

212 # Create and register the default style 

+

213 self._default_style = self._create_default_style() 

+

214 

+

215 def _create_default_style(self) -> AbstractStyle: 

+

216 """Create the default document style.""" 

+

217 default_style = AbstractStyle() 

+

218 style_id = "default" 

+

219 self._styles[style_id] = default_style 

+

220 self._style_to_id[default_style] = style_id 

+

221 return default_style 

+

222 

+

223 @property 

+

224 def default_style(self) -> AbstractStyle: 

+

225 """Get the default style for the document.""" 

+

226 return self._default_style 

+

227 

+

228 def _generate_style_id(self) -> str: 

+

229 """Generate a unique style ID.""" 

+

230 style_id = f"abstract_style_{self._next_id}" 

+

231 self._next_id += 1 

+

232 return style_id 

+

233 

+

234 def get_style_id(self, style: AbstractStyle) -> Optional[str]: 

+

235 """ 

+

236 Get the ID for a given style if it exists in the registry. 

+

237 

+

238 Args: 

+

239 style: AbstractStyle to find 

+

240 

+

241 Returns: 

+

242 Style ID if found, None otherwise 

+

243 """ 

+

244 return self._style_to_id.get(style) 

+

245 

+

246 def register_style( 

+

247 self, 

+

248 style: AbstractStyle, 

+

249 style_id: Optional[str] = None) -> str: 

+

250 """ 

+

251 Register a style in the registry. 

+

252 

+

253 Args: 

+

254 style: AbstractStyle to register 

+

255 style_id: Optional style ID. If None, one will be generated 

+

256 

+

257 Returns: 

+

258 The style ID 

+

259 """ 

+

260 # Check if style already exists 

+

261 existing_id = self.get_style_id(style) 

+

262 if existing_id is not None: 262 ↛ 263line 262 didn't jump to line 263 because the condition on line 262 was never true

+

263 return existing_id 

+

264 

+

265 if style_id is None: 265 ↛ 268line 265 didn't jump to line 268 because the condition on line 265 was always true

+

266 style_id = self._generate_style_id() 

+

267 

+

268 self._styles[style_id] = style 

+

269 self._style_to_id[style] = style_id 

+

270 return style_id 

+

271 

+

272 def get_or_create_style(self, 

+

273 style: Optional[AbstractStyle] = None, 

+

274 parent_id: Optional[str] = None, 

+

275 **kwargs) -> Tuple[str, AbstractStyle]: 

+

276 """ 

+

277 Get an existing style or create a new one. 

+

278 

+

279 Args: 

+

280 style: AbstractStyle object. If None, created from kwargs 

+

281 parent_id: Optional parent style ID 

+

282 **kwargs: Individual style properties (used if style is None) 

+

283 

+

284 Returns: 

+

285 Tuple of (style_id, AbstractStyle) 

+

286 """ 

+

287 # Create style object if not provided 

+

288 if style is None: 

+

289 # Filter out None values from kwargs 

+

290 filtered_kwargs = {k: v for k, v in kwargs.items() if v is not None} 

+

291 if parent_id: 291 ↛ 292line 291 didn't jump to line 292 because the condition on line 291 was never true

+

292 filtered_kwargs['parent_style_id'] = parent_id 

+

293 style = AbstractStyle(**filtered_kwargs) 

+

294 

+

295 # Check if we already have this style (using hashable property) 

+

296 existing_id = self.get_style_id(style) 

+

297 if existing_id is not None: 

+

298 return existing_id, style 

+

299 

+

300 # Create new style 

+

301 style_id = self.register_style(style) 

+

302 return style_id, style 

+

303 

+

304 def get_style_by_id(self, style_id: str) -> Optional[AbstractStyle]: 

+

305 """Get a style by its ID.""" 

+

306 return self._styles.get(style_id) 

+

307 

+

308 def create_derived_style(self, base_style_id: str, ** 

+

309 modifications) -> Tuple[str, AbstractStyle]: 

+

310 """ 

+

311 Create a new style derived from a base style. 

+

312 

+

313 Args: 

+

314 base_style_id: ID of the base style 

+

315 **modifications: Properties to modify 

+

316 

+

317 Returns: 

+

318 Tuple of (new_style_id, new_AbstractStyle) 

+

319 """ 

+

320 base_style = self.get_style_by_id(base_style_id) 

+

321 if base_style is None: 321 ↛ 322line 321 didn't jump to line 322 because the condition on line 321 was never true

+

322 raise ValueError(f"Base style '{base_style_id}' not found") 

+

323 

+

324 # Create derived style 

+

325 derived_style = base_style.with_modifications(**modifications) 

+

326 return self.get_or_create_style(derived_style) 

+

327 

+

328 def resolve_effective_style(self, style_id: str) -> AbstractStyle: 

+

329 """ 

+

330 Resolve the effective style including inheritance. 

+

331 

+

332 Args: 

+

333 style_id: Style ID to resolve 

+

334 

+

335 Returns: 

+

336 Effective AbstractStyle with inheritance applied 

+

337 """ 

+

338 style = self.get_style_by_id(style_id) 

+

339 if style is None: 339 ↛ 340line 339 didn't jump to line 340 because the condition on line 339 was never true

+

340 return self._default_style 

+

341 

+

342 if style.parent_style_id is None: 342 ↛ 346line 342 didn't jump to line 346 because the condition on line 342 was always true

+

343 return style 

+

344 

+

345 # Recursively resolve parent styles 

+

346 parent_style = self.resolve_effective_style(style.parent_style_id) 

+

347 return parent_style.merge_with(style) 

+

348 

+

349 def get_all_styles(self) -> Dict[str, AbstractStyle]: 

+

350 """Get all registered styles.""" 

+

351 return self._styles.copy() 

+

352 

+

353 def get_style_count(self) -> int: 

+

354 """Get the number of registered styles.""" 

+

355 return len(self._styles) 

+
+ + + diff --git a/cov_info/htmlcov/z_ba7f6bdeb0188088_alignment_py.html b/cov_info/htmlcov/z_ba7f6bdeb0188088_alignment_py.html new file mode 100644 index 0000000..130d634 --- /dev/null +++ b/cov_info/htmlcov/z_ba7f6bdeb0188088_alignment_py.html @@ -0,0 +1,124 @@ + + + + + Coverage for pyWebLayout/style/alignment.py: 91% + + + + + +
+
+

+ Coverage for pyWebLayout/style/alignment.py: + 91% +

+ +

+ 11 statements   + + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.15.4, + created at 2026-08-08 20:34 +0000 +

+ +
+
+
+

1""" 

+

2Alignment options for the pyWebLayout library. 

+

3 

+

4This module provides alignment-related functionality. 

+

5""" 

+

6 

+

7from enum import Enum 

+

8 

+

9 

+

10class Alignment(Enum): 

+

11 """Text and box alignment options""" 

+

12 # Horizontal alignment 

+

13 LEFT = "left" 

+

14 RIGHT = "right" 

+

15 CENTER = "center" 

+

16 JUSTIFY = "justify" 

+

17 

+

18 # Vertical alignment 

+

19 TOP = "top" 

+

20 MIDDLE = "middle" 

+

21 BOTTOM = "bottom" 

+

22 

+

23 def __str__(self): 

+

24 """Return the string value of the alignment.""" 

+

25 return self.value 

+
+ + + diff --git a/cov_info/htmlcov/z_ba7f6bdeb0188088_concrete_style_py.html b/cov_info/htmlcov/z_ba7f6bdeb0188088_concrete_style_py.html new file mode 100644 index 0000000..d5236a4 --- /dev/null +++ b/cov_info/htmlcov/z_ba7f6bdeb0188088_concrete_style_py.html @@ -0,0 +1,584 @@ + + + + + Coverage for pyWebLayout/style/concrete_style.py: 63% + + + + + +
+
+

+ Coverage for pyWebLayout/style/concrete_style.py: + 63% +

+ +

+ 207 statements   + + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.15.4, + created at 2026-08-08 20:34 +0000 +

+ +
+
+
+

1""" 

+

2Concrete style system for actual rendering parameters. 

+

3 

+

4This module converts abstract styles to concrete rendering parameters based on 

+

5user preferences, device capabilities, and rendering context. 

+

6""" 

+

7 

+

8from typing import Dict, Optional, Tuple, Union 

+

9from dataclasses import dataclass 

+

10from .abstract_style import AbstractStyle, FontFamily, FontSize 

+

11from pyWebLayout.style.alignment import Alignment as TextAlign 

+

12from .fonts import Font, FontWeight, FontStyle, TextDecoration 

+

13 

+

14 

+

15@dataclass(frozen=True) 

+

16class RenderingContext: 

+

17 """ 

+

18 Context information for style resolution. 

+

19 Contains user preferences and device capabilities. 

+

20 """ 

+

21 

+

22 # User preferences 

+

23 base_font_size: int = 16 # Base font size in points 

+

24 font_scale_factor: float = 1.0 # Global font scaling 

+

25 preferred_serif_font: Optional[str] = None 

+

26 preferred_sans_serif_font: Optional[str] = None 

+

27 preferred_monospace_font: Optional[str] = None 

+

28 

+

29 # Device/environment info 

+

30 dpi: int = 96 # Dots per inch 

+

31 available_width: Optional[int] = None # Available width in pixels 

+

32 available_height: Optional[int] = None # Available height in pixels 

+

33 

+

34 # Accessibility preferences 

+

35 high_contrast: bool = False 

+

36 large_text: bool = False 

+

37 reduce_motion: bool = False 

+

38 

+

39 # Language and locale 

+

40 default_language: str = "en-US" 

+

41 

+

42 

+

43@dataclass(frozen=True) 

+

44class ConcreteStyle: 

+

45 """ 

+

46 Concrete representation of text styling with actual rendering parameters. 

+

47 

+

48 This contains the resolved font files, pixel sizes, actual colors, etc. 

+

49 that will be used for rendering. This is also hashable for efficient caching. 

+

50 """ 

+

51 

+

52 # Concrete font properties 

+

53 font_path: Optional[str] = None 

+

54 font_size: int = 16 # Always in points/pixels 

+

55 color: Tuple[int, int, int] = (0, 0, 0) # Always RGB 

+

56 background_color: Optional[Tuple[int, int, int, int]] = None # Always RGBA or None 

+

57 

+

58 # Font attributes 

+

59 weight: FontWeight = FontWeight.NORMAL 

+

60 style: FontStyle = FontStyle.NORMAL 

+

61 decoration: TextDecoration = TextDecoration.NONE 

+

62 

+

63 # Layout properties 

+

64 # None means "not specified": the page's default_alignment applies. 

+

65 text_align: Optional[TextAlign] = None 

+

66 line_height: float = 1.0 # Multiplier 

+

67 letter_spacing: float = 0.0 # In pixels 

+

68 word_spacing: float = 0.0 # In pixels 

+

69 word_spacing_min: float = 0.0 # Minimum word spacing in pixels 

+

70 word_spacing_max: float = 0.0 # Maximum word spacing in pixels 

+

71 

+

72 # Language and locale 

+

73 language: str = "en-US" 

+

74 min_hyphenation_width: int = 64 # In pixels 

+

75 

+

76 # Reference to source abstract style 

+

77 abstract_style: Optional[AbstractStyle] = None 

+

78 

+

79 def create_font(self) -> Font: 

+

80 """Create a Font object from this concrete style.""" 

+

81 return Font( 

+

82 font_path=self.font_path, 

+

83 font_size=self.font_size, 

+

84 colour=self.color, 

+

85 weight=self.weight, 

+

86 style=self.style, 

+

87 decoration=self.decoration, 

+

88 background=self.background_color, 

+

89 language=self.language, 

+

90 min_hyphenation_width=self.min_hyphenation_width 

+

91 ) 

+

92 

+

93 

+

94class StyleResolver: 

+

95 """ 

+

96 Resolves abstract styles to concrete styles based on rendering context. 

+

97 

+

98 This class handles the conversion from semantic styling intent to actual 

+

99 rendering parameters, applying user preferences and device capabilities. 

+

100 """ 

+

101 

+

102 def __init__(self, context: RenderingContext): 

+

103 """ 

+

104 Initialize the style resolver with a rendering context. 

+

105 

+

106 Args: 

+

107 context: RenderingContext with user preferences and device info 

+

108 """ 

+

109 self.context = context 

+

110 self._concrete_cache: Dict[AbstractStyle, ConcreteStyle] = {} 

+

111 

+

112 # Font size mapping for semantic sizes 

+

113 self._semantic_font_sizes = { 

+

114 FontSize.XX_SMALL: 0.6, 

+

115 FontSize.X_SMALL: 0.75, 

+

116 FontSize.SMALL: 0.89, 

+

117 FontSize.MEDIUM: 1.0, 

+

118 FontSize.LARGE: 1.2, 

+

119 FontSize.X_LARGE: 1.5, 

+

120 FontSize.XX_LARGE: 2.0, 

+

121 } 

+

122 

+

123 # Color name mapping 

+

124 self._color_names = { 

+

125 "black": (0, 0, 0), 

+

126 "white": (255, 255, 255), 

+

127 "red": (255, 0, 0), 

+

128 "green": (0, 128, 0), 

+

129 "blue": (0, 0, 255), 

+

130 "yellow": (255, 255, 0), 

+

131 "cyan": (0, 255, 255), 

+

132 "magenta": (255, 0, 255), 

+

133 "silver": (192, 192, 192), 

+

134 "gray": (128, 128, 128), 

+

135 "maroon": (128, 0, 0), 

+

136 "olive": (128, 128, 0), 

+

137 "lime": (0, 255, 0), 

+

138 "aqua": (0, 255, 255), 

+

139 "teal": (0, 128, 128), 

+

140 "navy": (0, 0, 128), 

+

141 "fuchsia": (255, 0, 255), 

+

142 "purple": (128, 0, 128), 

+

143 } 

+

144 

+

145 def resolve_style(self, abstract_style: AbstractStyle) -> ConcreteStyle: 

+

146 """ 

+

147 Resolve an abstract style to a concrete style. 

+

148 

+

149 Args: 

+

150 abstract_style: AbstractStyle to resolve 

+

151 

+

152 Returns: 

+

153 ConcreteStyle with concrete rendering parameters 

+

154 """ 

+

155 # Check cache first 

+

156 if abstract_style in self._concrete_cache: 

+

157 return self._concrete_cache[abstract_style] 

+

158 

+

159 # Resolve each property 

+

160 font_path = self._resolve_font_path(abstract_style.font_family) 

+

161 font_size = self._resolve_font_size(abstract_style.font_size) 

+

162 # Ensure font_size is always an int before using in arithmetic 

+

163 font_size = int(font_size) 

+

164 color = self._resolve_color(abstract_style.color) 

+

165 background_color = self._resolve_background_color( 

+

166 abstract_style.background_color) 

+

167 line_height = self._resolve_line_height(abstract_style.line_height) 

+

168 letter_spacing = self._resolve_letter_spacing( 

+

169 abstract_style.letter_spacing, font_size) 

+

170 word_spacing = self._resolve_word_spacing( 

+

171 abstract_style.word_spacing, font_size) 

+

172 word_spacing_min = self._resolve_word_spacing( 

+

173 abstract_style.word_spacing_min, font_size) 

+

174 word_spacing_max = self._resolve_word_spacing( 

+

175 abstract_style.word_spacing_max, font_size) 

+

176 min_hyphenation_width = max(int(font_size) * 4, 32) # At least 32 pixels 

+

177 

+

178 # Apply default logic for word spacing constraints 

+

179 if word_spacing_min == 0.0 and word_spacing_max == 0.0: 

+

180 # If no constraints specified, use base word_spacing as reference 

+

181 if word_spacing > 0.0: 

+

182 word_spacing_min = word_spacing 

+

183 word_spacing_max = word_spacing * 2 

+

184 else: 

+

185 # Default constraints when no word spacing is specified 

+

186 word_spacing_min = 2.0 # Minimum 2 pixels 

+

187 word_spacing_max = font_size * 0.5 # Maximum 50% of font size 

+

188 elif word_spacing_min == 0.0: 

+

189 # Only max specified, use base word_spacing or min default 

+

190 word_spacing_min = max(word_spacing, 2.0) 

+

191 elif word_spacing_max == 0.0: 

+

192 # Only min specified, use base word_spacing or reasonable multiple 

+

193 word_spacing_max = max(word_spacing, word_spacing_min * 2) 

+

194 

+

195 # Create concrete style 

+

196 concrete_style = ConcreteStyle( 

+

197 font_path=font_path, 

+

198 font_size=font_size, 

+

199 color=color, 

+

200 background_color=background_color, 

+

201 weight=abstract_style.font_weight, 

+

202 style=abstract_style.font_style, 

+

203 decoration=abstract_style.text_decoration, 

+

204 text_align=abstract_style.text_align, 

+

205 line_height=line_height, 

+

206 letter_spacing=letter_spacing, 

+

207 word_spacing=word_spacing, 

+

208 word_spacing_min=word_spacing_min, 

+

209 word_spacing_max=word_spacing_max, 

+

210 language=abstract_style.language, 

+

211 min_hyphenation_width=min_hyphenation_width, 

+

212 abstract_style=abstract_style 

+

213 ) 

+

214 

+

215 # Cache and return 

+

216 self._concrete_cache[abstract_style] = concrete_style 

+

217 return concrete_style 

+

218 

+

219 def _resolve_font_path(self, font_family: FontFamily) -> Optional[str]: 

+

220 """Resolve font family to actual font file path.""" 

+

221 if font_family == FontFamily.SERIF: 221 ↛ 223line 221 didn't jump to line 223 because the condition on line 221 was always true

+

222 return self.context.preferred_serif_font 

+

223 elif font_family == FontFamily.SANS_SERIF: 

+

224 return self.context.preferred_sans_serif_font 

+

225 elif font_family == FontFamily.MONOSPACE: 

+

226 return self.context.preferred_monospace_font 

+

227 else: 

+

228 # For cursive and fantasy, fall back to sans-serif 

+

229 return self.context.preferred_sans_serif_font 

+

230 

+

231 def _resolve_font_size(self, font_size: Union[FontSize, int]) -> int: 

+

232 """Resolve font size to actual pixel/point size.""" 

+

233 # Ensure we handle FontSize enums properly 

+

234 if isinstance(font_size, FontSize): 

+

235 # Semantic size, convert to multiplier 

+

236 multiplier = self._semantic_font_sizes.get(font_size, 1.0) 

+

237 base_size = int(self.context.base_font_size * multiplier) 

+

238 elif isinstance(font_size, int): 238 ↛ 243line 238 didn't jump to line 243 because the condition on line 238 was always true

+

239 # Already a concrete size, apply scaling 

+

240 base_size = font_size 

+

241 else: 

+

242 # Fallback for any other type - try to convert to int 

+

243 try: 

+

244 base_size = int(font_size) 

+

245 except (ValueError, TypeError): 

+

246 # If conversion fails, use default 

+

247 base_size = self.context.base_font_size 

+

248 

+

249 # Apply global font scaling 

+

250 final_size = int(base_size * self.context.font_scale_factor) 

+

251 

+

252 # Apply accessibility adjustments 

+

253 if self.context.large_text: 

+

254 final_size = int(final_size * 1.2) 

+

255 

+

256 # Ensure we always return an int, minimum 8pt font 

+

257 return max(int(final_size), 8) 

+

258 

+

259 def _resolve_color( 

+

260 self, color: Union[str, Tuple[int, int, int]]) -> Tuple[int, int, int]: 

+

261 """Resolve color to RGB tuple.""" 

+

262 if isinstance(color, tuple): 

+

263 return color 

+

264 

+

265 if isinstance(color, str): 265 ↛ 300line 265 didn't jump to line 300 because the condition on line 265 was always true

+

266 # Check if it's a named color 

+

267 if color.lower() in self._color_names: 

+

268 base_color = self._color_names[color.lower()] 

+

269 elif color.startswith('#'): 269 ↛ 286line 269 didn't jump to line 286 because the condition on line 269 was always true

+

270 # Parse hex color 

+

271 try: 

+

272 hex_color = color[1:] 

+

273 if len(hex_color) == 3: 273 ↛ 275line 273 didn't jump to line 275 because the condition on line 273 was never true

+

274 # Short hex format #RGB -> #RRGGBB 

+

275 hex_color = ''.join(c * 2 for c in hex_color) 

+

276 if len(hex_color) == 6: 276 ↛ 282line 276 didn't jump to line 282 because the condition on line 276 was always true

+

277 r = int(hex_color[0:2], 16) 

+

278 g = int(hex_color[2:4], 16) 

+

279 b = int(hex_color[4:6], 16) 

+

280 base_color = (r, g, b) 

+

281 else: 

+

282 base_color = (0, 0, 0) # Fallback to black 

+

283 except ValueError: 

+

284 base_color = (0, 0, 0) # Fallback to black 

+

285 else: 

+

286 base_color = (0, 0, 0) # Fallback to black 

+

287 

+

288 # Apply high contrast if needed 

+

289 if self.context.high_contrast: 289 ↛ 291line 289 didn't jump to line 291 because the condition on line 289 was never true

+

290 # Simple high contrast: make dark colors black, light colors white 

+

291 r, g, b = base_color 

+

292 brightness = (r + g + b) / 3 

+

293 if brightness < 128: 

+

294 base_color = (0, 0, 0) # Black 

+

295 else: 

+

296 base_color = (255, 255, 255) # White 

+

297 

+

298 return base_color 

+

299 

+

300 return (0, 0, 0) # Fallback to black 

+

301 

+

302 def _resolve_background_color(self, 

+

303 bg_color: Optional[Union[str, 

+

304 Tuple[int, 

+

305 int, 

+

306 int, 

+

307 int]]]) -> Optional[Tuple[int, 

+

308 int, 

+

309 int, 

+

310 int]]: 

+

311 """Resolve background color to RGBA tuple or None.""" 

+

312 if bg_color is None: 312 ↛ 315line 312 didn't jump to line 315 because the condition on line 312 was always true

+

313 return None 

+

314 

+

315 if isinstance(bg_color, tuple): 

+

316 if len(bg_color) == 3: 

+

317 # RGB -> RGBA 

+

318 return bg_color + (255,) 

+

319 return bg_color 

+

320 

+

321 if isinstance(bg_color, str): 

+

322 if bg_color.lower() == "transparent": 

+

323 return None 

+

324 

+

325 # Resolve as RGB then add alpha 

+

326 rgb = self._resolve_color(bg_color) 

+

327 return rgb + (255,) 

+

328 

+

329 return None 

+

330 

+

331 def _resolve_line_height(self, line_height: Optional[Union[str, float]]) -> float: 

+

332 """Resolve line height to multiplier.""" 

+

333 if line_height is None or line_height == "normal": 333 ↛ 336line 333 didn't jump to line 336 because the condition on line 333 was always true

+

334 return 1.2 # Default line height 

+

335 

+

336 if isinstance(line_height, (int, float)): 

+

337 return float(line_height) 

+

338 

+

339 if isinstance(line_height, str): 

+

340 try: 

+

341 return float(line_height) 

+

342 except ValueError: 

+

343 return 1.2 # Fallback 

+

344 

+

345 return 1.2 

+

346 

+

347 def _resolve_letter_spacing( 

+

348 self, letter_spacing: Optional[Union[str, float]], font_size: int) -> float: 

+

349 """Resolve letter spacing to pixels.""" 

+

350 if letter_spacing is None or letter_spacing == "normal": 350 ↛ 353line 350 didn't jump to line 353 because the condition on line 350 was always true

+

351 return 0.0 

+

352 

+

353 if isinstance(letter_spacing, (int, float)): 

+

354 return float(letter_spacing) 

+

355 

+

356 if isinstance(letter_spacing, str): 

+

357 if letter_spacing.endswith("em"): 

+

358 try: 

+

359 em_value = float(letter_spacing[:-2]) 

+

360 return em_value * font_size 

+

361 except ValueError: 

+

362 return 0.0 

+

363 else: 

+

364 try: 

+

365 return float(letter_spacing) 

+

366 except ValueError: 

+

367 return 0.0 

+

368 

+

369 return 0.0 

+

370 

+

371 def _resolve_word_spacing( 

+

372 self, word_spacing: Optional[Union[str, float]], font_size: int) -> float: 

+

373 """Resolve word spacing to pixels.""" 

+

374 if word_spacing is None or word_spacing == "normal": 

+

375 return 0.0 

+

376 

+

377 if isinstance(word_spacing, (int, float)): 

+

378 return float(word_spacing) 

+

379 

+

380 if isinstance(word_spacing, str): 380 ↛ 393line 380 didn't jump to line 393 because the condition on line 380 was always true

+

381 if word_spacing.endswith("em"): 381 ↛ 388line 381 didn't jump to line 388 because the condition on line 381 was always true

+

382 try: 

+

383 em_value = float(word_spacing[:-2]) 

+

384 return em_value * font_size 

+

385 except ValueError: 

+

386 return 0.0 

+

387 else: 

+

388 try: 

+

389 return float(word_spacing) 

+

390 except ValueError: 

+

391 return 0.0 

+

392 

+

393 return 0.0 

+

394 

+

395 def update_context(self, **kwargs): 

+

396 """ 

+

397 Update the rendering context and clear cache. 

+

398 

+

399 Args: 

+

400 **kwargs: Context properties to update 

+

401 """ 

+

402 # Create new context with updates 

+

403 context_dict = { 

+

404 field.name: getattr(self.context, field.name) 

+

405 for field in self.context.__dataclass_fields__.values() 

+

406 } 

+

407 context_dict.update(kwargs) 

+

408 

+

409 self.context = RenderingContext(**context_dict) 

+

410 

+

411 # Clear cache since context changed 

+

412 self._concrete_cache.clear() 

+

413 

+

414 def clear_cache(self): 

+

415 """Clear the concrete style cache.""" 

+

416 self._concrete_cache.clear() 

+

417 

+

418 def get_cache_size(self) -> int: 

+

419 """Get the number of cached concrete styles.""" 

+

420 return len(self._concrete_cache) 

+

421 

+

422 

+

423class ConcreteStyleRegistry: 

+

424 """ 

+

425 Registry for managing concrete styles with efficient caching. 

+

426 

+

427 This registry manages the mapping between abstract and concrete styles, 

+

428 and provides efficient access to Font objects for rendering. 

+

429 """ 

+

430 

+

431 def __init__(self, resolver: StyleResolver): 

+

432 """ 

+

433 Initialize the concrete style registry. 

+

434 

+

435 Args: 

+

436 resolver: StyleResolver for converting abstract to concrete styles 

+

437 """ 

+

438 self.resolver = resolver 

+

439 self._font_cache: Dict[ConcreteStyle, Font] = {} 

+

440 

+

441 def get_concrete_style(self, abstract_style: AbstractStyle) -> ConcreteStyle: 

+

442 """ 

+

443 Get a concrete style for an abstract style. 

+

444 

+

445 Args: 

+

446 abstract_style: AbstractStyle to resolve 

+

447 

+

448 Returns: 

+

449 ConcreteStyle with rendering parameters 

+

450 """ 

+

451 return self.resolver.resolve_style(abstract_style) 

+

452 

+

453 def get_font(self, abstract_style: AbstractStyle) -> Font: 

+

454 """ 

+

455 Get a Font object for an abstract style. 

+

456 

+

457 Args: 

+

458 abstract_style: AbstractStyle to get font for 

+

459 

+

460 Returns: 

+

461 Font object ready for rendering 

+

462 """ 

+

463 concrete_style = self.get_concrete_style(abstract_style) 

+

464 

+

465 # Check font cache 

+

466 if concrete_style in self._font_cache: 

+

467 return self._font_cache[concrete_style] 

+

468 

+

469 # Create and cache font 

+

470 font = concrete_style.create_font() 

+

471 self._font_cache[concrete_style] = font 

+

472 

+

473 return font 

+

474 

+

475 def clear_caches(self): 

+

476 """Clear all caches.""" 

+

477 self.resolver.clear_cache() 

+

478 self._font_cache.clear() 

+

479 

+

480 def get_cache_stats(self) -> Dict[str, int]: 

+

481 """Get cache statistics.""" 

+

482 return { 

+

483 "concrete_styles": self.resolver.get_cache_size(), 

+

484 "fonts": len(self._font_cache) 

+

485 } 

+
+ + + diff --git a/cov_info/htmlcov/z_ba7f6bdeb0188088_fonts_py.html b/cov_info/htmlcov/z_ba7f6bdeb0188088_fonts_py.html new file mode 100644 index 0000000..b4e94c0 --- /dev/null +++ b/cov_info/htmlcov/z_ba7f6bdeb0188088_fonts_py.html @@ -0,0 +1,505 @@ + + + + + Coverage for pyWebLayout/style/fonts.py: 66% + + + + + +
+
+

+ Coverage for pyWebLayout/style/fonts.py: + 66% +

+ +

+ 161 statements   + + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.15.4, + created at 2026-08-08 20:34 +0000 +

+ +
+
+
+

1# this should contain classes for how different object can be rendered, 

+

2# e.g. bold, italic, regular 

+

3from PIL import ImageFont 

+

4from enum import Enum 

+

5from typing import Tuple, Optional, Dict 

+

6import os 

+

7import logging 

+

8 

+

9# Set up logging for font loading 

+

10logger = logging.getLogger(__name__) 

+

11 

+

12# Global cache for PIL ImageFont objects to avoid reloading fonts from disk 

+

13# Key: (font_path, font_size), Value: PIL ImageFont object 

+

14_FONT_CACHE: Dict[Tuple[Optional[str], int], ImageFont.FreeTypeFont] = {} 

+

15 

+

16# Cache for bundled font path to avoid repeated filesystem lookups 

+

17_BUNDLED_FONT_PATH: Optional[str] = None 

+

18 

+

19# Cache for bundled fonts directory 

+

20_BUNDLED_FONTS_DIR: Optional[str] = None 

+

21 

+

22 

+

23class FontWeight(Enum): 

+

24 NORMAL = "normal" 

+

25 BOLD = "bold" 

+

26 

+

27 

+

28class FontStyle(Enum): 

+

29 NORMAL = "normal" 

+

30 ITALIC = "italic" 

+

31 

+

32 

+

33class TextDecoration(Enum): 

+

34 NONE = "none" 

+

35 UNDERLINE = "underline" 

+

36 STRIKETHROUGH = "strikethrough" 

+

37 

+

38 

+

39class BundledFont(Enum): 

+

40 """Bundled font families available in pyWebLayout""" 

+

41 SANS = "sans" # DejaVu Sans - modern sans-serif 

+

42 SERIF = "serif" # DejaVu Serif - classic serif 

+

43 MONOSPACE = "monospace" # DejaVu Sans Mono - fixed-width 

+

44 

+

45 

+

46def get_bundled_fonts_dir(): 

+

47 """ 

+

48 Get the directory containing bundled fonts (cached). 

+

49 

+

50 Returns: 

+

51 str: Path to the fonts directory, or None if not found 

+

52 """ 

+

53 global _BUNDLED_FONTS_DIR 

+

54 

+

55 # Return cached path if available 

+

56 if _BUNDLED_FONTS_DIR is not None: 

+

57 return _BUNDLED_FONTS_DIR 

+

58 

+

59 # First time - determine the path and cache it 

+

60 current_dir = os.path.dirname(os.path.abspath(__file__)) 

+

61 fonts_dir = os.path.join(os.path.dirname(current_dir), 'assets', 'fonts') 

+

62 

+

63 if os.path.exists(fonts_dir) and os.path.isdir(fonts_dir): 

+

64 _BUNDLED_FONTS_DIR = fonts_dir 

+

65 logger.debug(f"Found bundled fonts directory at: {fonts_dir}") 

+

66 return fonts_dir 

+

67 else: 

+

68 logger.warning(f"Bundled fonts directory not found at: {fonts_dir}") 

+

69 _BUNDLED_FONTS_DIR = "" # Empty string to indicate "checked but not found" 

+

70 return None 

+

71 

+

72 

+

73def get_bundled_font_path( 

+

74 family: BundledFont = BundledFont.SANS, 

+

75 weight: FontWeight = FontWeight.NORMAL, 

+

76 style: FontStyle = FontStyle.NORMAL 

+

77) -> Optional[str]: 

+

78 """ 

+

79 Get the path to a specific bundled font file. 

+

80 

+

81 Args: 

+

82 family: The font family (SANS, SERIF, or MONOSPACE) 

+

83 weight: The font weight (NORMAL or BOLD) 

+

84 style: The font style (NORMAL or ITALIC) 

+

85 

+

86 Returns: 

+

87 str: Full path to the font file, or None if not found 

+

88 

+

89 Example: 

+

90 >>> # Get bold italic sans font 

+

91 >>> path = get_bundled_font_path(BundledFont.SANS, FontWeight.BOLD, FontStyle.ITALIC) 

+

92 >>> font = Font(font_path=path, font_size=16) 

+

93 """ 

+

94 fonts_dir = get_bundled_fonts_dir() 

+

95 if not fonts_dir: 

+

96 return None 

+

97 

+

98 # Map font parameters to filename 

+

99 family_map = { 

+

100 BundledFont.SANS: "DejaVuSans", 

+

101 BundledFont.SERIF: "DejaVuSerif", 

+

102 BundledFont.MONOSPACE: "DejaVuSansMono" 

+

103 } 

+

104 

+

105 base_name = family_map.get(family, "DejaVuSans") 

+

106 

+

107 # Build the font file name 

+

108 parts = [base_name] 

+

109 

+

110 if weight == FontWeight.BOLD and style == FontStyle.ITALIC: 

+

111 # Special case: both bold and italic 

+

112 if family == BundledFont.MONOSPACE: 

+

113 parts.append("BoldOblique") 

+

114 elif family == BundledFont.SERIF: 

+

115 parts.append("BoldItalic") 

+

116 else: # SANS 

+

117 parts.append("BoldOblique") 

+

118 elif weight == FontWeight.BOLD: 

+

119 parts.append("Bold") 

+

120 elif style == FontStyle.ITALIC: 

+

121 # Italic naming differs by family 

+

122 if family == BundledFont.MONOSPACE or family == BundledFont.SANS: 

+

123 parts.append("Oblique") 

+

124 else: # SERIF 

+

125 parts.append("Italic") 

+

126 

+

127 filename = "-".join(parts) + ".ttf" 

+

128 font_path = os.path.join(fonts_dir, filename) 

+

129 

+

130 if os.path.exists(font_path): 

+

131 logger.debug(f"Found bundled font: {filename}") 

+

132 return font_path 

+

133 else: 

+

134 logger.warning(f"Bundled font not found: {filename}") 

+

135 return None 

+

136 

+

137 

+

138class Font: 

+

139 """ 

+

140 Font class to manage text rendering properties including font face, size, color, and styling. 

+

141 This class is used by the text renderer to determine how to render text. 

+

142 """ 

+

143 

+

144 def __init__(self, 

+

145 font_path: Optional[str] = None, 

+

146 font_size: int = 16, 

+

147 colour: Tuple[int, int, int] = (0, 0, 0), 

+

148 weight: FontWeight = FontWeight.NORMAL, 

+

149 style: FontStyle = FontStyle.NORMAL, 

+

150 decoration: TextDecoration = TextDecoration.NONE, 

+

151 background: Optional[Tuple[int, int, int, int]] = None, 

+

152 language="en_EN", 

+

153 min_hyphenation_width: Optional[int] = None): 

+

154 """ 

+

155 Initialize a Font object with the specified properties. 

+

156 

+

157 Args: 

+

158 font_path: Path to the font file (.ttf, .otf). If None, uses default bundled font. 

+

159 font_size: Size of the font in points. 

+

160 colour: RGB color tuple for the text. 

+

161 weight: Font weight (normal or bold). 

+

162 style: Font style (normal or italic). 

+

163 decoration: Text decoration (none, underline, or strikethrough). 

+

164 background: RGBA background color for the text. If None, transparent background. 

+

165 language: Language code for hyphenation and text processing. 

+

166 min_hyphenation_width: Minimum width in pixels required for hyphenation to be considered. 

+

167 If None, defaults to 4 times the font size. 

+

168 """ 

+

169 self._font_path = font_path 

+

170 self._font_size = font_size 

+

171 self._colour = colour 

+

172 self._weight = weight 

+

173 self._style = style 

+

174 self._decoration = decoration 

+

175 self._background = background if background else (255, 255, 255, 0) 

+

176 self.language = language 

+

177 self._min_hyphenation_width = min_hyphenation_width if min_hyphenation_width is not None else font_size * 4 

+

178 # Load the font file or use default 

+

179 self._load_font() 

+

180 

+

181 @classmethod 

+

182 def from_family(cls, 

+

183 family: BundledFont = BundledFont.SANS, 

+

184 font_size: int = 16, 

+

185 colour: Tuple[int, int, int] = (0, 0, 0), 

+

186 weight: FontWeight = FontWeight.NORMAL, 

+

187 style: FontStyle = FontStyle.NORMAL, 

+

188 decoration: TextDecoration = TextDecoration.NONE, 

+

189 background: Optional[Tuple[int, int, int, int]] = None, 

+

190 language: str = "en_EN", 

+

191 min_hyphenation_width: Optional[int] = None) -> 'Font': 

+

192 """ 

+

193 Create a Font using a bundled font family. 

+

194 

+

195 This is a convenient way to use the bundled DejaVu fonts without needing to 

+

196 specify paths manually. 

+

197 

+

198 Args: 

+

199 family: The font family to use (SANS, SERIF, or MONOSPACE) 

+

200 font_size: Size of the font in points. 

+

201 colour: RGB color tuple for the text. 

+

202 weight: Font weight (normal or bold). 

+

203 style: Font style (normal or italic). 

+

204 decoration: Text decoration (none, underline, or strikethrough). 

+

205 background: RGBA background color for the text. If None, transparent background. 

+

206 language: Language code for hyphenation and text processing. 

+

207 min_hyphenation_width: Minimum width in pixels required for hyphenation. 

+

208 

+

209 Returns: 

+

210 Font object configured with the bundled font 

+

211 

+

212 Example: 

+

213 >>> # Create a bold serif font 

+

214 >>> font = Font.from_family(BundledFont.SERIF, font_size=18, weight=FontWeight.BOLD) 

+

215 >>> 

+

216 >>> # Create an italic monospace font 

+

217 >>> code_font = Font.from_family(BundledFont.MONOSPACE, style=FontStyle.ITALIC) 

+

218 """ 

+

219 font_path = get_bundled_font_path(family, weight, style) 

+

220 return cls( 

+

221 font_path=font_path, 

+

222 font_size=font_size, 

+

223 colour=colour, 

+

224 weight=weight, 

+

225 style=style, 

+

226 decoration=decoration, 

+

227 background=background, 

+

228 language=language, 

+

229 min_hyphenation_width=min_hyphenation_width 

+

230 ) 

+

231 

+

232 def _get_bundled_font_path(self): 

+

233 """Get the path to the bundled font (cached)""" 

+

234 global _BUNDLED_FONT_PATH 

+

235 

+

236 # Return cached path if available 

+

237 if _BUNDLED_FONT_PATH is not None: 

+

238 return _BUNDLED_FONT_PATH 

+

239 

+

240 # First time - determine the path and cache it 

+

241 # Get the directory containing this module 

+

242 current_dir = os.path.dirname(os.path.abspath(__file__)) 

+

243 # Navigate to the assets/fonts directory 

+

244 assets_dir = os.path.join(os.path.dirname(current_dir), 'assets', 'fonts') 

+

245 bundled_font_path = os.path.join(assets_dir, 'DejaVuSans.ttf') 

+

246 

+

247 logger.debug(f"Font loading: current_dir = {current_dir}") 

+

248 logger.debug(f"Font loading: assets_dir = {assets_dir}") 

+

249 logger.debug(f"Font loading: bundled_font_path = {bundled_font_path}") 

+

250 logger.debug( 

+

251 f"Font loading: bundled font exists = {os.path.exists(bundled_font_path)}" 

+

252 ) 

+

253 

+

254 if os.path.exists(bundled_font_path): 254 ↛ 259line 254 didn't jump to line 259 because the condition on line 254 was always true

+

255 logger.info(f"Found bundled font at: {bundled_font_path}") 

+

256 _BUNDLED_FONT_PATH = bundled_font_path 

+

257 return bundled_font_path 

+

258 else: 

+

259 logger.warning(f"Bundled font not found at: {bundled_font_path}") 

+

260 # Cache None to indicate bundled font is not available 

+

261 _BUNDLED_FONT_PATH = "" # Use empty string instead of None to differentiate from "not checked yet" 

+

262 return None 

+

263 

+

264 def _load_font(self): 

+

265 """Load the font using PIL's ImageFont with consistent bundled font and caching""" 

+

266 # Determine the actual font path to use 

+

267 font_path_to_use = self._font_path 

+

268 if not font_path_to_use: 

+

269 font_path_to_use = self._get_bundled_font_path() 

+

270 

+

271 # Create cache key 

+

272 cache_key = (font_path_to_use, self._font_size) 

+

273 

+

274 # Check if font is already cached 

+

275 if cache_key in _FONT_CACHE: 

+

276 self._font = _FONT_CACHE[cache_key] 

+

277 logger.debug(f"Reusing cached font: {font_path_to_use} at size {self._font_size}") 

+

278 return 

+

279 

+

280 # Font not cached, need to load it 

+

281 try: 

+

282 if self._font_path: 

+

283 # Use specified font path 

+

284 logger.info(f"Loading font from specified path: {self._font_path}") 

+

285 self._font = ImageFont.truetype( 

+

286 self._font_path, 

+

287 self._font_size 

+

288 ) 

+

289 logger.info(f"Successfully loaded font from: {self._font_path}") 

+

290 else: 

+

291 # Use bundled font for consistency across environments 

+

292 bundled_font_path = self._get_bundled_font_path() 

+

293 

+

294 if bundled_font_path: 294 ↛ 303line 294 didn't jump to line 303 because the condition on line 294 was always true

+

295 logger.info(f"Loading bundled font from: {bundled_font_path}") 

+

296 self._font = ImageFont.truetype(bundled_font_path, self._font_size) 

+

297 logger.info( 

+

298 f"Successfully loaded bundled font at size {self._font_size}" 

+

299 ) 

+

300 else: 

+

301 # Only fall back to PIL's default font if bundled font is not 

+

302 # available 

+

303 logger.warning( 

+

304 "Bundled font not available, falling back to PIL default font") 

+

305 self._font = ImageFont.load_default() 

+

306 

+

307 # Cache the loaded font 

+

308 _FONT_CACHE[cache_key] = self._font 

+

309 logger.debug(f"Cached font: {font_path_to_use} at size {self._font_size}") 

+

310 

+

311 except Exception as e: 

+

312 # Ultimate fallback to default font 

+

313 logger.error(f"Failed to load font: {e}, falling back to PIL default font") 

+

314 self._font = ImageFont.load_default() 

+

315 # Don't cache the default font as it doesn't have a path 

+

316 

+

317 @property 

+

318 def font(self): 

+

319 """Get the PIL ImageFont object""" 

+

320 return self._font 

+

321 

+

322 @property 

+

323 def font_size(self): 

+

324 """Get the font size""" 

+

325 return self._font_size 

+

326 

+

327 @property 

+

328 def colour(self): 

+

329 """Get the text color""" 

+

330 return self._colour 

+

331 

+

332 @property 

+

333 def color(self): 

+

334 """Alias for colour (American spelling)""" 

+

335 return self._colour 

+

336 

+

337 @property 

+

338 def background(self): 

+

339 """Get the background color""" 

+

340 return self._background 

+

341 

+

342 @property 

+

343 def weight(self): 

+

344 """Get the font weight""" 

+

345 return self._weight 

+

346 

+

347 @property 

+

348 def style(self): 

+

349 """Get the font style""" 

+

350 return self._style 

+

351 

+

352 @property 

+

353 def decoration(self): 

+

354 """Get the text decoration""" 

+

355 return self._decoration 

+

356 

+

357 @property 

+

358 def min_hyphenation_width(self): 

+

359 """Get the minimum width required for hyphenation to be considered""" 

+

360 return self._min_hyphenation_width 

+

361 

+

362 def _with_modified(self, **kwargs): 

+

363 """ 

+

364 Internal helper to create a new Font with modified parameters. 

+

365 

+

366 This consolidates the duplication across all with_* methods. 

+

367 

+

368 Args: 

+

369 **kwargs: Parameters to override (e.g., font_size=20, colour=(255,0,0)) 

+

370 

+

371 Returns: 

+

372 New Font object with modified parameters 

+

373 """ 

+

374 params = { 

+

375 'font_path': self._font_path, 

+

376 'font_size': self._font_size, 

+

377 'colour': self._colour, 

+

378 'weight': self._weight, 

+

379 'style': self._style, 

+

380 'decoration': self._decoration, 

+

381 'background': self._background, 

+

382 'language': self.language, 

+

383 'min_hyphenation_width': self._min_hyphenation_width 

+

384 } 

+

385 params.update(kwargs) 

+

386 return Font(**params) 

+

387 

+

388 def with_size(self, size: int): 

+

389 """Create a new Font object with modified size""" 

+

390 return self._with_modified(font_size=size) 

+

391 

+

392 def with_colour(self, colour: Tuple[int, int, int]): 

+

393 """Create a new Font object with modified colour""" 

+

394 return self._with_modified(colour=colour) 

+

395 

+

396 def with_weight(self, weight: FontWeight): 

+

397 """Create a new Font object with modified weight""" 

+

398 return self._with_modified(weight=weight) 

+

399 

+

400 def with_style(self, style: FontStyle): 

+

401 """Create a new Font object with modified style""" 

+

402 return self._with_modified(style=style) 

+

403 

+

404 def with_decoration(self, decoration: TextDecoration): 

+

405 """Create a new Font object with modified decoration""" 

+

406 return self._with_modified(decoration=decoration) 

+
+ + + diff --git a/cov_info/htmlcov/z_ba7f6bdeb0188088_page_style_py.html b/cov_info/htmlcov/z_ba7f6bdeb0188088_page_style_py.html new file mode 100644 index 0000000..d20d283 --- /dev/null +++ b/cov_info/htmlcov/z_ba7f6bdeb0188088_page_style_py.html @@ -0,0 +1,163 @@ + + + + + Coverage for pyWebLayout/style/page_style.py: 100% + + + + + +
+
+

+ Coverage for pyWebLayout/style/page_style.py: + 100% +

+ +

+ 35 statements   + + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.15.4, + created at 2026-08-08 20:34 +0000 +

+ +
+
+
+

1from typing import Tuple 

+

2from dataclasses import dataclass, field 

+

3 

+

4from pyWebLayout.style.alignment import Alignment 

+

5 

+

6 

+

7@dataclass 

+

8class PageStyle: 

+

9 """ 

+

10 Defines the styling properties for a page including borders, spacing, and layout. 

+

11 """ 

+

12 

+

13 # Alignment applied to body text that does not specify its own. Headings are 

+

14 # never justified regardless of this setting. 

+

15 default_alignment: Alignment = Alignment.JUSTIFY 

+

16 

+

17 # Border properties 

+

18 border_width: int = 0 

+

19 border_color: Tuple[int, int, int] = (0, 0, 0) 

+

20 

+

21 # Spacing properties 

+

22 line_spacing: int = 5 # Additional pixels between lines (added to font size) 

+

23 inter_block_spacing: int = 15 # Pixels between blocks (paragraphs, headings, etc.) 

+

24 word_spacing: int = 0 # Additional pixels between words (0 = use font defaults) 

+

25 

+

26 # Padding (top, right, bottom, left) 

+

27 padding: Tuple[int, int, int, int] = (20, 20, 20, 20) 

+

28 

+

29 # Background color 

+

30 background_color: Tuple[int, int, int] = (255, 255, 255) 

+

31 

+

32 # Typography properties 

+

33 max_font_size: int = 72 # Maximum font size allowed on a page 

+

34 

+

35 @property 

+

36 def padding_top(self) -> int: 

+

37 return self.padding[0] 

+

38 

+

39 @property 

+

40 def padding_right(self) -> int: 

+

41 return self.padding[1] 

+

42 

+

43 @property 

+

44 def padding_bottom(self) -> int: 

+

45 return self.padding[2] 

+

46 

+

47 @property 

+

48 def padding_left(self) -> int: 

+

49 return self.padding[3] 

+

50 

+

51 @property 

+

52 def total_horizontal_padding(self) -> int: 

+

53 """Get total horizontal padding (left + right)""" 

+

54 return self.padding_left + self.padding_right 

+

55 

+

56 @property 

+

57 def total_vertical_padding(self) -> int: 

+

58 """Get total vertical padding (top + bottom)""" 

+

59 return self.padding_top + self.padding_bottom 

+

60 

+

61 @property 

+

62 def total_border_width(self) -> int: 

+

63 """Get total border width (both sides)""" 

+

64 return self.border_width * 2 

+
+ + + diff --git a/cov_info/htmlcov/z_fc521de9aff00981___init___py.html b/cov_info/htmlcov/z_fc521de9aff00981___init___py.html new file mode 100644 index 0000000..72c0569 --- /dev/null +++ b/cov_info/htmlcov/z_fc521de9aff00981___init___py.html @@ -0,0 +1,107 @@ + + + + + Coverage for pyWebLayout/io/__init__.py: 100% + + + + + +
+
+

+ Coverage for pyWebLayout/io/__init__.py: + 100% +

+ +

+ 0 statements   + + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.15.4, + created at 2026-08-08 20:34 +0000 +

+ +
+
+
+

1""" 

+

2Input/Output module for pyWebLayout. 

+

3 

+

4This package provides functionality for reading and writing various file formats, 

+

5including HTML, EPUB, and other document formats. 

+

6""" 

+

7 

+

8# Readers 

+
+ + + diff --git a/docs/ARCHITECTURE_REVIEW.md b/docs/ARCHITECTURE_REVIEW.md new file mode 100644 index 0000000..12114ec --- /dev/null +++ b/docs/ARCHITECTURE_REVIEW.md @@ -0,0 +1,595 @@ +# Architecture Review + +Independent review of the codebase at `c5c61a3` (2026-08-06), answering one +question: **is this a well-architected library or an over-complex mess?** + +It is a well-architected library with one rotten subsystem inside it. The core +design holds up; roughly a fifth of the code is speculative or non-functional, +and it is concentrated in the ereader pagination/buffering layer. + +This document records the verdict, the evidence, and the findings **not already +covered** by [LAYOUT_REMEDIATION_SPEC.md](LAYOUT_REMEDIATION_SPEC.md). Where a +finding is already specced, it is cross-referenced rather than restated. + +## Contents + +| ID | Finding | Severity | Status | +|----|---------|----------|--------| +| [R1](#r1--the-process-pool-crashes-on-python-314) | The process pool crashes on Python 3.14 | Critical | New; raises priority of S12 | +| [R2](#r2--the-test-suite-hangs-at-interpreter-exit) | Test suite hangs at interpreter exit | High | New; same root cause as R1 | +| [R3](#r3--font-scaling-destroys-hyperlinks) | Font scaling destroys hyperlinks | High | New | +| [R4](#r4--three-packaging-configs-that-disagree) | Three packaging configs that disagree | Medium | New (corrected) | +| [R5](#r5--monkey-patched-page-methods-with-a-conflicting-signature) | Monkey-patched `Page` methods with a conflicting signature | Medium | New | +| [R6](#r6--dead-duck-typing-cluster-in-pagepy) | Dead duck-typing cluster in `page.py` | Low | New; extends S10.3 | +| [R7](#r7--two-orphaned-subsystems) | Two orphaned subsystems | Low | New | +| [R8](#r8--backward-pagination-is-guesswork) | Backward pagination is guesswork | Medium | Resolved by S16 | +| [R9](#r9--query_points-hit-region-is-offset-from-the-glyphs) | `query_point`'s hit region is offset from the glyphs | Medium | New, open | + +--- + +## Verdict + +**Well architected.** The concerns that usually decide this question are all on +the right side of the line: + +- **The abstract/concrete split is real, not aspirational.** Verified + empirically: `abstract/` never imports `concrete/`; `core/` imports nothing but + itself. The single crossing ([document.py:7](../pyWebLayout/abstract/document.py#L7)) + is into `style/`, which the dependency rules permit. Most codebases claiming + this layering have leaked it within a year. +- **The layouter contract is the right abstraction.** `paragraph_layouter` + returning `(fit, failed_word_index, remaining_pretext)` is what makes + pagination resumable, and the shape is consistent across content types. Layout + engines that return `List[Line]` cannot paginate without a second pass. +- **[core/cache.py](../pyWebLayout/core/cache.py) is exemplary.** Usage-ranked + eviction with periodic aging, sampled eviction instead of a maintained heap, + O(1) hit path with no reordering — every choice justified by a measurement in + the docstring. +- **[concrete/text.py](../pyWebLayout/concrete/text.py) is the strongest file.** + The glyph cache ([:581-647](../pyWebLayout/concrete/text.py#L581-L647)) + reimplements PIL's internals to skip per-call setup, with a permanent graceful + fallback when the private API is absent. Alignment is a clean strategy pattern, + and `render_alignment_handler` handles last-line-of-paragraph correctly. +- **[html_extraction.py](../pyWebLayout/io/readers/html_extraction.py) is + textbook.** An immutable `StyleContext` threaded down the tree plus a handler + dispatch table — no giant if/elif, no mutable parser state. + +**The complexity that is not earned** is concentrated in three places, all in the +same band of the code: + +1. `layout/page_buffer.py` — 520 lines of multiprocess prefetch that has never + worked (S12, plus R1/R2 below). +2. `BidirectionalLayouter.render_page_backward` — ~100 lines of convergence + heuristics standing in for an anchor list that already exists (R8). +3. The second block dispatcher in `ereader_layout.py`, which silently drops + tables and lists (S4, S8). + +The pattern is visible in the git history: work from `e000068` onward (caching, +alignment, page geometry) is markedly better than the ereader scaffolding it sits +on. This is not a mess. It is a solid library with an early prototype still +embedded in it. + +**Sizing the cleanup:** R1–R7 plus S12 and S10.3 remove roughly 800–900 lines and +fix four user-visible defects. None of it requires redesigning anything. + +## Test baseline + +At `c5c61a3`, in a clean venv on Python 3.14.6: + +``` +833 passed, 2 skipped, 24 subtests passed in 11.51s +``` + +(The 2 skips were environmental — the review venv lacked `requests`, so the URL +image tests skipped. With the `test` extra from R4 installed the suite reports +`853 passed, 24 subtests passed in 13.53s`.) + +The suite then **hangs indefinitely** rather than exiting. See R2. + +## Status of the existing remediation spec + +| Spec | Subject | State | +|------|---------|-------| +| S1 | Inline content in non-paragraph containers | Done (`284d521`) | +| S2 | Page geometry: origin and content rect | Done (`f18cec2`) | +| S3 | Draw/canvas lifecycle | Done (`202dacf`) | +| S11 | Partial-block progress discarded | Done (`a57da80`) | +| S13 | Word spacing and alignment | Done (`1262be6`) | +| S14 | Vertical centring in buttons and fields | Done (`c5c61a3`) | +| S4–S10, S12 | Dispatch, cells, table grid, pagination, hygiene, background rendering | Outstanding | + +The spec's analysis is sound and in places sharper than this review — S12 caught +that `_render_page_worker` omits `page_size` entirely, which this review missed. +Nothing below supersedes it. + +--- + +## R1 — The process pool crashes on Python 3.14 + +**Severity: critical. Raises S12 from "useless" to "fatal".** + +### Problem + +`PageBuffer` submits to a `ProcessPoolExecutor` from inside +`BufferedPageRenderer.render_page` +([page_buffer.py:431](../pyWebLayout/layout/page_buffer.py#L431)). Python 3.14 +changed the default multiprocessing start method on Linux from `fork` to +`forkserver`. Under a non-`fork` start method, `submit()` reaches +`_check_not_importing_main()`, which raises unless the caller sits inside an +`if __name__ == "__main__":` guard — and the child re-imports the caller's main +module, re-executing it. + +S12 documents this subsystem as delivering no benefit. On 3.14 it is worse than +that: `EreaderLayoutManager.get_current_page()` **raises** when called from +module-level script code. + +### Evidence + +A plain script calling `manager.get_current_page()` at module level, Python +3.14.6: + +``` +RuntimeError: + An attempt has been made to start a new process before the + current process has finished its bootstrapping phase. + ... + File "pyWebLayout/layout/page_buffer.py", line 221, in _queue_forward_renders + future = self.executor.submit(_render_page_worker, args) +ConnectionResetError: [Errno 104] Connection reset by peer +``` + +With a `__main__` guard added it does not raise, and instead confirms S12's +finding on every job: + +``` +Background render failed for position RenderingPosition(...): cannot pickle 'ImagingCore' object +Background render failed for position RenderingPosition(...): cannot pickle 'ImagingCore' object +``` + +### Action + +Fold into **S12**, and treat S12 as unblocked and urgent rather than phase 4. The +recommended resolution there — delete the pool, keep the LRU buffers and position +maps, replace prefetch with synchronous readahead — resolves R1 and R2 as a side +effect. S12's measurement gate still applies to the *readahead* decision; it does +not need to gate deletion of the pool, because the pool's contribution is +provably zero. + +### Files + +`pyWebLayout/layout/page_buffer.py` + +--- + +## R2 — The test suite hangs at interpreter exit + +**Severity: high.** Same root cause as R1. + +### Problem + +`PageBuffer.__del__` calls `shutdown()`, which calls +`executor.shutdown(wait=True)` ([page_buffer.py:342](../pyWebLayout/layout/page_buffer.py#L342)). +`EreaderLayoutManager.__del__` does the same via `renderer.shutdown()`. Running +`__del__` at interpreter shutdown and blocking on a process pool inside it +deadlocks. + +### Evidence + +``` +833 passed, 2 skipped, 24 subtests passed in 11.51s +``` + +...then the process sat at ~0% CPU with idle forkserver children for 13 minutes +before being killed. Reproduced twice; both runs completed the tests in under +12s and neither exited. + +This is why CI wall-clock does not resemble the 11.5s the tests actually take. + +### Action + +Resolved by S12's deletion of the executor. If for any reason the pool is +retained, `__del__` must not block: register an `atexit` handler or require +explicit `shutdown()`, and never `wait=True` from a finaliser. + +### Acceptance criteria + +- `pytest` returns to the shell within a second of printing its summary line. +- No `multiprocessing` child processes outlive the test session. + +### Files + +`pyWebLayout/layout/page_buffer.py`, `pyWebLayout/layout/ereader_manager.py` + +--- + +## R3 — Font scaling destroys hyperlinks + +**Severity: high. User-visible, silent, and trivially reproducible.** + +### Problem + +`BidirectionalLayouter._scale_block_fonts` +([ereader_layout.py:474-498](../pyWebLayout/layout/ereader_layout.py#L474-L498)) +rebuilds a scaled block by constructing plain `Word(word.text, scaled_style)` for +every word. `LinkedWord` is a `Word` subclass +([inline.py:288](../pyWebLayout/abstract/inline.py#L288)), so the reconstruction +downgrades it and the link target is discarded. + +The function returns the block unchanged when `font_scale == 1.0` and no family +override is set, which is why no test has caught this: the defect only appears +once the reader changes font size. + +Two further gaps in the same function: + +1. It handles only `Paragraph` and `Heading`. Every other block type is returned + unscaled, so a font-size change leaves images, tables and lists at their + original size while the text around them reflows. +2. It allocates a new `Paragraph` and a new `Word` per word on **every page + render** at any scale ≠ 1.0 — directly against the caching work in + `concrete/text.py`, and on the hot path. + +### Evidence + +At `c5c61a3`, parsing `

Go to this link now.

`: + +``` +scale=1.0: LinkedWords = 2 +scale=1.5: LinkedWords = 0 +``` + +### Design + +Stop reconstructing abstract blocks at layout time. Font scale and family are +*rendering context*, not document content — carrying them in a copied document +violates the "abstract content is not mutated by layout" principle in +[ARCHITECTURE.md](../ARCHITECTURE.md) in spirit, even though it copies rather +than mutates. + +Preferred: thread the scale/override into the layouter and resolve fonts at +`Text` construction, where `Font` objects are already deduplicated by +`FontRegistry`. `paragraph_layouter` already accepts an `alignment_override`; +`font_scale` and `font_family_override` belong in the same place. + +Minimum viable fix if the larger change is deferred: reconstruct via +`type(word)` and copy subclass state, and extend coverage to every block type. +This is strictly a stopgap — it keeps the per-page allocation cost. + +### Acceptance criteria + +- A document containing `` retains every `LinkedWord` after + `set_font_scale(1.5)`, and `query_point` over the rendered page still returns + `object_type="link"` with the correct target. +- An image block's rendered size is unaffected by `set_font_scale`, or scales + deliberately — not left inconsistent with the text around it. +- No new `Word`/`Paragraph` objects are allocated per page render at scale ≠ 1.0. + +### Files + +`pyWebLayout/layout/ereader_layout.py`, `pyWebLayout/layout/document_layouter.py` + +--- + +## R4 — Three packaging configs that disagree + +**Severity: medium.** *Corrected: the original review claimed a clean install +fails on first import. It does not — see below.* + +### Problem + +The project carries **three** sets of packaging metadata: + +| File | Declares | +|------|----------| +| `pyproject.toml` `[project]` | Pillow, numpy, pyphen, beautifulsoup4, flask, ebooklib, requests | +| `setup.cfg` `[options]` | Pillow, numpy | +| `setup.py` `setup(...)` kwargs | Pillow, numpy | + +`pyproject.toml`'s `[project]` table wins under any modern build backend, so the +shipped wheel is correct and `pip install pyWebLayout` works. The `setup.cfg` and +`setup.py` copies are dead, contradictory, and actively misleading — reading +either one gives the wrong answer about what the library needs. + +The authoritative list is itself wrong in the other direction: + +- **`flask` is a runtime dependency.** It is imported only by + `tests/abstract/test_abstract_blocks.py`, as a fixture HTTP server. Every user + installs Flask, Jinja2, Werkzeug, click, itsdangerous and blinker for nothing. +- **`ebooklib` is a runtime dependency and is never imported by the library.** + `epub_reader.py` uses `zipfile` + `xml.etree` directly. Only the *tests* use + ebooklib, to build EPUB fixtures. +- **`requests` is declared required but is optional.** `concrete/image.py:100-111` + imports it lazily and degrades to an error message on the image when absent. +- **`requires-python = ">=3.6"` is false.** The package uses dataclasses (3.7+) + and `from __future__ import annotations` (3.7+); CI tests 3.10, 3.12 and 3.13. + +Net effect: a runtime install pulls 7 direct dependencies where 4 are needed. + +### Action + +- Consolidate on `pyproject.toml`. Reduce `setup.cfg` to its `[flake8]` section + and `setup.py` to a `setup()` shim, each with a comment saying where metadata + lives. +- Runtime deps: Pillow, numpy, pyphen, beautifulsoup4. Move flask, werkzeug, + ebooklib and requests into a `test` extra; add a `remote-images` extra for + requests; add a `dev` extra composing them. +- Set `requires-python = ">=3.10"` to match the CI matrix, and add version + classifiers. +- Add a CI step that installs the package into an empty venv with **only** + declared runtime deps and imports every top-level subpackage. This class of + defect is only caught by installing what you ship — and it is what would have + caught the original misreading. + +### Files + +`pyproject.toml`, `setup.cfg`, `setup.py`, `.gitea/workflows/ci.yml` + +--- + +## R5 — Monkey-patched `Page` methods with a conflicting signature + +**Severity: medium.** Currently inert; a landmine if `Page` is ever refactored. + +### Problem + +[ereader_layout.py:741-761](../pyWebLayout/layout/ereader_layout.py#L741-L761) +defines `_add_page_methods()` and calls it at import time. It attaches +`can_fit_line` and `available_width` to the `Page` class if they are absent. + +`Page` defines both ([page.py:59](../pyWebLayout/concrete/page.py#L59), +[page.py:147](../pyWebLayout/concrete/page.py#L147)), so the patch never fires. +But the two definitions of `can_fit_line` **do not agree**: + +| Source | Signature | +|--------|-----------| +| `Page` | `can_fit_line(baseline_spacing, ascent=0, descent=0)` | +| monkey patch | `can_fit_line(line_height)` | + +The patched version also ignores descenders entirely — the exact bug S2 fixed. If +`Page.can_fit_line` were ever renamed or moved, this would silently reinstate +pre-S2 clipping behaviour, from an import side effect in a different package. + +### Action + +Delete `_add_page_methods` and its call site. Import-time monkey-patching of a +class in another module has no place here; if `Page` is missing something the +layout engine needs, it belongs on `Page`. + +### Files + +`pyWebLayout/layout/ereader_layout.py` + +--- + +## R6 — Dead duck-typing cluster in `page.py` + +**Severity: low.** Extends S10.3. + +### Problem + +[page.py:261-491](../pyWebLayout/concrete/page.py#L261-L491) contains a closed +cluster with no external callers: + +- `_get_child_property` (:261) — called only by the four below +- `_get_child_height` (:301) — called by nothing +- `_get_child_position` (:382) — called only by `_point_in_child` +- `_point_in_child` (:435) — called by nothing +- `_get_child_size` (:466) — called only by `_point_in_child` + +Verified by grep across `pyWebLayout/`, `tests/`, `examples/` and `scripts/`: +zero references outside the cluster. About 90 lines. + +It exists because `Renderable` declares neither `size` nor `origin`, so the code +probes `_size`, `size`, `_height`, `height`, `_origin` and `position` in turn with +`hasattr`. `query_point` (:399) already does the right thing instead — it relies +on the `Queriable` interface. + +### Action + +- Delete all five methods. +- Add `origin` and `size` to the `Renderable`/`Geometric` contract in + `core/base.py` so the duck-typing cannot grow back. This is the same concern as + S10.1's render contract and can ship with it. + +### Files + +`pyWebLayout/concrete/page.py`, `pyWebLayout/core/base.py` + +--- + +## R7 — Two orphaned subsystems + +**Severity: low**, but they are a large share of the "is this over-complex?" +impression: 559 lines that nothing in the library reaches. + +### Problem + +**`concrete/interaction_handler.py` (310 lines).** `InteractionHandler` and +`InteractionStateManager` are referenced only by +`examples/07_pressed_state_demo.py`. No library code, no ereader path, no tests. + +**`core/highlight.py` (249 lines).** `Highlight`, `HighlightColor` and +`HighlightManager` have tests (`tests/core/test_highlight.py`) but are not wired +into `EreaderLayoutManager` at all. Highlighting is not reachable through the +library's own top-level interface. + +`HighlightManager` also duplicates `BookmarkManager`'s JSON persistence +(directory, `_save`, `_load`, per-document file naming) with no shared base. + +### Action + +Decide per subsystem, and record the decision: + +- **Wire it up** — `EreaderLayoutManager` grows `add_highlight` / `highlights_for_page` + and the persistence merges with `BookmarkManager` into one document-state store. +- **Or move it out** — relocate to `examples/` or delete, and drop the tests with it. + +Either is fine. Leaving a tested, documented, unreachable subsystem in `core/` is +what makes the library look larger and less coherent than it is. + +### Files + +`pyWebLayout/concrete/interaction_handler.py`, `pyWebLayout/core/highlight.py`, +`pyWebLayout/layout/ereader_manager.py` + +--- + +## R8 — Backward pagination is guesswork + +**Severity: medium.** S11's closing note already flags this for audit; this +records what the audit found. + +### Problem + +`render_page_backward` +([ereader_layout.py:372-472](../pyWebLayout/layout/ereader_layout.py#L372-L472)) +finds the previous page by estimating a start position, rendering forward, +comparing the end against the target, and adjusting — **up to 10 times**. It then +has a fallback that jumps back up to 5 blocks and renders again, and a fallback +for *that* which renders from the start of the document. + +Worst case: one "previous page" tap costs up to 12 full page layouts. + +The estimator it converges from is `max(1, int(10 / font_scale))` blocks +([:684](../pyWebLayout/layout/ereader_layout.py#L684)) — a constant with no +relationship to page size, block length or font metrics. + +The correct answer is usually already known. +`EreaderLayoutManager._page_history` ([ereader_manager.py:210](../pyWebLayout/layout/ereader_manager.py#L210)) +records real page-start positions and serves them instantly; the refinement loop +only runs when history misses — after a jump, a bookmark, a font change, or +beyond 50 entries. + +S11's note asks whether these fallbacks were compensating for the +discarded-progress bug it fixed. They were, in part: the "failed to move +backward" branch at [:446](../pyWebLayout/layout/ereader_layout.py#L446) is +reachable precisely when forward rendering fails to advance, which S11 addressed. + +### Design + +Replace convergence with anchors. Maintain a sorted list of known page-start +positions — chapter starts from `ChapterNavigator` (free, already built) plus +every position visited. To go back from position P: binary-search the largest +anchor A < P, render forward from A collecting page starts until reaching P, and +return the last one. Cost is bounded by the anchor spacing, and every page start +discovered on the way is itself a new anchor, so the second traversal of any +region is free. + +This subsumes `_page_history`, so the two mechanisms become one. + +**Sequencing:** do this after S12, and after S8 — table and list pagination +changes what a page start can be, and re-deriving anchors is cheap only once +positions round-trip through tables correctly (S8 already notes this dependency). + +### Acceptance criteria + +- `previous_page()` from any position issues at most *k* page layouts, where *k* + is the anchor spacing, with no iteration count and no fallback ladder. +- Forward-then-backward round-trips exactly, from a cold cache, after a chapter + jump, and after a bookmark restore. +- `_estimate_page_start`, `_adjust_start_estimate` and the three-tier fallback + are deleted, not retained alongside. + +### Files + +`pyWebLayout/layout/ereader_layout.py`, `pyWebLayout/layout/ereader_manager.py` + +--- + +## Status + +All findings in this document are resolved. What remains is the existing +remediation spec: **S4 → S5 → S6 → S7 → S8 → S9**, plus **S10.1**, unchanged. + +| ID | Resolution | Commit | +|----|-----------|--------| +| R1 | Fixed with S12 — the pool that raised is gone | `1924cc2` | +| R2 | Fixed with S12 — no executor, no blocking finaliser | `1924cc2` | +| R3 | `Word.with_style` keeps subclasses; all container blocks scale | `f0dc675` | +| R4 | Consolidated on `pyproject.toml`; 7 runtime deps → 4 | `767e4c1` | +| R5 | Monkey patch deleted | `62ca151` | +| R6 | 138 dead lines deleted; contract hardening deferred to S10.1 | `e81ba48` | +| R7 | Both subsystems wired into `EreaderLayoutManager` | `8746d3f`, `0ce1aea` | +| R8 | Superseded by S16 (anchor replay); dead estimators removed | `bcae45a` | +| R9 | Open — see below | — | + +Two things worth carrying forward: + +- **S12's measurement stands as the argument against prefetch.** A page render + is 9–56 ms. Any future proposal to render ahead should have to beat that + number first. +- **Wiring an orphan found a bug.** R7's interaction handler had a crash on + every hovered or pressed link (`0ce1aea`). Unreachable code is not + neutral — it is untested code that looks tested. + +--- + +## R9 — query_point's hit region is offset from the glyphs + +**Severity: medium.** Found while verifying R3; not part of the original review. + +### Problem + +The region `Page.query_point` reports for a text object does not line up with +where that object says it is. Probing a `LinkText` at the centre of its own +`origin`/`size` box returns `object_type="empty"`. + +### Evidence + +A single-link page at 400×600, default scale: + +``` +'this' origin=(68.3, 35.0) size=(29.2, 19.0) centre=(82, 44) -> empty +'link' origin=(102.5, 35.0) size=(28.3, 19.0) centre=(116, 44) -> empty + +grid scan: link is detected across y≈20–39 +LinkText claims: y≈35–54 +``` + +The two bands overlap by about four pixels. The offset is close to the font +ascent, which points at a baseline-versus-top mismatch between the coordinates +`Text` stores and the ones `in_object` tests. + +This reproduces identically at scale 1.0 and 1.5, so it predates the R3 fix. + +### Why it matters + +Taps land through the grid because the region is only shifted, not absent — but +it is shifted by most of a line height. Near the top or bottom of a page, or +between tightly spaced lines, a tap can hit the neighbouring line instead of the +one under the finger. It also makes `LinkText.origin`/`size` unusable for +drawing selection or highlight overlays, which is what R7's highlighting now +depends on. + +### Action + +Establish which of the two is authoritative — almost certainly the drawn +position — and make the other agree. This sits close to S2 (page geometry) and +S3 (draw/canvas lifecycle), both already landed, so the conventions to match +are in place. + +### Acceptance criteria + +- `page.query_point(centre_of(obj))` returns `obj` for every text object on a + rendered page, at scales 0.8, 1.0, 1.5 and 2.0. +- The end-to-end test in `tests/layout/test_font_scaling.py` probes the centre + directly instead of scanning a grid. + +### Files + +`pyWebLayout/concrete/text.py`, `pyWebLayout/concrete/page.py`, +`pyWebLayout/core/base.py` + +## Reproducing the findings + +Reviewed at `c5c61a3` on Python 3.14.6, in a venv containing +`pytest pyphen Pillow numpy beautifulsoup4 lxml ebooklib`. + +- **R1**: call `EreaderLayoutManager(...).get_current_page()` from module-level + script code (no `__main__` guard). +- **R2**: `python -m pytest -q`; observe the summary line, then the hang. +- **R3**: parse HTML containing ``, call + `BidirectionalLayouter._scale_block_fonts(block, 1.5)`, count `LinkedWord` + instances in the result. +- **R5**: compare `inspect.signature(Page.can_fit_line)` against the patch body. +- **R6**: grep the five method names across `pyWebLayout/ tests/ examples/ scripts/`. +- **R8**: read the loop; no execution needed. diff --git a/docs/LAYOUT_REMEDIATION_SPEC.md b/docs/LAYOUT_REMEDIATION_SPEC.md new file mode 100644 index 0000000..00784cd --- /dev/null +++ b/docs/LAYOUT_REMEDIATION_SPEC.md @@ -0,0 +1,1404 @@ +# Layout Remediation Spec + +Remediation plan for the defects found in the block/table rendering path +(audit of 2026-08-06), plus two outstanding defects in the pagination and +background-rendering paths (S11, S12). Twelve specs, sequenced into five phases. + +Every problem statement below was reproduced against the code at `2a543d0`; the +reproductions are quoted verbatim so each spec has a falsifiable "before" state. + +**S11 is the most user-visible defect in this document** and the cheapest to +fix: a single paragraph larger than one page dead-ends the reader permanently. +It is independent of every other spec here. + +## Contents + +| ID | Spec | Phase | +|----|------|-------| +| [S1](#s1--inline-content-in-non-paragraph-containers) | Inline content in non-paragraph containers | 0 | +| [S2](#s2--page-geometry-origin-and-content-rect) | Page geometry: origin and content rect | 1 | +| [S3](#s3--drawcanvas-lifecycle) | Draw/canvas lifecycle | 1 | +| [S4](#s4--one-block-dispatch-one-measurement) | One block dispatch, one measurement | 2 | +| [S5](#s5--cells-as-sub-layouts) | Cells as sub-layouts | 2 | +| [S6](#s6--table-grid-model) | Table grid model | 3 | +| [S7](#s7--retained-mode-table-rendering) | Retained-mode table rendering | 3 | +| [S8](#s8--table-and-list-pagination) | Table and list pagination | 4 | +| [S9](#s9--interactivity-inside-tables) | Interactivity inside tables | 4 | +| [S10](#s10--contracts-and-hygiene) | Contracts and hygiene | 5 | +| [S11](#s11--partial-block-progress-is-discarded) | Partial-block progress is discarded | 0 | +| [S12](#s12--background-rendering) | Background rendering | 4 | +| [S13](#s13--word-spacing-and-alignment) | Word spacing and alignment | 0 | +| [S14](#s14--vertical-centring-in-buttons-and-fields) | Vertical centring in buttons and fields | 0 | +| [S15](#s15--form-field-label-geometry) | Form field label geometry | 0 | +| [S16](#s16--backward-page-navigation) | Backward page navigation | 0 | + +## Design invariants + +These are the rules the specs exist to establish. After remediation, a reviewer +should be able to reject a change by pointing at one of these. + +1. **One dispatch.** There is exactly one function that maps an abstract block to + concrete objects on a page. Tables, the document layouter and the ereader all + call it. No component re-implements paragraph layout. +2. **Retained mode everywhere.** Layout produces children; `Page.render()` draws + them. Nothing paints during layout. A page can be rendered any number of times + and produce identical output. +3. **Measure and render agree by construction.** Reported height comes from the + same objects that get drawn, never from a parallel estimator. +4. **A cell is a page.** Any block the layout engine can place on a page can be + placed in a table cell, a list item or a blockquote, with no per-container + type switch. +5. **Style flows from the document.** No layout component invents a font, a size + or a path. Absolute font paths never appear outside `style/fonts.py`. + +## Phasing + +``` +S11 (independent, ship today) +S1 (independent, ship first) +S12 (independent; decide delete-vs-fix before touching S8) +S2 ─┬─ S4 ── S5 ─┬─ S6 ── S7 ─┬─ S8 +S3 ─┘ │ └─ S9 + └──────────────── S10 (opportunistic, any time after S7) +``` + +S11 is a few lines and unblocks reading the affected books; it ships on its own, +immediately. S1 is independent of everything else and fixes silent content loss, +so it ships next regardless of appetite for the rest. S12 is independent but +should be decided before S8, since table pagination changes the cost model that +justifies background rendering at all. S2+S3 are small and unlock S4/S5, which +are the bulk of the work. S6/S7 are where the table actually becomes correct. +S8/S9 are integration. S10 is cleanup. + +--- + +## S1 — Inline content in non-paragraph containers + +### Problem + +Inline tags (`a`, `b`, `strong`, `em`, `span`, `code`, …) are registered to +[`ignore_handler`](../pyWebLayout/io/readers/html_extraction.py#L825-L828) +because they are meant to be consumed by +[`extract_text_content()`](../pyWebLayout/io/readers/html_extraction.py#L364-L466). +But only `paragraph_handler` and `heading_handler` ever call that function. +`div_handler`, `list_item_handler`, `table_cell_handler` and +`table_header_cell_handler` iterate children and call `process_element` per +child, so inline tags return `None` and their text is discarded. `div_handler` +additionally ignores `NavigableString` children outright. + +### Evidence + +``` +

hello world again

-> Paragraph ['hello', 'world', 'again'] ok +
hello world again
-> (no blocks at all) lost +
  • hello world again
  • -> HList with no words lost +hello world again -> Table with no words lost +
    link text -> Paragraph ['text'] link lost +``` + +Bare text nodes that *do* survive (in `td`) each become their own `Paragraph`, +so `a b c` would fragment onto three lines even once `` is handled. + +### Design + +Introduce a single helper that every block container uses to process its +children, replacing the four hand-rolled loops. It walks children in order, +accumulating consecutive *inline* children (tags and text) into a run, and +flushing that run into one `Paragraph` whenever a *block* child interrupts it or +the children are exhausted. + +Inline-ness is decided by one predicate, not by each caller: + +```python +# html_extraction.py + +INLINE_TAGS: FrozenSet[str] = frozenset({ + "a", "span", "strong", "b", "em", "i", "u", "s", "del", "ins", "mark", + "small", "sub", "sup", "code", "q", "cite", "abbr", "time", "br", +}) + +def is_inline(node) -> bool: + """True for text nodes and inline tags; False for block tags.""" + +def process_block_children(element: Tag, context: StyleContext) -> List[Block]: + """ + Process an element's children into a block list, coalescing runs of inline + content into paragraphs. + + This is the single entry point for any container that may hold a mix of + inline and block content: div, li, td, th, blockquote, figure, section... + """ +``` + +`process_block_children` delegates each inline run to the existing +`extract_text_content`, which already handles `` → `LinkedWord`, style +nesting and background inheritance — it is called on a synthetic run rather +than on the whole element. The simplest correct implementation wraps the run's +nodes in a detached `Tag` and calls `extract_text_content` on it; that keeps +one code path for inline styling. + +`
    ` inside a run terminates the current paragraph and starts a new one, +replacing the current no-op [`line_break_handler`](../pyWebLayout/io/readers/html_extraction.py#L791-L794). + +Callers become one line each: + +```python +def div_handler(element, context): return process_block_children(element, context) +def list_item_handler(element, context): item = ListItem(...); item._blocks = process_block_children(...) +def table_cell_handler(element, context): cell = TableCell(...); for b in process_block_children(...): cell.add_block(b) +``` + +`paragraph_handler`/`heading_handler` keep their current behaviour (they are +already correct); their image-splitting logic in +[html_extraction.py:492-552](../pyWebLayout/io/readers/html_extraction.py#L492-L552) +is subsumed by `process_block_children` and should be folded in — an `` is +a block child, so the "text before, image after" split falls out of the general +algorithm for free. + +### Acceptance criteria + +- All four markup samples in *Evidence* produce the same words as the `

    ` + control, in document order, with `LinkedWord` preserved for ``. +- `

    ` yields **one** `Paragraph` of three words, not three + paragraphs. +- `text

    para

    more` yields three blocks in order: Paragraph, Paragraph, + Paragraph — inline runs on either side of a block child are not merged. +- `a
    b` yields two paragraphs. +- `

    textmore

    ` behaves as it does today (regression). +- Round-trip test over `tests/io_tests/` fixtures shows no block-count or + word-count regressions. + +### Files + +`pyWebLayout/io/readers/html_extraction.py`, `tests/io_tests/test_html_extraction.py` + +### Risk + +Low, and contained to the reader. The change *adds* content where there was +none, so existing assertions on parsed structure may need their expected counts +raised. Grep for tests asserting `len(blocks) == N` before starting. + +--- + +## S2 — Page geometry: origin and content rect + +### Problem + +Two issues, one cause: `Page` assumes it is rooted at (0,0) and has no notion of +a content rectangle. + +1. **Horizontal padding is ignored.** `paragraph_layouter` sets + `x_cursor = page.border_size` ([document_layouter.py:154](../pyWebLayout/layout/document_layouter.py#L154)) + while line width is `page.available_width` (which *does* subtract both + paddings). With `border_width=2, padding=(40,40,40,40)` on a 400px page, the + line lands at `origin.x = 2, width = 316`: text hugs the border and the whole + 80px padding budget accumulates on the right. Vertical padding is honoured + ([page.py:34](../pyWebLayout/concrete/page.py#L34)), so the asymmetry is + horizontal only. +2. **A page cannot be placed inside another page**, which is the prerequisite for + S5 (cells as sub-layouts). + +### Design + +Give `Page` an origin and derive a content rect from it. Default `(0, 0)` keeps +every existing caller behaviourally identical apart from the padding fix. + +```python +class Page(Renderable, Queriable): + def __init__(self, size, style=None, origin: Tuple[int, int] = (0, 0)): ... + + @property + def origin(self) -> np.ndarray: + """Absolute top-left of the page box.""" + + @property + def content_origin(self) -> Tuple[int, int]: + """Absolute top-left of the content box (origin + border + padding).""" + + @property + def content_rect(self) -> Tuple[int, int, int, int]: + """(x, y, w, h) of the content box in absolute coordinates.""" + + @property + def remaining_height(self) -> int: + """Content-box height still available below _current_y_offset.""" +``` + +`remaining_height` replaces the ad-hoc +`page.size[1] - page._current_y_offset - page.border_size` computed inline by +`image_layouter`, `table_layouter`, `button_layouter` and `form_layouter` — all +four of which subtract the border but not the bottom padding, so every block +type may currently be placed up to `padding_bottom` past its boundary. + +The existing [`free_space()`](../pyWebLayout/concrete/page.py#L41-L43) has the +same defect (it returns full page width, and height ignoring bottom border and +padding) and has no callers; delete it in favour of `content_rect` / +`remaining_height`. + +All layouters switch from `page.border_size` to `page.content_origin[0]` for the +x cursor, and `_current_y_offset` is initialised to `content_origin[1]`. +`can_fit_line`'s `max_y` becomes `content_rect.y + content_rect.h`. + +`_current_y_offset` stays absolute, so no arithmetic elsewhere changes sign. + +### Acceptance criteria + +- With `size=(400,300), border_width=2, padding=(40,40,40,40)`: the first line + has `origin == (42, 42)` and `size[0] == 316`; rendered ink starts at + x ≥ 42 and ends at x ≤ 358. +- No block is placed within `padding_bottom` of the page bottom, for every block + type (paragraph, image, table, button, form). +- A `Page(size=(100,50), origin=(200,300))` places its first line at + `(200 + border + padding_left, 300 + border + padding_top)`. +- Existing rendering tests with default `padding=(20,20,20,20)` shift right by + 20px — golden images in `docs/images/` and `test_output/` must be regenerated + and eyeballed once, deliberately, as part of this spec. + +### Files + +`pyWebLayout/concrete/page.py`, `pyWebLayout/concrete/dynamic_page.py`, +`pyWebLayout/layout/document_layouter.py` + +### Risk + +Medium — it moves every existing rendering by `padding_left`. That is the point, +but it invalidates every golden image at once. Do it in its own commit, separate +from anything else, so the diff of regenerated images is reviewable. + +--- + +## S3 — Draw/canvas lifecycle + +### Problem + +[`Page.add_child`](../pyWebLayout/concrete/page.py#L140-L154) sets +`self._canvas = None` but leaves `self._draw` bound to the discarded canvas, and +the [`draw` property](../pyWebLayout/concrete/page.py#L131-L138) only rebuilds +when `_draw is None`. So after the first `add_child`, `page.draw` hands out a +draw context pointing at an orphaned image while `page._canvas` stays `None`. + +### Evidence + +``` +after first .draw: _canvas set? True +after add_child: _canvas set? False _draw is stale? True +page.draw returns same stale object? True page._canvas still None? True +=> a table laid out now receives canvas = None +``` + +Downstream: `table_layouter` passes `canvas=None` into `TableRenderer`, so every +image inside a cell silently degrades to a grey `[Image: WxH]` placeholder +([table.py:297-320](../pyWebLayout/concrete/table.py#L297-L320)). + +Note the trap: naively fixing the property so it rebuilds whenever +`_canvas is None` makes layout allocate a full-page RGBA canvas on *every* +`add_child`, because layout calls `page.draw` to measure text. The fix has to +separate the two uses. + +### Design + +Split measurement from rendering. + +```python +class Page: + @property + def measurement_draw(self) -> ImageDraw.ImageDraw: + """ + Persistent 1x1 scratch draw context used for text metrics during layout. + Never invalidated; matches the render canvas's mode so that + Text width caching keys stay consistent. + """ + + @property + def draw(self) -> ImageDraw.ImageDraw: + """Draw context bound to the live render canvas, rebuilt if invalidated.""" +``` + +- Layouters construct `Line`/`Text` with `page.measurement_draw`. +- `Page.render_children` already rebinds `child._draw = self._draw` and + `child._canvas = self._canvas` before rendering + ([page.py:256-269](../pyWebLayout/concrete/page.py#L256-L269)), so children + built against the scratch context draw onto the real canvas at render time. + That rebinding becomes load-bearing rather than incidental — document it as + such, and extend it to recurse into child pages (S5). +- `draw` rebuilds when `self._draw is None or self._canvas is None`. + +Because `Text._calculate_dimensions` keys its width cache on `self._draw.mode` +([text.py](../pyWebLayout/concrete/text.py)), the scratch image must be created +in the same mode as the render canvas (`RGBA`) or the cache will hold two +entries per word. + +### Acceptance criteria + +- Laying out 100 paragraphs allocates **zero** full-page canvases (assert via a + counter patched onto `_create_canvas`). +- After any sequence of `add_child` calls, `page.draw.im` is the same image + object as `page._canvas`. +- A table containing an image, laid out after a paragraph, renders the real + image and not the placeholder. +- `page.render()` called twice returns pixel-identical images. + +### Files + +`pyWebLayout/concrete/page.py`, `pyWebLayout/layout/document_layouter.py` + +--- + +## S4 — One block dispatch, one measurement + +### Problem + +There are three implementations of "lay out content in a box", which must agree +and do not: + +| Purpose | Location | Word spacing | Hyphenation | +|---|---|---|---| +| Rendering | [`_render_cell_content`](../pyWebLayout/concrete/table.py#L110-L242) | `0.25–0.5 × font_size` | real, via `Line.add_word` | +| Height | [`_estimate_wrapped_lines`](../pyWebLayout/concrete/table.py#L578-L639) | `0.25 × font_size` | "assume one line" | +| Width | [`layout_cell_content`](../pyWebLayout/layout/table_optimizer.py#L117-L186) | hardcoded `(3, 6)` | none | + +`layout_cell_content` additionally appends to `line._text_objects` directly, +bypassing the fitting logic it is trying to predict. + +Separately, block *dispatch* is duplicated three times: +[`DocumentLayouter.layout_document`](../pyWebLayout/layout/document_layouter.py#L683-L723), +[`EreaderLayout._layout_block_on_page`](../pyWebLayout/layout/ereader_layout.py#L493-L519), +and the `isinstance` chain in `_render_cell_content`. They support different +block types, which is why tables render paragraphs but not lists, and the +ereader renders neither. + +### Design + +Two new public functions, one module each. + +**Dispatch** — `pyWebLayout/layout/block_layouter.py`: + +```python +@dataclass +class LayoutResult: + complete: bool # block fully placed + next_word: Optional[int] = None # resume index if not complete + pretext: Optional[Text] = None # hyphenated remainder + next_item: Optional[int] = None # resume index for lists + next_row: Optional[int] = None # resume index for tables + +def layout_block(block: Block, + page: Page, + start_word: int = 0, + pretext: Optional[Text] = None, + **resume) -> LayoutResult: + """ + Place one abstract block onto a page, starting from a resume point. + The single dispatch point for Paragraph, Heading, Image, Table, HList, + Quote, CodeBlock, HorizontalRule, PageBreak, Button, Form. + """ +``` + +The existing `paragraph_layouter`, `image_layouter`, `table_layouter`, +`button_layouter`, `form_layouter` stay as the per-type implementations; +`layout_block` is the registry in front of them. `DocumentLayouter`, +`EreaderLayout` and the cell layout of S5 all call it, and gaining a block type +means adding one entry, once. + +**Measurement** — `pyWebLayout/layout/measure.py`: + +```python +@dataclass +class BlockMeasure: + min_width: int # narrowest the block can be without overflow + max_width: int # width at which the block never wraps + +def measure_block(block: Block, style_ctx: RenderingContext) -> BlockMeasure: + """Intrinsic width demands of a block, per the CSS min-content/max-content model.""" +``` + +Rules per type: +- `Paragraph`/`Heading`: `min_width` = widest unbreakable run — a word wider + than its font's `min_hyphenation_width` contributes its longest hyphenation + fragment, not its full width; `max_width` = Σ word widths + (n−1) × min spacing. +- `Image`: intrinsic width for both. +- `Table`: recurse (nested tables measure through the S6 grid). +- `HList`: item measure + marker indent. + +Measurement uses each word's *own* style, so it is correct for mixed formatting +— unlike all three current implementations, which assume one font for the box. + +Heights are **not** part of measurement. Height comes from running +`layout_block` at the final assigned width and reading `_current_y_offset`. +`_estimate_wrapped_lines` and `layout_cell_content` are deleted, and +`DynamicPage.get_min_width`/`get_preferred_width` are reimplemented in terms of +`measure_block` (they currently walk `Line._text_objects`, which only works if +someone has pre-poked the lines). + +### Acceptance criteria + +- `_estimate_wrapped_lines` and `layout_cell_content` no longer exist. +- Property test: for a corpus of paragraphs and widths, laying out at + `measure_block(b).max_width` produces exactly one line, and laying out at + `min_width` produces no line that overflows its box. +- `layout_block` handles every type in the union above; adding a type to the + registry makes it work in documents, cells and the ereader simultaneously + (assert with one test that exercises all three call sites). +- Mixed-font paragraph: a paragraph whose words have different sizes measures + wider than the same word count at the smallest size. + +### Files + +new `pyWebLayout/layout/block_layouter.py`, new `pyWebLayout/layout/measure.py`, +`pyWebLayout/layout/document_layouter.py`, `pyWebLayout/layout/table_optimizer.py`, +`pyWebLayout/concrete/dynamic_page.py`, `pyWebLayout/concrete/table.py` + +### Risk + +Largest spec by volume, but mostly deletion. Land `measure.py` first with tests +against the current optimizer's outputs to establish a baseline, then swap the +optimizer over, then delete. + +--- + +## S5 — Cells as sub-layouts + +### Problem + +[`TableCellRenderer._render_cell_content`](../pyWebLayout/concrete/table.py#L110-L242) +hand-rolls line breaking and handles only `Paragraph`, `Heading` and `Image` +([table.py:141-146](../pyWebLayout/concrete/table.py#L141-L146)); lists, nested +tables, quotes, code blocks, buttons and forms are silently dropped. It also +discards styling: it hardcodes +`/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf` (a Debian path — **it does not +exist on the dev machine**; 6 such literals across the package) and +`font_size = 12`, then rebuilds every word as a plain `Word(text, font)` +([table.py:156-171](../pyWebLayout/concrete/table.py#L156-L171)), throwing away +bold/italic/colour/size, the bundled-font system, the font-family override, and +`LinkedWord` link targets. + +`DynamicPage` — the abstraction that exists precisely for this — is used only +for measurement, despite `3bcd1bf` claiming cells can hold anything. + +### Design + +A cell owns a `DynamicPage` positioned at the cell's content origin, sharing the +parent's canvas, filled by `layout_block` (S4). + +```python +class TableCellRenderer(Box): + def __init__(self, cell, origin, size, style, is_header_section=False): + self._page = DynamicPage( + style=PageStyle(padding=style.cell_padding, border_width=0, + background_color=...), + origin=origin, # S2 + ) + self._page.layout(size) + for block in cell.blocks(): + layout_block(block, self._page) # S4 + + def render(self): + self._draw_background_and_border() + self._page.render_children() # children already positioned absolutely +``` + +Consequences that fall out rather than being coded: + +- **Arbitrary content works.** Nested tables, lists, buttons — anything in the + S4 registry. Invariant 4 is satisfied structurally, not by a type switch. +- **Styling is preserved**, because `layout_block` uses each word's own style. + Header cells get bold via `get_or_create_font(weight=FontWeight.BOLD)` + ([core/base.py:186](../pyWebLayout/core/base.py#L186)) rather than by + substituting a font *file path*. No absolute paths outside `style/fonts.py`. +- **Cell height is the laid-out height** (`_page._current_y_offset - origin.y`), + so S6's row heights are exact by construction (invariant 3). +- **Overflow is bounded**: `can_fit_line` against the cell page's content rect + stops content at the cell edge instead of painting over neighbours. + +`TableCellRenderer` keeps its constructor shape so `TableRowRenderer` is +unaffected, but loses the `draw`/`canvas` parameters — the canvas arrives at +render time via the S3 rebinding, which must now recurse into child pages: + +```python +def render_children(self): + for child in self._children: + if isinstance(child, Page): + child.attach_surface(self._canvas, self._draw) # recurse + ... +``` + +### Acceptance criteria + +- A cell containing an `HList`, a nested `Table`, a `Quote` and a `Button` + renders all four (today: a bare list and a nested table draw nothing but the + cell border). +- A cell whose words carry bold/italic/24px/red styling renders with those + attributes; assert on the `Text` objects' `_style`, not on pixels. +- `grep -rn "/usr/share/fonts" pyWebLayout/` returns 0 hits. +- A cell containing a `LinkedWord` produces a `LinkText` concrete object + (prerequisite for S9). +- Cell content that exceeds the cell's height is clipped at the cell boundary + and does not overpaint the next row. + +### Files + +`pyWebLayout/concrete/table.py`, `pyWebLayout/concrete/dynamic_page.py`, +`pyWebLayout/concrete/page.py` + +--- + +## S6 — Table grid model + +### Problem + +Four separate geometry defects: + +1. **`size` is wrong.** [table.py:445-446](../pyWebLayout/concrete/table.py#L445-L446) + computes total height as `sum(self._row_heights.values())`, but `_row_heights` + is a **three-entry dict** (`header`/`body`/`footer`), not one entry per row. + A 10-row table reports height **44px**; its bottom-most drawn pixel is at + **y=409**. The caption is drawn but not counted either. +2. **Uniform row heights.** [`_calculate_row_height_for_section`](../pyWebLayout/concrete/table.py#L499-L576) + takes the max across all rows in a section, so one verbose cell inflates every + row in the table. +3. **colspan is not counted.** [`get_column_count`](../pyWebLayout/layout/table_optimizer.py#L189-L205) + returns `first_row.cell_count`. For `` followed by + `` it returns **1**, and + [`TableRowRenderer.render`](../pyWebLayout/concrete/table.py#L383) silently + drops every cell past `len(column_widths)` — two of three cells never render. +4. **rowspan is parsed and stored but never read** by any renderer or measurer; + spanned rows just shift left. +5. **Row height ignores the cell padding it must contain.** The 40px minimum in + `_calculate_row_height_for_section` is a constant, so a larger `cell_padding` + eats into the content box rather than growing the row, and + `_render_cell_content` then clips the text against `available_height`. + Rendering the same header at two paddings: + + ``` + padding=(8,10,8,10) border=1: header h=40, ink=593 + padding=(10,12,10,12) border=2: header h=40, ink=288 + ``` + + Both rows are 40px tall; the second silently loses half its text. This is + visible in `docs/images/example_05_html_table_with_images.png`, whose second + table renders an empty header row. It is the same measure/render disagreement + as defect 1, and S5 removes it by construction: the cell page's content box + *is* the box its padding leaves. + +### Design + +A resolved grid, built once, that every other stage consumes. + +```python +# pyWebLayout/layout/table_grid.py + +@dataclass(frozen=True) +class GridCell: + cell: TableCell + row: int # resolved row index across all sections + col: int # resolved column index + colspan: int + rowspan: int + section: str # "header" | "body" | "footer" + +class TableGrid: + """ + Occupancy-resolved view of an abstract Table. + + Built by the standard HTML table algorithm: walk rows in order, maintaining a + set of slots occupied by open rowspans, placing each cell at the first free + column and marking its span. + """ + n_cols: int # max over rows of sum(colspan), not first row's cell count + n_rows: int + def cells(self) -> Iterator[GridCell]: ... + def row(self, index: int) -> List[GridCell]: ... + def cell_at(self, row: int, col: int) -> Optional[GridCell]: ... +``` + +**Column widths.** `optimize_table_layout` takes a `TableGrid` and per-cell +`measure_block` results (S4). Span distribution follows CSS: a cell spanning *k* +columns imposes its demand on the *sum* of those columns, and only widens them +(proportionally to their current demand) if the sum falls short. Single-column +demands are applied first so spanning cells cannot dominate. + +**Row heights.** Per row, not per section: lay out every cell in the row at its +assigned width (S5) and take the max of the resulting content heights. A cell +with `rowspan = k` contributes to row *r+k−1* only if the accumulated height of +rows *r…r+k−1* is less than the cell needs, and the shortfall is distributed +across those rows. + +**Size.** `TableRenderer._size` = caption height + Σ per-row heights + borders, +computed from the same per-row heights that `render()` walks. The two must come +from one list; a test asserts they cannot diverge. + +### Acceptance criteria + +- `` → `grid.n_cols == 3`, and all + four cells render. +- `` spans two rows vertically; the cell below it in the next row + is not shifted left. +- 10-row table: `renderer.size[1]` equals `last_drawn_pixel_y - origin_y + border` + (±1 for border rounding). This is the direct regression test for defect 1 — + today it is 44 vs 409. +- A table with one tall row and nine short ones is shorter than 10 × tall row. +- A captioned table's `size[1]` includes the caption. +- Fuzz: for randomly generated colspan/rowspan tables, no two rendered cells' + rectangles overlap, and every cell in the abstract table appears exactly once + in the grid. + +### Files + +new `pyWebLayout/layout/table_grid.py`, `pyWebLayout/layout/table_optimizer.py`, +`pyWebLayout/concrete/table.py` + +--- + +## S7 — Retained-mode table rendering + +### Problem + +Every other block type is retained-mode: layouters build objects, `add_child` +them, and `Page.render()` draws them onto a fresh canvas. Tables are +immediate-mode: [`table_layouter`](../pyWebLayout/layout/document_layouter.py#L351-L402) +grabs `page.draw`, paints directly, bumps `_current_y_offset`, and never adds +anything to `page._children`. + +### Evidence + +``` +layout_table -> True +pixels on canvas right after layout: 28328 +pixels after page.render(): 0 +children on page: 0 +``` + +`Page.render()` rebuilds the canvas ([page.py:279](../pyWebLayout/concrete/page.py#L279)) +and the table is not a child, so it is erased. Lay out a table *and* a +paragraph, render, and only the paragraph survives. The examples appear to work +only because they read `page._canvas` directly instead of calling `render()`. + +Because `size` lies (S6), the fit check +`if table_height > available_height: return False` is also meaningless: a ~950px +table "fits" a 300px page, draws past the border, and leaves +`_current_y_offset = 64` so the next paragraph overlaps it. + +### Design + +- `TableRenderer.__init__` measures only — it builds the grid, resolves widths, + lays out cells (S5) and computes `size`. It draws nothing and takes no + `draw`/`canvas` parameters. +- `TableRenderer.render()` draws, and is called by `Page.render_children()`. +- `table_layouter` becomes structurally identical to `image_layouter`: + +```python +def table_layouter(table, page, style=None) -> bool: + renderer = TableRenderer(table, origin=(page.content_origin[0], page._current_y_offset), + available_width=page.available_width, style=style) + if renderer.size[1] > page.remaining_height: + return False # honest check, honest size (S6) + page.add_child(renderer) # retained mode + return True +``` + +- `_row_renderers` / `_cell_renderers` are rebuilt per `render()` call, not + appended to (today they grow without bound on re-render). + +### Acceptance criteria + +- `layout_table(...)` then `page.render()` yields a canvas containing the table. +- `page.render()` twice → pixel-identical images. +- Table + following paragraph: both present, non-overlapping, paragraph starts + below `table.size[1]`. +- A table taller than the remaining space returns `False` and adds no child. +- `len(page.children) == 1` after laying out one table. +- Rendering the same `TableRenderer` five times leaves + `len(renderer._row_renderers)` equal to the row count. + +### Files + +`pyWebLayout/concrete/table.py`, `pyWebLayout/layout/document_layouter.py` + +--- + +## S8 — Table and list pagination + +### Problem + +[`EreaderLayout._layout_table_on_page`](../pyWebLayout/layout/ereader_layout.py#L598-L611) +skips tables outright ("For now, skip tables"), and `_layout_list_on_page` does +the same for lists. The ereader — the library's main consumer — renders neither. +`RenderingPosition` already carries `table_row`, `table_col` and +`list_item_index` ([ereader_layout.py:38-40](../pyWebLayout/layout/ereader_layout.py#L33-L45)), +so the state model anticipated this; only the layout half is missing. +`examples/13_table_pagination_demo.py` documents pagination behaviour that the +library does not implement. + +### Design + +With S4 (`layout_block` returning a resume point) and S6/S7 (honest per-row +geometry), pagination is row-splitting rather than new machinery. + +```python +def table_layouter(table, page, style=None, start_row: int = 0) -> LayoutResult: + """ + Place as many rows as fit from start_row. + Returns complete=False with next_row set when the table is split. + """ +``` + +Rules: +- Split only on row boundaries. A single row taller than a full page is placed + anyway and clipped — with a `log.warning`, not silently. +- Header rows repeat at the top of each continuation fragment. This is a + `TableStyle` flag (`repeat_header: bool = True`), because for a two-row table + it is noise. +- Column widths are resolved once for the whole table and reused across + fragments, so columns line up across pages. + +`EreaderLayout._layout_table_on_page` / `_layout_list_on_page` delegate to +`layout_block` and map `LayoutResult.next_row` / `next_item` onto +`RenderingPosition`. The existing bidirectional navigation +(`render_page_backward`) must round-trip through table positions — that is the +part most likely to bite, so it gets its own test. + +### Acceptance criteria + +- A 60-row table across a 3-page document renders every row exactly once, + no duplicates, no gaps. +- Forward then backward navigation across a table returns to the identical + starting position (extends `tests/layout/test_navigation_consistency.py`). +- With `repeat_header=True`, every fragment starts with the header row. +- A serialized `RenderingPosition` inside a table restores to the same page after + reload. +- Lists paginate mid-list and resume at the right item, with markers continuing + their numbering. + +### Files + +`pyWebLayout/layout/ereader_layout.py`, `pyWebLayout/layout/document_layouter.py`, +`pyWebLayout/concrete/table.py` + +### Risk + +The ereader's backward-navigation estimator +([ereader_layout.py:400-465](../pyWebLayout/layout/ereader_layout.py#L430-L465)) +assumes blocks are cheap to re-lay-out repeatedly. Tables are not. Expect to +need a per-table layout cache keyed on `(table_id, available_width, font_scale)` +before this is usable at speed. + +--- + +## S9 — Interactivity inside tables + +### Problem + +Nothing inside a table is hit-testable: `query_point` into a rendered table +returns `object_type="empty"`. Links, images and buttons in cells are invisible +to the query/selection/callback system the rest of the library is built on. +`examples/14_interactive_table.py` works around this by hand-painting buttons +*on top of* the table and doing its own coordinate maths — the demo fakes the +feature. Meanwhile `TableCellRenderer` sets bounds on `InteractiveImage` +([table.py:322-327](../pyWebLayout/concrete/table.py#L322-L327)), a third, +parallel interaction mechanism. + +### Design + +S5 and S7 make this mostly free: cells are pages, tables are children, and +`Page.query_point` already recurses into children that implement `query_point` +([page.py:345-358](../pyWebLayout/concrete/page.py#L345-L358)). + +Required: +- `TableRenderer` and `TableCellRenderer` implement `Queriable.in_object` (they + are `Box` subclasses, so bounds already exist) and `query_point`, delegating to + the cell's `DynamicPage`. +- Because child pages are positioned in absolute coordinates (S2), **no + coordinate translation is needed** — a translating implementation would be a + sign S2 was not applied. +- Callback registration cascades: a cell page's `CallbackRegistry` merges into + the owning page's registry at `add_child` time, so `page.callbacks` remains the + single lookup point. +- `InteractiveImage.set_rendered_bounds` becomes redundant for the table path; + keep it for direct users but stop calling it from the cell renderer. + +### Acceptance criteria + +- `page.query_point(p)` for a point over a link in a cell returns + `object_type == "link"` with the correct `link_target`. +- A `Button` in a cell fires its callback through the normal + `page.callbacks` path. +- `examples/14_interactive_table.py` is rewritten to put real `Button` blocks in + cells and delete its manual overlay maths — the example shrinks substantially, + which is the acceptance signal. +- `query_range` selection spanning a table returns the cell text in document + order. + +### Files + +`pyWebLayout/concrete/table.py`, `pyWebLayout/concrete/page.py`, +`pyWebLayout/core/callback_registry.py`, `examples/14_interactive_table.py` + +--- + +## S10 — Contracts and hygiene + +Small independent items. Each is a one-commit change; none blocks the others. + +### 10.1 Render contract + +`TableRenderer.render()`, `TableRowRenderer.render()` and +`TableCellRenderer.render()` are annotated `-> Image.Image` and all +`return None`. After S7, decide and enforce one contract: `render()` draws onto +the bound canvas and returns `None`; only `Page.render()` returns an image. +Update `Renderable.render`'s docstring in +[core/base.py:16-22](../pyWebLayout/core/base.py#L16-L22), which currently +promises a `PIL.Image`, and annotate accordingly. + +### 10.2 Abstract/concrete leak + +[concrete/__init__.py:18](../pyWebLayout/concrete/__init__.py) re-exports the +**abstract** `Table, TableRow as Row, TableCell as Cell` from the concrete +package, aliased to look concrete, while `TableRenderer` is not exported at all. +This contradicts `ARCHITECTURE.md`. Remove the re-export, export the renderers, +and add the `Cell`/`Row` names to a deprecation shim for one release if anything +external depends on them. + +### 10.3 Dead code + +- [table.py:229-242](../pyWebLayout/concrete/table.py#L229-L242): fallback + reading `self._cell._text_content`, an attribute that exists nowhere. +- `TableCellRenderer._children`: written never, read never. +- `dynamic_page.py`: `import numpy as np` unused; + `render_partial`/`has_more_content`/`reset_pagination` are an unused + pagination API superseded by S8 — delete or wire up, do not leave both. +- [document_layouter.py:157-161](../pyWebLayout/layout/document_layouter.py#L157-L161): + `temp_text.width` computed and discarded; `else: pass`. + +### 10.4 Exception handling + +13 bare `except Exception:` / `except BaseException:` in the package. The worst +is [table.py:331-333](../pyWebLayout/concrete/table.py#L331-L333), which swallows +every image failure into a 20px gap with no diagnostic. Policy: catch the +specific exception, log at `warning` with `exc_info=True`, and render a visible +placeholder for content failures. `except BaseException` (which catches +`KeyboardInterrupt`) is never correct here. + +### 10.5 Image source resolution + +[`_render_image_in_cell`](../pyWebLayout/concrete/table.py#L252-L266) probes six +attribute names (`source`, `_source`, `path`, `src`, `_path`, `_src`) to find an +image path. Give `abstract.block.Image` one documented accessor and use it. + +### Acceptance criteria + +- `grep -rn "except BaseException" pyWebLayout/` → 0 hits. +- No `hasattr` chain longer than one alternative anywhere in `concrete/table.py`. +- `python -W error -c "import pyWebLayout"` clean; flake8 reports no unused + imports in touched files. + +--- + +## S11 — Partial-block progress is discarded + +### Problem + +`BidirectionalLayouter.render_page_forward` discards the resume position of a +block that only partially fitted: + +```python +success, new_pos = self._layout_block_on_page(scaled_block, page, current_pos, font_scale) +if not success: + # Block doesn't fit, we're done with this page + break # <-- new_pos dropped on the floor +... +current_pos = new_pos +return page, current_pos +``` +[ereader_layout.py:341-347](../pyWebLayout/layout/ereader_layout.py#L330-L362) + +`paragraph_layouter` correctly returns the index of the first word that did not +fit, and `_layout_paragraph_on_page` correctly packs it into `new_pos.word_index` +([ereader_layout.py:570-582](../pyWebLayout/layout/ereader_layout.py#L570-L582)). +The information exists and is thrown away one frame up the stack. The returned +"next position" is therefore the *same* position the page started at. + +The bug is invisible for a document whose paragraphs each fit on a page — the +`not success` path is only taken when nothing more fits, which for normal prose +means the block boundary. It bites exactly when one block spans a page boundary, +i.e. any paragraph longer than a page. + +### Evidence + +A 2877-word paragraph at 800×600: + +``` +_layout_block_on_page -> success=False, new_pos.word_index=271 (start was 0) +render_page_forward -> next position (b0, w0) (start was b0, w0) + +page 0: start(b0,w0) -> next(b0,w0) lines=26 DEAD END +``` + +The page renders 26 lines of real content, then reports that the reader has made +no progress. Navigation is stuck on that page permanently; the book is +unreadable from that block onward. + +### Design + +Distinguish *nothing placed* from *something placed*. Only the former should +leave the position untouched. + +```python +if not success: + if self._position_compare(new_pos, current_pos) > 0: + # Partially placed: keep the progress, end the page here. + current_pos = new_pos + break +``` + +`_position_compare` already exists and is used by the backward navigation +([ereader_layout.py:439](../pyWebLayout/layout/ereader_layout.py#L439)), so +ordering semantics stay in one place. + +Two supporting changes, because a silent dead-end should not be possible again: + +1. **A no-progress guard in the navigation loop.** `EreaderManager.next_page` + asserts that the returned position is strictly greater than the requested + one; if not, it logs an error naming the block index and force-advances + `block_index += 1`. A malformed block should cost the reader one block, not + the rest of the book. +2. **The same audit for the backward path.** `render_page_backward`'s refinement + loop ([ereader_layout.py:400-465](../pyWebLayout/layout/ereader_layout.py#L430-L465)) + already has fallbacks for failing to move backward, which is the symmetric + symptom — check whether those fallbacks were compensating for this bug and + simplify them if so. + +### Acceptance criteria + +- A 2877-word single-paragraph document paginates to completion: 12 pages at + 800×600, every word appearing exactly once, positions strictly increasing. + (Verified against the patched implementation: pages advance + w0 → w271 → w531 → … → w2684 → end.) +- Forward-then-backward across a page-spanning paragraph round-trips to the + original position. +- A block that genuinely places nothing (e.g. an image taller than the page on + an empty page) still returns the unchanged position, and the no-progress guard + force-advances it with a logged error rather than looping. +- Regression test asserts `next_position > position` for **every** page of a + full-book pagination run over the EPUB fixtures. + +### Files + +`pyWebLayout/layout/ereader_layout.py`, `pyWebLayout/layout/ereader_manager.py` + +### Risk + +Low, and it makes pagination strictly more correct. Watch for page-count changes +in `tests/layout/test_navigation_consistency.py` — any fixture with a +page-spanning paragraph will now produce more pages, which is the fix working. + +--- + +## S12 — Background rendering + +### Problem + +`PageBuffer` starts a `ProcessPoolExecutor(max_workers=4)` +([page_buffer.py:117](../pyWebLayout/layout/page_buffer.py#L117)) and submits +page renders to it. **Every job fails.** `_render_page_worker` returns +`pickle.dumps(page)` ([page_buffer.py:50](../pyWebLayout/layout/page_buffer.py#L50)), +and a `Page` holds a live PIL canvas, which is not picklable. + +### Evidence + +``` +args pickle OK: 877.5 KB in 21 ms # submit succeeds +real pool round trip -> job FAILED: TypeError cannot pickle 'ImagingCore' object +``` + +So the cost is paid in full and the benefit is zero: 4 forked processes, the +entire block list pickled and shipped per job (~880KB for a 200-block document), +a full page laid out in the worker — then the result is thrown away by +`check_completed_renders`, which swallows the exception into a bare `print` +([page_buffer.py:274-276](../pyWebLayout/layout/page_buffer.py#L274-L276)). On a +4-core Pi with 512MB this is actively harmful: four interpreter copies plus four +copies of the book, to populate a cache that never populates. + +It is also not inert. The pool is started from `PageBuffer.initialize` inside a +process that already has threads, and CPython warns about exactly this: + +``` +DeprecationWarning: This process (pid=...) is multi-threaded, +use of fork() may lead to deadlocks in the child. +``` + +`tests/layout/test_ereader_image_rendering.py` intermittently hangs at +interpreter exit as a result — every test reports PASSED, then the process never +returns. Observed roughly one run in four. A reader that hangs on shutdown once +in four launches would be a shipped bug; the test suite is just where it shows +up first. This raises S12 from "wasted work" to "actively harmful". + +Four further defects in the same file, which matter only if the decision is to +keep it: + +1. `_render_page_worker` builds `BidirectionalLayouter(blocks, page_style, font_family_override=...)` + **without `page_size`**, so it silently uses the default `(800, 600)` + ([page_buffer.py:44](../pyWebLayout/layout/page_buffer.py#L44)). Fixing the + pickling alone would poison the cache with wrong-size pages. +2. `check_completed_renders` caches every result with `is_backward=False` + ([page_buffer.py:268](../pyWebLayout/layout/page_buffer.py#L268)), so backward + renders land in the forward buffer. +3. `_queue_forward_renders` / `_queue_backward_renders` `break` at the end of + their first loop body, so despite `for i in range(self.buffer_size)` they + queue at most one page each. +4. Failures are reported with `print`, not the logger. + +### Design + +**Recommendation: delete the process pool.** Replace it with synchronous +readahead, gated on a measurement. + +The justification for multiprocessing was sub-second navigation. That premise +predates the text caches now landing in `concrete/text.py`, whose own +measurements put a page at ~30ms on desktop once warm, with +`EreaderManager.prewarm_caches` ([ereader_manager.py:222](../pyWebLayout/layout/ereader_manager.py#L222)) +priming the working set at open time. If a page costs tens of milliseconds, a +process pool cannot pay for its own IPC, let alone its memory on the target +device. + +```python +class PageBuffer: + def __init__(self, buffer_size: int = 5): + """LRU page cache with synchronous readahead. No worker processes.""" + + def readahead(self, position: RenderingPosition, n: int = 1) -> None: + """ + Render and cache the next n pages on the calling thread. + Called after a navigation completes, when the reader is idle. + """ +``` + +This keeps the LRU buffers, the position maps and the invalidation logic — all +of which are fine — and removes the executor, the worker function, the pickling +and the `threading.Lock` that only guarded the pending-render dict. + +**Gate:** measure first, on the Pi, with the caches warm. Record page render time +at the target page size in the spec's PR description. If p95 is under ~150ms, +delete the pool. If it is materially worse, the fallback is a **single worker +thread**, not processes: layout is PIL-bound, PIL releases the GIL for +rasterisation, and a thread shares the block list instead of copying it. Fixing +the process pool properly would require making the entire concrete tree +picklable (Page → Line → Text → Font → `FreeTypeFont`), which is a large amount +of surface area to maintain for a cache. + +Whichever way the gate goes, defects 1–4 above are fixed or deleted with the code +that contains them, and failures are logged with `exc_info=True` rather than +printed. + +### Acceptance criteria + +- No `ProcessPoolExecutor` in the package, or — if the gate says keep it — a test + that submits a real job through a real pool and asserts it **succeeds**. The + absence of such a test is why this shipped broken. +- Measured page-render p95 on the target device recorded in the PR, before and + after. +- Peak RSS while paginating a full book drops by roughly the pool's share + (expect ~4 interpreter copies' worth on a 4-core device). +- Readahead of *n* pages populates the cache with *n* pages (today: one queued, + zero cached). +- Backward-rendered pages land in the backward buffer. +- Cache invalidation on font-scale and font-family change still clears both + buffers. + +### Files + +`pyWebLayout/layout/page_buffer.py`, `pyWebLayout/layout/ereader_manager.py` + +### Risk + +Low. The feature currently contributes nothing but overhead, so removing it +cannot regress rendering; the only risk is navigation latency, which is what the +gate measures. + +--- + +## S13 — Word spacing and alignment + +### Problem + +Three defects, all visible as a right edge that wobbles from line to line. + +1. **Ragged alignments stretched their gaps.** `LeftAlignmentHandler` distributed + the line's residual space across its word gaps, clamped to `max_spacing`. A + line whose residual divided to less than `max_spacing` was stretched flush; + one that exceeded it was not. So left-aligned text was justified *sometimes*, + by a different amount on each line. `CenterRightAlignmentHandler` did the same, + and additionally returned `ideal_space` while computing its start position from + a different value (`actual_spacing`), so centred lines were not centred. +2. **The last line of a justified paragraph was justified.** A three-word tail was + spread across the full measure. +3. **Justified lines fell 1–2px short.** `base_spacing = int(residual // gaps)` + with `remainder = int(residual % gaps)` discards the fractional part of both + terms, and word widths are fractional. + +### Design + +- Ragged alignments (left, centre, right) use a **constant** word space: the + font's own space advance, clamped to `[min_spacing, max_spacing]`, passed to + the handler as `natural_spacing`. They never absorb residual space — that + belongs in the margin. When a line cannot fit at natural spacing they report + overflow rather than tightening, so line breaking moves the word instead of + rendering deciding to squeeze it. +- `Line` carries `is_paragraph_end`, set by `paragraph_layouter` on the line + holding a paragraph's final word. `render_alignment_handler` substitutes flush + left for justify on that line only. A paragraph continued onto the next page + never reaches the marking code, so its lines stay justified — correct. +- Justification distributes the residual by **cumulative rounding** + (`round(total * i / gaps)` differenced), so the gaps sum to the residual + exactly and every line ends at the same x. +- Alignment becomes configurable: `PageStyle.default_alignment`, defaulting to + `JUSTIFY`, replaces the hardcoded `Alignment.LEFT` in `paragraph_layouter`. + `AbstractStyle.text_align` / `ConcreteStyle.text_align` now default to `None` + meaning "not specified", so HTML that sets no `text-align` inherits the page + default while explicit CSS still wins. Headings are never justified. + +### Acceptance criteria + +- Left-aligned word gaps are constant within a line and across lines (±1px). +- Left-aligned text does not end flush on every line — a flush edge means it was + justified. +- Justified body lines end within 2px of the margin; measured advance ends are + identical across lines, with ≤1px of ink variation from side bearings. +- The final line of a completed justified paragraph is not stretched. +- Centred lines have equal margins either side (±2px). +- Headings are flush left even when the page default is justify. + +### Files + +`pyWebLayout/concrete/text.py`, `pyWebLayout/layout/document_layouter.py`, +`pyWebLayout/style/page_style.py`, `pyWebLayout/style/abstract_style.py`, +`pyWebLayout/style/concrete_style.py` + +--- + +## S14 — Vertical centring in buttons and fields + +### Problem + +`ButtonText.render` and `FormFieldText.render` both placed the text baseline at +`box_top + box_height / 2 + descent / 2`. Centring glyphs whose visual height is +`ascent + descent` inside a box of height `H` puts the baseline at +`box_top + H/2 + (ascent - descent)/2`. The two agree only when +`ascent == 2 * descent`; DejaVu is nearer 4:1, so labels rode high against the +top edge of the control. + +`ButtonText` also sized itself as `font_size + padding`, but the text's visual +height exceeds the nominal size — DejaVu at 14px measures 17 — so the button was +too short to centre its own label in. + +### Evidence + +A 14px "Save Document" button with 6px vertical padding, measuring the label's +ink against the button rectangle: + +``` +gap above text: 5px +gap below text: 11px +``` + +### Design + +- `baseline = area_top + (area_height - (ascent + descent)) / 2 + ascent` in both + renderers. +- `ButtonText._padded_height` derives from `ascent + descent`, guarded so a mock + or unusual font object falls back to the nominal size. + +### Acceptance criteria + +- Label ink is centred within ±2px at font sizes 10, 14 and 20. +- Label ink stays inside the button rectangle. +- Button height is at least `ascent + descent + vertical padding`. +- A form field's value is centred within its input box (±3px). + +### Files + +`pyWebLayout/concrete/functional.py` + +### Note + +`docs/images/example_07_pressed_state.png` was stale — no example regenerates it; +`07_pressed_state_demo.py` writes `demo_07_pressed.png` at the repository root +and the docs copy had been placed by hand. It has been refreshed. Worth wiring +the demo to write straight to `docs/images/` so it cannot drift again. + +--- + +## S15 — Form field label geometry + +### Problem + +`FormFieldText` treats its origin as the control's top-left: `size` and +`in_object` both measure down from it. But it drew the label by calling +`Text.render` at that origin, and Text anchors on the **baseline**, so the +label's glyphs landed *above* the origin — outside the box the control claims, +on top of whatever was there. In a stacked form that is the previous field's +input box, which is what +`docs/images/example_10_forms.png` showed: every label but the first crowding +and touching the box above it. + +The height was also computed as `font_size + 5 + field_height`, understating the +label by the difference between nominal size and ink height, which left the gap +between label and box smaller than the intended 5px. + +### Design + +- The origin is documented as the top-left of the whole control. +- Rendering offsets the label down by its ascent, so the glyphs occupy + `[origin.y, origin.y + ascent + descent]`. +- `LABEL_GAP` names the 5px gap, and `field_area_offset` gives the distance from + the origin to the top of the input box. `render`, `handle_click` and the height + calculation all derive from it, instead of each recomputing `font_size + 5`. + +### Acceptance criteria + +- No label ink is drawn above the control's origin. +- All ink lies within `[origin.y, origin.y + size[1]]`. +- Consecutive fields laid out by `form_layouter` do not overlap. +- A click in the input area focuses the field; a click on the label does not. + +### Files + +`pyWebLayout/concrete/functional.py` + +--- + +## S16 — Backward page navigation + +### Problem + +`render_page_backward` *searched* for the previous page's start: estimate a block +index, lay out forward, compare the end against the target, bisect on the block +difference, repeat up to ten times. Both the estimator and the adjuster pinned +`word_index` to 0 and moved only `block_index`. + +Pages routinely start mid-block. Any such start was therefore **not in the search +space**, the loop could never match, and it fell through to a fallback that +jumped several blocks back or to the document start. + +### Evidence + +A document of short paragraphs around one 1200-word paragraph. Forward pagination +gives page starts at `(0,0), (2,208), (2,494), (2,780), (2,1057)`. Asking for the +page that ends where each of those begins: + +``` +from page 1 -> got (0,0) expected (0,0) ok (1 forward layout) +from page 2 -> got (0,0) expected (2,208) WRONG (10 forward layouts) +from page 3 -> got (0,0) expected (2,494) WRONG (10 forward layouts) +from page 4 -> got (0,0) expected (2,780) WRONG (10 forward layouts) +``` + +Every mid-paragraph case threw the reader to the start of the document after ten +full page layouts. The bisection was also unsound within its own space: a +document of 40 small paragraphs, where every page *does* start on a block +boundary, failed too. + +This is complementary to S11 rather than caused by it. Before S11 forward +pagination dead-ended at the first page-spanning block, so mid-block starts were +never produced and the block-granular search looked adequate. + +### Design + +Pagination is a pure function: laying out from `q` yields a page and the position +it stopped at, `next(q)`. The page before `P` is the `q` with `next(q) == P`. +That is found by **replaying the chain forward from an anchor**, not by guessing +`q`. Three sources, in order: + +1. **The recorded chain.** `render_page_forward` now records + `(font_scale, next(q)) -> q`. Stepping back to anywhere the reader has been is + exact and costs one layout. Keyed by font scale, since changing it + repaginates. +2. **Replay from an anchor.** Anchors are block starts, nearest first: the block + containing `P`, then up to `MAX_BACKWARD_ANCHORS` earlier ones, then the + document start. Lay out forward from the anchor until a page ends exactly on + `P`; that page's start is the answer. `MAX_REPLAY_PAGES` caps the walk so one + page turn cannot traverse a whole chapter. +3. **Nearest start before `P`.** If no chain passes exactly through `P` — which + happens when `P` was reached by a jump or a restored bookmark rather than by + reading forward, so it lies on no natural chain — return the last page start + before it. That overlaps `P`'s page slightly rather than skipping content, + which is the safe direction to be wrong in. + +The estimator and the bisecting adjuster are deleted. + +**What "correct" means here.** Each backward step returns a page ending exactly +where the reader currently is, so paging back never skips or repeats content. +That chain can differ from the one you would have seen reading forward from page +one, if you entered the document by a jump — pagination from a different starting +point is genuinely a different chain, and no algorithm can recover the original +without replaying from the start. + +### Measurements + +Same document, after the change: + +``` +warm (chain recorded by the forward pass): 4/4 exact, 1 layout each +cold, fresh layouter per call: 12/13 exact, worst 17 layouts +cold, one layouter, repeated back presses: 4 layouts per turn typical +``` + +The single inexact case is a target that lies on the canonical chain but not on +any chain reachable from a nearby anchor; it returns a start 15 words early, +i.e. a slightly overlapping page. + +### Acceptance criteria + +- For every page of a document, `render_page_backward(start[i])` returns + `start[i-1]` — verified for both a mid-paragraph-paginating document and one + where every page starts on a block boundary. +- Laying out forward from the returned position ends exactly on the requested + position. +- Forward-then-back returns to the original position. +- At the document start, backward stays there; an empty document is safe. +- Cost stays within a small bounded number of forward layouts. + +### Files + +`pyWebLayout/layout/ereader_layout.py` + +--- + +## Test plan + +Findings were reproduced with four probe scripts; each becomes a regression test +rather than being thrown away. + +| Regression test | Guards | Currently | +|---|---|---| +| `test_table_survives_page_render` | S7 | fails (0 px after render) | +| `test_table_size_matches_drawn_extent` | S6 | fails (44 vs 409) | +| `test_table_fit_check_rejects_oversized` | S6/S7 | fails (returns True) | +| `test_colspan_column_count` | S6 | fails (1 vs 3) | +| `test_rowspan_occupancy` | S6 | fails (ignored) | +| `test_inline_content_in_div_li_td` | S1 | fails (content lost) | +| `test_page_draw_not_stale_after_add_child` | S3 | fails (stale) | +| `test_horizontal_padding_honoured` | S2 | fails (x=2 vs 42) | +| `test_arbitrary_blocks_in_cell` | S5 | fails (dropped) | +| `test_query_point_into_table_cell` | S9 | fails ("empty") | +| `test_page_spanning_paragraph_advances` | S11 | fails (dead-ends at page 0) | +| `test_background_render_job_succeeds` | S12 | fails (unpicklable Page) | + +Note that `tests/concrete/test_table_rendering.py:541-553` currently asserts only +`height > 0`, which is why the size defect survived. Assertions of that shape +should be replaced wherever the tests touch geometry. + +Golden images in `docs/images/` are regenerated **once** under S2 and reviewed +deliberately; after that they are treated as fixtures and any diff is a +regression. + +## Out of scope + +Called out so their absence is a decision rather than an oversight: + +- CSS percentage widths in `parse_html_width` + ([table_optimizer.py:283-284](../pyWebLayout/layout/table_optimizer.py#L283-L284)) — + needs a containing-block model. +- `border-collapse: separate`, per-cell borders, per-cell background colour. +- Vertical alignment within cells (`valign`); everything is top-aligned. +- RTL and vertical writing modes. +- Floats and absolute positioning. diff --git a/docs/images/README.md b/docs/images/README.md new file mode 100644 index 0000000..c79db16 --- /dev/null +++ b/docs/images/README.md @@ -0,0 +1,213 @@ +# pyWebLayout Visual Documentation + +This directory contains visual documentation for pyWebLayout, including animated GIF demonstrations of the EbookReader functionality and static example outputs showcasing various features. + +## Generated GIFs + +### 1. Page Navigation (`ereader_page_navigation.gif`) +Demonstrates forward and backward page navigation through an EPUB book. Shows smooth transitions between pages using `next_page()` and `previous_page()` methods. + +**Features shown:** +- Sequential page advancement +- Page-by-page content rendering +- Natural reading flow + +### 2. Font Size Adjustment (`ereader_font_size.gif`) +Shows dynamic font size scaling from 0.8x to 1.4x and back. The reader maintains the current reading position even as the layout changes with different font sizes. + +**Features shown:** +- `increase_font_size()` / `decrease_font_size()` +- `set_font_size(scale)` with specific values +- Position preservation across layout changes +- Text reflow with different sizes + +### 3. Chapter Navigation (`ereader_chapter_navigation.gif`) +Demonstrates jumping between chapters in a book. Each chapter's first page is displayed, showing the ability to navigate non-linearly through the content. + +**Features shown:** +- `jump_to_chapter(index)` for index-based navigation +- `jump_to_chapter(title)` for title-based navigation +- `get_chapters()` to list available chapters +- Quick access to any part of the book + +### 4. Bookmarks & Positions (`ereader_bookmarks.gif`) +Illustrates the bookmark system: navigating to a position, saving it, navigating away, and then returning to the saved position. + +**Features shown:** +- `save_position(name)` to bookmark current location +- `load_position(name)` to return to saved bookmark +- Position stability across navigation +- Multiple bookmark support + +## Generating Your Own GIFs + +To generate these animations with your own EPUB file: + +```bash +cd examples +python generate_ereader_gifs.py path/to/your/book.epub ../docs/images/ +``` + +This will create all four GIF animations in the specified output directory. + +### Script Options + +```python +python generate_ereader_gifs.py [output_dir] +``` + +- `epub_path`: Path to your EPUB file (required) +- `output_dir`: Directory to save GIFs (default: current directory) + +### Customization + +You can modify `generate_ereader_gifs.py` to adjust: +- Frame duration (`duration` parameter in `create_gif()`) +- Page dimensions (change `page_size` in `EbookReader`) +- Number of frames for each animation +- Font scale ranges +- Animation sequences + +## Technical Details + +- **Format**: Animated GIF +- **Page Size**: 600x800 pixels +- **Frame Rate**: Variable (500-1000ms per frame) +- **Loop**: Infinite +- **Book Used**: Alice's Adventures in Wonderland (test.epub) + +## File Sizes + +| GIF | Size | Frames | Duration per Frame | +|-----|------|--------|-------------------| +| `ereader_page_navigation.gif` | ~500 KB | 10 | 600ms | +| `ereader_font_size.gif` | ~680 KB | 13 | 500ms | +| `ereader_chapter_navigation.gif` | ~290 KB | 11 | 1000ms | +| `ereader_bookmarks.gif` | ~500 KB | 17 | 600ms | + +--- + +## Example Outputs + +Static PNG images generated by the example scripts, demonstrating various pyWebLayout features. + +### Example 01: Simple Page Rendering +**File:** `example_01_page_rendering.png` +**Source:** [examples/01_simple_page_rendering.py](../../examples/01_simple_page_rendering.py) +**Demonstrates:** Page styles, borders, padding, background colors + +### Example 06: Functional Elements +**File:** `example_06_functional_elements.png` +**Source:** [examples/06_functional_elements_demo.py](../../examples/06_functional_elements_demo.py) +**Demonstrates:** Buttons, form fields, interactive elements + +### Example 08: Pagination (NEW) +**Files:** +- `example_08_pagination_explicit.png` (109 KB) - 5 pages with explicit PageBreaks +- `example_08_pagination_auto.png` (87 KB) - 2 pages with automatic pagination + +**Source:** [examples/08_pagination_demo.py](../../examples/08_pagination_demo.py) +**Test:** [tests/examples/test_08_pagination_demo.py](../../tests/examples/test_08_pagination_demo.py) + +**Demonstrates:** +- Using `PageBreak` to force content onto new pages +- Multi-page document layout with explicit breaks +- Automatic pagination when content overflows +- Page numbering functionality +- Document flow control + +**Coverage:** ✅ Fills critical gap - PageBreak had NO examples before this + +### Example 09: Link Navigation (NEW) +**File:** `example_09_link_navigation.png` (60 KB) +**Source:** [examples/09_link_navigation_demo.py](../../examples/09_link_navigation_demo.py) +**Test:** [tests/examples/test_09_link_navigation_demo.py](../../tests/examples/test_09_link_navigation_demo.py) + +**Demonstrates:** +- **Internal links** - Document navigation (`#section1`, `#section2`) +- **External links** - Web URLs (`https://example.com`) +- **API links** - API endpoints (`/api/settings`, `/api/save`) +- **Function links** - Direct function calls (`calculate()`, `process()`) +- Link styling (underlined, color-coded by type) +- Link callbacks and interactivity + +**Coverage:** ✅ Comprehensive - All 4 LinkType variations demonstrated + +### Example 10: Comprehensive Forms (NEW) +**File:** `example_10_forms.png` (31 KB) +**Source:** [examples/10_forms_demo.py](../../examples/10_forms_demo.py) +**Test:** [tests/examples/test_10_forms_demo.py](../../tests/examples/test_10_forms_demo.py) + +**Demonstrates all 14 FormFieldType variations:** + +**Text-Based Fields:** +- `TEXT` - Standard text input +- `EMAIL` - Email validation field +- `PASSWORD` - Password masking +- `URL` - URL validation +- `TEXTAREA` - Multi-line text + +**Number/Date/Time Fields:** +- `NUMBER` - Numeric input +- `DATE` - Date picker +- `TIME` - Time selector +- `RANGE` - Slider control +- `COLOR` - Color picker + +**Selection Fields:** +- `CHECKBOX` - Boolean selection +- `RADIO` - Single choice from options +- `SELECT` - Dropdown menu +- `HIDDEN` - Hidden form data + +**Coverage:** ✅ Complete - All 14 field types across 4 practical examples + +--- + +## Generating New Examples + +### Run Individual Examples +```bash +# Navigate to project root +cd /path/to/pyWebLayout + +# Run specific example +python examples/08_pagination_demo.py +python examples/09_link_navigation_demo.py +python examples/10_forms_demo.py +``` + +### Run All Example Tests +```bash +# Run all example tests with pytest +python -m pytest tests/examples/ -v + +# Run specific test file +python -m pytest tests/examples/test_08_pagination_demo.py -v +``` + +All new examples (08, 09, 10) include: +- ✅ Comprehensive documentation +- ✅ Full test coverage (30 tests total) +- ✅ Visual output verification +- ✅ Working code examples + +See the main [README.md](../../README.md) and [examples/README.md](../../examples/README.md) for detailed information. + +--- + +## Usage in Documentation + +These visual assets are used throughout the pyWebLayout documentation to showcase capabilities. + +To embed in Markdown: +```markdown +![Page Navigation](docs/images/ereader_page_navigation.gif) +![Pagination Example](docs/images/example_08_pagination_explicit.png) +``` + +To embed in HTML with size control: +```html +Page Navigation +Pagination +``` diff --git a/docs/images/demo_08_bundled_fonts.png b/docs/images/demo_08_bundled_fonts.png new file mode 100644 index 0000000000000000000000000000000000000000..0397e4b5d6104f0e23f931e21071da44534ef0cb GIT binary patch literal 97208 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 CjhKM| literal 0 HcmV?d00001 diff --git a/docs/images/example_07_button_animation.gif b/docs/images/example_07_button_animation.gif new file mode 100644 index 0000000000000000000000000000000000000000..15df78255b2ee7cb6b2d73b72113e2a633e02c21 GIT binary patch literal 21030 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 E0WeE$(*OVf literal 0 HcmV?d00001 diff --git a/docs/images/example_07_pressed_state.png b/docs/images/example_07_pressed_state.png new file mode 100644 index 0000000000000000000000000000000000000000..49f83cea822fcd9f7bc5c218486c9149f4e74b9c GIT binary patch literal 16548 zcmeHucT|(zmo7H2A(l@N0a29R6r?v5kY1#BM4I&8tI{k8N(&(r>Agd!Au0kw2oVBA z2t`T=AwXy$5D47YZ|1H$v*!NRo%yX<>;5zUgvGnw^PY3|*=L`x)4&)+;x zM@Pq~p{`;;M|aYUj_$w&^%rq;B|%9@Dk&t-L5YJ`!~CkU{6`LQWKHBPo2 z;}v}3(=>c}W$vsUgye>#3{JICru;~Qhf8S?x;`KF6LfUZ60dem&H&w?WCl9F^ZCpL zI=U}nfBuPfJS_w~N1O$QNSAS?4)_(i`y6Ou_Vmk1I=VWcUu&;VoHzk(VWgweFKDGP zNPms12c{G~(wo=I(|#6s^WVLn7!7puob|+ix`Y3|2Cs|XFy1=M58uZb5T*}afE6Pg zp0Sh;E>CRdZK3tp7OPu4K@6O$cY45hcAvY%qqHKLP-QsB?C;-?-X>P@r3;#-WeD3B zd0;O%{{ABA`}K_RyQ*xu-^m8p*JI^q#MR5*$>Do*C;7DoRw4`ibL5gtKi8cWGn zmg|QfE9l({{;h?ska+nsLCl_B`8?fUw#Cr*mmd*)XCtg}n|-wsa8!1iF0;@tgHhF! z(J8ETA|0#d;yeyht+o!7^=@AhEKg&)Ju3bD^>_{PO7f5DmLp3^_g@f#jBR-RcX>QM zuO$mkYQu|fx8TQ{2(#gyTC-JNa9RY3diSoHdcq~qeykTNBP~t5EyDwmahg?j$FSV? z*k^kxo-uDnBxq-uUp$(UVL6lTggoL&7f^WVDQqFn9kq`*J<&o^&n4G+&zXo{Zhain z$Ng~UM7ago&IP>`!YUWousR)f&-S1?22PHgR=Uoackk<03i!aJ`W>=fh*z&fUFEy7 zQyafwWTOE{Ragm!3_g_dnh}jUTu{4uZJT8~+#2=fiY;QUo40e?da|dOnE3AI9Xq>j zB+0O}hJV!edav8kXt_}cE}eA*x^|&NzI`T?d?Ya44&(Cb+D~PbGS0jewtDm2h05oV zD3^I#dbN}i@@<0!ZvnadPUR}gWxgKiQFRAcHq!@$M@n?J1)uvxW3%I<95LWFxkr9 zrUL;(@&GG@*_RvyB!X{UgDFs^K8S_-=Y@}5h@LMsYk3~B%;rR1ncJEU>UKconsPtb zJYGB?aemB_xQk?X6MrN5YYg`7LilYCImw4nQ5#<`1I`HbS@@+*jGT)rccM|N_q?Kr zf<%mxAg}JB*8GC)CzK+a>+8N0ZY=T_lkLO9@869w-d;=4G8ln4v0+=_%Mn(k;8{OC zXRyq+-2`;)j7-1^N2BL73#0M5bA1j!J|>6P9F&*xgmjO6e(d2$7~$2K@IMBLa@r4A zb_6c%b>gs;M}^tzOE&GvVr0K^1ELdw*97dk7#N+KY!XewE?b0 zwQ@pcNjDxvM~7wS$T2wyTB`r3)21BQ&vxC_O&5rgB${`HJD2p$&tF9blIw>Hqr73d zQU}#F?JE+EQos#&3XG`zukA^i=rP|jncYKvg# zL2Y{lxaGp5zRUY=BSpp~g1_Ghu4M7gcQw7dV&9eH3LDCU16?b}H-SRQ1>8!mqi^m{ zb7*!@8!~h#T9&7GuzUOZ`A_PmBE#6c+*D1D_fcGiOlyY7pD~CLsxz*7%Go zVjVkX(Kr9PapQKfyl()s4`8pKYU?T?G z12^2jH2gSG;#8+`=%cZ`6oMCyRX_JrZM~s6A^p43(Vnw8)IEXKZ?3ZH+&MLI8V&2^ zv*e5)`k=ibW7`DL@6ho7QWI$FDwUdG6Z4}{+in!M-+3vcdP~w{FHZs9?4p31;13s&{I+doSQTs1{es--_%GDIjtZ>C74Q$HDN+QHJ zW*X(AQ>s;(o?p+{*tiVLwWo`^a*IgBlw)dPlIX+8W16)NA~{lgOv-=;&B7i}YTOcZBBQBhZCffp_|;&HXAZ* z?r725?6%<8^Uk_tOsDh{epDe?ab*NLZ!uFK1X9B58X2kjfs^3Jv``?#rwLi?#I{k} zZMq(Hw4O&O9X(Li%~qF$GqL}I{^CR>PA5&S<)Pg|2qSAVet83lu9w;fItBZp=hQb#)$YJtPIoqfURC z50_83iGSRU2Lc*$ueYj+ZF|C*H>}&ZJAUl_=QKKZ!R8Z{J_|RO@EBWB@oJT>uCbNO ztEf0yZX^}HbPREM|0(_)t)!guFIlX5tsc4ohmHj02N0=CrI&6*t+(+zjc3G*aO_kN zS7(oN*z+nK!*+&kmMG47w7j;~xhv{G?!Z2X3zpZSc@+$*2ENYx~~k$ z$z?IU%O0@I3~;gZ5%zkPAz15&*NdX6thW=D08j7za4^^--W=dZUNLK1NJ`dW4WNUS zN|fIo0sM)7+hVvfFKA__q0PG@{tDx5+i;(tx6-+}p<~2fpG+ko)RRMtW&+ z|153j{UwKS7aXWh}Bj(GLS&f?>0 z1LZ|1b}}=nr7&iRJaSI-2V)J@@psg*rAM!MPcKiFk1}=kw5yd3eh8m*KWQH&?Q|s|X z_FuhLJo!Z&dc_Ao=~Ac->KGcbZsZw?eGGGtAD?)0#nka;6VvC#3wyMIq7cy=mp|5O z-2qCsH2Ncld;>Xpp!)h;cKJUPbN_v{);}s2|3eRtTrQevfke_zoT69P)=o4k(|^1& zUM*;98_^t2BV$ssvRpGl*4yw!2F04-Ts*O>6A0~v28~WSZjLRmu7XMWA;Lss>CPHf z3MJ4VdW6G=3$5C_hr!NFsC~)9m}-#ojt|vtN_h(55Fk)lSs7O)reJSs!x_S)xcB+= z>C@^iCW|MCYb~=GmpEkA$tz>HKF+ALLbc?9Z*dojEn7Ur{Fn8CeC^#|4EDW=GPR-^ z1(TqWraZnJX*IhAdA`K$m&ZpmU&?e4qRy^+aj`!&B`wV%fW~M3?%{J`n@+Q%L%b4V zdAJZ)-7=E`q(>o^bvs>cet1nF!>p^GZ%h(04i(={Q(wt);{&v+uQpY z3xlJ~bJ_yu@%JdxY9&|xIR`1sxqtR!uW>o zPB@pym}2mVAD`~x_VIb3yJ|_9JUXzkazkFU7z5L-G>B(=f0`O+*fvtCv04o=UTdpV zZq?Qh8y}yQ(Xqdce{UJKyZXtk7H0D8+qWVcSm+j&SnC`bzVZ9(NP7U$9ff8&_#ict ziSJr$&Qqf8r24OnY6zHA@qPOA$+w$P>PPPfwg|~9cw^4``g$?%-;c_TD>Gsf60{(a z9u9|ln<3+06F4K6g)Q~EH#r#hRzeWn~nUew(Or_xVX5oyCM$SEdp16)$5v>nblCL&Fa6E zo7ETip>?EWh?v@-q+1FrgIN-YHeyXjCnE8t#6bD8!gvY^WE(kg?$V_|#lwwms}`lu zotL~~Vq#cYdlwL0#AkQaoL4|#pbE9n%jV_(L|HipSeh_W z7zsuhY~KlhKwjq-b}bqMtA`Ko98y3``yzywIyE&kHr=oxYn9dQe9J_^THnQ8#SBcC ziYH3+$AfHf=cJH`TC27ZQov-5X$w9TE89rhmkriOm{Mqs)2< zKPN8_ce{+1Ju6{(W|DjDwy-dgRmQg{BsK4$i$N|HCH$NasSP+1n#OqZq4zqBVndpR zg_3=!N%zU7es{l#2FDu2?{W|jUK}i(wwI74?$DDN5Z_iA(QQj-PZUQB z-g!L2Kg?#zyPHwB#dEszRi!U(VCZhL{EL}zpsaCP=e++s8Vw}$@|5z=_?IT0E8`xt z>|{)cE~0HB(B{CkQRMs<)z|Qn)Cs{-%A2WN+mH5{hF^Reb+P1?-Edz^XQ+~`S`xd9 z%`x`}CG)Z$3~ksf)sNZP?pX%S`(m|Sq()~{R*7@Y+5^8OZS)8I%acVFjkm-{maxFq z$eJ}YFmM>kmT`(#8*d0$o0KrbXn2x~ke#e#0=nx! zTz%O)uy4qT8%R&((aHMpUbO3F)*+4cY(<}WUeIO_3&itOHH$T9vMZv4D7H*==uhFE zSZR7z_;krh%LeBiX2V=Gv?Uv~6&6YLv+*a*OeeOD;;R7XPCKseyCrTf!Xjk$JYyK= z$9DDkHan89wmra73BMgsgRu5PN<@Ml)J8NLvz&>D$VsN0wRLCjKCEi2TNzv1PczNA z-ivn&4i2uduhNR)x?m=&_(V|3jngZVa6zAer5(&wacbe10zU?Kxxucdb-i^~AnYs- zA4Q~RX1)}W+8hw94L)d7XsNgF8<^`(upR^L+B+ACiyDWAhu5HVn5NHEVw;*Cl6J)^ zfZfk7>;I#cs(+1F_n{%UInNg4HQNPCWRns>H7xI?Vr;^YqmEWq+5Dg*qg<`fR{ogr zwl0^wNnTqD4g_fj?6B1Gf9&wmdxUD!^zzgG?Y5l}Eu@&NjpngUi)Rk_Rq^B1@a)2) z(P}JJCa>0yqp)ypv^&oOA8>1;>;hYD?YGF}jh@~IgDBYxl{VA4dLd)xcHv!5k8)3F zHgb7<9g(uuS)#5re)ey#j{HdYg6dp;HTJ5Birj}{J;ix-?V#TXyG`5+VCUQicDzI# zy`|1&hK5K)Ki{1@Uk^u#3H%D?5y*eR$7wAeBv4_=)@U3FGvUpPtA-R9-Z$FnYkLFSBO&tm)@c zz`vV*ll#QT#l#$iFlbwdMa51jUXgbD=XlAsPm)u*<#c;>-Zoj5(zM~Qh=Oj*CcBTu zeZZL7!PWL&EZkUf&WSUw^+c6+u2kJuv{n0XlUpuu_2WpD`l*b-E6V}3Hj(>|dC6|6 z@UG4Fd|qpih&6yV!Go@6`kO}#4`n{+wpx5x>m?~G$WMH`0@R>y!2MD#BlpnZS)5cZ zh+;%&$-&OOaaKt(5hE2P_44PJPCngi={ajB;&}B$R*xT+#WvYxKY(6uTuPEAN!~^& zL3maVN7e}1X#ld=%dQMtcJ~bUjLGQVLs}EgxX_BdZp&&(xaF*9OI9})%rABBlnza0 z%k3*D(PTuMy2vE+Mklb;OyUVG=SQ@SZliT27!7ja?cK(UYM`bQaW2C!FQmn z{CO8ajB{)zK21jdIsK9%Jgrr7eRz+LC~(+9@Jt9kC%omS0&NFfqS$u)8M(?^{XR1@lN|#>Czdg?NoF2Y8domG#>Pqm=9EAsCnuwtBjvGgK75c6 z*YS6XA0Mu)?aK0)ICqUl8hG7bz^LrE|N7L#M5~WI)I3&ZP0J=3nJt4Jv>9$lNJxNa zxmQ`X3^|)`S0wkR@W=0J*HL&DE<{2iyH8;ks~6N%zcaEG*6gR3M)` zFs7G?JPa?_ThaA>_C`$fcq}PABfsom|S0T6g}dSgV^o;a5?Kim`y`!BxG1 z&VVq-`34SOu6^xK8EeCABI%W))%KA}kfk4+=<){fxy(UJxM@l4l*hWDVDC&|@ps zvQQ~o)s_}G&TV@<-g`?j86M{4yzipZscqId-GSsiuoWpa7#bj^Mu|tkAGTJrD`vRw z66Y^esC(;251L&UR1%uz@2+h#6M@%NWJ~7=EJV)m9Mc?2iL`O@x|S?AF6I>O-Gxd` zzzqhiTlz+QvKBHkIwe0;KpeeX(O(Wb52izw%udkhn_S062Q0Z~W$P*kg2t0G+T0d! z+Lx?7y>!j(Ju7_8&ajSD*ULw^xwReB_Ded}PcX6o=HK#Q7!G4)u$+V2-m~r7=^KpKj(&dJLX4j#j8>IMJ)fR{z4eE+X2@?N zecvkhe~T^0(?dUX%z}9jQgpK}|0*@MzLsG^lD`*FAlNSf8r!57YH4q&60X&?5U$m> z5EdpHe{%NfFsN}gSDOLX>)PbDYFgMD={%hcZ>yU0(GTR5Uk`;h3*Gde&1yGUF{yTV zf`LY;=|rPL6RXT}|7=;!Z?}U!7<%bg-?h%dq||Z}`1P1x3bzEds;{Ud;P$e=U+do* z+6gcyb3_!|BYt$(!l6xq<{tmDR9+kmozSft|hHJ(Y4(2`iB@3k`wO4E3HI!cjqjKh32EVsX(&<}VCq@@1}w*U zCzfxu>juoi@emVkwx)EQj-x>;?33sPTGZ3|U`}$-P=4uDNWTj#u6`E4r80xAPzzqU zTCdvi+lu^XQdd=rRVV+@2iG|y++&w<>~*m=5`Xgg6e^C%quip^u6F0;mAlpujAYDk z6gHryS-p>oJ5NT?)Fw?)zvFY0n?3{nxYH+ZKg?_XoQ49#saMLeD2fPGz>Z*oR!Si z0df3l%dc#3b^Ca!4ugk_73F}*(k>IaUSA-s?xlpuQT^h_F8cVV2DKL4;8y?Ai6e?u zU}hgyqUKIME~eP0TAkI$`T~jCe4u}-Adq*Z`Ku>s^ylgR*LC%?w+@oCQ{KC(WHy|l zi+(YsT2`%?H#BjNR#PR)w9{qwO)x_objPg05gxvOP<*qgXQk~NJgd*kB`BX=G1t8& z`TDi#d+2r(lB~N$&5B9oJ<;%EH>d~U^LEj-ez7{kyM7a|l>1V2F!P`bVzyVgDz#Qr z5s7f}1CZcPEtOSSZ|yGYCZxZuQ#QHmApe6aYHHl#oAQV#`uY$L|`Dek&yYgV+thAv33Hc_Q`}Va9WygeyuAI0mL(ro4gmUWB&w>=#+QS z|5u$!^vKmZe`fFykMhK~i%;ktnd>hKo5fZ&#Mm>^@d>_Ll{GW3eH%BD>}Gp{PE#zZ zLBIa*bfSlYI@8fc<&_;=eN?zj#bS}z8RZiP3hUP8 zU|tvds;%G~AubcV+LU(TeA*I=%AZ3{2hxxpADa5Eo;Py?3;sQEm%MnVPN4|8wBN-4%?M z$-t^Ox-Q(nzV+hCyk#JM`154e!H1oWbjgk9<} zEe>J{&DK`5Z>Jg|2l5R>r1d3_XDrgwp~1#{EPQtlB==$NCrYB*Io1SD15R?~gjUS^ zK+@Qo5tFj->>%{VZM>q6BeLdR$Wcc?ia%FRVr3|0MiPkT4@z+IH)a}*bQf@^B5i+z zH{Bfc3ZjHv0ry$uhg!!@rniKerT{^%+kY5$ufiY%Z6M)Cwvq7{G9N0Damuh;@=ou7 zy)C^**3NoZ!;wo|5MFtTbF&5V)(HdmQyDi~yh~lIZw{BfeOUHP<8{nQ?uHjK*ZlpR zJ`d5$hl-8)Qv%#Akp2rjDyC_29oEa3K!5GAs@dKC4<0K&iphdn>TS2nZH2YzgIs<` zgi|YEPM^adPlvBsD5@893udw=L|F9vH8KN4#gvL)fmbTJ`wSgcW1C0t?Y^GQ6 zZ9rq{Yw8wqetM?w_&_V0^-=As6+HzJG3+oiv>&l>!S3&MWBx6@UNg#i`SNWMmVjc) zwOP&S_JCrcNmy9RVDKiBs`W*nL}B<7WY)RF5kM^@^tX=Br!Vd);Iji;H~w9`_-CYd(SKr% ze&_J(|7%qJXG^PM?&kntqE}&5&eh-FugIwsxSAOo7uOGf@-9b_|H}H*U`V7b$DYDu zPeA+=)znaf&&3L)3+RAGikwl5BQ2zo0+FZSjVZrPgYps4KSt%VK0!>9TmSg|y9!u7 zqyg2(tDSk9S1e1>xfOfB%fU=lF>LJoK>9XX%Z(q zW%881z;*L40su%q6^Qi^^O$@n6TI=fq%V*(BMiX4sY*wi$yYC5{@sWWm6SB0zr@bh z>^hdRvEl84F8PqisWfvCJdOikT7Hz$aVNWs@AoQ0uYi@&{^rDgz_N%7ivu0F08RzP zyWR(+32d+^rGI6|CR)7m2)D+>l=gndD7JKjw`KUe6S4~a6*g<>wf2zWBKWB9sE?8!kr|r{l)Y6hV9+r6<3ke}-^E1!)R~pheSS1`DnnSQ?^_;~ zoOyq)?xEW*BsTttYi+VRHC<3|v2Lw!q#PYuZnnwtHqlB%YO2m$(o;U&qfNq1SM&G? zQtUc*{pyu^t5EVJpP)&$CI0tUY*7HQc_J56jiqnf>_xKfI<|~f?N_eO=DD^DHnXkG zzmG~ib0G=bSE{X%!Yn+3m@e$2f19k=Z;}*AlhncuAdq4lvQ?@3+vq$<02>2S?k9-n zq&xIk>5yi~HZo$y@oCZ&H%f_96cA~qM@JVgl^}w{_v~I%jy``bHmIG^WEFFOeD5u3 zwMeKHc_J0CRkvz_)1vEP?|7k&iSw>uqz3ZTpzp4!(w@hDW|Af09j7sq1FbX0kH zIr#M_>im1r?>_9?@foR>UNB<-q;Z)07Po{4s1#lRX-}aYP!6n)0DA7@aG@H6$=SMP zW`2I&m-dJRKut}qW13K}SrI{#s*i@Hx*B~+96EG^Ow&usObQM&i%gt~LICFkMd4~% zxt9l%M~2V*SGT^B;$DDl_4xwTc33*kd6(yT=VLRC(VfllU=KhB`G} zq2*O(KKHa_#`~9t8MzgY1khK>zwFr-W0=H97(hfSaPAy`gSQ=nhWau;35I#XqcbUb zKyl`jqV%#MkyNg`F+D!iOiQaN|FOHT!B>eJ$ty*|F{kV59Yp=a88*pAKUjwmCqa--?CYCff>1e%uul0$6a|)cFtEYLe zOS~r}q;FF7(q+I0De=sgJjNy#r+|`tNT;x+F8nqb{^V(58GqE#wupJBGz>@iAhiO_ z{Q;oJy_bX@pPHW+p(q}&cT|Rv^mIM%8kS0?!@|(Yb71kL%7Devn&~tJO1qAd-VW`y zqT)n4GoDRQQIsmVc)diT<@`fOi&9y9mep#^g1b&y^&qtp)rM*X8Ox0Wb zgU8!0idn!yo1465XHLs1Lu|&2E^Xw#5`JvFRXy z-zt64^KNKkt{X-HD6$_eV-?0we9+qD7?6kOQAhxF1D4H}_9>`boAvG%1$eCYND?59 zl}Q&wdEW|gfL&vk?~w@<8k=$ic!gA*q`CL;O?@_+c^?8#;~Ibe&otzvn$ryLPO>1}{2Lbp^m zM@+cI11rgU|Nc(`x-^;61rYKwNPuOAMn**TQqIx!K>jd#qUg`nMgReqiiDs$)OUi` zsQuOC3ad8Xks^(2Apqf~Lo_uoNEs8$QH+>OyX$rhp2UhaNd;sGOKz~q>Cdwk)5|jo z7R;i$Y3Ne4!&pTjpkfe6RtU^DD!a^gf%#sRaxyqhB zQ@t|W?A9b?m6@5(&V{kD6h@0hP>rOfKA4Y)$S#IlO8Ktn;PC2!o2{_ot=}nyX z6`C5a?NZ4Qk`ck!uz?9jPd_{vT$_TT9VQwUMq0d<-d-6V{yI``-#VBRHhP$oo$b{A zn@Xuo@i^YnvgGLqO5QlKZg#CRD!=M(5i|f&YWdLnLHVArPRCd(H+Rn`8NZ~gEvH)i zS@YHnGpL7EM8J~l%W)y2!1W!4Mk&E*5-=M3cG2!`5M&+BqhkW;gL)~gVUF6X-Pjy4 z+bSIl;wyl$36fK7;xGw)%jz8(4hX!G(?}|N1WIGJWo@an3nOCP{tzn@G#AIBxzPIo z76sTPGeblnAtok9To~KzW{N(~^m&JWE4tV|^W>>hu$XDGbKLps+NoilJi6I0XTCpt zLrAEWgeS8}4QYZO))f~Q1A6x+5{3PO`gCD98`D6bg_GzY0{} zia8J60rYQmYyDJX;^N;V3K*Xk!y-fT zMrx4Xzl*~$+cumf-1NK+uWrRc=h~oinmkk6omi=<8j_#G=ff z8CRA>Z9bE=w8$$YZY2`8rh1b&+{RD+ZSU*XuG!PauCxl$N=k(dWTVZMg!~F;dB|6p zs7uACZ~+P}vymOzV6oyajw4z!FP8!R@FQrSe{2?nH@pq^Gby(rDn>t>21d;YFvt0%y+A>mQGmKFKCLitRppnho>Xs*&*ni9j zfIfP!ca@dhK6mji8SmeTDBOE=*?$To750C0=}s$nUB^W z8NTNYyhPN#jlQ22uv%=>r*{fg>nFbOog<>_QW~b-JlDsX)$8UAU7DimtvdObX&}yX z|6%7s><6mD?hED^j{~kDo~kyx+SISu;@H?#PX$tu7f#$xjBZq&ZG2voB$9dyI4ACV zb@J+FvpwnzAewkdJ&7bRsAFo39mcC-8lHc(yH=G(U!5+PJiw&1nhNzOEPBzi!<_i> z<0Hbj@Z`lON@0?e@lzi6!HNpo39pA#s*EdNkJj2gz;3mFuz^+dOU+u@z3v|=?&@#| zkXb7k7|3h5zG${*2n5Xd{v86Cfy-;c*8=LPkW)V8F=>>-ZzvXI=c@G+$nC_ZJvE6% zS|D%ny(#flEQp>_Ad}8_{(Bpf(e%vj+IsuqR*DU3PRzxiV3wSn`tJIvh+BpV3A(x3 z;s?3I7+X$XSW@EDcr)Wo*AhKc_$w=p?=|8nfC|X#7RGJE(Swl&h^EA8)%lekSg4ra z(x5hl^sByGZt+gU?32N&7df$hyHy?IkLkE=zmQ^}TQ>J^e{nXAXOz9GU8+kQ!#1~B zLkNvY#y=4vodDNg(yV^!gYH+(2QOl+BKL76ZN~^vcpAg4;4g#H#2Hvv2~Ht$Z;pX5 z3x^NpDqzF!yCKwd0j(K;mM*&&0t%q1(`9zu@!-uzl_u3lP zs>91y%r3z$#s)fuwzD^->x$m}$hIaaSlJ{Y2>+9Z39N}J6H2QC%?Tj|-Iv?_30zD{ zhjmRahrd~knxRSoW>{;zgQVCvg_N2VLV!f2^3|0wfQZkr_Zguf52z(rCqM6WRo1j? z0%}!~xM> zDWIZpvV8F1L0g8)(jZtj;Sxvo$B&nJ<>jXc*p}KxPwOSHv+K9mbGqH}j5+783rGWM zmq1C+sbxQAyAL-d4l7d0#AGmQ6sz<3P^TL!G`}VG!QE^|wj}UG+*k$D|H+dQSslob zggC>(f*#E#iLCqKW@GlRUxrRjwl9|-0*5zej;ZZ*hKF@3FDWRLVzZ*6hVtj)Dv?{8 z6!r)1-gqT7o(+I5{LHUJCaMYBY7ZtyrUQ8|ml9bn^6)Xhu^Q{fokwJkS7W3RAkLtg zcGrj!^H>5_S4{kp@{_NTx2 zYu>05`Yo-^1wH1Jv$@$H;;Ed6fv#DQW}Q*8LqGGe*#qey$lCYshbs|6ckc8Eab73C zH!L3Tb^cYyp=)K;V7|VMVp?oVs%kttJ>HCMIGC5wdtq;n9PIk}Qy(yl?9d;s+NCWv z_O$bt=o5fsJk79J^E%Mn4rrboGP`%UjS?4@^PH0YLp3jvYsCwqXk=5rnwjuzBsc?G zXBT7c4AjqqAytjV&GMU@n=_6jI*=tlHoA9zLn<%7o{Zl9nwK$jIKn)BL8SFdlPesM zGsHnT=KFv!FqYZQ5~CM7;<2WaNRUjDo--my{BvHs_N9^Vt-_TvxU zoI7O;G7=7iV%DA6+2v-ee>Pr~#aab+((%UM1hdG`KWwvp?D8KNr|#bZzyGN_lz%sP z^oPUcn*oOXPk-Y7&$Y^b09yT%X84~u{68+-{&U{-KW6lYpyw;`8>~h1C$k|2yn@95eNiEtwtDA1nz4mIc47_X1NHrLnO; zmpA2vO4Z+UW07xZsA*w%`YA_>_=(t8;dd)4DwXL8@lYHWo7~dUQh&B1XJuuTEb4W3cBbd?w^XmzJz)9o-}0F{*KOMK zee+~5zE{5=b(DP!f8~2|Z74@Aj9NrkL_{BX?yRJwME4}S>kY!~O}#o|u-JaCb!ccP zFfg#SwN)yN`bG8YzkR6^yGy-j_&C4U&TAuVW>Tq)t4G^&k3D{NcN2KazkbbUs?v2` z#>3;(_X=U%`N0I8+~>2cCc?1y?y(( ztgI|i$R(6oL|fnnv*n#fdIoALxJx~WI(43I9v;>8^++QlT1l06@7~GC$Pi0*$8gQ% zmru*e$(^=|p~Pvd?d;IaHO7D6XFPX1IqH}9-H}Li`HXk9 zd2dp-GV)|4tJdQ%Neo#Bf2>9eL`6kQ%zLSMEv_&9=#1ttuk+rhTr%rPsI-~j3rw?} zsZ*#wnhO(S*Z%$?Et^R#^Q+dkixFvQX|(Pe#;w5uj*DGW)h-;m6`V%RR~LOuJ3nLY zKP`KG-I|Py?0}`RvXVvPYlYJak=Oj^Hf7@dekq#%&9Z0?ebj1R;j#>@TX$#YS_uEN z>%q8r%K~Y=mR;>WDKpWWm8fQ+-c-4rzMGqyjg1Wi5zH?H(E|nshPtb^jMk${ zQ&N03=KIs7UQ{{#$xnk{fxDZob}7`a_nvDFsdn8?)5yzUv_zxMl|+xfY(HaKoN$@( zI-GBhfT?_GV?*`8awwcmuF7Ro@sNp~0#>Fqx_+zk2@$@5C1&xar>E^|wp*k1y`QV9 zs%mL1udP*koxToCdw-2e5Q*~i_0`eV-k7S^R9AO8SRWVhIB;4U2{~7DXGvqs{q}7e zg0wA+rm(OuOTY5fpT^v0c_`Fjz0kvlMe)|tHL=GhYlZc7um5UWTISY@NV#Fc(`w}F z%a<=x>%F#q{|LCrfC=30cz!|}(FB(``TOUmhgp8Fwr5-7`R!DR7uMEJ2jn;s1sva_ zCf?QNa9%y%&G_=gWU9(}T_#b`d9`}8PN&|xw!Xf;d2*HvuXnt}95b(zlk!Lph+>ZV zvtoE%Sa-rKvwLvxx=ShKQfF`POojcu^TFJPMJ^K)69u33(fQ#_wbM$mS$AAdqL3PKDco`)&2BFe#Umu- z0lRC@&3R{D#ursvQ^U=}Gy3(p1X4#|zaD-qA%XIw`E-9&AA-rCuX%9LamIKV;eE2g zaMFA65+oHqn_oi%1Kb7;K6`t6#%-Y)nVD4;6~gX&jC6GGTxN%dhanSwVeA+mck75` zdK=!_%;SCLDkfIX{-R2m7<1W0f@l{t*mwZ}0jc$RW!AS7yC8DP%gP2KR)=y1;~rQ! zI6A_Z26NQ1K7alUlMeqE_>;~1oAReymnTO?%IxQ!y1)JO=@VS3tE($GB!tfMp7+t# zAIM}+ks_{J27<;h4ca<7gM))Bb2A>BwFwCcu=nC=&3=Bod;9k7WUqtURC=(VBLtrp z-?fP4{DH{CMyaY?XO6PmUmdoqJ6>$^CtNwqgf~qzHTj}e3hPlPEoAx@ym4`H(7aiLO6u>Fy3m%f@84 zJdJz^$FE;md3lF^7qMs-Ojf&urDap_+dh$${DyQ|8OT^%G;>GK&0_-nn^DVVJ=)gJ zuHJS^(EZ^_+}Nl-GNe~tP7crOc~egh`<*)}a?$MG-riYC$(Y-R9YuEQmbA?8tYq&h zBwAEb<=)fatJkh&GBQ7W_>hHV?d*8>!Gi~4=X-;Wy&?>}-nN z*y4K}j5A2ImyXT^Y>Sm=pXn&xe#W`^&#gNWa^ih{5<(+ZS5)*dEgSae;1ZvtN_TfR zR4(HUi6X<~3|_C(gAB$%RCi=rwy4)hUwvhsI5DfH+gIR5oj0ea1_yVQkS7t*(PVZcTr53ZiKw_&YHq7J zS<4(TdC0vPtzvUulUU^+2r~p9iK#ti<=aC0^+!iXH-D7|)m>J3{#+*cGo&a;87ouO zhwTvz=+k<1e96s4g3w(ZMep-{%@akT^E|F|p^9xoCE@MM=PecXb0dZNN)L0Om`1T^ z^6~Kr3JD>P{`l`vBvkZzVe&VME$YwdX1$$-MM|LqC;uA=u=)AqDYXXOLc9 z5Ro$VvvYICo?{R&9)}wWDnqe6=EWr?Nc8#P`}gl56#xDzwOSj=PY{WYifUD*yi7bGMrxyS|B(loWm)b-sz@KN<|I zq)>VGtVP!ZxfpA{ER!gPL_vNYS#bhL^1$o(o6F|3g#WdS%a4n-5f-6`Lq622 zbTEY)Us_t~?CcB$j~4ajnv4NJj?D6JFvk%LN^k))GczJoEUmk;`1nVcSj5c>$Q&(% zuiSL67~0dpfm>1~p3mk4vY(LiDlrAc>3T`;K>aKo9o^sQ+S76KWM@Z5Om4(1)7I9j zafvF|?b&419>d}UY&rLVFEXwEIqJC!ozeAZ)F~SqTUVCYOXblNZf0bqWhaJlZLG$cC#mG9 z=jZ49(?}qP#@gFo9`7u`EL+*wOcfdW{xFe2_3-$ZKyoY!`&Lq-18DG3gFxt^)vc(Lg{snGT5+Ukf1 zHD%>KhGfsTRYKga*T*6wBY*$?JzeVo5q7vSHPP8=1*H-qBt!nvZqPB5{qe%QVM|R- z%>%5@D(O$zbjqw7d@cZbxGn6uRpYh`*+#4=e(&#uOG_ZhC2V}oqwjlT-;B%cW<+6c zCy99Es^`MLp^!Q!%4}delH7O|&Hlob438w>0%Sy}+#GdBvk^+7FYWAR>XB$n`sJwI z3;&QUeGv~2FDN9$8Sv$+SFW(l$9i3Uw7pES{#h;W6L2)@@j2%No0w2o568f9p(6?+ zt@dO!4+3dqZ687**>gw6(b2ItNraF}@bV!8g6Q`S83KXM%+LQjW;}L#1!`F1J3K?F z3s>JLU?F^vyAVYM&r24U`_rF2dnTAz0F@qj)I^i15PxvC+ba=@c>Iq~fZpI$IyLkN z9|^|PF|EV654`{XpD#U_LJ|`b7lJOs{V87#2?_Z;uFe=JtE3b?b_srzLJ8RR3vYS3 zE9Mt`9)Co+T%1&c<_=+UiH`)nJmNF>|JqAa`;$E^B)Ww`DXAp(&DR^!*AO3ClZvs) z$;q?ipN~&YhJV}|CDW&*`+hvCADyqgFP7|CY%%$eBj8hl(BbY|toRta#Dps0bEo>1 z!Attyp9z_c}K1|2uIAso!SY zRSzj*(jLA-EO~yQ&3*m@BRLPrsn)Dv)JP1K`oWO5~doC*>G1vGmesl6T1a**%9@5a9v3dE@`BlSt zKt#Nbs`A#Qb7$xMslyrcdOL&1(UGu3eWOrz@OXn1?bWCb_Pc)yOU#i)X5CrUF7<1_ z`;MdaZSBS#4Ca5P%Fp|$2|3JniJ@~+>qU{MC70>(GH0tjxEc~&yRuCu>t(sxf8o;P zTVJoS{A)C#`}?D&^1W80-npaYucj%iUcU5NO8l$+*Ysopg50i-NzCJ*ZK~S%%C+@- zMjHr&CPpu{{w z+iRo9{XNR>Mw0Wtf1Ky$+9G$_QLt5d#d^a+>1%b>bS6GVs5u<0FTjTKaNVnX{nu9d zgL`W*j*{3^@3h{t=WY*}Ue3VI1?t4W+`O#~sYzAsxK!Ss?q9##6O1~40Rf4Ecqn{v z>O96t*u^L*8Mn}B{?0F3$YsNGxv%~E_peRm?bGNj&1HU$u@UF&|(5mg-?!IPez1wgJ^j?5p8+C&4&x))16XlTD(8O5Sch`R$V z^Hk08Bb85P{b}7OR1(~=v-4lKlf7fik~O@s!{>{#s}ndpm>A5Izin)8uBp7$RAv*a zQ%>!^Em^vajr`rful;v{OwSe7&96@_T#>AJxV*jXucj7upN_4eiwYrlM;0x;9dL7j zfFR`0ALA7}iN`~sq1T>2pOM`>j<@TeqN3un)m>f@=xc8`Jlc8#p$6amiMKlvGp%;9 z`&5rMf^2ALXwSst4=@0DkM-r{t@RQmn=#JbHgk(GYIb(^&~zz6{bCb$wTQan;y7C^ zUC%lXmuNyUPsg7hQHqM!rl%!DkM0DzRE+B3q2_3O-S=0g^Wz66D^oW%3LvI~j+r)7 zxXmun1YD(8q!jRS|NX<40XF_xuh@)siP`I9QKhSizt#$s-1mp=Mcga)R)^33mS?FD zFU-yo(}*4BWKBZHtLJGS8P>})JnTa7KJ>$_(EU}8JgRu(xKU;q;!3-q1rcGECD&P|AvoKYTjf~Kno*)c77|%JP*4OPk4%R!) z0{^bB-=bW1Jgdko*f^vX(fzXLxi)fco=nwO?c%Z+1?Wj^w@ZzLL=z?n_M^?O@1+7BVG;$1Pg%RRY z8g8kD=eW6vbbBgn%0Sz@K6#8D#Y~(1JHaG_Wjsm%#25Oxw)6O6N8cG-&$&>`h?ww zCQDh+UN;tNRT;DSJ2>iv>%2}4=SJh>b+mkOse67)m?jFIw|UB7)4jP*aU|Wm4LNsn z`g~L&6B63+$VjX1DQz;j5VJcec61StD);FWFBgLPyBy;acoHRr#+{0RNs!BNyZe- zZ$JAJI(xU3fqM3@gEaLra-9VQb%R4+fVhdSs7S=tQ7O0W*{go47Lnv;-*|a5ll~mC z3F;78_~5|`@@(5(cal9(vDdR%gOWKavBZ%~O3EMZLeT4^F;f9;fac}w%d0~j4(AOP z>eVVpRhxC&GBtg|?-201Bp*qjCwgMB35B+$tv6AK-)Z3f45UyFmg>i%5135Mk%tS> zZB|wk&@xom&5XvzwoFx52U8x?j2>DvHnvXIxY;dqB$Bb8?QEqgW(}gxcJn#v9+nw4 zkvU*pLh$)IpjD6m_{+I$jwmLQH=q&-yYmV|PZ#5NU?cex(kw6Rf6rQK2qMd)y*D~2 zw!h>4YwBFaH;lr0W#Aa{-8IUi_s9O^09#NeYsGkE-myZ5_ZFpH4jIsE^^YangzeLM z0i+y_)_Zr=aJ%g=E?#N|%yZlufrlq2C(HghQumAgZNZ@__*4&%s1@vmggSAC%%9k+ zgZjjh8CmtGXS=>7#dRXyXX}uC(w~Y*sSI^Q*{N%2Z26_1w+G20{4w;+cGa()Pm^Nhtn0q;kdNmR74gD4_o#h1?YX;9=_qk}@OQYf(Yi?HWbdCn?91@x; zMHrnZWDNOoWqp0DG;i>WOkI0>KOr^m$H@ysB8*FW1e}t5Tk4uKaA6ufJtZD=WT_5+ zmY-ie>Oe|k@I+R^;MR*Gta{| zOJfd3pjYy7^HFG-gf*cOaELpXeN_||M%Z?42n|CX^bL+BmctYt2gDj08bA^j+0E#2 z)Xwvp_n?s@cj3LyF2x5{-WK)zEk)DKe_GUlX5e*lvRJnGc@^RGRSAjdJ~XaaGs71j z>xN8>#G_2*r4|qxswgAF5)>8A3p{-IApx63A}B&!H>6-latUz zi+RU+bpvyzlq~u}N9Po@iJGlOJfJBv^YZcrnBOG*obwOQf##j(<-ixW-9-UG!8nV^ zHbU}cKI^d}$skfUcXw6XJ9qE?8OhfXdVMybXp=#gn;>mX_|BFYx_#(s_eQT=qm))u z6b0f#rU}RzbZ@{#;&;Bj5>KDD0(a)IziPUb4tNHbN&+guJv-!SyuW`NhMa|-++|}D z*kDNKfq9n)|IE#mKt~SL?as~)hT3}YpryGv1_l%sgt7tV0(bzR|Nc(i&Pv4IYKWKc zpH`R7&dUo53q$|$rvZ*x4|)sGFviEnfkmpStjyQ?_BJg$JUkre5p_+?>Z+=`oemZd z1h6ND4SniT}3y6e+g99Ko zzRd7mAs`1TWe-#-dtlMvXT(}$KhM%Hwszg{t*t%JR!Ps4k5z>|cJpRLWTey4R(+Df zI7lx`$w+TtXOu+ti6vDO6fo~R1Bv5>uI|N)7cH%<0GY^;UHVf|>%Q;45T%KWhqpOd zDRC$>`pDRL4oGhy2%E@AVs)|X?6Cw> zGOJG6`ba(!&&dUtMgWvB#~^8vuxJpIl7{=e(9(KnKido|dvtUJQi>%IwLr7=basO7 zcl?=6CpR~jG4KSmxsu{y5KKd9#He8r7U#vTSg25{4n8=yl5HNt-7R-* zPL%g{ceC;Fm6h)6qY6O4jE;_mIp(={uW5WdHqo)s9wgb4l9H*ZDd@x7YGM)-^EL8- zS5i|`<2p}CGy2)vt6y$Ab+W%Exy~44Ip4)+9+397?MWz;@ndO4(lf22QkMG<+qO5^ zj`;cC8MnmC8W{WrrEwkptgHywPFC#fI2%|;5#}T7c?m&(ZY-obG$euz`WdywTbl8t7&}Q|AR5-t_mEgFwkYVfb7^ zj??LT-_`dgmno}IOgYvx4==Gt=-iCXljJ$vAAuamZ|IMkBl!~`33Z@ef}+_LN<~3J zvOgQdkvnV&N>JC#Px!o{wf&~|=cloy%u%bAOQ?*F3b$P|N5@?dQm!oigbkX+zOlNx z3S&M#I+Dhz1`!x|PJ`aG)YR16T-mI_+T9+Zy`aBg_AV|A2^D<;MwtNtPn6xqYxYNU zkmqaqEG#TC*FL9t@*zk#?aD$@XEvTOM*)_0bv-&eM!SnPf~XiC7pJYFqN1rOB{YkX zEzHeFCnvk+CwqD}fL2beU!;m2_W~IO$~d}5XcJgye}UgCS;_P({ye#<66pPu%Erb< z$jG2BN1XBbyunP)rJKUc4)V7ihmXMf}0dH^Yrc0i8D z^Jq&Zg5Cxe=TGCiJ(i7)jn}VV4-cbvT#IuG3d%}K)@JI(=m^X*9$q5-WHTpL_G@AS zBWE|_ad@JRmkJ6B;P>CWc>}z84DDT-Xw-uP2u)aZh}{%@6O`qTJ>S4U)C!~@7G`D# z2Zzrb`ikuYhHfcu+oag%3d!$Fi%AIK2&{0PCkVTRA6LwQwxRERY%2RnYkPr15897; zsB@s0!Zh!+GbDHQ^gN|n+1Qw_bXLO1!vxW^qT-KL@QE(jE#oee2&aS_Qh8nbnc@*(7`zEUeq**n>1j@Yb%6V?C}TtEl`nW7d79n=zx z#QyRnSC1PAbWtqm&vbZH<+Zhm8-oaWNbnZI!or}bL;WnX9)BE*=&`1VWMp=gpO4}G zylwI$j83i_|51H;N(`srMX#ILU%ou~CKSkK$4o*(LgTg94+VbD{Tw6@B&b{mG^yC2 zVwwUJ$;ruq^(ZJTR8mx|bJ?UmXBzwY^CtvE5E%y_FRv#!AbMDboS#4O<$pqpdbcyz?+{M*p*XfE04L|n3k)1vRr$u*vm!Y^4_{(gSS;o_zfDNj^>7}K^zZX2I# zzkx&-;CdZd{JSkY!;G?TlWiolTMxCfw|5464^%-Uls7Qx)Slex+J@<}wYJtRGI$dd z)QQJY(~!^rA#7n~1wxOcghUgFapl(Ijk+J77a52{4TBdBkfP0#+{{4x*;@l!NuBeW zhPrxWdiq>*z)i_XtdPJ!_t^jjQINO%{4Q&1j)JS_e7_L6U_%1pI1LRADe1`2Q1da9 z%n&8l;1fw&-dgOgczzBJ4yD4;!I=Z-rFVP9z#0V14bE;Djo9frkNu&6z-}0hmTuYj z*u(?_Jw2ptHy8(ikN#2;qU$G&I&6ztu`h>>j(SU-uy86&;OOThB*WKDQ%zrb+!8~YaYAVI?=9HH1L!GPv3G<6g z1S?f!cX(2g?aFn;MC}hQ9%2d`erNzS%{5_7!h+FbeRS}On@I7 z-|2jG<%gNrIYyz0q4_@vHj?+t?wkCzknZm&p1@tr%Fk!x;@Soy!Ixx__03JfWaKkZ zGokKuo8|N76TYaJm|OiyEa3wqvY$*likG?*@KQfYiaDUIu1mfYm(i_E5H*gZ z-NU$0N-dwMsC<&mr~-8Q^?6CAQnI2)?ZChQGYd<(%|z>7FcAsfei&|Cv4sj3uvY^o zZYNVV z76~XhcBS-R<~EpfzQOg;!R^k4TD|1BxLke$kanT9=>GXL8@xOP1=Cf|R+5tM^a{hE z3zL;KqNz0aaj{gp77%t-m7uR8)Vvb7&)Ym^%T9tlWsoppxQw5ImC4HsEHX$MO3H-z z_#pCM27MqxQi*yVfj8Wn{@1b$x9xX z_FemLS1b?Y;umUagF{2GVdxI-Z7HW2JwHo#y_x$YfJtlldfr|E8x zs%vU$@Mpi^4FBJ1AACnqaKe~peFh13tYmw;U`{YlI0S-PJZ=@5=# z8JD){YyV=`XYFK({KGY*dc=UCfsz21lJt`&m)+)LR(478Wl}q$EIk*lkMPCA2)rQ+ zgO`t$h2<20JI2r`zbN@3Rwn!&t#!Ed$49d~pXFXyn?-sY2Te*__FbQ&rTZRxu-q^4 zorSFM>a)CIfOB8uMt=PGbG(!coO<}SqHI-s^AaD1bfA zSTYD5;h=Z#6=DeV@JFy5zp$%f7$;6nl&M*rAYg++hbU5WQvf~5cWzl*?Y-MQqK zOnBKMm}asO??Y@p)`Is59u&7C!T+PwK2iRsSh8&0+}NNhigpqUk#g@+GHMoz|9>%B zUvEA&!TTkSK*$qVGnd&lVSQplAj~BC<8eE3gLa0v*_EEMeO1lDmv7qXa8l7w4>r;C zBN;}N5kzZxSl=mje*OAs#x(?@p>;E79q>1JE}(flK0XFBD2eLtmBvjd*-KINIRzfGwiEcTbMm`0T6$(l5*))x%eJ0DuSy#k=w8 z0M`=GFkkte~JEJVHVaM#eGVG(bcF zgD-=xu_*XRTY|_4DfqsA`O^F-1a7784Ihc#FiWa-%y~Z3{S;T0+&8zL`H_(Dd{>TV z1z$s3?b$ok+8v`5QZu9#E)HWB$#6@{J2e?Ab8`mpUEkKuRv-m`#GEbjtv8HIpYXA% zl@H1@Upe9yPXv~v8NDaX5;1F&@#X}Ql0+Z%___0Tl6JVLY2=E7whGWc%Cg$_8tV9u zOWV@at{@NwQ~y|r!=ar6rb7AX!N9<86M;o(Mi+0UijaP)0$}^`h^e- zA+KEJ6Z?f^F$U_l7#Ekya*B_G;)KQCs}YY?CU-R%Y3VJbJP;AUS;G6kcEuPkku^vK zYzGJOI0&rY;^G(Dw^3JtO=PsX2cZglTbW=2iD6IIPi|5-hS=CxsKM5@wwd)dwzf&o z^rYve)4W~U+(aI%KksO6cJkR#JCO?X^7I6wQ$G-aP~sgHIx;ge0lsyV{cLZqgqPqj zUhGMfbEpAYL`DYZd>H(Y~5QQk4R(8*O-8Y^2WWV%7K53^Io`Q*fH}0(oKIF631;S3vP6IdHjwctKUS5 z3JQ``0i!46dpuy~S7*4D$SJ7;aA|vc8{6AxZoE70f!sl`{WZ3q67o&FcZI1VUMb=@ zSy|>dOa{xZ8b4B&-(HOe2Sbs?=XE-S(*1jUvjBxX8Xxmc9Ek&JOAlrii10f=Sum*w@ zs6vdxGE>IT@DFrMJUl#eb943J0fUuLOcDkn(+(_nU|Gw_&29916D^UxH~f~Kgor3@ zbPXta1&o(kACd<(b*Rk6CK0dtBQ3@?xz`f1Dd<&zg&UXtcBawfxFscr@=X@1Fq712gv))G45?bf7Z=j4`g` zY+zvUe_`$H4L>gD$+$Z@IK&CNao;Or0xGRPy$zbsM-RQat^-c%=;|^Fd<}gj#?@az z8R)|!c0Z`a19vO%yQx6fkmde6JTh=1?z?*dO4rUm*xpX%j{?>UW1=Qx?fD~5W^zYg zBCq5<%lVGx^XIYPkC)8=_c!FyWHBTdm=iLmQziV#tNm1@0PEvr>}MDr`rf{lev|Ys zgV|Ltd^+5-zQ-cX^KEmEl8qon#K*^@!8*uUC=3GQx{KDq5S&ox(D=!T%YDPpU~l-b zN4%i3B|vipg^;{FD*!iITDLY^aKrCHznU|;7VEB-quwiWnDEb=c}t4tjJX-QBq?6Y zQtiS~{fbwqwY9atJp&^J?0FTYbv`|WMN3b=$AgEtqj@z?g8UwGTPprl8Ts1Q9fPh< zH3deQG-t2;8jUmV4oS$n3fmRnU!(Vg52A`NnWMqMZ8Fe(?!LRXN6Bp>t*4h%UylZ^ zgWr0rL5TPGI_>3&7|QU|F9N&woW4EedN)6D%f;mo#1_laUvO3cV(rAPV=2i)F>iR_ zP#d3jo)l-`$;c}nW2)%-u@w2*x1r4zUhl58<=o694pS!NYTrK6O_guOE?c~C3EOF7 z@)$bhE|#dMC|_&>(AP!rBPq!<3h4{^`2dJ?x3{3G8NjasesZ_NSU-7Y`B_=&v!n@4cgVfu+Z~Dw%hMiLbZak zNsRdxc*DY%6}h}e4yQdfA%PQ19-p4_v`L9z+c`@ri0cZ^sux--HyT8mj8+h-Ge?QP zNqtZH_lM-O$O>8du3(u#Mc+W0dj_ou7w7d%^fb9V9>iMIpKerx8VIB(7`)d;3wyz1 z4J_yPa>(oX#l;Rlaa+%E;Qalt!*lf>9i3!fI8*2qqQ13#$o}pT-+-3T*Pnsz1tY9L zYe;zQ+H{F|*bgpfQ=Y>Kx8U16PJ&cZu-cn5_4Ne>*N|WI3YFyLgFk$LP^|#NJFq-J zXF(x~O%*uh+PHjk{w)^X(-3lJIQRi*Au>7|h&U9mfvtmQKs(2BLOF}!5~eilou~*;ZRs$ zAg2-V^Mt0cyY;xpCC8wr0&G3dXzifR|odzE!$IPnqtfS*z1a{VJ!Ia3qf& zSQ|`61(`P(^-@6r4(1uxp-!yMn0%gd`yO$L8%?uA^#IKe%A;PP7yzADueLzBS*H0H z`+}_|;iaJJjcxcri~DwJuNBkj+kwp-1C!b^L*l0bMqFtc!Q;fn)RK~LYUDi>CslgH zW*kYDK1r549T~QxkuuJ@-L^!KM%Qd-QcXAxkMY2F3ghcyvJI?3#IgL+Llao}TWMZR z+WD`cRRiU_00c2hOGrLM#wK^OaCE%oZ#l5VVyWyca0GuHH4AQJlwy546v!2C)RkWR z@Hy5Sm+rtIsf5)i&0E23DQROfv;L%sjy-`@9)|^e{|Y__B758z(@(U!*e{DJtcNU1 zHS!X9%tJNufGi)f;MLAAgYp9jIVmaWZDYz&M;MMo0bR)**%@`^Yw8p{xQ^P^A^NF= zO)&~2rqod_59BVUu12}E5QLg=UhgihfAGmvL zATqvju)h!9h~I$RC@3iW2`CoV*7Eu-l3ZtT0C3fKXgxXRqB6QD>b7G70Ox%F-=YZP z`3_mu*U``ab;tbivRq+-j)`g7AT-}>mB@CN#PQt@fARc1sUXK(pry4nu}V8VuF{jG z8ir5bd~WND`RSQ3k^la5PilZH9Wou*=9U#C)BCq9Y2STL-Zo6QpcKQII*(Rf4>iJy z$Nlfu;)<8$4GVmgX!M`2MWoptv&C6#={Bh7N^~19W_w>^S@Lt*g-pA=`K6`7>JR_s z6(lE>Q;QbNaqYfgVJd*)^TLq-$||OOQ5Mt-BptXmbSn7r8+vr=wGO!Ua6@WZW^TLI zx+(xAW|?o3Oy}#ACxQ7CD4^Z<44sp1O0crv|z*8WuGZLlmofnvMIJ_{!*!1@ni zFVjpw;&~j~sADZooPU!q>nmAsBlz954*a4hNk~dvw~g%Uo#L-v=VZCiha-0}xbcyH zzR9jw6#?PZ7#r*ieH1s70Wr4qC96V{>zorrfpx%PgKcB4f96|Sm*-^ltk0?z>mYrTqBh9DH^ZE4K!Tp&`@Ryxmk)}f(vue#ujvyI7AYCvDF8aSV zEj>VcUyc%&mBmeWk<8J-$`@*Nl9PEq*x$)FNn$|hS3kM=jl zlO$QpK1o;te;2e_U}qV)_`cL?6#Me!Cr_UahymY#Ijt85=(-UFjdllSn`JGBOw+RtFa<*Xi`ec%69bhm?dra<9h!m zvVvP8FnIJG94z`zz6u@r2oQTgYJfvht>ALrAJzofGEvygK~HZQNJUVVRG&Sg%f=u3 z@gK+-Vy+zE3V;$n*?W01adL73J_tA?C3{V|^vQqpe4GS)(YZkYc83S#3fkJ*PoKUA z2wiP$@&Aw}!ESpeA8y=(qad=fvdf20Je^I=&GUv=!4w5J@9a~nz>h+1`X0;zgt6C> zze~HPcdf_0A)!A|5*Q}E+0HvE4FQ|Fftt^%KB<3H7a*?MW8nV8EEA|kt(Yf%Z&b>0|Aq`Y$+ch5;|u1Jn7i@oI^B1^cg4|%(rVI$vHhkw zPgD08^sdcS?lSjA1vv1N=miuKn=`vD-8Z=S(c(UdiP~pP78XWH#@IaN^rAZ@PK79xFN%M^t*s1qA4j>f%AGSEL$)&f z;fk}W6H#^Oe21fC>ZAhJCQXzDJ{=iRgb&W6G|X&2Pr%%^GPmZXoB2-ZoNl*2X6v-% zSHD@|6A=-)f8URrE3LclF-4l+$oxmm=z#T3Kk34!!U+tL?=Cm-SMKPlFdpuZO^Y%2 z-;!m&Pk;HQ5AnSm<@n;O`CluDQ%wq5({ux#8j71XusNMU8&z5#q)QI7ka8WQtL*tFl3Te?DJq_2VO$!{5Pk+z&DBgmv%GW#2rMci61LuD_0RKBx z<)A#PHSTfmTLdmaYvBA>A0h+wX4$@lxJvhTdXiJcIZG)#)p(kU4inWJ)nDTts~ zh~u||Lm%k@PgBu<_lgXgNZGWX9C1wBP=74BID)5MC<)H$$10IwVZ8v^V4HMwc7nh? zJ~Tvp^QLi{ipB8{=qbSOhJkQrZFggSC|XK7XdsHL_kZP5kB#t=kb@x`#4Uwb?u?3x zhxGKVPTW%%Z5d=YIAss;GxhX-bZWS(C5)#Qygs0f^Ru#cxjH>Q=gfLP(x4a^zG4&? z@$KGNt%5t^)Ky7KM>C+LY8RBH|`-=pQKh|x>A z^dWiCH6P)GM-6yezzG&5D@juZN0}29%i#dGyxADT9SZpfKztam5sgc6<=w2Efs?N3$GJ|aCZ|t_0Boi1?)1pL6kPy#CC2 z;T65I-v!O=)v*L>i%!{;b{0||cG22(l-sjr+L0H^A}N;PYHZf{OY*SUsYN}tw6$ev zAT9%w_NmA%+^J&n_#UPY%i1T~sT=3%u zFWK1R@XE9h zTLq3?jx_Nboy$<|)BY!y&#uZ-Ehg?4i%D>trwMg_I^(vK2(lRGmFpeKTuHzFZQa?e z(+pqscZ=T^-hCLfJ5UHZ4n z=1z3#tVk*Ja)sI1K#D%E^Ar$81e_Q~XiLMyymrk-aN-bu(T(cFwxK2bjz!n^mRZ9h zyY<|7k;QbXouuY5!H;6XIA_=Z0D zj+ySx9#QTG$*)3&BGSBf%Dubq3{OVE5D@Zr z2QxM&l`&^kxGnlpzI*|JkE5qw<@}?oCB_*V9$pORkKz0r+lwmV2jWa8hBfkWBm!VCwwgmV@~$)Lb#QOv)=hQ~_pF7{m9Cx5PE7_&<(fp-h9X;1uk0pcvi}&}+Va zLpS4bIP8G2uS!@ZRX&nMsyMs}90-DCzkIz~9K`2born4eH3Kj3V$1)x*Co;QkQEWB z|CJ_34nG;e>qp=Iw=_BM=i`(*v$6l@;_Pq_bCnw%Mg^uWk0X+j*b7*ueJ(f>vGnm; z4>x4ehQo7EPESwk;YkF3ZEelXTcSsu%tf&QuiVY$WM$tI(%62Y#uo&!<~EnnWz*`P zs?x=p=?b6AW54Afvy2qMJv6(rg7FwWdnSoO2+`fC-cxVA#ztonwUHRrqHBeF&i2su z0#Ev~!aFr4=EtLf18ac?PNHBBgoDP=;m^QBPc$@0cMbC3VIBVO-*Xd=X_c6Tl$7iO zw-5GFc#MXG3UNN+FK|Zy(IY^W-TEP(qm{Vxb5fdcb#_t+12^=inG%s>BpiFDO-_?8(YTG{EWX1NS-g4vI!i*1nbvX6vUoK0Mt9aO^0j=_}j!$|XHztJ5C&m=igm7ue zif8`$Px!CnL}erMtx6#7Kxx`~nW}IiEC_H;-|gDKg9t|2x0&=qZkdHLK8GdptY%6Jmc!X2h|EVyX+1D59KD$XcX z{i8IhF^4FA25dQ|?-TH(lBbfA!$U(ARaIu@=Kk;A-PWHgeF`GKL1Y?ydcvZ5e^L)T z7G&U%FGdrV58Pu`XPt@P)*?sWxbMujLuo!~fxe*bY^T$qJDSqISTDdxlW(5yc@N@Y zyj*{A+K7rS_0Q72`&4m*Vj+U^pELO5`85NBei>w9M6XAFXnt7a^rSO0rtl+6RvSSTXu^|E|FbYJ8D;<$QCgLElx(c?-fiG#NidYH=c#Sbhy!d zP}2pG3-fp&AtA`jn8&9S9v4ugUO?tE~tbb51fa&p1} zD_~H;c>ur!ye}ZnpFxuhhby}%2cap2=cRyPbmN-(ja|uDBbf#~V`Yio1~?$8=jOAAJJiYci6dvFi!*mKDjnC*6CuV+UsW=Pq7U?ry}e;Q#!U|GJQxOhl0;4cXMqJIG&B_CK}vpGN=JDU6BAEQ zPbe7Ra)IiLC7>+XMP{76fHhO{hyfn9Yy;oxEiQ%V13saz|l@tl1wWYUi~Pl&)ITOnKRH7bQZ`M zEoUr@JeLFweJJ3^Z++f=yZTR=UzrbcCl^!^YODyX^jC0q6$af-vP=#+AR{yt$478{}k&*%pkI~ZN2DMteX0mK-ijx$p3`-XmJdeiP z$SAE({K7&)S^x5%uZKfwds2ab4=m7cWHVL=vwFc(2marsV&Y)3;#l^*+ zKa+xC@C0W*W)56{tEoTXJcDtP)JwMJ=4Sj`oCa`OfluInQelA?fJZiE(cJ8toTMZo z!aQ;4f3Ww~QC+Ql*DtMvK_jSyfRvPgl+s8`mxwe-sDua@AfS?p2+~TIbf_RGBA}EK zBB7$7lvoJZappp|+a34&JZFqE-gC}7p8dzY+4mk>ezDfNuIoGJ{7krnKap5_Ujq3_ zaPBro^&&!l$Fl`EIWJ&h2x}>Zpyf7oRYgUD{QM`XHu=9Ck#_D#fGmqE3#aPKqd^{+ zFtZhu{8f~tJ}%1BRV5>qWA=?Aku&S<8fkY^e5FwC_w2y=-x*V9c>f9iFf@%m8G@;F znoT0qn2vMj%X;A>CRvr2FLMH5g>eb?l8z1zh9d?lp*v!f1~M-|_RW^RapkCBRPj>6 z9>)1axbYxYeZIL$N&K_`%d)8+&uWp#a*M*ptovjq{T47kW#_P;vvvR_ zBf|h2^*UUL5fKqkN3hpSJa-P*CAh2zBYm=X%#lj0gn9;sWj34L93<2#EcD9<5p>=N z>;9cPcSuP|acEJq+;rvUbxQSXZ?;PEU-D9b-5Y?N41jhqL;D0|RbS3!&~( z>b&oc1(?gb`62}rBkwtc%;ek~QI z3K+oV-@b(p`x>%8m{Rv%vrG^kq)WAI<~IK8l%S1a(`^fVx4wz4)5}lR%vlChUUo|o z=YU1Yf79n}CjvE~J5`a~tJ}b%MK0?&=_Sxx=5_RM(rJ^20yX7@hHs{Kj>>MzVjC=9 zzU(@p-rw-W4OC~`So%)BjLz%vH&){zd8Ho=SB=v zZ)_}AN_@0xlp)IJYt;VC{Jgv)u-D=WSU5?q%DF*w--oQ@pMu&?CyBZ<=kmS6)M00B zvp^!I4n7m9+ZlBwTcDLL25mLjx0Fsef40>^pUm~BllPx5(;cH6Snr8B^VUQ4SFa*$ zF$)di{p-hrZc=y(c8SQI+`KDp$DDM6+n|ineeay}UukR~XbXIJwpbwf^^LWv#?C`4 zw07(u6pzQ>-fhX^r`!A?i25|ElPU9 zcwfLpkIfgX4~Pm}b#=R*0(ji(+9xkNU8>i0IuBmry6^o_H~o?c%b4iv{pn@9v(ADX za`*P_p(Jn={?u%0w1+`<;~% za=cQujFa0J94c=yY-C^=W57tbXN&2y#o;4I;57oFCdFg7fci4QN5LZFp0y(PFBipm zC^*xT;JDQs&~z!a~3*x8`or1Ek77|@~Pif8V4YD5U9g-)ZOJ_s{8oyblCFymae;UrQ&r=_G;GLmSm0eQkG?hh2q1nuIu)qvn=OlA<-hq zs0e7fK$E;O?OqF6h?niRc-qyk$oQMhr7KC4Rc}2qB{*GQB#h;UFL|XMrYDsNZB|Mr zC*g1Xv~9^iEx__04bC zZkw@ukReH3QRL@`N8;IY=QM-YF)~&=ce32MiM_h-cuOSAt>+RF=z#G1xBl28S2zW!D7<05!CJnV>@?VxDrCo2o?2uNR=^ha%;HGA5!*yWYlO9g zi;2O!K;$c?r>=jvosF0ttk+b^r28L%@qYxyUjif5JMwsEivQ>I#bvwycgl-A3nc?X zZylTCJlaJf`nE79kPo!QG^exdd}=X$Q{2TwBW1g@cXzyUVbH`b@30MP=+vuU?9+Ej zVRn*k>*~y9uu;i)N{C<8nV(bw&jg0GScXHu27o32h(+LIA;5wthREl}^8wABowTpl zvlt!KJDD7^9F@ecZE`r7gCeB-X1ahrld^S;)m z@VV8+wnoi%4Ney4vX0;BypoN5uZC$=WHV#0+-q$o5xcg*RavZ7QB(nhskv1E=41s0 zg&>n&H>rkHt*i;x!)&fijQn~6AC;J{=74k5{M+wx=*nm4&f&2sNdEWV)nX>euNYHe zJzB$fUcHUXm8YxPG&l`1owFKMY~7 zaF#MhJaaYgxvlnm@MmtaZLoYO_K2U1qDzOwuQ{G1N4&>gc=*fdy*J6pT+&pq`_Q9d zJ5KtbX+(GpMc1Y9Y#c9eWVnCeqwdUwZ)EtfWch!3Zc|pQ%XPEqE(z0+3z|p`Wn#;c zzN$uk0fyzvtfDq-Y;3a59ZWjfY%SV?$(9_oD|gOrkHXHGijJ+qb68S$8JVTeE~cGS z@)@Eyq0et8P7CX4-89nW4@AAw?sq@Y(xQ&+E&#YN0cw<1sib2*cOTxAgpz~Mb%a3Q zyuJ%6XWSrO3R()Xd?y$j@Ay%V~ZG)$uUkxJn1_juz+90on%!HHEGl1N{ z4u;kVPrzo6`ywjNaq;oZ)4PnGo2$&8l6YhsKjybkwW2oN#ZB*|-p;HQMyek9Y6chl ztgHx~7}l3MhgKTB181)K$lWbpwe17RzTGZ2PxT|X20sT?&b~+WW)9L0%4<&PuI{{H z>v8W8?PQF(NYu3g2HYn~Q-Obaltb*Xeqpx{$Hl@92!bCFD06Xk=Hums%}Ei4WohX} zjFrdbZ^0x*I4ZH9#a3_%wq_6j%YCVomE_Z|Rp`zhaXYhC!zg@J&z+7Ac2EQe`zq#* z)zv8lpR+>o3l9Ybe#Aj&v!rBtMh5K9DP>)8JWmf~3AjTu#Hj;T5lkEKQsA$>dSI5N zKewV{Kh|LQYhg(RzIp(rQn1qC$G(hI9tO6iW@gkva|9;pgXf^yoLJu1KeA z=n3)is+SJ}Kb-pQ5|GK5mnbTx$CI0G>V^Ic(e%xmH*nW`J-DFL{|9I306z7Wt}xKbR~H*RxiA>1|b@eH4~((m2{#lgK+9&RgXY3ATu zZ-X}%VhL8XbC_NuhfJB3e1ot8B|gW%Vz+C_@XhQ4>sc^>HrOUTovF-5)t15b^H2 zuxjs#eLkd@vJGENh&2S8W=ka4Xz7vhlSFD7tH?M95_b}GJ1Tzb8BP1Ql3YfYqjny2 z>@N*m?RA3X9;8qHlMVHH#akvdf~3b)GMF-2$~G*?fCo)(eBRE-uGW6M<(KfuQb4@z zgsy=mb>T+>dbU)&&&|DO_uAF#gUe1$ouwD!wz;O~gg}hWo;%OtPZ%}ZxY6;!T7f}@!7A54=N&$;(BTK_IZ;ByVDiqAz zzZW5+7VQ?nV0~82c2|Cd*x3!0>~s6iGoA0FdmIi0lyqC-sj@^$Eka849_jYc3^QQ+ zuu3`3lcvd!)-tNPS)-R_J4Dxzm4%6BTz2)8A`wZ<=X>{mBmx#6wq!C=;qOf0K1#-P z_`!1BKN#`JYZGM6-;HX$$W;xij{G86&Wd!p?4YarSgWiI`B@zWiY+1-MNtW z{0MjnkndhSI{0`6E$!y@8BocJvZO1{W;@jB1^kNMcCKeJ@1#HjSsj zs+v~5o@dO_Ygo_PhJE|~?(%i-;)oTi3CS#^So`U7KCxV=OF-}nZ``;&&$#y_re`1v z0dh)zcFjBL+f@Czh{zF}jl5<}^%a&Z7aIy3FU)(@ZE)X7zS>w#Y&Bp z6@c!oQ>U60H(sz%iW%$~cdifbHu5o#iA~Jl(}SYIWZ%SGEH6{AY@~|8?QK+RO!N-1 ztHHL+l@SM6O;;7BJ~O4|xGC2k7Z(Q_1RgqlxA16!3!T`Pd7l@8v{?SS$dANw z-&-{=WHBeXtGCU(AvUp{fuF>bOyi<&`?{Nih-X8lZ(JE(Y<*`z@+)I;#PKqPz^!lC zL)-1|)<8N%ZgP`=KxVKX5ZsA6(tvW0c0G~U+q_eVLE{MvvD!~xK9s0~b%164dVu>5 zC(xTm=_S<_^!4;qK;bwz8VKLn)-KX;Nz<5uOL(=HF8O0?)9^Zx_mjQtgpg*waO+Ui zJ-q4>HJS+#aw}HVO7$0vt7Y6Kj`FnW()GFodqeHnY#`s{? ztrsHEBcl|wn5a@oyG`z;LS2&?#mrvp{XO}kj@3aQ%5pMw( zxW$e^K&wtDX~l^8dN*3=&Ko1vBq=tA3pvow8d$DkSaBmcMP62xf|8OMV=s^*UOh_t zR=rbw?9ok`F%f2e-x+DUqb2)HB#QJdXJv8YSeun~%;f0FnUjMi3QJLPj-$KJ{HA5< zWE%dfmbvtWL(6>X`U#R^VnTwGi_5izKHY_rRq9N(VxEk5jJdVf<=J8T*~w;d+YbqB zCJh|z>DZ+N1O?lH&i3^)Rl?n>5=I4jy@+kY6JAFO$OArMYkqas_9Z&P{v#CoU$YjhkDypY8 zd!asiYX%Y%fP4*s=>pRL!N^t|az~&QUiL0>84N9+zIf5#K2VH10)A*iCXLxJfH^Qf z3pQ9*EHOSAEzw$)S0Hd#wAl4&B;8usxS7>_{;jk}hQ)UwhGjIYX|~e6F1E2c`MjLO zv_1A{9~3JZI|~hls#HxKv}*ik5Z)N*X~&;i8~N|-J#X;o;r#peghvcB+W^j`!&*p4 z2x7JJ4cIF>G&HnDBts3=9Usra#v?{!=(u)j&bhQ6h3FfZ%MP!nYam{q zlIj5g)k87Xl+B?kR@o9JlWWgZ$B9pE2;7)2yw8#DoN?S9*?-&rRUQ3JSSn>Av>S5- zK3_fn2O9+Mgv=u91DR3DVv&O9T153;=}lxX)wNkh?P42v=xJ>}N_;vD3QSpBCvEVX zZ&>0t^V=N!LKf+7*J(DIo_Vf{Rpm19Sbzw*C`v$-LWnG2>^Q5yEU}i0>lmrj-$y#d zXM_-2_aTdo8U2S30iWvbkByB1?6QJ}hUY@6;j-eMkK_84yyth(-!UEi`6&dZrRjHe zq%FI2&?@L8;&|y7l?gRXYMW~Ffm>p6Zp$v&Oms)yFs6Cw|8LpjfKvJi*xvIB4VMWO z$eGm?%)`UB(70lf8Bzxfi|pThTwlmf9LPM6?I|dA{xGB^KF3i7V># z&uu2TGih8?Qh0zVX_?O^DeaS!3p;XFo^_t-p6pH>&z4CpI;OhQ?J}IkYA5LMfWTLN z{qRRgN3TkMrbdbEgzP0ruT*x2D`-Y=3F1cAvBb!5>e;iN>on3*QsT?1y%aG+Uf2_M zFw-De{MgL7Q6qJzWKn#iV#>}t)HN$FlJd$}>0OFVqqW|vcemko+OpA=&T~5$S?D?# zHtp@8I?ns*u)0}}&4j)!^*Gbu-!dtgo@75h2(Dei9ebnS`+=hc<_6n#g31JYL#5-b zs@<^r0nf4*bV4~FCuFyQw#Ci?V2^ot3^w1wq4a%6qA-j};pAp5hm9abzacYyr|!gb zwTio@J-e!V*e32AbM>i)+sVt~S5Ca$ahB=Vi@TE+U94Hxk=DS_B3OovKOj%Om@`B~ zEdQc#nmOxX=WgO#B3_(ubAL)=VxIX$oD_$|Ab)sc(~*~#_jf`EmZe*38Gj>j)Brx9 zLdc%2Z0>FSpN&JTp2p&7kYonMQt$glXR=r9RrWgmSDkp6`F(kgzbacVQ@q|jRGK4GB zPNaGG7GP|w2`h+q62jEPM5}y5ARRIkCO81gtym)&Ud4?#2?*#Uc09@(iT$g<@`xtYoM^9^$x9NyHR~sTPwh;Bep;L2=|1a-UzTN0 zA6en%^prEsB5w^)dxG^IkqHAO{!HW_d3l{c;lqc%Kw2~I3Gb}&_?ay(vxJdU!wBs> zKu5Uk*&`?>2CTC)ruc|mI`72#3W6L_ID?Zl#LDll0qX@Z^~2}SIj0W(Weg#i)uRz^ zb$`;irsG29+YP4Mkl=JjasU&Xmqo}P!x=gY{U6h@9-MtgH2eR{)Thn^L9I8Da|OQw zpM!vj{s?>q923qQ>|Bjz|gf$wXmQfeu5Uet$Y!}k_+yKj!bEl5 zwe0M`X)vu`EsiJ#$a8q0$;!!5E?b5iScX!ttqq%iMOlh;WimG|wuF?FcBYpoPdblc z0;|IVyqW0_`_}a}#Se?F~b zTzQuI)k_~?IxDZLk_-+G{^J;6uv@U%?Wa!qhroOLn7q?%47lQ9gdPXC$hOv_@&u96RZ0ifn;?7-x@zZmFCdZV4o~k3!W4*pe`|2NPG`&W|sEEuW%Wi4v z^p9s+xw}1retx=(L0^%~0nZC;4g#ok(V`rwfQQ$EcjxrV%F9buKUsUnPi0%{pkel| z2VX32#Rk0-{`E00-1MMk_IfFuj}s3VfX|8ApOh3ioOU{#mM<vb(Mu^AGj)@jTV!YaJ9`2sm73=weQO?4sYga@lo~c-vI^ zB4ewmG$3JpLWP%)e?&WX)?BK%QZ{S!)^oCFDwZdSAK7{QT9)ed@!46tXRA{4PMgoF z-11s8-yG>(FFM0`c--cJVD$c^gR%yLn-n;AZ#J3Uo!n4uMlw=!Eb9^;2igK5H{=nzwg$pj zf$w$j{(Z^v%+>31{k~WBR1b_J>XXFu=erO60IV!AXT;_UUm7M}L{k~d^K$7kzg|$A zre&nOt;#h^TZ^(aQkBoth_gjydP_s-g6t0cuD34Xz(VD{P9qlZBZuuGUqk?iq z7YA_NLW%5ZTn$7E4v8S?$DeLnN7EeUUkfu-V;9h7<} z_1zsvn`Fd-+bNeHBxSU}m}*JFm4%of`bDh1u?c1%q^5r@f`mh5jPnjD$ytXZKc?AC zw7<9m#BlpJZ+Et=bdlt`N=yy_;6aEo8wp)vE^_z6h4be}z3IBwt%=-o*>+Qz+XcUp zmj)>mL#FcbXamBtBf4=+H zzL$OvPcxB+UO^QZJcVnH-74F4mFt-DvRgs1*VkbZc00(cT_DD%#X-H4ty-`kFhTub z)Liv71RXaTq-P35ZIU|`L^t|8DmC;Ffz3jn?}N-iv_$B z^^(_|&Cj4>R?=*Wk)uR52?^zQ?vlMks}f48goK2Qi%b#1xghOvAFB19eP~y+c6Ocs z++wJ0H^T`1gzIWe9i6C5K9jYpcryoQu_7+=JKhMT#Gwz{&uf?${NO7!pw%wS5m)P< z9Hx5c%cD5d?Ao>Nn-;)M@aNFe!&>DHZw^|AuxUg3WV4#@JI9CA{I@A-SqK6Wj{3N= z1jNKR(X@w6cTB;@xm%o@?Rok5B^j$+97P^Jr7d(B@iia%)+t+>=t0f%U4g=Zr!GxsRiE7REgKg=f%;c7!~D&K{spxxq2j{ z(!vJUg$x#TMCsr>WmpOxmL|6*Z4D$6n7Mnlv~e9x2(e-jl-w6rc$wCEOylzjm{b0a zacaA*YQT8uOS1SgYf0MdX>4w-%DdTqC2w1kp$S&ekU{fgJuN#RTrSRbT1BJsI_y94 zQ|Zi5!ggi-y!f8+jx&jO@*s8CXXg!DjC-lMg6o~NrRE%K))e)+)mVFB*aXHs(jt_{ zRpt^o8Bf^gx~L>vE6QA3DC5F3Su@0bU6u2MM4C{CqENKQISSPQ;nz_dYuoL(Qk{c6 zcsfhCE=suA@+Ur_dA>H7$W&4etm=sF;^1xLd4UHlln<;dz!0tEkm(v6w8-gIoNHl3 zA~xWCMZ{Z2`4)=9KaH8h#;i|Bi833$i}Whnosu*;Yb)7|ghxGvS`~%v9Eft~+eN3x zwkqp6Gap@>uD|md>*`?DAH%l|3w?zerm1u?wGgl-T`BovxPZU^XA1! z3|reydQ&OdH;2c6R|8>@*;DBE-__ zP@>^thhR&quD`y0%0zdyw+0lF$cs*<0YDiM3|ZXhcR0?A_)cvlGNj3$xZm^W_L3l= z1-??>)rhdLo4^DH1O5f+^Ucp#WsSiQ-j+BGIgN_LFr&H84^Ut0*FG;E<)#t}f+2%{ zx$aVY_xEWTMJ~HbYlvx8Zio?E#fp-~c$FqkU4c(OP8++Dt|xq(A>uhrQU5S8{z|Do zP>E_A=#sZg$gwCB)H+6QU9BMXhB)dRg1ZU1d(5u062eu|6G9_vjbgqRoY_Bv>7;D1l-G#VWMznX3q(b?3sR*nT6%iG;0l5ux_`gnJt2-3>SRuj zN2Vmo?LiihdQBBRg)>db%Gu1BL45m~#XvK^&=j~;#dBuh#+Zu!IVh;>E_nc_+ zyIsRf-nKPT}|7?Jat zZ=gyTyFyX9_C>8`<7K-$ZmrqVhlIePS@1oNq1F_p#~d8Gv~2hPpebvA_X8x4sHw#M zKvlJQ?&+yOJ#ARL{^}troE!L^X*IWMh%2oM?t}(wEAORfrSlZ= zxp{u3b+pH(U-OYr%@%`y%~0xdH@avh<*^^{yYMSQ3HDn6-!h}FQ50lpiepWkS8e(4g z58)7rN!&>~lkRreiF=v&#mE^BT)c8%i_D|cWe82G5X!vI?TekcL5{Y$78};z)9%%N zAs;V%m-#sIm;Y2Xy@|K2+HSK#ucj}y`I)nFHjzf8%75^KoYm0+-H_Jnr*6n!CC13p zxTG60GtfQ$k(}(@9q_$S-BF+}E_t;ciE&J>s#gb3P1U$D5yXu4;bT8enMhh*Z}za> zH+8RmEM(1yKV(gs&ysQsOS0yeVD}sSLdcp=OlGF7twJcoACa*XJQ8u0nhrnqorI!2 zp9HDLJXv8ICNFOyBr+XhZAD3zzq2Vcz-nyGily*QNHFH`c$g}y2)y6WiXTD-{!v*S z)vglh==?gi#Xv}kO*p=qOee&MURWi`WAZt(d+w=o{-U@ngL7rMxsRQrDKsrcnWtW= z)E+2UxbFGckAJ(|j~&U*8aMi}>j9SomG974iWxStsQl+COAL~c6U%VpV(-3Z@7}Vv zoG0z56pEPJtw@`y20ShX+3kE_>h|L0%k7~1!|aW55cZS(?S3dMSnLokM%lG@d( zE3Sl=ET!Whtl3s3drQnnPk%@80Y^d95J}dofh|NosWRKQs`Bt0U|R+ls;Ln3ks{6w)OQnt*bM zs@Hn>Q+!_ubosswT)Y{uVv}vY?$BC3*VZA+VF+tYDa!OzHd)1vzCoFrgpcu;TXtRf zuu%1R%8WkR@}LT#j2hHdNThFGK}{X_;f+0JR!3qC3WP;P5qw}S;6APPM{3V;U78N2 zw#eh)`KzoPBIs0)sl~U`)U>ppQNsWX@m(3WGo6O9IS#fD5x;;rdW+Xci(=0p8=|4* zKFnhM%wC?Y4=hte?8%h*3~S_*9!mUFZF!k>3yUZ8#QK%8{>&1teWwQ_hztaf&``2Z z+u{8%Ei%RVa;lqd{ZsyiJRT#8z4U}N>gZLAB2%3EFh$1@Yx^4eyxV>$hlIe@`TYKM z@KnY@4BT$6RTfYKjt*UpcoW##bbq$h&gPsnD|2wFH;yR{;1E6w)wYl#%5aLNmW;?= zT2V{cw~m|D;^U%ht(1S8u%hjq(wN znNUMf4pR&qaFZ`b6(x6lO3DDteDc{d5zC4Yn-fv*?-3DKIFUAPbOjdd0}1bQ96-78 z!3>XOku~ue#`&0ua@0gB_OK)<(-vE1WwF0Mol>4~V57ww8fl8Ncm1FoAPfG(w#ntb%Z{Ji+m&2zUhupXRhU*UHwEiu0$bT#};a@|C zm9N4ViO^w65&Ap;Ai@Y7M;9EoW|XdKFpX{{ltKF~Nw(yZw?4@*pY8Ai627m$AO9;m zaWMI4z}8xzp|W3BjJ;<@fU`kw(38j*7E;idm326W$(&g>5(-6}+M$hahuF_AJWQRC z0MWQm^9bd$&pO`F(PMa-9usYltzbm z4L$X=#&2{apoJn~^Z+|m?Dh8!RUCU3K(D60KaoZoxmCyNZOm~qWGpvy_M{j`)44!iLpR9LEj`Ela`OSN0H*ER&`u*)(;A*zfu-JPSvGdqTF9W)Xiw25&<0O?~`f!7}j$# zGT!|BMC@Xd3xD%W#wG+Cw;jHs5hOmHizab!pA%m4=yJE)#>!ZYy0qng%ZQQ#;54G3 zz!kTxnHlp(@)rpqFZxW@DAXjxnbjXOXYnl+T)6z?3T0OesL%#^h0Avd>y!(R^nZj_ z1UEn_cA(e?;{nhWR_b#1{5p2_4n;dh%Ja)9O8*69%w@zFt#P~S}NwlUftJbr=D0K<1B=gPU)Q8!kz(FHY4-Do0S-`5SM zCwXda6`FC0uqE)ZZ+5;zFGox5yDk2pL;YKsHakJeloN=K|8xHbJur<%Q&322FskZ- z&YT|{cN*&IJ=fMjjDuc9ol1y+5g+4|9kgFGKl>>H<_#T33wFNqL^nPikxDiC5yv~k zZ;l~;^KT*`wY%UUdcWCc&!hm|X=608v>ZYL1;u^GAbJ62;#859z?&f^Dm zwoqWNf@w4-cCf9?#CF+pB-*jhs%9u}U+fAiA%zi}mw*HNE+`(`7 zTh29Lhbsb^T3Qj_rWpDam6xNdK;(04Q&SVNpLmT(AHoU(9=#HF4mczeqN=YRZbrn{ zvioP$Wr-xA^Pd{2Fo$ZIh|q)dZpuAY60^6XV*Liw>E(sV(Vb{ch0UAD5U)E>J%!r0&ont+Gmx|FoSXDC$N^;iHUGMvLqOD{D zwa>({3N@=Q{Y%$TbaIt9ccd(*#DIWq;8}}M+13NUILKbw8{LjoVfwvkS8{0`$CWj|e%3_N}Yr zEfv4jfZh^Jc~zHDCisqM=A5h^HQCXdrlYD#=JkB#^5w~>LxL~F#DvlL>K9DD-@pX{ z}%$t0CabJA{YKVWK4h-x!)s4_9yW%9(jsHxHPOKAn zziVb1V^Bnm#%o*t*SuH)y(DhW_SpfC_T=v@C7Kamd<*EPZ@LJa)71qIBH#hP@ z&ibeR)_L46bl>^w!O9UztJnD{@DRhFGMe?Pip=3w9lqn!?jyFOFL<^63^PcHpj8n(d=E# zz`994sD1I7ZIT&k&uucpsZ$YLE1txpTwYS*DpB%`VNXek2>OJ$xJb%h@-iKM`^iM^ z^H@!|gU$8umG>}@!k7(%Ad|QM9~BP?4`IWlvxaZlXLmTi54#+mz4XHIjPZgV0$^g> zpv3=|!isy4)ibuRc%O9r%ki&-!UG!^y1BNeP-R`SZGK9*#!TbNIipH#>Lpq$V{s(2 z6oq!pRde?spqv zFGRC&H8v(M506=N^W7fi=06GTuK7!Zt;jpearI^%o8|>C=1(VRzDAXozXq_P)_Rlg zOIjdJ^FE(OV((llHugS$igs08|L^4$h1RP6dXKTO6$}R&LJwCqEZsfrCoTVmMCILL z%MpA&YO~TfOQ)R(J&*ktW1npEiwI5w)RlaQdvtUZD--M&FBEbFS||l2T%ee?l3s}7 z9oxIzmxKKN#YjpL28@;0zbPQgzsZk4qFq|j^=D+^S3@Ez%=AS_;(ENdB%+QO=5u+7cQ%bTk=xcj$U;zPv~raKRm3mvwlS#@{v z>1BU;Vg*yNx$0jEMJ=Yu5!h@a0oG7`l2_s=@Qq^kfp@WkY)B#@UcPFDyx(?>R9`ui{Zr<2Hc{yRUR~* zBKywA3IjB0V8fTwjG5m3>bu(>G#)xQ`o<|m;k9;)pk3R&Rdqz<(z4WiH+H0>8eo8) zM}-^AqF%S1Xdmz}D;Kgd(F^9=BmvpAdn-4=K z`C>VXreK}>SHURyZS(#Sutqw=YI+wv_1`1*fy^Os4$iRSEK|h47vb;P# zkY6MveGnOew-gH_I%-p-J0Q-jMm4~JY9tG2%dLl!C`xWjsJj1#J4m&l~M z#TPMQS1$d$k#b3tdTbII9i4Mwz5%tsh`YfHVck@L!^jVF_@@G>pv8-GlO#EnVcE=yNB`?x7BE!$`)u|=7y2i3J#eUm_1?l1Xa4L|Eu8* zc(%|acNo(T=xi8aplU@PH4S{Hh)3%%v4(h-^5S=0CBg)HArc`U4f$a`N38g;SjUUX~S_1I} zJnhZ1$T9S5>AYdJYKb%(WO76I@ETqk*|`kexni9yL_jdjfZ&6A2EfENW?hWlC$QP6UtSZ>Tu_wTsP9q zRPUxG%{Opgrx!ap_b@PVszV*O-W!J9;QAUg-{A`~<6EMLg4_ITpd(6`p2K#7h{6SY zZq5&Ov(cW75Ep-Lp4I4`x>r!Hsq=({i<{yS{WIV2-P`qhXlA50*NY4YBz$Y>v{yW6 zJ5eWz!BVsET;jMM-|KV=Jrai4+)&Ssx9*TPm-G2DyEC!m_O>n)v)Bjr=VmUjKg+V1 zx;NKV4MMwP5WCXi&ZPeNJ9CyBV4@^D{|XZ&WeoWtTtc-x{7*zFF#U>zZs-1oZa2U& zPv~};22lnMgJ*04%LEWRc=9!eAvWuossn>Ksn${)GrFI6+yBrO#UK5!q#N>UB05Px zWS20iXO5(-21fPdd~RW!-3NY)^M6$=!@+oU;AM6*WUOJY zf;k`8KRW94q-ZuN5&9ao*yu`5{4Ptec}Tld>kbXcxl-Gm1LzCRSH+x9$^BlTph-Rx zD=WZ($p-lkF#Wf%4a}>kc!Rl^-vyvM8Fsvcia%Jo^Zl!*@$4F74Dt9%ds{NS;ySqs(jT?gL=DG%g& zFUGU>;n;|NYE;_g7#B_7glVFne+Hd;5$$xJDV)Og;D~uSXwm(tvHgZ@Dv@ZjQ4=CH zdFhf2cxiB+VkocbT-b1a1eANMtQgXt_uX!P{_Ne0oNXR%5I>qH1>}U=+B)ta{db2V zO8g0p*}9+dR$!|`TWdkmBc<{v%a(fD;$^1#KE=ml$I|y`Y28NR0iWiKBOXn#YoPi? zc17UYdg@}+0F&*4Y_mhA*1*hkbiAFP2P6>aNtDdY2kT+ljLdY!(XT<5RHAK8xUXfN zA4jzWdIcKx^z%<1yVkb)IYAZ;NGWssjVziyQ1|SVJ)u#G9^)^vR5jb=8kF*l+N?!& zFr#680MMQlt|Q26BA&s*#MGlCANV#`CyyN&^?fdH*tU#qVvgk5A5Eq#f3zEH9wsJdb8;Nv(#9ha>$z;*qS!Ql zq4AhLk)XR)B);$co3+S28cuYgZB|vRYQS4`WRp+gslY=>RZdWjFdc$X$65? zUAc;xr<4tQmiz;*`i!w9M)$m} zDv?&*Sh6hggk6^&IIfr?!4HG-qoDV%^uG^p%6{|8K5hRwnyYmS?BYt`9VMIQKj`U+ zgUc19K*8%b7jDiHy4>m`Cs$lA*SfdrsdK$cb!9do02ltIbDrI?db4O?&>vqoFMH_R z<|7DaU~-D&#qOXPzE5gaJT&F3m#AHTWCF4MbdG@ry^z5Xm5sHN3V_pugl7{It#b0k zoVZ(|4x*+Q`@@GYU%`Bv%`c##6q7WgbMj$Z6ivwHV@PMM22Zw^%8TX)PnJJ|-NKM~ z4WajU#f4f#akD9z08Iv~)9%Kg127`+-il`93B72lWsEx@ZLugskF{dCxzMo_^OTvS z85IK;w~#g&AE2G0wWA>#Q(}w${AbU1OC~ffrk{_==1<#}vfKYcGLm0gm8RqTqQ{uU z%;&X;kg8Qz?F$1p=&P*+v^Pn5C`nX3DIeCrHlW6R^iXWx8)i(zzGuob1u zW_p9yp4!^&aKLWr3sc#Ma2^apnVzgxG}`anYuew+Y?fJkZQ9iiQ9*I*R&@7%d@J`Ge>Ay?ZdWFE4{nvYzydT-gcM(YMO5JwBi=3=h+!Z5@AP&*ql2%jH*vzvSMQ zh8?t-x7(;we>$Vpu7`ZvDk|djz2MA}M>J5g^au1aS-pA@3x$FzGt5@*m89 zYY=7QDAFn#E40VU2Gk3yT2tjT0`D6rK zu$OCi(@dMNNUY_^nsM!#1++r$XG|^`}gZsh+%kv?B=Nb}MUqNg0d-748^rbwNQok;YQ~ZL8AzI%%*oA zXJ=aMrCjL%PG90X7NOVAq3TT_)`xB1Q^O3`|&}< z5h``-A$e3FK{$@dk@ua{L-jBK!^2_#u)+Uxxwba;UwY0{9C0W;G||DFVX(28;NuFI zh)@TR3LWB+gk-vu`)#{A0+HULAMXr6)=VO63&94_X)vfVoV)FN@EQ)U#x1tC_ z$1e$eY^JND!}#I=F+yd9RCNWL4%9(og#5VvfUuAd%Gr@wh{Uqwcq%d`Cg$|%Rj}N+ zy90ZCor_Sl9q4Kso*a@%UjCU;(zvC}bGa9B*QcHp+v2iLJ5G32__~hq3;cwlZrzd) zORL@TF``J*>Sth*Aj1H`u@Qsx`sIY zAF=WO1zjTw{i*0M)L5~p+0lLexrrcS!m9|rJ;&uHQ4@_CCMJ1-%=CU4_O0oQ=YyiY zX?a5pe5qtuWC}l7iqxLIhQN8y(FGrj6N3<7ZRWi&sKu)Kzb}jE`(C{K35xV{il?9Q1d7YbM7M2QFV-VX zpRQKjUBwF&;?dm=W6Ax!$UV7xhPQe|?%K8Mfczum8X-1gMuFy4_{mJIt)ByL4wc-h z+Qq(E{9S8)ZmzhT$`Y=z^*?k*?x9Ll^Lbi3=UapDQZu)g;<=M2m+8FNG{V=_20t!_ zAvwK0w9p5!hbDUcAQYb5i)L!z)0h3|j6BUaQpFhK&_=g$+|z7%M=Tw=<^yff4=tZf zhQB`9lqG$o7w^`ubMemIM25cyR#H*@b*# zeDx+KH@_Tfl}n!qrF-VF1Z0KP?_x5 zAsWKjlOf|Zf;c2z6Xz#3@4%Zsp|XBP7r8chGiw&{^3VYrtC(u}k4{ICYwirV zV4~Gh2Ny(et!S2-gTji*U(Myq?_Ugqx77%E4c5#n!QZcG=6L)0Il0^*Uh(0m9|6hA zQyhygEZ+P9UoFOszDL8#@e2{G>x z1FP_>4L7~(k80d4tE>A6IUn+RLL~RxBF)zI7lad129%!&?Ybpwa5Cm+gf;vx{ft^} z4gA00q9V>n4=2{0>R%L&K;Io65UBA*^`DiR(Sje0|jL#dh`2fI*d7*A^ERYULU4x_&qPs(_Oogv-)SI`Q2-S;9W%E?!yMO&|-vyU4n*2E5I}u$xP4Aa&U6q zrRyF(?Y%LXi&ex@9D^eQ4H57r6QgAGaiKgw^8Dm2&fu=_GYJVT;gzU*!DhGA{1EHciMh!UK+Zhq8YSAK7x%9-Vrf$a>~`r>2P8(=)3X-~9APmSYto z{E;S6Kf`&y^!oqD!p@%2bM>afKmz37mv=?Pq8Ve06Z6QLtx6u={o zI9?28*N0Es3q&06^jf+2YituQUwXN^Zr>+_GHnY+!J02ItDe#YG>233eLv}NkV$yR z6hXjWzbl3a0-A=;GWGxK??O##TlmQ{W}RvN&1Y|0HpP^4suBcJ#Mt?ilrD^FKx4t3 zxzox@aQp<#@l8L(zCN*F?~Wj^uAd22f$Adld4Y06b2s5g2RonlLMW*;iHJ^mk=iWT zndbU53am*+i}u1dL?Qy$y8r9Zj4g^*lXnggQBH|PwO2%kBN~Q@o$Nt+)CPmb{SB8r z&)&i(op+f=9cZ)NGAiVp8_|!*6xvd?vE&&}tbqDQf6jZL9ui_jjl4Jm{-wMpU)uLx zuK?vH&&@=(U7IvV+>XQY5MNlx@Hs;?d1CtAR4*DV`1MBzF7rQPF~98Jm%^B0FQy!* z5ZGRBV-iQUp~mH)+mo}Hm%amp0bs}iy%beJ>M&VBN=A>I`}Q7Rwb0+*6+%jm%5j$& zWfI-8XopVaf__R$O7CxOs0rwDBrT1Ag}8Uy9_#M7Im-negA~Ln077JS439l^${*DLGk)0Sc-y>LTQDS+lNYuwdbn4^cjb)s`yL%3KgEY@6?|0j70S`-^5=}aRo8Ht8 zazh@mLHlkj75lw6MZ{-%O7ZoB%XhoGH_6DHV-YWbF_DXli)cWe!JJ14z!Ss;fzd*` z6|dkij;j=p=}!mlkW6oP?PvAh)3|C=e2zfW8`2T% zm@zd_06-RR%HLmR-cQN&&4bJPaDaGR=j)2D!s=>Y)HwGit}%9M5M|PMiGX`yAt-7f z#E79L00uV*qz4N(nL$}CNr|#UnySvgs!`?nCLi>1lIQrYMDE2?T zKR5{b0ZMC3d_s!(*damVul4I~4Gk**Jq2*^8$?y!M^SFcsi`>l-~=xzDTy=bu(R4c z(mbNJ4FRZ#?8+J39>Yct8@I8s>h+D|7ljoRc9xnWwV9uvKYm*34)xJQ3Gemz0s{PI z1_YSa&H3!VPrd}39W^DizDP2I!&Al$#eGX^Tz-EC5TOwIJpWr|^2uS^L24=grvC|A zH^m&=S|2l@b$j2R0w8X^hL$9tea}|-M%tSF0Eq%m2A1f!am$Ip<(+q2iCzCY*0yHs z`eW+N+z2&LXXTqJ`youUGa$8vTbYw4#;ll)c}AH3UZ9 z=!-U)|Ai;2-9*8)RQ@PZPTWcVpp<&nORqxrVJQ30?n6*i8;I`1!n4_DhYs*ADS>;1 ze#HWRX`kbN!~*})&$vSG^32Q(j%*2$^dPAcq248us?%o&b1R{!qC8bciiTE*+?Gg0PN7DU9f?T#KN68l@0@*j%{Q#SbGF$e zbu7&gB4|@VUvF&{2{oZ68eFxyA{%6&>sv5Wc6iZqrK}Fpz)D zVm2}VeHNoHMe}uE0Sb&Sx%Kii2hZ)D4@qaPu+0qG6pAoRR5prxur=%L>oc~sz2-B~ zf$JX=cg9%N&Q<6rL|s82o(&U9D{s*G8xujt5a)}7=8H|2E*15lE9o@5w0lQ_AXFOI zLy+ZrhjNTW3tK@Ekp|zXm5(;o(IvK+96~=hhG~sx4u^lfEr;>>Njd2nDu3eyJ=Xqo z^*d2M=~dQp9n(hj9>l##on97W4X5P-x?@GlMqTurp$yyQVAI|PnkuMQW8?p zgiMiHGM711gfbLS#>iNtjG0o1km1=*thKuD_4_~nSI_5p^ILE3rFG}JuJbz2Of`4f$aWuj!Qd?K&;OO|hH6vP;fh!|X z6g~!6AP{u+wL>lJYwrYjmW=v^N}-tt6zaUPva+AvA&02KLY041>&;3;ATb0vHfZtd zT`CJ**66LFRa$`$txe6vPBT(dk3*lp)tse|RMA1?Eh8hBiu6v8^S8KYJ2C53Aubw> zfky-ZVg0>vg9P+RNDXt$Tanm>0Gu?aoagFULSq@_$_w++^24m5?Z+I?D=`^@cb4Fy zMhV;>|A32HUFIt=Cv-e-#Ta?65LK<}aH9lQs9W`yIWpE6Nedhp33nK7IXBXlg}wBr zpGwrhWcB00*);OJ-)SnO#esFV{9F*^^!^qL~N;7d8#!o z7p1ej=HI$!VT>hy^#gzZ`lhDdfdO4r)y+G0jC}dBOXd7L<=<-TV_UA8_M$6L!}ulO z)~G>zVy4Ne0xH_?$xMf?IpHt>3@Q+oW2+#+MKm1+bv3(8`oB{S=`^#BN{fs#?=#*z ztdjnP)_OgA^kDwQ9!)N-ix;z^qm6OUva`$1FFpH4^nQSNxfxSvNn6;Fg!azPx-;SB zKX3nPa&~fpXNHEB7M5*Nshnd--Ap(ux}E#K`KrP*YXy4(5<^vGc+5VL4K?}lbUYZR=#oMwx_Iy2kjHC+v>F4!5%r5Tm@|qA*se(-uCEAu-W>GcTKIWc^F1T8|hP~ z>6*$h7WcbfxR$0JD=e>b?P?_DxfQGsgYiPE!yGn~L&_wXnx9H%R)?(~elcfZ{>RS| z7;{10xt?@onanH(7YKjwZzpo_MvsvMbzb_+BY1QH>eX?$JV-c=tSCki=O?cJSf|2>VTUteRu z_SHqoFa~f59xAA9fe2%ygMlrU22NlFL{2VHqt@j&G4!^lXT zC;CMEb$<vX4B{_P3ekZ8Et zld6vJ=4Cn3HSXgelN@Q!+Y}9^CPOp%N$tA|Jf2fwrwYbM%ZSLrs|mhxy*#WC>kXc-Gt7E3oq>(rDqvaQWFF7ZkiAsV(ChKQ_uoUj?`g z#4T$hqn+P-h{M|U&6_tfFvxrCN3G}++DZ>dBc}Y8^B+q`DWvw=`DU$uAs~OL$!@Fj zX+ip*H{RX8NxZ+`a!DIyto+ngxSy6)4~bF8{%fq?U)%Sc^8xu&Xal%!|E~X9{Ql6E zg1ZU%#_R6gnClWqpvR0JwXjGm!HQ&qn$_l38JS!d$kIx$m_-@?;BVrn88PO#L>#48 z6t=9oGkni@SHngdP%O7LsZX&Cv{Y7}0TRQ9BLS3W^nCdXf2_8cC-(BCAxClh0acY3 zPpnRS#kK=K%{FG{6H=|>2M^9+uoQ-gUX86ohiS!)3xQpI%3T^>C9!f9Z2!r0{H~g` zlIe)AM(%k2=rk$yx0fNQg$Ix&8T$#AN)axul!Sx|1Y@D(Ln84sV}=Mo^`r>696H=5 zoQSYaNS{H)d9Mo1PM>xJTByf()Wwd zMe; z{NXQPFbl9^9v+^w^mG?g4mc#cz-G`NhKJFpz)mKy(ena<$gQGLTy1otD(bEeVkhs2 z>S;bZ5JCAq+pOjOHin7A?_&)X&IAx}-sLxU<&s)6r_RbipOw`BoTtcJrQWjTrt8-% zmj8$$brOG?o!w-e>+MbXyBy*2D6T-PA()vsAbELtVd5UMhTYhf!ptEF_4M?Vm6igE zh259`EunnUOY`ziM{Qs)UAW^zleIDlnbhA~?W-q8-CMQB^dDhX(4yxk^~FPGDQY1r z!Yp15`# zZTJ`Mb^k~EG5+fxP&wjnza^XVc8=*8N1Xu*cM?wesMOF2W4*FtHw9AvfX+cn5!FyN z;~o-Sh9996^%s#gX#C9P16m!%SHI<=w;(^rB_h&XRwkW8zk$StY+8|&WWi!rdUA3B z{v$Tt?c2A*(uKwzh&9OTKs*itWDNE63Xq42e_S!fhz2QM7$?osSCcxgP1GWmp(DEP=Aai-Q`i859q2V{c08s3`e}7c135MM3qa6pl;szEq zXhe{bIXjHa4P$`EcaYGp!7IHi2^TOeh0fKDB=X+gCY-5Zrbe&u;lV3*z=v(zxRFMt z_ThnjfXU_O>poU58mrjuFNT*BrH$#yhCgm9uglN07!*TaA0K;1$BguJ;Ly2b)J_03 zhIgslxtqlPFs=;Ad3Gr881cbsip^(X@Pz9v;Z-CEGb16{j|U!BGLI3G8@s;9O5&#Z zgEa7W=LvtDz1)|Id33_{abjZi7IOTV$)vsWk1sa|D&U9CCL?Z7-KdHR+3LB+EEXpC z>E+aot6ccs8#ez6YF@rQ|Kna>{{L`pBJMB}68{bVa)jbfNk06aSzQ13|LWhH)|cJqN4hi8nK*QVDT;H{#sO1+cDTJ)hV?0h%h7D>$}{~Ew}bEu?koTaIU5% zUBkYh@9?y>`I7Y9D?Zus!`&Gd?)YzxPv^Y5Q>j}*@s5^!@BS(Ymt&v&{TT#qb%u>- zHPIJ!bD7ZZI8yxe?DwfbiBDRFhM)E|?mL(vbx^=wOLDTQQKL=rr=sa5yqg;iV0~%6 z?`R&~f}cnjkIXD?{Srl7{}{$)@I%VV_5c6>N3UE6|8=#s%}w>l)Ig|2Y+5?!12H-U z_P4f*t(%sTTni^ukaXK|WYxEa&ngGHcKm9(WFf31D~lXs(lP<@$Wc#@s4z$xXETcW zM@Q?#m@s2oQ(gW0_vgh!h+>5oEL}GXho&``Gb>Md`S{SxH-LQI`2Ib;2MZgU1!QD+ zZXK$+cC0^XDI1*iRsy{-m>Cb?)V;^=V5vY2coJYO;^8*b)qT73LSI!dsIQ2sRFpqB zR56lo<*Z>!&(6tdg<%9R03i$57ENk9Iyx#SQ1loe9H?+%cDQ7w*7nd$Zy>Dm06t!y zsNIhxOpfV$SC^Wx@jI96ct{^Ue28)&fa)h&jBkB?<2=n{=gF<5j($UE?fXxkumTi- zLF#S_Qv%X@kVMpnY$vMiqCTed@eR{&_xLG26uaL*#Vj`R{4AmZ4u5xDe1WqTD#oGX?xyH(=GlB0 zM^ys(61L+{0MVf_2cz%SsU85uhVDp?!xAM(Pld^Cr( zcr}aw5W)djLGP2nJ$%1y)VPo5#>fW`hQ57+w?p6E6buxcN?@BC z>g$_?ys!wf$Yx*Pvqzl{vUhpY@p^fB&Xh4PymWbiD~XfQBvL;SwPYHGDB>vy7N1d% zLnJ~Ht)spD!O12K*V)Escx3bMAOwf0ARP!K+L${Fo;tch`z+{u`L`U`@Qo{c#uC%23VLe@}bk}8fk#G#P@ICt}o92 z5*&78xK(0p`E-Wp6FhlBKw)%s!zxdiot0hBL2x|<9EKb`MzUV7pS#yErbgFFq`BU`R*M^vhHat?aA3Ol_wt68c zElnw-6}g@8qjmXptZaZcz;_3I=fEAlW7Cl@h*A&qt#jqY{AgSxF4@Xo0P4)cgX3m14lF$npjtu%#aS1p zZmaM%3b< zVOH{OU?2&h#$e#F-n@1fSdQDT{Bfu2>JE&Ko>$Ac_TvD}t#?%HQqwn&4XcW^$T44s z+vmak`xnubL0U8V@1W3Bep?%DT4ZD^K;M?JflT^`biR&{Y`js%8>%+~R5Jc_cPn^4 zsE&THcHCjEF$fjQ#wIN#1(&s_1T!^1@F~!d!61BghEmkQD|nbow^P%}q0z;Bz>anA z-a&AQ(ayF)4&2rg2VY4U*)B4i2DP%5b4aACx$r~;|N~zSDc(`fOt$x^O=8^kYI`Xn>e9${S=h$c;T4w z=+PbSkMJ%XLe*w};L%}WDOPW`D->b1{Ur;N894Qh{rZJt(#_c<4VGC``c%cbW9u*=M0afFeX!M)3vl|ks#+r_zei+Ag?AYVav$5z12 zJq#q2ZOQx;5om!5Kg^@PZ-dm&B(?YirSO2Tu<34Z{-McPJkhb~ac>Ey47JPbU?SGA z#puNe*Lr;9Q5qa+eM4W=6Nfi~=bI*s z2Zx7G;#s7`w~Ac^Ix)bOcRV~UwNm6$ewg0#VZs4f1xXhRTz-^wN$e)rQ11>-P+dLO zitdhM$#3i_zuR6&Xq(~x1@{1C|Jm8$jERW}_$20uquTcUDqUj%1=(U@KPHQd4=Dw> zNd?flm*=Pa?En59eC27>!ANGp;*Ep@g^kLGmN*BSb19Pc@QUAmp&NJ5>BkLIn2(Pt zk#fCx^X4=HFJ$BK4e^VKh0RO#?R$67T$u60Ro=P0jaNyNtB9w!XM9^Unz}~l12yXy zGaaydguM}?CH0Msdww0qg^}BNrFU*-VkD0sh(od(aPJW}rSC^kVPs@9;;QjBYr2NK z{EYl7oJ3IfBPV>J6*n{}&|mplIS=4l9xd6zqmyo6T7o2Vi+8#q=sZb{zug1bvd0Lk zCIc8eGfh8Cb*bJ6q6$GT2{eHQRJ>u)@7~GQQ!h;qRkIm>!je4c2M@ z?skf(LQC9(ia}jl+fpceVa(695K;h*97=$ax$$c7osonI3 z`g8&n4B{xvU~QclS-MJHqoWH%xhEyHh`5KmoJqHHNTN?^)oH(opR6`M zYG7!H#sX$50V0m}r59wuuD94uH*=rJBv58=%(ke;B4UiZ9!xS!ZHn*Rh&I)t@#3U0 zB^}QIcJ6(rH_y=ipmTs?0{xh~#=pX5{;gb`ZD1{D3Q$^%xHMAYashv5o~l!*zZb z4Z-6s$!keUhi0eRZ6U;K9N$*|d7vah0dp8QUBR7=+dtZQC>i%8ZpHVVM^@Dxh8Tx2 z0&JZSXl%dhlDusB`B~Wlx7694agPGAVx?@`@`crEuIvDbiPnzq%Qg(ln}~ZQAyHj9 zg*GoAJBc5K6WN;+_DsQb105fY95ZyNFyV?oTzys5dRMC2c?P#Ljt0DZxhKS%uPiY+ z<|AQW!lUri7=2)y&&7s6GjWn2So3gnr)FeC3z)86lnc|Q$TE62CDSwU-pNUYxg@^P z6^V^+yP?D&&ZT?pP83XTYgfYZsSW&L^@RMJ^JN|V29^mg=0?yo% zpb&_tMO)J{H4FFYmoHz?BqD^s;f#`dUS5Q)zg!UL|IhHWV>wo4-`bAgRhSL1r}y{? zN7Uk)qLM~W1NUx6wuK@%_|DGa`*Wv1o@SS|dgGGg4nqB&;f+9HQOe6T1JP-RZ zLjKb2eq>~=TNcV(%!S7{IN{iXDsaWPV-tvb0v6yBoLS@JZ!_h@#g*EK;$=^S3t@6I z6Q{-8+{@4s@Y>VIL6F8kM_UG<{XhlEw8q86e8I-jS8SMliahTxUVq#Yub-z4rVR{L zpF8&r#Au9?Vg~I)D-M7tk<3Rki4jk>^^|KwgxU3#DvnnfX%kF*=QG9_d&1VwzJN{= zCOn94^Zc2UC)@Gi7Ez;g6DB&nWfNUPXXl|a?^)v;tVAQQf6>#@ieT<$N)DANN{Wo~ zCRO9WplBI-8pQ;bktU|Ndul`2tP)LK1OPhsq?^UaAUn zHMOM|`)ZIA2PgFIya4ij=FK^Q50AyIw$ys`HsFXp4<9q?8ev8_uWW=FaT%$|ZZ}x% z5VZ+IHgZWxNtytC@_0|F>e;gvq6adaoUxJOLspC%{N#HZw2%J0%FQb^A) zzrTYnPH!PkJG-VTo6)@ALW?{-t!|9te$ov+?fIBOyq+0PDGZP>ViLQPj!r0!7#PWX zW#TlF^#&Qcm{{~0?;&vg=sW_SIz=o(MC_ptd&6uWznWggK8BTm0$Winvz?DGMaJpx z-L__Hg>;g$4EgP`07EVJ5D9)$8LYc%n%Tjn@dF0qtke`?W0TSYGTL#gTt*ANwt1_e zp-v3RB$x@%vx?let3KfrX7v)QM8@L&2cOh-m}qPXo)iyMqwl+0epg@6Qp2rZjiPmL zLa?2nfayWdGlB%@`9Ek`SQSiAYttAm^ZJ_!UOy|1K+mv8y?5HNH8q{u$tfvjj`zVR zz!4dzB-IX8)6Blp8*Hnnhj|7x8({>RPf* z@T5WJ-1eM^4aeCHk;ufzTrky%-~z=c;=5Fu_rH2S*y;vspJ1PZ5x0avZ}gMcY+^=g@pwL9s8x27fiD1DIG~ZQdFpCe)0YP>{8S)Y^>kk>?mDc~H_Qze-p?kM;9p7BtzO0{i4d(% z5)#c7JRkAn!*y(oI@^a$)b1JOCd8~&v8w%EU&=9$M~>gP_~5~V*B~7Dgk;_gY!FCR zv1W2;@}t3u?Sp3%$J?omYcsz-w|YjU#I6Br=YU^-U1YT+UHwh5E90!gftJlwSiBHs zhjubbj0gG_-eRlTdy>%Nw$l4P^slCo{#A;U6Vac$&}f-CG>GMO?YedCCUW<^WghDH zuc-|>EA>%RbwN$9LbV{${N?crGDRt@vg()5RdHEcjP0ckF2K4$u@oDQ{S;Wb%2UMr z7xrQ7cNnL{5def;6e)O)3NbaIXW<_aQ9Ve+hU*cr;X+_gkQ+zDh8vtLAgnm) z(rVH6m~uivK?nXYnmbFe;WFj#KYe7=d+xB(|5s_e8fqyy&^Pp9J|DTWQ4VUjF!WE4femsmH5rG2m zEV_lipIecP#6SH&`68{;gSJwTN*cI+SV6D_;e3S|H{=UkJn>7Kv;-p@@Y$e&4qMzt z$Q|kF_lB#;_EM))mI=W9mb_$ANl#0oqNF^DGZJDq^4ZaJK$!)p9?dUIH0fraszmXN znaWnM1hI92C{xL`#zg>I?O^7>CgXb$ z8jfi=`~#Qw6cHMZOR$uz#lp|O$@P)IGM@w7?XH2UR5d>K?!L`W*3~dP(GxC8{=!zg zgM#$obX~BMR`7Otiw{9h65(@aITRqP+T;rl4trV2~gkhR62`fIuO>#N^={qpMe%+$8J(ql`o{=EHajlA(>P9Fp+ zsC?zlgH`d7vC61&&y(Fn{jxfb8T*91oDST%6sj3@cI&tETmjU^Y;~IBb#T1J)F0=n zPDo1!d6FP%6v&$DF&bX`VLi!i{XBP7lcm*jcLm{hy;Vl3L%#BbUno-5=&mpF9kpJ< zb7)vWTJDresi#-qV`YaK@93Uno`~${D|imdtxxRY1+FmoC!HK@F|>M~y8*vX?=NRs zinFH_Aq&nX8#D4g^>2ri4-o1Cv zGL0&iL7(`<1f3&00CbMN=DTt~A6aQH(K*gid|S#Bpj`?g2c1I|X4$hE3H4R)mv|&z zcC?Ic7Qr_kJr6$rNk%!Z#r-3da>I%lFU{1LUCvabdyC}WxnPE3a=gdY*SO{lxhefg z0O23~`JvZ`EK=Y{3CWRc5h6pJzc`rR>zTwym_U9?KS?87g+G&9z2k?E`_fwavVh^4;8+>fDQyRU+yf z+!CQ8-U(9bI6AyiAO)@-?J;72q1EVWEdoCj;xk<(zJ2|gXI4&8TH@sB3{<}upK6iA zk;Vv$G66J)fYcrx96V!f?L7P>56=mRo@v4q+)|h{c$1=)iU_G3&^flZ$ukjjj;SX` zr<>PEv?r8y4-LUBCWOZwXi>~u4p-U}?o~9UTbr5;N?anpt)TjGnn_TOAjC-2id_S+ zfvIhH;ucB6)>tmgasO#t$4fdr9NXb~(qCUdW}v5Sa?&~G8*mc@5iiqz0T|3e7@kd%b7jJ^fywQvO7SBX`i$7}YJ2jC z_}(=7?c7p|VI;@49yfg%oGSk8@w$)2e@BLwxXRNCmOrm1=p0@tZfXtqVBIVQ62ldj62q;R;2aj@TS>TXHVqlry~*U2tak`K9H_|? zo1!gTWUz?5>H;FuAi&+kZh9XR8gD^+HBwSjKQKp%T|AoDh)bm`w9n~|GOMCY?D`Hq zE12hgJ7oOP0`w9p<+?v;_e&EQ!srTrcIxqk$a87k<|RW+4;#7k(n!+7*# z6=&0?lg)&AZhYB1$J3XhFKWeoCFUFF%@D0DJ89Y}kop4%Qh(JFsUJQJ@kq?zoyQ)C zuM3_Gd$(4Pb8)PfACwF7xsGMsRLaZ9IMF2JJ-7~5y?vHSs55ZtKnG(?ntO?sAxh9P zkO~lDr^UNHC-`8!L(t)WXc;s_VmJkn82-GVKt@)!1?;$`9rkq{74%;sWhU1y<@9$@4W7|M4VA7nTl|;_9@XiT~i8-N_iE0QIKGp+KW_O*kh`{oA(}yjI ztAz|-@`)v_WvttF1D?2z8J%lMt~T>N} z&w9Q2i@}MuJ#(hOevlfV{+t^_?=@&8azg#J3LLd*FFg@i0`=SYBnSg9-%guc_)kzNjIP=a3kJQgRXtAbaQdr6AF36gO5Mc8$G20& zItX}&^`8jqT=BAXj_2WBG-q3PD~xz;n;q|Z&XR>xW8ah=O?H$-T)0fuKx|Ud`sj7V zgfq?fPv_jaCFh*m9xnpZf9f*JLC2t=Ux>R?4c=v-pyxIGw`^Cp2aupytWwDRi@tav zXNgQ7oJts!k|iTY3brva&LWQmXljK+3(k-{AbfxfW#a@%ke)wdv)A90{!-5$FG^1U zilJn>js46${qsk^^N@M({Bb4o2VcB+*W&u--ELF#xyzt>^8_C8IIG>Jb#mh>X*!)p zc|z1f&LAc9qm3vO9IyFj=o(-_MFjT6YdIjM2E}FDKjD1wg!;s3w1ff17;cA}b^!RH zwB*XmG3x(tITQ#kN7`$G%dw9)_0-PkP760Cbt^v^otv2^0wGp4LFBDWC5M|f+T>Q9 zq-j#$$T84TR%Q<2;2{U9+`m+ro@t`Efp1Q+b+$?4gy%=%a=;%diK77eCch1a^Yimz z(L#V%zzOB_HCm&~Tn;|BeOh+vy^Q7*FEUSEia4XkHu3ToANP0F^eJ6?+Vup8V#PNX z=U|em)fuPD^SI+Y#LfSfEeb1!?^+=`l2fJ++>zSkrl>l+T;?y)eE75th$H@7pUQxE zuzgIF*hLmzUf=5|Kz)(i5uiW8=AqJ(syb5T(ZSBmc*E)v8-e9`-*?l1Qu%!>?|frY znWmm|#Fb0I`ANK%*0J>=?N7i>o*wH#eU2<_euMm%dM9@5*0d5?kH_c zAFf8t3PQ@#8$nb!rumWXMHx@Ln4H`jYIh%6DcT}xF zIi99r(i?hDSjnwyL7+8WuhQVzMafD3;>R~>Bu#5nc-9k84)P@^hp@_@t=)HGTkb7G zIWno2p&Y_@9MC|;+AOx?UmhA=Z}@RBgrU4axl8b}^JH=WIhW(>fSl4D6L8C&vaqOR zqN%St6XCP7?&1kaWu#8+*>ii6dBFZTN0gHpMEs1RsRu7!Nc0%6va&+j8#l(}+RJ=r zCHAdDv2QUY+!rM`$O(7*O7lSFz{54B)ip^%bXN$*683hq8qJxvUcjI)R2qmDI1py9 z%k+8*33IQf+@~<6ZOXP>RAp;PNueLSNK8yjP9A)c*EuvgsshyJL&{>C9(0CF=D*pF zZF>Lf$B(3pjMF8DoPL-fmLj&^t(0f@r;z@)6RN+rSV#N&`$JH`yUL2H3JD28?;5(p z3{Iw)Lo+`o?00bdEs`1-YJc1;OMii;8XX$u_&dO~V8ZwDqen40k=bjb>0MJ~q_ChS zsR*PSyPLwA0P@V(YC}p0dONTGRMT^yQ##*0eGeo zdn1fJaHe$k^q{YYNtloI^ph?&jTb7BQ@xS^IMp!`{JDgqANA*_szI@LHgbf=Cef z%EW~ExrG2d+IcX62qf}W98$5WuG{?Dhv8ngP4oJXe*ijWRsbD5H~AI|X0x)gY%ao= z0*1x=`O91`Rt3dI<2@D(T579x3h(75B(Srv0Q<=b2N~QkFnoq%ToFmOsQ;eXes)1P zrL{2+@D^)`kY;qn$!_mP-y$=!{?jKWbI*{zRCeJ(ujLPc{O<$@@(D=g*IMnCeF}Rx zC=W+U&>Yd<#rX7;w41}Z_8$5O1zTHNb8~YTl5}K`I3sqx~@3|!;D;vo(9vyJHf{rf?= zfV$?OfyE32`JRQ_Y~l|mZ&8I>C)KB)yP5+IM^=`gu&`!g$ecCd|+89hz2{l13me@9kG# zU^%NjN|+92xRViw`SIDZGXCl;kwzY{pqb7rugIC3+j24mcXOO{ue=phgZF8XX(H;>~m!xgpQgo5W!%ErE*)_FI%FI9y(e?MY zJlXjBFQ(jXi68G`5N) zEKY*3K_ipV`WZK4dwax-ZPfJiNLYuLt+HgF*Db!|Zh{&QpE#_$#^rVA#eD5KDzS8S zI+EjPa7ZG{JIzCb$nrkG@0v`CEN||vw52TX#q(U?2pddnW{Vu@D9|XnOTuxahsg3y z?RHwu@)lmn^0r>~tR|5y8(HyV+yRkfQKl@YG;gb_oX5(yp->@ICye|-3SBU_8hyv9 z^rFZ5V)FI($#oJI)Fv8(-)-75u`WcPn|2y#$}|v! zjkE_2bpH@G6rX#|q*luOh8~77>hx#Nv=bW*3+ya#h=E9NUUUxL16Z+PAnOHTV?dfv zrr-w14pF+)p?2egOfsZ*b0TW zv7`Tq61J#_$@LNbDyC8V{Gi|$W5OC!?J!4R3o3|}`Mt&A7P%efqA{upTlNf8U33J{ z;Q=tyv`biO`pX(VAJC5Ze4q}NS5z?iRsi~g`b`oY8%Vci5hHMeFU-xr(!j*TG!g^I zrsdOuqN3{ZnE&`q z&Qax?0{iGB!P=lUdxdQl=yB__XVK{S)YKFd9$r}-!SgI->gzVk4VC zY_ur;MwbguslXVaP)3SRqCyxs0oYX(J{*im)UFgJAvG*6O8#Ma-Vg%dM#3ud^uslH zQo$jBc$D>d!18u{MrBDNpr_y!=Y&Egxv{QXS?=zU~+GEK{1 z*_fZ7w{UxPJeYZ#yS}2OT+ULOw~8eVU2YqNvFWC=Fl7;oom%B0imPLXwCu`x@AYPG zt{pGQ4Y1G-X{O)DpQ>=d;_^Ky&ts%D*+c942=&u^3w-#8VDiN19zu~ZUy6Hf16i55 zpCsMp%{Lr3X#_F1?q>gjlQ}{+vW$_wJoY%$$V}K1%bjPHb7bYp2X=azOg+IFNf0&) z{v>Rq2<3EKA?HRb%sVdN3C&KKuK^tQRadAN)EjE%v2tEn&zDeG*TeB$c&MT+NbS~-Y z$k5EVfn|XIlk)_?KlTQLvD6)-Q*4YTQ_mET^!LJh?b@}d5#k>|b^~DJHm}rKC+}}} zS3b@kfQ_RQNsm;u&vU!205-HV3BZPanGkpQX;T@P%IdX*xC)`lNW%IG|8mkPr<58) zXrHtK=0hGGgdmyLI{YqD;>{=v8d>iS{T_jjV($`QgR#?0BJNCNChrnpL-G-&3XI$E zRHEv!%()};(8pc=vifjlJkq@XHlsgt+;u~;RpzOMiBLEujbvPfA<>U!-PriLGLS2`>Yl?j|8-s|17J4MduEm5_VYQkpmS5RvAca#ll|Nb|m*RFjLs zo3pB%-V_#w|9kCS#dRBpA(ii5>UmZAjy{7SYCcKvp|(l1BGTsKy&tOxR{JF;uh~cF z3?qdo&*arD=x1gr-W|c*&Mw3dy3=nVW|xPkeb8@zMt>cr8;aB|YpO07_~~w3(2wst zDR4T+vf-y>Fd?EJc!wclbaUXvNV`EeRF~T|BPUQYA&4;^9%yil;NJzK7>hKD2_J-c zsdg0G&?EBm#bh_A=Ds!)3daWoXao(rDELQJ!fM4NEticu<4i_(~>2NEC5~8H8%OPU+Jg?*e9lIAn z?=L9wd~lvYWQE>uw;lY77S$<*oo6--itK;R)yx^H_PToa!xxXn4I1rsL;|Bk!V6NSIgL+T0L4=Uoisq;q zlv>ZplS;+f49$@bil`_QOs~I!yf+}o8;_#jfP7a*4H4wsN(6a-UkUPdNPNe>9OO;9 zKHv$#*0|br_jHZ}zw4@bRqoUyFYPJY?vt!L7~i!-r2fJGSKIrW%OiTMn-|3X)%MO3 z=e(FURCK>hzK^Wuu$}f{$_rd(yaRUU42tZ0n<}g7EO1Bq(VRYYDpP$`q{=VAs&Ihs zfid!uqhsEC&XLb5zh`Ei=w{ve!QUCV)Bow}dz6|PhV?%aN(Gc)v4fa()!7*(SSQ3m zlp1K5bNiN7ZG!7I%epzm%ZuV0$QmGFtRu)8*Hc^dCSTS)OU^|1&;8DwnkdN^XxXx{ zl{*W4f`gVY%VJ@GC++la*3j4hCu@{jxd>kYD0?6B2*t!>U34|2&zf?`RN4#H%J-&m z?$4A7!Q78o@?Xxl<|TD?$=spO{H$H!hoH^11&@47{6%gJ4ipDKreu_zs4 z<9`I_6mW$_Sj5C5?@8JqL4`f=OmzrvSj~?UhYy9Z!x9Y7>b4LHW-rD}{*6~o5N{Bg z78-^#)6)mxu(h*D3W)j$kq%pYZ_QOX+4Cde9b)~37T_5r?B44yqAOt1P>o+4@xPtVv!|HRmc7zG)j%RcTG zeUu-g0!1GLWBC}9W)%8_V_Ut)>xv2&%tPTk*uOpJtwmC4QvNm&X|XOpyF9Etdb9ZG z`pL3@DlHH{mcSZz2p7Mhuq$^d#anSx&L_6|r4(;kW&Tzo#e4L5VC(HiyTf)YD4{wNhP;b9EjiCIrfhKS5!|STxQC1g+gCQ{mL}4#{8) zB4ShRp7M@)&@ed6cjRKtI#g=4X(z;(pKxABXaul;x8yziJH-G=c2{4#u*U@fr!mWSMrZfA_hxxgV;l zOsF{(3sOQhd{o$JC7}>wp_SI5>NaoQpL*(3C2gS{;tciy?VX~Mh?k8JKBJB!C3<{@ zU6ZB}obr*D7k#RYui5NsM6Y9O?sAGZeCB}>5tvwg0p{IYJzfpq{@P`5f6!wCt!lD! zY@2eJ*y{f#T|hJ5+H(7-w18y-YBAV^@l3$T1Qxu>V!N`M)BY^wV~TE{4X$ix+xezl zGmWbeQ7i~E9~c>-3I1$4d;ez>&io-oW$vGKXaIW=GK~0u6zv#lPMtqKS|!I_hR}mW ze~H16BB#Ua9*Cr(sfZM6AR0$JvNKRDDe@fDt|ALp2a8)*nX(JiObl2%8g6d zeiLB(%Lr`0(Gs?Q!wz8k0|{*Z%Fhp{ma+ZYma+YerDMz3ek;1=S=K*X#IsB(5eix( z^+qe$eyy6^ITc+B3W|Z}0aWyvY{^2(Wg9oxa0S70yBmSKz$$VaJjBK(GyckWAR4$|WrB~Y7coX=7oPLcn2+@0ox!lm-2D7WeSHj&99tlf z;Ej3yQ?q>IH?~t+qmf$W24^?S^NO8cwLN}NSw?fOLhMPjdQ!Bze_K2{W~VMz9Q2yGMH&Y)kC_GB*lYk_0jvNn+A#9I=XjxIseFqQ#4IWP-V&DSH zT>1|JXra;OCjTHlB%)rz`&Hb!Ydd)gtCLiwrH)=c{wK5F`yQwMGP8d;IB&D};~)!0 z6ng)fUo+pq{D(4>`npk1U!M_R&{+p4WIzY)J!@^pltl9nQzL+Q3)}=#qg|<*D;&D1 zo~Z}TNArgN2=LyVyXNGRaXQl6pfd93lW=$)xWxX|6D2DHDr5}zyc9bGhYxa@tKR1x z!Z2hUaXG%iZHQRNz^JIYL#^U`e4}sg@4>^~s}VSSI9OttsiD}V6#Nh|=3%9LJRdqN z^|dPx{wS)hk_bwbq%$O4f=G;Qp-x9?KoV1%b#NXho#NiS_5+vtS=rg2<>sOo#id4% zg(722oCW0&(h3STYN}o0%1xZW%Z(Im7zHvR6qV+B*MhCjRoIx4u+AS>R5Ulnv9m{! zzvxlRwaVDQNiLcf<3MV7k?s0u;7K4g>{s%;6^Zpc9Uhl7n1D}sm1D^5WD{INf;L>?oLUGx_!EW8exr6XsjGVdc7z3?4 zayaCZ8s}fc{&9}DxVGliOT_-t{_K$LjorIFrb$Q)!{v`1(R|7})PE#fT5BbddRd7h zqP$axDDT6G`BD2$tKgF0&|qTn1@Z)a;$axbmK0mOscy+g^o~$&mnPwbKQ%N6T2#?2 zc;l>zlL#@R3v;lwwKc+X2oE8~wXhuAl(2+CCN=ltcIBzK3lNo8vb_6F9Fxi7W?stj zj;pmGvb;Gxy@)Jt-jytGdyQwDtm3wODU8irw&Myh8U`r*;fk7vgW#^8-L9cs8T<6jf>*)lZ_tZ%W#bbp5>tt*8Yqb8zRa3 zB9gq{BFUTY+He08Qu9FM~rzcPlgV=jP^IvvRMoo;Ukc?({6fHPTk#8XICi zEyFc7uD~_^XzRAHK&r!RgNC$*lTxJ7^^{Y$s#cP`Ar#_UwOdD;bLk^{yvL;ujT4Q` zd+Xl5HJM-c?+N#9SW*czCEfus*GY^|VhGQ9ezH~Y9k)ohQj!M?S|OoVcRr5y4+s## z;)_RBqVK}SJG}eit8{xKF^meyatDsW*OgxW=Xm=|t~x;WAG7YfZ0=wHFQ@?3RvMZd z2+P?*KW3?7bYF>?Nz6Lf7SAN$0LZ*JE!Q-LLmylwBdyC+ zu-!DzRl_VIjbu~*Yo`4`i^d1wDj;J69YG)~F=L+Q+kanfXE;Ruc!on4+Ag=w{@aZg z^h_h<%hsL~xdU$ioNOkqn5-61!b1tCf>BJhKn=_tQn>$HEVm%gk)D2Q*0ft7cJYGdqEOQWk3nvM zk1BSrI69-NzOS<-mnm?mBz+D&-V9qcfg<=^^0}=I$?XT>)NnUN>5ZyxaDj*ihs-oX z$V|)e;E00XvPBIYfT$=J%q$_no*2%}&b9-3AEyHd3)H#%bW5KuvI>!>KAUso~Sder&$((&^ObhUnKJnw*JBYSTLLMndG&g%Z68s5K;(RZYXuJHN? z%V*}fBOe`J!(iBOMnY@LsKQ2$p;@-*It;?1KAHY%YHS?6VYKxAv~%B1zjw4%HP*W? zXr=c&UZV0trl;O$s+NOC663!`RvuwHs2MZJCzhsJ2ac7}@R?3Mqqr*Vb{+l8X+UX2 z7=1f(`Il8eD05TgW1>WII9I7*W!Evol3prU{6MxOlrKfaYOI%3WMy#0v`I;;*~J^t z{;d=KA&=_tD)U@^xdY$bLregJgmE&5ShH)^ByruTZ{_|4E`qA1cI+1I z*n2u5{MzrAr2d)B_^vR;XwZPxTKw#>KK~9U-noOpRxSf{)n+^t_XS+`%0TG<_5Jg& zsi}Sp^^uXmaEfJc{JT$F45hkh4PX$TRz-WUbe(ra%=o>Pm~o{|(;BOWyMmxnUZ|@! zwq8|g_)1X%?OLhHd}@*Gg!H2GOx%GXbOTq1dyBG$P)7gM_$qbUC)P{v9{ zIYn7+1kGniP6Ye|W<-2Spc-=E{#>Mg@OvLc<7mFVfpxTf%7jYz+BVHu3Fzu?-@Q9s z0yoNs;_EL4OcP-}z;rj50@}+blLbG4Cp2b7QQy2PzCIrN@$is~T;d|yM*ih})uA{n zl#?Gsh6pA3+Svp5&dz-+L0Qx?tR!n1w1;kwb$OVwOl>^ta}A&9NyzW`&0=K&c%nPT zJU6{MX{e?VL(G&_tz)B-m3MFpj%gg^K`el9?z(MSYW6F-5h<;d>`R{mq;HGf2Be!U zSlczY0MUW5z|<#CK%9oZK`;M87e*0vZBHT)4>ugNx|s@N>e=W%hrozoF?14$SYp^UnP_Qq0Y~vR$G9>fNkYGfP-1`PX_Uc_HOJQW4f7e% z2rSkxykHkXs$l^(UJOM7pLpJ?!R1b*$t{CtHhRcUB*LVbP^aR3^-YJ6ng zg=r_<(anTc2s$DF4cKG$i-;hXB|8K76=I&c!xWE0lEP*>GHWZPoG_pJK}BhVFgW?X z80|M4%arEO%fjd~BJ((ZH$o32Mlh@SOI}B>4r6gmtj9tqkt-PKWr%Oi%AS$CP)OtfXZco|91?u!?{F_HT4zT3m|HZvDZlX6cv5Hl_U*+z+i6!AN<=+(F@Y`g^hQo2tClXi+I zZrc<&q@_;p^Cm8C>BZ^_tYkjrUz9#ijCZD-;W$9Zh$+YKv)#_oH8|v4)M+IQPIjA> zFgq(NJr!Ae1A*&@x5(AiRnJsF{V$yq{&LQBDTH+Yzax$RJJR^SBaQz%()hn4jsH8+ z_`f5K|KE-@wrTEI7bK=V*?D~Ak=^Z!yAE*2kc!b}(b*S>rW+N=4xO_2nhp1YoEHeE zy2&?G60df9a#+@cjeR_uQSjo$$ul%lwuvCGX{YHPncntYWz$=|Rc}aBTKWE8K!aYv zvZj6Oh>0qYAW*tZF?*P-kw_+{DPI;h52rbsoNm|Dg{=wUo|f;T3;`zy2r!cjABC0G z*kz^<3Md4_r`H3jRpk%nf|&+RSYw23F#~8)J0ex3cv50>GXvvJJrC<=wbKsT-o^qJ z+?6B}Rm$D1H@K@QY?gJnURrO4ZGlGK3yb5SC3jQq^7_9R?VY@lB5yj0xN($j#en;P zoENF`X6G|dM*}N~G-xHuX^vgYN`dg6O%AtGD!Ofw+0eGlqfpHjZxGf9@Qz^4Cun&E zLYnd?&S7jCZaf5g^E{8g@UAix(`q6)PT_o}mx3PBnw$5XCf|C)YV}^Kd^@c-D?;3I z9zfqO{$!Q}T}M6QEve;ZZf*#dm6Q8ibTuzF_NHXTDIu`x%3r^(Kl8S+URm`qOE@OV z#3~Zrx3>dxqzgY2Oye8E&!aIpAdNh$5>RG=-I>^+k34Nt%?@uEQBYmCBZKbOVdLGH z1C{UNGD=W9N5py;@R%Zc+Fw6Xag?4IvmB+i3h-jnZ1zeB9gc>AM98~G-fvdg=$aty zP8E)-`|@Rx-|_H*VNL*byu2luU#91&WKNo!?W8zUntdLTyt)w%ZnF;V?0}k~R(k2@V=)PskVR1Ppkv1*g)7r&IqiXh}0NU+zbA^HvRlge&a zU*!2Jh0a@i#UvdBYN*nCLSwrnbGNc9!SKk38ONuV8xE1U&6=j_A0HFdc5xyi!2d%@ z%;wTQpH#XaIH8q2+DlJJHa386}A)y5KF`XKnr^N_AQcPS-xr0=g1p`?D=P zQ_^}gZu@t%AIn)*4e?b|n{5c^yZY7Llpf7U3@`#zux{;IjDjzv0YJ8dc#P?gRcpx7 z^m8dhj<69QR_8!3@oD48cZl03VO-Qp)atC$(zvL|k*56Y_lij@A-SX3G=&OO5KhU~ zt@EQ7>;|naJUpEBjRw^ger==?m<#(f&T1Y`pll;u7ks#EW_9KbmA0IMUI^a`o%G zaPUCEus4(^qiPG#3{Noi0H0U^(B;h-$paW}>(1za0G63A{2njQu(J$BK3T#Y-7<8} z9C%b*>;kHa&qSQU?qnlh6}?W;isS{A@(^?&wtZepP1ucOk^##j1+0qm@NL%2%+pZ3l?tj4`>`-PN5O0ywTY1Et&l_rf;R-*=$ zNGfU2q(Legh-MKg&2t%=ODReyA<`g88W5V$@SfKS+rIbS_r8zkIgaOe-uK=9u^pS% zTGzUU-|u^#=jZ&6-dyq!lk?s_W(5&KWW8M!I%p)t;A1eUcJ5FZ?=MZ`Eod5NA)3ag z&@_Ivd3CZra)r=Ynxd3WL*zC@sqV7TV>15yL=7NVa8>fEOLj8>!dlqzhl1Y=1pKl|FOt)&w zXdtWEUWlb+0oQuK7$7fR9pZ3&h9M6fzHs3kxO`|8E2|syue*0uKM|SA#sr}*CXH9m zQyf#tnM%YuW9)urD*IM7K3N@_JJ+&4N%L!_5_;_mqNtB?`EvNGks3mZ2@iXR2O)=iJZ~_wdJs9KysyY^#rbR4${N3{_gp?!U`PD@cKy!m9bBO7e zvoH23&tGPrt8_2Kct`2hvxgGZwp>f`<#3JT(>N@4IG2(2dYz^qQ@c~y*u(^AUB{sI z0V4)oKch-AqWWGbMruC;7d+?kz9@W!8p+~ zj@jD-*L&}In8*bY)r+;iEZ5N)wxtZ#Vu`f3a5h2@fDxnX=QYoN2J;3|?Q~RK`q%%a z1wK441^`Pr1~|gPj+pOQ!P0^MMe3YoDl&6d?$Tl1tFZl4>-4q83ZEeaoA|YuRzG zQA%}N&$^_?_{Lu)PSaA}Jxq)Vn_2Zv+eXjD24TPXjS1M>?Xi;PSD~Ml2;63v<)LKp zbrl-}1MawIf`WpSLs{Va6ciC@>FnGF{SpcL4iV&nL3_bG4Npa!=!Gkxz1OkXa^0G+ z6JeOyxG(CI=@)Vbx{Aa`bBCra5m)qA)5wtOG|mp!2o?D4{yqkITjaaEB|~PoU!nvC>BI z6OC(2J{nK16oj53bv4lHG`rYBE^=y+U2K0CUI;-)*2jB=HRHu0QTh<q1%K z5vdi{gh$%OhNsg!?bUZJEJlZ!%du|A9rz zwOoK#osGCtA;seoMTVqhS$-!Vi_-&TJw&zU$)+o0dVe=jY|^2{{UYJW$xhd?@+W-s#ZyOVCgbuo=AQ z{r!lpqL>`uW6RA%8yVo(@8t}WYK?^MI?ja*cMQc=p&kQ5@2|Unk3`{j+EuyeZu&09 z5U1$~8ErJzM_sr;NTC?;@5H`>7>$1@5us1Kx8i#SH9FMT`d~1j(;MVCs&j=1Yv-E0 zu(4DNBFL^y`^5|n!-8=Bz$KkJ11*0WAJP$3jrdE1oIUDU@!U?kw21rX_od!($r~M)mR}Tr(z|RQ+uF?O}yK4xFSWn zDzP*#cEsICMUS>kluwe&!Z0jL{jifnL1ECEjLn}`MqG$c{@g3S&H^$$@eT zg9cUca=w){E5-_TIv@j3PFIkF-ec1zwo4W!N??sZu6<*+9e`h_*@BLuC+`U=3gd6WXki?}KB!#) z=W+zK0dU5kAi>{cT7x?AXM9s(eYV1?JOT6UtK`V&r?ud%5V3=#CG_+=#)9YW9v*xx zbAVT-brw^)AxUjRAG)hR!@mz9j*f2l{rhn|Lc7_Ox3dB@%nKgwd|77Yn$DRwk_%TF zEMH8q!Ea~W>XEd0PiuK43MgJ)jIDY+FQCjX+EHIifZ}Yk@8)P^Msqz*MeIc1bcUId zU*wgK^!*fmdPmB4`4W$=c&fj`^R--zCtW!63@0`6_Z8r5L+(Qi5Gjj;@X61Lh)*0~ zO6@#yz4)>00I5Gg6a^-uZjyG-fQ5hPlMoS62(xQTEqqkosMbcDC>#3=ln_sGS{U=* z_T2V@_4GrH-X3Z1sWUrgr&UGPgu=aouEmh$?!%o(j?#=euWXxq;fq!8@3K@O@jKL9 z(%9sNAYbDr9|^L++PtK9Ij8^4(xg;*LI;YbM_Y2lFfIS~v{oY~Hd`B8qMiIL5#`g@ zl3C2>6XwV6Oba3+mucP9lh;YpgIRfy@(lLu$!Zj5nudt zx@yDd+Qx_L!eARV(nytl&r@wD2>|-}oO;lQkwQZb#|>I$Ue(MNsAupyv)qA~%Nx5N zXt$fsYuYVGNjN4AU0rA79@$AAu(qzda|a<^uveY2!-tTGWl%?R(lm|Ctpz2ZwIcCP zD2vcQk%ENjTISefzGFO2I7(I()YP)pV@`qr9>8j4k=fw_0|x1(qZ z==f1j9!jn!FW3u76e9~5k#TuA*xEKWHSHsK$dibrm(&8%suR6NfbMZ~oQ(+kdR{Lh zLiZg?@dhIZ&aAje%z!W!gOy5WJPr9C;=`f+B3O<}SYtnf9JgxI3uP~_Z)itACjf4W zU?(7i3MBY*PC+!d8Bw{lJ{2lIzKL{;r-}5B21W!Eq&`yIEI-*}|2TKN2sN z;M@{YdZag~V=k+?(}^*dHcs!{I*o+~3yO)mKRB-C?gcG2nyRY8Y;<^Sn73el#D2T5 zX7AO;@WqNbTt(}RyGhzhNgKszLyheM^YTwxG^NIiLi>#8I<7s&SMrKwhm+x{{a?C` z8$|i<*@m5u)#EBI+;ZnK!TvNFJMjsx7hFI?O-+ry<+m77zzzY09p~J_6FmiYRxhTZ zsV+Oo+ZaBCc5aiQYFAreGZCY8UL7z4}$+;m?m1l2#8+{_HQSib^Kvt}TRHzm%MD!~E zLddPXXCEsy3d;3ZBvImVQZdtQb9BcMKNz2XXFFhy-@$8=7N_4>R~_(iS*Dpz&EE2T z2GNOq4>Sy6EeKmgzdlQU^SgjwW17NVdr~qpLpqhgn80=r&94ea6a#O?T$U)WJNo_O zAc(3WZ7GtZAN8D+=t0uu;lCNJAR=7i6^$dxce_sGXpY7C=%%9lB=~NS(PZvI#13^2 z0|YNqbgSN*O&-K|NiHV`Hx?}lxH_!d7%=&Q=VBbOB${Ok*w z9t7a5BktE{T)$`x&BfkV()_{4j=w55D6~ zm-Y5J39nV(x2t(3C5$Uq+MQUXyou$A?Rk3f-MP~?ySIPYVa=}Cck~S9`diFnnGtMi zPp2>3itFejsQA{= z+p&62b5qXPW1S;Y?{bev<#Efs&5uLk6Gc`z+4o5*Ey^ibs_gv;mNcx5_kf}df~d$7 z<@b%u+p4;}Crrz|&(q88#L@`iy**)-R(F;LJ~>5cEtR`NfZ8`l=YwFJm~oNK*GuX! zj(@w@pOmx?D;;p9^{WY-C!n4I0Sm^{5Tf7&g$3X#47jiX#x;eTH0&QLZPJ(UU&x5B zLRkr>-54hzH8?z!M42D{V876Cp(m&Q1{9+7%)bUM(C-Rlwsq1oPobm)gH^C`cN-c? z!8BP~x+Rx?#Z}efO?0*YjUJc{S#*=P@aT2JvkONWNE3vGb22g#<49;L!427Ukm;K} zhEs%>%4B8o!gOOxkw2}dQGFz+T>JZqmt-0oOY2%T zG4a)8mhT}RFRt)wL8I|2Orve1?(u*X`@mG!BUcNYGFA0s(Lb8Wf5K3Wh=5`rFr!2> zdDfE)=>ty}YF|hk;NWZ78p-dKlewAuqd$MPiqYtF8BNBL?$`6|v=Te-nWIWv!HTEk zY#ge@=OC~kz<&H=+A(q9sU&`$wFEAQhP8Q{fNJH^F+1(9y|Ip>ohT*;1eIJ&{x3hx z#n`BnDREye3%7Bfip3|`4B^PIt5JsgGML5V+Qs$RwlV zJr_(oa(M|O_y;X zA8V)5`fHd}!6TmRsl5^op;=jn;8Vlp zjzO0$aI70!VN;ugt;+;T)$~`_8@rs?`GV8Nkp@6K(G%%FXl{W-FS)YBHI`p{$@lEz z1<#o1C4bG~Xl-6>?LoRHJ|y!s>57B<ZhL408$UIppt#I2d2lB|Bq9{~9l2-ABm z8K-mHSMbMjX;P#w(9LpgSXE1?qo(t;bPED9jax>Yrk8&QZK`tkH}pJoLCMGNR83%B z_xrlMy1gi2Cx!{)4NWf@B+m&j2d;B0y8?6c@QaF_X`jc(&xD5VDRtp(q{&iBz1O*W z0Fk~;n|yjtZ$>}r!&UM;igoU`nDsbxS5_SEmTj0&XPnNz>cqUhRnPJleN-WVzm2RW zdvi4hFozmELDrOYG$MTt0E+j;wf(+0a_cCvEGv7_*;#?-2guXttwb$(_gn6C-E*4Y za01uiczEK*xq-r}VL|`{3>xz!ZT1pTpT%yW@aR37vZashoH<48P$q$F2kB$2;^n+HzsB1MN-qA+!{c@(2@N zw`oWVbgJzaHQsG3?Hku^>>VR{ICvykP;}4wg&=3@mTcj8}Cs1-hZ;;u>l4 zesr7PJIosDGDSPF`N7WT!GoPTUsk1Zs_lA*8NhQHyGd{5L?1Sx*6Zg_Gc$vx>lFRl zM~gq^A9tjol~4f*iT+CSvarho;FP0(4*+o>3F>@!LX$>Q+eEO_AA%4P_dLb%G9HonTS^A0G=}N?h^ED7jU--RB=BW5Jt4NtOKc<;TamwM8j z@hk#)jhgOmbe;4;u76FR-#RZ-V;7OyRM7A1GD2qaX>+%KFH62id>2V6I*EyBC69$* zgsmgGCmrDTg^BjswJ5LrC+McZ8oZd=4zMQ+@F%cyzjybpl=oNhTH$Qj^x|Uo$LTsr zOG#^82npxQquo(+iA1dGOiuC?@SDB8cXB8Mz0LqFI%(e%9DQKv38U*tEC!De^ia~q zV@7NF3O2M`MfBCP(s4#{aeO$>LL?N^sUn^Pk96qhcEPeOujlW`Yt5Sre+HsJ5R;_C ztG|v7A%sKBO6za60IwLPOUflgKQLko`^}(B@#mH68X56^nIy_DP%DXIr~IBTU%y(G zmn+QWdXUL5Dk1g+0{mC)Lq~ROtTN5DcVj-S=7$~_u>< zCL=%#!=pvLifRdc`syMQ)RvUA$$OB(j^^#BFMxj!Zs6a5~x= z(igAn!SomNYo|+bSmq+QT{%*-IIK4riyJb}b7=RjZCtj*;Q&Bx3|j%rO@^Iw&`d`N zJLqSZC8W@j&vh4wZg!^cQYh5eh~K2pa>bJG0Zx1|oNIrPLM;oToXXG_wG*cYX4SE= z2AEx-hj4@=Z++6705>GhERm+`wFQ+%HC-=Wnt>^ccIBXvSee8j(xc;^->XUL@a+)Q zB)0csS8g%NQ$QHd=|Z**MbiCf8zY?|)mUmLL2#PJvchhTH^40RUkQ3yU3lA&vzYPh^gS3f(FY%LAy7^w!sD)|if#33GkyJWWM%pR{Pd@F6WWs4W$9?_$CMi=VXHM;N)zoZ) zjyqjWOziu7wxGl}i`QR#JZ^OmSU0PPQ zLrF<9VN0|OH&|uZR7y(TKx7gP&oT3(anuxZHcVa;q31R~aRy|Bh(rz1ct#(KY>aMTN&f}CaO@ii5DEnsTFU+WTcmJ>Vb{g4pUo!cv$a=A zs;f^3x8iDb{7d}aysRAqRQ$(F$3GD8ECnMXBQe}&zkg@Ffx}>jD2c0RcX}AtL4WBt z32t2tjg1i!>#hVwZf4&WbK=q=+&Q848SZuicL%d`+>~%M*!yetiK4~tH6mtKHa0e} zRn6|emJJwv|nvQ zWnoxV+*BwZ?(klL{)Xi#&v4VGboC^9ccui~968*r4_2QI8XmWq6Ms7H<)+#)% z+uvTz5z66TW%$Q_Tm_>8+KV>aG~cszw}!YFljy-7c_K2&UYU_n^Go;bmS4MXk9&_I z_(nrRFzkJZlLcf_!P7XFb^AD9K;ENJ4;tl-lkWS}jxD0s ze{Uac&5K;RaCdnPdkjyFJl>ww>FAZO2*LTneUodj(jDNNsv>MtwDaw)R`#lFn`JJ) z5ve`GJFu(o>$z(Ix_i7t=s3jh*4CczJw61Y3#MfUvV#SJg&J-ZF34gUv?lH4Yd@zq zpp`2TbBrdUef_RAi4sQQVrMLw#y@^!N5^1`&G68Wm5@+Tj2H?vyAIuwiL-yRRbM&Z zUWlj=O}Md}xhMCvV^Xeppz})Mz167ayJtGs=dhMp)h33@pw_!CkHXl{YD$Kg!J$&@2rZW;+^%gv!VAm}% z(AM6Ou6B6A9L*s};`>K%)9>(|Mn{SG|6n|<+u?$@eqkeMC`RqFn zo~EMMSTdEuvZjN|?nIHX6{URCOj!oqN-4+ticv0ws0$ohH}B$dL0@fZ3eugAvSb+G zpMY8LP$LG~%FepF_MR4qrk#zB9s@-MZXzOTha55Nj?*2@9lNySj#9h6;$u)v+wvhw z%}*<(stBj{)YLJxmI3$+5wo8`Wo2cvy(|=!dnENHXdhxAa|_9Q{MLqcyr6(>Iyb zZ3v)mTo%SS^os&HE%pu$Q|851cJfBUDzy4rI%b8yl z$Pgpux~zx@11H6N9+v3pSr$3*>oY9o02m4p3s zJ?uiVY= zq>Tw7K`u55ruY*HlIh<`kgP1d2kNA>m~D6PDiIQ-79l~Zo9P7YEjRqV1er1?K{gkM z<#8&o7BW))r35)xZ}}|=L%*a#GaSrh@=4QpFjHG5B=)ZEHC>x|+^l(w>SoH@iroux zC8i1^JXrLjLPHmuawXT4fh*Dqpb9GL)q~O+JGgeO)9DpW^|@<$SA9V>mK;#ahc~@? z^JW|!_qab^0x`C7inN*K1*_IjfIaUJRkH9E2Bw&87F-0ml8dWl`jI_Xb#XS8{Kjk= z$dl7IBJ5}1N^ae2a+Cw8_ zw!V&+mlq3W?CP&q%%>bK*%U80KeB;|ot^hmP)HCjw$A(vvahrZPOu2h)+VB-fbrMI+jdq}`gjWCFgUZ88EstRzY+L{{uJTvci z?Gm|gnrJC0DS^e{>G>6=q!Juz5=ydWsh2Y0UHr8~r$&K>o(E`SQh!CVG0; zal*&PbA+fV|CuM=)-RKbC9)wZCWeKT)xgxW0x@|g0ATrqZ|Kw&ZvxqzjSl#8yh$u+ zrwT1RfNzBh3$C8x9eJ2BB92o~S$WBO*KGrgdCr^V<)J3?-o08*-lj!9(IQ7y6UqP5 zPqtB^!S2#ZLzK=chghK8xCaZJRG{2g>C9!O*GGn(4jr_%PB|5Rl>zPDTmQD&VT}lN zL!8>=LV{X7|^GD4mly9KAKdxH{~(TxrM?)w^=Rv5Hb|YJDY=Eu(!x<+o!` z?5tLMR-%xXIVTbV>r_gJaRfB>(`RYeWh$owi-dIoj_cfPe5du;<`_9gJ7}fa%VKMkTuyU3g6FgXZitFt0xrvPT z#Yzg>(8b6oR9s^lrQBT=dbWK;Yg6@8CW2Qr5zZ$b6tOt?*QQ98%lKhWYLv1%cr?ux zZXnBh_<8fMvm5LGkoPjX3>neGNbRaicg1vvQ(sAHGe(Vn_-UGHgzl>T#?~T9&w+_s zXg;GWe`o2QIETfwv`NXy?EH%MK`}&ICiltm((HZa+)5tgHpX~nw#vE2$X^JvYvU-Q z*wp&kqj?iT;9k{)^%+n{53+FfZEt*>>^VFkQL zip%Q0ZH@Cb_zke+%XmvlCvV?KHzg#0)@}&_YYjcz7n<~r$dmj7aQz41`VYW$4lMf* z!1W)1YtUz^e*muk09?1JhMa4q@=+WP-Av=tKpzh4-x|NQZP$pr(5hyCaIv}mM$uPgVtu3}Rs|pH^ zx%b!;%o*MwT?1&lUhM*k9rI?==8c*I@IpXAfzIomotXf*;PG?^=qAW6f$%!};Dhjl7nYQv?*YtaKg#(;?hjB)Ri5U{$F-;^;yzT(`i#D9neCpPn5{r7 z#xB_}pzG~N$W=`ZM8nHlJw8GIL0+#`7|}`w1_orTr~Bn*&tJY=<xwVLch&^@3cZ#}Jn39rm z)27+k#dXlj%RD}FzZl0@2C?$J<_D0YdxQ6mxsH9+B1*q$yxX@QCP>AH-QKk`GcjRa zhYY7$kOi!k&)YDAYi?`1k> zn+GXhnxnoIEDcl$R;8t#w1^|X$e;~aumw%WTEv0ZTx5=3Lz01kb1>ZFFRFZfQQ6`a zOGWWja72!oVv-dI{8^Ngg0YHO3_dW6fMLGx>BW1YW9a`}=DuWtT4wYdXUs{MDBubj8m7OI>@npIDf|L6qt4^@Wde__G`{*ZleT w;{)mry!>1Q9lyy-{Gku#fAs&dU7wt#{A^a4O0Dsce3MaA-l>$Uu-E_p0CcAQwg3PC literal 0 HcmV?d00001 diff --git a/docs/images/example_08_pagination_explicit.png b/docs/images/example_08_pagination_explicit.png new file mode 100644 index 0000000000000000000000000000000000000000..a1093287e1e86ed974cbf868f2496c631aa4ef2d GIT binary patch literal 111264 zcmb@uXH=Bg)-8;=MUXZi3J9%YAWN25iV7l0at6sr&KU{=2BH*6GD^-M6d6iEat=if zA~{2m3##fay8Aodd(Y`J#=T=)e{`QiOV#u2z1LcE%{kZp@JvpUlAM;DjEsy@>d7NT zGP0vSWMseJ|LrLJ#NcGxZL%X|WKxeFD7(fkjh<0g#^QI^DX;(W+b2Jo&QLS+CsW3q zxZm733uR=Hk_y5G2-#~FsVkj$Te|NS;Epm!XQb;Jq=bJliPl&+$9XgTXJDmt)Nd3n zpGcwm2Qvs$j+izdj+d*m^YeDwOI3XpW9aS5*7z7%6O+{AWMm9%!>2|V$;cAPj*tq@ui0B&EyqZKBL`8545R;#c;LPzG;HjHhyc7PQW^wB_qb6gD{|>uPrDj$i>C= z;lqcNloVFA+@BmDwLILp5We&6PB5q$36MycnVvy6}Y7DlQzdd|)R9oo)*TSik zCWoDOlt&OYU!6F^Bn98uU*mh`rW7r=QR8ABrrMy+uLD6ES-Y;kzr9S_>p=2b^vR6v z_G#H%y=QwLm3)QAgz46;cwv|Jv9W5G)iJ~RfWcC`$#_u@dwYA$66>p%F7o*r2BCmh#yD zl8KA;;MeJQjNW&oP=gkYD5jLu)RU)9NuzqW=b1P}NZfk2Q?5c4|NwUa{e+l(!(tXS8Dm6etr5LS^0H(WB4 zl!k_e#A%HZ>(QXA!sjns7#$hu%hk~`nKaJTdI%44hKj1AsVPG(Pghwv%=@$E!~!;} zprD|sscCR<5YCL{X!%xQhuCnAmH-_c9pAO^D6Dk0os|k%EHv}vPY@gI6}!uw7PYmtDOSTRf|;bs%F49hfeKB7 ziTazp8A>*l>-BJ6mY0_)srG)prrIPCiTkUZde!t7E*P4cn(Ptw$4RhX&USVMo;*A} z4&&8c2-3mEDMrtq%ggMdn;A+7#T!ymQY3so#?Q}hDyBbIXEitbZWXKG@QDz z{8pNBa&oUvQhxdJg~4l6p-EFH&bmI}ih%XVN1L(o*`MEE!&(K4ZmZW7;(w1nQOf8_ zSxT!}Xd=sb46n8?5zN4j*}5epgdcSo$C{(ho;_P=+L5bMQOqQD;g-xUF}})U$D6R# zpClO^(Q&}&z56}gOHXf{vwGKHdub5YmYtn_`ob;GjWz)|{zXhuSFT)1lZ)dPN`}Q= z-)TG&i$D@9R;zgeD~pO8*MHT)pSEZ|tt$7emC*`A1B2MOxHIJBUy_ni-tjr92J}98 z@nZhVWB;iIVatL1H*ek?ei43iUgGAXOINR6WnkbSc{GPHv-nLE3Wd#5YuWFJ=39)hsXk3fnbR27G!H?Rcwm`_BoV#vn_NS)zv}!Mkq7H8 z<6f}ELVST)+9epmw$`IZk5pAvlOzJg@N-Fi%6e`gE!drKHC-tgnUSx?Ef8P{?hDLX z!x#ny)=w%Erbieq#~?70sj}I48S5?GZRz@JZap;ZH2|H&`=s-j=v#@ZpyBeDh7ccLOBRNMq zdNsHdrH>((3L5Hz898m(Rz1q7v&$rrJ@xex-mH1JWW937`Sv(r6w}`B?&k8a8Y($b z&wDQ`ue#iQ%REJ95Hd%50>WlfVh;XSvDL7u*Cb2-vhZr9(z9osMK5}&#C8)QKP%St z=ILP|IY0@(9N?rU>jUBaRu&hB(H3fZW8@}V-mxcK?gSFMd*cnxwYY*VkjQXgG2ZEIV6U><-;qA#|$QkizIL z{j`^jiXE$|sS)+qX49?Qh01eSA2L<4D1y0L??(l;^po{w55IbKR0^e8WcE3MMtU=k z=HjJG3jzxy_HnHW=jFjdQwa5gevhdUbKYoyG1uC`!LnBx)ZUkVzWhwq&xVsUnMRr# z8`ss>H%DR=6&0u61#B-5cf^ZUJa~N)aj;ekHOs2_oY!YQkDly+0E#`r4P3%DZo{N! z^spT6F%pTyjhVL4O!0+mx{Q>Udexp9vUhQMBGd=9Us|R5C36}558O>pq_vKX5q)WN z)xR1+IbMgl2x)f{*?Of9!55ey;-)xVzTEk_wkh;B)m5P)+liVe9#a;ke0Ye|8(2hw z^+5jTgW%kq5y*9rL|?pk0U1RKavxMIkIgxS@7ZFlrbk$4^59rQZe6X~iI+>W7%Iv* z7#gP0^R%04JXd>t;pfk15aL;CdDie>&R6JSyuIp0Aq{^laaYWsmmu|q zgy(3~6?j`MZS8E5PvBCNX*}03t}RADGT^d)=`_J(O5Xi))s=nkWI2*&iuMgY z0}BhfbLY5VSs>N7?KBSGmW?)YCc$y;hqIiVJk{bcl{b?P2%+qx)=V<|&|+zz!2c3^ z_q-!{soY_^qNRbZXyH9v%Zv=G|UVi3Z4M1-~@Vzu4|m*YW&}J_Kaph|zTbLM1li z)sPShP1?Rffo^TpF17t!RpkXq4NlBLZ^q=*R4;sEGoiYQSt-?VuJb#DE&Kw6W`LjH z*Uic_i~iiMt}ZAK=ADTze;$kvp`9No+wh}i#cwY! z0-i86ot>EAGVe{FjrV9@YKK(v;r)BFzU-)Is(Tv0;JnTx9~R@DH2Iy)&7zVLo3YBW z2^aN+3XatclD2A`^}B0xdFoa4;8)Ofg@oxT%3}YHak6y&C?ri zXfR#-`CVRQ{kTRUt%h%abSI$xZ!!7MuKZuO1^*Wv#Q&SWTH!~(G&7^>7D7h$tZt;r zqkqBB?IZ=+eHD+BY;*!Elg;msllib1Vl$twpD0!Q1x-(8dYEav_N#wtYX09m(!aM# z|4%;EhegjvGEmdXkf*)gM0h=^p**7 zf7=GRJ%~wRNiQwk++L2|nB}QH!2M{A(yFR731q06Ss4}SvQrV=v57kJd!1b7rD$He z3b&1wR9Q(~WK~+14w9H5DXs)_99 zgtKH+;pRAsdw(RN-$_9kvU*-vQd)vZt~yt9U*F8ZUO>nV zHUG1eWXH2^ofzorGud5hhbJ%W2)aCqwwu(`k_htD%#3#5_B;rYjAfF_6wf=Z#qI66 z6UCDd$@F4pB@BwpZ6SKH-!q~wK9@pC1h_s$CCkPT7t!4+c9 z<)2}PQK;^~;2@Z%c3IoH91f-Gi zwAF~K(OhS6Rn=^74jnX!a~I@W>%vT^`Aw2%C-HH){w| z60jKyf7sUA$|pM2z}Ru(WIaGt9(wv$qa(Kngw>FiNTi;+x;AstC$Xx8!ot@?svA}C zXnYoDx0kM96TcgI?4OR0?;)-Shr_mfR}?-)70<|Lj*jea6><`%<&W)7X*qBi+ncEm ztWfQ+e0O%k(y|`bT2{JZtC=;y)m_fP_BZ12xQR^k=3oNSe7a>m!P9Jc~4{e3O_0_MD1j3jlpnO4fJmTbk!tv+WsxRkUq)t-Bp zu{&y-nTw+y1%Yj)7?SAM($Zc(YIWuhSONQ))TF$F~HetGdr zT0Ct&lN4=RQZQnSNpNo{52NdaYcj#r%f!(FLcWZXn?KCtR^XG>=!{~jIk{hOKZ^j2ccXiamWFSBM)LEH+ zjPAzvupW{`7{qVIO-||+78bIvl4ocCXg*8-gpOxMiJ&LfpKF|}OPGw*iyx?PG2h>A z3qQ3>*2n)$R|U<+EOuJVz@D&|w;XQWo1W%Wt}nwq{7bkG&?a*tP~^I9JI zvbCtUwVCa*yH@w{V<0tSB$E`*(AXW%h-W`cxBIIdLxY3W`zwj8YGWN8q}{0|0YG-!=C6T(#7}(jdzVDE_nweQojz+Q#4yT8- z^yPf-&yyT2E1dG@6o;(a(%c*)=CycC-Ne1Pn4eA{$g2Ct^4f$yk0~*2aTSBuU#S=e z*qyDlvjDh+G)|W8eL_{$z?czPiFt5rmR;+XkuiHqyDqVg=2}}!KgLVgbgq+{TrHDf zYUrf-9MAJIG7Myq-Q6T;1-=FbwsB&#rR1vnQe_Phm`jS(1r}<4fo=Da0)g<-ta2|R zo?ENh(dDn~uT2_j;dEK(TP|>39ux6Cz!ex`UhoDt=Ur(@v8eM?$gAvD8QYsr(^?ti zL0b(EN~qD^lxl8lj7N~Pq)^4yl-?_XjBZJxp#;`E$K`=_8``#t7=D5W@!)%~*ihmt z!)(2A4OH^TNX@HP2B%I{ILvbY`bEUY4%GNQOp_yYC~a&n$Jh*C!jCvGdhd)z2|5(d zCs62AMC=jwE10C9U)Ocr8GV5NgD+ZgT0!k&MMcZ^*F00d-IR*vPO(w!wu#}tD(Em% zFT6?-ExM2Bs3RO`S`OUp8}^WM$1nG!=x}s#39&iLFjA18@4CA- z0M(7=(xu_y;nB3mH!B=WrK;SKRkc9#)d%Vz_ARvu-zQ>!!zu4$i(!C*(&BBg- z{7B?Bi7PqH{&0V`EUm1ZmwxNf8>ba1H5@Jugf49vsxZH`2o6J9v|&C*WodPcZ|90_ zmg%AcW6)X3`f1&Yi%KGbn~(JEyPw4 z8r?;*`@`d4{gyqp(s4f6k7*67MgYpNucD;H^X29cT4w(HLRV5w>_%du{5y(0=+3i& z`@0gZwZ)qM^?a6qtF5HhRyf*Xc0CxmD}~zJ>c`yqvHcD5h(p4W7Wr3HjMb$tdP1y6 zHJkKwujxg4KM`5~F%lNG_*RR8bf88GX{+++$ky2C!H~S>@mn%UR05_(MopxT<9wr^ zA3A6alRWnONb|k(HmO$e_w4q@NO=Q;fsuJH$M-SGq!K2nx2yJFqYqHUPz?_@<-BXV zLgo1#Z13s@%8O#ZIxlNJetZ++*9P0HgG$yzRn*bja62M zhE~n%U`X*f9T=z6o7?;=1^Tt+X!F5{urSrWhQ5|9!Jh(SbI70fhvG%kkihu&Q(L?(z4^wPr9(UiS&M@1~vXEUIt52AQoZ5y)GRvqVm zZ4DR(CNJE)ZqV|pu7Z6u3(3VMEUZWLJKy6K&7WghzTO^3s=SF#{;`V|sH&+spHTy3 ze%|$kY3#G=sPHYdSOx|Y@vnu2PEI)v`_4g?K!&=k5>+Y#KY%gJQbg&!$aeAo@d0JDuaTn<3q=1Ki`w4 zV_<+=%(GE;AZU;V8E(|o?Mz$B$az=1Vm~<8II&k?Q}brUu-?%E+dPUz2OY#?p@7qQ z7dO<^DWI4N6DZ2;YFp8=ZlrJDIAZvtkX8ALMkX`O3F)VQGh?^0b0TDoFZQ>+QP$RG z!HBAl&5j|6?pc`!>*4RrO&HaWu9hSxZw?k>P^@;^BNLw=@*O)_RAPNXRjANkLsyW4 z1L;+q<1SB`nP9#}n=ivrKHF~Gn`xYdoXx-Kv~b|Gx7f!%>2EUIVVb_w-hGVVbsfPs zKe(|Tj9|c~_^Z7>>9mz_SxUOXxO8?hn6y=p5_&uDQ+3Sdj}NS=>@9h@%i{@|q|#Z! z8l<73p`*2tC#U>FF+_KC;5&9;|dPG8742UOf`|Ax3RUVwgl?on~3X7i90wyU?dsd5zw5Wcr zTI_k<5q6Gs-x;;Z(`jk#;1q&1zci=^y;DF}JhTGS@~f5Gnj?PHh>_|zAUr*%8dCr~ zcW%-Z-ju>^X5U6m*J;^U0pi;F{(82saN`WQUr&z>stp$U~4+k(0NrZgz3dt z<;%AZBp!))*i806c#v@GX3Fd|wXw+zgYfq7NZGKShn3gOc1yR>;NxPA#>GZWW1JWf zg_7Lb2=)s=v3RY9exIC@KiLtz*U>aM*pu7alS8r}%o~@yc{3%| zW()aI>6m&>dDTg6-OyWGWBC(ngM|XObcq4&!EwYlD&y{MS>31(LA$x{R>9RLvBTugKIH1MEqRHSaWz@ za?0pax#~;wmDldMzV_aqYVrb(U04bgAZ8-uv3*8R@Pz-R<)Nba_Y*r&jlIa2PbpOK zLqkIW^n07#DYaV(qTzKv0xCWCI*y&{9&?>+5g6O;Udt^RbAK`SEN~*iZQ~>n&RM1k zB3El?qudEWH?~!CMqc$-tLEpg=he$vWF{QGB}tM1<#C~yu8KyBdAZkA$p>P-klN7_ zf!h35w5&+(QpwY0s}hrE8R169Clzj%Lp zSPD0{v}75`!89~5z@~j~YM@|nvi<~o(m+!a5N<>4TID-Sd%GU1Rk0|=6_<}d-ojHW z0X%?|6c9j5DixxGov6)?2=}z(Q2RNjOUPuVPSE!o4d0gz1vGZ!mI5kchBeANr z1Zfc9bnm=ll4M?p9tB4~dA2s33{hvx_O*$)7+$+9z+cd@lL&ewJ?|)sZ9^w2OLMZU zK_e4Ql0H(P1S3X$*LtKOOZ9{n<57mvtwpLsvf;yR5&5`Z&2*VQXRpo>a4q2-Vjfnz zYusb1r$sK~Yg~46w-WxyoA2XE!h$0<>*T1<&N9(x@GQLataDkJ*n2B?nQCjDS3ITNJ2XW z+0P?MWNHo_Vv8F~h4mP+W6JEe<*S*6jwj;QY5z@j#}<>FI(3RutGLzC!_{>cWMkHW z-KD|8akp7-ufeI>#38D+>Rhhg^efq6?=JSXwyI$T^rvTLP98u0smh}9uYk zK0r>OQ8wPc|1Bdm=!>7p&j|{Tm65V>&y9W?6)Z5sQlYnit9DyP!%JUXRpQ+7CLh{b zZ(yXNq}*Dcko67E7iT~@QE%J6x%O0r{Tpz>4MA5!NmXteGrvpG=sEc!Vn(`bBHK4)Hf z&d1HeGus;F07@*dncf7v@4I)yLql)hy%R9+H3Q8OLQ}}m4CLAQ`T3QVmDi`Jyg_NV zv})4}u`$|u)E!9NIAPLkg}IZ^ zXsT??`Sa(k$0`Sbt<6wM1H$&fpXLH0VIPBQX2n?pQ4I=vG>>VJ=Y!VKQBjbVrkX+@ zKYo1d)LD8igCB=t|5RfL{CW^*^aJ_&_AOu;05worLgFp(t?3F$Cn;%}N-ojUFHKA+ zsH>atE&!{`01VIA>L6jO;y$RzR$X6wqmM)k#zp&3e>Gx$4mD$Y*oH39V4}vCfq?;} z=2KJ*o7>w#fq^>t&%XnqKYd%Ky1M$@xpT<fE2Rx)kJ4fMZ(CNN;a%BcrLYDv#+C&LBvDG}#oAzElJo#EChuH!%1WNGCAG zKV6rUKnQ15(==~TRDSDyu-*VVm?7voz=X{zdtLYW6cDUUOiVa5iw?C9 zP|k7;{YOn1Y=+kdI)q|g`_SJZ%je9)XZ@PK@S!SWOvANpsGK7cUD$TuDzv2 z`svdJu)`P_e8Hu|xl5J`y~M7mJa!~=KDYI+J(n&tl-b8zs-9Fd+>#N563)ylb1GpE>LwjB%?S#oLnAaTL-^0}@(cO!8b)@SQh4OwzZ z=(95Qgr(G(ya*t7u$pQU7^fGojwQ~(c^$_fk-i6#5)unE1K>VLw10VJ+a44xXlDT{ zNyFJTG1Ze@lTeWTbXR}AI|X=6kjmG0b`Z8deNtptcyp0Y6b59cqZCcfn9Es(pDDe* z*xda(CPkuotCV*FlcsX*Zfz^V6Evwp{1Pa^4)J>$Y^M|{l_0aL8^Yc zGgju1mYA66v9l5s6a>fo^XJbXQt%DSJ$UdyT>Mq#>zJ6~FlHrMKK!CIJ(yW0fkr|S zob>ec)YSUdCmRl>K4kOs>?|OE-#2eI=ej_29c*Zj^!}oh${^~#xjH@$-#9*wF>Z+f z?c8m9&@?VK7Q6yIV4VRyJ0(BgcC_3Hq6kh7JOgduVE}{5r6jmuPx*`?$==2MXmQw0 zUmt8oyOO*ppHbuQw{K4-+{@O(5TUeo_w;b;R+_no)s*(SSTVawKXkUYcZYKyib63- zJ$?F=mfvy!LW8(F8GLYX5Egb3fxvK^X0{q-8aW3Tx1%*~-n{sCldUv z!-)Fz;{)y}ys^bdX@U2n<~RmnXBh{1FpN+!iiseJp2EVyJ%egjDhE{xKuZMw4!g$J zAKk-GmHBfkwW3U)MzN=hpj*&BIqHWI7DmV1KiRExvPYBo4Dqu{9OnZ%U1NJECvYF! z1tO+42{I5Yd57puY(j!wB!|u>jZUC;l{-2=KOY2hT$?7^yy=YQ81csi`MI6Vt*vnR zB~V;ny?O<@T)4o@Ok2z%Ne4u)+87QF4%h| zjfP*z7mv7jR23ndqlxCObb|yp-4dA~X70Z`NQpv4Tu|B z9+OtsZ4f=}V2hKJlO+L_w71K(dH6TWq&_uiP@mDJ3z0U252O{a7P0;L4WdZB8IzM^ zrJxW}0xUvePEHQ~L@JW<9xJP~H`^Gv2OzBnnCO*V^;Ic=Kmsi{Tc<+x^V5vh0GzdP zJhO3eBCeOC|7fv+j)uFfl6A6nuyhX5Mw8XzJEADl;Ye|T0)EsUL<7*P=@H%s;2;7` za$~k5_``=6gI`nd+tWOQX8t1?o{{!sUx6R-{UodPac8{JtuW;o)-hApt%7G+=bCiH zS?2zw%himM)nBGpR~cz&07hAZ{|wkNaH`mY@8nFaC9FiB8E*nK{b1@sYv#Z!NhF1w zv4xPOOc>BmG~rg^;4d^}bI1*xlTM=E>!*6;m6n^E3+AOE*qcLJ*3X~MlLpnyBn4S) zY*rx$LN4_JT`;0t^-A)eFFbAa`8?xbGdh<|w3JyuF}ERQh$N6ht-VHtS@1kvE(1WH zHt-e`6O)+O*y!kJH@4WMEqZWx7;Iab)f$S5sOV@Gl9YvwO#%w_Wt>M)a1e4Hiiwkx z6LikWhTxdunP0z9;o&J5x9R3^`#QFg1IWISi+A?gxKpz<+aqPtF{K!++jN8 z`APv)K<$j?w=y?1jf{v;1rZ$6zotHIm!W-Gm7>hBAqZz&fA*@-z|c^${QK!NRD>oE zMMY(kvbtreVfL=JRx>6o%@pMMj#$APtE!rs(Gd~1NEMZF3ywmyf>Tu`4q2|vchyaCF9?#s-~3~>R5h-$}q z#pllxnWP}y+>i_k`0zo{_UClG-p;~uM`WoU4?-MV!v6`;e^)S=f=Z$a7K(UD%% z-4Wmv)DP9HD!WMuaq$|kXS9gqL;S;60zz0gr5>`fdGV`J-O`-^aa1y(j?(gS>)~Q{ zsEOcAg0fc_b_8Ne50+2E`-&?h9-Ocs5Qj|=|DZ}dojCElHoqlCM%uq^=`yQWe*|uI zZtkAz+RwQ;Gq;!QI^~=2w)hHV=jiZoYB>7BozQVxt)6_r9E_JMh@4-7?-zyyXuq+FlJS;Am z7?Gdg*PEU`RPd|4JPNC{%;6X|R#sMmvqjMRz!jEtZOwKoP^%>7D68gO&1)<)wvYP` z4cn};iC2{1WFY@n@87L{x_DW;2FKyWsZ`EbX{2|$I~+sK$AR|%_L8$Fc9 zCCpcNo|93vb|p#n^z`g+&MRqZmcxpG4-5Q9kq1d{lgN&huExVLAnfl1U*@_FP6OE( zelUKRcPF2Alcbb9a?*Aj4!BIkJMClcd^Dp4?>w}%T?2z$GVQ}QS7~W!XkFsK3Te(uOK|Z~JI!asuTZtl#q=E<4$%TO4Cl^W z6WwGmN{7r$n2E8WC@C_>Ex+eUHHO1Mpx~ zk_N>@*qcr8r{O$HNeK&CG7-!oMy8gly}q`Vh(BtevV2rqT^;(38bEbWj`#QX!CEno zbT3a&zau2%4rdtBK#o>PJnswC!9EXwRWQ1qK7AU;X)hlqga%B9b9uG1I<7~WF8Uum z2Dat@q{qN1z~_OP?$ChK*toX2NzZ5A3qdq>Nkl{hTuJ~Fv^6z{z7|UK{55LUXP^eHAjQl6aWY`QSgpvG#Xlqmj_D6p>q7yTHwbk+B-Ue0|G`WT=HT= zAy#{{)Z`hSxVpN61=6JT)92p-=9th4&}HhsjyRv&8A5&Wr1c9E&}mv*A-00>WN_Gp zTviSMp&oW3rKJOY%37MI-!Am$k-!Ud`_?U}#Sg8O+EpL}et&(cs;UZ>_(jG!UufK+ z@f>%Z41_MGdadRRJ`lV)5hplfTDiyd@rHBr~ke|Pq(X0<2ajPi<#ii7D6 zi70`J*ELOdI-vL932gsOT9)~T$>yB?JumVj%6P%te8(0Kxv1qV`(yl7{SX!1c!l`6 zl5y|h^b9!p8k$%M=|fy)Y=Sm#%qANZHEgJ#JO07X4?2m+_IS}4AX?43z5w_EFP41u zVp6b}J8^Fo2_9IeB)RGZzYdLU?pv91Y5pHR%wx0q!A=fMBq}tNhE4sCRfOm6+SX$K zqE@`1Luz%#U)_xt_}Mgbw!wSWYsL#*_#oOMQZ;K7?8ONQ<52gfMGXxN;YFjNCjrk< zSXeK7*N-2MybdjC{pM%@x<`&2(E}yKv+=L)0umbdbLT*`fsP0q-vB6}O`xWsF)giK z9xee-Gsqk7F454mXn|LIOYqRNOyR%{{^!QVC$$fvD4P@~Q$N%4n=wzA3vq8jAO_J1 z$myTzd8rkxv32CK6CV7{aDvCn|J+lZ5lCq5v4Wgcn~H$D#FPUiLO*^?#F|snDzw+W zIej65LkA?I>b)5{B$5Q(^QWNA9(lx7LHlIfkEnV>=Gur~a3%q81_lRrw6uV!atKle zthdGGw<%Y{7_M$Rg9 z9FO@ls>U~c^RDw1kz-D0B_{p>wk=-XB4{tHt*w<(WxDhfS=rbEf`bn?&CAP6M@Oe2 zm~nM_dTY=$;XgYH)4psCb3CX}k|ZKwT2IjeOnLxvuZr@H5^TP95kL-d&HbnRk z20wcACO9}aDJe-J=?cH)Ct_MYY?avaZ)CS`-)3Y?@bU4va9b{&J{4Ybt}97LTN|hn zu+4%Mm2x%be|8pk5V!Xa!$dCj&MXrfTQ)dqBQ!a9n3C$L)t|aPuIC_~jf?LZUX(eN zd$(|!G7%hp0|kcQa247wlpFGrvrYoH{Pb;4TrP} z0Egz{MMvo1E4{7|zc@i3Mrl)CT54o!Dvff4G7mMev-6o-#X&;|F^!cn@AvZZ@&)92 zwanPq7#<03JdPVT9%f<3D%~6ac7b^imG|%oib=1s90*G|fW+Umw6uJEt(lY0&~SXR z(!TaY`gC67KwI@mOJ@G*C>2H@?W-2pOf0m5FOQv)`*ioaVleq%Yi4Y0+(e}dG>#=0 z1U+q_6!!9d5s`!hSP3QRkYC6)ZN6Z7IXH!a^~e0gL>J_FXk-tqg&iGv(vci^wf9EG zD~ZH_yTf7PmgK<<!QAaZZ=)W2Tog`r`gQ4P{t*>%Fpu1%#wdR0@DX12`3jIeeeLy95}f8C+!er~Uv} ze?x%ySAPJt)t8o83N_Kd3o|0iD=Qp`V+?%e%1}+0mzDq<`uY1$UlkD*1(F>+#8>uA zz@DqAsaf_5Dr0_csd#$CdEMMx*7Gqntz&5n)QUQ};V_=GX+^7O_UIam2K3GVy~==u zfWkdJhLw+nO{MAsBYaX93>W-pXn>WD(G`b^f@?B!mjEYdjMdX?{JxS@zJI@cU!0PJ z*J!X0<*Eu}^5BPJ9<$6=6`kBaviw-IeXi2ezooeH)pHNm+uIAH5LAeRXUNla|Fttn zN}7ab62>0R(%<_S6XOAF#m<#vdbYm1jIqGFL_8VKFnkPP+p!0<8`#Mff-aHc66-paY2=X0gD> z-QM1A4P!nqku|L*_Kyt$66k~eJUuFM@=R#s0I0T!-HLc|u)7Y$m^2kC4~7MH7ndE# z&*1xJVLX5N@;w-yfZ9DXI~(K0L-_Xc7#zW?G&E|8irhdmgxrt>1xf<`Jbr8izIo^q zUcP(@Tx;JdW}DT-*|1>K2d-`yx*|&Q!`XzV^i!R9e76)WW8y${d6y2$S<*|+1lE|Cf<^ZH5;7(tO+Ut zE^Xyg^uC|6niH_8&bGFl$f3*6QaL?APYpMp;d%Z^$bHky-Q67+fv3{au+B*6IAQTn z>H9C0kEQ=cZtJuGiwrmr3DbTs5Mnh{q=7b93B4uz=#+|Rh&dVc_nT%gjHvx8w`LW* zx`_*44u=*0VJxxvcLs)P+X?Ynr1CRk^y_yCHQI=&mgg{SB!Thyfcxt1SIfA9R`_t| zn~$5mzP>T>)A!dl3)RJLdMaf;S@hi6(`%3ET|2>{FVp>+ZN z2ni3z0-X07sUezXoL#jv9<(!MIronviFDaOM)*0K~~bOZH!g`7P#I zPFiP}T-q>Hl?Zk%Zfo)s95nCn1=`pQ-(7Jtv8gw!!q_8^r$y(+}K$eT?9zIx{Capg&#Mv;AfeZ$&`5< z6{W_ry|%U{5lGwiiR+Lw*XEC;%C}s;&AdJFxng>3z4;v zk;C6{_wZ<_uh;Z@n$`oG%6#qG1EL-7FE!;mZzT}j>gL~zIKx?)U@8Ak+8fnb2LM=j z<3ya}Gu?D*ksFXWXgIV>J$9`3)+#GS2*LGQn$jj|5`x!Qlrn!WOi`*|LfzE~ln8!F zp24rl50jZiVGpI6w0YE(DITk$v&LY9gA2bTmULOFncMy8`94Ben9Nj8+Sk}XhhhG7HCIZHOp!5K1 zbLry6Mg*TbmpBhD;}^2>Dyyns_^Bm5JsmzC7D!64257_Bdv*p}3s|U! z&d$zb7$l~exln=GRMCqijK`*KfPGq3-^U&LHOmJNz5_s+ih+Lea83xaP>VL5u{-Pn zs9eX69h)u!8cSUEH6+G~i3xz;-5_Ydi-JR^BTm?KD6Db=&H*sX0H}es$@;ws#v%aF zUbu8=8%Fxzv>3u^GO1N!2vX2nP-}#_FBlgngJ*^DHy7wKq1kg^DVv3by9!h}HMP@x z_sy)0j7rR4&qQ_nIS^X_gP^Ef936Q@M!I`r;-a}TWHQ8CP2{iiP{im2E+|_cjfZvYQL;sH=no|l&gC}nqj5)$APhG+zv!+7_(PiSapaIpOS z``uB5uIankv^ z_D+4bWb$R1sp~$?p+cjwd;U=CI-C%8{dJmf2{e`3ulla%u<&Q?REaS}n>rY$;SGUm zauSLIqwCF!J=LxJu=oE$<|_<)C3gu22e85*%{o9KJ$FL_h)P~&=J^mw1`rZA*4Md& z>1}Lm2pFKWBSS(yW6fc*<^_KZgbZ+C^Hkst!zO5wD=8D%C68!nY9- z%aNs3k~<+wLlX%miFx8(4~Xr+{x+V5r+Mj%_PK?^&%cM? zoL)$8ZH;tUYVJwy%xGL`kLtIX#AMRYK8{g+o^!?5|={CL$`;<*C_}c z$sqaPrK+1=77|<0DwpX^tuqYW7pXjM>MvDiOt6umJ?MXWX0-&pF>A@=4 zV=TV;9ElKee{T9_(3zqHtib6Bv`KH$r84YWS^8f$W79jMy$A37NV-L3a)Vh#fYU^N z%{+;m#M}jamxsqbXj+7bsVO5M`orIWZE^&D43qlMRiXd;=qWRoSMoaUscnaF7y8A;!GoNt%+LFxxx%p#$a)Vup8@&q7pq9Du>c* zP7M54c;8%0$7}X;VWAuz9y(lT%R6EO@+yzq6A}ty5ETNMg^No8y4q5^8`rMsLw&$U ziUTIOhS9>oS78Scc}kuLNlZ1GEB+Zhl6(ytRJ=yCVaC_DK4VLCTn_$kh^vA(0xc+;EIm_d3fQ%SXG7c zmdb>5CyEIqUucx&;zb07g)3lTpzmhisFM?g|JT^q2-Ow_|9QcZefRDfz?W&t=l=!i zH@g7N`;*BZGX4LUe1rKDFqCuhEWIoAU!|p`km8^QL*vv9hxS?x2%~^}pi5|8otXod zA<;b~B|TRUGX`lMv}wOIalwu0s}&mL$9RAJ`qd5e&!Hd$h63~JkRLap-+2A%6*S&G z0Hsai-Rdn1p>c4SX`Kfy4;X0Z*05~+KotSo&=x1W1JtBNXX5vkolpG6+oInhzL1|j z4Y>vwNjza^d>H2YjGHeOPM`q>OqYiB=TK+2q}j)$rag>0gZ1O7=>%DYl4`Xp=^c?n zTFK^xd9VDNqpUNHcWNV4lcJR)SsA3=Itwp0xjGs0dc0Dj;!o{(*9+UtbD(3O&VKJ+ zDWo%Ctcq4_{}vc}oX9IqoH!vHd#^w{@)hHGH?gLxAh&0=Nz(9Pi9Q+~IFDc!RE2)|0`Ch}SfuW(lzrTF!JpfoC2?+=< zykpmc&lJFu<-QPvfg(QupxEicSMSa!95daUc!0}#1H*yFfE)p9c*DRh@UZYpn}=4F z@$Hp`g@wgM7zMsYdgJsj?+)Zd0F0|6WlDLvqI>&~!G;3JwghZ%At4h$E5L9pumTzj z9B?-U^eO))4p5#w3+)9=RExT6v_XGh)5+&Lxafwkp}~32+S{g zkyzmKhJc-hYhGac7cq@409e6qmO6es>!VcCT~Pm~mI0xG0HOh!D6!$gznU9kAy41C zt?lnqvgM!^1+b4B_lNr=;4jBCA8D4M;$&g*3kn)5FqDL=9AJbNW~7-&+hOyMVv0@p zC#dzRAn5p_=b&KEn`&5N{;lteq(RkaWZbgf%WZ9K1;flIa9liBnx0*_YpAEfh`n3Z7i6F~44T)Nv=^DqYfr|8V8hUbTkn zNLDIVLDw!SEmcqJc`PAO4W1lOGhrm}02q3}HM}-sB5oZ}s$qe^4h0+mkZW*RhANbN;t&`ptMje$vcQHI9*(XbNQ0DOXPG@q$}90Jwmat#jTHOho|8pi)9}4#Xm? z?I2uh5YA)R`<1$YnW|XZ3&y5jbbWdJ_MJ?e5FaomSb`n6~@J^)&R%uVxzl=`;kP0UdnpoCVXZUAYg12USA+xl;dh|KpY`Cc=4HR*< zLSyM;yg~>6Ra65FU{G%sIMehBJmg&Hp z*6*21(i#xaaQ>e>c@oSZ3Z#*jrzdzZRAv;+>n)fA;CyT@_RAv%ppt|B?+hngdo(I8 z1ebd~i~7=~`i6!>KM2qjK>q>3yMx1ZBUP&Gan$E^HM06qV4wVVqn5o$MEldN5G`|U z!69y%VzFWJjJqq3l^$qhGULVE=2W<8KChHSYf#%Sy8eDOJ+-jVypM-Rkeou#)XvE%9Jx+}BKEIk z(v&_koNhe*f=?zIzoESMyR@jL-Pb#u)Y5K+27jpLHAx%C@kN*vyQkl>so@^EuJ;kV zb;17r4lXVwXf!A$7E)3l<_mDAnUVDmQOQs?AW19ArU5uKDWM}8RHpE^P;?9r@5A*F z02mVB{)Os_e=s6(QL2;~^A%(;LedRuQhLsuAaNv;^`!4zPft$b+D5c%bDE zukmO$+tUAZcuu}>pECR4JxXicHTz6(kNk9A7&a@hRf9huUpqvr4Nsh~%#f9y zmtr0B&vV`Z@otv1)?cGV4^i9B^1{Omc z8W|hU+vt|r*KH2i*F87tOnd+Z3paTpAn0{ea z9;8vQ6+_UggE23K;~zNvZbwFGD^2o_kCBmTe_3&+XgvXB?rqG%nnLWSrluaU`XB@? zoXQpB-+HYw4il`#aAwzakK(KY>>^6+AB4<^E|!sT7iQ#;t(Lk$j%j@ArWk6swZ zkRX{=L!1D(VOcU^rYPi5%+1ccl6Vttx`1mczz7EjqN%+d2(vBdLV$3@(pxZ3gnxCX zzNLAMLE1FMDTGcjsIObYl9zm|$9!I7w7~G4L=xNyqWrIQ>|>|?GqpIf%^KS9DNS=e zhyhy5AC8fq-w@o51$h=2FreVr#^D7cVq)MfC3TQlL18}h+~frY{ImZ@U0R2N{@xl` zJpe~T^#nPZSIdVTo$-78HSoWHi8+W7F;-u10@n{ga{1qUO3eM=RO(Z`^QRYB@7}cq z#T#1DwvLWS#0`B{ngrN}J_lCs zA=r?poeM9%1>tz`ib8@4-!bLi&!1P+)D&{VO#$T#S&b7aHo?EmOT*tP0^;gnJPfzLZ-1QVi|Gni zPQ+1AL}Cn-6eR@&8$<-@4v`K4Noht0un+?<&s&@sXJp>J z-+k;qzT-Q7^Y0)$&$`#W)^(lN8OaBVeR`f{_&+-`+jI9k#^A06qAF=nvSS+jhVJTi zn$ki*-QR2dN3nrmUg7gA&3tB!ol>0sb*B|hNHh{$RPHiGvH`!zY;Y8&i@!^SJt#en0!n{YYP^M9-D zEhMg01UW6(B*^WiqMG@j>G8m!{V21fW^MB~my+Dos-$geNJ*X-dMs;TXkfcy3mKU@ zs5uS>$G+G1^hz}syzUkE zh=bPlI30&y++sl25S10Ky}ct^Wr;DQR}=kUM9BHPWLm+^4zTb`eb|7l#gwHj;f#dL##hn z*0A!?Ej&Gb3**Qb$xA47*xlPk6sjeeJ;eW-737UC%?eI>?iW4j9S0F{1|SJm<#fpP zui4t-wt&;mNjWzI{lUEX$)>Jy&r_}@&6yh6>F$@GUZ(Ie^inDb*i*e$KtRB&-m6!N z)1Z1F9Fw|l#s3!6{v0#E9ZQKhY#`^kKTPP@&=LC+PdT+x$hb<#s9ae{RrVOU6jO7J zUCw?@)bQy^B}&^OJre5@ve)b|3uUjB?Z0z0yli7d3x}1_U2!n;33l$+uV1lB*wFZx z0}$h2Mh2!Q-oE-_ZgfvLW8W?Y9oD!m6ee=+T!lf}s5ibNCWU>&6Jujd`3^a9aS#S< zS-<`(1_YfsAmFI~sZ($qd9eZIjJyV;Ep2RSU>|vM-VEdp|6jMHYnF(WvXEHKJ>UTtYSXnq+}4`Y2qi0bX8SU<`qOnwfbtP<#rInE>35 zh)NJpyL`OD7f^B3^K5ISLRr@6kQay~cbN|7EYzj|Lv>G$MF`L-7 zdpAGuJ18Tt1mI(1+yEw*5>4#OMrL_$a(1H_2hozW+cim2qBhdT5;m%Ss<(w$(ni3F z0)is7hfGKM%B!cpR4X3l*K+BVFuxbKD^vf&Mh{-5o+1ki@0r*~^cpcaqgEP>;$AV1 zm;T`KthL8U4AMEaI6pWE*RK8U>WW)5y)5i6PK#e&1}6Ed)^0X6GaLQ#WyAXQN3VYR zeJ<)V$T_@MKfS`wXJ=($@BlA91?~GfyE$ckoGM5%MJ?$D)E9oq!2Hgtx2g-%FiSJD zJ}fT^eH8yY=YxKp9Iz})Cer@evAX1z)V+c-$=xu@LW#F&!-ihqFfhXa`3KXZ{UTCZ zq_o-afl$ld`FjIVecXx3B)Tp^AVpE?jcG(`Qc%NwhMsayc4CjSm~4Wx!^T2I?BuRq ziLBorturQ1f2k>+)m=0|CH-)3zxVGY>qxz9;!L(hS?cx|#&0ZG3jxwkR22Ym+O!aV zF}v82vMFa}XX|Kbo%6K7xwcf6+kj@0;D0`;0-WW)rUIRtP|d&fQHvP8??kUG_XGET zCSc|xy@&z&MkNr z=YkvfB%7_J&&l|GiMtRcdAN%dk92h92M|zu8=vl#nJ`Q$V`0?Flxk$FH!TU^5iL=X z*FKP2wxf*pvL(}I-IE$`Hdcq2ZSYfV?JKN{iclE~rBk($)@t#TaI}!Eb`d`!-v6%7 zO_3shHS2p+NPm{PK5VYQCW(@ED$A_v{2IG2QIP zcBtG4j0cxGTuShN!36Y)a~Wly%k2R99J5^jiJIT3IYDqn>FUysSyT9&{FA(nzQq!P zk2T|I1gte?wnjgFXVw>J5){RL*LID~on-Q)52GzlS7(en*F_~A%u~3w^f7yNgx=Tj z`fQO5+mo?$W`5O@2daM~Ab!_`5ns&~TRCyb2)0|ZUp^?X%C@gurdAcrUrU(OzIycv zEgObWidIH*F&}8@TN7jU#vn56LgXs>^~&u>inAx;ABUb--!|3p?3q&ROP?ob?{Ss$ zpv@)h3xUwFwe1tB0&x)RURrW;8JzH-QVWiiaZn6{)5yZZgL%QTQT`yziwq_&i(b0) z@iDJCZ14htgV_omw!-USIec74`SASs9m{`0-X$Kk%P?#x)Y>^VmPyT(kPGeHwha*X zHVTRfaIQ1-tBHzvX0Y!@YE3DIQn(NoiMho@oifHRz*B&%r18nPq{%;E*+n~x{tixf z0ZY4v$!ouecI#a#nIQHQ(aLi+2UYh_bZJ|?A*vYumafZuU@|MaJf)yDk5_nK!WcVF zfAu)+?zo89dEQHVEcjaP?>XK1e&u=DlYeqo82Gl=y*A5R}t`$B{ zV54oiA$w!NCMpu=*gMPCqp?;iaK@?usI>9exKxrWE-D4BG>MK+pD z`u+Rg99Iwk84Td&8+k*4W5<98f&%k-aFE8~J%(7x$;mJ&6VR^;$A)KqWnvYkactwN zRPxwUf)qOEyHs{kQ25bY!hay)PN)a-8`iO))ytB0iTO+W+a&?r?|4DJvA#tlul3cd z4-U%PIBQ2zo#2w|dOw+zKT`tS1QULue?&2W!?1G@RSpI>O)%z0iNynM9k@AAk(Q2+ z01C41t9{Qg z+&Kut>FDTisJaVrU~ZOD5CY-D)QVMWK^BG&@aom8!HYQMm_6C$qfRUWYA>{r_~1YS zxCJKh&~T-ro&*tOd}L%jDJgy+;gk%8{EsEpk)L@Fn6Oq2PuECaB;CdS3O3$QRtX9& zz@{8Ny_S}iG{+rt371WoCZoBHH<&mEH-;#cM9ug~`5KC@lF_m1!?d5^KuG3fWr_K* zM_`yxvRwmTT7Y70G{jqIIm?1WesVMq zP3yX7gLRjs_Qqi%WJPBchT~n)Zo57>SASyA&4@fvdbKB-$u5&?xbvA{6n2 z`vDj@G|*!C`bLAX(wzC={{5#gob|fG$HxapU`*@zxVX5OnGI31;)$`h3<8VAF#jSL z4=gMf#KiJIpTk3;2$dr2urZ5oZdR$zq566De~}LE0YCA5-;bMuT+v?WhS~=m7D(}! zvdmzQU{37)NuKi29z25ewNiqmT+%xHvXqzp0n>%CMQo)0gL!PmTSKsp^End^^J(Ki zebfhh3v4b`u?~{hR6Il5kI#QY_jvEl9cD(xU9dvWulX0oq1QG3zYK>VwD%mq<&(1?rf@zB% zyY}sd*YNZOmGt1nAWS(>vh@Q-EJ-@4bi1wUNy&wrQ_&lzWsXaGCmZNH<2onQ`9Sf} zQrJ!oz`NxAIi?Gbk5(S8K}Yw9_m>i!fEx28(Hs60&y|l@--=`YKGN3oH<%>bvc~MkghA}}RV*fgD?Vz3ePg&LWW9GDe)z9NL+s(v4J1Mg&$|VtJ0F5qNq6M(8C1-` zJHRTc4HwzIX*SfTb34gh(+yN4B&ZwW=5kOwMBo0uq;34KSjhgA;qm|7-%9unpD2G0 zu_DQTGmqrI{apwOv9tegy8j++^56bd|4)G_|M{)|S%Lqoz<*ZYKP&J*Ujf2doom(q zEU@ao_^ZVKHdq_9VtJ7X$=|ZC{)=DrV@%e6e)K;p@Shd<&kFqiWCi$t30(PQ~O9f@WX9$Ao#Kjl+|;s|UWIA^>|+X>}^1|x(ehKeII&b5H_F{1e6Z}yjol>+QY*DosiI1R$ktc zsxJ+$CaeS(=iV<~v$5$_hB4W7xPu`Vkluotqzo1&WHHbSfEhVQ81jLl2Y9jjT_8d3 z0We}Q)I1#%6~qUR40t0!tq-n*m zPKM{t3%^o5UFe22UwEY+FvB#bMaQgTrA<&ZKvXm50bd(}Xc<)v7Vo{jO)?a75u1u8Tp+f`w=v%ti=oWZfc`g!?ZTy4V_jWof9b2`Q)&P|9n|JR*tgeXe z7kcPLIALSBNO--O)W&+gB zZJ*FxgyJaL+Rh+23aJ%f7bqzB&a7Q=Y(K;E6S7sn%>d=X_i5+OogSFwS{x;bhd)fL zC%&QuprFt&5{V7hw(t1)@{An_c>3CX7DKQVBu`wQg5{Gsb1A=%hD74wH=i2KtktB5 zaLHM%`k;i2Gk|Hnp<=%i~lV>y-yuKd5eiT^uUGDLo+1P6jB(cJ$pxAa65=M$Ze zyHE!G4~h&GV~F13ggA^B7q(An3Zj(yJy50y7-*uh&$Z4sy>Jk>Ff+3suY3&;M3|UF zH8=Ok<$f8?=jYaZecMCOpK|L+`1o^}=XJ~udki;Oe=7591A8ySyl3x3ad-EQ)vJGJ z8XoKLC=Gl}O?yHK;T7P773bM`ARCF5E$NBIX&y2USBDwBnF+g++S<3bNth_#7gn&7 z$PBKWYck=valI801zslIT@>&Snfm%AwX;(ce|}QXl4|Qn=QGW?@Y(qAaL^5SB4USU zCYm%fMB%xY*k;!8dJxkNI(~1s6QuF9Qqptsk!+1L*!hvm5Kf<6R=uBSwy{UcGQWqS zdVE~D&B15CtOGZ6@|~SY7;DWrEoL~uH%&z~5Hb$C!A3f}DcU&c;x8xHu0q;Otg15{o|mk5 zTWf2lTi3cFcPibat;94Ke%#O>BEF%suXeMru$_D4V1E+XibABJFB5%X47V_ zckTc~PAK+p((ie<4|Y`v`$ZleWP50B_ob)+3U6PWmX@QP+IM!kLB%fF8sz7fa=9pd z)jG#42mUxm2Onew9M^dHT$uLigW;BE9V3rWr_#j?Gf&?;wS4WEhg;@I(1R%8KZx{9mQY~0;7p9xPsE>5QEgPm`328oB-jp#s z)bP*(&o(N^AnJfC80AMaTXXDrPv`qRc;I(`q_Dj(ipR)z@YCp``p3EMfMDx^qb!TF zh&Ol_l-Jmj7bG;cc;`Mvkdvblreq3pV6bfvbt5lK)2X+4)6-{n6jwVg@bd20k_g}~FSRFomu-2=hfVkE*K1JZhH95a z`W$M3!IZSwXv9Uqh9fqk67$B8#RhU;`I_?H3eliLWACSmUfE9+?L2)A-q#4Xl z{KhOSJS!PCgFssC_d!Ge1VS&l4;-L2ER=A%t}eKf!TNE%cuI?Jc;V9_rUT2ab_RS*N@wyb$qAv4M?8&?}Tzo zDSiCf?4Ufo2+Zc+bLcuYJkEa<4kNoq7ZIMFYo%a$FSmgf>=Ozl(;G!UAegzZTKJ^F)R!_c8m&dRnGpK z{;B6Y$!Qln3f-g;lkl=4qy;&Nm(B`X_VaajcJ7kPS?71ctI$3=a_xz7Nr(KKVImJ_ zkOFHz)$(lBx|!gkSBGBR;uP_;;JY)60cYu=#0Oz|hX)du#`i{p_**oa=1=?uED5CI zi@DbH(@Nl|P|zT*H3g@ax?|R_`+s9{o0bqGb@mS{IlhXUzF<#(iB}t!e>#Ftj3>Fu(N#4pCZK(z`>R9!!jqu>wIsq}5f4IDF+7sS#C@QdO0B?HZ?q zwEXW94|gPRo8Xj0<+5dW9W&odsTXs>mMyf=f~?7{dHe{p)%9i(x#{lybX+k0_Q49N zV+OAJs-+|n)xj%}-ZffCmO*_y$os(qMHv}nAx`4~a+@q*($ihuf-s17m#8GQdH7KJ zA{FEQ!-wE4o8l}_Gu+?ZLjv#6jg?`!Eaa7dPoc0nC5KD9vn({b`&9G zN;_iz-@YECt~jVtH42Tx1`$lO$El3-@YDD0g}Rf&$FrOkMG9%MExyz(+P%6dxNghi zxh|hg4-c?bn%IBZz$W6En@4A98V%d*DKkl)*&`KCo;cb~9hjf#caaQhtu-;KE1P#( zoO0QCZ>Uy&=fY^I&Agbq&SUB-SJD*buSde!t4C|)*I5tp!CYV-@-G$7a2Udih-7iX z;Qs5^EMsHK+VHbFChD_c9;Xh9!T8cj`}JK4p{Xsqs`8|^=`qrBrI>twOl&Gg1&`VL z2Df#%Q`N@Fmev&&#W>8$RY#Jl=YF-E?u!UiO^6UZh@+*sX;u=qmu?T=l_qYdiP{B| zJc@7=SX>$x-5&Y)@i#R!G0m(knb5j?O2g)+Gnb`+3M9~Y(cZZ%0hV8P!%ikAO%puHIdU~mOut{;$ zT$oG0a%|k9E%U1QnO7rV>YH0y#;P_Y;#G2V^nk%9JLh=Ud66d(*J{f0Em>EcD1v3Y zj*`REfF^aeMx<1KiIsifwRID;fWLk7Ma*y&M5XxT_P;rd{Jq%v*@3+_L%Mao_Laoh z@AQoaw8mgm4d;##wlZ8)<=~ajnGVbDJ_hhM@ygKyrN9mx#19`7`n_$> zzPmT;pmVjm)#R}+Y!~@?c?s$6!W0Z&k0YEI{(>>QrbttUnml**bUd`Pugv@|r@ zVSZ0gAijB^1pD~(z}J>4r1?6?wCB$=VRi`vBPfRw5(Yu$=-dze)6C3_(3vxHMA|YL zLBU)yGi)A3{{(|F9^H4;6MjedL-D+ZTXLpa*@X2mF(z20>|_syVG%k2p#yVL?%pHa1oKbWsaJqf;5Z7$(6ipzDV)OIm+x~Rxje|n$9lB$Es9P;Fid&m6S(38 z&k*y=Vvsw~*G@BtHl%QtM&3y$g4gRqMx}W8N-Qc7y3`curl7P(NZ$l(>E350Jsq$l z=LDM=Vj5)4^+UxBvlUwSv3z5WLdA&jH9VmzgB!c}%-tDS%uahBXwjofF^{TndHL3G z|B{eXNH@B7G=1;d(`W@ccAMlL!EfBD_BAP(orCX)z|dQ+&-GPrLmj*1W&ang*Y!I_ z7nrPJep!X6?vlGFqpFd74B`(bcz5s$0Cx$`5uZJhRH?7Nim=E9;Rwb@>JWM2QYRjt z6%n%Cg9g1G*8{joa4iXfNDwWk_bx3cEJWGzEKtTnhlzCrtq7?oHjyXAeGiAAlEzfJ zyrkqjT-QOpfL&Vq79au0`Lc%2M@dD6#pKrAyEPeeFV@jfrpL2$cngg$v6HJToVxaJ zrW3RWGBJZ(w#SUc92zfI|KVj=o*r|5uV{;+|31+ppL;$d))tBASXWs6VG9wd=&4GB zpyR=9<&au#^kV@RMQ)|kjji@$<*28-e-{w9oK6R{$UKGGp2d%vj61l z1DA(q=S!SMmZ;N>`5Qz%!+iGK}+ZdUh7NxnwZ6;Gz@%@S!vm0q}B}1N-ZdpFmf2$Tr2R$&?9OyHewN zU%q|Y3%}8e@UhtsuV#HfTu@uVw*5GAE0C3d=UGC1)}~sUZ?yo|!$h5~fWcGRkGA*7Onn=)Fo4T9@_lkemvIz&asnJ8-Olu$olWZv=s zB%||LVkwNtHGvVlOwzb?CKeO_vP>CZPl>YUli53QuoSpZSt7A!125md$9t?_=6eLR zU@#tDfdC8sERj|YMh}#)?7|`JsvA#NNOSaVG>>Y=zAX<55wc0=bH(Wkhe^Vmn7pbOS4SMXk22Sit`mCqesK4$y$VB4+zsX8X5tog*jheUtoxG_C4&H=t;fW_~mpq z>CC2CdTb?Vfojt;P| z%GFjMs(%MyG4jsaM)GKspr*q98&`&TEXJxu6AxoA_qV{w- z>vNEm{g0zJS5m_33=9=D!)7*Pg$(mQJx088tp3gIT$U&xK(q#}?e(iykS6`g!!t#= zHAB6+U*un9hh8@6UyEn@0h2!@W>8u}tB}-mcSp@QT5IpvFFY2o58a%W*E5$Tw2f z-raBdDC3poOYT@LxhAp=OKKDA$q;{^JxAJHu!L>Z@P}uo9ghz_yeMdc&wvZd&tZi^ zm!IL8uC3$;^2)LZA&qZPh)zc;-&zO~A@+y1M^W;|V1li4EbYHpsKM zYY#f0xbuYCE+M1U%-Wi4{d!7%tN0m5RvOx6$lGT8%MiZH^E-{SelTBLfp?uu)D9k% zf74J>!^{`1q}VsOpW(WN{0mA-O7zv*^9y+3uB_IBjj~Q6=e2d>DIdJtgQNN<*487x z$L!KtG5n(DT9%t#WpplTRK`wIdX7qZ??~v~vyu5s7Qa^yVOWN@qBANNPaP*Ao}`XC ziJVh671W{9ysJeYs&zNV44?Ar-DO~var@QBf7mGcFz0TKqkR&)BA~47LL8Zux%tEW zh>Z1+PuQ4P6D{Edo-`5njxStClUUsg1JAuqk5ZCPuX3I}rT%rOHP6s;AHp>cul^U$ zL-u_n2ILzLuf7J;9>SU9@~fMWO2Mc;UMVdW#)4(5ovB<_-n~788>ZCmCf@v1yElT) zRB_$;V3L#kwk(GHM2X@f{zG9cLKX+QF;3#!_d=<%lWsZa?Ue1hHIkS$Rs z#1y=vPp&!2!cvxND?ltTYqGih=*=+DX=;o*PxDirs2i#)JEn4(NWrav(;;ePXG#o|5ds0*%z&G3$Y+y~P__z;9iKe^5kLG3(x%P3pbhP!m&Tmdo~gdu{h4T59fA zT1X2quX3+LEVFea`AUhS4_E_#t#hw2WsE&0!kVVTSui#G>|ON9F8c7fXekc& z`GRj*MJ)@rdt38nomZ^Fug2$JxpF0l?t2ym@_(ksSF4YBgBAae3Rbdp;ZGvFNXSXt zCHR+v@Z1s9g2bEPa?h(~p{_3n`E?3GAaOD<%qzSFB=1rs&YKZuusTBiypH^MYf zpTXaY(M`VFjD2^3zeX%|SY;>^0|Oie?hE&IcXMl(Jc5^K%*A*L4sCL5*-KVAIP{cm zcd3@?DBIoluKN_~t;1j)G%D9hfC-bpUvT*euYuJX%y;BF^{yeb=S}o*&u5xi+ z`(xnXG&8u7R1o@8M1NR0I_5)J1?>Q$DWEG)Td_N$2J!p;t_-S*vnx|q8X zZu#ihU~>aWn=dzgu%(bP`CgwerJw6v%9O$*SA_GMmr)+36xI~WC5Fcwd&S8;(NPuk z(t@vKpopbfkwN|j)0n1}^Ty&5$r$SEC0*gkT-7a)I%Eo>Yo?+dBTF_vm+I&WENADh zVmr%qCrfHnr{x6g^KGYWWCuknP>CAQ3ZcjZaollXT9@lekf1~3NE>uNkxri(IrU6T zkiRgD+8q~Q4mv@d;1Uenn_F825ra}d|3~J}cPG9{Ku(_*ul&8{WmT19cUN~@d+Z-V z#8Q3;y}`eqMPS2dIaK;kwzC)O-J)<~xGTDcLJkjZUQH&FIDGR7pU}Ql(hA}f;ahdF zt0KfyL!pJBhCYCVnC$;{>^cn6PWMuj?jqR7!6SHZTok`|2dRj&w-YxkH zS0V||l}1ib_<4BbZFv3^?;SXB0GVzWDE<>|x?nID8nH8=%VL_x=^=v-k#J#d5A3OdTf6c!L!Fw)tnl0h9S% zU}Y70oPK_O6mQM_I+7o4>}R<9#|>3iS$P$*0-!2zb|O5xU=4=k&nRl&mu9iQx*Vn0 z0a8FAdS@uR4n@s?W|3-9nX{K8Vk>bseS6VEyZ=}=%|MMC+J|r*DQB5!!?ww*b1y&{~ijH!F>{3)j&&1R>)#hYU9a)YLx{}RhRc5hAMXy3T%g7Q7Lt3iHY=}g> z1f@$>Fg$+aovzFyb=p8&%BAjZcm0UwIp{Z+?w3)?U%Iq4XO0P>_`&cC9EfYRv6o_$ z*8in8pxRX+9<`fY>*n0_S(g?_+~K<&U8xt&B-(XO@~m!2)h&tIi>7rbDVR zI)bZ&ieNF!DIkysqXV2d5L)6i)WIF&MF#WCE2O&^f4>wdaazLJF#BMzy%{8P;C8$z zEk)XKIWlxmP3)Bw=F$z9rQvX)y8g{sBXhNC8sV&Qg5vD5=|1Ei&+k|=-tN`lLVW-y z4dIVlX-DUtRLT37?8>m!IQE7@`-9JOulR13bmN{_%Y}D|H6a@$(>9IZVpF#p zejbg0q@*NQLjO8yCt14Q!lc|?@7}&8(nnwvG%|(agAOs)!0q5TGoCuZ(qT*G>}oKYiV zn~9#_>;Q*|=Y;|XjMcum|M3t1<^H!mYvy;~Ky@zF-BrC7z^KmSa(6mP@P|->F_`$&7gZUy9SMNjC@kiG1KbabWR%APRA~b7bU_ z$Y*4a-YK`zu+rt~vGaRCsnb-|;T2KL;}vz~LD$hzFSpn|DgSiI>z(ufA`EfaBBX)% zdpVbjZo@*32pWO=2K>bkPzUSrxY`oC*UMtfqd}vrJ~t+bc|3BF9&| zIFuczz8|4M#VrUmClKryzt3Q+{gwB5Nd6Q{dD2XxC7BKOq) z;Iq)yr6h`z1l~ktcw6z&Yo_8%cLXG-ro7BDQe9h?Nbf>Cph02p4iF9{&V0LCEJ3E{3PD1?O!mNyEcvO(B0n!5i>yHL4<8iU*Y6WwwnWb_XAu3!F%yurN{piDf$Dbvpx%jZ3Q2g#ldiWE;G))OCH(Wop2hZ3z%9bo^&rxzg z92efY&6FR}XLNLI-@4WG)2j`lyprpyr0Rv3w0*V3qet&D#B*{TblC%&4!@0}e()Cm zmTiU59ycO!ITK-%Zt|De%afly^Mio_Ox36G4bfT=nKz*J1eg`{na4d%@gK{RVt-=) z$;YN~htj#3`fwbP4>WH+6Et%%hNmHQk^wc7B*Qk$(Rg!KRlUp zp0EDdAsy00(iB%fzHpME1$D% zxHWEN7+NVJoRb)$=Bn7gqE>J(6^Gb+k@o(6Lq)xe^L(Q1(W98)0EoP;U5_gEUbehP zk%bm^BW6u}2=D;wa}0#YL`W;)@7N>+L)|8R+ZgVbTK-1sSRn z7=OW2V!-Oepqhi_>lmh9mExgQgMl=WLS#`F{2eK88K*74-4w->QZzhnm4=;(hU(}A$;~M{lSBvc2K&%h6^kXL$G>l zIIa{W!CuyEg!~6Fv}pGPu{+$l=Cyp`l6b?JMf{A8^y-Tn$~m*h(9b+A)W>+((!A% z8z3Y;etY*0!<>E$A9inc1uM8o2%efqfJ+4wgH{R1BFrB%<*0MGD!*-%&jpj5iASok z%Z|A33q4Xj{+Qf0wlkXC_L`j?@*RoL34ojj$n~o!HYrQH&6E01^)*8nOEk zZc0QJHogy56ri{b0CsSnE74^M&(^9dyL%-S$yN@jRGEJ=9IN!+y3g`w zxN(ExyFe=)8ygGhb6mZDMMVV$4G3Iagv+oI9M?4EWBT!w0tM{2`S`Mdq+-Vo>?BDJ4t zzFsfnwZEmGW~1;K&B$6?sn0QmHpxFio;o}Ri#OfI^#PQn6wF=l?3D-&QJpLM4;mD;0K=z8*bF2P?+yR zYBM~BV-u-iZ%Ik1ASXA8vJ$2N+VF8B{C}Y#fH}zr96RtBCW?<_9SMp1m3n0*CD@-- zPfc-Gj4Y=;gFVXLEGMEHksLpZ!;(2RgRElD@1{|g;m8`5}SsT>#Q zcOeo0UUGJ}Ep8(?E*q5Uq!8^39Gm(&%5}?pucAs&G>H=w)`mYhntTj7uxk-a=Y3GM zUxgJFH%8Rni%$GtumH*f5XRf*d&R|YE)0pSb=e_Bn4ZI!AXboHfQ7{fPA+w1GH|{o za$Ne%el62U31gJJ(ZXd&b2H3ZACKa#Rw7IBS2N^EQT_KS0u$0cWg-Y0_Y7*SCVaUBC?kGbM|;#^CN=xki%aK3MLDS4(*qxaa)FnJ z)d2xA0eH+{1c1>&WD_JleDsL((sO6@mg@QTytpCIjB=%O1(ai(dK?>lgEL=UMkWq4D^_?c#j546v=%S%4K$K&;}t2O?r>2Zn%QpJ7)r*zA7 z#xOoq)Eq-w6b-%$4(l{=5&<8YCOnbqhSPHWhq?N6#CjaB_Byg$Z?x^fQ$uTbXjy8ti&&fPj-QXy*m#U)As+ti7aC;Rs9 z#Yitwus9Z;fFONb`yAp1j2wN_Tio#Q!I=k_?wb1F zqAkxWd{&pfn3iUzmsf2|cT;+-Ii1+&boL`JaQWB%f`%`RK&5+bwcOAXeQ$8+y_PP& zv~U_Nm4L3dwA%$cU-8;m)Ra6OgDN~NBqb};4CUQP0rgyA_rn!Z;{#4$UvpMUXTO%kVNyyke`(_d0AgdeDBPskwFG=r8E;yUw2RN;{|V)Yyer(ysLylv;lkp z!!o*@u`{#3Dt%TF%Slf&Ghnln?+>Xo*Xb?btmV59)z|&f(Nc}!@fDlT6(NYcVqjyN znw$jxxfom{xNRboO!X-9ZtsAD&ae`tIE$l76ruEki66VWBfliqWz;CDY2MNn6DFIg zYfBb#O4KisnMk(oUppZ&JnZ7;2J<$gYG>!{5sn?W{e zh7DYNy=I2ugEd{ZdK%U29UNfwJmCqxIj&PwlePr|pB1C(#QNfV-wnwxy1Z=g#s}yh z8p?;oD~vy=k6k+qOgJYUa24#K;Y8H#vXj>Ft44%|!H0||Xo^^N-u$!ntBe!|MgFd*Dp}*+z~mM>2PDs=^wZkjzRz=z=&g z2@+*lnGy2YZ22X^(vc|9uvp+4kWVV;Oa#4S8!9QqGTcUngN%#}a3?_Mm3l~jLt?bj z`SZ6om3Mb{Bgd2lPRt&Eunz^R=$9uY-Y@?6*bxey1qwk|hv^|DJA|n<_Nb@iyXl~GZZs_-*t1n2e z_ftmFZt3oIt5-AJ9<7l!$(@k9YnF2J)-8M1N7mVi;yzdGR?(A6)evWF;gHqbmn|Nnj*!K zX>jV)%W_q^dCgpk*cCoU1Wd(RQWtM(7Tu;OAstvdtae(4jlL^W<>G|G6#+33n&#|b zAAOq?gAXf+LX(_ai`_3DVSxCO@ZCnbY~01Fn;sq}4ar?_5vDk+n0xID%GKI~=iFRe z${+JeJH%T6{)6eAqGAJ(1qU#S&>MNs@Dna9C|VEW6i1^5a0R5HzQq<+~d2j&sr8+v$ ztL^3B;5c^79(J3+xUSX=fHM6OAn3k*wb!F}J{lh%Um_lG@bYHmuMNP79Tb$mlzaK! zkK^ExBUdp-E0}E=-oc`9-_vsx9iNaT*#KPt$1c@m2Z8@kPaj&FGO8yS%dpY=M@C80esHuBxuaghrwN z9(6hAyza>aFY8#wD~;8sFW%~#Hw#gn<_o&Pr)+9aW2GB5Dbp<3V`#Rj>f@@+Kj^i4J36G*Z*VPFC0p3qW`{E1?Xg8mg5}N8db{o6+i{@121)8+Zm2oYjsO?I zm5hE4ww++&SE?HEZ-4%au#4rDPg&269Bn%}@r=B~s$+ps(-u!E6NktkDiJQmJpXkT52x&TUZhrXfXufgu`56JQ7I zj3FFO0PLw-YSQ?n(pKTE%;-vhV<~Fw&n9w`k3jMN+#9>_BIvI=SUXE~Wn9 zB%*H@3i|?7it5Kz$Tl}vS_K>A%4a{mk+F6 zNsdIhaSQ>l=Wo=0G&xxxi)1R?k$}uWNlkNeBm9S%nVI|JzCgYLb_@J1W7u%?fxyl> zJo7_&YPI_+vfU^*V9c{MCoTgb^~Lijn%pieudD`i&c0{u3i3PX*S+^Y*1r zUP?gMW@lqF2)onCgp!hy0(A3`NCW`AgC#0rOp@~QTfE6=_`>u2Cn(bnI)@qPk=HTu z^*9G5_O(X~9@-pL_fA3Q`i$kJOHUB@T51_rswgoPNlkNL@L^Fyhb)`VCSk9n4qm-d z$xSoDSFE|~=F4v1yeZ+d61in9PpYT9*MCGOiOgOw;(ffl=0^(2Q1e-YJop0_4W%jt zdF7uar&8P29DqLXzeZFl$nd#VgGxD9Em4sG_ajnzwb;9QdLSx&G2<%A zoV(7SwR2yzm~cxSZ9Esk0>V|U}oW)dU1!YhkOg$PAX#Yq6^l{ho$rH?(Z+l z<|QE6w9%0fg=`Cb)GXkC)MFTdauygLZxdc@U6gppk~W#GLmP|>=O9=I3w0;XocX;g z0bdO5xsb3h52lk&iwkJ^(^`&ka*iYO0zQ|Ezzwi@7Cy*|Pq1=xrvu0WK#%VQ5gGx9 z5qgLWS_dh{ z$`ctEaH!|u+XI37pxRb>?I$SKDv(OE#+QVu_NlUhL-85;BT1DimNIm@ZqkOju4Hr& zWBDMkMpl-~{;R3z=1hrl{qjcrFxXbiTI_j>1oAirybFy!0$}Z=MTw z?k~cyEj)*WBr~RY?l`1|AJ@J*_|bNh$} zm2fEh^@r+p;W!5UvnY+uQ?Dm^p5Q|AdeQe=3A(b2#JT?lfw`V2To9&4)runxkQruA z6KFB;-338vN9w^e7zj{r1|HUUiJGgH3l&Q;=#zM9zy<`2aTRAGfGv6QA}w}Ivse%Z zUtiCIOg!4r!D#^h^T^Jr{gc8_B-&f7jl~T#6((lt#kJF zxxhp7w_~(+0#GmhH@wGh{<1F!5yS9@O=-vlX$)8yGWDL~BjMz78m|q^i-a-L{W1;{U+ip~<(9+NcN#mw1e$!Bhrv@GNye?;#a@Q3yv{v>uM=9`SY%1ztjv zF-(rJO%Z4S!rNSr?CntazaVDq_~@H`I8-4)gD?>a1K&^vjGgdf1gr`@8*ms9p9#uD zdRV26Fp+pO!RwC~{0h<-u#_|V2f^p~9li}hLb1c1`6Q+ZTwh7<-MmT3ZR9&+l!OX> z>T_jBh#M_}E@0Yv3593=bocIwho6=~+z3hG@}*E?e3+QQ08Y+hpu-_zZ13Ke*Z`cI z7C5}pjGN#x4-E7}aq&Tn*8xDmCtr@d3(pI1BQAik$;mAnH%i%Qv-{O0sUJr{gT#8! zIB&c_N1`{eqJt{1J`6-tRBj-bB8=mV0L?~{=d9+NlqAXfqpti2jfFD6gqGXy!jDBo zbSX?=Yh1p=T{CIlJ}Z*+P}~CpsoP}OBA3@62Wpo*T?q|2c?}4Pcw3PTV2xHCj|IG1 z*ype?Gozc4mgiQF5agE*8>0e4A;{Mk4k!<>C@Br`m^Jt0){(9tnTT5|7ha2c`Ezo4 ze^9M%5UZ&*U`eu6&dGsJzz!xkxaBNR$Aja?rj&|@8uP;_SnaA;W{JA!${5RdnCzHSAH zseALb6$6O;M4As?12DeP9st|HSP7#`wCC>ByzF?v@mn)9zt+mD%MPzKG?EI_#*Mek z^~INw(2l|q*aPGc(QmcDr6@3MKZVVY(i#FFd`DcrcZE98`vbWJF^c?b<>$G7!az#b zv+&K3Mu~R>C)sh7IwX5ekgE z*aE0bU(!|R7i+R_T`5I@3seFj)aID=MP3X}bW%1%4GuId;JDKQqQ()}F;<)?MXvoX zGfK&pSCT#WKJS7AcGJ+bK%$IeW@@lWUUg{sfnu`7uNc%oLIfa#iH%KuQDy^)>0l<_ zoUGidCr&trGC;!kTG^zi#xUvQ?LUx_;Y}o&RxB@IJWublJB!|!7_^1$Aua{exVg<= zwm`u^OZmlJApx7!rH9!b1!{tT^VhU|%Q;n4j{f=|_B3JUHX~#j`~4 zCrDP=J2UlLIJaL|Jn_nnrFBKK7S&wa3S5vVhJb9~gj<+xTU0}5i~R(+5NGkEE3;g@ zgz-5ZqvF1n1P-YOp^~lrDkHm37WT1H*^|UIx3r+35CjTmH zF~fK}S-<+gSxG{Jj()@#R8;Io$XAWPj|9OOdI!lI&1XXj0AGN>>+APIx`1p-7>~yv z38i8#nC=d+oz(j~-l8TahpIb*0i$ktIk3g?cQB>^|5qHg^gu_`1NfE7(|f4)$;#uH z!`-Y6Q3_bNzzYniS4ORkPSoe5Li!(|aG|@DzwF!@#r;{v3#)(wUAKujrNPLW+5NYp zu_;1^R;AH9#8fW}yco*vPgFYSY2^ZdIUCI}OwgnWcX;ie-r(hvn=iaCnY77vS01DBFto zvt7Glj4kU`twF;8*aF|DYoKG{juE+Vflx{W4Mjvn;h?{QUfYV}f~Iz$oyyN>O4uRD zdP~a6RGwWv3-}i~&zR3*D{1Cry*3b^ut(AYw)we>7cuF3C6x#6^~JaNhp{(`Kh2iE22KI0UT}S6)Un!wD5^1+ojG`eJ4g7H z)vx#7arwMwbic}LDOaato|Dj!m#e2@K5nfK3IqY+BditVTy~TgsGAukG;kK-L;?Ek z)EnVLc;LarAFKO4@MF)Ob`+h4xCx!CF2e3L*oXzS_%)xmS_kY+s52&cmcH4tS zFe)02jV4KqiUqJRARu6302L7tq!$&bLy*Tl;X8z@UpZ9t8{%y}$qD0crK=vz=L1l!6 zh38uy{+c#uR-6DSYoAGv zYooP`qHm?t7hjx43mwIwh74B7P6U>3g|Do&)f4L^V(NM%=SZb@({8ED4$x9tp(XEw zQjU+W37a``AZd19SY$E1$MVbN{iU?DEBcV4;%9TTn)Dj6poX%Z^Ta;W4qk?n9d0h& zUa<9#v09?T-rA$>KjofJc`F|)UfMCN6JIRvBqrvcA~i+TFf=5jQ!rhuUJc0?KOdho zX5n|`5V^gAU*eei0}FH|f#Ml@acC8MI*5C`hsO(wFTYySBaPIuUM{<9%|E2vuc%OA zKSQmGk__41n_%_Cn}lxanLJ5zyFUhNYmq%tVXB~3OP7;&w!`cLVI>$cv+hnN=!Som z)PtejT$;9KeO?(lOd-c&e);7ymI;P~wadTHpTQtFJBfGwdQ&&STEmL?u*4kRQ`qOw z(4YvM{@XZE5r&sozBKwyW^$j+>$pnSsaG1jz1nREVG-577g12qjIjrv5=<)4;X8nl zci7VtJN-7levsE$D`n!Wk2#cR`1Nc7R@wY3&5pp0-?3u{!hjNLbMWHn6xpB@WGQ2B z|LwOm@#H5@rS%nTwV{pLuy!rtKpNc>^YfdWBH_}@j9Yu><-AMD)DRd7KTbDpshOwD$J!Au)u3VXtmRDJqa!HrloHy`Y$Ajr3 zLi4|}Tu&;&!JzXp>ytz=xQ16@T8GVH@4kI{ndXrmB`-Y#v$n-XZ$2ygnV&c>G<$j=g2wHcrxf8hx1HnpM|E-NnhvAr9?^khkQ*;Up=9y!rG7dI+UFz zFy;3Q1OF%w^+iO)D~$56++^vqk_4sf&*4tu0;7Y8hXs*t_lkO>gpV#DJkxD?X%iVoacJs?(B?t&H&xh9qDcLw&7DIW|6M6)7EtD06(yp^JC@>H! zIcDU0_wG$E?UdG{uD29CZDZ39mj^;zZzFmOyBJE$yca)_so5QIm6V(O^`9qg;t`Y{ zCEB_$JGM==wfGLjePGAOa6Q^ zb^hW#qQ1L$T%OBeS*M@golB)+1#<3w(T2RLi+yQj2M)}c>0{l(Alc8~-v(#PcA0}vqr=4e`Ck`^4f3w2uRnmo>(dxLe8i-!+ORuQfz?h(4`eMEXA zrqS3{VSU?&lhKCKM6>U9gu3DS*rVV;JEYEt`V)F!!N?VZdJ9m2WL!QRZ?XSoPPeZQ zO*B~Ev>3+R3pnOjT3VhuRX5o7kjZs#ZbzN_P9=&{-YU}88(~I2);~PG8+Z!T!=Kl0 za12$_akH0}{tDCQZvDh|V%ezFG^BPq=~0>4^nYEy?Gh68P$gp10fVK#HogW<`|DFh zm%AMqDPLaedAq~4{XC=?_Ac#SUK|Q)P!0O!wD`u;3^^${*M7cc&0dT*dM$!ai38E?qbat(*m$M43W%Gs2&2ixp~t-~(foqz9F}n=^?~aHvnm7H z1ShBLj!0eF1Mo2L`2zLmS;7y{Lgiu8!skY}b+}r5=9Be}<8X|?mAMv)Bs&97b=%UZ zLFW>-%f`ddesyrZP0>j^d&!=BE!hjgq2tFjZDfxfc;k6=>pv>Uo4+;)O%&#N< za9;2Q{6qonNo@{FL^xce;FMKRTaQ0PybLR?3!OzQbOr7HGI$vOqImOVu)~n~v*5VV zrdmpX>_k1|tU@VPjNbFt@mPwJ)VYNhiinQ8rI(>5b4NW}ga$Jj5?Y2Xcd^U{<=|LN=u7!T2up-m z;pj{JX!Mlow)tU^(>LD#yPOSudQ)*exSUX35jynxrk%Y8y>BlwX0cBb<^Ws)9UUE{ znL-CuXY&SxB2;UT2qHu&-hd|jLn(1Xo*=w82XoYC{d&;C4pc{%5inyzLpfM1QN<^g zgUJG8)jga)&_wQf^JbS}fwr5XMc|XMd(xJ7M7@#dj6p~fvd%GL#%K}}O%~2VG+}D# zOgyC@+Bq`3j?m88;?c~+5rccvg;!V%_%7LJHdoux_MZy1PN+}YZ+U_`=M<1ialyN_ zE0&EtCt1~VgKO`DTmeBNrLyC0M;okLtG`a&BTpKO%Q(xoO;=K@Ymj34X;J<;^Tl(# z_nw~9#W)U68XQxFg@rs^4~dRtm#)$BKh;`jSreKs%p>tJEo@(+vPE}k-P*N8G+CD+ z_K|T61`u(EaM-c8HC?SKg^CBeIN&O#CGKBmJuf%tUzia-;an3}qL`EupWeX;etr^r9|>=()6 zcEe#42Q~DnUEJKbmn^}pqu|tC)EV&f94_t1zN!PCuV3FepWOzE1Nxs*I|8FS{Qaql zDz~i@G_xeSe;OM?-yM2H_3};cbZw4Q`OKHCdbHy_^!DEF1`Dg#x~q4MWYo*n>+WGX z+-W7{a<$y!8x-kA;|8Y>{LXl>I$OmQ#zXUYnb&o^#BpL!dNB<-*C`+p?84czudzqh zhJ4~16_@j1wD0khXc{Jj9cfI_^9lSxcH4b1vpuP;Z7$9S2^5L%Ep(HGeY)PIqND5; z7tkxlGsXtWyTzqTkks-=ZP`Vs zU*|lp(jC_(KhCWBRiR=K)em2;3u0fPoIsm=H#)z2{ zmFk8QIZoD&{go9J$^M1*$^1^^iHyTg6!e~~00S!Ut>>LVja7Nj2yPus_ z-)@Rle4ME&uGC{cTX2T-iUSlEe_!9Dh+%v3WH+B5j`J8tflRE{TKN9#t~@REdNdD~ z2c@eUi5SLtO9dJ(3E_#B<}C$z5NKlhiKUFtlcEh-uJ?z(f^z0w?%W;4 zNUMK6J{rl!cC}r)y#w7734bW*;=<+-VVTfTA2-)hZ=&zNU)t$7DJo;>ai&`I*`{r` zHfOUMi`=RAPYdtZagr~I>ms`Bf&Wx)vKAWh)iuVHUdDhBnhCt1QPAV!4Z@3KWy_Dm z?Og`>;wT6U$6LraM4+s}Fr+pIBO7GvN}9ijK>nYZr(a8Bo4Mn|6v&ji&<#Ug@2OKf zLUQ`DL$|HJt4blSz2EWh+<5T4U1A~P7U`%oV5))XL$c+TG(wLnZgAh&l#lLDv{ zz-1Abu6+0~q56gl1w`RdU9&*Od_fUkej0lP#847Azd=uwh8q-MaMyE0x69DV$_j4_ zB$b%JfYg+WfMk}Lz5g;3s{V8?d2M}T%;?p}HsM*s71o8dqH$_8fT*=)>gf4b%U3Iv zRW8X|e?MMUI^o9H#q*D{`A$TjXNmkCKZ=io)LYckk+rem+PULxUX~7}1~ruM&6^0(chj zjrSdyB^)_XI4^AH5#a3mk!Xuz!$S2hf+;`haXs5Y3Spz68ZYL^{n_pvI7sgW&qPiH zCx?0N!t=!XEpl=mkZtj}Z(z~CN2?ZM@HC8f(U}2w2U{_c$dC3nPb3%A_RB*RiA4Eu? z;(+g06%amTDhsYzla8{cb++G`peMl3b8P5Q?6nKrDaCr{pO|+~C<){zX{UXg*C-`Z zinVXMu<(=NUzqr>c+^yQV|=3^TSE{cZJ_@|B3bukzGUTUZkq%Sxxe&(Bc^O`Vz+|` z3Nu90$qg0g72=q1Q_?of^$PJ5&+R3A9q=WAx(Yoorjp@k2&3_^R-rM|;#!n&Hv3aq zIXUz&f-vB7s{Pi_|7Jxe{9eefj8~Zx#b z!TCXDuaA}<^xgP*P;CK5ibYFfnfJNMI6PG-kkD;Vjs=L<`N^VC{nQmE>5LDm9skDT4qouw~*^DhnQElH}|Eha;Fh*d;gjW#_56xap^jv$fk->qBUOC(4 z`qb^)I6q^N?MuuIKXf+d(t^`gAN51isO|`lFr3R^bf75=2)&xY93j7x{ZoC%l&f&V z6cxoYMy@zF4MDY@VHR+b8%r(ZcbG&#A-!VNs_?xg&rtCi0^E%mJ|->-OjTTItZ&o7 z#WM=CQzKRK&Ix=JAuW7zd{~SkCy7BtXz~VLl$0 z_U&JVbwBgjp3=~`g9pBD>C&Y)ucLheUy5J(xxGkk@qp0? z-Cj)1xxCMyi`FyJ!#uvmkF`z#^o}mf2oV!HdbP6S?b_9=_n^Ja+GCA|B5`Ipv`Q8R z>wS>_qONxzsgR_k7HrC1LX(QpogOd`nCgm+e)y*8=TwIYC)Gk-;>whEylm^N@v{8} z)i$f2A%o5`hO#&{;lc8W=GUoD8nB+4p4JOnootjWf-45KyCd*(cBkI&xVA!xFmF4) z6BqZyN@AyPR7-7E%NS}SI6J@o`aAc=IJM2eVyZ#){YBF@<-g$tq&wC|#PU%DXn1Zg z6v3FMHn=mfmOHR1^lLsm5-(u@o{e^Jk%qp#a+rb|F4+hoEQ~#B3x`wI>398P$t;dG z=nW6|UwMtZt*Ek%F3W;-D8hZ$?OXdqgI7w1a|x8dzK+D1DRh4kI-84X2bv{}8wj~$ zsU=o1Qg*_$N0!zJRSQ{}glb8q!HtT}4@b}C{mHts0+bTh*lgw731^b);-+(J)qqBQ zJl0MB{o}$~t2=JOD4O%Iv@)k3XY3r%y-(?ns-L5UmY#vplh!^+mR(P|xsMj*XfBL1 zhdHI6Z^dPB39<8oWRc*!8sZ#e&xrKSV9j^E1>w;>*$RCOx8b#+z%)V6aU2s76RVo) zEh#w&tsW%7kZ=SD$rCLx%zDt0OioJ5WT0-$#&RFD9<@52(om+C!h0pBxA*m_xXTIs z?I)eFcX@rikV%pBtO&`k7zm$Oya`cmR+Ax!Q1=;-+2z9 z4;!E-Y&)BwQHIkDQs`hYBb2zw(KXb|kUDmCbwQ9{UucX+8b!GB(W6ZOjNk}vXxIYX zTP^XOBqg0utKi>NR#qBfc7RK{Rsk^L4=PbAeSy(>R?(GiE4zQvojq}lC%)+LKh(!| z;zz{mbGDE|d$e8rR(9{2AVtZ_Cre2m^FMwultkN9#89tlG;V9Uoi^4PVmpS}P-F6o zSAuY%AOAG!`1m{mA@K2? zSwIc^f6+TbkRVEuzlXK&?}Fz4>Q^Az8a6x^7yVa%&WBR#k3aF_g7ks@{^LXOKYJ*= zd>$R=+`kqwES&T@JJIVUK|Dq6&oA#dOPuX77lxN)BTilB=E4fxHo|y!ZFb?jhn53@ z%7f|#Q}b8$GK7q7rND);Ml<$sa4}#%d4|(VBQ~LO%l#(HqNWpG24eh;X%3EG5b>cy zisL4X!UoaR4KTKV*bEPG7G%FTs9_Wan>~hj7r-LV&#N|v7^oPMrU8#}ndoX^Sw0TZ z>XH(k9&i`XU1Sg#!3N_19RI)?@`R?3NM6F!8Nxgl;9vlBfTssXL?Ehn@83TG;fC_D zV>9&DxfmOdX(;~DmTZN;=4CYJiO)G#G;5F&GB6fDprAl#pFepsyB) znMGWju!34Y9NW(0?3twmjq2XL6O3&9Wr)cQQl`-DhBW}!=U5&m=e+<9E}KVSa~Kw3 z7>m{lWKH;JYVA4(h6V=K5+QYB@F>0`%{4-lxu9TW^3_*o;Wftr-5PQj6O#_eCJ3-Nz~QlkCVqGr;%T&fV6Y2!AG--f4UD}Ojprep z0fiM;WDZtHXu=6azVym>x4_Q?-OqY4A0rbNWYW$#KoE)_F9NH8$4qXNFm#e3(v$>i znZne3Q3I}5%NA`kKTJ%s?b;O~EW)rJYJ(%r++Mv)Pe**22SfsXvCdmVjyUd_HAD*S z-`n8k_lkdH!KPlTEW^Rk{M8?4!3aZ{#T*9@*%OlMoR0P492`8%^^OL=32}Jo@&2>Q z`rmY_^HMU9Sh;j*TyYo&$BPogz&!1+p8RA5KG5_hNq})l_C#;v;1J)4z_ranC9IdL zI5_xxT_@i>eP?_BZ@gyRs{!R(&$}&N)@|iZ$xFQlZighfa=hBR>hAYDckR+GmTP}? zzhI=xb7W|!Xrxkem0SZFMPZzrD!nZe+rlx2DD2FlZ5@B>5Mb|)&KW|S>&k@8Fqg_M zXs(S>x#F{_N6z%Qx15}zwB8L_w+j3l+vbTJ#6H(uD7TV;1LyYZ*Dqo6dB*efT>fx? z=fxtj*4Qff+uL_00uKL$aNOgP?=OFO$Q6CU+H%np#{G4-B{=wgxhzRI6KC4!--(2gnrWf5Hk&pT1q^A#8h0RiG<2(!v{ZgyW-?ks?OI43efs8ZB(Dn65}IUN1@Rq~#10s>;jGI2Vtow=#-m%UwoVjvu`>R4%4~v<{e$ zf+HfV#m1&LPXly{`*5dt_sxl$9Up#MBj3BrpGu`aT*dx!ONSAbii(o& zxytRM)B0*uS5f*JB))zl%JNCCf0$=~*Pf9t$ji%?^PJ1U;yOd~Wbg{gzL}er+2NaR z!|*?qI5*Vha|Ko&3kzmiS?A;=Nv9YVE)z=D2s8D183$zf6PLrNUY~F(0~ULoRGr}TZCl*U$6wV;pRts^#V4}3q~h}u zYdTlwo7{*MB;<+r( z=`pdoRA3oqn)9}Q|9uYc3idX&%y{`(o|SXqHamUyB6$w?gnMN{oB1`RO>dzI60)X` zU7~OdG*6y#$$MH|)~aPV3F}9Z-fVtBGtihh>^bMKQc=r^xoIJ@=Z7;35KX%e&<_l# zU3%huDB1V$M%InGHvd*c%jk36daO`Dg~J@MjCG_pxF@PN?!ju+Ki*se8gKUf```5U z+XSsch^V-XOtJI8J#5@}+@~X6zVv0%i!aWyGBHhp%s(l=@-k02gpqwIRGVa8KBmxKy6NF%Rwnf+fmu$-}8?vmVTpJRk~B_IM1r$ZC^xKWSDv^=H`R8OgPlN zp>X1s@&&%u3Fc&8h-huGn^&+RBl~hwzxlW&9ZCf^5Y#jonbnMcieMsM2MdG z=>ey{tCa>CT3T$TEg9sZ=ffR)3=P@SdFRXgJF+|^+()YiF@c6eGCY91WJwp&zE-cc z3y*GHQ_Rec!M(;5&dt`gif%)m@{wzpXa@jd@u?q13cuZe-+vdaS5pUqd$TZ3^Yp^P5J} z&Nj3H7cY||Bb}T8hsoH#`LWT;$zeJ?puHzRP2`6kZiP!l_r~U}Tc_3e+-0OUH6T^z zbK7Zdg)F|^5sBg9RoT`?-zlw9Tf0TpJ^a=)|N3y!&f!Kg+Ew0+2(zkRL=F1Ug1^!c zn;<8*Y2a`%x@s{`i_2MBo{U{^?%)df6A)+O~;KWq!g87MAp$2#FS{HCm+ zke}UaG!0UFV|lQxrh|r{sh^FO#*HMSqUrw14TcxGdQ&}Nj?B7~O35O#rdxAJH&cih z#9z~UZ|)WqQh_yEj+1`U^UGem#1b2&TV2c&)6;!d$!VXP^={+qFUY9FmWmCNBpm1B zms6XZkT5si5}gcZxq$3cO=_`W`b9qIA6`c*c`jRKnX>Im@!N8<4*JdHt?DL6pJg~V z4;2?*=u1)(VvfvByJ78(%59jyz6Pc889&RJF0H49eB=Fs^;G8@9nN=%uZGQl6+MWJ zSX&xNN~zGsTUEdIl)nrRYgfgsNioA24ZOJG);-Mr{<%i8R&BPfX7Z+s_j<8_GI~>E zw`{Abosdn+4`%e=uiJM`mU==?7NRrMGWFOBAcR+*cE99|111rD}OL}+tt>ArGb1r``rRSMg(Ua|> z5_4mE(5asj)6bo=3}SbUTOhJ$;8{7ls_OuG_3H89;1JWwi|y0_k6mJt?(Et4iHSSu z>C4K6w&!h?%=tpv^=Vx(CCngr^we4`{Js7tnM_PQvP{Zj@l*sHcuB9mg)N;u?WvqP zx1_SlQg(cxDvvL-SlHK|X26~#y{b!;-zB`?$k1l0Go*c=SxWZYjl|)>^W#8SzI-$? z+%a6}R;*KW@tS%e1hOqmIya05U+u6YKC@AQ_DLNbO520S;24P*6-n(G@}Am3Wq2o= zJ;<%!%h9Y6?u9EPH@CmHaC|R$7xsS}maXFE&DSe|>Z+Vg zaH#EJFldHXLiU<;R7cBKgcZh=Qa3A&C8nm+Wr_8iEYjO5P$Cj_#&eNjw$;gtGu<|f zxkQt4Iv>e?d}JhVyk%vEsh>{TYhISz9c&WUcg8-yMT$`lmMJWhb$1UzMtDDaw)l)# z!DrSMk7{a+D{f8PxY2h1ell2n*pA$=#fTLQhVP6?wY~QE3L7{wIv@MUGhHl~^(pQ) zs8X_4VcYi;XdDQ2?yRL1?@HNl$VV=srPBS)>SVS@$J@qdiM8WVVf{m zw!!|~TVIhj^J!nXSlKAk%@+Fub^-QmYoTV!ZD^l2_pV29>#y?c$$(@%$??%q)P=J% zSk|t(yl8C6w)i~v%)H`>+KPhT*Ptk;o13_T@-O*GmrTD(eJ&n%zS^cEoSx; zV7Z`Q+ClG__Y+*}R(p6NfE8IwtDLtID0IX)sdNQI_a0hIQSotTuiepk`Rx&Ri#Ya< zRPa7viFaz{ld^vUsfZ`T&G8VZ}wlpB0i3m-$NI zVFg&PhekwJFtcc&UPYKa1c*StJ%wb&(iS?()4A&W=Un$T0o4Or&h&7(7z{o>D|puk z2@Ul3Z~7NUmOb)QB#dBRZ`}&4J3w?q+VIcY!p=MAIH=0Oh6BA5fOjS)aFqLP-1C*F zXeEQ;J}*!zoSKkgL(mBEON(8ZV9!=4tch_axUZ?Hw&<=vZej~Jh|9>_$Y)>v1G#}Y z)(YiObM77y5hiRDgjj!GALYETqKQh8SJ|rW_hl7&ESM~VA=We+zHKoY5)`y-{9SDf zgp_!Cl0xpr1FHdZ0M>%8!zk|{aa6J zmw_{*BW@KGk$Y~cY^?#92iB^(Ho#(_(6k6J@N5iC_F_1g-Tz@p_U4m%2ohx=a2RS3 z7}S}Y&Qj}M>{~}Ny+opq2^t8AZ;cE)w3U*4!e<2Jxw?i_>si69`07JsLcNB2&BmY3 z4h4ADItap+A{GXD9w^G zcl_^76mEqR)r~AT_7Gpa)B=u z3PdYJP9UEbGRDxrdbr3QX6bW>pLA%^b;;5E+0 z5KX~@&;WxZkmj%}z<&a-3*ua|AWZSV7K}@%i+0}PRR_*P zoZ2!q@2Or>i=)~16CG(5scjZ6x$&NY=(Y{;I%fT2+-Nmd zCO@|aoL`ty;%rrJ?EZ4gmb>-jTNE~P>^t8r6Ft90N;1XHZ1;V+023qIWBffP2AY$- zkc&pge0%5+paJOl!FC7*D2RATtw?eGiz(Mbc^Y~)Xi%Z(A|PdufZ@D;JwIkaM+AU! z*5ld>x?it7&0NT!JKpYk*(y=7idFj}K3`|vAp0Cd4^UoUQHS~Y1C*~K)tDj{&u~o$ zAqQ%h$!1Nz>iZ_`)Vv`$Ncs;$aP2-PSrg~TMaL_?#|a8r(U+hOH0Eh}TdRtjULy0< zr}>@yNmNQ57mPP9!uV z+`Y5Y2@nt50p?76^))W0Y0(mE_vMdUTLF-HF~tA{1RND5B_&|Gb0NCK_c$@|wz2MR zIa@A37UDqsEpzcOc~7}{@L5jd*4p15jS$c?ZjT>f|g}0(=?S zZhvCQSP$myyab;&{cE_Mrw&waN`DNVD|1X=l_xYIgb*j~GWsjV#Hp;u@FbO$lyyhC zH=e;A>SumJhIh@Hs3I%GkVwzymZik@iE5O@OA#Rvv`#~wY}x7?>s2(w@qc0RAi-u2IhY>+kf)u%?Wx_) zB_o>lETs zQZzlu&#V0{txg&j2RS7LYo|ubp4;GRLyu6WIMn(xJPd1oYf3gt-cqMtr8^ywv3f(T zv{R^wb2jlbfYL~H`YEgIY%o*%w)H(x*}!~F^A)j*SVMKlgn*Nf$D`4U{%(1r)al(D zQ4be~!x3UG?)3KMYKpK98&%0@Gy#~b7#*+Nzc5?$1v(?@*tm&T;$*B#r$bh=(TQ6*4+;- zD8skWTD^+&<^KKqrKDV78?hXITf8z(o~s2eh$(6X0gZ)ek64&cdD3Ex{N|q{bL$24 zZ}yqPaEpy#h@q|yZXSWqvT5G$>^zRGck9<*7a%JFxz;~jy?TJs6KFCE>qF4*tlrsR zI2Ltm?jWNz9&R+udB&Eb2;aD|0Sp8rQZz9vtgMp4!;OGpBPs{{Q?TU|m*au_8yMJf zM*GQEe9o3Bvb7oM2=Wq?5{0VWRf=O^K*+&R4gxZEI zDLqalhZ3jnrdHV{)0=SNEaR$+$k$G&Zc|8E=QK`sCQ+>|1Dd|VFbkN>+lWlZa144^ zmJxTp^FUZgNTSCZc-&VkUtV5O!NbEtnfxTP{|Km5us$?;P?L@RO0N-?O#Op{M$OJ^ zv_gQ-ohfH~79&fqwnF#iJU%Uvwq5*@&i$;{^+UhAf$j5t)f@kOki_~&14P|D#`qT3tznoFgl2DLD3kk*Ry4y6rq`FJz^foIF%vVskrIbEGU;l$#QBJk*sjx8*H~LBobbWSxPPfsr5>?*(X)!pVzHb0dnJ zq~=|uq+me{NqgC@#a>)~o{#LJ^^m2@vY0^+1ZKLE;iD-$Up($@6^crc51gi;3fPc|&lGslHGMEC+9|nCG z;A5YlccG7klvNf@0EBShrDvF_f@?t}10&TMX$^q1DX9g*z<~W4aN5UZWoV&dveB45 zeUiQ5BG~n?KmHAQOr_Ea($0K<&WVwerN5l`-Sxm~3k^y}n0KpdZ?gRCTUFd_iI58a z9nr;?dv0yL_B1~SJob-n{vcn-P2+Xo=+ARfaJdsDSuJD^U>i+_ zaq7yS0Y7=wfx!|yLCMp$;(P#^&4CbbtD@6$rd`i@EN^#8^5% zH5VW2sUzcP3QQNODIEmm0_!&r$Ip?nU3su>%OU>3oT?9Lw>@S`|4^WATce=nI@X6% z^kE?(A!V7MEj{Yd_{qF{d_7DiHa|s(!{tnxs*A+MEnX4{UO_$@5WzE03&N0*t@;e9 zEGO4)0m&I{A?yRwfMY)^F9*)FuFIr8VF8-3zVRg}fpLI`=LV+`5X_Gu+Q&XzXPe)% z?$pT2miltF`n(Oy7HRq^v#x_5uz0iE#%l`vW zM+(9*uIb!f27E(7TVUA7>T#kNArFWASR%>*mEowh9;Bxtjbf@=__4R78ZNeU{>wNhW4XHplp_vaAfZoV;lwR~rK*@fyB!cyrwckapP6iGZ8dV& zpjvhh6{zDa@k`}w{oxN#-U+{s4+^>Z##i8Wum*bN zTOVAw&~W+t$2f!%?+uC~lvo2`-DeuKf!d%oAl(A=AXo%F+^_rktf;Ccw3gt__?v&H zy~x!4plG6RsNR*^GcHVPP%9j-@28?n>G&zm2u=u?yzq)tR8*Ya4L!R}OMw*pw^J>0 zUR<%9+?b+_!yf=E)L1`54|Mb3;8>4>!O>BHeWu6oVxv=l(>9Euu$92Y#uDGSZrydP zBshnQ^WkU&GeXdBzeS~qDX4+3I-liWnNxwq9p%CQrV(X7k9Os+#{ehi}h3{VZ5cuomNdG2OGeVk0TqntTa?m zSUi}b3m|07F^q{4{yju2b9%)qu38v#55C=^hb>K7by(Ye+efXAi*{J}fMkgg!iyqxsu;TmooeMmt z{T9LFze&c)3fOzF0F3b=gs4ZUi5xcPY*4X^+5$sxdRG5z!#aL`I*M(ayVb(=f|vq- zh98(0bL2i*^NxG6&h{J0rA7vKySVn-Xg=rUkZcp_=5Hnz0Yzhd{pJBrw%gmHgIo?A zUYkD|BoY2Ga^nHMK>>ITBvagsIGVNJl`t|$CGh4JI6@Nrf6Ae!^zL-FyU`2X#pAQaNgd=pSq z|66wwBJuWBeaT1r??Ms4AJ>`xXq{R0bLgV;!U$VH_Bvo>$R|VFifHPSAMd?+3|Ym; z5mx5nGKs_dKwj4--<8cLmY#l9`ns)c54j#*6ws?P(3UHP#5*Nx1iFD1P_n`4Edq5A zSOWZA#X}s6CF78}hO9X;aD!A-RDg6svk}?PC+zHo2;+-5AGou>Ub{AeWJdHAkSvpi zW*AiJ2zObva;2sulC@KTZjTbi53t7Ya@3ro7Cm&yI8YrSHgFA2ngRUoTx!EU#sB4o*gHWR>8bO^Hc<*SX%WfBd`uo|FTb_aEi|!SL zj&5F0k0m9q2R4$7`=<#QC3FdINt70^b<`xDDL8A;nqcDjENC+1{3>~mf{kXgi&G4={e`p$)20bw#d)AR$J=xQ( z7_QQMqo6d_HozBI*+WA@SYw*8kI6%zC@DnnC%&Uo-fE6}0sjRD%^*_qPSG$wR z?EZqI;TznAAVAM_iv>KccXU8nRRtm~fzAN!7nUmDC~w3yl3%J`z8uN5 z`+%xf5^}F~q65cgWA@l{+`v`9ul-qb#j+yKhv6 zyuYrd&m?BNXoCMmgw z9kUlpZ}TqwByKuyadWP3Pszo)M?W7iRYOFE$E)wTEq7fchBK62yzvsz)iG_|Euo`A)NE$^10g@yD(Xx;T z!Ad&yWOKkp+-@{Kk>P$6h+_1W<5J>@yFeHK7Z?>KkO{vcmcpTE99@~<27d^xjS~Th=ly`ndsAwq^Mf!bhE7QSnV-;W zW%k~TDq?=xMK`FiS;e}VvS8>FA0=^~LhH8X&vf;Eh3NhMrv9a?tBX%1&tJvreU?hs zYlVxft9xE@hF-hxw1~Kd&E__pdM|>g+Lk>CIR7&S(jd-*{SwMe-4II*3p#p<|12II z8Nnbxtr)WGf01eFiwS>*XA6Td3*jf`L~1l*ysPiT8ZWachsG2dl9%aG!l3s25eSJC zG9qH3U>zbB^6nvm(h(9*EFFM0;fg?K7!ez5=HXGK{`k`chFer;K|$7us_s8vwz|>Z zC{bkoh`|h&L=0yC_$NJ<{=UY}ox;+!`M+NRjrmIurd!|Bz{z}i9u!ro`+j(K8C8O> zE>rdq!IOG^TQKqH5wj+sU7{+D*-3_r@KCPVw24Ts*toH~zrO{N&V+;=RI+rP5Hm=S zQtI@xNyP$T2==^>NrGylvsFuGETbsWlEWTj02eC2lGf`F`RW_tQbwLnyn z;x^3_C>hnYU^DAuj zH%Ar_l#O=MIKqrKku8;WI%vE%F5HVb7TMpm^899M)}jVFT?T;{Qc_ZzH$Q~_PsSNj zb`eN94f*iyd;sMtKmI;QmXJcl&+WI=xgPRi^rGb>f8KptK^4SI@?Bzf2V^&xQ3WMm zUMc8`?E~A(O5VNS%_`_)JNcJTYl@xb%(f%7ve{)uTaIj3tXMYi^s=h1Y3Pj>%d1um zTT7=^CJM@@t(8U6FQpY0T1(WFdi=z<=gMLLq0KE2YIBvl2fvYN~Pku2GlU zWFN~Vd|=YC(uFd0KRVZdo#0@Y+BHDyZE>Mgx`blgFMs+p4{=3ruTS#nnJW)qi7X8e z3J(m-#Jn8!HjsB>+V>7d$3xOYNR_b|*F}1O%`9Rln__p~stw8a*RHAS>sw8WlKgKd zPLoW_f(-Tek1+3=G?lqZ95mizF6JdIAOm48GRcdXBa05_g^GQnx0aW;5xWwRZE;=8 z5VEDED_4dgN7p;B@71f%goKLCs|luQ9+irK>rh!YP4^whkIc=zfDvF1Jr`*R*yC*6 z+{BM+uHAyXNffiV+E9pMtXz8GioZV|i(Bvj;S_)^2!gp|>_ag%Z(;X>5bJ314PT)I!4V4wDm*%;ufFW?(r^KY`LzjqjT&szVUzv91V zVf}x)^F@p``d1YEyJ* zq0!#9Wy_xKgVEn|EcUzN?fvj7@4mGPQkXrW)epIR$5f!EuI?1ZBN%!mwec;DIP+P_ z!OeB(ETK(dlzX0V^dhxYSlApkavT6yKMP#L@fYqCyy+z zkoEA92SFka&fPQCVlR`KRU6Ik5FkBRVB9g9M3~zsf>U4^iWh!{uBIz^6zFLKDF(52 zLcj_>@$O3-3~!(Xa`dRL#i#h&>WIjKe@cHFhxX&0rz7m*P(or0FUDR(+}5y}x2^%cn#NW?k(uiO`3yuxs_hYP);Rz!GU-gFK3 zhD9?%5VMN=Oj~QbylSZQ_0c7K{KSudaRptUR(u-_g-(5<=)Ykob>7i23CKx#j{-*rj#^^0xdq-mBHiY$9%lsSpwx#I(Tj7f zKeC?h8<0FK1+B=JeP|kKc7?)J%!u$q()xB}$Ha{k=@X;Pc!M8OREg2%w)TJf0sQ?< z<$PRKC7M(=xj`TUK0<~?t&0({u)L@>72c6{wW8CN~!NUwPDWYFPI$oc;>%o zg%Kb0zx6>$(#w6?|E*(HU!z0j>L4x-^V^44kzT@Z_dUgtYR*AtSdoVfKxn&Y5fJK^~=#bKj*$tlT@gZ;S z*5V_6+#KTHs>pbGPQASyZ9Kv)RG{jXK_b7b467Jtu{?$z+Zp6v2$Q-t-WkH5o?eLw z!I(vjBEKX(t9S4!ud$F@lJiLCN+I`A%E_0pi1hNib*7ibrd3DBN;?;IO?I*0bkXjn zzdZlu$Lbltud|*(JpnMqBW7d;VP`a3dFEVC!&z)YHGRA5a zwnm4Lgc}h?7rR7>yjoR*>_J%Dc?VB+c#^fR4t%QiK_yDa+Y=`Bemv?Jg zE;`s-_VhET0nK;jZ;W8cL!a^9mydVu8*#ia@dJA*^=hKl6^w2WGYW)o{H5jU!J@O@ zF|10we2yxY$EbR;;i26wA|hk=+Dc#l65 z?)zw$mb2yWsniwE-?rlAP1R|WZUc{xh2AI>H-BYCr_(X>KW#z1Fhb3xui z+3`%Cr4s=<8Kq^94kX*uB%CTgm1yd>N%mDgl`c*;9cZgIt&N)lIE+4K>+9s`sGYq_ zbjOZbI=!a4`u@Y{3gZBlN5}P_>$5$3ljD>&x!l7WkdjiVD#v@{m)%+LuCqlGCVhk| zU5B5_JN0$9b$7ognl%WJZ*gPwG=j$`pBhpze{r7l1AR$p$)ohLUC90WstStFDrk6T zFXQkkE#G)?$r3i3N7}iWXg=Kf>OVpAFR|t?!AJ)=ihDc{h>G^Zm7ZC68$%GncnjRI zbZ9>&K}2zaLe8*Dvo3o~uM@VZX>w}eR)%j(VdnWR^8?)JipqSdo0BJ}1wETA5?YrS z=<5pz2v`lp!>EiLUS*zB)mSWw0AmJrup2{To4CDaqESPjm{S2La{U1Pl>+Isv5oQ7 zHH$CAS|o5HoDlB2$hiUt#E2>arpWBR354O&5~5&AMxqaIjWg<|EZbci&6ZiRw6=P3 z0;km#l$LNtcVTD>mH^-0bB93~DJ*n{7zK3`Q6M5B5=AJ?#E1)UZ5zM4riH zSCJ;cW$JG!)KgmdV53>MAF|z%+BG~3v~;3o5u|g-rbI=xRsjQ56oesi?o&c@ zgWP^j2?}pnQ;;N}dWH(9^wA??NEN8AtILPso7=d}h7F*kMquPjL{wMaX6H@*cK@_C zY<{R+k+&q{*bRT_k6_^3(9+e^Byxjrr-x=m6o3Z-HnV{faH!GKc2n+DE^GpO4Frf27m9ORv#3U6`#|v(RdO?kwN;o zBXCf9KV)yZ3i?n2s%Xd=v(9L&-GcdD!@V=?$#8c3oHU@}W@cs-&Kr9-u0VCz06RL= zB9N<0Jym0#h2X z&V&`c>cGg=C}B((r^=1y2=UaVAAh{DX*xUeQP5inp^`1iK^v@yLZ!*p4-XuRN>Rb0m z+HP4fIDz0(6KR36vhJviF-C3bcvMyfuEh+3;4wXQ>k4(AnVm&=XRI7trU$U|M7ZiAajl{Vsn)IPX1NiS!UFp%-8i22KQfcIX?8&0_ zRMYakahe)}F)amq6!8S$gajF>rYAt)-T3D)#gi^qW0DH}2MoTY-+~=)qR$b7kIJ=D zU;>8^h;?KkcJh;Kd0~FX&mcq7G%G&MM6llanN?p*X?J7pZ={vSY8LB6{|d-_cTH>+ zc7hYSy3+W{_-vZwJU@}@8#XmT8vF;ZnDO&hN##srj)bwEzBGLJ!`1s*;VTNQTYm(k z2pouT*PiWopb%jOO>4ALz%4l}j&+C|2lTHM@R;)Qom;oYfGWkKjrSC4hR{96iZ(X= zKz?Lh8D_B{!gZ!l>fSoSjtX82O*l3)a}=QToAXKMc(Mig`B8GZQOW@jAd%|{V>$wS zpFTYjTjR&O&lFIkx&$iLBLwo*iQok{@43U!Q(rb#{GL zzKA#1?I|oY5H&p7{{+Rbvx5UZb@ zNh0N8zRFDzMv~pAmCjSrrW+)Qy}Nf0s(Mbal%gxV({}p-uB+sDf_r0xp2};pd-! zR`Wc?KG+K21*2C z&Yk-@y|=O&^IT#>9qP9!yA?9HY`QqIGcsyCoS+tf{unvVCy?_EBp;&R^Bv!wtQxLc zjyLKXd}9PJ;E4VG_uoTNktLF#*%4Rv^Hk(^mVrf9&rfQ8R4sCB*6Q7!k8V?abJx!aqImri}*b+F9-MOH@GHY^yto=JAt*cYBkWq?G_LeM32QU;GBsG+CQ{2q9`DquR6}JL~SbKGf1@Kt!=p4 zBvIP@)9TH*uLS7`@E-=`=%kg=iHYPhXCjBFj{!ZS< zbo^8j=wD@al!&`e>KSiFo5>J87Vrsh0vo~A1n(>A?}>( zw5v1JNm?g-7rFO;wD;{{G3Ni@POG)+uolTAtXMfD(Ns!VY=u;k4ki^k&ve%LXoYMw zVx@DUbZ$y&Dy2f2P;{Ir(+Q-aEf4)fVv0L&Pd~t)Ew-O$bqtLEo5G~(uq_VT!D*{ zUgnmTSqE~uh6KHsa)`5k3BVE6&4~IFS385qn5ujCRxDelJLIZAJ_9aFGP3VIv$ z8I;6}chel5oJd=Jy&?6){vQ++M9hjjbnAE_LyNyr($2#6C$`^x8|!^y!k6hnNqi+l z*&_1<<$t?H^+k-|?dtB9!(+}ZacBAz+M1-0jOK0T%Twaki8dP6yC|9*=XCYf_N{}D{q>DK%FD{c<>d*1Qx{n?a$|Y!2`G&k zsBX4UBSE%Iq#V11kVQ&fq$4gw-%r!)Yxy&sV&Kto%;{TQ;A)#PSKvA z=mUm31#ufiqxUzfSatkn?WMJ8<3^-VVAP2ZwmTJVV*s4M<}%P2JiYTf)Akg--nL^q z`+3yB>jLaQR=wxVJ3MLepW!lw#Qnco0p7zO7c5}ofQ0nA!Pd?H)&^>WgkWmMtX7#8 z1$=cZkFL09D)W>!*!N{Ray`1*Z|U#dySMxBjiV-$<*X6;`4HJtGiG3bJO$MnYBmj% z&6n?O9x0;u-2b+qT%Io@Kc!$74`vfcqJUxQW2FEsLy{^a;YxM%?P91y_GdL#+_^(U zs?A(ZlGIe3bw(YcsBaG*FFD#y7yy<{+??2-!-j%VP9i@Xl3BTS@0_t=PI63i!wQ+Z zg|>k^xbvHXe-50K8HC;ePX@WiLgtV4Ev&AGvFXD`NPIzh*u}GFv7|lUP-}af(QR1m z-yhl&&h>Zk)lob3gt4mR^i-F3=p))S%NifI;?JOTSg>FL#-PS3`*>Pgr((=r%;O77YjH*!6{5C+41Qe+|m>;#OUEsv*+gGC@Y0V8<{{v#1Q3pc)0$71CNk| zqK{cV$l3v5?&UR%hvY-KK3@{#YjG1@*t%En-A7XU41=v zRP23==FNM9-bM1jwH1izy!FIy)e$5o;Y)V;MK0OmewDHS)9k5CiyQB|qK%D>zpVUy z5q#0mXNF?qV{=pJS>*C&J>mKh7TTsAT31&Gn*ppwMFjg13g7h$V z>XCE){yR`^#(BTKhvTUV#R(-{CXku;Yr1uF9v>m1v2aQuv?VNc;C8`^Fewu|#$rPY z55Gz037Ivi5BmjlJm? z91sdj*b4Azh!tQ|BQnkZ7N~)(;5vv#2H!8%t*!svJGt4fiS)dsOYJv^OjO{fWKU-K zX!zwh{<3a%l1Da&n$Mxs`;aY6On=!k+Oc%py(`i(xU@vTF)TYZtKRB@YBq$MluDdp zut9CYPka5^DM)>L!eSaDYKw;xHr-o?FnOff|M>B$?(Ibe^*^zdYwT`argqd)J2q~p zGsp#*(uN8V<6ZRj?!Agm6&)OO#Sg=dhBK!=;?^IALHzxUGvl>D^ z`<}>+iNq~5%s)e?+hSoeTh-Oob@uIxPe>4&De@TN;oe>xh%YGQn|E20v*ja5Dd+*g2g)6A*DsJeq zgfRZ=ssqyCgBI%d9+!NOW1eGpG=;<4yD=@>j24s!7YmszfDsQH0i>}(!NG3_2J-rS zMTO^>(bV;emW!$-Z9BEljkD6qHpxxT9>KXiSvc|FQ-PCU^HHo(BxI}l92^Z|xv$+_ zpY8zkpT|!~?|=Oo!_!TE{q+ti#3eucPy-8%uq4I62G+!o{_Z78o~Sc>1w(*B5Se?h z#QPEAvvI0xDZjt+K5>6@dI2cIJ$|ETzF?1uii}J(&fGg+rPxW=R*Jq&-fzqsj>YU} zC=t3=Wx%C~4smdJI6A+AH9g{*`7ar>l7fQN=}fd76q+qr*pj$`FEc`I@&Ya>$e+k?fPE@(i7y(p${Y#zPfEKXx8 zga+HJe)Ke6$?fGq?B!GEY&q0YRb8z$JZCRW=-R+f?fuN!Y`y7PNqCQ-#;4A?g2RTTHrpgN^il%=w0a^=^?2EOfen7{cc;yPF zJ_B?@7M32`Dc2v;w5uO>g+#o2I554z^%t8+5{+p{vlUC*pl8NkZ7RcQT0=i#8z+Wd z6jX^Tz$9XZ7otd+D=%xGaZUI3_C}6(J~k{<*337%ZEcJhvvZ@Ji6xSzdrx>Fqg=e4 zV$FVNB;39u+3}L~ezT6dGIw9a=S2;yv_gdhcW4J;nxNULzZw{@tBo(m&P8R0`}_O% z#OLFS_c)^(!OSxBMqQ2V`zu10%8foME8F2yE{ljmJSRjot)FEK_%Ey6M^aClv4}l! z3v#vT8~QfH1nvOEiZMEBf<_>iu=nUStG15bJtxkj9-D4B{SNmK#{g#z_iota+CvOo z5(?zsNi(Mhb!2Ft8Q6n~JsvPMVL!Y!n7TkTT0eg(V{B!_QgPWO45oRBm7)u;6CC+a z>Qy`%NS;%OE6R{G;s@F+jyy#&ZrL_@q-Mt)U^maYx{OPGeSl4(3TH~*LXE#=%NG1z zQc@$UvHRIG05Bfdl(}AnbEC5pJNeqcKO!RutO=hPS;NmdJBevzZ>L110tN*VaS$kr zxVR}+8#*ohrAuXP3=oM4RDLmuM0Hf7=|;$w5gR#Hdme=F`}V!YiHRPvnzB%4IzE6k z3=!zL@fom|XUv#^=Vs&5GMHq+>+@TS@(T(Ef%5X!PU26uN^$%}W_k|mr6G+$w^{VV zEE40IUIt%17kJj@&DT>>o)figlN0S(gS-wN5>!tR{L54y`tySAS5&pxN}*MH*P?ek zBc>>e?iZqhloTs`XY>U#zW+Y`&Gkxd9Xszwu3C%W^^aoI6^O0h77!3X zNCuWKkB0yN1zbw`8%_FhadE5%BCh)ex>l?h;C2}DSJy^hj(>RuM!}6?*`gX8MwWFh zt?^|~`+=6VU)vd~99&$)WUOjXUT$)Kbtp&?*>M*YT<+iauHGUg?kDMI6FMe1M;7MU zJc^h87Gu~^GU2E1YZspaB9-*tEs9_t&^ZcR~sK9EDKE^oLsuqdwR@tt!XH6T4D+IU6?hP0<;^qlQo{k;kTIE_E3pukPvQ+l~q=prB)JL z!EPg_T`Tu($(@uOtX?fGEqe7IvOi*&AzaP8P3m~M}c#TDH_){{%~UzlDmhXW?8GTEw-^tQt`gSFr%IV_X$;nHzMr1PSiwOi9Zrv!BsUPD+n@87)({Y5~J4sO42>2Qf1k_3q$NZXcz z+8fIqftTyrTVfJa9IPa(R;&Oa+IE8iEeKvpTCl);=$T+S62O(;cb*4=*yCIXR4`gu zfWeHJCZ)#1hDpTOM>tU7J)Ay$I^m_`=2`{bZ~ETJ-mk7+3cNs5^I}df^j7(8r`9Q9?MQ-->fuM*D_+!AK5DGbKlW+GcjFXCujb-nbcYk%s zBN0zKTD$zHTb#lSiQO2b>g_!ORu)9r@#Ducjf=TS8lwEHdoB(R&s&S$0k*u3@CQIT zCH*W>-u0WvwXCkTN01}W=oWc5CqhgIcftqu-}<^buu}~i*=KN$z!BD7QDKSglyg6u zvqp#T!@^FC-bHX{o|e^Hx2gzfQnm8yyW)UY;OWX6d|_x`oG5W0mlXIL_N4JvgjuT5 zXuq^pa=jz7^07tMJO6?~9ppgrJ3JH*Q6}MWMzutgqBvTybD&|fQPKVp^IURrfP6M2 z5F2t=P%N~)D?hPArp2Gw zp`kZ$6%$tNF|=V*r%u(;(NW*M8)$f`*}XsTXABmfMPXx)s-@zL*jfn*1AILcpvW1p zG(MLK-4^;(&R{QETlCV&+C3k8v#eJ0FT1xC{oSLZqwFILjrjh?{5DS-dhaCBgFpkp z&7gU4R#7?WdXKcj_NzY3R1G^5-dEsR?7EplA$z>;JaGbj;VTr?Xe$>jWaBFYz1 ztggSy*w`5EiV9N*$s*vbpc61FDK^gP!$QTkHb^I5QZh;Eec=6zOd#244x8eRS-5yF zBF<#7JQO46csj&I!Ei@c)7+42+=u(=LM~G?b4hb_X`=f@{pfm;T69(k7Xc(0^*z{b z*Ba`@GCLYri=h7^m=VO47`nW}je@)*gtx0`_A2(!+rrBt{#nY4xSdklFr>x?VYdXT zv2*9?p`EC`u}+h;;$HUl_I7q&sqcXaKcVCW?a{SoXMfreEHxPc1uwh0ILHuwbHvBT z2LPUtk&%mn5xNreY)o}ML0|iLYVt)Q(jU~(?AdVNTG5M|o?<;9j`caLm*>x)2V?^j z7XynQ$N_m#*U<2JdA%D!si?zHupqv_cZB~+R*%|dE=Vj}73aOt{C+4NdQe9SFkb?W zAae{39Ed!34n%{=G$EmAt~gyUoj&IJP(PU>NrAE)Zq%bl;KW=kVNOKr7trx(GCq-D8;nU3ct5)-4bLjZlLCILM(hBmx!Z4!M%O^cI@RKR;XV4bnV17l#d}M zWF7z?Jmfh4alA8aoArE_-UoCGX6Q&saaL9zf#4I*{&WmI?ObPY_2!Xm;c7eXO2d|P zmXXb2OUwKTn$oUsqDtR2AKK6`Flxne>u)khr9Y#UO0|m6lbWGLB}+(1AebJ{R$CjO zt*bD#Czv${a34J21D$Mzs1&EJ@B%?;{x!rkd(fX*<=@A2oK2gKqSOaQvX{q46akj8 zcl$wHZbUc`F~ei~>9m^zfoI-H$RyQV%hH5xe;$w^7;4_v!5Sh1I${_v?IC4G@c=Q*_(d_Kxl zBGCh|Y--=sl(Jm{*E(!3>ETv50rB&N7cD~PwgAl^01IqwH@O2CIn{(`may&%c;@{I z7vm+8mWC-|iZ=Qf0%%wnonE$njgXK~ea=PH1DeS=cQrLM1PBgyk7Z!w5U8A_xXgR^ z@4p@x;AGxqvzZv6gMo>qW0dsWlDvO*wQHE?;^O?kInJ2(GlqdE$jisztb+casFORJ z3AJ%$hAM%w?fcx(QqO9sVKRc3hsS{%&V8V%7cahHT@TFY+VlfcyLuU8<;rhscJSEc z;M7nXrAwV=hwtS|X8SCmDRv0Y;Y~NOCNCiiBiU+h$4ShNW`45`rQ98m@V^^}sb zN8i3yKU2nT3ot2^YZxFeFC)`H`3-v!9?3@pR)x~5e5bkt}0)% zU*;nRFVQbhdIczK=AnIYa)KruUiaMbbTd5Su4sNB6M!z{J)m10pZccv^5(dprnw24@1)AGj6x zWLQ(n)~vaI>(=Gjv`bu1+S$a!G0_i^;?aqyw+O<$s%lTi)?*Cf)_6G_*pedZn#gu< zFlfHS9c(P9%k9=QW&0rh0bMt44)FzT^^NG%m~)^Zfc-l_7_#HkOzrV^jms?WCZ*pq zIWFsT^eD=!N8oPWRc)%q_%H&aB(lTSX5B#a+1$m8w^-M$=m-P22N)ZJC+(-tSz^P; zt*NO29FctG3bMlx+MQOB7B7KF2GUBy{Q*V|T0+oaSrSsK#6*G-3kj)4*oq&N?O-dE zMiqm+pq>A^lO>OpZr-wG=8q zT_bi75b%-@;uv*D-4br%`-oeDPR#^IHY8TYo;`^;-Vw}!OrueI1gb$=CdT;%ym|lm zOD+<5C>^oNcoB*U3PbPTgZN-%pBS}vvyxhO=@Q3Z3IV+9shgk`Vdt4lQN;B_qCD(BFK5M}N)p3P+?afMh64;;miiKOx2pp18muJ!oNyq7uKFIx0d zSFf=X(PRjV8>rYCbaNT=o^7+Z6D17H7dW`#zre-N?(p0pzWRFqW34jQ zrJl@O%pGYwETz}VaI5&{6}BjWrw@2CoFY=KVa*5rrQx^kTsbvEre(L=bE4HZliJq) za~~n?xBh7YFYPgq?U>&dw(fva(Av@A_VF=DzE->8_7k*V{YbYV(MMp$>O1=bF7yLH zBp^u~006{uR;ZWSl^GH(7fo8(n6r=8{hMLV#?3!9DZf~5`68PCaMt5yV;dWr(Z26$ zmYr;QT8Ogq27qv)K7&IAtR2hnY+8F^K6V|$k_bw%kQb&<96IzEUlU`4!k|i&IcvB% zjq?KaFAxPTQmIr`F)2gIOw|A9&M-z>ld3~Ri;Y{$LdQ$m9+=Z7dZP+>4K&4MOn%c% z*8^ZSf|+-Jgy@+?;vInpjce^AsL1P3Q$HtTSJspzyB_+m4#&jwZt&tNy=J_z?>TfhI%J^^d&Iu znz)ukTG*s?xer5*Tql)+vUow&8t9T6LxU?=y5(O@#JUny{o1P3W*7rRhE}$6zR=5 z)vgg0YIYXMiC?gQ)BU$)HKJ`9hFEaA{}Y0TL2e@sJ+>q;CSWiQNwD*$U8#Efd&NX$LeS~tpsr#xR6j0V=ytc@#2LG0+y&f zW|90$&zo#Vl&8mz9mCNB9W4$-i2Pvj+V4Y!QUraCOc=yA6DTFwc|RPTm^q~+P0+r- zd=2p_aHi;S8%j9y2o1J0V)PM|b?tz|>2ol)_5JX$Nul$u1T_ra9D`C^QkkY)PMS({ zJl8;_W-xDYoAhWGS@+-Le-Y(K>vUSqA--e%qg*{E>qxzbbp0Rc*@^r3;&(^3>}43x zWzoZk&Ur;Z6|AW(+!@;vP6}7g0RD=i1n@06Yzv}AP1#ic(*MjU7jAnvgQ=itySb?3 z(6bYVI__V;@IfGH`_xZ)#1Iv;WmUwv=x8qtlj$hl3BY23$}TOfrI>F5ssJ@F$VA{X z8htwp|LPPmzO6t{k&>aW9g05Q9k?4#Rb+f(LqkFKk>#{+Efm1?mE}{8sk;-%4%biE z_iFU5s4evyPrm4RANxyVqZ}Yrpr>6c)ALoK(kMiygCY)X?Iw%h9>#6k)pz2*CRwSL z>}2nMbmq3}bczfNM*y!woQB;R>=?p^t`W>(D(VWP6ZYFbe$!nIb`N^kVvg*F*ry~R z&eP@6=IS%LWLTW08zg;Ay6sij@5v9(xU$LXW%+Y+Sd_%P;2q32$2JEMPw8mqc`amx z#V>gbte2aAE#Gx}jn(DyqsJu^h&iRWeyMW%USJc#S6SlqLfX=@8)qeU&~dDZSx%ZM zY)49SPWp9)xFixI$!EsU=dN0oi9IdH=TDl@+QcBhsWHv6Z9UYsuKHu&lZ=zVm^ zl^{3V)#^TTj7dSPFg$w2*wBJiP*Y~2N*J6?;Cj}{JE7eIeF$7eW9k0yJ&;lu95VR+ zedNW6JyP|Mq&@5Ijt=(*)Cs%{kg-5tFh{O__&RsrX%$e}U^RnIpLRz*3RMH(MbjHB zJCYYhdfu(BZtN96T`E||5r*f`1VE^a|*?xrmk!fr@im-im>1}NBB*D;o-QTZk#_6He?Re#K(Am}m{`PqHJ zBoh~t??|nDq`M=n&?l4P|I159917l+ca62-$R2 zh*hXZ7}h1o)CagKhc>uOU5P8(7mj4)t%@OQ#1t zx{+DPH@7)V_nWO3{c8fXC?dROS@#L*}?_A=_&Kaz6abo>`c&z|askJ*m{6%zfBq@^xZU@;PN-eBo%-U!%zUD zn)WC&EsY1#p44op_>)y`&C|`>p7D4T0|ToO;8A`I-40P+%--~lhOl}khNiNH(Y)D@ zu~qy-t@(ajG|#?0vn28=kasYo_p7Qh2I8iFZsbCWNp209jr$Q}hJa2cWLP+opvfU5 zTf{{TpW4Uz4}0p;+CA7-w( z6F)&y%L+2Wmil#9Z0}`6Dz|GdF-oqtZc%-MvS$QD7Z^l8)Su9sEk)~x{e@+T$95bf z3@zUbTLMm@`=P6G^%3N%pLuXIvYQG1f8in$v1)l+$$7yH{@MzPXvTx%tQQS=RPkn@ zfEl_}bUj*5mzCe0zs@(R_1C?1;%G&2IjeCzW^c#t6c(%g8huq< z^DpyCrtv52-Z;+wP@h8YpYCV4`Oj`DdOZ17bF&xt4fL51DD&8od@))^V*{eya2Z!= zL|rKnJpxrcH#YFh8CMq4o7zg<|dB;8hFE(h~skxG}R!|!XU1;^gKTgHouZ>e70$vIKkX=B@amCje=cLu5>FRp<(r#$m zlceWebV**>r`)}D>%={py*_G79afc0VZ~ZQzTncP-y!dK!_5axG0J%+<)fL(O+#;%CPQX-zC}&VU~AjjCj&2yOgXtz z<$?HAR;p(;V*5tj0wcjj2&YC_-h$N6&DWS6P^7;MU1^?*v-#W6?gL804D8UjxOU(> zzypItq;KDNMSbu1rN=GBWF`$>6NUscY!;kY@c$H#4uVgrLCy82zVx#l%p6P#wzPMU zlBKjA(JE5Jn#R8xp~D7uf=>jTwBza1;3C6j)qhij%KG}7Lf(Kz#srWFL>zbD_G6iu z0W#!FG4OJdl&yREErKZVajFK%H(+1E)2wG`Y+FJ|J8jpeNzbN0w;K?kwrkft%(#Su zWC@9+00jX^F2Xg!pfICQIYmgDQV(i!EEtqG*i6$ke1NO8yLZeI3_pkZ4p0p^pv<W;)gX{YP(Tawg()ZgpZhddHCz2k-VV&P$o2+XZX&p01&Z!XJgWyu%?BE&Z!wHKFadC^m-5lBiG#SPrtXOP3BI!x~HkAswTiM6V4z zK>?masOVrLtGTodSZUYE=(=73FfWLvAS;Cr!Q`6RJ^21)0^zZnsNqTp0S-?%zgT~m zz~DAG=q|A2znnI0noV;a#7LIr=HCbj!7`rAcFoVD-#T*0fN~D) zTs5m_5P-#HJ!u$YKu?J*tp;5E?p-O2CFMpybQjUi)3VQTRKfgxXlh<66HQHJ`h#%R z!TqL-Wth_cgMZDa!|9qVt*bJX+Ynud3~|8UVBDSu27bp+ZIJlYlx;0jHxMW@JEoAS zXKGHZO0tgRgwa#MouhLZY_>1e^^;t`ehf0yL8x$}GEXS1+e$mC&riuu)F zRK_#=JNE`yGxMr!woaFvZA4FhCJdE}&&J$)<8zwOq-@#J!e;9x*8?ESFez;tY=BJ| zBV{E}-=i>sgRuLe*Td@SYOqfmH`aj{8tvVc@PmL=3!nuidlx6Cx*lg!RVM~Tr1RdGbZbG!s+Y*t3wndaxrM&o+BE9G-D zvptf6wwY4MGetHKI1SIJ6Xv8k&>R&@ys92O+Pq@L6)e21Bd>PQD|eZ4G9*_W#rgNy zJx5mE70uL5Zrv%mwru|lDmrDv0(u_|XjH4h#tV2VAep0{V*)ulK1u%!u!fjc$>=NX@MA>?@E0`G%SQ}w2 zAhGSeaAD8%e6wCoPEL^2&NgW2`qY5bq5;Y8?{q>J2{S=4P!_y;FLhUk-%VjrYsV9` zjBW0t;1tRD2V$p8R=<<{MZJAbDk^CS|X!+)} zvRsOxMP4j<9{CC73@p4DAOaf_>11@ibd%00@7v_$H{_zKxfy4IpOEG8w^`oh}%y+p339jO{ z89TjB^wv;<8j`R^WYxD=Ib*?nLo~+vfVyryd1G zt8FyMt>pS1NTngO04@j>WSVy9Py;&asyHVHhpQ2OWY^?}6LbxEFXYi&%$x@@0ptcm zUXZC{qA`9@Wo6|+`Xd*S!vd)xLdz;kz^>&!y;~T0IR!CPGL4<2ymjmN)AAr#I50}q z*}*{`x+YLe^aoe-d}Z{ETEq01fjrsUBnI0clIxz z3p)LBz!KOVFw4Fe8wCgy@MP;=8s}Rx76etM>gDL!@|u}63@|6`e0aLy#I^<#oF`Mp z9jY~OaWSN@*Q7OnOCz=%YNI(NiD_AjEuFSGK+IVbL{;7Q* zclUK^v4YF>#*!L4&zDXUBauAOia~Dx5m-&l-XPk3?NIcopf*re!xBN@@)%(7)?_y4 zyWfL@aWeA36}tv(-;9ko0|KEgBLB z==1=(vEMyyZi*c`O{e34LL&DcLoE8uo4<4jh~eNL`vQjEH=tPhfxi-17IRjE+~Gxj zwlL-oE%N7{ad3AZ1hBe%`ErD%5a};^UVXu-3VUb_14GTcY6>&855+0uWcV1Mp}Swc zTp}#&icveH=)=aRdvZ8>Mq*)M9x7PLd=52Z54~_t<;X{&Y*jdYbF3y_;Y*-~akm8r ze9@7+M0{9^mp(2KLzr^bQOlU#}xBgpmU^>k)V^51Op*~0W#q*WVe;!{?{BHTg zPO}e6IjMi6C^e;quu^i#kX71eCrn=FNtw4 z?j4b5wsdJ6Q4>IR3|6}J>S6z(L3OF(D{axP=? z>Svtb0Tnc4jb#CIz-hQ)Uk5&j|7XXxnmmaV^z+i2cxJ%!gEj_MyL|Z(Y+!6zD?DC^ zkHg`Nv)X6^>kGMUWl2Vc4Tz|(s;X@FN;@6&A}D^rRL4NmgDDSYW>;_)3_We4yn*h< z6JHRfUPXu#JUlQxhG1!bo;h>UC2blv{L%uI{{DXHu@+p6krD4=Qxazw3(HkuE@vYDBw^4eoC)d28I3 zsk6Xt-m`i=nMlGIyL9VN1hP2(j$U5!oWb>07X)DFf{HfgLp3F;FKnjCp`$)+eZDFh zubW7D!%Uk$$wAcWb@(M526jU^AGIdFzGHCntlAR+NoCLD$3)Zx3TPGWFOI#|l8;~+ zuln=pNhd=>B62kUqZS3=63Y@ozZ=EHOyb9Xd8|@z)XbVxFe>wY5{Lq0C^69+!@8=M zWlX>yD%!Y)|JclVESRQ%ZG35WEr;m*Y9A5z69^BM>qq0uvHC zKv4lx!_cIlz(BpsgC2mbU`3#JnOj&uOgRqV4{HL>1}tANMdu6q!R!+f3-wd8CN0Z= zzG3#?!I>1&P3&=434rm4S?)zeUIqqD2=-_5O-)UKm;puV{1D_YUql3%2(HuKAk9?T{Be6-1ATW&50cXUuc6DfM#O-J4U*d`;X5a4+t z&PlEDmwI&JNtd#(f=Q*sx&fVH_W1m6UkpV*>`w$aZ&xHb^AtQ|sd0C7NhWV5O`@Ow zFt6Zci~dcf3y{38_<5q=ucJ?y)Z+NEU=qd@{EIc0_ybYkJ5lK}TN6?9Zh}nhe%Itl zFMrS&`!KaYWumDC4$adYKFaP%mewvhy1D{z@k&F_6Ch7`yEe(5m|q7{KBqraCt!8a zBV8Kp71zAzXijTeF|E_zp|_V_pOGyc6CkRmGb*wVdj=}9@??Oh1i-aAKNd_IF>MY4 z@f%QSCpOoKl(Sf_A1Bw}=f8Z|W3ANso8|A#*4E{+NvxI?8suXrhOzP*;vmKxL?11- z#eMH}#wgfEkQz^Wh9Q%aOLL0SW6LGDK6)u*k^kVpc50x#PqBUb;h^iO)2~Y6_Q(SE z1Eqq=76^*_2_Fldv46?;aT)%|hNBOp7VTf_`nB;C`)?t1y;#gb0pnt900_GK_phVm zroak3!Pd?mRzN0B)u2YhN9g0?Cd)G(CWr;Pj-((JZRqhK0Fbk9v&sI&R0_x@P}{za zj`J}wHWW>!CE_hgu-$;C2RP{Vyyh?x8K6VkJ0IFUN&G_90em>Jumi&9gD?Z3kB8>{ zPlN7|Ik&5bfQ9k=a=|{}9~c^@h`J8J9jMa{wFzY=j7NYT1eEjHi|Oa2{$j47XxqED z21O_|g{I2C;=E`r^(&2+7$520${sK@oWxF3{u@DW|BYOnBhv#Et&&plwQKk(=Rhp- zEI483RdZ}#6fpmI;Dsyqqv>xPy(oTVGdCC8xC?aZNGpjWkOjf12u@|3t_UsRQ z)B9y*30JOk4)>Kpia%%8EHSJAh}d0F76LKV01=HBbHxvumW6WDUFRcbZ64=Li|3hO z*TxK&ijf~O^DJx@?W1R3%lk1mTyJ#FKEsDI9mij$dYCG>z07djE2sOH{Wl&+AW%0$ z{w+`{LjMF!zEWDN(2Mq5+_D+9kgxx1`V6Td4S9u2b8e9YR-52qtpJh_CEMNb-)I=0(ezy*TA0P|Azh! z#*-x3F(}{1@C?w6hCe|fq?xA{8oMK4!Puwe=}XVz5=fP>#x{w}1c%a(z)~0vQ|-1w znF_;h+Scf>1nJk6RJ2ML5TP3N08iZ+0|!KWLD@LNZOv{!u3E(@mC&XfR+%Gbv^H$D{{&=Pzr#w-1Oy zcF@-{@01?G8dFDP96bO61?D z0$+-ai9t(v(xq+h@sga!#pt`{#~mf-e5%vIOcHipIs)d7(l zP~Vl3Kp}DPf5C_FJT2X`%L5 zCv0xxA20DG)h--<9-aaIgFMbo{z*#yN$rkf4FzkGG-HVYS8!@Q#RDS4@^{sN71eAV zp(INxEi5b)UI3KoDt%TugIaOATs(hFJfW6}o!L4x>GMT?VzKy&i~OejI{9I8l>DQU z5L=<^^=om)+<$;Mso6mgYie0obfM^xvFm&c_2t*bwx=+nPJ*1$ni zw~idD3yyFIBZA%&tM_zq@hB2Pup*A4!4WLY?_w|uivCH|dqyZBu!ZH9gLIA8Y)@*X zg>0Dh#C5N*TGVVQAX8&(3l%+7MbyN&xMD(_0+8-zhGa<;!B$|W|8wWiVib% z`6Aiwot&d)!7{U5X^GZLZJ%5#wWqY0#1O$}Oh#26YBU9ldz)eAi?05eo$fCiXUS25u<=0(}Nw`(Fe7}E>F z^19#nSoPNNk>lTdPfRGRHceyA&C5PK>-NZ9M)zGSH+~XOvXhNZmZh<&S?tJLmo?R# z>zypWahSkdoTXQU76~X5;0GOX|9~%(h0;R)mG{;OH~lhj=)(E)rs|tyzROquxavbk zU?zyw!83z#WX5m|Ir{%ixGY_|JUP2)sQMz>+jRR|d{nxCu;5d-9gz#MUhq|)vDd`R zp!3gm!p7_U1nAeEtgc90%YQU8^k@6iEgBDBru16-ubWA?CWNL$(S$|;ycISlsGF0q z{x4R94t(3(v_VB1%vD>dpI_pVs*sQ626YRJqhDUloB_JElU=uZO&9d{$QoP-v>8?) zH@9~P9zr3=Nt*_>0km3bs;b;akI-I6+CS#;6s&44A#K+JB|b|iAt3=P0hy7=^lreh z3$6sIHmB7G)Wl?3=F}OV>cXq>_3N?w2Fip$m({T|9Mdya_}Gy z1__YouA=Gn?oY1038w9j%H`%k`z3_e6`$gR)!FGsSe^O2gJ_nZE`$R7Acd0VyCz9> z0%Xk6vGQwic=ulVGc&K8-m81bp4?^Mmp#qJDhE$Wcte2@1jgS0>HzKp2j2sf5WOCI ztb?yg&e8#pD4u^&JfW2t&5bQwU>x9|l9%2(uxUrrxNY!|^p)n|R$K+%jiX=UQq&qf zGLiBGX|3?qhe1OdL}nsGYu{Fe*IKoWf7zX_DG%(n^ETh6zrSm{!l=mnWej_)s?{X!$u~BoY3WqB%cgB3&Ofxz_P#+5ioSCLG?UdeH8WFE z&Y2_+$XTH%KcvS z891XK{TJXY-DG0m?Dma#97u%d3NOS4L5+ka0o@)JDEfT_mmVnbzRq;YVBRN^J_)Hl z){+v+|D=#~=JUU2vqrREzDrpE`3ovLY&3d(p<|yyNsAisuE-YvzuJyCF8mxsg21W- z+L?jv7zz4RL~>!Fre;rrIvPdL*B#_vJrQrAtKM*w%Nt9WS!!m82D|||2k2U^9V?zzLZ@_#Fm1;yx)VDz09AkGoj6_Y6H~ThT&JS3W)| zT9Z#)zm+nBw068ptP2GbcAGBLSq(p;Jfh@UT~^|9W=r#}nU4DzgT-wwL|YOt?>`9L;OZMA}udUj}37Q?FowC*&p7P8)HMg1P+;!g2!=!)?yry zcd3RyPt1HXfnJ)!R*ZfGZJ++q$S+jLjq58Y=|5{-j5&Mu&;}7@=zQ=515${3=R7e9 zKh*HaOlzcq=rZ&^V0L;T0R|mj3_O%P;EC741NlF2*0$<1Pz(GmMs}#-qv_YmmXfYC zjQcrJB5T#VYfTl@9Mj5ax}pki2AVcv1%H*inx&A#DctmdB5EY5!}K5K=ot0xb|`ZS zj25sbBNE91Uloy@Xq|AU_qCd1RpKHyTol)lT|b>~EV3}|{R_i`W#r6}Q%Y4DIBD)0 z+=j14L;b4k7k9IHV8@=UM%ez6l9IwgLy^qyi?s(4_};jEm@2y1+v)>D{#N$=_bFc{ z49WWZEyxHUc+W)sR-Oemt9X3QKsDXm z^|hxp_@826^7Jc|imwuq(o1HhWi!!{5L2N&gR%i?W9#b9u2^yua>E%j2yqpWL2Eh# zw7efLH900W76xFrFCn-{YuE=`j7sIf3)PLky1L-_>*4cleY3u&`N+hI85Y{uy6$N< zt(;Hz6wn2-Dn2`jMT<-y<}Q>y6XMy&1|6uPjgAfSe7J<&pjZQe|Kf#BhnRtFOaf`4 zzf*lf!D~_827UM-KdG&$GlLB@@zGZA6p%zOo#La}@=tz%Ie^u`RaO z9EuuQX++;fo{0GKE+|hv#15Jyp?7f(55XRVG#J6rW z$!bI%T!fuzb|Bhdv;a_z=&!xXGLCMGE%~BLVH+IpA-h5~0QfyI>|^Zfz|E<1M3(ly zd9x{mg;cvB;wtmk)2hy-l8S6(Cb9(s@6T4`7GL%8o-$<$E-@A>;njjzOibBZUP);* zv*;zxpABulv00nb9Hk}X=*$c4j{~mvoE>;xyJM7yTW9^4LC<5ouD7U6p~^M0dO(1G zZ6jXVq2hvMFML{V-r`vV|1M3UH&jXi)q!^7%RJYv;WQyeyu_DWJeLi5sS*-Ja4|-Z z@evAXIAvXdv?@N-gfbDR!rtJAcegg}e&>Xw8TXx8OFob?XVBc+jbV@u0IVV=v!2 zHkeRI__DAd*>PxW4Rfs=AK#oTnvVUS8p*Z_HD&0gVHE()^akQZid&3$?Sh_aTwAs= z%&*&R+4uxD_6$1j;Bh>l@2^)q&G{%smKkk9dyha2%uM`_-|zAKazMlP5rlCS-XW78(ihT0qszjKt*XY5UuG>y`{@y8-i>evgGgyZ?l*Jh zMC?nmBBr?yqN2vhhgei3vK&M>pH~(m^(Yb73x))z3|YDN`Ez-kEBJFjc)?Nt?Yp@7 z=+EocRoz%AJZxp^&$O(_Rbuj9BSR08CE|zyX)c&cf4K!+DvA;;Y-|rEOzMUHD_I&| z-VJ_J5t3u%lWYg<7+hLtWTf?okb{3t;zDkYeaHP$j{uLq6w~{dCs1xQx*Iiet!V2? z3pHq?A=jw>F%zU;xnGa2p8P!(6xV~z^763kNTCuzX@sr?%p?kH-MGNdoO)&e!Kq&; z{uiny*qme{gSOb+%L{MwoS5o_Brqo#ig_oWUyRHkq;aD_>NqK%@KxgyC3IDF0a^+4 zDEJm%jIVABGj5KzTY2K-$nF&iujta>Q2&~q2M@}p3e^!sIUhOE?aag!-t zHhtG18o0F0A5~T~N~!3)`Xg?)zlxA;cxvJ)_49fn=+N;!u|NM)8RzAwK z=GUvmWnNr88j-N8AZJR!zjQ)7<-W^LMu*Q64B_bjsBcmz=_Wk;sLTSlmmk_9m_$!~ zXp8#ACv1!U$FXex9mh$`IsJCxrt*KhdjDTfv-*D+x&8ITum4`2|Ia6v{r6t~Z;eI& zUz$Gu-|5Q#o#H-`8Rx%8&ZKGoJ(2&ro&Rrb=l|E0jlOdI?NCIIRK)HLs zptnx%g%ls42M`P0`H6snAT~Nw^PB3)Nz+LSK}W(7BoRoMD8R^SOgUcZilIs}qy*x?!wG)LHi zpp1e11O^F2sX}s(w?U9#Gkp8wi1$b-S~!0`;-tY|)|Z!?Lo@{3p-ybdD?iLG?tqpx z8zxB=ZD_6+Em`vN=~E|&?w|;`5Eiy35SabEB})LS5y`#w<6G_H;f{tgNdFxnvB1dR zp@P_0A{un;b$BUocr#>U0BNsNn)>10#YdN35E(C4blvkIc@GA4?M$Ib>o7*QCS{h$hGoji(4CyY zAHgu6!8dW^HxMmCTvogVrjVVCm%uszeT8P#9@f8cd?gXL#nMOa5SJKcE0F&!ZyhJm zr(xjldSp%|m7W@VIM_=hs=}{_KspTC8!L6?*XV6u_Fb6-?dpne*A>@H_ax8W{b=^{ zz$G`u^Nw}9n~m?0uc+=Ul-hJ1t4x=j$CuIY>+31BI}jEZJoDgBH~0Q=ScK!k=pEWp z?YOwUV^ZUc4WU>1hCMFU>r9fndg7Z&lc&u5>+kaQhl|_e3|~$@7U0ew_I>YAFOxre z>ZFyM&vd*#U{W(_FL&Wvzx#70X?1<``9;JV;8*qIum0PYZ~UKr&+hY?++<=jRrb7c RLpdz5;oWo1bw18x21O=2Rk|c`=NNiFAO_L;N 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 z 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 LYpOj_G4=j$84t&5 literal 0 HcmV?d00001 diff --git a/docs/images/example_01_page_rendering.png b/docs/images/example_01_page_rendering.png new file mode 100644 index 0000000000000000000000000000000000000000..1bf85a43519b15e78038829f33222341aaa9a961 GIT binary patch literal 8202 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 WLVLR{e+-`SBaBa=_4AKk4Ehh;qeB${ literal 0 HcmV?d00001 diff --git a/docs/images/example_02_text_and_layout.png b/docs/images/example_02_text_and_layout.png new file mode 100644 index 0000000000000000000000000000000000000000..2467e66ba55343bffc8481abb4a35e2a60fe606a GIT binary patch literal 23455 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 YCe+Xx>CBgbKTkSxNabMs{&ODxA1mxO{r~^~ literal 0 HcmV?d00001 diff --git a/docs/images/example_03_page_layouts.png b/docs/images/example_03_page_layouts.png new file mode 100644 index 0000000000000000000000000000000000000000..2b73c9a3e9daa04ceba92cb1d21916d20f3a7ce1 GIT binary patch literal 11923 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