commit 735face59352265f8fddeda1e7e6a795a47678e3 Author: Gitea Action Date: Sat Aug 8 20:35:15 2026 +0000 Update coverage badges [skip ci] 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 0000000..8649f04 Binary files /dev/null and b/cov_info/htmlcov/favicon_32_cb_c827f16f.png differ diff --git a/cov_info/htmlcov/function_index.html b/cov_info/htmlcov/function_index.html new file mode 100644 index 0000000..2f554dd --- /dev/null +++ b/cov_info/htmlcov/function_index.html @@ -0,0 +1,11473 @@ + + + + + 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 0000000..ba119c4 Binary files /dev/null and b/cov_info/htmlcov/keybd_closed_cb_900cfef5.png differ 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 0000000..0397e4b Binary files /dev/null and b/docs/images/demo_08_bundled_fonts.png differ diff --git a/docs/images/example_01_page_rendering.png b/docs/images/example_01_page_rendering.png new file mode 100644 index 0000000..1bf85a4 Binary files /dev/null and b/docs/images/example_01_page_rendering.png differ 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 0000000..2467e66 Binary files /dev/null and b/docs/images/example_02_text_and_layout.png differ diff --git a/docs/images/example_03_page_layouts.png b/docs/images/example_03_page_layouts.png new file mode 100644 index 0000000..2b73c9a Binary files /dev/null and b/docs/images/example_03_page_layouts.png differ diff --git a/docs/images/example_04_table_rendering.png b/docs/images/example_04_table_rendering.png new file mode 100644 index 0000000..39d56e9 Binary files /dev/null and b/docs/images/example_04_table_rendering.png differ diff --git a/docs/images/example_05_html_table_with_images.png b/docs/images/example_05_html_table_with_images.png new file mode 100644 index 0000000..e25a835 Binary files /dev/null and b/docs/images/example_05_html_table_with_images.png differ diff --git a/docs/images/example_06_functional_elements.png b/docs/images/example_06_functional_elements.png new file mode 100644 index 0000000..22c5392 Binary files /dev/null and b/docs/images/example_06_functional_elements.png differ diff --git a/docs/images/example_07_button_animation.gif b/docs/images/example_07_button_animation.gif new file mode 100644 index 0000000..15df782 Binary files /dev/null and b/docs/images/example_07_button_animation.gif differ diff --git a/docs/images/example_07_pressed_state.png b/docs/images/example_07_pressed_state.png new file mode 100644 index 0000000..49f83ce Binary files /dev/null and b/docs/images/example_07_pressed_state.png differ diff --git a/docs/images/example_08_pagination_auto.png b/docs/images/example_08_pagination_auto.png new file mode 100644 index 0000000..3b855d6 Binary files /dev/null and b/docs/images/example_08_pagination_auto.png differ diff --git a/docs/images/example_08_pagination_explicit.png b/docs/images/example_08_pagination_explicit.png new file mode 100644 index 0000000..a109328 Binary files /dev/null and b/docs/images/example_08_pagination_explicit.png differ diff --git a/docs/images/example_09_link_navigation.png b/docs/images/example_09_link_navigation.png new file mode 100644 index 0000000..5339820 Binary files /dev/null and b/docs/images/example_09_link_navigation.png differ diff --git a/docs/images/example_10_forms.png b/docs/images/example_10_forms.png new file mode 100644 index 0000000..62bd3a5 Binary files /dev/null and b/docs/images/example_10_forms.png differ diff --git a/docs/images/example_11_table_text_wrapping.png b/docs/images/example_11_table_text_wrapping.png new file mode 100644 index 0000000..8e3a954 Binary files /dev/null and b/docs/images/example_11_table_text_wrapping.png differ diff --git a/docs/images/example_11b_simple_wrapping.png b/docs/images/example_11b_simple_wrapping.png new file mode 100644 index 0000000..cfcbf0c Binary files /dev/null and b/docs/images/example_11b_simple_wrapping.png differ diff --git a/docs/images/example_12_optimized_table_layout.png b/docs/images/example_12_optimized_table_layout.png new file mode 100644 index 0000000..eeee2b7 Binary files /dev/null and b/docs/images/example_12_optimized_table_layout.png differ diff --git a/docs/images/example_13_table_pagination.png b/docs/images/example_13_table_pagination.png new file mode 100644 index 0000000..c100b18 Binary files /dev/null and b/docs/images/example_13_table_pagination.png differ diff --git a/docs/images/example_14_interactive_table.png b/docs/images/example_14_interactive_table.png new file mode 100644 index 0000000..997fc39 Binary files /dev/null and b/docs/images/example_14_interactive_table.png differ diff --git a/docs/images/font_family_switching.png b/docs/images/font_family_switching.png new file mode 100644 index 0000000..92b88b9 Binary files /dev/null and b/docs/images/font_family_switching.png differ diff --git a/docs/images/font_family_switching_vertical.png b/docs/images/font_family_switching_vertical.png new file mode 100644 index 0000000..91007bc Binary files /dev/null and b/docs/images/font_family_switching_vertical.png differ diff --git a/docs/images/functional_elements_demo.png b/docs/images/functional_elements_demo.png new file mode 100644 index 0000000..22c5392 Binary files /dev/null and b/docs/images/functional_elements_demo.png differ diff --git a/examples/01_simple_page_rendering.py b/examples/01_simple_page_rendering.py new file mode 100644 index 0000000..9b9110e --- /dev/null +++ b/examples/01_simple_page_rendering.py @@ -0,0 +1,217 @@ +#!/usr/bin/env python3 +""" +Simple Page Rendering Example + +This example demonstrates: +- Creating pages with different styles +- Setting borders, padding, and background colors +- Understanding the page layout system +- Rendering pages to images + +This is a foundational example showing the basic Page API. +""" + +from pyWebLayout.style.page_style import PageStyle +from pyWebLayout.concrete.page import Page +import sys +from pathlib import Path +from PIL import Image, ImageDraw, ImageFont + +# Add pyWebLayout to path +sys.path.insert(0, str(Path(__file__).parent.parent)) + + +def draw_placeholder_content(page: Page): + """Draw some placeholder content directly on the page to visualize the layout.""" + if page.draw is None: + # Trigger canvas creation + page.render() + + draw = page.draw + + # Draw content area boundary (for visualization) + content_x = page.border_size + page.style.padding_left + content_y = page.border_size + page.style.padding_top + content_w = page.content_size[0] + content_h = page.content_size[1] + + # Draw a light blue rectangle showing the content area + draw.rectangle( + [content_x, content_y, content_x + content_w, content_y + content_h], + outline=(100, 150, 255), + width=1 + ) + + # Add some text labels + try: + font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 12) + except BaseException: + font = ImageFont.load_default() + + # Label the areas + draw.text( + (content_x + 10, + content_y + 10), + "Content Area", + fill=( + 100, + 100, + 100), + font=font) + draw.text( + (10, 10), f"Border: {page.border_size}px", fill=( + 150, 150, 150), font=font) + draw.text( + (content_x + 10, + content_y + 30), + f"Size: {content_w}x{content_h}", + fill=( + 100, + 100, + 100), + font=font) + + +def create_example_1(): + """Example 1: Default page style.""" + print("\n Creating Example 1: Default style...") + + page = Page(size=(400, 300)) + draw_placeholder_content(page) + + return page + + +def create_example_2(): + """Example 2: Page with visible borders.""" + print(" Creating Example 2: With borders...") + + page_style = PageStyle( + border_width=3, + border_color=(255, 100, 100), + padding=(20, 20, 20, 20), + background_color=(255, 250, 250) + ) + + page = Page(size=(400, 300), style=page_style) + draw_placeholder_content(page) + + return page + + +def create_example_3(): + """Example 3: Page with generous padding.""" + print(" Creating Example 3: With padding...") + + page_style = PageStyle( + border_width=2, + border_color=(100, 100, 255), + padding=(40, 40, 40, 40), + background_color=(250, 250, 255) + ) + + page = Page(size=(400, 300), style=page_style) + draw_placeholder_content(page) + + return page + + +def create_example_4(): + """Example 4: Clean, borderless design.""" + print(" Creating Example 4: Borderless...") + + page_style = PageStyle( + border_width=0, + padding=(30, 30, 30, 30), + background_color=(245, 245, 245) + ) + + page = Page(size=(400, 300), style=page_style) + draw_placeholder_content(page) + + return page + + +def combine_into_grid(pages, title): + """Combine multiple pages into a 2x2 grid with title.""" + print("\n Combining pages into grid...") + + # Render all pages + images = [page.render() for page in pages] + + # Grid layout + padding = 20 + title_height = 40 + cols = 2 + rows = 2 + + # Calculate dimensions + img_width = images[0].size[0] + img_height = images[0].size[1] + + total_width = cols * img_width + (cols + 1) * padding + total_height = rows * img_height + (rows + 1) * padding + title_height + + # Create combined image + combined = Image.new('RGB', (total_width, total_height), (250, 250, 250)) + draw = ImageDraw.Draw(combined) + + # Draw title + try: + title_font = ImageFont.truetype( + "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 20) + except BaseException: + title_font = ImageFont.load_default() + + # Center the title + bbox = draw.textbbox((0, 0), title, font=title_font) + text_width = bbox[2] - bbox[0] + title_x = (total_width - text_width) // 2 + draw.text((title_x, 10), title, fill=(50, 50, 50), font=title_font) + + # Place pages in grid + y_offset = title_height + padding + for row in range(rows): + x_offset = padding + for col in range(cols): + idx = row * cols + col + if idx < len(images): + combined.paste(images[idx], (x_offset, y_offset)) + x_offset += img_width + padding + y_offset += img_height + padding + + return combined + + +def main(): + """Demonstrate basic page rendering.""" + print("Simple Page Rendering Example") + print("=" * 50) + + # Create different page examples + pages = [ + create_example_1(), + create_example_2(), + create_example_3(), + create_example_4() + ] + + # Combine into a single demonstration image + combined_image = combine_into_grid(pages, "Page Styles: Border & Padding Examples") + + # Save output + output_dir = Path("docs/images") + output_dir.mkdir(parents=True, exist_ok=True) + output_path = output_dir / "example_01_page_rendering.png" + combined_image.save(output_path) + + print("\n✓ Example completed!") + print(f" Output saved to: {output_path}") + print(f" Image size: {combined_image.size[0]}x{combined_image.size[1]} pixels") + print(f" Created {len(pages)} page examples") + + return combined_image + + +if __name__ == "__main__": + main() diff --git a/examples/02_text_and_layout.py b/examples/02_text_and_layout.py new file mode 100644 index 0000000..8699175 --- /dev/null +++ b/examples/02_text_and_layout.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +""" +Text and Layout Example + +This example demonstrates text rendering using the pyWebLayout system: +- Different text alignments +- Font sizes and styles +- Multi-line paragraphs +- Document layout and pagination + +This example uses the HTML parsing system to create rich text layouts. +""" + +from pyWebLayout.style.page_style import PageStyle +from pyWebLayout.concrete.page import Page +from pyWebLayout.style import Font +from pyWebLayout.io.readers.html_extraction import parse_html_string +import sys +from pathlib import Path +from PIL import Image, ImageDraw, ImageFont + +# Add pyWebLayout to path +sys.path.insert(0, str(Path(__file__).parent.parent)) + + +def create_sample_document(): + """Create different HTML samples demonstrating various features.""" + samples = [] + + # Sample 1: Text alignment examples + samples.append(( + "Text Alignment", + """ + +

    Left Aligned

    +

    This is left-aligned text. It is the default alignment for most text.

    + +

    Justified Text

    +

    This paragraph is justified. The text stretches to fill + the entire width of the line, creating clean edges on both sides.

    + +

    Centered

    +

    This text is centered.

    + + """ + )) + + # Sample 2: Font sizes + samples.append(( + "Font Sizes", + """ + +

    Heading 1

    +

    Heading 2

    +

    Heading 3

    +

    Normal paragraph text at the default size.

    +

    Small text for fine print.

    + + """ + )) + + # Sample 3: Text styles + samples.append(( + "Text Styles", + """ + +

    Normal text with bold words and italic text.

    +

    Completely bold paragraph.

    +

    Completely italic paragraph.

    +

    Text with underlined words for emphasis.

    + + """ + )) + + # Sample 4: Mixed content + samples.append(( + "Mixed Content", + """ + +

    Document Title

    +

    A paragraph with bold, italic, and normal text all mixed together.

    +

    Subsection

    +

    Another paragraph demonstrating the layout system.

    + + """ + )) + + return samples + + +def render_html_to_image(html_content, page_size=(500, 400)): + """Render HTML content to an image using the pyWebLayout system.""" + # Create a page + page_style = PageStyle( + border_width=2, + border_color=(200, 200, 200), + padding=(30, 30, 30, 30), + background_color=(255, 255, 255) + ) + + page = Page(size=page_size, style=page_style) + + # Parse HTML + base_font = Font(font_size=14) + blocks = parse_html_string(html_content, base_font=base_font) + + # For now, just render the page structure + # (The full layout engine would place the blocks, but we'll show the page) + image = page.render() + draw = ImageDraw.Draw(image) + + # Add a note that this is HTML-parsed content + try: + font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 11) + except BaseException: + font = ImageFont.load_default() + + # Draw info about what was parsed + content_x = page.border_size + page.style.padding_left + 10 + content_y = page.border_size + page.style.padding_top + 10 + + draw.text((content_x, content_y), + f"Parsed {len(blocks)} block(s) from HTML", + fill=(100, 100, 100), font=font) + + # List the block types + y_offset = content_y + 25 + for i, block in enumerate(blocks[:10]): # Show first 10 + block_type = type(block).__name__ + draw.text((content_x, y_offset), + f" {i + 1}. {block_type}", + fill=(60, 60, 60), font=font) + y_offset += 18 + + if y_offset > page.size[1] - 60: # Don't overflow + break + + return image + + +def combine_samples(samples): + """Combine multiple sample renders into a grid.""" + print("\n Rendering samples...") + + images = [] + for title, html in samples: + print(f" - {title}") + img = render_html_to_image(html) + + # Add title to image + draw = ImageDraw.Draw(img) + try: + font = ImageFont.truetype( + "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 14) + except BaseException: + font = ImageFont.load_default() + + draw.text((10, 10), title, fill=(50, 50, 150), font=font) + images.append(img) + + # Create grid (2x2) + padding = 20 + cols = 2 + rows = 2 + + img_width = images[0].size[0] + img_height = images[0].size[1] + + total_width = cols * img_width + (cols + 1) * padding + total_height = rows * img_height + (rows + 1) * padding + + combined = Image.new('RGB', (total_width, total_height), (240, 240, 240)) + + # Place images + y_offset = padding + for row in range(rows): + x_offset = padding + for col in range(cols): + idx = row * cols + col + if idx < len(images): + combined.paste(images[idx], (x_offset, y_offset)) + x_offset += img_width + padding + y_offset += img_height + padding + + return combined + + +def main(): + """Demonstrate text and layout features.""" + print("Text and Layout Example") + print("=" * 50) + + # Create sample documents + samples = create_sample_document() + + # Render and combine + combined_image = combine_samples(samples) + + # Save output + output_dir = Path("docs/images") + output_dir.mkdir(parents=True, exist_ok=True) + output_path = output_dir / "example_02_text_and_layout.png" + combined_image.save(output_path) + + print("\n✓ Example completed!") + print(f" Output saved to: {output_path}") + print(f" Image size: {combined_image.size[0]}x{combined_image.size[1]} pixels") + print(" Note: This example demonstrates HTML parsing") + print(" Full layout rendering requires the typesetting engine") + + return combined_image + + +if __name__ == "__main__": + main() diff --git a/examples/03_page_layouts.py b/examples/03_page_layouts.py new file mode 100644 index 0000000..49a5af3 --- /dev/null +++ b/examples/03_page_layouts.py @@ -0,0 +1,247 @@ +#!/usr/bin/env python3 +""" +Page Layouts Example + +This example demonstrates different page layout configurations: +- Various page sizes (small, medium, large) +- Different aspect ratios (portrait, landscape, square) +- Border and padding variations +- Color schemes + +Shows how the pyWebLayout system handles different page dimensions. +""" + +from pyWebLayout.style.page_style import PageStyle +from pyWebLayout.concrete.page import Page +import sys +from pathlib import Path +from PIL import Image, ImageDraw, ImageFont + +# Add pyWebLayout to path +sys.path.insert(0, str(Path(__file__).parent.parent)) + + +def add_page_info(page: Page, title: str): + """Add informational text to a page showing its properties.""" + if page.draw is None: + page.render() + + draw = page.draw + + try: + font_large = ImageFont.truetype( + "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 14) + font_small = ImageFont.truetype( + "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 11) + except BaseException: + font_large = ImageFont.load_default() + font_small = ImageFont.load_default() + + # Title + content_x = page.border_size + page.style.padding_left + 5 + content_y = page.border_size + page.style.padding_top + 5 + + draw.text((content_x, content_y), title, fill=(40, 40, 40), font=font_large) + + # Page info + y = content_y + 25 + info = [ + f"Page: {page.size[0]}×{page.size[1]}px", + f"Content: {page.content_size[0]}×{page.content_size[1]}px", + f"Border: {page.border_size}px", + f"Padding: {page.style.padding}", + ] + + for line in info: + draw.text((content_x, y), line, fill=(80, 80, 80), font=font_small) + y += 16 + + # Draw content area boundary + cx = page.border_size + page.style.padding_left + cy = page.border_size + page.style.padding_top + cw = page.content_size[0] + ch = page.content_size[1] + + draw.rectangle( + [cx, cy, cx + cw, cy + ch], + outline=(150, 150, 255), + width=1 + ) + + +def create_layouts(): + """Create various page layout examples.""" + layouts = [] + + # 1. Small portrait page + print("\n Creating layout examples...") + print(" - Small portrait") + style1 = PageStyle( + border_width=2, + border_color=(100, 100, 100), + padding=(15, 15, 15, 15), + background_color=(255, 255, 255) + ) + page1 = Page(size=(300, 400), style=style1) + add_page_info(page1, "Small Portrait") + layouts.append(("small_portrait", page1)) + + # 2. Large portrait page + print(" - Large portrait") + style2 = PageStyle( + border_width=3, + border_color=(150, 100, 100), + padding=(30, 30, 30, 30), + background_color=(255, 250, 250) + ) + page2 = Page(size=(400, 600), style=style2) + add_page_info(page2, "Large Portrait") + layouts.append(("large_portrait", page2)) + + # 3. Landscape page + print(" - Landscape") + style3 = PageStyle( + border_width=2, + border_color=(100, 150, 100), + padding=(20, 40, 20, 40), + background_color=(250, 255, 250) + ) + page3 = Page(size=(600, 350), style=style3) + add_page_info(page3, "Landscape") + layouts.append(("landscape", page3)) + + # 4. Square page + print(" - Square") + style4 = PageStyle( + border_width=3, + border_color=(100, 100, 150), + padding=(25, 25, 25, 25), + background_color=(250, 250, 255) + ) + page4 = Page(size=(400, 400), style=style4) + add_page_info(page4, "Square") + layouts.append(("square", page4)) + + # 5. Minimal padding + print(" - Minimal padding") + style5 = PageStyle( + border_width=1, + border_color=(180, 180, 180), + padding=(5, 5, 5, 5), + background_color=(245, 245, 245) + ) + page5 = Page(size=(350, 300), style=style5) + add_page_info(page5, "Minimal Padding") + layouts.append(("minimal", page5)) + + # 6. Generous padding + print(" - Generous padding") + style6 = PageStyle( + border_width=2, + border_color=(150, 120, 100), + padding=(50, 50, 50, 50), + background_color=(255, 250, 245) + ) + page6 = Page(size=(400, 400), style=style6) + add_page_info(page6, "Generous Padding") + layouts.append(("generous", page6)) + + return layouts + + +def create_layout_showcase(layouts): + """Create a showcase image displaying all layouts.""" + print("\n Creating layout showcase...") + + # Render all pages + images = [(name, page.render()) for name, page in layouts] + + # Calculate grid layout (3×2) + padding = 15 + title_height = 50 + cols = 3 + rows = 2 + + # Find max dimensions for each row/column + max_widths = [] + for col in range(cols): + col_images = [images[row * cols + col][1] + for row in range(rows) if row * cols + col < len(images)] + if col_images: + max_widths.append(max(img.size[0] for img in col_images)) + + max_heights = [] + for row in range(rows): + row_images = [images[row * cols + col][1] + for col in range(cols) if row * cols + col < len(images)] + if row_images: + max_heights.append(max(img.size[1] for img in row_images)) + + # Calculate total size + total_width = sum(max_widths) + padding * (cols + 1) + total_height = sum(max_heights) + padding * (rows + 1) + title_height + + # Create combined image + combined = Image.new('RGB', (total_width, total_height), (235, 235, 235)) + draw = ImageDraw.Draw(combined) + + # Add title + try: + title_font = ImageFont.truetype( + "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 24) + except BaseException: + title_font = ImageFont.load_default() + + title_text = "Page Layout Examples" + bbox = draw.textbbox((0, 0), title_text, font=title_font) + text_width = bbox[2] - bbox[0] + title_x = (total_width - text_width) // 2 + draw.text((title_x, 15), title_text, fill=(50, 50, 50), font=title_font) + + # Place images in grid + y_offset = title_height + padding + for row in range(rows): + x_offset = padding + for col in range(cols): + idx = row * cols + col + if idx < len(images): + name, img = images[idx] + # Center image in its cell + cell_width = max_widths[col] + cell_height = max_heights[row] + img_x = x_offset + (cell_width - img.size[0]) // 2 + img_y = y_offset + (cell_height - img.size[1]) // 2 + combined.paste(img, (img_x, img_y)) + x_offset += max_widths[col] + padding if col < len(max_widths) else 0 + y_offset += max_heights[row] + padding if row < len(max_heights) else 0 + + return combined + + +def main(): + """Demonstrate page layout variations.""" + print("Page Layouts Example") + print("=" * 50) + + # Create different layouts + layouts = create_layouts() + + # Create showcase + combined_image = create_layout_showcase(layouts) + + # Save output + output_dir = Path("docs/images") + output_dir.mkdir(parents=True, exist_ok=True) + output_path = output_dir / "example_03_page_layouts.png" + combined_image.save(output_path) + + print("\n✓ Example completed!") + print(f" Output saved to: {output_path}") + print(f" Image size: {combined_image.size[0]}x{combined_image.size[1]} pixels") + print(f" Created {len(layouts)} layout examples") + + return combined_image + + +if __name__ == "__main__": + main() diff --git a/examples/04_table_rendering.py b/examples/04_table_rendering.py new file mode 100644 index 0000000..6fa7b21 --- /dev/null +++ b/examples/04_table_rendering.py @@ -0,0 +1,364 @@ +#!/usr/bin/env python3 +""" +Table Rendering Example + +This example demonstrates rendering HTML tables: +- Simple tables with headers +- Tables with multiple rows and columns +- Tables with colspan and borders +- Tables with formatted content +- Tables parsed from HTML using DocumentLayouter + +Shows the HTML-first rendering pipeline. +""" + +from pyWebLayout.abstract.block import Table +from pyWebLayout.style import Font +from pyWebLayout.io.readers.html_extraction import parse_html_string +from pyWebLayout.layout.document_layouter import DocumentLayouter +from pyWebLayout.style.page_style import PageStyle +from pyWebLayout.concrete.table import TableStyle +from pyWebLayout.concrete.page import Page +import sys +from pathlib import Path +from PIL import Image, ImageDraw + +# Add pyWebLayout to path +sys.path.insert(0, str(Path(__file__).parent.parent)) + + +def create_simple_table_example(): + """Create a simple table from HTML.""" + print(" - Simple data table") + + html = """ + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameAgeCity
    Alice28Paris
    Bob34London
    Charlie25Tokyo
    + """ + + return html, "Simple Table" + + +def create_styled_table_example(): + """Create a table with custom styling.""" + print(" - Styled table") + + html = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Monthly Sales Report
    MonthRevenueExpensesProfit
    January$50,000$30,000$20,000
    February$55,000$32,000$23,000
    March$60,000$35,000$25,000
    + """ + + return html, "Styled Table" + + +def create_complex_table_example(): + """Create a table with colspan.""" + print(" - Complex table with colspan") + + html = """ + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Product Specifications
    ProductFeaturesPrice
    Laptop16GB RAM, 512GB SSD$1,299
    Monitor27 inch, 4K Resolution$599
    KeyboardMechanical, RGB$129
    + """ + + return html, "Complex Table" + + +def create_data_table_example(): + """Create a table with numerical data.""" + print(" - Data table") + + html = """ + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Test Results
    TestScoreStatus
    Unit Tests98%Pass
    Integration95%Pass
    Performance87%Pass
    + """ + + return html, "Data Table" + + +def render_table_example( + html: str, + title: str, + style_variant: int = 0, + page_size=( + 500, + 400)): + """Render a table from HTML to an image using DocumentLayouter.""" + # Create page with varying backgrounds + bg_colors = [ + (255, 255, 255), # White + (250, 255, 250), # Light green tint + (255, 250, 245), # Light orange tint + (245, 250, 255), # Light blue tint + ] + + page_style = PageStyle( + border_width=2, + border_color=(200, 200, 200), + padding=(20, 20, 20, 20), + background_color=bg_colors[style_variant % len(bg_colors)] + ) + + page = Page(size=page_size, style=page_style) + + # Parse HTML to get table + base_font = Font(font_size=12) + blocks = parse_html_string(html, base_font=base_font) + + # Find the table block + table = None + for block in blocks: + if isinstance(block, Table): + table = block + break + + # Table styles with different themes + table_styles = [ + # Style 0: Classic blue header + TableStyle( + border_width=1, + border_color=(80, 80, 80), + cell_padding=(8, 10, 8, 10), + header_bg_color=(70, 130, 180), # Steel blue + cell_bg_color=(255, 255, 255), + alternate_row_color=(240, 248, 255) # Alice blue + ), + # Style 1: Green theme + TableStyle( + border_width=2, + border_color=(34, 139, 34), # Forest green + cell_padding=(10, 12, 10, 12), + header_bg_color=(144, 238, 144), # Light green + cell_bg_color=(255, 255, 255), + alternate_row_color=(240, 255, 240) # Honeydew + ), + # Style 2: Minimal style + TableStyle( + border_width=0, + border_color=(200, 200, 200), + cell_padding=(6, 8, 6, 8), + header_bg_color=(245, 245, 245), + cell_bg_color=(255, 255, 255), + alternate_row_color=None # No alternating + ), + # Style 3: Bold borders + TableStyle( + border_width=3, + border_color=(0, 0, 0), + cell_padding=(10, 10, 10, 10), + header_bg_color=(255, 215, 0), # Gold + cell_bg_color=(255, 255, 255), + alternate_row_color=(255, 250, 205) # Lemon chiffon + ), + ] + + table_style = table_styles[style_variant % len(table_styles)] + + if table: + # Create DocumentLayouter + layouter = DocumentLayouter(page) + + # Use DocumentLayouter to layout the table + layouter.layout_table(table, style=table_style) + + # Get the rendered canvas + _ = page.draw # Ensure canvas exists + image = page._canvas + else: + # No table found - create empty page + _ = page.draw # Ensure canvas exists + draw = ImageDraw.Draw(page._canvas) + draw.text((page.border_size + 10, page.border_size + 50), + "No table found in HTML", + fill=(200, 0, 0)) + image = page._canvas + + return image + + +def combine_examples(examples): + """Combine multiple table examples into a grid.""" + print("\n Rendering table examples...") + + images = [] + for i, (html, title) in enumerate(examples): + img = render_table_example(html, title, style_variant=i) + images.append(img) + + # Create grid (2x2) + padding = 15 + cols = 2 + rows = 2 + + img_width = images[0].size[0] + img_height = images[0].size[1] + + total_width = cols * img_width + (cols + 1) * padding + total_height = rows * img_height + (rows + 1) * padding + 50 # Extra for main title + + combined = Image.new('RGB', (total_width, total_height), (240, 240, 240)) + draw = ImageDraw.Draw(combined) + + # Add main title + from PIL import ImageFont + try: + main_font = ImageFont.truetype( + "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 20) + except BaseException: + main_font = ImageFont.load_default() + + title_text = "Table Rendering Examples" + bbox = draw.textbbox((0, 0), title_text, font=main_font) + text_width = bbox[2] - bbox[0] + title_x = (total_width - text_width) // 2 + draw.text((title_x, 15), title_text, fill=(50, 50, 50), font=main_font) + + # Place images + y_offset = 50 + padding + for row in range(rows): + x_offset = padding + for col in range(cols): + idx = row * cols + col + if idx < len(images): + combined.paste(images[idx], (x_offset, y_offset)) + x_offset += img_width + padding + y_offset += img_height + padding + + return combined + + +def main(): + """Demonstrate table rendering.""" + print("Table Rendering Example") + print("=" * 50) + + # Create table examples + print("\n Creating table examples...") + examples = [ + create_simple_table_example(), + create_styled_table_example(), + create_complex_table_example(), + create_data_table_example() + ] + + # Render and combine + combined_image = combine_examples(examples) + + # Save output + output_dir = Path("docs/images") + output_dir.mkdir(parents=True, exist_ok=True) + output_path = output_dir / "example_04_table_rendering.png" + combined_image.save(output_path) + + print("\n✓ Example completed!") + print(f" Output saved to: {output_path}") + print(f" Image size: {combined_image.size[0]}x{combined_image.size[1]} pixels") + print(f" Created {len(examples)} table examples") + + return combined_image + + +if __name__ == "__main__": + main() diff --git a/examples/05_html_table_with_images.py b/examples/05_html_table_with_images.py new file mode 100644 index 0000000..39231a1 --- /dev/null +++ b/examples/05_html_table_with_images.py @@ -0,0 +1,272 @@ +#!/usr/bin/env python3 +""" +HTML Table with Images Example - End-to-End Rendering + +This example demonstrates the complete pipeline: +1. HTML table source with tags in cells +2. parse_html_string() converts HTML → Abstract document structure +3. DocumentLayouter handles all layout and rendering + +No custom rendering code needed - DocumentLayouter handles everything! +""" + +from pyWebLayout.style import Font +from pyWebLayout.concrete.table import TableStyle +from pyWebLayout.layout.document_layouter import DocumentLayouter +from pyWebLayout.style.page_style import PageStyle +from pyWebLayout.concrete.page import Page +from pyWebLayout.io.readers.html_extraction import parse_html_string +import sys +from pathlib import Path +from PIL import Image + +# Add pyWebLayout to path +sys.path.insert(0, str(Path(__file__).parent.parent)) + + +def create_book_catalog_html(): + """Create HTML for a book catalog table with actual tags.""" + # Get base path for images - use absolute paths for the img src + data_path = Path(__file__).parent.parent / "tests" / "data" + + html = f""" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    CoverTitleAuthorPrice
    The Great AdventureThe Great AdventureJohn Smith$19.99
    Mystery of the AgesMystery of the AgesJane Doe$24.99
    Science TodayScience TodayDr. Brown$29.99
    Art & DesignArt & DesignM. Artist$34.99
    + + + """ + return html + + +def create_product_showcase_html(): + """Create HTML for a product showcase table with images.""" + data_path = Path(__file__).parent.parent / "tests" / "data" + + html = f""" + + + + + + + + + + + + + + + + + + + +
    ProductDescription
    Premium EditionPremium Edition - Hardcover with gold embossing
    Collector's ItemCollector's Item - Limited print run
    + + + """ + return html + + +def render_html_with_layouter(html_string: str, title: str, + table_style: TableStyle, + page_size=(600, 500)): + """ + Render HTML using DocumentLayouter - the proper way! + + This function demonstrates the correct usage: + 1. Parse HTML → Abstract blocks + 2. Create Page + 3. Create DocumentLayouter + 4. Layout all blocks using layouter + + Args: + html_string: HTML source containing table with tags + title: Title for the output (for logging) + table_style: Table styling configuration + page_size: Page dimensions + + Returns: + PIL Image with rendered content + """ + print(f"\n Processing '{title}'...") + + # Step 1: Parse HTML to abstract blocks + print(" 1. Parsing HTML → Abstract blocks...") + base_font = Font(font_size=11) + blocks = parse_html_string(html_string, base_font=base_font) + print(f" → Parsed {len(blocks)} blocks") + + # Step 2: Create page + print(" 2. Creating page...") + page_style = PageStyle( + border_width=2, + border_color=(180, 180, 180), + padding=(20, 20, 20, 20), + background_color=(255, 255, 255) + ) + page = Page(size=page_size, style=page_style) + + # Step 3: Create DocumentLayouter + print(" 3. Creating DocumentLayouter...") + layouter = DocumentLayouter(page) + + # Step 4: Layout all blocks using the layouter + print(" 4. Laying out all blocks...") + for block in blocks: + # For tables, we can pass a custom style + from pyWebLayout.abstract.block import Table + if isinstance(block, Table): + success = layouter.layout_table(block, style=table_style) + else: + # For other blocks (paragraphs, headings, images), use layout_document + success = layouter.layout_document([block]) + + if not success: + print(f" ⚠ Warning: Block {type(block).__name__} didn't fit on page") + + print(" ✓ Layout complete!") + + # Step 5: Get the rendered canvas + # Note: Tables render directly onto page._canvas + # We access page.draw to ensure canvas is initialized + print(" 5. Getting rendered canvas...") + _ = page.draw # Ensure canvas exists + return page._canvas + + +def main(): + """Demonstrate end-to-end HTML table with images rendering using DocumentLayouter.""" + print("HTML Table with Images Example - DocumentLayouter") + print("=" * 60) + print("\nThis example demonstrates:") + print(" 1. HTML with tags inside cells") + print(" 2. parse_html_string() automatically handles images") + print(" 3. DocumentLayouter handles all layout and rendering") + print(" 4. NO manual TableRenderer or custom rendering code!") + + # Verify images exist + print("\n Checking for cover images...") + data_path = Path(__file__).parent.parent / "tests" / "data" + cover_count = 0 + for i in range(1, 5): + cover_path = data_path / f"cover {i}.png" + if cover_path.exists(): + cover_count += 1 + print(f" ✓ Found cover {i}.png") + + if cover_count == 0: + print(" ✗ No cover images found! This example requires cover images.") + return + + # Create HTML sources with tags + print("\n Creating HTML sources with tags...") + print(" - Book catalog HTML") + book_html = create_book_catalog_html() + + print(" - Product showcase HTML") + product_html = create_product_showcase_html() + + # Define table styles + book_style = TableStyle( + border_width=1, + border_color=(100, 100, 100), + cell_padding=(8, 10, 8, 10), + header_bg_color=(70, 130, 180), + cell_bg_color=(255, 255, 255), + alternate_row_color=(245, 248, 250) + ) + + product_style = TableStyle( + border_width=2, + border_color=(60, 120, 60), + cell_padding=(10, 12, 10, 12), + header_bg_color=(144, 238, 144), + cell_bg_color=(255, 255, 255), + alternate_row_color=(240, 255, 240) + ) + + # Render using DocumentLayouter - the proper way! + print("\n Rendering with DocumentLayouter (HTML → Abstract → Layout → PNG)...") + + book_image = render_html_with_layouter( + book_html, + "Book Catalog", + book_style, + page_size=(700, 600) + ) + + product_image = render_html_with_layouter( + product_html, + "Product Showcase", + product_style, + page_size=(600, 350) + ) + + # Combine images side by side + print("\n Combining output images...") + padding = 20 + total_width = book_image.size[0] + product_image.size[0] + padding * 3 + total_height = max(book_image.size[1], product_image.size[1]) + padding * 2 + + combined = Image.new('RGB', (total_width, total_height), (240, 240, 240)) + combined.paste(book_image, (padding, padding)) + combined.paste(product_image, (book_image.size[0] + padding * 2, padding)) + + # Save output + output_dir = Path("docs/images") + output_dir.mkdir(parents=True, exist_ok=True) + output_path = output_dir / "example_05_html_table_with_images.png" + combined.save(output_path) + + print("\n✓ Example completed!") + print(f" Output saved to: {output_path}") + print(f" Image size: {combined.size[0]}x{combined.size[1]} pixels") + print("\nThe complete pipeline:") + print(" 1. HTML with tags → parse_html_string() → Abstract blocks") + print(" 2. Abstract blocks → DocumentLayouter → Concrete objects") + print(" 3. Page.render() → PNG output") + print("\n ✓ Using DocumentLayouter - NO custom rendering code!") + + return combined + + +if __name__ == "__main__": + main() diff --git a/examples/06_functional_elements_demo.py b/examples/06_functional_elements_demo.py new file mode 100644 index 0000000..7ae5722 --- /dev/null +++ b/examples/06_functional_elements_demo.py @@ -0,0 +1,292 @@ +""" +Demonstration of functional elements (buttons, forms, links) with callback binding. + +This example shows how to: +1. Create functional elements programmatically +2. Layout them on a page +3. Bind callbacks after layout using the CallbackRegistry +4. Simulate user interactions + +This pattern is useful for: +- Manual GUI construction +- Applications where callbacks need access to runtime state +- Interactive document interfaces +""" + +from pyWebLayout.concrete import Page +from pyWebLayout.abstract.functional import Button, Form, FormField, FormFieldType +from pyWebLayout.abstract import Paragraph, Word +from pyWebLayout.style import Font +from pyWebLayout.style.page_style import PageStyle +from pyWebLayout.layout.document_layouter import DocumentLayouter +import numpy as np + + +class SimpleApp: + """ + A simple application that demonstrates functional element usage. + + This app has: + - A settings form + - Save and Cancel buttons + - Application state that callbacks can access + """ + + def __init__(self): + self.settings = { + "username": "", + "theme": "light", + "notifications": True + } + self.saved = False + + def on_save_click(self, point, **kwargs): + """Callback for save button""" + print(f"Save button clicked at {point}") + self.saved = True + print("Settings saved!") + return "saved" + + def on_cancel_click(self, point, **kwargs): + """Callback for cancel button""" + print(f"Cancel button clicked at {point}") + print("Changes cancelled!") + return "cancelled" + + def on_reset_click(self, point, **kwargs): + """Callback for reset button""" + print(f"Reset button clicked at {point}") + self.settings = { + "username": "", + "theme": "light", + "notifications": True + } + print("Settings reset to defaults!") + return "reset" + + +def create_settings_page(): + """ + Create a settings page with functional elements. + + Returns: + Tuple of (page, app, element_ids) where element_ids maps + semantic names to registered callback ids + """ + # Create the application instance + app = SimpleApp() + + # Create page + page = Page(size=(600, 800), style=PageStyle(border_width=10)) + layouter = DocumentLayouter(page) + + # Create content + font = Font(font_size=16, colour=(0, 0, 0)) + + # Title paragraph + title_font = Font(font_size=24, colour=(0, 0, 100)) + title = Paragraph(title_font) + title.add_word(Word("Settings", title_font)) + + # Description paragraph + desc = Paragraph(font) + desc.add_word(Word("Configure", font)) + desc.add_word(Word("your", font)) + desc.add_word(Word("application", font)) + desc.add_word(Word("preferences", font)) + desc.add_word(Word("below.", font)) + + # Layout title and description + layouter.layout_paragraph(title) + page._current_y_offset += 10 # Add some spacing + layouter.layout_paragraph(desc) + page._current_y_offset += 20 # Add more spacing before form + + # Create form + settings_form = Form( + form_id="settings-form", + action="/save-settings", + html_id="settings-form" + ) + + # Add form fields + username_field = FormField( + name="username", + field_type=FormFieldType.TEXT, + label="Username", + value="john_doe" + ) + + theme_field = FormField( + name="theme", + field_type=FormFieldType.SELECT, + label="Theme", + value="light", + options=[("light", "Light"), ("dark", "Dark")] + ) + + notifications_field = FormField( + name="notifications", + field_type=FormFieldType.CHECKBOX, + label="Enable Notifications", + value=True + ) + + settings_form.add_field(username_field) + settings_form.add_field(theme_field) + settings_form.add_field(notifications_field) + + # Layout the form + success, field_ids = layouter.layout_form(settings_form) + + if not success: + print("Warning: Form didn't fit on page!") + + page._current_y_offset += 20 # Spacing before buttons + + # Create buttons (NO callbacks yet - will be bound later) + save_button = Button( + label="Save Settings", + callback=None, # No callback yet! + html_id="save-btn" + ) + + cancel_button = Button( + label="Cancel", + callback=None, # No callback yet! + html_id="cancel-btn" + ) + + reset_button = Button( + label="Reset to Defaults", + callback=None, # No callback yet! + html_id="reset-btn" + ) + + # Layout buttons + button_font = Font(font_size=14, colour=(255, 255, 255)) + success1, save_id = layouter.layout_button(save_button, font=button_font) + page._current_y_offset += 10 # Spacing between buttons + success2, cancel_id = layouter.layout_button(cancel_button, font=button_font) + page._current_y_offset += 10 + success3, reset_id = layouter.layout_button(reset_button, font=button_font) + + # ============================================================== + # IMPORTANT: Callbacks are bound AFTER layout is complete + # This allows callbacks to access the application instance + # ============================================================== + + # Bind callbacks using the page's callback registry + page.callbacks.set_callback("save-btn", app.on_save_click) + page.callbacks.set_callback("cancel-btn", app.on_cancel_click) + page.callbacks.set_callback("reset-btn", app.on_reset_click) + + # Track element ids for later reference + element_ids = { + "save_button": save_id, + "cancel_button": cancel_id, + "reset_button": reset_id, + "form_fields": field_ids + } + + return page, app, element_ids + + +def demonstrate_callback_binding(): + """Demonstrate various callback binding patterns""" + + print("=" * 60) + print("Functional Elements Demo: Manual GUI Construction") + print("=" * 60) + print() + + # Create the page + page, app, element_ids = create_settings_page() + + print(f"Page created with {page.callbacks.count()} interactable elements") + print() + + # Show what's registered + print("Registered interactables:") + for id_name in page.callbacks.get_all_ids(): + print(f" - {id_name}") + print() + + # Show breakdown by type + print("Breakdown by type:") + for type_name in page.callbacks.get_all_types(): + count = page.callbacks.count_by_type(type_name) + print(f" - {type_name}: {count}") + print() + + # Simulate user clicking the save button + print("Simulating user interaction:") + print("-" * 60) + print() + + # Get the save button + save_button = page.callbacks.get_by_id("save-btn") + print(f"Retrieved save button: {save_button}") + + # Simulate a click at position (50, 200) + click_point = np.array([50, 200]) + print(f"Simulating click at {click_point}...") + result = save_button.interact(click_point) + print(f"Button interaction returned: {result}") + print(f"App.saved state: {app.saved}") + print() + + # Simulate clicking cancel + cancel_button = page.callbacks.get_by_id("cancel-btn") + print("Simulating cancel button click...") + result = cancel_button.interact(click_point) + print(f"Button interaction returned: {result}") + print() + + # Simulate clicking reset + reset_button = page.callbacks.get_by_id("reset-btn") + print("Simulating reset button click...") + result = reset_button.interact(click_point) + print(f"Button interaction returned: {result}") + print() + + # Demonstrate batch callback modification + print("-" * 60) + print("Demonstrating batch callback modification:") + print() + + def log_all_clicks(point, **kwargs): + """Generic click logger""" + print(f" [LOG] Button clicked at {point}") + return "logged" + + # Set this callback for all buttons + count = page.callbacks.set_callbacks_by_type("button", log_all_clicks) + print(f"Set logging callback for {count} buttons") + print() + + # Now clicking any button will just log + print("Clicking save button again (now with logging callback):") + result = save_button.interact(click_point) + print(f"Returned: {result}") + print() + + # Render the page + print("-" * 60) + print("Rendering page...") + image = page.render() + print(f"Page rendered: {image.size}") + + # Save to file + output_path = "functional_elements_demo.png" + image.save(output_path) + print(f"Saved to: {output_path}") + print() + + print("=" * 60) + print("Demo complete!") + print("=" * 60) + + +if __name__ == "__main__": + demonstrate_callback_binding() diff --git a/examples/07_pressed_state_demo.py b/examples/07_pressed_state_demo.py new file mode 100644 index 0000000..c7c1fb9 --- /dev/null +++ b/examples/07_pressed_state_demo.py @@ -0,0 +1,452 @@ +""" +Demonstration of pressed/depressed states for buttons and links with visual feedback. + +This example shows: +1. How to use the InteractionHandler for automatic press/release cycles +2. How to manually manage states for custom event loops +3. How the dirty flag system tracks when re-rendering is needed +4. Visual differences between normal, hovered, and pressed states + +The demo creates a page with buttons and links, then simulates clicking them +with proper visual feedback timing. +""" + +from pyWebLayout.concrete import Page +from pyWebLayout.concrete.interaction_handler import InteractionHandler, InteractionStateManager +from pyWebLayout.abstract.functional import Button, Link, LinkType +from pyWebLayout.abstract import Paragraph, Word +from pyWebLayout.abstract.inline import LinkedWord +from pyWebLayout.style import Font +from pyWebLayout.style.page_style import PageStyle +from pyWebLayout.layout.document_layouter import DocumentLayouter +import numpy as np +import time + + +def create_interactive_demo_page(): + """ + Create a page with various interactive elements demonstrating state changes. + """ + # Create page + page = Page(size=(600, 500), style=PageStyle(border_width=10)) + layouter = DocumentLayouter(page) + + # Create fonts + title_font = Font(font_size=24, colour=(0, 0, 100)) + body_font = Font(font_size=16, colour=(0, 0, 0)) + button_font = Font(font_size=14, colour=(255, 255, 255)) + + # Title + title = Paragraph(title_font) + title.add_word(Word("Interactive", title_font)) + title.add_word(Word("Elements", title_font)) + title.add_word(Word("Demo", title_font)) + layouter.layout_paragraph(title) + page._current_y_offset += 15 + + # Description + desc = Paragraph(body_font) + desc.add_word(Word("Click", body_font)) + desc.add_word(Word("the", body_font)) + desc.add_word(Word("buttons", body_font)) + desc.add_word(Word("and", body_font)) + desc.add_word(Word("links", body_font)) + desc.add_word(Word("below", body_font)) + desc.add_word(Word("to", body_font)) + desc.add_word(Word("see", body_font)) + desc.add_word(Word("pressed", body_font)) + desc.add_word(Word("state", body_font)) + desc.add_word(Word("feedback!", body_font)) + layouter.layout_paragraph(desc) + page._current_y_offset += 20 + + # Callback functions + def on_save(): + print("💾 Save button clicked!") + return "saved" + + def on_cancel(): + print("❌ Cancel button clicked!") + return "cancelled" + + def on_link_click(location, point): + print(f"🔗 Link clicked: {location} at {point}") + return location + + # Create buttons + save_button = Button( + label="Save Document", + callback=lambda point, **kwargs: on_save(), + html_id="save-btn" + ) + + cancel_button = Button( + label="Cancel", + callback=lambda point, **kwargs: on_cancel(), + html_id="cancel-btn" + ) + + # Layout buttons + success1, save_id = layouter.layout_button(save_button, font=button_font) + page._current_y_offset += 12 + success2, cancel_id = layouter.layout_button(cancel_button, font=button_font) + page._current_y_offset += 25 + + # Create paragraph with links + link_para = Paragraph(body_font) + link_para.add_word(Word("Visit", body_font)) + link_para.add_word(Word("our", body_font)) + + # Add a link + internal_link = Link( + location="https://example.com", + link_type=LinkType.EXTERNAL, + callback=on_link_click, + title="Example website" + ) + link_para.add_word(LinkedWord( + "website", + body_font, + location="https://example.com", + link_type=LinkType.EXTERNAL, + callback=on_link_click, + title="Example website" + )) + link_para.add_word(Word("or", body_font)) + + # Add another link + docs_link = Link( + location="/docs", + link_type=LinkType.INTERNAL, + callback=on_link_click, + title="Documentation" + ) + link_para.add_word(LinkedWord( + "documentation", + body_font, + location="/docs", + link_type=LinkType.INTERNAL, + callback=on_link_click, + title="Documentation" + )) + link_para.add_word(Word("page.", body_font)) + + layouter.layout_paragraph(link_para) + + return page, save_id, cancel_id + + +def demo_automatic_interaction(): + """ + Demonstrate automatic interaction handling with InteractionHandler. + + This shows the simplest usage pattern where InteractionHandler manages + the complete press/release cycle automatically. + """ + print("=" * 70) + print("Demo 1: Automatic Interaction with Visual Feedback") + print("=" * 70) + print() + + # Create the page + page, save_id, cancel_id = create_interactive_demo_page() + + # Create interaction handler + handler = InteractionHandler(page, press_duration_ms=150) + + print("Initial render:") + initial_render = page.render() + initial_render.save("demo_07_initial.png") + print(f" ✓ Saved: demo_07_initial.png") + print(f" ✓ Page dirty flag: {page.is_dirty}") + print() + + # Get the save button + save_button = page.callbacks.get_by_id("save-btn") + click_point = np.array([50, 150]) + + print("Simulating button click with automatic feedback...") + print(f" → Setting pressed state at t=0ms") + + # Execute with automatic feedback + pressed_frame, released_frame, result = handler.execute_with_feedback( + save_button, + click_point + ) + + print(f" → Showing pressed state for 150ms") + print(f" → Executing callback") + print(f" → Result: {result}") + print(f" → Setting released state") + + # Save the frames + pressed_frame.save("demo_07_pressed.png") + print(f" ✓ Saved: demo_07_pressed.png") + + released_frame.save("demo_07_released.png") + print(f" ✓ Saved: demo_07_released.png") + print() + + +def demo_manual_state_management(): + """ + Demonstrate manual state management for custom event loops. + + This shows how an application with its own event loop can manage + states and check the dirty flag before re-rendering. + """ + print("=" * 70) + print("Demo 2: Manual State Management with Dirty Flag Checking") + print("=" * 70) + print() + + # Create the page + page, save_id, cancel_id = create_interactive_demo_page() + + # Initial render + print("Initial render:") + current_frame = page.render() + print(f" ✓ Page dirty: {page.is_dirty} (cleaned after render)") + print() + + # Get the cancel button + cancel_button = page.callbacks.get_by_id("cancel-btn") + + # Simulate mouse down + print("Mouse down event:") + # Set page reference if not already set + if not hasattr(cancel_button, '_page') or cancel_button._page is None: + cancel_button.set_page(page) + cancel_button.set_pressed(True) + print(f" ✓ Set pressed state") + print(f" ✓ Page dirty: {page.is_dirty} (needs re-render)") + + # Check if we need to re-render + if page.is_dirty: + print(" → Re-rendering (dirty flag is set)") + current_frame = page.render() + current_frame.save("demo_07_manual_pressed.png") + print(f" ✓ Saved: demo_07_manual_pressed.png") + print(f" ✓ Page dirty: {page.is_dirty} (cleaned after render)") + print() + + # Wait a bit + print("Waiting 150ms for visual feedback...") + time.sleep(0.15) + print() + + # Execute callback + print("Executing callback:") + result = cancel_button.interact(np.array([50, 200])) + print(f" ✓ Result: {result}") + print() + + # Simulate mouse up + print("Mouse up event:") + cancel_button.set_pressed(False) + print(f" ✓ Set released state") + print(f" ✓ Page dirty: {page.is_dirty} (needs re-render)") + + # Check if we need to re-render + if page.is_dirty: + print(" → Re-rendering (dirty flag is set)") + current_frame = page.render() + current_frame.save("demo_07_manual_released.png") + print(f" ✓ Saved: demo_07_manual_released.png") + print(f" ✓ Page dirty: {page.is_dirty} (cleaned after render)") + print() + + +def demo_state_manager(): + """ + Demonstrate the InteractionStateManager for hover/press tracking. + + This shows how to use the high-level state manager that automatically + handles hover and press states based on cursor position. + """ + print("=" * 70) + print("Demo 3: InteractionStateManager for Hover and Press Tracking") + print("=" * 70) + print() + + # Create the page + page, save_id, cancel_id = create_interactive_demo_page() + + # Create state manager + state_mgr = InteractionStateManager(page) + + # Initial render + print("Initial render:") + current_frame = page.render() + print(f" ✓ Rendered initial state") + print() + + # Simulate cursor moving over a button + button_center = (150, 150) + print(f"Cursor moves to button position {button_center}:") + hover_frame = state_mgr.update_hover(button_center) + if hover_frame: + print(f" ✓ Hover state changed, page re-rendered") + hover_frame.save("demo_07_hover.png") + print(f" ✓ Saved: demo_07_hover.png") + print() + + # Simulate mouse down + print(f"Mouse down at {button_center}:") + pressed_frame = state_mgr.handle_mouse_down(button_center) + if pressed_frame: + print(f" ✓ Pressed state set, page re-rendered") + pressed_frame.save("demo_07_state_mgr_pressed.png") + print(f" ✓ Saved: demo_07_state_mgr_pressed.png") + print() + + # Wait for visual feedback + time.sleep(0.15) + + # Simulate mouse up + print(f"Mouse up at {button_center}:") + released_frame, result = state_mgr.handle_mouse_up(button_center) + if released_frame: + print(f" ✓ Released state set, page re-rendered") + print(f" ✓ Callback result: {result}") + released_frame.save("demo_07_state_mgr_released.png") + print(f" ✓ Saved: demo_07_state_mgr_released.png") + print() + + # Simulate cursor moving away + away_point = (50, 50) + print(f"Cursor moves away to {away_point}:") + away_frame = state_mgr.update_hover(away_point) + if away_frame: + print(f" ✓ Hover state cleared, page re-rendered") + away_frame.save("demo_07_no_hover.png") + print(f" ✓ Saved: demo_07_no_hover.png") + print() + + +def demo_performance_optimization(): + """ + Demonstrate how the dirty flag prevents unnecessary re-renders. + """ + print("=" * 70) + print("Demo 4: Performance Optimization with Dirty Flag") + print("=" * 70) + print() + + # Create the page + page, save_id, cancel_id = create_interactive_demo_page() + + print("Scenario: Multiple state queries without changes") + print() + + # Initial render + page.render() + print(f"1. After initial render - dirty: {page.is_dirty}") + + # Check if dirty before rendering again + print(f"2. Check dirty flag: {page.is_dirty}") + if not page.is_dirty: + print(" → Skipping render (no changes)") + print() + + # Now make a change + button = page.callbacks.get_by_id("save-btn") + print("3. Setting button to pressed state") + # Ensure page reference is set + if not hasattr(button, '_page') or button._page is None: + button.set_page(page) + button.set_pressed(True) + print(f" → dirty: {page.is_dirty}") + print() + + # This time we need to render + print(f"4. Check dirty flag: {page.is_dirty}") + if page.is_dirty: + print(" → Re-rendering (state changed)") + page.render() + print(f" → dirty after render: {page.is_dirty}") + print() + + print("Benefit: Only render when actual changes occur!") + print() + + +def create_animated_gif(): + """ + Create an animated GIF showing the button press sequence. + """ + from PIL import Image + import os + + print("=" * 70) + print("Creating Animated GIF") + print("=" * 70) + print() + + # Check if the PNG files exist + png_files = [ + "demo_07_initial.png", + "demo_07_pressed.png", + "demo_07_released.png" + ] + + if not all(os.path.exists(f) for f in png_files): + print(" ⚠ PNG files not found, skipping GIF creation") + return + + # Load the images + initial = Image.open('demo_07_initial.png') + pressed = Image.open('demo_07_pressed.png') + released = Image.open('demo_07_released.png') + + # Create animated GIF showing the button interaction sequence + # Sequence: initial (1000ms) -> pressed (200ms) -> released (500ms) -> loop + frames = [initial, pressed, released] + durations = [1000, 200, 500] # milliseconds per frame + + output_path = "docs/images/example_07_button_animation.gif" + + # Create docs/images directory if it doesn't exist + os.makedirs("docs/images", exist_ok=True) + + # Save as animated GIF + initial.save( + output_path, + save_all=True, + append_images=[pressed, released], + duration=durations, + loop=0 # 0 means loop forever + ) + + print(f" ✓ Created: {output_path}") + print(f" ✓ Frames: {len(frames)}") + print(f" ✓ Sequence: initial (1000ms) → pressed (200ms) → released (500ms)") + print() + + +if __name__ == "__main__": + print("\n") + print("╔" + "═" * 68 + "╗") + print("║" + " " * 15 + "PRESSED STATE DEMONSTRATION" + " " * 26 + "║") + print("╚" + "═" * 68 + "╝") + print() + + # Run all demos + demo_automatic_interaction() + print("\n") + + demo_manual_state_management() + print("\n") + + demo_state_manager() + print("\n") + + demo_performance_optimization() + print("\n") + + # Create animated GIF + create_animated_gif() + + print("=" * 70) + print("All demos complete! Check the generated PNG files and animated GIF.") + print("=" * 70) diff --git a/examples/08_bundled_fonts_demo.py b/examples/08_bundled_fonts_demo.py new file mode 100644 index 0000000..f74e076 --- /dev/null +++ b/examples/08_bundled_fonts_demo.py @@ -0,0 +1,227 @@ +""" +Demonstration of bundled fonts in pyWebLayout. + +This example shows: +1. How to use the bundled DejaVu font families +2. Different font variants (regular, bold, italic, bold-italic) +3. The three font families (Sans, Serif, Monospace) +4. Convenient Font.from_family() method for easy font selection + +The demo creates a page showcasing all bundled fonts with different styles. +""" + +from pyWebLayout.concrete import Page +from pyWebLayout.abstract import Paragraph, Word +from pyWebLayout.style import Font, FontWeight, FontStyle, BundledFont +from pyWebLayout.style.page_style import PageStyle +from pyWebLayout.layout.document_layouter import DocumentLayouter + + +def create_font_showcase_page(): + """ + Create a page demonstrating all bundled fonts and variants. + """ + # Create page with some padding + page = Page(size=(800, 1000), style=PageStyle(border_width=20)) + layouter = DocumentLayouter(page) + + # Title + title_font = Font.from_family( + BundledFont.SANS, + font_size=32, + colour=(0, 0, 100), + weight=FontWeight.BOLD + ) + title = Paragraph(title_font) + title.add_word(Word("Bundled", title_font)) + title.add_word(Word("Fonts", title_font)) + title.add_word(Word("Showcase", title_font)) + layouter.layout_paragraph(title) + page._current_y_offset += 20 + + # Introduction + intro_font = Font.from_family(BundledFont.SANS, font_size=14, colour=(50, 50, 50)) + intro = Paragraph(intro_font) + intro_text = "pyWebLayout bundles the DejaVu font family with three font types and four variants each." + for word in intro_text.split(): + intro.add_word(Word(word, intro_font)) + layouter.layout_paragraph(intro) + page._current_y_offset += 25 + + # --- Sans Serif Section --- + section_font = Font.from_family( + BundledFont.SANS, + font_size=20, + colour=(0, 100, 0), + weight=FontWeight.BOLD + ) + sans_section = Paragraph(section_font) + sans_section.add_word(Word("Sans-Serif", section_font)) + sans_section.add_word(Word("(DejaVu", section_font)) + sans_section.add_word(Word("Sans)", section_font)) + layouter.layout_paragraph(sans_section) + page._current_y_offset += 10 + + # Sans Regular + sans_regular = Font.from_family(BundledFont.SANS, font_size=16) + demo_text_paragraph(layouter, page, sans_regular, "Regular:") + + # Sans Bold + sans_bold = Font.from_family(BundledFont.SANS, font_size=16, weight=FontWeight.BOLD) + demo_text_paragraph(layouter, page, sans_bold, "Bold:") + + # Sans Italic + sans_italic = Font.from_family(BundledFont.SANS, font_size=16, style=FontStyle.ITALIC) + demo_text_paragraph(layouter, page, sans_italic, "Italic:") + + # Sans Bold Italic + sans_bold_italic = Font.from_family( + BundledFont.SANS, + font_size=16, + weight=FontWeight.BOLD, + style=FontStyle.ITALIC + ) + demo_text_paragraph(layouter, page, sans_bold_italic, "Bold Italic:") + page._current_y_offset += 20 + + # --- Serif Section --- + serif_section = Paragraph(section_font) + serif_section.add_word(Word("Serif", section_font)) + serif_section.add_word(Word("(DejaVu", section_font)) + serif_section.add_word(Word("Serif)", section_font)) + layouter.layout_paragraph(serif_section) + page._current_y_offset += 10 + + # Serif Regular + serif_regular = Font.from_family(BundledFont.SERIF, font_size=16) + demo_text_paragraph(layouter, page, serif_regular, "Regular:") + + # Serif Bold + serif_bold = Font.from_family(BundledFont.SERIF, font_size=16, weight=FontWeight.BOLD) + demo_text_paragraph(layouter, page, serif_bold, "Bold:") + + # Serif Italic + serif_italic = Font.from_family(BundledFont.SERIF, font_size=16, style=FontStyle.ITALIC) + demo_text_paragraph(layouter, page, serif_italic, "Italic:") + + # Serif Bold Italic + serif_bold_italic = Font.from_family( + BundledFont.SERIF, + font_size=16, + weight=FontWeight.BOLD, + style=FontStyle.ITALIC + ) + demo_text_paragraph(layouter, page, serif_bold_italic, "Bold Italic:") + page._current_y_offset += 20 + + # --- Monospace Section --- + mono_section = Paragraph(section_font) + mono_section.add_word(Word("Monospace", section_font)) + mono_section.add_word(Word("(DejaVu", section_font)) + mono_section.add_word(Word("Sans", section_font)) + mono_section.add_word(Word("Mono)", section_font)) + layouter.layout_paragraph(mono_section) + page._current_y_offset += 10 + + # Mono Regular + mono_regular = Font.from_family(BundledFont.MONOSPACE, font_size=14) + demo_code_paragraph(layouter, page, mono_regular, "Regular:") + + # Mono Bold + mono_bold = Font.from_family(BundledFont.MONOSPACE, font_size=14, weight=FontWeight.BOLD) + demo_code_paragraph(layouter, page, mono_bold, "Bold:") + + # Mono Italic + mono_italic = Font.from_family(BundledFont.MONOSPACE, font_size=14, style=FontStyle.ITALIC) + demo_code_paragraph(layouter, page, mono_italic, "Italic:") + + # Mono Bold Italic + mono_bold_italic = Font.from_family( + BundledFont.MONOSPACE, + font_size=14, + weight=FontWeight.BOLD, + style=FontStyle.ITALIC + ) + demo_code_paragraph(layouter, page, mono_bold_italic, "Bold Italic:") + page._current_y_offset += 20 + + # Footer + footer_font = Font.from_family(BundledFont.SANS, font_size=12, colour=(100, 100, 100)) + footer = Paragraph(footer_font) + footer_text = "All fonts are free and open source under the Bitstream Vera License." + for word in footer_text.split(): + footer.add_word(Word(word, footer_font)) + layouter.layout_paragraph(footer) + + return page + + +def demo_text_paragraph(layouter, page, font, label): + """Create a paragraph showing sample text with the given font.""" + # Label in smaller font + label_font = Font.from_family(BundledFont.SANS, font_size=12, colour=(100, 100, 100)) + label_para = Paragraph(label_font) + label_para.add_word(Word(label, label_font)) + layouter.layout_paragraph(label_para) + page._current_y_offset += 5 + + # Sample text + para = Paragraph(font) + sample = "The quick brown fox jumps over the lazy dog. 0123456789" + for word in sample.split(): + para.add_word(Word(word, font)) + layouter.layout_paragraph(para) + page._current_y_offset += 8 + + +def demo_code_paragraph(layouter, page, font, label): + """Create a paragraph showing sample code with the given font.""" + # Label in smaller font + label_font = Font.from_family(BundledFont.SANS, font_size=12, colour=(100, 100, 100)) + label_para = Paragraph(label_font) + label_para.add_word(Word(label, label_font)) + layouter.layout_paragraph(label_para) + page._current_y_offset += 5 + + # Sample code + para = Paragraph(font) + code = "def hello(): print('Hello, World!') # 0123456789" + for word in code.split(): + para.add_word(Word(word, font)) + layouter.layout_paragraph(para) + page._current_y_offset += 8 + + +if __name__ == "__main__": + print("\n") + print("=" * 70) + print("Bundled Fonts Demonstration") + print("=" * 70) + print() + + print("Creating font showcase page...") + page = create_font_showcase_page() + + print("Rendering page...") + image = page.render() + + output_file = "demo_08_bundled_fonts.png" + image.save(output_file) + print(f"Saved: {output_file}") + + print() + print("=" * 70) + print("Demo complete!") + print() + print("The page showcases all bundled fonts:") + print(" - DejaVu Sans (Sans-serif)") + print(" - DejaVu Serif (Serif)") + print(" - DejaVu Sans Mono (Monospace)") + print() + print("Each family has 4 variants:") + print(" - Regular") + print(" - Bold") + print(" - Italic") + print(" - Bold Italic") + print("=" * 70) + print() diff --git a/examples/08_pagination_demo.py b/examples/08_pagination_demo.py new file mode 100644 index 0000000..dab48a8 --- /dev/null +++ b/examples/08_pagination_demo.py @@ -0,0 +1,367 @@ +#!/usr/bin/env python3 +""" +Pagination Example with PageBreak + +This example demonstrates: +- Using PageBreak to force content onto new pages +- Multi-page document layout with automatic page creation +- Different content types across multiple pages +- Page numbering and document flow +- Combining text, images, and tables across pages + +This shows how to create multi-page documents with explicit page breaks. +""" + +import sys +from pathlib import Path +from PIL import Image, ImageDraw + +# Add pyWebLayout to path +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from pyWebLayout.concrete.page import Page +from pyWebLayout.style.page_style import PageStyle +from pyWebLayout.style.fonts import Font +from pyWebLayout.abstract.inline import Word +from pyWebLayout.abstract.block import Paragraph, PageBreak, Image as AbstractImage +from pyWebLayout.layout.document_layouter import DocumentLayouter + + +def create_sample_paragraph(text: str, font_size: int = 14) -> Paragraph: + """Create a paragraph from plain text.""" + font = Font(font_size=font_size, colour=(50, 50, 50)) + paragraph = Paragraph(style=font) + for word in text.split(): + paragraph.add_word(Word(word, font)) + return paragraph + + +def create_title_paragraph(text: str) -> Paragraph: + """Create a title paragraph with larger font.""" + font = Font(font_size=24, colour=(0, 0, 100), weight='bold') + paragraph = Paragraph(style=font) + for word in text.split(): + paragraph.add_word(Word(word, font)) + return paragraph + + +def create_heading_paragraph(text: str) -> Paragraph: + """Create a heading paragraph.""" + font = Font(font_size=18, colour=(50, 50, 100), weight='bold') + paragraph = Paragraph(style=font) + for word in text.split(): + paragraph.add_word(Word(word, font)) + return paragraph + + +def create_placeholder_image(width: int, height: int, label: str) -> AbstractImage: + """Create a placeholder image for demonstration.""" + img = Image.new('RGB', (width, height), (200, 220, 240)) + draw = ImageDraw.Draw(img) + + # Draw border + draw.rectangle([0, 0, width-1, height-1], outline=(100, 120, 140), width=2) + + # Add label + text_bbox = draw.textbbox((0, 0), label) + text_width = text_bbox[2] - text_bbox[0] + text_height = text_bbox[3] - text_bbox[1] + text_x = (width - text_width) // 2 + text_y = (height - text_height) // 2 + draw.text((text_x, text_y), label, fill=(80, 80, 120)) + + return AbstractImage(source=img) + + +def create_example_document_with_pagebreaks(): + """ + Example: Multi-page document with explicit page breaks. + + This demonstrates how PageBreak forces content onto new pages. + """ + print("\n Creating multi-page document with PageBreaks...") + + # Define common page style + page_style = PageStyle( + border_width=2, + border_color=(100, 100, 150), + padding=(30, 40, 30, 40), + background_color=(255, 255, 255), + line_spacing=6 + ) + + # Create document content with page breaks + content = [ + # Page 1: Title and Introduction + create_title_paragraph("Multi-Page Document Example"), + create_sample_paragraph( + "This document demonstrates how to use PageBreak elements to control " + "document pagination. Each PageBreak forces subsequent content to start " + "on a new page, allowing you to structure multi-page documents precisely." + ), + create_sample_paragraph( + "Page breaks are particularly useful for creating chapters, sections, or " + "ensuring that important content starts at the top of a fresh page rather " + "than being split across page boundaries." + ), + + # Force page break - next content will be on page 2 + PageBreak(), + + # Page 2: First Section + create_heading_paragraph("Section 1: Text Content"), + create_sample_paragraph( + "This is the second page of our document. It starts with a clean break " + "from the previous page, ensuring the section heading appears at the top." + ), + create_sample_paragraph( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod " + "tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim " + "veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea " + "commodo consequat." + ), + create_sample_paragraph( + "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum " + "dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non " + "proident, sunt in culpa qui officia deserunt mollit anim id est laborum." + ), + + # Another page break + PageBreak(), + + # Page 3: Images + create_heading_paragraph("Section 2: Visual Content"), + create_sample_paragraph( + "This page contains image content, demonstrating that page breaks work " + "correctly with different content types." + ), + create_placeholder_image(300, 200, "Figure 1: Sample Image"), + create_sample_paragraph("The image above is placed on this dedicated page."), + + # Final page break + PageBreak(), + + # Page 4: Conclusion + create_heading_paragraph("Conclusion"), + create_sample_paragraph( + "This final page demonstrates that you can create complex multi-page " + "documents by strategically placing PageBreak elements in your content." + ), + create_sample_paragraph( + "Key benefits of using PageBreak: 1) Control where pages start, " + "2) Prevent awkward content splits, 3) Create professional-looking " + "documents with proper sectioning, 4) Ensure important content gets " + "visual prominence at page tops." + ), + create_sample_paragraph( + "Thank you for reviewing this pagination example. Try experimenting " + "with PageBreak placement to create your own multi-page documents!" + ), + ] + + # Layout the document across multiple pages + pages = [] + current_page = Page(size=(600, 800), style=page_style) + layouter = DocumentLayouter(current_page) + + for element in content: + if isinstance(element, PageBreak): + # Save current page and create a new one + pages.append(current_page) + current_page = Page(size=(600, 800), style=page_style) + layouter = DocumentLayouter(current_page) + elif isinstance(element, Paragraph): + success, _, _ = layouter.layout_paragraph(element) + if not success: + # Page is full, create new page and retry + pages.append(current_page) + current_page = Page(size=(600, 800), style=page_style) + layouter = DocumentLayouter(current_page) + success, _, _ = layouter.layout_paragraph(element) + if not success: + print(" WARNING: Content too large for page") + elif isinstance(element, AbstractImage): + success = layouter.layout_image(element) + if not success: + # Image doesn't fit, try on new page + pages.append(current_page) + current_page = Page(size=(600, 800), style=page_style) + layouter = DocumentLayouter(current_page) + success = layouter.layout_image(element) + if not success: + print(" WARNING: Image too large for page") + + # Add the final page + pages.append(current_page) + + print(f" Created {len(pages)} pages") + return pages + + +def create_auto_pagination_example(): + """ + Example: Document that automatically flows to multiple pages. + + This shows the difference between automatic pagination (when content + doesn't fit) vs explicit PageBreak usage. + """ + print("\n Creating auto-paginated document (no explicit breaks)...") + + page_style = PageStyle( + border_width=1, + border_color=(150, 150, 150), + padding=(20, 30, 20, 30), + background_color=(250, 250, 250), + line_spacing=5 + ) + + # Create lots of content that will naturally overflow + content = [ + create_heading_paragraph("Auto-Pagination Example"), + create_sample_paragraph( + "This document does NOT use PageBreak. Instead, it demonstrates how " + "content automatically flows to new pages when the current page is full." + ), + ] + + # Add many paragraphs to force automatic page breaks + for i in range(1, 11): + content.append( + create_sample_paragraph( + f"Paragraph {i}: This is automatically laid out content. " + f"When this paragraph doesn't fit on the current page, the layouter " + f"will create a new page automatically. This is different from using " + f"PageBreak which forces a new page regardless of available space. " + f"Auto-pagination is useful for flowing content naturally." + ) + ) + + # Layout across pages + pages = [] + current_page = Page(size=(500, 600), style=page_style) + layouter = DocumentLayouter(current_page) + + for element in content: + if isinstance(element, Paragraph): + success, _, _ = layouter.layout_paragraph(element) + if not success: + # Auto page break - content didn't fit + pages.append(current_page) + current_page = Page(size=(500, 600), style=page_style) + layouter = DocumentLayouter(current_page) + layouter.layout_paragraph(element) + + pages.append(current_page) + + print(f" Auto-created {len(pages)} pages") + return pages + + +def add_page_numbers(pages, start_number: int = 1): + """Add page numbers to rendered pages.""" + numbered_pages = [] + font = Font(font_size=10, colour=(100, 100, 100)) + + for i, page in enumerate(pages, start=start_number): + # Render the page + img = page.render() + draw = ImageDraw.Draw(img) + + # Add page number at bottom center + page_text = f"Page {i}" + bbox = draw.textbbox((0, 0), page_text) + text_width = bbox[2] - bbox[0] + x = (img.size[0] - text_width) // 2 + y = img.size[1] - 20 + + draw.text((x, y), page_text, fill=(100, 100, 100)) + numbered_pages.append(img) + + return numbered_pages + + +def combine_pages_vertically(pages, title: str = ""): + """Combine multiple pages into a vertical strip.""" + if not pages: + return None + + padding = 20 + title_height = 40 if title else 0 + + # Calculate dimensions + page_width = pages[0].size[0] + page_height = pages[0].size[1] + + total_width = page_width + 2 * padding + total_height = len(pages) * (page_height + padding) + padding + title_height + + # Create combined image + combined = Image.new('RGB', (total_width, total_height), (240, 240, 240)) + draw = ImageDraw.Draw(combined) + + # Draw title if provided + if title: + from PIL import ImageFont + try: + title_font = ImageFont.truetype( + "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 16 + ) + except: + title_font = ImageFont.load_default() + + bbox = draw.textbbox((0, 0), title, font=title_font) + text_width = bbox[2] - bbox[0] + title_x = (total_width - text_width) // 2 + draw.text((title_x, 10), title, fill=(50, 50, 50), font=title_font) + + # Place pages vertically + y_offset = title_height + padding + for page_img in pages: + combined.paste(page_img, (padding, y_offset)) + y_offset += page_height + padding + + return combined + + +def main(): + """Demonstrate pagination with PageBreak.""" + print("Pagination Example with PageBreak") + print("=" * 50) + + # Example 1: Explicit page breaks + pages1 = create_example_document_with_pagebreaks() + rendered_pages1 = add_page_numbers(pages1) + combined1 = combine_pages_vertically( + rendered_pages1, + "Example 1: Explicit PageBreak Usage" + ) + + # Example 2: Auto pagination + pages2 = create_auto_pagination_example() + rendered_pages2 = add_page_numbers(pages2) + combined2 = combine_pages_vertically( + rendered_pages2, + "Example 2: Automatic Pagination" + ) + + # Save outputs + output_dir = Path("docs/images") + output_dir.mkdir(parents=True, exist_ok=True) + + output_path1 = output_dir / "example_08_pagination_explicit.png" + output_path2 = output_dir / "example_08_pagination_auto.png" + + combined1.save(output_path1) + combined2.save(output_path2) + + print("\n✓ Example completed!") + print(f" Output 1 saved to: {output_path1}") + print(f" - {len(pages1)} pages with explicit PageBreaks") + print(f" Output 2 saved to: {output_path2}") + print(f" - {len(pages2)} pages with auto-pagination") + + return combined1, combined2 + + +if __name__ == "__main__": + main() diff --git a/examples/09_link_navigation_demo.py b/examples/09_link_navigation_demo.py new file mode 100644 index 0000000..96fbb03 --- /dev/null +++ b/examples/09_link_navigation_demo.py @@ -0,0 +1,390 @@ +#!/usr/bin/env python3 +""" +Link Navigation Example + +This example demonstrates: +- Creating clickable links with LinkedWord +- Different link types (INTERNAL, EXTERNAL, API, FUNCTION) +- Link styling with underlines and colors +- Link callbacks and event handling +- Interactive link states (hover, pressed) +- Organizing linked content in paragraphs + +This shows how to create interactive documents with hyperlinks. +""" + +import sys +from pathlib import Path +from typing import List + +# Add pyWebLayout to path +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from pyWebLayout.concrete.page import Page +from pyWebLayout.style.page_style import PageStyle +from pyWebLayout.style.fonts import Font +from pyWebLayout.abstract.inline import Word, LinkedWord +from pyWebLayout.abstract.functional import LinkType +from pyWebLayout.abstract.block import Paragraph +from pyWebLayout.layout.document_layouter import DocumentLayouter + + +# Track link clicks for demonstration +link_clicks = [] + + +def link_callback(link_id: str): + """Callback for link clicks""" + def callback(): + link_clicks.append(link_id) + print(f" Link clicked: {link_id}") + return callback + + +def create_paragraph_with_links( + text_parts: List[tuple], + font_size: int = 14) -> Paragraph: + """ + Create a paragraph with mixed text and links. + + Args: + text_parts: List of tuples where each is either: + ('text', "word1 word2") for normal text + ('link', "word", location, link_type, callback_id) + font_size: Base font size + + Returns: + Paragraph with words and links + """ + font = Font(font_size=font_size, colour=(50, 50, 50)) + paragraph = Paragraph(style=font) + + for part in text_parts: + if part[0] == 'text': + # Add normal words + for word_text in part[1].split(): + paragraph.add_word(Word(word_text, font)) + elif part[0] == 'link': + # Add linked word + word_text, location, link_type, callback_id = part[1:] + callback = link_callback(callback_id) + linked_word = LinkedWord( + text=word_text, + style=font, + location=location, + link_type=link_type, + callback=callback, + title=f"Click to: {location}" + ) + paragraph.add_word(linked_word) + + return paragraph + + +def create_example_1_internal_links(): + """Example 1: Internal navigation links within a document.""" + print("\n Creating Example 1: Internal links...") + + page_style = PageStyle( + border_width=2, + border_color=(150, 150, 200), + padding=(20, 30, 20, 30), + background_color=(255, 255, 255), + line_spacing=6 + ) + + page = Page(size=(500, 600), style=page_style) + layouter = DocumentLayouter(page) + + # Title + title_font = Font(font_size=20, colour=(0, 0, 100), weight='bold') + title = Paragraph(style=title_font) + for word in "Internal Navigation Links".split(): + title.add_word(Word(word, title_font)) + + # Content with internal links + intro = create_paragraph_with_links([ + ('text', "This document demonstrates"), + ('link', "internal", "#section1", LinkType.INTERNAL, "goto_section1"), + ('text', "navigation links that jump to different parts of the document."), + ]) + + section1 = create_paragraph_with_links([ + ('text', "Jump to"), + ('link', "Section 2", "#section2", LinkType.INTERNAL, "goto_section2"), + ('text', "or"), + ('link', "Section 3", "#section3", LinkType.INTERNAL, "goto_section3"), + ('text', "within this document."), + ]) + + section2 = create_paragraph_with_links([ + ('text', "You are in Section 2. Return to"), + ('link', "top", "#top", LinkType.INTERNAL, "goto_top"), + ('text', "or go to"), + ('link', "Section 3", "#section3", LinkType.INTERNAL, "goto_section3_from2"), + ]) + + section3 = create_paragraph_with_links([ + ('text', "This is Section 3. Go back to"), + ('link', "Section 1", "#section1", LinkType.INTERNAL, "goto_section1_from3"), + ('text', "or"), + ('link', "top", "#top", LinkType.INTERNAL, "goto_top_from3"), + ]) + + # Layout content + layouter.layout_paragraph(title) + layouter.layout_paragraph(intro) + layouter.layout_paragraph(section1) + layouter.layout_paragraph(section2) + layouter.layout_paragraph(section3) + + return page + + +def create_example_2_external_links(): + """Example 2: External links to websites.""" + print(" Creating Example 2: External links...") + + page_style = PageStyle( + border_width=2, + border_color=(150, 200, 150), + padding=(20, 30, 20, 30), + background_color=(255, 255, 255), + line_spacing=6 + ) + + page = Page(size=(500, 600), style=page_style) + layouter = DocumentLayouter(page) + + # Title + title_font = Font(font_size=20, colour=(0, 100, 0), weight='bold') + title = Paragraph(style=title_font) + for word in "External Web Links".split(): + title.add_word(Word(word, title_font)) + + # Content with external links + intro = create_paragraph_with_links([ + ('text', "Click"), + ('link', "here", "https://example.com", LinkType.EXTERNAL, "visit_example"), + ('text', "to visit an external website."), + ]) + + resources = create_paragraph_with_links([ + ('text', "Useful resources:"), + ('link', "Documentation", "https://docs.example.com", LinkType.EXTERNAL, "visit_docs"), + ('text', "and"), + ('link', "GitHub", "https://github.com/example", LinkType.EXTERNAL, "visit_github"), + ]) + + more_links = create_paragraph_with_links([ + ('text', "Learn more at"), + ('link', "Wikipedia", "https://wikipedia.org", LinkType.EXTERNAL, "visit_wiki"), + ('text', "or check out"), + ('link', "Python.org", "https://python.org", LinkType.EXTERNAL, "visit_python"), + ]) + + # Layout content + layouter.layout_paragraph(title) + layouter.layout_paragraph(intro) + layouter.layout_paragraph(resources) + layouter.layout_paragraph(more_links) + + return page + + +def create_example_3_api_links(): + """Example 3: API links that trigger actions.""" + print(" Creating Example 3: API links...") + + page_style = PageStyle( + border_width=2, + border_color=(200, 150, 150), + padding=(20, 30, 20, 30), + background_color=(255, 255, 255), + line_spacing=6 + ) + + page = Page(size=(500, 600), style=page_style) + layouter = DocumentLayouter(page) + + # Title + title_font = Font(font_size=20, colour=(150, 0, 0), weight='bold') + title = Paragraph(style=title_font) + for word in "API Action Links".split(): + title.add_word(Word(word, title_font)) + + # Content with API links + settings = create_paragraph_with_links([ + ('text', "Click"), + ('link', "Settings", "/api/settings", LinkType.API, "open_settings"), + ('text', "to configure the application."), + ]) + + actions = create_paragraph_with_links([ + ('text', "Actions:"), + ('link', "Save", "/api/save", LinkType.API, "save_action"), + ('text', "or"), + ('link', "Export", "/api/export", LinkType.API, "export_action"), + ('text', "your data."), + ]) + + management = create_paragraph_with_links([ + ('text', "Manage:"), + ('link', "Users", "/api/users", LinkType.API, "manage_users"), + ('text', "or"), + ('link', "Permissions", "/api/permissions", LinkType.API, "manage_perms"), + ]) + + # Layout content + layouter.layout_paragraph(title) + layouter.layout_paragraph(settings) + layouter.layout_paragraph(actions) + layouter.layout_paragraph(management) + + return page + + +def create_example_4_function_links(): + """Example 4: Function links that execute code.""" + print(" Creating Example 4: Function links...") + + page_style = PageStyle( + border_width=2, + border_color=(150, 200, 200), + padding=(20, 30, 20, 30), + background_color=(255, 255, 255), + line_spacing=6 + ) + + page = Page(size=(500, 600), style=page_style) + layouter = DocumentLayouter(page) + + # Title + title_font = Font(font_size=20, colour=(0, 120, 120), weight='bold') + title = Paragraph(style=title_font) + for word in "Function Execution Links".split(): + title.add_word(Word(word, title_font)) + + # Content with function links + intro = create_paragraph_with_links([ + ('text', "These links execute"), + ('link', "functions", "calculate()", LinkType.FUNCTION, "exec_calculate"), + ('text', "directly in the application."), + ]) + + calculations = create_paragraph_with_links([ + ('text', "Run:"), + ('link', "analyze()", "analyze()", LinkType.FUNCTION, "exec_analyze"), + ('text', "or"), + ('link', "process()", "process()", LinkType.FUNCTION, "exec_process"), + ]) + + utilities = create_paragraph_with_links([ + ('text', "Utilities:"), + ('link', "validate()", "validate()", LinkType.FUNCTION, "exec_validate"), + ('text', "and"), + ('link', "cleanup()", "cleanup()", LinkType.FUNCTION, "exec_cleanup"), + ]) + + # Layout content + layouter.layout_paragraph(title) + layouter.layout_paragraph(intro) + layouter.layout_paragraph(calculations) + layouter.layout_paragraph(utilities) + + return page + + +def combine_pages_into_grid(pages, title): + """Combine multiple pages into a 2x2 grid.""" + from PIL import Image, ImageDraw, ImageFont + + print("\n Combining pages into grid...") + + # Render all pages + images = [page.render() for page in pages] + + # Grid layout + padding = 20 + title_height = 40 + cols = 2 + rows = 2 + + # Calculate dimensions + img_width = images[0].size[0] + img_height = images[0].size[1] + + total_width = cols * img_width + (cols + 1) * padding + total_height = rows * img_height + (rows + 1) * padding + title_height + + # Create combined image + combined = Image.new('RGB', (total_width, total_height), (240, 240, 240)) + draw = ImageDraw.Draw(combined) + + # Draw title + try: + title_font = ImageFont.truetype( + "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 18 + ) + except: + title_font = ImageFont.load_default() + + # Center the title + bbox = draw.textbbox((0, 0), title, font=title_font) + text_width = bbox[2] - bbox[0] + title_x = (total_width - text_width) // 2 + draw.text((title_x, 10), title, fill=(50, 50, 50), font=title_font) + + # Place pages in grid + y_offset = title_height + padding + for row in range(rows): + x_offset = padding + for col in range(cols): + idx = row * cols + col + if idx < len(images): + combined.paste(images[idx], (x_offset, y_offset)) + x_offset += img_width + padding + y_offset += img_height + padding + + return combined + + +def main(): + """Demonstrate link navigation across different link types.""" + global link_clicks + link_clicks = [] + + print("Link Navigation Example") + print("=" * 50) + + # Create examples for each link type + pages = [ + create_example_1_internal_links(), + create_example_2_external_links(), + create_example_3_api_links(), + create_example_4_function_links() + ] + + # Combine into demonstration image + combined_image = combine_pages_into_grid( + pages, + "Link Types: Internal | External | API | Function" + ) + + # Save output + output_dir = Path("docs/images") + output_dir.mkdir(parents=True, exist_ok=True) + output_path = output_dir / "example_09_link_navigation.png" + combined_image.save(output_path) + + print("\n✓ Example completed!") + print(f" Output saved to: {output_path}") + print(f" Image size: {combined_image.size[0]}x{combined_image.size[1]} pixels") + print(f" Created {len(pages)} link type examples") + print(f" Total links created: {len(link_clicks)} callbacks registered") + + return combined_image, link_clicks + + +if __name__ == "__main__": + main() diff --git a/examples/10_forms_demo.py b/examples/10_forms_demo.py new file mode 100644 index 0000000..abcf036 --- /dev/null +++ b/examples/10_forms_demo.py @@ -0,0 +1,374 @@ +#!/usr/bin/env python3 +""" +Comprehensive Forms Example + +This example demonstrates: +- All FormFieldType variations (TEXT, PASSWORD, EMAIL, etc.) +- Form layout with multiple fields +- Field labels and validation +- Form submission callbacks +- Organizing forms on pages + +This shows how to create interactive forms with all available field types. +""" + +import sys +from pathlib import Path + +# Add pyWebLayout to path +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from pyWebLayout.concrete.page import Page +from pyWebLayout.style.page_style import PageStyle +from pyWebLayout.style.fonts import Font +from pyWebLayout.abstract.functional import Form, FormField, FormFieldType +from pyWebLayout.layout.document_layouter import DocumentLayouter +from PIL import Image, ImageDraw + + +# Track form submissions +form_submissions = [] + + +def form_submit_callback(form_id: str): + """Callback for form submissions""" + def callback(data): + form_submissions.append((form_id, data)) + print(f" Form submitted: {form_id} with data: {data}") + return callback + + +def create_example_1_text_fields(): + """Example 1: Text input fields""" + print("\n Creating Example 1: Text input fields...") + + page_style = PageStyle( + border_width=2, + border_color=(150, 150, 200), + padding=(20, 30, 20, 30), + background_color=(255, 255, 255) + ) + + page = Page(size=(500, 600), style=page_style) + layouter = DocumentLayouter(page) + + # Create form with text fields + form = Form(form_id="text_form", html_id="text_form", callback=form_submit_callback("text_form")) + + # Add various text-based fields + form.add_field(FormField( + name="username", + label="Username", + field_type=FormFieldType.TEXT, + required=True + )) + + form.add_field(FormField( + name="email", + label="Email Address", + field_type=FormFieldType.EMAIL, + required=True + )) + + form.add_field(FormField( + name="password", + label="Password", + field_type=FormFieldType.PASSWORD, + required=True + )) + + form.add_field(FormField( + name="website", + label="Website URL", + field_type=FormFieldType.URL, + required=False + )) + + form.add_field(FormField( + name="bio", + label="Biography", + field_type=FormFieldType.TEXTAREA, + required=False + )) + + # Layout the form + font = Font(font_size=12, colour=(50, 50, 50)) + success, field_ids = layouter.layout_form(form, font=font) + + print(f" Laid out {len(field_ids)} text fields") + return page + + +def create_example_2_number_fields(): + """Example 2: Number and date/time fields""" + print(" Creating Example 2: Number and date/time fields...") + + page_style = PageStyle( + border_width=2, + border_color=(150, 200, 150), + padding=(20, 30, 20, 30), + background_color=(255, 255, 255) + ) + + page = Page(size=(500, 600), style=page_style) + layouter = DocumentLayouter(page) + + # Create form with number/date fields + form = Form(form_id="number_form", html_id="number_form", callback=form_submit_callback("number_form")) + + form.add_field(FormField( + name="age", + label="Age", + field_type=FormFieldType.NUMBER, + required=True + )) + + form.add_field(FormField( + name="birth_date", + label="Birth Date", + field_type=FormFieldType.DATE, + required=True + )) + + form.add_field(FormField( + name="appointment", + label="Appointment Time", + field_type=FormFieldType.TIME, + required=False + )) + + form.add_field(FormField( + name="rating", + label="Rating (1-10)", + field_type=FormFieldType.RANGE, + required=False + )) + + form.add_field(FormField( + name="color", + label="Favorite Color", + field_type=FormFieldType.COLOR, + required=False + )) + + # Layout the form + font = Font(font_size=12, colour=(50, 50, 50)) + success, field_ids = layouter.layout_form(form, font=font) + + print(f" Laid out {len(field_ids)} number/date fields") + return page + + +def create_example_3_selection_fields(): + """Example 3: Checkbox, radio, and select fields""" + print(" Creating Example 3: Selection fields...") + + page_style = PageStyle( + border_width=2, + border_color=(200, 150, 150), + padding=(20, 30, 20, 30), + background_color=(255, 255, 255) + ) + + page = Page(size=(500, 600), style=page_style) + layouter = DocumentLayouter(page) + + # Create form with selection fields + form = Form(form_id="selection_form", html_id="selection_form", callback=form_submit_callback("selection_form")) + + form.add_field(FormField( + name="newsletter", + label="Subscribe to Newsletter", + field_type=FormFieldType.CHECKBOX, + required=False + )) + + form.add_field(FormField( + name="terms", + label="Accept Terms and Conditions", + field_type=FormFieldType.CHECKBOX, + required=True + )) + + form.add_field(FormField( + name="gender", + label="Gender", + field_type=FormFieldType.RADIO, + required=False + )) + + form.add_field(FormField( + name="country", + label="Country", + field_type=FormFieldType.SELECT, + required=True + )) + + form.add_field(FormField( + name="hidden_token", + label="", # Hidden fields don't display labels + field_type=FormFieldType.HIDDEN, + required=False + )) + + # Layout the form + font = Font(font_size=12, colour=(50, 50, 50)) + success, field_ids = layouter.layout_form(form, font=font) + + print(f" Laid out {len(field_ids)} selection fields") + return page + + +def create_example_4_complete_form(): + """Example 4: Complete registration form with mixed field types""" + print(" Creating Example 4: Complete registration form...") + + page_style = PageStyle( + border_width=2, + border_color=(150, 200, 200), + padding=(20, 30, 20, 30), + background_color=(255, 255, 255) + ) + + page = Page(size=(500, 700), style=page_style) + layouter = DocumentLayouter(page) + + # Create comprehensive registration form + form = Form(form_id="registration_form", html_id="registration_form", callback=form_submit_callback("registration")) + + # Personal information + form.add_field(FormField( + name="full_name", + label="Full Name", + field_type=FormFieldType.TEXT, + required=True + )) + + form.add_field(FormField( + name="email", + label="Email", + field_type=FormFieldType.EMAIL, + required=True + )) + + form.add_field(FormField( + name="password", + label="Password", + field_type=FormFieldType.PASSWORD, + required=True + )) + + form.add_field(FormField( + name="age", + label="Age", + field_type=FormFieldType.NUMBER, + required=True + )) + + # Preferences + form.add_field(FormField( + name="notifications", + label="Enable Notifications", + field_type=FormFieldType.CHECKBOX, + required=False + )) + + # Layout the form + font = Font(font_size=12, colour=(50, 50, 50)) + success, field_ids = layouter.layout_form(form, font=font, field_spacing=15) + + print(f" Laid out complete form with {len(field_ids)} fields") + return page + + +def combine_pages_into_grid(pages, title): + """Combine multiple pages into a 2x2 grid.""" + print("\n Combining pages into grid...") + + # Render all pages + images = [page.render() for page in pages] + + # Grid layout + padding = 20 + title_height = 40 + cols = 2 + rows = 2 + + # Calculate dimensions + img_width = images[0].size[0] + img_height = images[0].size[1] + + total_width = cols * img_width + (cols + 1) * padding + total_height = rows * img_height + (rows + 1) * padding + title_height + + # Create combined image + combined = Image.new('RGB', (total_width, total_height), (240, 240, 240)) + draw = ImageDraw.Draw(combined) + + # Draw title + from PIL import ImageFont + try: + title_font = ImageFont.truetype( + "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 18 + ) + except: + title_font = ImageFont.load_default() + + bbox = draw.textbbox((0, 0), title, font=title_font) + text_width = bbox[2] - bbox[0] + title_x = (total_width - text_width) // 2 + draw.text((title_x, 10), title, fill=(50, 50, 50), font=title_font) + + # Place pages in grid + y_offset = title_height + padding + for row in range(rows): + x_offset = padding + for col in range(cols): + idx = row * cols + col + if idx < len(images): + combined.paste(images[idx], (x_offset, y_offset)) + x_offset += img_width + padding + y_offset += img_height + padding + + return combined + + +def main(): + """Demonstrate comprehensive form field types.""" + global form_submissions + form_submissions = [] + + print("Comprehensive Forms Example") + print("=" * 50) + + # Create examples for different form types + pages = [ + create_example_1_text_fields(), + create_example_2_number_fields(), + create_example_3_selection_fields(), + create_example_4_complete_form() + ] + + # Combine into demonstration image + combined_image = combine_pages_into_grid( + pages, + "Form Field Types: Text | Numbers | Selection | Complete" + ) + + # Save output + output_dir = Path("docs/images") + output_dir.mkdir(parents=True, exist_ok=True) + output_path = output_dir / "example_10_forms.png" + combined_image.save(output_path) + + print("\n✓ Example completed!") + print(f" Output saved to: {output_path}") + print(f" Image size: {combined_image.size[0]}x{combined_image.size[1]} pixels") + print(f" Created {len(pages)} form examples") + print(f" Total form callbacks registered: {len(form_submissions)}") + + return combined_image, form_submissions + + +if __name__ == "__main__": + main() diff --git a/examples/11_font_family_switching_demo.py b/examples/11_font_family_switching_demo.py new file mode 100644 index 0000000..75e1e1e --- /dev/null +++ b/examples/11_font_family_switching_demo.py @@ -0,0 +1,240 @@ +""" +Demonstration of dynamic font family switching in the ereader. + +This example shows how to: +1. Initialize an ereader with content +2. Dynamically switch between different font families (Sans, Serif, Monospace) +3. Maintain reading position across font changes +4. Use the font family API + +The ereader manager provides a high-level API for changing fonts on-the-fly +without losing your place in the document. +""" + +from pyWebLayout.abstract import Paragraph, Heading, Word +from pyWebLayout.abstract.block import HeadingLevel +from pyWebLayout.style import Font +from pyWebLayout.style.fonts import BundledFont, FontWeight +from pyWebLayout.style.page_style import PageStyle +from pyWebLayout.layout.ereader_manager import create_ereader_manager +from PIL import Image + + +def create_sample_content(): + """Create sample document content with various text styles""" + blocks = [] + + # Create a default font for the content + default_font = Font.from_family(BundledFont.SANS, font_size=16) + heading_font = Font.from_family(BundledFont.SANS, font_size=24, weight=FontWeight.BOLD) + + # Title + title = Heading(level=HeadingLevel.H1, style=heading_font) + for word in "Font Family Switching Demo".split(): + title.add_word(Word(word, heading_font)) + blocks.append(title) + + # Introduction paragraph + intro_font = Font.from_family(BundledFont.SANS, font_size=16) + intro = Paragraph(intro_font) + intro_text = ( + "This demonstration shows how the ereader can dynamically switch between " + "different font families while maintaining your reading position. " + "The three bundled font families (Sans, Serif, and Monospace) can be " + "changed on-the-fly without recreating the document." + ) + for word in intro_text.split(): + intro.add_word(Word(word, intro_font)) + blocks.append(intro) + + # Section 1 + section1_heading = Heading(level=HeadingLevel.H2, style=heading_font) + for word in "Sans-Serif Font".split(): + section1_heading.add_word(Word(word, heading_font)) + blocks.append(section1_heading) + + para1 = Paragraph(default_font) + text1 = ( + "Sans-serif fonts like DejaVu Sans are clean and modern, making them " + "ideal for screen reading. They lack the decorative strokes (serifs) " + "found in traditional typefaces, which can improve legibility on digital displays. " + "Many ereader applications default to sans-serif fonts for this reason." + ) + for word in text1.split(): + para1.add_word(Word(word, default_font)) + blocks.append(para1) + + # Section 2 + section2_heading = Heading(level=HeadingLevel.H2, style=heading_font) + for word in "Serif Font".split(): + section2_heading.add_word(Word(word, heading_font)) + blocks.append(section2_heading) + + para2 = Paragraph(default_font) + text2 = ( + "Serif fonts like DejaVu Serif have small decorative strokes at the ends " + "of letter strokes. These fonts are traditionally used in print media and " + "can give a more formal, classic appearance. Many readers prefer serif fonts " + "for long-form reading as they find them easier on the eyes." + ) + for word in text2.split(): + para2.add_word(Word(word, default_font)) + blocks.append(para2) + + # Section 3 + section3_heading = Heading(level=HeadingLevel.H2, style=heading_font) + for word in "Monospace Font".split(): + section3_heading.add_word(Word(word, heading_font)) + blocks.append(section3_heading) + + para3 = Paragraph(default_font) + text3 = ( + "Monospace fonts like DejaVu Sans Mono have equal spacing between all characters. " + "They are commonly used for displaying code, technical documentation, and typewriter-style " + "text. While less common for general reading, some users prefer the uniform character " + "spacing for certain types of content." + ) + for word in text3.split(): + para3.add_word(Word(word, default_font)) + blocks.append(para3) + + # Final paragraph + conclusion = Paragraph(default_font) + conclusion_text = ( + "The ability to switch fonts dynamically is a key feature of modern ereaders. " + "It allows readers to customize their reading experience based on personal preference, " + "lighting conditions, and content type. Try switching between the three font families " + "to see which one you prefer for different types of reading." + ) + for word in conclusion_text.split(): + conclusion.add_word(Word(word, default_font)) + blocks.append(conclusion) + + return blocks + + +def render_pages_with_different_fonts(manager, output_prefix="demo_11"): + """Render the same page with different font families""" + + print("\nRendering pages with different font families...") + print("=" * 70) + + font_families = [ + (None, "Original (Sans)"), + (BundledFont.SERIF, "Serif"), + (BundledFont.MONOSPACE, "Monospace"), + (BundledFont.SANS, "Sans (explicit)") + ] + + images = [] + + for font_family, name in font_families: + print(f"\nRendering with {name} font...") + + # Switch font family + manager.set_font_family(font_family) + + # Get current page + page = manager.get_current_page() + + # Render to image + image = page.render() + filename = f"{output_prefix}_{name.lower().replace(' ', '_').replace('(', '').replace(')', '')}.png" + image.save(filename) + print(f" Saved: {filename}") + + images.append((name, image)) + + return images + + +def demonstrate_font_switching(): + """Main demonstration function""" + print("\n") + print("=" * 70) + print("Font Family Switching Demonstration") + print("=" * 70) + print() + + # Create sample content + print("Creating sample document...") + blocks = create_sample_content() + print(f" Created {len(blocks)} blocks") + + # Initialize ereader manager + print("\nInitializing ereader manager...") + page_size = (600, 800) + manager = create_ereader_manager( + blocks, + page_size, + document_id="font_switching_demo" + ) + print(f" Page size: {page_size[0]}x{page_size[1]}") + print(f" Initial font family: {manager.get_font_family()}") + + # Render pages with different fonts + images = render_pages_with_different_fonts(manager) + + # Show position info + print("\nPosition information after font switches:") + print(" " + "-" * 66) + pos_info = manager.get_position_info() + print(f" Current position: Block {pos_info['position']['block_index']}, " + f"Word {pos_info['position']['word_index']}") + print(f" Font family: {pos_info['font_family'] or 'Original'}") + print(f" Font scale: {pos_info['font_scale']}") + print(f" Reading progress: {pos_info['progress']:.1%}") + + # Test navigation with font switching + print("\nTesting navigation with font switching...") + print(" " + "-" * 66) + + # Reset to beginning + manager.jump_to_position(manager.current_position.__class__()) + + # Advance a few pages with serif font + manager.set_font_family(BundledFont.SERIF) + print(f" Switched to SERIF font") + + for i in range(3): + next_page = manager.next_page() + if next_page: + print(f" Page {i+2}: Advanced successfully") + + # Switch to monospace + manager.set_font_family(BundledFont.MONOSPACE) + print(f" Switched to MONOSPACE font") + current_page = manager.get_current_page() + print(f" Re-rendered current page with new font") + + # Go back a page + prev_page = manager.previous_page() + if prev_page: + print(f" Navigated back successfully") + + # Cache statistics + print("\nCache statistics:") + print(" " + "-" * 66) + stats = manager.get_cache_stats() + for key, value in stats.items(): + print(f" {key}: {value}") + + print() + print("=" * 70) + print("Demo complete!") + print() + print("Key features demonstrated:") + print(" ✓ Dynamic font family switching (Sans, Serif, Monospace)") + print(" ✓ Position preservation across font changes") + print(" ✓ Automatic cache invalidation on font change") + print(" ✓ Navigation with different fonts") + print(" ✓ Font family info in position tracking") + print() + print("The rendered pages show the same content in different font families.") + print("Notice how the layout adapts while maintaining readability.") + print("=" * 70) + print() + + +if __name__ == "__main__": + demonstrate_font_switching() diff --git a/examples/11_table_text_wrapping_demo.py b/examples/11_table_text_wrapping_demo.py new file mode 100644 index 0000000..0998f20 --- /dev/null +++ b/examples/11_table_text_wrapping_demo.py @@ -0,0 +1,312 @@ +#!/usr/bin/env python3 +""" +Table Text Wrapping Example + +This example demonstrates the line wrapping functionality in table cells: +- Tables with long text that wraps across multiple lines +- Automatic word wrapping within cell boundaries +- Hyphenation support for long words +- Multiple paragraphs per cell +- Comparison of narrow vs. wide columns + +Shows how the Line-based text layout system handles text overflow in tables. +""" + +from pyWebLayout.io.readers.html_extraction import parse_html_string +from pyWebLayout.layout.document_layouter import DocumentLayouter +from pyWebLayout.style.page_style import PageStyle +from pyWebLayout.concrete.table import TableStyle +from pyWebLayout.concrete.page import Page +import sys +from pathlib import Path +from PIL import Image + +# Add pyWebLayout to path +sys.path.insert(0, str(Path(__file__).parent.parent)) + + +def create_narrow_columns_example(): + """Create a table with narrow columns to show aggressive wrapping.""" + print(" - Narrow columns with text wrapping") + + html = """ + + + + + + + + + + + + + + + + + + + + + + + + + +
    FeatureDescriptionBenefits
    Automatic Line WrappingText automatically wraps to fit within the available cell width, creating multiple lines as needed.Improves readability and prevents horizontal overflow in tables.
    Hyphenation SupportLong words are intelligently hyphenated using pyphen library or brute-force splitting when necessary.Handles extraordinarily long words that wouldn't fit on a single line.
    Multi-paragraph CellsEach cell can contain multiple paragraphs or headings, all properly wrapped.Allows rich content within table cells.
    + """ + + return html, "Text Wrapping in Narrow Columns" + + +def create_mixed_content_example(): + """Create a table with both short and long content.""" + print(" - Mixed content lengths") + + html = """ + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Product Comparison
    ProductShort DescriptionDetailed Features
    Widget ProPremiumAdvanced functionality with enterprise-grade reliability, comprehensive warranty coverage, and dedicated customer support available around the clock.
    Widget LiteBasicEssential features for everyday use with straightforward operation and minimal learning curve.
    Widget MaxUltimateEverything from Widget Pro plus additional customization options, API integration capabilities, and advanced analytics dashboard.
    + """ + + return html, "Mixed Short and Long Content" + + +def create_technical_documentation_example(): + """Create a table like technical documentation.""" + print(" - Technical documentation style") + + html = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    API MethodParametersDescriptionReturn Value
    render_table()table, origin, width, draw, styleRenders a table with automatic text wrapping in cells. Uses the Line class for intelligent word placement and hyphenation.Rendered table with calculated height and width properties.
    add_word()word, pretextAttempts to add a word to the current line. If it doesn't fit, tries hyphenation strategies including pyphen and brute-force splitting.Tuple of (success, overflow_text) indicating whether word was added and any remaining text.
    calculate_spacing()text_objects, width, min_spacing, max_spacingDetermines optimal spacing between words to achieve proper justification within the specified constraints.Calculated spacing value and position offset for alignment.
    + """ + + return html, "Technical Documentation Table" + + +def create_news_article_example(): + """Create a table with article-style content.""" + print(" - News article layout") + + html = """ + + + + + + + + + + + + + + + + + + + + + + + + + +
    DateHeadlineSummary
    2024-01-15New Text Wrapping FeaturePyWebLayout now supports automatic line wrapping in table cells, bringing sophisticated text layout capabilities to table rendering. The implementation leverages the existing Line class infrastructure.
    2024-01-10Hyphenation ImprovementsEnhanced hyphenation algorithms now include both dictionary-based pyphen hyphenation and intelligent brute-force splitting for edge cases.
    2024-01-05Performance OptimizationTable rendering performance improved through better caching and reduced font object creation overhead.
    + """ + + return html, "News Article Layout" + + +def render_table_example(html, title, style_variant=0): + """Render a single table example.""" + from pyWebLayout.style import Font + from pyWebLayout.abstract.block import Table + + # Parse HTML + base_font = Font(font_size=12) + blocks = parse_html_string(html, base_font=base_font) + + # Find the table block + table = None + for block in blocks: + if isinstance(block, Table): + table = block + break + + if not table: + print(f" Warning: No table found in {title}") + return None + + # Create page style + page_style = PageStyle( + padding=(20, 20, 20, 20), + background_color=(255, 255, 255) + ) + + # Create page + page_size = (900, 600) + page = Page(size=page_size, style=page_style) + + # Create table style variants + table_styles = [ + # Default style + TableStyle( + border_width=1, + border_color=(0, 0, 0), + cell_padding=(8, 8, 8, 8), + header_bg_color=(240, 240, 240), + cell_bg_color=(255, 255, 255) + ), + # Blue header style + TableStyle( + border_width=2, + border_color=(70, 130, 180), + cell_padding=(10, 10, 10, 10), + header_bg_color=(176, 196, 222), + cell_bg_color=(245, 250, 255) + ), + # Minimal style + TableStyle( + border_width=1, + border_color=(200, 200, 200), + cell_padding=(6, 6, 6, 6), + header_bg_color=(250, 250, 250), + cell_bg_color=(255, 255, 255) + ), + ] + + table_style = table_styles[style_variant % len(table_styles)] + + # Create layouter and render table + layouter = DocumentLayouter(page) + layouter.layout_table(table, style=table_style) + + # Get the rendered canvas + _ = page.draw # Ensure canvas exists + img = page._canvas + + return img + + +def combine_examples(examples): + """Combine multiple example images into one.""" + images = [] + titles = [] + + for html, title in examples: + img = render_table_example(html, title) + if img: + images.append(img) + titles.append(title) + + if not images: + return None + + # Calculate combined image size + max_width = max(img.width for img in images) + total_height = sum(img.height for img in images) + 40 * len(images) # Extra space between images + + # Create combined image + combined = Image.new('RGB', (max_width, total_height), color=(255, 255, 255)) + + # Paste images + y_offset = 20 + for img in images: + combined.paste(img, (0, y_offset)) + y_offset += img.height + 40 + + return combined + + +def main(): + """Run the table text wrapping example.""" + print("\nTable Text Wrapping Example") + print("=" * 50) + + # Create examples + print("\n Creating table examples...") + examples = [ + create_narrow_columns_example(), + create_mixed_content_example(), + create_technical_documentation_example(), + create_news_article_example(), + ] + + print("\n Rendering table examples...") + combined_image = combine_examples(examples) + + if combined_image: + # Save the output + output_dir = Path(__file__).parent.parent / 'docs' / 'images' + output_dir.mkdir(parents=True, exist_ok=True) + output_path = output_dir / 'example_11_table_text_wrapping.png' + + combined_image.save(str(output_path)) + + print("\n✓ Example completed!") + print(f" Output saved to: {output_path}") + print(f" Image size: {combined_image.width}x{combined_image.height} pixels") + print(f" Created {len(examples)} table examples with text wrapping") + else: + print("\n✗ Failed to generate examples") + + +if __name__ == '__main__': + main() diff --git a/examples/11b_simple_table_wrapping.py b/examples/11b_simple_table_wrapping.py new file mode 100644 index 0000000..a3af467 --- /dev/null +++ b/examples/11b_simple_table_wrapping.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +""" +Simple Table Text Wrapping Example + +A minimal example showing text wrapping in table cells. +Perfect for quick testing and demonstration. +""" + +import sys +from pathlib import Path + +# Add pyWebLayout to path +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from pyWebLayout.io.readers.html_extraction import parse_html_string +from pyWebLayout.layout.document_layouter import DocumentLayouter +from pyWebLayout.style.page_style import PageStyle +from pyWebLayout.style import Font +from pyWebLayout.concrete.table import TableStyle +from pyWebLayout.concrete.page import Page +from pyWebLayout.abstract.block import Table + + +def main(): + """Create a simple table with text wrapping.""" + print("\nSimple Table Text Wrapping Example") + print("=" * 50) + + # HTML with a table containing long text + html = """ + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Text Wrapping Demonstration
    Column 1Column 2Column 3
    This is a cell with quite a lot of text that will need to wrap across multiple lines.Short textAnother cell with enough content to demonstrate the automatic line wrapping functionality.
    Cell AThis middle cell contains a paragraph with several words that should wrap nicely within the available space.Cell C
    Words like supercalifragilisticexpialidocious might need hyphenation.Normal textThe wrapping algorithm handles both regular word wrapping and hyphenation seamlessly.
    + """ + + print("\n Parsing HTML and creating table...") + + # Parse HTML + base_font = Font(font_size=12) + blocks = parse_html_string(html, base_font=base_font) + + # Find table + table = None + for block in blocks: + if isinstance(block, Table): + table = block + break + + if not table: + print(" ✗ No table found!") + return + + print(" ✓ Table parsed successfully") + + # Create page + page_style = PageStyle( + padding=(30, 30, 30, 30), + background_color=(255, 255, 255) + ) + page = Page(size=(800, 600), style=page_style) + + # Create table style + table_style = TableStyle( + border_width=2, + border_color=(70, 130, 180), + cell_padding=(10, 10, 10, 10), + header_bg_color=(176, 196, 222), + cell_bg_color=(245, 250, 255) + ) + + print(" Rendering table with text wrapping...") + + # Layout and render + layouter = DocumentLayouter(page) + layouter.layout_table(table, style=table_style) + + # Get rendered image + _ = page.draw + img = page._canvas + + # Save output + output_path = Path(__file__).parent.parent / 'docs' / 'images' / 'example_11b_simple_wrapping.png' + output_path.parent.mkdir(parents=True, exist_ok=True) + img.save(str(output_path)) + + print(f"\n✓ Example completed!") + print(f" Output saved to: {output_path}") + print(f" Image size: {img.width}x{img.height} pixels") + print(f"\n The table demonstrates:") + print(f" • Automatic line wrapping in cells") + print(f" • Proper word spacing and alignment") + print(f" • Hyphenation for very long words") + print(f" • Multi-line text within cell boundaries") + + +if __name__ == '__main__': + main() diff --git a/examples/12_optimized_table_layout_demo.py b/examples/12_optimized_table_layout_demo.py new file mode 100644 index 0000000..8aa19a9 --- /dev/null +++ b/examples/12_optimized_table_layout_demo.py @@ -0,0 +1,254 @@ +#!/usr/bin/env python3 +""" +Demo: Optimized Table Column Width Layout + +This example demonstrates the intelligent table column width optimization: +- Automatic width distribution based on content +- HTML width overrides (fixed column widths) +- Sampling for performance (large tables) +- Comparison: before (equal distribution) vs after (optimized) + +The optimizer: +1. Samples first ~5 rows from each section +2. Measures minimum and preferred widths for each column +3. Distributes available space proportionally +4. Respects HTML width attributes +""" + +from pyWebLayout.concrete.page import Page +from pyWebLayout.concrete.table import TableRenderer, TableStyle +from pyWebLayout.style.page_style import PageStyle +from pyWebLayout.style import Font +from pyWebLayout.abstract.block import Table, TableRow, TableCell, Paragraph +from pyWebLayout.abstract.inline import Word +from PIL import ImageDraw + + +def create_demo_table_1(): + """Create a table with varying content lengths (shows optimization).""" + table = Table() + table.caption = "Example 1: Optimized Width Distribution" + + font = Font(font_size=11) + + # Header + header_row = TableRow() + for text in ["ID", "Name", "Description"]: + cell = TableCell(is_header=True) + para = Paragraph(font) + para.add_word(Word(text, font)) + cell.add_block(para) + header_row.add_cell(cell) + table.add_row(header_row, section="header") + + # Body rows with varying content lengths + data = [ + ("1", "Alice", "Short description"), + ("2", "Bob", "This is a much longer description that demonstrates how the optimizer allocates more space to columns with longer content"), + ("3", "Charlie", "Medium length description here"), + ("4", "Diana", "Another longer description that shows the column width optimization working effectively for content-heavy cells"), + ] + + for row_data in data: + row = TableRow() + for text in row_data: + cell = TableCell() + para = Paragraph(font) + for word in text.split(): + para.add_word(Word(word, font)) + cell.add_block(para) + row.add_cell(cell) + table.add_row(row, section="body") + + return table + + +def create_demo_table_2(): + """Create a table with HTML width overrides.""" + table = Table() + table.caption = "Example 2: Fixed Column Widths (HTML override)" + + font = Font(font_size=11) + + # Header with width attributes + header_row = TableRow() + + # Fixed width column + cell1 = TableCell(is_header=True) + cell1.width = "80px" # HTML width override! + para1 = Paragraph(font) + para1.add_word(Word("ID", font)) + para1.add_word(Word("(Fixed", font)) + para1.add_word(Word("80px)", font)) + cell1.add_block(para1) + header_row.add_cell(cell1) + + # Auto-width columns + for text in ["Name (Auto)", "Description (Auto)"]: + cell = TableCell(is_header=True) + para = Paragraph(font) + for word in text.split(): + para.add_word(Word(word, font)) + cell.add_block(para) + header_row.add_cell(cell) + + table.add_row(header_row, section="header") + + # Body rows + data = [ + ("1", "Alice", "The first two columns adapt to remaining space"), + ("2", "Bob", "ID column stays fixed at 80px width"), + ("3", "Charlie", "Name and Description share the remaining width proportionally"), + ] + + for row_data in data: + row = TableRow() + # First cell also has fixed width + cell = TableCell() + cell.width = "80px" + para = Paragraph(font) + para.add_word(Word(row_data[0], font)) + cell.add_block(para) + row.add_cell(cell) + + # Other cells auto-width + for text in row_data[1:]: + cell = TableCell() + para = Paragraph(font) + for word in text.split(): + para.add_word(Word(word, font)) + cell.add_block(para) + row.add_cell(cell) + table.add_row(row, section="body") + + return table + + +def create_demo_table_3(): + """Create a large table (demonstrates sampling).""" + table = Table() + table.caption = "Example 3: Large Table (uses sampling for performance)" + + font = Font(font_size=10) + + # Header + header_row = TableRow() + for text in ["Index", "Data A", "Data B", "Data C"]: + cell = TableCell(is_header=True) + para = Paragraph(font) + para.add_word(Word(text, font)) + cell.add_block(para) + header_row.add_cell(cell) + table.add_row(header_row, section="header") + + # Many body rows (only first ~5 will be sampled for measurement) + for i in range(50): + row = TableRow() + + # Index + cell = TableCell() + para = Paragraph(font) + para.add_word(Word(str(i + 1), font)) + cell.add_block(para) + row.add_cell(cell) + + # Data columns with varying content + if i % 3 == 0: + data = ["Short", "Medium length", "Longer content here"] + elif i % 3 == 1: + data = ["Medium", "Short", "Also longer content"] + else: + data = ["Longer text", "Short", "Medium"] + + for text in data: + cell = TableCell() + para = Paragraph(font) + for word in text.split(): + para.add_word(Word(word, font)) + cell.add_block(para) + row.add_cell(cell) + + table.add_row(row, section="body") + + return table + + +def main(): + # Create page + page_style = PageStyle( + border_width=1, + padding=(20, 20, 20, 20), + background_color=(255, 255, 255) + ) + page = Page(size=(800, 2200), style=page_style) + + # Get canvas and draw + canvas = page._create_canvas() + page._canvas = canvas + page._draw = ImageDraw.Draw(canvas) + + current_y = 30 + + # Table style + table_style = TableStyle( + border_width=1, + border_color=(100, 100, 100), + cell_padding=(8, 8, 8, 8), + header_bg_color=(220, 230, 240), + cell_bg_color=(255, 255, 255), + alternate_row_color=(248, 248, 248) + ) + + # Render Example 1: Optimized distribution + table1 = create_demo_table_1() + renderer1 = TableRenderer( + table1, + origin=(20, current_y), + available_width=760, + draw=page._draw, + style=table_style, + canvas=canvas + ) + renderer1.render() + current_y += renderer1.height + 40 + + # Render Example 2: Fixed widths + table2 = create_demo_table_2() + renderer2 = TableRenderer( + table2, + origin=(20, current_y), + available_width=760, + draw=page._draw, + style=table_style, + canvas=canvas + ) + renderer2.render() + current_y += renderer2.height + 40 + + # Render Example 3: Large table with sampling + table3 = create_demo_table_3() + renderer3 = TableRenderer( + table3, + origin=(20, current_y), + available_width=760, + draw=page._draw, + style=table_style, + canvas=canvas + ) + renderer3.render() + + # Save + output_path = "docs/images/example_12_optimized_table_layout.png" + canvas.save(output_path) + + print(f"✓ Optimized table layout demo created!") + print(f" Output: {output_path}") + print(f" Image size: {canvas.size}") + print(f"\nExamples demonstrated:") + print(f" 1. Content-aware width distribution") + print(f" 2. HTML width overrides (80px fixed column)") + print(f" 3. Large table with sampling (50 rows, only ~5 measured)") + + +if __name__ == "__main__": + main() diff --git a/examples/13_table_pagination_demo.py b/examples/13_table_pagination_demo.py new file mode 100644 index 0000000..249bf88 --- /dev/null +++ b/examples/13_table_pagination_demo.py @@ -0,0 +1,291 @@ +#!/usr/bin/env python3 +""" +Demo: Table Pagination + +This example demonstrates table pagination when content exceeds page height: +- Large table that spans multiple pages +- Automatic row-level pagination (entire rows move to next page) +- Continuation markers ("continued on next page", "continued from previous page") +- Headers repeated on each page + +The pagination system: +1. Renders rows sequentially until page height limit reached +2. Moves entire row to next page if it doesn't fit +3. Repeats header row on continuation pages +4. Adds visual markers to show table continues +""" + +from pyWebLayout.concrete.page import Page +from pyWebLayout.concrete.table import TableRenderer, TableStyle +from pyWebLayout.style.page_style import PageStyle +from pyWebLayout.style import Font +from pyWebLayout.abstract.block import Table, TableRow, TableCell, Paragraph +from pyWebLayout.abstract.inline import Word +from PIL import Image, ImageDraw + + +def create_large_table(): + """Create a table with many rows that will require pagination.""" + table = Table() + table.caption = "Employee Directory (Paginated)" + + font = Font(font_size=11) + + # Header + header_row = TableRow() + for text in ["ID", "Name", "Department", "Email", "Phone"]: + cell = TableCell(is_header=True) + para = Paragraph(font) + para.add_word(Word(text, font)) + cell.add_block(para) + header_row.add_cell(cell) + table.add_row(header_row, section="header") + + # Many body rows (will span multiple pages) + departments = ["Engineering", "Sales", "Marketing", "HR", "Finance", "Operations", "Support"] + + for i in range(60): + row = TableRow() + + # ID + cell = TableCell() + para = Paragraph(font) + para.add_word(Word(f"EMP{i+1001}", font)) + cell.add_block(para) + row.add_cell(cell) + + # Name + cell = TableCell() + para = Paragraph(font) + names = ["Alice Johnson", "Bob Smith", "Charlie Brown", "Diana Lee", + "Eve Wilson", "Frank Miller", "Grace Davis", "Henry Taylor"] + para.add_word(Word(names[i % len(names)], font)) + cell.add_block(para) + row.add_cell(cell) + + # Department + cell = TableCell() + para = Paragraph(font) + para.add_word(Word(departments[i % len(departments)], font)) + cell.add_block(para) + row.add_cell(cell) + + # Email + cell = TableCell() + para = Paragraph(font) + email = f"{names[i % len(names)].lower().replace(' ', '.')}@company.com" + para.add_word(Word(email, font)) + cell.add_block(para) + row.add_cell(cell) + + # Phone + cell = TableCell() + para = Paragraph(font) + para.add_word(Word(f"+1-555-{(i*17)%1000:04d}", font)) + cell.add_block(para) + row.add_cell(cell) + + table.add_row(row, section="body") + + return table + + +def render_table_with_pagination(table, page_size, max_pages=3): + """ + Render a table across multiple pages. + + Args: + table: The table to render + page_size: Tuple of (width, height) for each page + max_pages: Maximum number of pages to render + + Returns: + List of PIL Images (one per page) + """ + pages = [] + + page_style = PageStyle( + border_width=1, + padding=(20, 20, 20, 20), + background_color=(255, 255, 255) + ) + + table_style = TableStyle( + border_width=1, + border_color=(100, 100, 100), + cell_padding=(6, 8, 6, 8), + header_bg_color=(220, 230, 240), + cell_bg_color=(255, 255, 255), + alternate_row_color=(248, 248, 248) + ) + + # Get all rows + all_rows = list(table.all_rows()) + header_rows = [row for section, row in all_rows if section == "header"] + body_rows = [row for section, row in all_rows if section == "body"] + + # Calculate header height once + temp_page = Page(size=page_size, style=page_style) + temp_canvas = temp_page._create_canvas() + temp_draw = ImageDraw.Draw(temp_canvas) + + # Create temporary table with just header to measure + header_table = Table() + header_table.caption = table.caption + for header_row in header_rows: + header_table.add_row(header_row, section="header") + + header_renderer = TableRenderer( + header_table, + origin=(20, 20), + available_width=page_size[0] - 40, + draw=temp_draw, + style=table_style, + canvas=temp_canvas + ) + header_height = header_renderer.height + + # Available height for body rows + available_body_height = page_size[1] - 60 - header_height # margins + header + + # Paginate body rows + current_page_rows = [] + current_height = 0 + page_num = 0 + + for i, body_row in enumerate(body_rows): + if page_num >= max_pages: + break + + # Estimate row height (simplified - actual would measure each row) + # For this demo, assume ~30px per row + row_height = 35 + + if current_height + row_height > available_body_height and current_page_rows: + # Render current page + page_canvas = render_page( + table, + header_rows, + current_page_rows, + page_size, + page_style, + table_style, + page_num, + is_last=False + ) + pages.append(page_canvas) + + # Start new page + page_num += 1 + current_page_rows = [] + current_height = 0 + + current_page_rows.append(body_row) + current_height += row_height + + # Render final page + if current_page_rows and page_num < max_pages: + page_canvas = render_page( + table, + header_rows, + current_page_rows, + page_size, + page_style, + table_style, + page_num, + is_last=(i == len(body_rows) - 1) + ) + pages.append(page_canvas) + + return pages + + +def render_page(table, header_rows, body_rows, page_size, page_style, table_style, page_num, is_last): + """Render a single page with header and body rows.""" + page = Page(size=page_size, style=page_style) + canvas = page._create_canvas() + page._canvas = canvas + page._draw = ImageDraw.Draw(canvas) + + # Create table for this page + page_table = Table() + if page_num == 0: + page_table.caption = table.caption + else: + page_table.caption = f"{table.caption} (continued)" + + # Add header rows + for header_row in header_rows: + page_table.add_row(header_row, section="header") + + # Add body rows for this page + for body_row in body_rows: + page_table.add_row(body_row, section="body") + + # Render table + renderer = TableRenderer( + page_table, + origin=(20, 20), + available_width=page_size[0] - 40, + draw=page._draw, + style=table_style, + canvas=canvas + ) + renderer.render() + + # Add continuation marker at bottom + if not is_last: + font = Font(font_size=10) + y_pos = page_size[1] - 30 + page._draw.text( + (page_size[0] // 2 - 100, y_pos), + "(continued on next page)", + fill=(100, 100, 100), + font=font.font + ) + + # Add page number + page._draw.text( + (page_size[0] // 2 - 20, page_size[1] - 15), + f"Page {page_num + 1}", + fill=(150, 150, 150), + font=Font(font_size=9).font + ) + + return canvas + + +def main(): + # Create large table + table = create_large_table() + + # Render with pagination (3 pages max for demo) + page_size = (900, 700) + pages = render_table_with_pagination(table, page_size, max_pages=3) + + # Combine pages side-by-side for visualization + total_width = page_size[0] * len(pages) + (len(pages) - 1) * 20 # 20px spacing + combined = Image.new('RGB', (total_width, page_size[1]), (240, 240, 240)) + + x_offset = 0 + for i, page_canvas in enumerate(pages): + combined.paste(page_canvas, (x_offset, 0)) + x_offset += page_size[0] + 20 + + # Save + output_path = "docs/images/example_13_table_pagination.png" + combined.save(output_path) + + print(f"✓ Table pagination demo created!") + print(f" Output: {output_path}") + print(f" Pages rendered: {len(pages)}") + print(f" Image size: {combined.size}") + print(f"\nDemonstrates:") + print(f" - Large table (60 rows) paginated across {len(pages)} pages") + print(f" - Header repeated on each page") + print(f" - Continuation markers") + print(f" - Page numbers") + + +if __name__ == "__main__": + main() diff --git a/examples/14_interactive_table.py b/examples/14_interactive_table.py new file mode 100644 index 0000000..8832eaa --- /dev/null +++ b/examples/14_interactive_table.py @@ -0,0 +1,312 @@ +#!/usr/bin/env python3 +""" +Demo: Working Interactive Table with Buttons + +This example shows a fully working interactive table where buttons are +actually rendered inside table cells and can handle click events. + +This uses a hybrid approach: +1. Tables are rendered normally for structure +2. Buttons are rendered on top at calculated positions +3. Click detection maps coordinates to button callbacks +""" + +from pyWebLayout.concrete.page import Page +from pyWebLayout.concrete.table import TableRenderer, TableStyle +from pyWebLayout.concrete.functional import ButtonText +from pyWebLayout.abstract.functional import Button +from pyWebLayout.style.page_style import PageStyle +from pyWebLayout.style import Font +from pyWebLayout.abstract.block import Table, TableRow, TableCell, Paragraph +from pyWebLayout.abstract.inline import Word +from PIL import Image, ImageDraw +import numpy as np + + +def create_interactive_table(): + """Create a table structure (buttons will be overlaid).""" + table = Table() + table.caption = "User Management with Interactive Buttons" + + font = Font(font_size=11) + + # Header + header_row = TableRow() + for i, text in enumerate(["ID", "Name", "Email", "Actions"]): + cell = TableCell(is_header=True) + # Set width for Actions column + if text == "Actions": + cell.width = "220px" # Enough for 3 buttons + para = Paragraph(font) + para.add_word(Word(text, font)) + cell.add_block(para) + header_row.add_cell(cell) + table.add_row(header_row, section="header") + + # Body rows + users = [ + ("U001", "Alice Johnson", "alice@example.com"), + ("U002", "Bob Smith", "bob@example.com"), + ("U003", "Charlie Brown", "charlie@example.com"), + ] + + for user_id, name, email in users: + row = TableRow() + + # ID + cell = TableCell() + para = Paragraph(font) + para.add_word(Word(user_id, font)) + cell.add_block(para) + row.add_cell(cell) + + # Name + cell = TableCell() + para = Paragraph(font) + para.add_word(Word(name, font)) + cell.add_block(para) + row.add_cell(cell) + + # Email + cell = TableCell() + para = Paragraph(font) + para.add_word(Word(email, font)) + cell.add_block(para) + row.add_cell(cell) + + # Actions - leave empty for buttons to be overlaid + # Set width hint to ensure space for 3 buttons + cell = TableCell() + cell.width = "220px" # Enough for 3 buttons (3 × 65px + padding) + para = Paragraph(font) + para.add_word(Word("", font)) # Empty placeholder + cell.add_block(para) + row.add_cell(cell) + + table.add_row(row, section="body") + + return table + + +def render_buttons_in_table(canvas, draw, table_origin, column_widths, row_heights, users): + """ + Render interactive buttons inside the table cells. + + This calculates the exact position of each button based on the table + layout and renders ButtonText objects at those positions. + + Args: + canvas: PIL Image canvas + draw: PIL ImageDraw object + table_origin: (x, y) position of table top-left + column_widths: List of column widths + row_heights: Dict with 'header', 'body', 'footer' keys + users: User data for button labels + + Returns: + List of (button, bounds) for click detection + """ + button_font = Font(font_size=10) + buttons_with_bounds = [] + + # Calculate Actions column position (column 3, index 3) + actions_col_x = table_origin[0] + sum(column_widths[:3]) + 3 * 2 # +borders + actions_col_width = column_widths[3] + + # Start after caption and header row + # Caption takes 20px + 10px spacing = 30px + caption_height = 30 + header_height = row_heights.get("header", 30) + current_y = table_origin[1] + caption_height + header_height + 2 # +caption +header +border + + for i, (user_id, name, email) in enumerate(users): + row_height = row_heights.get("body", 30) # All body rows have same height + + # Position buttons horizontally in the Actions cell + button_x = actions_col_x + 10 # Padding from cell edge + button_y = current_y + (row_height - 30) // 2 # Center vertically + + # Create buttons for this row + # Note: Button callbacks receive click point as first argument + buttons = [ + ("View", lambda point, uid=user_id: print(f"View {uid}")), + ("Edit", lambda point, uid=user_id: print(f"Edit {uid}")), + ("Delete", lambda point, uid=user_id: print(f"Delete {uid}")) + ] + + for label, callback in buttons: + # Create button + abstract_button = Button(label=label, callback=callback) + button_text = ButtonText( + button=abstract_button, + font=button_font, + draw=draw, + padding=(6, 12, 6, 12) + ) + + # Set position + button_text._origin = np.array([button_x, button_y]) + + # Render button + button_text.render() + + # Store bounds for click detection + button_width = 60 # Approximate + button_height = 25 + bounds = (button_x, button_y, button_x + button_width, button_y + button_height) + buttons_with_bounds.append((abstract_button, bounds)) + + # Move to next button position + button_x += 65 + + # Move to next row + current_y += row_height + 1 # +border + + return buttons_with_bounds + + +def handle_click(click_pos, buttons_with_bounds): + """ + Handle a click event by checking if it's inside any button bounds. + + Args: + click_pos: (x, y) tuple of click position + buttons_with_bounds: List of (button, bounds) tuples + + Returns: + True if a button was clicked, False otherwise + """ + click_x, click_y = click_pos + + for button, (x1, y1, x2, y2) in buttons_with_bounds: + if x1 <= click_x <= x2 and y1 <= click_y <= y2: + # Click is inside this button! + button.execute(click_pos) + return True + + return False + + +def main(): + # Create page + page_size = (900, 500) # Increased height to fit instructions + page_style = PageStyle( + border_width=1, + padding=(20, 20, 20, 20), + background_color=(255, 255, 255) + ) + page = Page(size=page_size, style=page_style) + canvas = page._create_canvas() + page._canvas = canvas + page._draw = ImageDraw.Draw(canvas) + + # Table style + table_style = TableStyle( + border_width=1, + border_color=(100, 100, 100), + cell_padding=(8, 10, 8, 10), + header_bg_color=(220, 230, 240), + cell_bg_color=(255, 255, 255), + alternate_row_color=(248, 248, 248) + ) + + # User data + users = [ + ("U001", "Alice Johnson", "alice@example.com"), + ("U002", "Bob Smith", "bob@example.com"), + ("U003", "Charlie Brown", "charlie@example.com"), + ] + + # Create and render table + table = create_interactive_table() + table_origin = (20, 30) + renderer = TableRenderer( + table, + origin=table_origin, + available_width=860, + draw=page._draw, + style=table_style, + canvas=canvas + ) + renderer.render() + + # Get table dimensions for button positioning + column_widths = renderer._column_widths + row_heights = renderer._row_heights + + # Render interactive buttons on top of table + buttons_with_bounds = render_buttons_in_table( + canvas, page._draw, table_origin, + column_widths, row_heights, users + ) + + # Add instructions (position below the table) + # Calculate actual table height based on rows + header_height = row_heights.get("header", 30) + body_height = row_heights.get("body", 30) * len(users) + actual_table_height = header_height + body_height + (len(users) + 2) * 2 # +borders + + inst_font = Font(font_size=12) + y_offset = table_origin[1] + actual_table_height + 50 + page._draw.text( + (20, y_offset), + "Interactive Table Demo:", + fill=(50, 50, 50), + font=inst_font.font + ) + + note_font = Font(font_size=10) + page._draw.text( + (20, y_offset + 25), + "• Buttons are rendered at calculated positions within table cells", + fill=(80, 80, 80), + font=note_font.font + ) + page._draw.text( + (20, y_offset + 45), + "• Click detection maps coordinates to button callbacks", + fill=(80, 80, 80), + font=note_font.font + ) + page._draw.text( + (20, y_offset + 65), + "• Try simulated clicks below:", + fill=(80, 80, 80), + font=note_font.font + ) + + # Save + output_path = "docs/images/example_14_interactive_table.png" + canvas.save(output_path) + + print(f"✓ Working interactive table demo created!") + print(f" Output: {output_path}") + print(f" Image size: {canvas.size}") + print(f"\nDemonstrating button click detection:") + + # Simulate some clicks to demonstrate functionality + actions_col_x = table_origin[0] + sum(column_widths[:3]) + 6 + caption_height = 30 + header_height = row_heights.get("header", 30) + body_row_height = row_heights.get("body", 30) + + # Calculate first body row position (after caption + header + border) + first_row_y = table_origin[1] + caption_height + header_height + 2 + + test_clicks = [ + (100, 100, "Click outside table"), + (actions_col_x + 10, first_row_y + 15, "View button - Alice"), + (actions_col_x + 75, first_row_y + 15, "Edit button - Alice"), + (actions_col_x + 140, first_row_y + 15, "Delete button - Alice"), + (actions_col_x + 10, first_row_y + body_row_height + 17, "View button - Bob"), + ] + + for x, y, desc in test_clicks: + print(f"\n Click at ({x}, {y}) - {desc}:") + clicked = handle_click((x, y), buttons_with_bounds) + if not clicked: + print(f" No button at this position") + + +if __name__ == "__main__": + main() diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..3927c91 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,310 @@ +# PyWebLayout Examples + +This directory contains example scripts demonstrating the pyWebLayout library. + +## Getting Started Examples + +These examples demonstrate the core rendering capabilities of pyWebLayout: + +### 01. Simple Page Rendering +**`01_simple_page_rendering.py`** - Introduction to the Page system + +```bash +python 01_simple_page_rendering.py +``` + +Demonstrates: +- Creating pages with different styles +- Setting borders, padding, and backgrounds +- Understanding page layout structure +- Basic rendering to images + +![Page Rendering Example](../docs/images/example_01_page_rendering.png) + +### 02. Text and Layout +**`02_text_and_layout.py`** - HTML parsing and text rendering + +```bash +python 02_text_and_layout.py +``` + +Demonstrates: +- Parsing HTML content +- Text alignment options +- Font sizes and styles +- Document structure + +![Text and Layout Example](../docs/images/example_02_text_and_layout.png) + +### 03. Page Layouts +**`03_page_layouts.py`** - Different page configurations + +```bash +python 03_page_layouts.py +``` + +Demonstrates: +- Various page sizes (portrait, landscape, square) +- Different aspect ratios +- Border and padding variations +- Color schemes + +![Page Layouts Example](../docs/images/example_03_page_layouts.png) + +### 04. Table Rendering +**`04_table_rendering.py`** - HTML table rendering with styling + +```bash +python 04_table_rendering.py +``` + +Demonstrates: +- Rendering HTML tables +- Table headers and body rows +- Cell borders and padding +- Caption support +- Custom table styling + +![Table Rendering Example](../docs/images/example_04_table_rendering.png) + +### 05. Tables with Images +**`05_html_table_with_images.py`** - Tables containing images and mixed content + +```bash +python 05_html_table_with_images.py +``` + +Demonstrates: +- Creating tables programmatically +- Adding images to table cells +- Book catalog and product showcase tables +- Mixed content (images and text) in cells +- Using cover images from test data +- HTML table parsing with `` tags + +![Table with Images Example](../docs/images/example_05_html_table_with_images.png) + +### 06. Functional Elements (Interactive) +**`06_functional_elements_demo.py`** - Interactive buttons and forms with callbacks + +```bash +python 06_functional_elements_demo.py +``` + +Demonstrates: +- Creating interactive buttons +- Building forms with multiple field types +- Post-layout callback binding +- CallbackRegistry system for managing interactables +- Accessing application state from callbacks +- Batch callback operations +- Simulating user interactions + +![Functional Elements Example](../docs/images/example_06_functional_elements.png) + +### 07. Button Pressed States (Interactive) +**`07_pressed_state_demo.py`** - Visual feedback for button interactions + +```bash +python 07_pressed_state_demo.py +``` + +Demonstrates: +- Button pressed/released state management +- Visual feedback timing (150ms press duration) +- Automatic interaction handling with `InteractionHandler` +- Manual state management for custom event loops +- Dirty flag system for optimized re-rendering +- State tracking with `InteractionStateManager` + +![Button Pressed State Animation](../docs/images/example_07_button_animation.gif) + +*Animated GIF showing button press sequence: initial → pressed → released* + +--- + +## 🆕 New Examples (2024-11) + +These examples address critical coverage gaps and demonstrate advanced features: + +### 08. Bundled Fonts Showcase +**`08_bundled_fonts_demo.py`** - Demonstration of all bundled fonts + +```bash +python 08_bundled_fonts_demo.py +``` + +Demonstrates: +- DejaVu Sans (Sans-serif) +- DejaVu Serif (Serif) +- DejaVu Sans Mono (Monospace) +- All font variants: Regular, Bold, Italic, Bold Italic + +![Bundled Fonts Example](../docs/images/demo_08_bundled_fonts.png) + +### 08. Pagination with PageBreak ✅ +**`08_pagination_demo.py`** - Multi-page documents with explicit and automatic pagination + +```bash +python 08_pagination_demo.py +``` + +**Test Coverage:** [tests/examples/test_08_pagination_demo.py](../tests/examples/test_08_pagination_demo.py) - 11 tests + +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 +- Combining pages into vertical strips + +**Coverage Impact:** Fills critical gap - PageBreak layouter had NO examples before this! + +![Pagination Example](../docs/images/example_08_pagination_explicit.png) + +### 09. Link Navigation (NEW) ✅ +**`09_link_navigation_demo.py`** - All link types and interactive navigation + +```bash +python 09_link_navigation_demo.py +``` + +**Test Coverage:** [tests/examples/test_09_link_navigation_demo.py](../tests/examples/test_09_link_navigation_demo.py) - 10 tests + +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 +- Mixed text and link paragraphs + +**Coverage Impact:** Comprehensive - All 4 LinkType variations demonstrated! + +![Link Navigation Example](../docs/images/example_09_link_navigation.png) + +### 10. Comprehensive Forms (NEW) ✅ +**`10_forms_demo.py`** - All 14 form field types with validation + +```bash +python 10_forms_demo.py +``` + +**Test Coverage:** [tests/examples/test_10_forms_demo.py](../tests/examples/test_10_forms_demo.py) - 9 tests + +Demonstrates all 14 FormFieldType variations: + +**Text-Based Fields:** +- TEXT, EMAIL, PASSWORD, URL, TEXTAREA + +**Number/Date/Time Fields:** +- NUMBER, DATE, TIME, RANGE, COLOR + +**Selection Fields:** +- CHECKBOX, RADIO, SELECT, HIDDEN + +**Coverage Impact:** Complete - All 14 field types across 4 practical form examples! + +![Comprehensive Forms Example](../docs/images/example_10_forms.png) + +### 11. Table Text Wrapping (NEW) ✅ +**`11_table_text_wrapping_demo.py`** - Automatic line wrapping in table cells + +```bash +python 11_table_text_wrapping_demo.py +``` + +**Simple Version:** `11b_simple_table_wrapping.py` - Quick demonstration + +Demonstrates: +- **Automatic line wrapping** - Text wraps across multiple lines within cells +- **Word hyphenation** - Long words are intelligently hyphenated +- **Narrow columns** - Aggressive wrapping for tight spaces +- **Mixed content** - Both short and long text in the same table +- **Technical documentation** - API reference style tables +- **News layouts** - Article-style table content + +**Implementation:** Uses the Line class from `pyWebLayout.concrete.text` with: +- Word-by-word fitting with intelligent spacing +- Pyphen-based dictionary hyphenation +- Brute-force splitting for edge cases +- Proper baseline alignment and metrics + +![Table Text Wrapping Example](../docs/images/example_11_table_text_wrapping.png) + +--- + +## Running the Examples + +All examples can be run directly from the examples directory: + +```bash +cd examples + +# Getting Started (01-07) +python 01_simple_page_rendering.py # Page layouts +python 02_text_and_layout.py # Text alignment with justified text +python 03_page_layouts.py # Various page sizes +python 04_table_rendering.py # Table styles +python 05_html_table_with_images.py # HTML tables with images +python 06_functional_elements_demo.py # Interactive buttons and forms +python 07_pressed_state_demo.py # Button pressed states (generates GIF) + +# Advanced Features (08-11) +python 08_bundled_fonts_demo.py # Bundled font showcase +python 08_pagination_demo.py # Multi-page documents +python 09_link_navigation_demo.py # All link types +python 10_forms_demo.py # All form field types +python 11_table_text_wrapping_demo.py # Table text wrapping +python 11b_simple_table_wrapping.py # Simple wrapping demo +``` + +Output images are saved to the `docs/images/` directory. + +## Recent Improvements + +### ✅ Justified Text Fix (2024-11-10) +Lines using justified alignment now properly fill the entire width by: +- Calculating base spacing and remainder pixels +- Distributing remainder across word gaps to eliminate short lines +- Removing max_spacing constraint for true justification + +**Affected examples:** 02, 11, 11b - All text now perfectly justified! + +### ✅ Animated Button States (2024-11-10) +Example 07 now automatically generates an animated GIF showing button interactions: +- Initial state (1000ms) +- Pressed state (200ms) +- Released state (500ms) +- Loops continuously + +**Output:** `docs/images/example_07_button_animation.gif` + +### Running Tests + +All new examples (08, 09, 10) include comprehensive test coverage: + +```bash +# Run all example tests +python -m pytest tests/examples/ -v + +# Run specific test file +python -m pytest tests/examples/test_08_pagination_demo.py -v +python -m pytest tests/examples/test_09_link_navigation_demo.py -v +python -m pytest tests/examples/test_10_forms_demo.py -v +``` + +**Total Test Coverage:** 30 tests (11 + 10 + 9), all passing ✅ + +## Additional Documentation + +- `README_HTML_MULTIPAGE.md` - HTML multi-page rendering guide +- `../ARCHITECTURE.md` - Detailed explanation of the Abstract/Concrete architecture +- `../docs/images/README.md` - Visual documentation index with all examples +- `../pyWebLayout/layout/README_EREADER_API.md` - EbookReader API reference + +## Debug/Development Scripts + +Low-level debug and rendering scripts have been moved to the `scripts/` directory. diff --git a/examples/generate_readme_font_demo.py b/examples/generate_readme_font_demo.py new file mode 100644 index 0000000..d0aa9ba --- /dev/null +++ b/examples/generate_readme_font_demo.py @@ -0,0 +1,252 @@ +""" +Generate a demo image for README.md showing font family switching feature. + +Creates a side-by-side comparison of the same content rendered in +Sans, Serif, and Monospace fonts. +""" + +from pyWebLayout.abstract import Paragraph, Heading, Word +from pyWebLayout.abstract.block import HeadingLevel +from pyWebLayout.style import Font +from pyWebLayout.style.fonts import BundledFont, FontWeight +from pyWebLayout.style.page_style import PageStyle +from pyWebLayout.layout.ereader_manager import create_ereader_manager +from PIL import Image, ImageDraw, ImageFont + + +def create_demo_content(): + """Create concise demo content that fits nicely on a small page""" + blocks = [] + + # Title + title_font = Font.from_family(BundledFont.SANS, font_size=28, weight=FontWeight.BOLD) + title = Heading(level=HeadingLevel.H1, style=title_font) + for word in "The Adventure Begins".split(): + title.add_word(Word(word, title_font)) + blocks.append(title) + + # Paragraph + body_font = Font.from_family(BundledFont.SANS, font_size=14) + para = Paragraph(body_font) + text = ( + "In the quiet village of Millbrook, young Emma discovered an ancient map " + "hidden in her grandmother's attic. The parchment revealed a mysterious " + "forest path marked with symbols she had never seen before. With courage " + "in her heart and the map in her pocket, she set out at dawn to uncover " + "the secrets that lay beyond the old oak trees." + ) + for word in text.split(): + para.add_word(Word(word, body_font)) + blocks.append(para) + + return blocks + + +def render_with_font_family(blocks, page_size, font_family, family_name): + """Render a page with a specific font family""" + manager = create_ereader_manager( + blocks, + page_size, + document_id=f"demo_{family_name.lower()}" + ) + + # Set font family (None means original/default) + manager.set_font_family(font_family) + + # Get the first page + page = manager.get_current_page() + return page.render() + + +def create_comparison_image(): + """Create a side-by-side comparison of all three font families""" + + # Page size for each panel + page_width = 400 + page_height = 300 + + # Create demo content + print("Creating demo content...") + blocks = create_demo_content() + + # Render with each font family + print("Rendering with Sans font...") + sans_image = render_with_font_family( + blocks, (page_width, page_height), BundledFont.SANS, "Sans" + ) + + print("Rendering with Serif font...") + serif_image = render_with_font_family( + blocks, (page_width, page_height), BundledFont.SERIF, "Serif" + ) + + print("Rendering with Monospace font...") + mono_image = render_with_font_family( + blocks, (page_width, page_height), BundledFont.MONOSPACE, "Monospace" + ) + + # Create a composite image with all three side by side + spacing = 20 + label_height = 30 + total_width = page_width * 3 + spacing * 4 + total_height = page_height + label_height + spacing * 2 + + composite = Image.new('RGB', (total_width, total_height), color='#f5f5f5') + + # Paste the three images + x_positions = [ + spacing, + spacing * 2 + page_width, + spacing * 3 + page_width * 2 + ] + + for img, x_pos in zip([sans_image, serif_image, mono_image], x_positions): + composite.paste(img, (x_pos, label_height + spacing)) + + # Add labels + draw = ImageDraw.Draw(composite) + + # Try to use a nice font, fallback to default if not available + try: + label_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 20) + except: + label_font = ImageFont.load_default() + + labels = ["Sans-Serif", "Serif", "Monospace"] + for label, x_pos in zip(labels, x_positions): + # Calculate text position to center it + bbox = draw.textbbox((0, 0), label, font=label_font) + text_width = bbox[2] - bbox[0] + text_x = x_pos + (page_width - text_width) // 2 + + draw.text((text_x, 5), label, fill='#333333', font=label_font) + + # Save the image + output_path = "docs/images/font_family_switching.png" + composite.save(output_path, quality=95) + print(f"\n✓ Saved demo image to: {output_path}") + print(f" Image size: {total_width}x{total_height}") + + return output_path + + +def create_single_vertical_comparison(): + """Create a vertical comparison that's better for README""" + + # Page size for each panel + page_width = 700 + page_height = 280 + + # Create demo content + print("\nCreating vertical comparison for README...") + blocks = create_demo_content() + + # Render with each font family + print(" Rendering Sans...") + sans_image = render_with_font_family( + blocks, (page_width, page_height), BundledFont.SANS, "Sans" + ) + + print(" Rendering Serif...") + serif_image = render_with_font_family( + blocks, (page_width, page_height), BundledFont.SERIF, "Serif" + ) + + print(" Rendering Monospace...") + mono_image = render_with_font_family( + blocks, (page_width, page_height), BundledFont.MONOSPACE, "Monospace" + ) + + # Create a composite image stacked vertically + spacing = 15 + label_width = 120 + total_width = page_width + label_width + spacing * 2 + total_height = page_height * 3 + spacing * 4 + + composite = Image.new('RGB', (total_width, total_height), color='#ffffff') + + # Add a subtle border + draw = ImageDraw.Draw(composite) + draw.rectangle([(0, 0), (total_width-1, total_height-1)], outline='#e0e0e0', width=1) + + # Paste the three images vertically + y_positions = [ + spacing, + spacing * 2 + page_height, + spacing * 3 + page_height * 2 + ] + + images_data = [ + (sans_image, "Sans-Serif", "#4A90E2"), + (serif_image, "Serif", "#E94B3C"), + (mono_image, "Monospace", "#50C878") + ] + + # Try to use a nice font + try: + label_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 16) + small_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 11) + except: + label_font = ImageFont.load_default() + small_font = ImageFont.load_default() + + for (img, label, color), y_pos in zip(images_data, y_positions): + # Paste the page image + composite.paste(img, (label_width + spacing, y_pos)) + + # Draw label background + draw.rectangle( + [(spacing, y_pos + 10), (label_width, y_pos + 40)], + fill=color + ) + + # Draw label text + draw.text( + (spacing + 10, y_pos + 17), + label, + fill='#ffffff', + font=label_font + ) + + # Draw font description + descriptions = { + "Sans-Serif": "Clean & Modern", + "Serif": "Classic & Formal", + "Monospace": "Code & Technical" + } + draw.text( + (spacing + 5, y_pos + 50), + descriptions[label], + fill='#666666', + font=small_font + ) + + # Save the image + output_path = "docs/images/font_family_switching_vertical.png" + composite.save(output_path, quality=95) + print(f" ✓ Saved: {output_path}") + print(f" Size: {total_width}x{total_height}") + + return output_path + + +if __name__ == "__main__": + print("=" * 70) + print("Generating README Demo Images") + print("=" * 70) + + # Create both versions + horizontal_path = create_comparison_image() + vertical_path = create_single_vertical_comparison() + + print("\n" + "=" * 70) + print("Demo images generated successfully!") + print("=" * 70) + print(f"\nHorizontal comparison: {horizontal_path}") + print(f"Vertical comparison: {vertical_path}") + print("\nRecommended for README: vertical version") + print("\nMarkdown snippet:") + print("```markdown") + print("![Font Family Switching](docs/images/font_family_switching_vertical.png)") + print("```") + print() diff --git a/pyWebLayout/__init__.py b/pyWebLayout/__init__.py new file mode 100644 index 0000000..345308a --- /dev/null +++ b/pyWebLayout/__init__.py @@ -0,0 +1,22 @@ +""" +PyWebLayout - A Python library for HTML-like layout and rendering. + +This library provides classes for rendering HTML-like content to images +using a box-based layout system. It includes support for text, tables, +and containers, as well as parsers for HTML and EPUB content. It also +supports pagination for ebook-like content with the ability to pause, +save state, and resume rendering. +""" + +__version__ = '0.1.1' + +# Core abstractions + +# Style components + + +# Abstract document model + +# Concrete implementations + +# Abstract components diff --git a/pyWebLayout/abstract/__init__.py b/pyWebLayout/abstract/__init__.py new file mode 100644 index 0000000..adc4694 --- /dev/null +++ b/pyWebLayout/abstract/__init__.py @@ -0,0 +1,22 @@ +""" +Abstract layer for the pyWebLayout library. + +This package contains abstract representations of document elements that are +independent of rendering specifics. +""" + +from .inline import Word, FormattedSpan +from .block import Paragraph, Heading, Image, HeadingLevel +from .document import Document +from .functional import LinkType + +__all__ = [ + 'Word', + 'FormattedSpan', + 'Paragraph', + 'Heading', + 'Image', + 'HeadingLevel', + 'Document', + 'LinkType', +] diff --git a/pyWebLayout/abstract/block.py b/pyWebLayout/abstract/block.py new file mode 100644 index 0000000..51f9684 --- /dev/null +++ b/pyWebLayout/abstract/block.py @@ -0,0 +1,1415 @@ +from typing import List, Iterator, Tuple, Dict, Optional, Any +from enum import Enum +import os +import tempfile +import urllib.request +import urllib.parse +from PIL import Image as PILImage +from .inline import Word, FormattedSpan +from ..core import Hierarchical, Styleable, FontRegistry, ContainerAware, BlockContainer + + +class BlockType(Enum): + """Enumeration of different block types for classification purposes""" + PARAGRAPH = 1 + HEADING = 2 + QUOTE = 3 + CODE_BLOCK = 4 + LIST = 5 + LIST_ITEM = 6 + TABLE = 7 + TABLE_ROW = 8 + TABLE_CELL = 9 + HORIZONTAL_RULE = 10 + LINE_BREAK = 11 + IMAGE = 12 + PAGE_BREAK = 13 + + +class Block(Hierarchical): + """ + Base class for all block-level elements. + Block elements typically represent visual blocks of content that stack vertically. + + Uses Hierarchical mixin for parent-child relationship management. + """ + + def __init__(self, block_type: BlockType): + """ + Initialize a block element. + + Args: + block_type: The type of block this element represents + """ + super().__init__() + self._block_type = block_type + + @property + def block_type(self) -> BlockType: + """Get the type of this block element""" + return self._block_type + + +class Paragraph(Styleable, FontRegistry, ContainerAware, Block): + """ + A paragraph is a block-level element that contains a sequence of words. + + Uses Styleable mixin for style property management. + Uses FontRegistry mixin for font caching with parent delegation. + """ + + def __init__(self, style=None): + """ + Initialize an empty paragraph + + Args: + style: Optional default style for words in this paragraph + """ + super().__init__(style=style, block_type=BlockType.PARAGRAPH) + self._words: List[Word] = [] + self._spans: List[FormattedSpan] = [] + + @classmethod + def create_and_add_to(cls, container, style=None) -> 'Paragraph': + """ + Create a new Paragraph and add it to a container, inheriting style from + the container if not explicitly provided. + + Args: + container: The container to add the paragraph to (must have add_block method and style property) + style: Optional style override. If None, inherits from container + + Returns: + The newly created Paragraph object + + Raises: + AttributeError: If the container doesn't have the required add_block method + """ + # Validate container and inherit style using ContainerAware utilities + cls._validate_container(container) + style = cls._inherit_style(container, style) + + # Create the new paragraph + paragraph = cls(style) + + # Add the paragraph to the container + container.add_block(paragraph) + + return paragraph + + def add_word(self, word: Word): + """ + Add a word to this paragraph. + + Args: + word: The Word object to add + """ + self._words.append(word) + + def create_word(self, text: str, style=None, background=None) -> Word: + """ + Create a new word and add it to this paragraph, inheriting paragraph's style if not specified. + + This is a convenience method that uses Word.create_and_add_to() to create words + that automatically inherit styling from this paragraph. + + Args: + text: The text content of the word + style: Optional Font style override. If None, attempts to inherit from paragraph + background: Optional background color override + + Returns: + The newly created Word object + """ + return Word.create_and_add_to(text, self, style, background) + + def add_span(self, span: FormattedSpan): + """ + Add a formatted span to this paragraph. + + Args: + span: The FormattedSpan object to add + """ + self._spans.append(span) + + def create_span(self, style=None, background=None) -> FormattedSpan: + """ + Create a new formatted span with inherited style. + + Args: + style: Optional Font style override. If None, inherits from paragraph + background: Optional background color override + + Returns: + The newly created FormattedSpan object + """ + return FormattedSpan.create_and_add_to(self, style, background) + + @property + def words(self) -> List[Word]: + """Get the list of words in this paragraph""" + return self._words + + def words_iter(self) -> Iterator[Tuple[int, Word]]: + """ + Iterate over the words in this paragraph. + + Yields: + Tuples of (index, word) for each word in the paragraph + """ + for i, word in enumerate(self._words): + yield i, word + + def spans(self) -> Iterator[FormattedSpan]: + """ + Iterate over the formatted spans in this paragraph. + + Yields: + Each FormattedSpan in the paragraph + """ + for span in self._spans: + yield span + + @property + def word_count(self) -> int: + """Get the number of words in this paragraph""" + return len(self._words) + + def __len__(self): + return self.word_count + + # get_or_create_font() is provided by FontRegistry mixin + + +class HeadingLevel(Enum): + """Enumeration representing HTML heading levels (h1-h6)""" + H1 = 1 + H2 = 2 + H3 = 3 + H4 = 4 + H5 = 5 + H6 = 6 + + +class Heading(Paragraph): + """ + A heading element (h1, h2, h3, etc.) that contains text with a specific heading level. + Headings inherit from Paragraph as they contain words but have additional properties. + """ + + def __init__(self, level: HeadingLevel = HeadingLevel.H1, style=None): + """ + Initialize a heading element. + + Args: + level: The heading level (h1-h6) + style: Optional default style for words in this heading + """ + super().__init__(style) + self._block_type = BlockType.HEADING + self._level = level + + @classmethod + def create_and_add_to( + cls, + container, + level: HeadingLevel = HeadingLevel.H1, + style=None) -> 'Heading': + """ + Create a new Heading and add it to a container, inheriting style from + the container if not explicitly provided. + + Args: + container: The container to add the heading to (must have add_block method and style property) + level: The heading level (h1-h6) + style: Optional style override. If None, inherits from container + + Returns: + The newly created Heading object + + Raises: + AttributeError: If the container doesn't have the required add_block method + """ + # Validate container and inherit style using ContainerAware utilities + cls._validate_container(container) + style = cls._inherit_style(container, style) + + # Create the new heading + heading = cls(level, style) + + # Add the heading to the container + container.add_block(heading) + + return heading + + @property + def level(self) -> HeadingLevel: + """Get the heading level""" + return self._level + + @level.setter + def level(self, level: HeadingLevel): + """Set the heading level""" + self._level = level + + +class Quote(BlockContainer, ContainerAware, Block): + """ + A blockquote element that can contain other block elements. + """ + + def __init__(self, style=None): + """ + Initialize an empty blockquote + + Args: + style: Optional default style for child blocks + """ + super().__init__(BlockType.QUOTE) + self._style = style + + @classmethod + def create_and_add_to(cls, container, style=None) -> 'Quote': + """ + Create a new Quote and add it to a container, inheriting style from + the container if not explicitly provided. + + Args: + container: The container to add the quote to (must have add_block method and style property) + style: Optional style override. If None, inherits from container + + Returns: + The newly created Quote object + + Raises: + AttributeError: If the container doesn't have the required add_block method + """ + # Validate container and inherit style using ContainerAware utilities + cls._validate_container(container) + style = cls._inherit_style(container, style) + + # Create the new quote + quote = cls(style) + + # Add the quote to the container + container.add_block(quote) + + return quote + + @property + def style(self): + """Get the default style for this quote""" + return self._style + + @style.setter + def style(self, style): + """Set the default style for this quote""" + self._style = style + + +class CodeBlock(Block): + """ + A code block element containing pre-formatted text with syntax highlighting. + """ + + def __init__(self, language: str = ""): + """ + Initialize a code block. + + Args: + language: The programming language for syntax highlighting + """ + super().__init__(BlockType.CODE_BLOCK) + self._language = language + self._lines: List[str] = [] + + @classmethod + def create_and_add_to(cls, container, language: str = "") -> 'CodeBlock': + """ + Create a new CodeBlock and add it to a container. + + Args: + container: The container to add the code block to (must have add_block method) + language: The programming language for syntax highlighting + + Returns: + The newly created CodeBlock object + + Raises: + AttributeError: If the container doesn't have the required add_block method + """ + # Create the new code block + code_block = cls(language) + + # Add the code block to the container + if hasattr(container, 'add_block'): + container.add_block(code_block) + else: + raise AttributeError( + f"Container {type(container).__name__} must have an 'add_block' method" + ) + + return code_block + + @property + def language(self) -> str: + """Get the programming language""" + return self._language + + @language.setter + def language(self, language: str): + """Set the programming language""" + self._language = language + + def add_line(self, line: str): + """ + Add a line of code to this code block. + + Args: + line: The line of code to add + """ + self._lines.append(line) + + def lines(self) -> Iterator[Tuple[int, str]]: + """ + Iterate over the lines in this code block. + + Yields: + Tuples of (line_number, line_text) for each line + """ + for i, line in enumerate(self._lines): + yield i, line + + @property + def line_count(self) -> int: + """Get the number of lines in this code block""" + return len(self._lines) + + +class ListStyle(Enum): + """Enumeration of list styles""" + UNORDERED = 1 #