636 lines
21 KiB
Python
636 lines
21 KiB
Python
"""
|
|
HTML generation functions for dreader UI.
|
|
|
|
Generates HTML strings programmatically for library view, reader view,
|
|
and various overlays (settings, TOC, etc.) that can be passed to a HAL
|
|
for rendering.
|
|
"""
|
|
|
|
from pathlib import Path
|
|
from typing import List, Dict, Optional
|
|
import base64
|
|
from io import BytesIO
|
|
|
|
|
|
def generate_library_html(books: List[Dict[str, str]], save_covers_to_disk: bool = False) -> str:
|
|
"""
|
|
Generate HTML for the library view showing all books in a simple table.
|
|
|
|
Args:
|
|
books: List of book dictionaries with keys:
|
|
- title: Book title
|
|
- author: Book author
|
|
- filename: EPUB filename
|
|
- cover_data: Optional base64 encoded cover image
|
|
- cover_path: Optional path to saved cover image (if save_covers_to_disk=True)
|
|
save_covers_to_disk: If True, expect cover_path instead of cover_data
|
|
|
|
Returns:
|
|
Complete HTML string for library view
|
|
"""
|
|
# Build table rows
|
|
rows = []
|
|
|
|
for book in books:
|
|
# Add cover image cell if available
|
|
if save_covers_to_disk and book.get('cover_path'):
|
|
cover_cell = f'<td><img src="{book["cover_path"]}" width="150"/></td>'
|
|
elif book.get('cover_data'):
|
|
cover_cell = f'<td><img src="data:image/png;base64,{book["cover_data"]}" width="150"/></td>'
|
|
else:
|
|
cover_cell = '<td>[No cover]</td>'
|
|
|
|
# Add book info cell
|
|
info_cell = f'<td><b>{book["title"]}</b><br/>{book["author"]}</td>'
|
|
|
|
rows.append(f'<tr>{cover_cell}{info_cell}</tr>')
|
|
|
|
table_html = '\n'.join(rows)
|
|
|
|
return f'''
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Library</title>
|
|
</head>
|
|
<body>
|
|
<h1>My Library</h1>
|
|
<p>{len(books)} books</p>
|
|
<table>
|
|
{table_html}
|
|
</table>
|
|
</body>
|
|
</html>'''
|
|
|
|
|
|
def generate_reader_html(book_title: str, book_author: str, page_image_data: str) -> str:
|
|
"""
|
|
Generate HTML for the reader view with page display.
|
|
|
|
Args:
|
|
book_title: Title of current book
|
|
book_author: Author of current book
|
|
page_image_data: Base64 encoded page image
|
|
|
|
Returns:
|
|
Complete HTML string for reader view (page layer only)
|
|
"""
|
|
html = f'''
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{book_title}</title>
|
|
<style>
|
|
* {{
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}}
|
|
body {{
|
|
font-family: Arial, sans-serif;
|
|
background-color: #2c2c2c;
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100vh;
|
|
}}
|
|
.header {{
|
|
background-color: #1a1a1a;
|
|
color: white;
|
|
padding: 10px 20px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}}
|
|
.book-info {{
|
|
flex: 1;
|
|
}}
|
|
.book-title {{
|
|
font-weight: bold;
|
|
font-size: 16px;
|
|
}}
|
|
.book-author {{
|
|
font-size: 12px;
|
|
color: #aaa;
|
|
}}
|
|
.header-buttons {{
|
|
display: flex;
|
|
gap: 10px;
|
|
}}
|
|
.header-button {{
|
|
background-color: #444;
|
|
color: white;
|
|
border: none;
|
|
padding: 8px 16px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
}}
|
|
.header-button:hover {{
|
|
background-color: #555;
|
|
}}
|
|
.page-container {{
|
|
flex: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 20px;
|
|
}}
|
|
.page-image {{
|
|
max-width: 100%;
|
|
max-height: 100%;
|
|
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
|
|
}}
|
|
.footer {{
|
|
background-color: #1a1a1a;
|
|
color: white;
|
|
padding: 10px 20px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}}
|
|
.nav-button {{
|
|
background-color: #444;
|
|
color: white;
|
|
border: none;
|
|
padding: 10px 20px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 16px;
|
|
}}
|
|
.nav-button:hover {{
|
|
background-color: #555;
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<div class="book-info">
|
|
<div class="book-title">{book_title}</div>
|
|
<div class="book-author">{book_author}</div>
|
|
</div>
|
|
<div class="header-buttons">
|
|
<button class="header-button" id="btn-library">Library</button>
|
|
<button class="header-button" id="btn-toc">Contents</button>
|
|
<button class="header-button" id="btn-settings">Settings</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="page-container">
|
|
<img src="data:image/png;base64,{page_image_data}" alt="Page" class="page-image">
|
|
</div>
|
|
|
|
<div class="footer">
|
|
<button class="nav-button" id="btn-prev">← Previous</button>
|
|
<div id="page-info"></div>
|
|
<button class="nav-button" id="btn-next">Next →</button>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
'''
|
|
return html
|
|
|
|
|
|
def generate_settings_overlay(
|
|
font_scale: float = 1.0,
|
|
line_spacing: int = 5,
|
|
inter_block_spacing: int = 15,
|
|
word_spacing: int = 0,
|
|
page_size: tuple = (800, 1200)
|
|
) -> str:
|
|
"""
|
|
Generate HTML for the settings overlay with current values.
|
|
|
|
Uses simple paragraphs with links, similar to TOC overlay,
|
|
since pyWebLayout doesn't support HTML tables.
|
|
|
|
Args:
|
|
font_scale: Current font scale (e.g., 1.0 = 100%, 1.2 = 120%)
|
|
line_spacing: Current line spacing in pixels
|
|
inter_block_spacing: Current inter-block spacing in pixels
|
|
word_spacing: Current word spacing in pixels
|
|
page_size: Page dimensions (width, height) for sizing the overlay
|
|
|
|
Returns:
|
|
HTML string for settings overlay with clickable controls
|
|
"""
|
|
# Format current values for display
|
|
font_percent = int(font_scale * 100)
|
|
|
|
html = f'''
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Settings</title>
|
|
</head>
|
|
<body style="background-color: white; margin: 0; padding: 25px; font-family: Arial, sans-serif;">
|
|
|
|
<h1 style="color: #000; margin: 0 0 8px 0; font-size: 24px; text-align: center; font-weight: bold;">
|
|
Settings
|
|
</h1>
|
|
|
|
<p style="text-align: center; color: #666; margin: 0 0 15px 0; padding-bottom: 12px;
|
|
border-bottom: 2px solid #ccc; font-size: 13px;">
|
|
Adjust reading preferences
|
|
</p>
|
|
|
|
<div style="margin: 15px 0;">
|
|
<p style="padding: 12px; margin: 5px 0; background-color: #f0f0f0; border-left: 3px solid #007bff;">
|
|
<b>Font Size: {font_percent}%</b>
|
|
</p>
|
|
<p style="margin: 5px 0; background-color: #f0f0f0;">
|
|
<a href="setting:font_decrease" style="text-decoration: none; color: #000; display: block; padding: 12px;">Decrease [ - ]</a>
|
|
</p>
|
|
<p style="margin: 5px 0; background-color: #f0f0f0;">
|
|
<a href="setting:font_increase" style="text-decoration: none; color: #000; display: block; padding: 12px;">Increase [ + ]</a>
|
|
</p>
|
|
|
|
<p style="padding: 12px; margin: 5px 0; background-color: #f0f0f0; border-left: 3px solid #28a745;">
|
|
<b>Line Spacing: {line_spacing}px</b>
|
|
</p>
|
|
<p style="margin: 5px 0; background-color: #f0f0f0;">
|
|
<a href="setting:line_spacing_decrease" style="text-decoration: none; color: #000; display: block; padding: 12px;">Decrease [ - ]</a>
|
|
</p>
|
|
<p style="margin: 5px 0; background-color: #f0f0f0;">
|
|
<a href="setting:line_spacing_increase" style="text-decoration: none; color: #000; display: block; padding: 12px;">Increase [ + ]</a>
|
|
</p>
|
|
|
|
<p style="padding: 12px; margin: 5px 0; background-color: #f0f0f0; border-left: 3px solid #17a2b8;">
|
|
<b>Paragraph Spacing: {inter_block_spacing}px</b>
|
|
</p>
|
|
<p style="margin: 5px 0; background-color: #f0f0f0;">
|
|
<a href="setting:block_spacing_decrease" style="text-decoration: none; color: #000; display: block; padding: 12px;">Decrease [ - ]</a>
|
|
</p>
|
|
<p style="margin: 5px 0; background-color: #f0f0f0;">
|
|
<a href="setting:block_spacing_increase" style="text-decoration: none; color: #000; display: block; padding: 12px;">Increase [ + ]</a>
|
|
</p>
|
|
|
|
<p style="padding: 12px; margin: 5px 0; background-color: #f0f0f0; border-left: 3px solid #ffc107;">
|
|
<b>Word Spacing: {word_spacing}px</b>
|
|
</p>
|
|
<p style="margin: 5px 0; background-color: #f0f0f0;">
|
|
<a href="setting:word_spacing_decrease" style="text-decoration: none; color: #000; display: block; padding: 12px;">Decrease [ - ]</a>
|
|
</p>
|
|
<p style="margin: 5px 0; background-color: #f0f0f0;">
|
|
<a href="setting:word_spacing_increase" style="text-decoration: none; color: #000; display: block; padding: 12px;">Increase [ + ]</a>
|
|
</p>
|
|
</div>
|
|
|
|
<div style="margin: 20px 0;">
|
|
<p style="margin: 5px 0; background-color: #dc3545; text-align: center; border-radius: 5px;">
|
|
<a href="action:back_to_library" style="text-decoration: none; color: white; font-weight: bold; font-size: 14px; display: block; padding: 15px;">◄ Back to Library</a>
|
|
</p>
|
|
</div>
|
|
|
|
<p style="text-align: center; margin: 15px 0 0 0; padding-top: 12px;
|
|
border-top: 2px solid #ccc; color: #888; font-size: 11px;">
|
|
Changes apply in real-time • Tap outside to close
|
|
</p>
|
|
</body>
|
|
</html>
|
|
'''
|
|
return html
|
|
|
|
|
|
def generate_toc_overlay(chapters: List[Dict], page_size: tuple = (800, 1200)) -> str:
|
|
"""
|
|
Generate HTML for the table of contents overlay.
|
|
|
|
Args:
|
|
chapters: List of chapter dictionaries with keys:
|
|
- index: Chapter index
|
|
- title: Chapter title
|
|
page_size: Page dimensions (width, height) for sizing the overlay
|
|
|
|
Returns:
|
|
HTML string for TOC overlay (60% popup with transparent background)
|
|
"""
|
|
# Build chapter list items with clickable links for pyWebLayout query
|
|
chapter_items = []
|
|
for i, chapter in enumerate(chapters):
|
|
title = chapter["title"]
|
|
|
|
# Wrap each row in a paragraph with an inline link
|
|
# For very short titles (I, II), pad the link text to ensure it's clickable
|
|
link_text = f'{i+1}. {title}'
|
|
if len(title) <= 2:
|
|
# Add extra padding spaces inside the link to make it easier to click
|
|
link_text = f'{i+1}. {title} ' # Extra spaces for padding
|
|
|
|
chapter_items.append(
|
|
f'<p style="padding: 12px; margin: 5px 0; background-color: #f0f0f0; '
|
|
f'border-left: 3px solid #000;">'
|
|
f'<a href="chapter:{chapter["index"]}" style="text-decoration: none; color: #000;">'
|
|
f'{link_text}</a></p>'
|
|
)
|
|
|
|
# Render simple white panel - compositing will be done by OverlayManager
|
|
html = f'''
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Table of Contents</title>
|
|
</head>
|
|
<body style="background-color: white; margin: 0; padding: 25px; font-family: Arial, sans-serif;">
|
|
|
|
<h1 style="color: #000; margin: 0 0 8px 0; font-size: 24px; text-align: center; font-weight: bold;">
|
|
Table of Contents
|
|
</h1>
|
|
|
|
<p style="text-align: center; color: #666; margin: 0 0 15px 0; padding-bottom: 12px;
|
|
border-bottom: 2px solid #ccc; font-size: 13px;">
|
|
{len(chapters)} chapters
|
|
</p>
|
|
|
|
<div style="max-height: 600px; overflow-y: auto;">
|
|
{"".join(chapter_items)}
|
|
</div>
|
|
|
|
<p style="text-align: center; margin: 15px 0 0 0; padding-top: 12px;
|
|
border-top: 2px solid #ccc; color: #888; font-size: 11px;">
|
|
Tap a chapter to navigate • Tap outside to close
|
|
</p>
|
|
</body>
|
|
</html>
|
|
'''
|
|
return html
|
|
|
|
|
|
def generate_bookmarks_overlay(bookmarks: List[Dict]) -> str:
|
|
"""
|
|
Generate HTML for the bookmarks overlay.
|
|
|
|
Args:
|
|
bookmarks: List of bookmark dictionaries with keys:
|
|
- name: Bookmark name
|
|
- position: Position info
|
|
|
|
Returns:
|
|
HTML string for bookmarks overlay
|
|
"""
|
|
bookmark_rows = []
|
|
for bookmark in bookmarks:
|
|
bookmark_rows.append(f'''
|
|
<tr class="bookmark-row" data-bookmark-name="{bookmark['name']}">
|
|
<td class="bookmark-cell">
|
|
<div class="bookmark-name">{bookmark['name']}</div>
|
|
<div class="bookmark-position">{bookmark.get('position', '')}</div>
|
|
</td>
|
|
<td class="bookmark-actions">
|
|
<button class="action-button delete-button" data-bookmark="{bookmark['name']}">Delete</button>
|
|
</td>
|
|
</tr>
|
|
''')
|
|
|
|
html = f'''
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Bookmarks</title>
|
|
<style>
|
|
* {{
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}}
|
|
body {{
|
|
font-family: Arial, sans-serif;
|
|
background-color: rgba(0, 0, 0, 0.7);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 100vh;
|
|
}}
|
|
.overlay-panel {{
|
|
background-color: white;
|
|
border-radius: 8px;
|
|
box-shadow: 0 4px 16px rgba(0,0,0,0.3);
|
|
padding: 20px;
|
|
min-width: 500px;
|
|
max-height: 80vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}}
|
|
.overlay-header {{
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 20px;
|
|
padding-bottom: 10px;
|
|
border-bottom: 2px solid #ddd;
|
|
}}
|
|
.overlay-title {{
|
|
font-size: 24px;
|
|
font-weight: bold;
|
|
}}
|
|
.close-button {{
|
|
background-color: #dc3545;
|
|
color: white;
|
|
border: none;
|
|
padding: 8px 16px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
}}
|
|
.close-button:hover {{
|
|
background-color: #c82333;
|
|
}}
|
|
.bookmarks-container {{
|
|
overflow-y: auto;
|
|
flex: 1;
|
|
}}
|
|
.bookmarks-table {{
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}}
|
|
.bookmark-row {{
|
|
cursor: pointer;
|
|
}}
|
|
.bookmark-row:hover {{
|
|
background-color: #f0f0f0;
|
|
}}
|
|
.bookmark-cell {{
|
|
padding: 12px;
|
|
border-bottom: 1px solid #eee;
|
|
}}
|
|
.bookmark-name {{
|
|
font-weight: bold;
|
|
margin-bottom: 4px;
|
|
}}
|
|
.bookmark-position {{
|
|
font-size: 12px;
|
|
color: #666;
|
|
}}
|
|
.bookmark-actions {{
|
|
padding: 12px;
|
|
border-bottom: 1px solid #eee;
|
|
text-align: right;
|
|
width: 100px;
|
|
}}
|
|
.action-button {{
|
|
background-color: #dc3545;
|
|
color: white;
|
|
border: none;
|
|
padding: 6px 12px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
}}
|
|
.action-button:hover {{
|
|
background-color: #c82333;
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="overlay-panel">
|
|
<div class="overlay-header">
|
|
<span class="overlay-title">Bookmarks</span>
|
|
<button class="close-button" id="btn-close">Close</button>
|
|
</div>
|
|
|
|
<div class="bookmarks-container">
|
|
<table class="bookmarks-table">
|
|
{"".join(bookmark_rows)}
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
'''
|
|
return html
|
|
|
|
|
|
def generate_navigation_overlay(
|
|
chapters: List[Dict],
|
|
bookmarks: List[Dict],
|
|
active_tab: str = "contents",
|
|
page_size: tuple = (800, 1200)
|
|
) -> str:
|
|
"""
|
|
Generate HTML for the unified navigation overlay with Contents and Bookmarks tabs.
|
|
|
|
This combines TOC and Bookmarks into a single overlay with tab switching.
|
|
Tabs are clickable links that switch between contents (tab:contents) and bookmarks (tab:bookmarks).
|
|
|
|
Args:
|
|
chapters: List of chapter dictionaries with keys:
|
|
- index: Chapter index
|
|
- title: Chapter title
|
|
bookmarks: List of bookmark dictionaries with keys:
|
|
- name: Bookmark name
|
|
- position: Position info (optional)
|
|
active_tab: Which tab to show ("contents" or "bookmarks")
|
|
page_size: Page dimensions (width, height) for sizing the overlay
|
|
|
|
Returns:
|
|
HTML string for navigation overlay with tab switching
|
|
"""
|
|
# Build chapter list items with clickable links
|
|
chapter_items = []
|
|
for i, chapter in enumerate(chapters):
|
|
title = chapter["title"]
|
|
link_text = f'{i+1}. {title}'
|
|
if len(title) <= 2:
|
|
link_text = f'{i+1}. {title} ' # Extra spaces for padding
|
|
|
|
chapter_items.append(
|
|
f'<p style="margin: 5px 0; background-color: #f0f0f0; border-left: 3px solid #000;">'
|
|
f'<a href="chapter:{chapter["index"]}" style="text-decoration: none; color: #000; display: block; padding: 12px;">'
|
|
f'{link_text}</a></p>'
|
|
)
|
|
|
|
# Build bookmark list items with clickable links
|
|
bookmark_items = []
|
|
for bookmark in bookmarks:
|
|
name = bookmark['name']
|
|
position_text = bookmark.get('position', 'Saved position')
|
|
|
|
bookmark_items.append(
|
|
f'<p style="margin: 5px 0; background-color: #f0f0f0; border-left: 3px solid #000;">'
|
|
f'<a href="bookmark:{name}" style="text-decoration: none; color: #000; display: block; padding: 12px;">'
|
|
f'<span style="font-weight: bold; display: block;">{name}</span>'
|
|
f'<span style="font-size: 11px; color: #666;">{position_text}</span>'
|
|
f'</a></p>'
|
|
)
|
|
|
|
# Determine which content to show
|
|
contents_display = "block" if active_tab == "contents" else "none"
|
|
bookmarks_display = "block" if active_tab == "bookmarks" else "none"
|
|
|
|
# Style active tab
|
|
contents_tab_style = "background-color: #000; color: #fff;" if active_tab == "contents" else "background-color: #f0f0f0; color: #000;"
|
|
bookmarks_tab_style = "background-color: #000; color: #fff;" if active_tab == "bookmarks" else "background-color: #f0f0f0; color: #000;"
|
|
|
|
chapters_html = ''.join(chapter_items) if chapter_items else '<p style="padding: 20px; text-align: center; color: #999;">No chapters available</p>'
|
|
bookmarks_html = ''.join(bookmark_items) if bookmark_items else '<p style="padding: 20px; text-align: center; color: #999;">No bookmarks yet</p>'
|
|
|
|
html = f'''
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Navigation</title>
|
|
</head>
|
|
<body style="background-color: white; margin: 0; padding: 0; font-family: Arial, sans-serif;">
|
|
|
|
<!-- Tab Bar -->
|
|
<div style="display: flex; border-bottom: 2px solid #ccc; background-color: #f8f8f8;">
|
|
<a href="tab:contents"
|
|
style="flex: 1; padding: 15px; text-align: center; font-weight: bold;
|
|
text-decoration: none; border-right: 1px solid #ccc; {contents_tab_style}">
|
|
Contents
|
|
</a>
|
|
<a href="tab:bookmarks"
|
|
style="flex: 1; padding: 15px; text-align: center; font-weight: bold;
|
|
text-decoration: none; {bookmarks_tab_style}">
|
|
Bookmarks
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Contents Tab Content -->
|
|
<div id="contents-tab" style="padding: 25px; display: {contents_display};">
|
|
<h2 style="color: #000; margin: 0 0 15px 0; font-size: 20px; text-align: center;">
|
|
Table of Contents
|
|
</h2>
|
|
<p style="text-align: center; color: #666; margin: 0 0 15px 0; padding-bottom: 12px;
|
|
border-bottom: 2px solid #ccc; font-size: 13px;">
|
|
{len(chapters)} chapters
|
|
</p>
|
|
<div style="overflow-y: auto; max-height: calc(100vh - 200px);">
|
|
{chapters_html}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Bookmarks Tab Content -->
|
|
<div id="bookmarks-tab" style="padding: 25px; display: {bookmarks_display};">
|
|
<h2 style="color: #000; margin: 0 0 15px 0; font-size: 20px; text-align: center;">
|
|
Bookmarks
|
|
</h2>
|
|
<p style="text-align: center; color: #666; margin: 0 0 15px 0; padding-bottom: 12px;
|
|
border-bottom: 2px solid #ccc; font-size: 13px;">
|
|
{len(bookmarks)} saved
|
|
</p>
|
|
<div style="overflow-y: auto; max-height: calc(100vh - 200px);">
|
|
{bookmarks_html}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Close Button (bottom right) -->
|
|
<div style="position: fixed; bottom: 20px; right: 20px;">
|
|
<a href="action:close"
|
|
style="display: inline-block; padding: 12px 24px; background-color: #dc3545;
|
|
color: white; text-decoration: none; border-radius: 4px; font-weight: bold;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.2);">
|
|
Close
|
|
</a>
|
|
</div>
|
|
|
|
</body>
|
|
</html>
|
|
'''
|
|
return html
|