Compare commits
No commits in common. "cc34c79495145c340d48107f577405f9a7ab05e2" and "2b14517344ce6834873e8dc6261a1ed6c8e8437f" have entirely different histories.
cc34c79495
...
2b14517344
27
README.md
27
README.md
@ -119,25 +119,6 @@ The library supports various page layouts and configurations:
|
||||
<em>Buttons, forms, and callback binding</em>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" width="50%">
|
||||
<b>🆕 Pagination & PageBreak</b><br>
|
||||
<img src="docs/images/example_08_pagination_explicit.png" width="300" alt="Pagination"><br>
|
||||
<em>Multi-page documents with explicit and automatic breaks</em>
|
||||
</td>
|
||||
<td align="center" width="50%">
|
||||
<b>🆕 Link Navigation</b><br>
|
||||
<img src="docs/images/example_09_link_navigation.png" width="300" alt="Links"><br>
|
||||
<em>All 4 link types: Internal, External, API, Function</em>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" colspan="2">
|
||||
<b>🆕 Comprehensive Forms</b><br>
|
||||
<img src="docs/images/example_10_forms.png" width="300" alt="Forms"><br>
|
||||
<em>All 14 form field types with validation</em>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## Examples
|
||||
@ -152,20 +133,12 @@ The `examples/` directory contains working demonstrations:
|
||||
- **[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
|
||||
|
||||
### 🆕 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))
|
||||
|
||||
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!** See [NEW_EXAMPLES_AND_TESTS_SUMMARY.md](docs/NEW_EXAMPLES_AND_TESTS_SUMMARY.md) for details.
|
||||
|
||||
See **[examples/README.md](examples/README.md)** for detailed documentation.
|
||||
|
||||
## Documentation
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# pyWebLayout Visual Documentation
|
||||
# EbookReader Animated Demonstrations
|
||||
|
||||
This directory contains visual documentation for pyWebLayout, including animated GIF demonstrations of the EbookReader functionality and static example outputs showcasing various features.
|
||||
This directory contains animated GIF demonstrations of the pyWebLayout EbookReader functionality.
|
||||
|
||||
## Generated GIFs
|
||||
|
||||
@ -85,129 +85,16 @@ You can modify `generate_ereader_gifs.py` to adjust:
|
||||
| `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 [NEW_EXAMPLES_AND_TESTS_SUMMARY.md](../NEW_EXAMPLES_AND_TESTS_SUMMARY.md) for detailed information.
|
||||
|
||||
---
|
||||
|
||||
## Usage in Documentation
|
||||
|
||||
These visual assets are used throughout the pyWebLayout documentation to showcase capabilities.
|
||||
These GIFs are embedded in the main [README.md](../../README.md) to showcase the EbookReader's capabilities to potential users.
|
||||
|
||||
To embed in Markdown:
|
||||
```markdown
|
||||

|
||||

|
||||
```
|
||||
|
||||
To embed in HTML with size control:
|
||||
```html
|
||||
<img src="docs/images/ereader_page_navigation.gif" width="300" alt="Page Navigation">
|
||||
<img src="docs/images/example_08_pagination_explicit.png" width="400" alt="Pagination">
|
||||
```
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 87 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 109 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 59 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 31 KiB |
@ -1,367 +0,0 @@
|
||||
#!/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()
|
||||
@ -1,390 +0,0 @@
|
||||
#!/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()
|
||||
@ -1,374 +0,0 @@
|
||||
#!/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()
|
||||
@ -101,81 +101,6 @@ Demonstrates:
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 🆕 New Examples (2024-11)
|
||||
|
||||
These examples address critical coverage gaps and demonstrate advanced features:
|
||||
|
||||
### 08. Pagination with PageBreak (NEW) ✅
|
||||
**`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!
|
||||
|
||||

|
||||
|
||||
### 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!
|
||||
|
||||

|
||||
|
||||
### 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!
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Advanced Examples
|
||||
|
||||
### HTML Rendering
|
||||
@ -194,46 +119,21 @@ All examples can be run directly from the examples directory:
|
||||
|
||||
```bash
|
||||
cd examples
|
||||
|
||||
# Getting Started
|
||||
python 01_simple_page_rendering.py
|
||||
python 02_text_and_layout.py
|
||||
python 03_page_layouts.py
|
||||
python 04_table_rendering.py
|
||||
python 05_table_with_images.py
|
||||
python 06_functional_elements_demo.py
|
||||
|
||||
# NEW: Advanced Features
|
||||
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
|
||||
```
|
||||
|
||||
Output images are saved to the `docs/images/` directory.
|
||||
|
||||
### 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
|
||||
- `../docs/NEW_EXAMPLES_AND_TESTS_SUMMARY.md` - Detailed summary of new examples (08, 09, 10)
|
||||
- `../ARCHITECTURE.md` - Detailed explanation of the Abstract/Concrete architecture
|
||||
- `../docs/images/` - Rendered example outputs
|
||||
- `../docs/images/README.md` - Visual documentation index
|
||||
|
||||
## Debug/Development Scripts
|
||||
|
||||
|
||||
@ -1,221 +0,0 @@
|
||||
"""
|
||||
Test for pagination example (08_pagination_demo.py).
|
||||
|
||||
This test ensures the pagination example runs correctly and produces expected output.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import importlib.util
|
||||
|
||||
# Add examples to path
|
||||
examples_dir = Path(__file__).parent.parent.parent / "examples"
|
||||
sys.path.insert(0, str(examples_dir))
|
||||
|
||||
# Load the demo module
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"pagination_demo",
|
||||
examples_dir / "08_pagination_demo.py"
|
||||
)
|
||||
demo_module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(demo_module)
|
||||
|
||||
|
||||
def test_pagination_demo_imports():
|
||||
"""Test that the pagination demo can be imported without errors."""
|
||||
assert demo_module is not None
|
||||
|
||||
|
||||
def test_create_sample_paragraph():
|
||||
"""Test creating a sample paragraph."""
|
||||
create_sample_paragraph = demo_module.create_sample_paragraph
|
||||
from pyWebLayout.abstract.block import Paragraph
|
||||
|
||||
para = create_sample_paragraph("Hello world test")
|
||||
assert isinstance(para, Paragraph)
|
||||
assert len(para.words) == 3
|
||||
assert para.words[0].text == "Hello"
|
||||
assert para.words[1].text == "world"
|
||||
assert para.words[2].text == "test"
|
||||
|
||||
|
||||
def test_create_title_paragraph():
|
||||
"""Test creating a title paragraph."""
|
||||
create_title_paragraph = demo_module.create_title_paragraph
|
||||
from pyWebLayout.abstract.block import Paragraph
|
||||
|
||||
title = create_title_paragraph("Test Title")
|
||||
assert isinstance(title, Paragraph)
|
||||
assert len(title.words) == 2
|
||||
# Title should have larger font
|
||||
assert title.style.font_size == 24
|
||||
|
||||
|
||||
def test_create_heading_paragraph():
|
||||
"""Test creating a heading paragraph."""
|
||||
create_heading_paragraph = demo_module.create_heading_paragraph
|
||||
from pyWebLayout.abstract.block import Paragraph
|
||||
|
||||
heading = create_heading_paragraph("Test Heading")
|
||||
assert isinstance(heading, Paragraph)
|
||||
assert len(heading.words) == 2
|
||||
# Heading should have medium font
|
||||
assert heading.style.font_size == 18
|
||||
|
||||
|
||||
def test_create_placeholder_image():
|
||||
"""Test creating a placeholder image."""
|
||||
create_placeholder_image = demo_module.create_placeholder_image
|
||||
from pyWebLayout.abstract.block import Image as AbstractImage
|
||||
|
||||
img = create_placeholder_image(200, 150, "Test Image")
|
||||
assert isinstance(img, AbstractImage)
|
||||
assert img.source.size == (200, 150)
|
||||
|
||||
|
||||
def test_pagebreak_layouter_integration():
|
||||
"""Test that PageBreak properly forces new pages."""
|
||||
create_sample_paragraph = demo_module.create_sample_paragraph
|
||||
from pyWebLayout.abstract.block import PageBreak
|
||||
from pyWebLayout.concrete.page import Page
|
||||
from pyWebLayout.style.page_style import PageStyle
|
||||
from pyWebLayout.layout.document_layouter import DocumentLayouter
|
||||
|
||||
# Create a small page
|
||||
page_style = PageStyle(padding=(10, 10, 10, 10))
|
||||
page1 = Page(size=(200, 200), style=page_style)
|
||||
layouter1 = DocumentLayouter(page1)
|
||||
|
||||
# Add some content
|
||||
para1 = create_sample_paragraph("First paragraph")
|
||||
success, _, _ = layouter1.layout_paragraph(para1)
|
||||
assert success
|
||||
|
||||
# Record y offset before pagebreak
|
||||
y_before_break = page1._current_y_offset
|
||||
|
||||
# Simulating PageBreak by creating new page
|
||||
# (PageBreak doesn't get laid out - it signals page creation)
|
||||
page2 = Page(size=(200, 200), style=page_style)
|
||||
layouter2 = DocumentLayouter(page2)
|
||||
|
||||
# Verify new page starts at initial offset
|
||||
initial_offset = page2._current_y_offset
|
||||
|
||||
# Add content to new page
|
||||
para2 = create_sample_paragraph("Second paragraph")
|
||||
success, _, _ = layouter2.layout_paragraph(para2)
|
||||
assert success
|
||||
|
||||
# New page should start fresh (at border_size + padding)
|
||||
# Both pages should have same initial offset
|
||||
assert initial_offset == page1.border_size + page_style.padding_top
|
||||
|
||||
|
||||
def test_example_document_with_pagebreaks():
|
||||
"""Test creating the main example document with pagebreaks."""
|
||||
create_example_document_with_pagebreaks = demo_module.create_example_document_with_pagebreaks
|
||||
|
||||
pages = create_example_document_with_pagebreaks()
|
||||
|
||||
# Should create multiple pages
|
||||
assert len(pages) > 1
|
||||
# Should have created 5 pages (based on PageBreak placements)
|
||||
assert len(pages) == 5
|
||||
|
||||
# Each page should be valid
|
||||
for page in pages:
|
||||
assert page.size == (600, 800)
|
||||
# Page should have some content or be a clean break page
|
||||
assert page._current_y_offset >= page.border_size
|
||||
|
||||
|
||||
def test_auto_pagination_example():
|
||||
"""Test auto-pagination without explicit PageBreaks."""
|
||||
create_auto_pagination_example = demo_module.create_auto_pagination_example
|
||||
|
||||
pages = create_auto_pagination_example()
|
||||
|
||||
# Should create multiple pages due to content overflow
|
||||
assert len(pages) >= 1
|
||||
|
||||
# Each page should be valid
|
||||
for page in pages:
|
||||
assert page.size == (500, 600)
|
||||
|
||||
|
||||
def test_add_page_numbers():
|
||||
"""Test adding page numbers to rendered pages."""
|
||||
add_page_numbers = demo_module.add_page_numbers
|
||||
from pyWebLayout.concrete.page import Page
|
||||
from pyWebLayout.style.page_style import PageStyle
|
||||
|
||||
# Create a few simple pages
|
||||
page_style = PageStyle()
|
||||
pages = [
|
||||
Page(size=(200, 200), style=page_style),
|
||||
Page(size=(200, 200), style=page_style),
|
||||
Page(size=(200, 200), style=page_style)
|
||||
]
|
||||
|
||||
# Add page numbers
|
||||
numbered = add_page_numbers(pages, start_number=1)
|
||||
|
||||
# Should return same number of pages
|
||||
assert len(numbered) == 3
|
||||
|
||||
# Each should be a rendered image
|
||||
from PIL import Image
|
||||
for img in numbered:
|
||||
assert isinstance(img, Image.Image)
|
||||
assert img.size == (200, 200)
|
||||
|
||||
|
||||
def test_combine_pages_vertically():
|
||||
"""Test combining pages into vertical strip."""
|
||||
combine_pages_vertically = demo_module.combine_pages_vertically
|
||||
from pyWebLayout.concrete.page import Page
|
||||
from pyWebLayout.style.page_style import PageStyle
|
||||
|
||||
# Create a few pages
|
||||
page_style = PageStyle()
|
||||
pages = [
|
||||
Page(size=(200, 200), style=page_style).render(),
|
||||
Page(size=(200, 200), style=page_style).render()
|
||||
]
|
||||
|
||||
combined = combine_pages_vertically(pages, title="Test Title")
|
||||
|
||||
# Should create a combined image
|
||||
from PIL import Image
|
||||
assert isinstance(combined, Image.Image)
|
||||
|
||||
# Should be taller than individual pages
|
||||
assert combined.size[1] > 200
|
||||
|
||||
|
||||
def test_main_function():
|
||||
"""Test that the main function runs without errors."""
|
||||
main = demo_module.main
|
||||
|
||||
# Run main (will create output files)
|
||||
result = main()
|
||||
|
||||
# Should return two combined images
|
||||
assert result is not None
|
||||
assert len(result) == 2
|
||||
|
||||
# Both should be PIL images
|
||||
from PIL import Image
|
||||
assert isinstance(result[0], Image.Image)
|
||||
assert isinstance(result[1], Image.Image)
|
||||
|
||||
# Output files should exist
|
||||
output_dir = Path("docs/images")
|
||||
assert (output_dir / "example_08_pagination_explicit.png").exists()
|
||||
assert (output_dir / "example_08_pagination_auto.png").exists()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
@ -1,179 +0,0 @@
|
||||
"""
|
||||
Test for link navigation example (09_link_navigation_demo.py).
|
||||
|
||||
This test ensures the link navigation example runs correctly and produces expected output.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import importlib.util
|
||||
|
||||
# Add examples to path
|
||||
examples_dir = Path(__file__).parent.parent.parent / "examples"
|
||||
sys.path.insert(0, str(examples_dir))
|
||||
|
||||
# Load the demo module
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"link_navigation_demo",
|
||||
examples_dir / "09_link_navigation_demo.py"
|
||||
)
|
||||
demo_module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(demo_module)
|
||||
|
||||
|
||||
def test_link_demo_imports():
|
||||
"""Test that the link demo can be imported without errors."""
|
||||
assert demo_module is not None
|
||||
|
||||
|
||||
def test_create_paragraph_with_links():
|
||||
"""Test creating a paragraph with mixed text and links."""
|
||||
create_paragraph_with_links = demo_module.create_paragraph_with_links
|
||||
from pyWebLayout.abstract.block import Paragraph
|
||||
from pyWebLayout.abstract.inline import LinkedWord, Word
|
||||
from pyWebLayout.abstract.functional import LinkType
|
||||
|
||||
text_parts = [
|
||||
('text', "Click"),
|
||||
('link', "here", "https://example.com", LinkType.EXTERNAL, "test_link"),
|
||||
('text', "to visit"),
|
||||
]
|
||||
|
||||
para = create_paragraph_with_links(text_parts)
|
||||
assert isinstance(para, Paragraph)
|
||||
assert len(para.words) == 4 # "Click", "here" (linked), "to", "visit"
|
||||
|
||||
# Check that the second word is a LinkedWord
|
||||
assert isinstance(para.words[1], LinkedWord)
|
||||
assert para.words[1].text == "here"
|
||||
assert para.words[1].location == "https://example.com"
|
||||
assert para.words[1]._link_type == LinkType.EXTERNAL
|
||||
|
||||
|
||||
def test_link_callback_function():
|
||||
"""Test that link callbacks work correctly."""
|
||||
link_callback = demo_module.link_callback
|
||||
demo_module.link_clicks = []
|
||||
|
||||
callback = link_callback("test_id")
|
||||
callback()
|
||||
|
||||
assert "test_id" in demo_module.link_clicks
|
||||
|
||||
|
||||
def test_create_example_1_internal_links():
|
||||
"""Test creating internal links example."""
|
||||
create_example_1 = demo_module.create_example_1_internal_links
|
||||
from pyWebLayout.concrete.page import Page
|
||||
|
||||
page = create_example_1()
|
||||
assert isinstance(page, Page)
|
||||
assert page.size == (500, 600)
|
||||
|
||||
|
||||
def test_create_example_2_external_links():
|
||||
"""Test creating external links example."""
|
||||
create_example_2 = demo_module.create_example_2_external_links
|
||||
from pyWebLayout.concrete.page import Page
|
||||
|
||||
page = create_example_2()
|
||||
assert isinstance(page, Page)
|
||||
assert page.size == (500, 600)
|
||||
|
||||
|
||||
def test_create_example_3_api_links():
|
||||
"""Test creating API links example."""
|
||||
create_example_3 = demo_module.create_example_3_api_links
|
||||
from pyWebLayout.concrete.page import Page
|
||||
|
||||
page = create_example_3()
|
||||
assert isinstance(page, Page)
|
||||
assert page.size == (500, 600)
|
||||
|
||||
|
||||
def test_create_example_4_function_links():
|
||||
"""Test creating function links example."""
|
||||
create_example_4 = demo_module.create_example_4_function_links
|
||||
from pyWebLayout.concrete.page import Page
|
||||
|
||||
page = create_example_4()
|
||||
assert isinstance(page, Page)
|
||||
assert page.size == (500, 600)
|
||||
|
||||
|
||||
def test_different_link_types():
|
||||
"""Test that different link types are created correctly."""
|
||||
create_paragraph_with_links = demo_module.create_paragraph_with_links
|
||||
from pyWebLayout.abstract.functional import LinkType
|
||||
|
||||
# Test each link type
|
||||
link_types = [
|
||||
(LinkType.INTERNAL, "#section1"),
|
||||
(LinkType.EXTERNAL, "https://example.com"),
|
||||
(LinkType.API, "/api/action"),
|
||||
(LinkType.FUNCTION, "function()"),
|
||||
]
|
||||
|
||||
for link_type, location in link_types:
|
||||
para = create_paragraph_with_links([
|
||||
('link', "test", location, link_type, f"test_{link_type.name}")
|
||||
])
|
||||
assert len(para.words) == 1
|
||||
assert para.words[0]._link_type == link_type
|
||||
assert para.words[0].location == location
|
||||
|
||||
|
||||
def test_combine_pages_into_grid():
|
||||
"""Test combining pages into grid."""
|
||||
combine_pages_into_grid = demo_module.combine_pages_into_grid
|
||||
from pyWebLayout.concrete.page import Page
|
||||
from pyWebLayout.style.page_style import PageStyle
|
||||
|
||||
# Create a few pages
|
||||
page_style = PageStyle()
|
||||
pages = [
|
||||
Page(size=(200, 200), style=page_style),
|
||||
Page(size=(200, 200), style=page_style),
|
||||
Page(size=(200, 200), style=page_style),
|
||||
Page(size=(200, 200), style=page_style)
|
||||
]
|
||||
|
||||
combined = combine_pages_into_grid(pages, "Test Title")
|
||||
|
||||
# Should create a combined image
|
||||
from PIL import Image
|
||||
assert isinstance(combined, Image.Image)
|
||||
|
||||
# Should be larger than individual pages
|
||||
assert combined.size[0] > 200
|
||||
assert combined.size[1] > 200
|
||||
|
||||
|
||||
def test_main_function():
|
||||
"""Test that the main function runs without errors."""
|
||||
main = demo_module.main
|
||||
|
||||
# Run main (will create output files)
|
||||
result = main()
|
||||
|
||||
# Should return combined image and link clicks
|
||||
assert result is not None
|
||||
assert len(result) == 2
|
||||
|
||||
combined_image, link_clicks = result
|
||||
|
||||
# Should be a PIL image
|
||||
from PIL import Image
|
||||
assert isinstance(combined_image, Image.Image)
|
||||
|
||||
# Link clicks should be a list (may be empty since we're not actually clicking)
|
||||
assert isinstance(link_clicks, list)
|
||||
|
||||
# Output file should exist
|
||||
output_dir = Path("docs/images")
|
||||
assert (output_dir / "example_09_link_navigation.png").exists()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
@ -1,159 +0,0 @@
|
||||
"""
|
||||
Test for forms example (10_forms_demo.py).
|
||||
|
||||
This test ensures the forms example runs correctly and produces expected output.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import importlib.util
|
||||
|
||||
# Add examples to path
|
||||
examples_dir = Path(__file__).parent.parent.parent / "examples"
|
||||
sys.path.insert(0, str(examples_dir))
|
||||
|
||||
# Load the demo module
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"forms_demo",
|
||||
examples_dir / "10_forms_demo.py"
|
||||
)
|
||||
demo_module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(demo_module)
|
||||
|
||||
|
||||
def test_forms_demo_imports():
|
||||
"""Test that the forms demo can be imported without errors."""
|
||||
assert demo_module is not None
|
||||
|
||||
|
||||
def test_form_submit_callback():
|
||||
"""Test that form submit callbacks work correctly."""
|
||||
form_submit_callback = demo_module.form_submit_callback
|
||||
demo_module.form_submissions = []
|
||||
|
||||
callback = form_submit_callback("test_form")
|
||||
callback({"field1": "value1"})
|
||||
|
||||
assert len(demo_module.form_submissions) == 1
|
||||
assert demo_module.form_submissions[0][0] == "test_form"
|
||||
assert demo_module.form_submissions[0][1] == {"field1": "value1"}
|
||||
|
||||
|
||||
def test_create_example_1_text_fields():
|
||||
"""Test creating text fields example."""
|
||||
create_example_1 = demo_module.create_example_1_text_fields
|
||||
from pyWebLayout.concrete.page import Page
|
||||
|
||||
page = create_example_1()
|
||||
assert isinstance(page, Page)
|
||||
assert page.size == (500, 600)
|
||||
|
||||
|
||||
def test_create_example_2_number_fields():
|
||||
"""Test creating number fields example."""
|
||||
create_example_2 = demo_module.create_example_2_number_fields
|
||||
from pyWebLayout.concrete.page import Page
|
||||
|
||||
page = create_example_2()
|
||||
assert isinstance(page, Page)
|
||||
assert page.size == (500, 600)
|
||||
|
||||
|
||||
def test_create_example_3_selection_fields():
|
||||
"""Test creating selection fields example."""
|
||||
create_example_3 = demo_module.create_example_3_selection_fields
|
||||
from pyWebLayout.concrete.page import Page
|
||||
|
||||
page = create_example_3()
|
||||
assert isinstance(page, Page)
|
||||
assert page.size == (500, 600)
|
||||
|
||||
|
||||
def test_create_example_4_complete_form():
|
||||
"""Test creating complete form example."""
|
||||
create_example_4 = demo_module.create_example_4_complete_form
|
||||
from pyWebLayout.concrete.page import Page
|
||||
|
||||
page = create_example_4()
|
||||
assert isinstance(page, Page)
|
||||
assert page.size == (500, 700)
|
||||
|
||||
|
||||
def test_all_form_field_types():
|
||||
"""Test that all FormFieldType values are demonstrated."""
|
||||
from pyWebLayout.abstract.functional import FormFieldType
|
||||
|
||||
# All field types that should be in examples
|
||||
expected_types = {
|
||||
FormFieldType.TEXT,
|
||||
FormFieldType.PASSWORD,
|
||||
FormFieldType.EMAIL,
|
||||
FormFieldType.URL,
|
||||
FormFieldType.TEXTAREA,
|
||||
FormFieldType.NUMBER,
|
||||
FormFieldType.DATE,
|
||||
FormFieldType.TIME,
|
||||
FormFieldType.RANGE,
|
||||
FormFieldType.COLOR,
|
||||
FormFieldType.CHECKBOX,
|
||||
FormFieldType.RADIO,
|
||||
FormFieldType.SELECT,
|
||||
FormFieldType.HIDDEN
|
||||
}
|
||||
|
||||
# This test verifies that the example includes all major field types
|
||||
# (the actual verification would happen by inspecting the forms,
|
||||
# but this confirms the enum exists)
|
||||
assert len(expected_types) == 14
|
||||
|
||||
|
||||
def test_combine_pages_into_grid():
|
||||
"""Test combining pages into grid."""
|
||||
combine_pages_into_grid = demo_module.combine_pages_into_grid
|
||||
from pyWebLayout.concrete.page import Page
|
||||
from pyWebLayout.style.page_style import PageStyle
|
||||
|
||||
# Create a few pages
|
||||
page_style = PageStyle()
|
||||
pages = [
|
||||
Page(size=(200, 200), style=page_style),
|
||||
Page(size=(200, 200), style=page_style),
|
||||
Page(size=(200, 200), style=page_style),
|
||||
Page(size=(200, 200), style=page_style)
|
||||
]
|
||||
|
||||
combined = combine_pages_into_grid(pages, "Test Title")
|
||||
|
||||
# Should create a combined image
|
||||
from PIL import Image
|
||||
assert isinstance(combined, Image.Image)
|
||||
|
||||
|
||||
def test_main_function():
|
||||
"""Test that the main function runs without errors."""
|
||||
main = demo_module.main
|
||||
|
||||
# Run main (will create output files)
|
||||
result = main()
|
||||
|
||||
# Should return combined image and form submissions
|
||||
assert result is not None
|
||||
assert len(result) == 2
|
||||
|
||||
combined_image, form_submissions = result
|
||||
|
||||
# Should be a PIL image
|
||||
from PIL import Image
|
||||
assert isinstance(combined_image, Image.Image)
|
||||
|
||||
# Form submissions should be a list
|
||||
assert isinstance(form_submissions, list)
|
||||
|
||||
# Output file should exist
|
||||
output_dir = Path("docs/images")
|
||||
assert (output_dir / "example_10_forms.png").exists()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
Loading…
x
Reference in New Issue
Block a user