tts-html-utils 0.5.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tts_html_utils/__init__.py +0 -0
- tts_html_utils/core/__init__.py +0 -0
- tts_html_utils/core/compiler.py +215 -0
- tts_html_utils/core/components/__init__.py +6 -0
- tts_html_utils/core/components/base.py +300 -0
- tts_html_utils/core/components/favicon.py +17 -0
- tts_html_utils/core/components/flexbox.py +133 -0
- tts_html_utils/core/components/list.py +168 -0
- tts_html_utils/core/components/misc.py +237 -0
- tts_html_utils/core/components/navigation.py +55 -0
- tts_html_utils/core/components/plot.py +166 -0
- tts_html_utils/core/components/structure.py +159 -0
- tts_html_utils/core/components/table.py +392 -0
- tts_html_utils/core/components/text.py +105 -0
- tts_html_utils/css/gantt_chart/gantt_chart.css +330 -0
- tts_html_utils/css/pane_container/pane_container.css +8 -0
- tts_html_utils/django/__init__.py +0 -0
- tts_html_utils/django/forms/__init__.py +0 -0
- tts_html_utils/django/forms/time_range_form.py +56 -0
- tts_html_utils/django/tables/__init__.py +0 -0
- tts_html_utils/django/tables/filter_table.py +201 -0
- tts_html_utils/django/templates/filter_table/filter_table.html +39 -0
- tts_html_utils/django/templates/filter_table/filter_table_rows.html +22 -0
- tts_html_utils/django/templates/filter_table/javascript.html +108 -0
- tts_html_utils/django/templates/filter_table/pagination.html +18 -0
- tts_html_utils/gantt_chart/gantt_chart.py +167 -0
- tts_html_utils/javascript/filter_table/django_html_filter_table.js +85 -0
- tts_html_utils/javascript/filter_table/django_html_table_sort.js +78 -0
- tts_html_utils/javascript/filter_table/show_hide_details.js +24 -0
- tts_html_utils/javascript/filter_table/static_html_filter_table.js +72 -0
- tts_html_utils/javascript/filter_table/static_html_table_sort.js +122 -0
- tts_html_utils/javascript/gantt_chart/gantt_chart.js +908 -0
- tts_html_utils/javascript/pane_container/pane_container.js +141 -0
- tts_html_utils/javascript/pane_container.js +43 -0
- tts_html_utils/javascript/plotlyv1.58.5.js +62 -0
- tts_html_utils/javascript/visdiff_tables/hide_columns_without_mismatch.js +35 -0
- tts_html_utils/javascript/visdiff_tables/row_height_match.js +55 -0
- tts_html_utils/javascript/visdiff_tables/scroll_sync.js +29 -0
- tts_html_utils/jupyter/__init__.py +0 -0
- tts_html_utils/jupyter/grid.py +465 -0
- tts_html_utils/resource.py +225 -0
- tts_html_utils/resources/css/navbar.css +39 -0
- tts_html_utils/resources/html_templates/full_report.html +56 -0
- tts_html_utils/resources/html_templates/generic.html +2 -0
- tts_html_utils/resources/html_templates/generic_singleton.html +2 -0
- tts_html_utils/resources/html_templates/helpers.html +31 -0
- tts_html_utils/resources/html_templates/plotly_config.html +6 -0
- tts_html_utils/resources/html_templates/script_general.html +3 -0
- tts_html_utils/resources/html_templates/table.html +21 -0
- tts_html_utils/resources/html_templates/test.html +4 -0
- tts_html_utils/resources/img/favicon.png +0 -0
- tts_html_utils/resources/report_base.css +474 -0
- tts_html_utils/resources/rule_result_table.css +40 -0
- tts_html_utils/resources/rule_summary.css +79 -0
- tts_html_utils/resources/scripts/collapsible.js +40 -0
- tts_html_utils/resources/scripts/link_functions.js +13 -0
- tts_html_utils/resources/scripts/open_tab_function.js +29 -0
- tts_html_utils/resources/scripts/resize_bodyheight.js +7 -0
- tts_html_utils/resources/scripts/sorttable.js +489 -0
- tts_html_utils/resources/scripts/sticky_headers.js +11 -0
- tts_html_utils/test/__init__.py +0 -0
- tts_html_utils/test/test_base.py +110 -0
- tts_html_utils/test/test_compiler.py +227 -0
- tts_html_utils/test/test_list.py +127 -0
- tts_html_utils/test/test_misc.py +145 -0
- tts_html_utils/test/test_navigation.py +59 -0
- tts_html_utils/test/test_plot.py +150 -0
- tts_html_utils/test/test_resource.py +133 -0
- tts_html_utils/test/test_structure.py +176 -0
- tts_html_utils/test/test_table.py +256 -0
- tts_html_utils/test/test_text.py +62 -0
- tts_html_utils/visdiff/visdiff.py +98 -0
- tts_html_utils-0.5.4.dist-info/METADATA +72 -0
- tts_html_utils-0.5.4.dist-info/RECORD +76 -0
- tts_html_utils-0.5.4.dist-info/WHEEL +5 -0
- tts_html_utils-0.5.4.dist-info/top_level.txt +1 -0
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
#Standard Library Imports
|
|
2
|
+
import pdb
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from typing import List, Tuple, Dict, Any
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import html
|
|
7
|
+
|
|
8
|
+
#Installed dependency imports
|
|
9
|
+
from jinja2 import Template
|
|
10
|
+
|
|
11
|
+
#Teamtools Studio Imports
|
|
12
|
+
from tts_utilities.logger import create_logger
|
|
13
|
+
|
|
14
|
+
#This Library Imports
|
|
15
|
+
from tts_html_utils.core.components.base import HtmlComponent
|
|
16
|
+
from tts_html_utils.core.components.misc import HTML, Head, Body, Footer, Style, Script, TtsFavicon, Title
|
|
17
|
+
|
|
18
|
+
logger = create_logger(name='html_utils.core.components.base')
|
|
19
|
+
|
|
20
|
+
def make_hashable(obj: Any):
|
|
21
|
+
"""
|
|
22
|
+
Recursively converts mutable objects (dicts, lists) into immutable equivalents
|
|
23
|
+
(frozensets, tuples) to facilitate uniqueness checks.
|
|
24
|
+
|
|
25
|
+
This is necessary because we need to detect duplicate configuration dictionaries
|
|
26
|
+
when aggregating resources. Since Python `sets` cannot contain mutable dictionaries,
|
|
27
|
+
we must freeze them first to check if they are identical.
|
|
28
|
+
|
|
29
|
+
:param obj: The object to convert.
|
|
30
|
+
:return: An immutable, hashable version of the input.
|
|
31
|
+
"""
|
|
32
|
+
if isinstance(obj, dict):
|
|
33
|
+
return frozenset((k, make_hashable(v)) for k, v in obj.items())
|
|
34
|
+
elif isinstance(obj, list):
|
|
35
|
+
return tuple(make_hashable(v) for v in obj)
|
|
36
|
+
elif isinstance(obj, set):
|
|
37
|
+
return frozenset(make_hashable(v) for v in obj)
|
|
38
|
+
else:
|
|
39
|
+
return obj
|
|
40
|
+
|
|
41
|
+
def group_by_path_with_unique_dicts(tuples: List[Tuple[Path, Dict]]) -> List[Tuple[Path, List[Dict]]]:
|
|
42
|
+
"""
|
|
43
|
+
Groups resource requests by file path and removes duplicate configurations.
|
|
44
|
+
|
|
45
|
+
This is used to manage dependencies efficiently. For example, if multiple
|
|
46
|
+
components request the same JavaScript library with identical settings,
|
|
47
|
+
this ensures the library is only loaded once. If they request the same
|
|
48
|
+
library with *different* settings, both configurations are preserved.
|
|
49
|
+
|
|
50
|
+
:param tuples: A list of (FilePath, ConfigDict) pairs.
|
|
51
|
+
:return: A list of tuples where each FilePath is associated with a list of unique ConfigDicts.
|
|
52
|
+
"""
|
|
53
|
+
grouped = defaultdict(set)
|
|
54
|
+
|
|
55
|
+
for path, meta in tuples:
|
|
56
|
+
hashable_meta = make_hashable(meta)
|
|
57
|
+
grouped[path].add(hashable_meta)
|
|
58
|
+
|
|
59
|
+
result = [
|
|
60
|
+
(path, [dict(items) for items in dicts])
|
|
61
|
+
for path, dicts in grouped.items()
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
return result
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class HtmlCompiler:
|
|
68
|
+
"""
|
|
69
|
+
Manages the lifecycle of an HTML document, handling component aggregation
|
|
70
|
+
and dependency resolution.
|
|
71
|
+
|
|
72
|
+
This class abstracts away the manual construction of the `<html>` structure.
|
|
73
|
+
It allows you to add Python component objects from this libary to the
|
|
74
|
+
Head or Body, and automatically handles the "plumbing"—ensuring that any
|
|
75
|
+
CSS or JavaScript required by those components is injected into the document
|
|
76
|
+
correctly and without duplication.
|
|
77
|
+
|
|
78
|
+
:param title: The document title (displayed in the browser tab).
|
|
79
|
+
"""
|
|
80
|
+
def __init__(self, title):
|
|
81
|
+
self.title = title
|
|
82
|
+
self.head_components = [TtsFavicon()]
|
|
83
|
+
self.body_components = list()
|
|
84
|
+
self.scripts = list()
|
|
85
|
+
self.styles = list()
|
|
86
|
+
|
|
87
|
+
def add_head_component(self, component):
|
|
88
|
+
"""
|
|
89
|
+
Adds a component to the `<head>` section of the document.
|
|
90
|
+
|
|
91
|
+
The Head is used for metadata and configuration that is not directly
|
|
92
|
+
displayed on the page canvas (e.g., meta tags, third-party analytics scripts,
|
|
93
|
+
favicons, or social media open graph tags).
|
|
94
|
+
|
|
95
|
+
:param component: An `HtmlComponent` appropriate for the head section.
|
|
96
|
+
"""
|
|
97
|
+
if isinstance(component, HtmlComponent):
|
|
98
|
+
self.head_components.append(component)
|
|
99
|
+
else:
|
|
100
|
+
raise Exception(f"{component} is not an HtmlComponent")
|
|
101
|
+
|
|
102
|
+
def add_body_component(self, component):
|
|
103
|
+
"""
|
|
104
|
+
Adds a component to the `<body>` section of the document.
|
|
105
|
+
|
|
106
|
+
The Body contains the visible content of the page. Use this for
|
|
107
|
+
visual elements like layout containers, text, tables, and widgets.
|
|
108
|
+
|
|
109
|
+
:param component: An `HtmlComponent` appropriate for the body section.
|
|
110
|
+
"""
|
|
111
|
+
if isinstance(component, HtmlComponent):
|
|
112
|
+
self.body_components.append(component)
|
|
113
|
+
else:
|
|
114
|
+
raise Exception(f"{component} is not an HtmlComponent")
|
|
115
|
+
|
|
116
|
+
def compile(self):
|
|
117
|
+
"""
|
|
118
|
+
Assembles the final object tree for the HTML document.
|
|
119
|
+
|
|
120
|
+
This method performs dependency injection and deduplication:
|
|
121
|
+
1. **Aggregation:** Scans all added components to find their required CSS/JS files.
|
|
122
|
+
2. **Deduplication:** Ensures shared resources (like a common plotting library) are not included twice.
|
|
123
|
+
3. **Rendering:** processes any Jinja2 templates found in the resource files.
|
|
124
|
+
4. **Construction:** Builds the root `HTML` object containing the configured `Head` and `Body`.
|
|
125
|
+
|
|
126
|
+
:return: The root `HTML` object ready for final string rendering.
|
|
127
|
+
"""
|
|
128
|
+
css_includes = []
|
|
129
|
+
js_includes = []
|
|
130
|
+
|
|
131
|
+
# 1. Collect dependencies from all components
|
|
132
|
+
for component in self.head_components + self.body_components:
|
|
133
|
+
css_includes += component.aggregate_attributes('css_includes')
|
|
134
|
+
js_includes += component.aggregate_attributes('js_includes')
|
|
135
|
+
|
|
136
|
+
# 2. Remove duplicates
|
|
137
|
+
css_includes = group_by_path_with_unique_dicts(css_includes)
|
|
138
|
+
js_includes = group_by_path_with_unique_dicts(js_includes)
|
|
139
|
+
|
|
140
|
+
document = HTML()
|
|
141
|
+
head = Head()
|
|
142
|
+
head.add_child(Title(self.title))
|
|
143
|
+
|
|
144
|
+
# 3. Process and attach CSS
|
|
145
|
+
for path, dicts in css_includes:
|
|
146
|
+
if isinstance(path, str):
|
|
147
|
+
# Direct string path inclusion (e.g. for CDNs)
|
|
148
|
+
head.add_child(Style(path))
|
|
149
|
+
continue
|
|
150
|
+
|
|
151
|
+
# Render template-based CSS files
|
|
152
|
+
css_template_text = path.open('r').read()
|
|
153
|
+
template = Template(css_template_text)
|
|
154
|
+
for d in dicts:
|
|
155
|
+
rendered_css = template.render(**d)
|
|
156
|
+
head.add_child(Style(rendered_css))
|
|
157
|
+
|
|
158
|
+
# 4. Process and attach JS
|
|
159
|
+
for path, dicts in js_includes:
|
|
160
|
+
if isinstance(path, str):
|
|
161
|
+
# Direct string path inclusion
|
|
162
|
+
head.add_child(Script(attr={'src': path}))
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
# Render template-based JS files
|
|
166
|
+
js_template_text = path.open('r').read()
|
|
167
|
+
template = Template(js_template_text)
|
|
168
|
+
for d in dicts:
|
|
169
|
+
rendered_js = template.render(**d)
|
|
170
|
+
head.add_child(Script(rendered_js))
|
|
171
|
+
|
|
172
|
+
# 5. Attach explicit components
|
|
173
|
+
for head_component in self.head_components:
|
|
174
|
+
head.add_child(head_component)
|
|
175
|
+
|
|
176
|
+
body = Body(style={'padding': '10px'})
|
|
177
|
+
for body_component in self.body_components:
|
|
178
|
+
body.add_child(body_component)
|
|
179
|
+
|
|
180
|
+
document.add_child(head)
|
|
181
|
+
document.add_child(body)
|
|
182
|
+
|
|
183
|
+
return document
|
|
184
|
+
|
|
185
|
+
def render(self):
|
|
186
|
+
"""
|
|
187
|
+
Compiles the object tree and serializes it into an HTML string.
|
|
188
|
+
|
|
189
|
+
:return: The complete HTML source code.
|
|
190
|
+
"""
|
|
191
|
+
return self.compile().render()
|
|
192
|
+
|
|
193
|
+
def render_to_file(self, filepath):
|
|
194
|
+
"""
|
|
195
|
+
Compiles the HTML and writes it to the specified file path.
|
|
196
|
+
|
|
197
|
+
:param filepath: The full destination path for the .html file.
|
|
198
|
+
"""
|
|
199
|
+
with open(filepath,'w') as f:
|
|
200
|
+
f.write(self.render())
|
|
201
|
+
|
|
202
|
+
def render_to_jnb_iframe(self, width='100%', height='300px'):
|
|
203
|
+
"""
|
|
204
|
+
Compiles the HTML and writes it to the specified file path.
|
|
205
|
+
|
|
206
|
+
:param filepath: The full destination path for the .html file.
|
|
207
|
+
"""
|
|
208
|
+
escaped_html = html.escape(self.render())
|
|
209
|
+
return f'''
|
|
210
|
+
<iframe
|
|
211
|
+
srcdoc="{escaped_html}"
|
|
212
|
+
style="width: {width}; height: {height}; border: 1px solid #ddd;"
|
|
213
|
+
sandbox="allow-scripts allow-same-origin"
|
|
214
|
+
></iframe>
|
|
215
|
+
'''
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
#Standard Library Imports
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from uuid import uuid4
|
|
4
|
+
import pdb
|
|
5
|
+
|
|
6
|
+
#Installed Dependecy Imports
|
|
7
|
+
#none yet
|
|
8
|
+
|
|
9
|
+
#Teamtools Studio Imports
|
|
10
|
+
from tts_utilities.util import as_list
|
|
11
|
+
from tts_utilities.logger import create_logger
|
|
12
|
+
from bs4 import BeautifulSoup
|
|
13
|
+
|
|
14
|
+
#This Library Imports
|
|
15
|
+
# None
|
|
16
|
+
|
|
17
|
+
logger = create_logger(name='html_utils.core.components.base')
|
|
18
|
+
|
|
19
|
+
#================================================
|
|
20
|
+
# :: Base Component
|
|
21
|
+
#================================================
|
|
22
|
+
class HtmlComponent(ABC):
|
|
23
|
+
"""
|
|
24
|
+
The fundamental blueprint for all HTML elements in this library.
|
|
25
|
+
|
|
26
|
+
**The Concept:**
|
|
27
|
+
HTML is hierarchical (a tree). A `<div>` contains a `<table>`, which contains
|
|
28
|
+
`<tr>` rows, which contain `<td>` cells. This class provides the machinery
|
|
29
|
+
to replicate that structure in Python.
|
|
30
|
+
|
|
31
|
+
**How it works:**
|
|
32
|
+
Every specific HTML tag (like `Div`, `Span`, or `Img`) inherits from this class.
|
|
33
|
+
This base class handles the logic shared by *all* of them, such as:
|
|
34
|
+
1. **Parent/Child Relationships:** Tracking what is inside what.
|
|
35
|
+
2. **Attribute Management:** Handling IDs, CSS classes, and inline styles.
|
|
36
|
+
3. **Dependency bubbling:** If a button deep inside a table requires a specific
|
|
37
|
+
JavaScript file, this class allows that requirement to bubble up to the
|
|
38
|
+
top of the document so the `HtmlCompiler` can see it.
|
|
39
|
+
|
|
40
|
+
**Note:** This is an "Abstract Base Class" (ABC). You cannot create a generic `HtmlComponent`
|
|
41
|
+
directly. You must use one of its subclasses that represents a real HTML tag.
|
|
42
|
+
|
|
43
|
+
:param children: Any nested components that go *inside* this element.
|
|
44
|
+
:type children: List of HtmlComponent objects (or plain strings), optional
|
|
45
|
+
:param class_name: Primary CSS class(es) to control the look/feel.
|
|
46
|
+
:type class_name: str or list of str, optional
|
|
47
|
+
:param extra_class_name: Additional CSS classes to append. Useful when extending components
|
|
48
|
+
where you want to keep the base class but add a modifier.
|
|
49
|
+
:type extra_class_name: str or list of str, optional
|
|
50
|
+
:param tag: The actual HTML tag name (e.g., 'div', 'a', 'span'). Defaults to the class's `TAG` constant.
|
|
51
|
+
:type tag: str, optional
|
|
52
|
+
:param id: The unique HTML `id` attribute for this element.
|
|
53
|
+
:type id: str, optional
|
|
54
|
+
:param style: Inline CSS styles (e.g., `{'color': 'red', 'margin': '10px'}`).
|
|
55
|
+
:type style: dict, optional
|
|
56
|
+
:param attr: A dictionary of any other HTML attributes not covered above (e.g., `{'href': '...', 'onclick': '...'}`).
|
|
57
|
+
:type attr: dict, optional
|
|
58
|
+
"""
|
|
59
|
+
TAG = None
|
|
60
|
+
DEFAULT_CLASS = []
|
|
61
|
+
STYLESHEETS = []
|
|
62
|
+
|
|
63
|
+
def __init__(self, children=None, class_name=None, extra_class_name=None, tag=None, id=None, style=None, attr=None):
|
|
64
|
+
self.id = id
|
|
65
|
+
self.tag = tag or self.TAG
|
|
66
|
+
self.parent_id = None
|
|
67
|
+
self.children = []
|
|
68
|
+
self.js_includes = []
|
|
69
|
+
self.css_includes = []
|
|
70
|
+
self.attr = attr if attr is not None else {}
|
|
71
|
+
|
|
72
|
+
for child in as_list(children or []):
|
|
73
|
+
self.add_child(child)
|
|
74
|
+
|
|
75
|
+
self._class = [] + as_list(class_name or self.DEFAULT_CLASS)
|
|
76
|
+
if extra_class_name is not None:
|
|
77
|
+
self._class += as_list(extra_class_name)
|
|
78
|
+
|
|
79
|
+
self.style = style or {}
|
|
80
|
+
|
|
81
|
+
def aggregate_attributes(self, attr, lst=None):
|
|
82
|
+
"""
|
|
83
|
+
The "Harvester" method.
|
|
84
|
+
|
|
85
|
+
This function recursively dives down into every child (and grandchild)
|
|
86
|
+
of this component to find specific attributes (usually 'js_includes' or
|
|
87
|
+
'css_includes').
|
|
88
|
+
|
|
89
|
+
**Why?** When you render a full page, the top-level document needs to know about
|
|
90
|
+
every JavaScript file required by every tiny widget deep in the page structure.
|
|
91
|
+
This method bubbles those requirements up to the surface.
|
|
92
|
+
|
|
93
|
+
:param attr: The name of the attribute to collect (e.g., `'js_includes'`).
|
|
94
|
+
:type attr: str
|
|
95
|
+
:param lst: The accumulator list. If None, a new list is created.
|
|
96
|
+
:type lst: list, optional
|
|
97
|
+
:return: A list containing the collected attributes from this component and all descendents.
|
|
98
|
+
"""
|
|
99
|
+
if lst is None: lst = []
|
|
100
|
+
v = getattr(self, attr, None)
|
|
101
|
+
if v is not None: lst += v #support elements without the given attr set
|
|
102
|
+
for child in self.children:
|
|
103
|
+
if not isinstance(child, HtmlComponent): continue
|
|
104
|
+
try:
|
|
105
|
+
child.aggregate_attributes(attr, lst=lst)
|
|
106
|
+
except:
|
|
107
|
+
pdb.set_trace()
|
|
108
|
+
return lst
|
|
109
|
+
|
|
110
|
+
def add_child(self, child):
|
|
111
|
+
"""
|
|
112
|
+
Nests a new element inside this one.
|
|
113
|
+
|
|
114
|
+
:param child: The component to insert.
|
|
115
|
+
:type child: HtmlComponent or str
|
|
116
|
+
"""
|
|
117
|
+
self.children.append(child)
|
|
118
|
+
if isinstance(child, HtmlComponent):
|
|
119
|
+
child.parent_id = self.id
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def class_name(self):
|
|
123
|
+
"""
|
|
124
|
+
Returns the final, space-separated string of CSS classes.
|
|
125
|
+
|
|
126
|
+
This combines the default classes (from `DEFAULT_CLASS`) with any specific
|
|
127
|
+
classes added during initialization or via `add_class`. This string is
|
|
128
|
+
what actually gets written to the HTML `class="..."` attribute.
|
|
129
|
+
"""
|
|
130
|
+
return " ".join(self._class)
|
|
131
|
+
|
|
132
|
+
def add_class(self, new_class):
|
|
133
|
+
"""
|
|
134
|
+
Appends a new CSS class to the existing list of classes.
|
|
135
|
+
Useful for dynamically highlighting an element (e.g., adding 'active' or 'error').
|
|
136
|
+
|
|
137
|
+
:param new_class: The CSS class string to add.
|
|
138
|
+
:type new_class: str
|
|
139
|
+
"""
|
|
140
|
+
if new_class not in self._class:
|
|
141
|
+
self._class.append(new_class)
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def rendered(self):
|
|
145
|
+
"""
|
|
146
|
+
A convenience property that triggers the full rendering process.
|
|
147
|
+
Accessing `.rendered` is equivalent to calling the `.render()` method.
|
|
148
|
+
|
|
149
|
+
:return: The fully compiled HTML string for this component.
|
|
150
|
+
:rtype: str
|
|
151
|
+
"""
|
|
152
|
+
return self.render()
|
|
153
|
+
|
|
154
|
+
@abstractmethod
|
|
155
|
+
def render(self):
|
|
156
|
+
"""
|
|
157
|
+
This method transforms the abstract Python object into an actual HTML string.
|
|
158
|
+
Since this is the base class, it doesn't know *how* to render itself yet.
|
|
159
|
+
Specific implementations (like `HtmlComponentSimple`) provide the logic here.
|
|
160
|
+
"""
|
|
161
|
+
pass
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def content(self):
|
|
165
|
+
"""
|
|
166
|
+
A convenience property that returns the *inner* HTML of the component.
|
|
167
|
+
|
|
168
|
+
Example: If the component is `<div><b>Hello</b></div>`, `.content` returns `<b>Hello</b>`.
|
|
169
|
+
"""
|
|
170
|
+
return self.render_content()
|
|
171
|
+
|
|
172
|
+
def render_content(self):
|
|
173
|
+
"""
|
|
174
|
+
A hook for how an individual component should render its content as a string.
|
|
175
|
+
Most compoenents inherit from HtmlComponentSimple, which inherits from this.
|
|
176
|
+
HtmlComponentSimple defines render_content itself, but it can always be
|
|
177
|
+
overridden by a descendent class.
|
|
178
|
+
"""
|
|
179
|
+
raise NotImplementedError(f'This class does not implement content rendering')
|
|
180
|
+
|
|
181
|
+
def recurse_stylesheets(self):
|
|
182
|
+
"""
|
|
183
|
+
Crawls the component tree to find all stylesheets declared by children.
|
|
184
|
+
|
|
185
|
+
Similar to `aggregate_attributes`, this ensures that if a nested component
|
|
186
|
+
defines a custom `STYLESHEET`, that sheet is bubbled up and included in the
|
|
187
|
+
document head.
|
|
188
|
+
"""
|
|
189
|
+
child_sheets = self.STYLESHEETS
|
|
190
|
+
for child in self.children:
|
|
191
|
+
if isinstance(child, HtmlComponent):
|
|
192
|
+
child_sheets += child.recurse_stylesheets()
|
|
193
|
+
return child_sheets
|
|
194
|
+
|
|
195
|
+
def pretty_print(self):
|
|
196
|
+
"""
|
|
197
|
+
A debugging tool that prints the HTML in a readable, indented format.
|
|
198
|
+
|
|
199
|
+
Raw HTML strings are often one massive, unreadable line. This uses
|
|
200
|
+
BeautifulSoup to break it into indented lines so you can see the structure
|
|
201
|
+
visually.
|
|
202
|
+
"""
|
|
203
|
+
html = self.render()
|
|
204
|
+
soup = BeautifulSoup(html, 'html.parser')
|
|
205
|
+
return soup.prettify()
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class HtmlComponentSimple(HtmlComponent):
|
|
209
|
+
"""
|
|
210
|
+
The implementation for standard "Container" HTML tags.
|
|
211
|
+
|
|
212
|
+
**Concept:**
|
|
213
|
+
Most HTML tags work like containers: they open, they hold content, and they close.
|
|
214
|
+
Examples: `<div>...</div>`, `<p>...</p>`, `<b>...</b>`.
|
|
215
|
+
|
|
216
|
+
This class implements the `render` logic for this open-close pattern.
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
def _render_open(self):
|
|
220
|
+
"""
|
|
221
|
+
Internal helper: specific logic to build the *opening* tag.
|
|
222
|
+
|
|
223
|
+
It takes the Python dictionaries for style/attributes and converts them
|
|
224
|
+
into string format.
|
|
225
|
+
"""
|
|
226
|
+
# 1. Start with the tag itself
|
|
227
|
+
parts = [self.tag]
|
|
228
|
+
|
|
229
|
+
# 2. Add ID if present
|
|
230
|
+
if self.id:
|
|
231
|
+
parts.append(f'id="{self.id}"')
|
|
232
|
+
|
|
233
|
+
# 3. Add Class if present
|
|
234
|
+
if self.class_name:
|
|
235
|
+
parts.append(f'class="{self.class_name}"')
|
|
236
|
+
|
|
237
|
+
# 4. Add Style if present
|
|
238
|
+
if self.style:
|
|
239
|
+
style_str = ' '.join(f'{k}: {v};' for k, v in self.style.items())
|
|
240
|
+
parts.append(f'style="{style_str}"')
|
|
241
|
+
|
|
242
|
+
# 5. Add other attributes if present
|
|
243
|
+
if self.attr:
|
|
244
|
+
attr_str = ' '.join(f'{k}="{v}"' for k, v in self.attr.items())
|
|
245
|
+
parts.append(attr_str)
|
|
246
|
+
|
|
247
|
+
# 6. Join with spaces and wrap in brackets
|
|
248
|
+
# ' '.join(parts) ensures there is exactly one space between attributes
|
|
249
|
+
# and no trailing space before the closing bracket.
|
|
250
|
+
return f'<{" ".join(parts)}>'
|
|
251
|
+
|
|
252
|
+
def render(self):
|
|
253
|
+
"""
|
|
254
|
+
Generates the full HTML string for this element and all its children.
|
|
255
|
+
|
|
256
|
+
Structure: `<tag> [All Children Rendered Here] </tag>`
|
|
257
|
+
"""
|
|
258
|
+
return f'{self._render_open()}{self.content}</{self.tag}>'
|
|
259
|
+
|
|
260
|
+
def render_content(self):
|
|
261
|
+
"""
|
|
262
|
+
Iterates through the children list, asks them to render themselves,
|
|
263
|
+
and concatenates the results.
|
|
264
|
+
"""
|
|
265
|
+
out = ''
|
|
266
|
+
for child in self.children:
|
|
267
|
+
if isinstance(child, HtmlComponent):
|
|
268
|
+
out += child.rendered
|
|
269
|
+
else:
|
|
270
|
+
out += str(child)
|
|
271
|
+
return out
|
|
272
|
+
|
|
273
|
+
class HtmlComponentSingleSimple(HtmlComponentSimple):
|
|
274
|
+
"""
|
|
275
|
+
The implementation for "Void" or "Self-Closing" HTML tags.
|
|
276
|
+
|
|
277
|
+
**Concept:**
|
|
278
|
+
Some HTML tags do not contain other elements. They are standalone instructions.
|
|
279
|
+
Examples:
|
|
280
|
+
- `<img>`: Just places an image.
|
|
281
|
+
- `<br>`: Just creates a line break.
|
|
282
|
+
- `<input>`: Just creates a text box.
|
|
283
|
+
|
|
284
|
+
These tags **must not** have a closing tag (e.g., `</img>` is invalid HTML).
|
|
285
|
+
This class overrides the render method to ensure we only print the opening tag.
|
|
286
|
+
"""
|
|
287
|
+
def render(self):
|
|
288
|
+
"""
|
|
289
|
+
Renders the tag without a closing partner or internal content.
|
|
290
|
+
|
|
291
|
+
Structure: `<tag attribute="value">`
|
|
292
|
+
"""
|
|
293
|
+
return self._render_open()
|
|
294
|
+
|
|
295
|
+
def render_content(self):
|
|
296
|
+
"""
|
|
297
|
+
Disabled for HtmlComponentSingleSimple, but still here in case it's called
|
|
298
|
+
so it raises a useful exception.
|
|
299
|
+
"""
|
|
300
|
+
raise NotImplementedError(f'No content allowed for Singleton HTML elements')
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
def png_to_base64_string(image_path):
|
|
5
|
+
# 1. Open the image file in binary read mode ('rb')
|
|
6
|
+
with open(image_path, "rb") as image_file:
|
|
7
|
+
|
|
8
|
+
# 2. Read the binary data and encode it to base64
|
|
9
|
+
base64_bytes = base64.b64encode(image_file.read())
|
|
10
|
+
|
|
11
|
+
# 3. Decode bytes to a UTF-8 string so it can be printed
|
|
12
|
+
base64_string = base64_bytes.decode('utf-8')
|
|
13
|
+
|
|
14
|
+
return base64_string
|
|
15
|
+
|
|
16
|
+
favicon_data = f'data:image/png;base64,{png_to_base64_string(Path(__file__).parent.parent.parent / "resources/img/favicon.png")}'
|
|
17
|
+
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#Standard Library Imports
|
|
2
|
+
import pdb
|
|
3
|
+
|
|
4
|
+
#Installed dependency imports
|
|
5
|
+
#None yet
|
|
6
|
+
|
|
7
|
+
#Teamtools Studio Imports
|
|
8
|
+
from tts_utilities.logger import create_logger
|
|
9
|
+
|
|
10
|
+
#This Library Imports
|
|
11
|
+
from tts_html_utils.core.components.base import HtmlComponentSimple
|
|
12
|
+
from tts_html_utils.core.components.misc import Div
|
|
13
|
+
|
|
14
|
+
logger = create_logger(name='tts_html_utils.core.components.flexbox')
|
|
15
|
+
|
|
16
|
+
class FlexContainer(HtmlComponentSimple):
|
|
17
|
+
"""
|
|
18
|
+
A flexible container that uses CSS Flexbox for layout.
|
|
19
|
+
|
|
20
|
+
**Concept:**
|
|
21
|
+
The FlexContainer is a div with display:flex that allows for easy
|
|
22
|
+
arrangement of child elements in rows or columns with precise control
|
|
23
|
+
over sizing and spacing.
|
|
24
|
+
|
|
25
|
+
:param children: Child components to be arranged in the flex container.
|
|
26
|
+
:type children: list or single HtmlComponent
|
|
27
|
+
:param direction: The flex-direction property. Options: 'row', 'column', 'row-reverse', 'column-reverse'.
|
|
28
|
+
:type direction: str, optional
|
|
29
|
+
:param justify: The justify-content property. Options: 'flex-start', 'flex-end', 'center', 'space-between', 'space-around', 'space-evenly'.
|
|
30
|
+
:type justify: str, optional
|
|
31
|
+
:param align: The align-items property. Options: 'flex-start', 'flex-end', 'center', 'stretch', 'baseline'.
|
|
32
|
+
:type align: str, optional
|
|
33
|
+
:param wrap: The flex-wrap property. Options: 'nowrap', 'wrap', 'wrap-reverse'.
|
|
34
|
+
:type wrap: str, optional
|
|
35
|
+
:param gap: The gap between flex items (e.g., '5px', '1rem').
|
|
36
|
+
:type gap: str, optional
|
|
37
|
+
:param kwargs: Additional keyword arguments passed to the parent Div.
|
|
38
|
+
"""
|
|
39
|
+
TAG = 'div'
|
|
40
|
+
|
|
41
|
+
def __init__(self, children=None, direction='row', justify='flex-start',
|
|
42
|
+
align='stretch', wrap='nowrap', gap=None, **kwargs):
|
|
43
|
+
# Build the style dictionary for flexbox
|
|
44
|
+
style = kwargs.get('style', {})
|
|
45
|
+
style.update({
|
|
46
|
+
'display': 'flex',
|
|
47
|
+
'flex-direction': direction,
|
|
48
|
+
'justify-content': justify,
|
|
49
|
+
'align-items': align,
|
|
50
|
+
'flex-wrap': wrap
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
# Add gap if specified
|
|
54
|
+
if gap:
|
|
55
|
+
style['gap'] = gap
|
|
56
|
+
|
|
57
|
+
kwargs['style'] = style
|
|
58
|
+
super().__init__(children=children, **kwargs)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class FlexItem(HtmlComponentSimple):
|
|
62
|
+
"""
|
|
63
|
+
A flex item that can be placed inside a FlexContainer.
|
|
64
|
+
|
|
65
|
+
**Concept:**
|
|
66
|
+
FlexItems are divs with flex properties that control how they grow, shrink,
|
|
67
|
+
and what their base size is within a FlexContainer.
|
|
68
|
+
|
|
69
|
+
:param children: Child components inside this flex item.
|
|
70
|
+
:type children: list or single HtmlComponent
|
|
71
|
+
:param width: The width of the item (e.g., '50%', '200px', 'auto').
|
|
72
|
+
:type width: str, optional
|
|
73
|
+
:param height: The height of the item (e.g., '100px', 'auto').
|
|
74
|
+
:type height: str, optional
|
|
75
|
+
:param grow: The flex-grow property (how much the item can grow). Default is 0 (no growing).
|
|
76
|
+
:type grow: int, optional
|
|
77
|
+
:param shrink: The flex-shrink property (how much the item can shrink). Default is 1 (can shrink).
|
|
78
|
+
:type shrink: int, optional
|
|
79
|
+
:param basis: The flex-basis property (base size before growing/shrinking). Default is 'auto'.
|
|
80
|
+
:type basis: str, optional
|
|
81
|
+
:param align_self: Override the container's align-items for this specific item.
|
|
82
|
+
:type align_self: str, optional
|
|
83
|
+
:param kwargs: Additional keyword arguments passed to the parent Div.
|
|
84
|
+
"""
|
|
85
|
+
TAG = 'div'
|
|
86
|
+
|
|
87
|
+
def __init__(self, children=None, width=None, height=None, grow=None,
|
|
88
|
+
shrink=None, basis=None, align_self=None, **kwargs):
|
|
89
|
+
# Build the style dictionary for the flex item
|
|
90
|
+
style = kwargs.get('style', {})
|
|
91
|
+
|
|
92
|
+
# Add width and height if specified
|
|
93
|
+
if width:
|
|
94
|
+
style['width'] = width
|
|
95
|
+
if height:
|
|
96
|
+
style['height'] = height
|
|
97
|
+
|
|
98
|
+
# Add flex properties if specified
|
|
99
|
+
if grow is not None or shrink is not None or basis is not None:
|
|
100
|
+
flex_parts = []
|
|
101
|
+
flex_parts.append(str(grow) if grow is not None else '0')
|
|
102
|
+
flex_parts.append(str(shrink) if shrink is not None else '1')
|
|
103
|
+
flex_parts.append(basis if basis is not None else 'auto')
|
|
104
|
+
style['flex'] = ' '.join(flex_parts)
|
|
105
|
+
|
|
106
|
+
# Add align-self if specified
|
|
107
|
+
if align_self:
|
|
108
|
+
style['align-self'] = align_self
|
|
109
|
+
|
|
110
|
+
kwargs['style'] = style
|
|
111
|
+
super().__init__(children=children, **kwargs)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class FlexRow(FlexContainer):
|
|
115
|
+
"""
|
|
116
|
+
A convenience class for a horizontal flex container.
|
|
117
|
+
|
|
118
|
+
This is a FlexContainer with direction='row' preset.
|
|
119
|
+
"""
|
|
120
|
+
def __init__(self, children=None, **kwargs):
|
|
121
|
+
kwargs['direction'] = 'row'
|
|
122
|
+
super().__init__(children=children, **kwargs)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class FlexColumn(FlexContainer):
|
|
126
|
+
"""
|
|
127
|
+
A convenience class for a vertical flex container.
|
|
128
|
+
|
|
129
|
+
This is a FlexContainer with direction='column' preset.
|
|
130
|
+
"""
|
|
131
|
+
def __init__(self, children=None, **kwargs):
|
|
132
|
+
kwargs['direction'] = 'column'
|
|
133
|
+
super().__init__(children=children, **kwargs)
|