tina4-python 3.13.51__tar.gz → 3.13.52__tar.gz
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.
- {tina4_python-3.13.51 → tina4_python-3.13.52}/PKG-INFO +1 -1
- {tina4_python-3.13.51 → tina4_python-3.13.52}/pyproject.toml +1 -1
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/core/server.py +6 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/database/connection.py +1 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/frond/FROND.md +79 -0
- tina4_python-3.13.52/tina4_python/frond/__init__.py +85 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/frond/engine.py +109 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/public/js/frond.js +138 -1
- tina4_python-3.13.52/tina4_python/public/js/frond.min.js +2 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/scss/__init__.py +49 -1
- tina4_python-3.13.51/tina4_python/frond/__init__.py +0 -12
- tina4_python-3.13.51/tina4_python/public/js/frond.min.js +0 -2
- {tina4_python-3.13.51 → tina4_python-3.13.52}/.gitignore +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/README.md +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/CLAUDE.md +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/Testing.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/__init__.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/ai/__init__.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/auth/__init__.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/cli/__init__.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/core/events.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/core/middleware.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/core/rate_limiter.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/core/request.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/core/response.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/core/router.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/crud/__init__.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/database/adapter.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/database/firebird.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/database/mongodb.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/database/mssql.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/database/mysql.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/database/odbc.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/database/postgres.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/database/sqlite.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/debug/__init__.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/dev_admin/__init__.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/dev_admin/metrics.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/dev_admin/plan.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/dev_admin/project_index.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/docs.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/docstore/__init__.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/env.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/graphql/__init__.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/mcp/__init__.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/mcp/protocol.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/mcp/tools.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/messenger/__init__.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/migration/runner.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/orm/__init__.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/orm/fields.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/orm/model.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/public/__feedback/widget.js +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/public/js/tina4-dev-admin.js +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/query_builder/__init__.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/queue/__init__.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/queue/job.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/queue/kafka_backend.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/queue/lite_backend.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/queue/mongo_backend.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/queue/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/scss/tina4css/_utilities.scss +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/scss/tina4css/tina4.scss +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/session/__init__.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/swagger/__init__.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/test/__init__.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/test_client/__init__.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/websocket/__init__.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/websocket/backplane.py +0 -0
- {tina4_python-3.13.51 → tina4_python-3.13.52}/tina4_python/wsdl/__init__.py +0 -0
|
@@ -276,6 +276,12 @@ Router.add("GET", _HEALTH_PATH, _health_handler)
|
|
|
276
276
|
if _HEALTH_PATH != "/health":
|
|
277
277
|
Router.add("GET", "/health", _health_handler)
|
|
278
278
|
|
|
279
|
+
# Frond live blocks: re-render a registered {% live %} fragment on demand.
|
|
280
|
+
# Always on (production too) - the poll/sse client fetches this; auth re-applies
|
|
281
|
+
# through the normal middleware chain on every refresh.
|
|
282
|
+
from tina4_python.frond import live_endpoint as _live_endpoint
|
|
283
|
+
Router.add("GET", "/__frond/live/{name}", _live_endpoint)
|
|
284
|
+
|
|
279
285
|
|
|
280
286
|
def _render_error_page(status_code: int, path: str, request_id: str, error_message: str = "") -> str | None:
|
|
281
287
|
"""Render a styled error page using Frond engine.
|
|
@@ -110,6 +110,7 @@ except ImportError:
|
|
|
110
110
|
from tina4_python.database.postgres import PostgreSQLAdapter
|
|
111
111
|
register_driver("postgresql", PostgreSQLAdapter)
|
|
112
112
|
register_driver("postgres", PostgreSQLAdapter)
|
|
113
|
+
register_driver("pgsql", PostgreSQLAdapter) # PDO / Laravel / Doctrine scheme name (issue #58)
|
|
113
114
|
|
|
114
115
|
# Register MySQL (mysql-connector-python — optional)
|
|
115
116
|
from tina4_python.database.mysql import MySQLAdapter
|
|
@@ -383,6 +383,85 @@ Macros don't inherit parent context — pass everything explicitly via parameter
|
|
|
383
383
|
|
|
384
384
|
---
|
|
385
385
|
|
|
386
|
+
## Live Blocks
|
|
387
|
+
|
|
388
|
+
A live block renders on the server, then keeps itself fresh. The first paint ships with the
|
|
389
|
+
page, so there is no loading flash. After that, the block re-fetches its own HTML and swaps
|
|
390
|
+
it in place. You write server-side Frond and the block stays current, with no hand-written
|
|
391
|
+
JavaScript.
|
|
392
|
+
|
|
393
|
+
```twig
|
|
394
|
+
{% live "cart" poll 5 %}
|
|
395
|
+
<strong>{{ count }}</strong> items - {{ total | number_format(2) }}
|
|
396
|
+
{% endlive %}
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
That block paints once with the page, then re-renders every 5 seconds. `frond.js` (already
|
|
400
|
+
loaded from `/js/frond.js`) finds the marker, calls `GET /__frond/live/cart`, and morphs the
|
|
401
|
+
returned HTML into place. A focused input and its caret survive the swap.
|
|
402
|
+
|
|
403
|
+
### Transports
|
|
404
|
+
|
|
405
|
+
Pick how the block refreshes:
|
|
406
|
+
|
|
407
|
+
```twig
|
|
408
|
+
{% live "cart" poll 5 %}...{% endlive %} {# re-fetch every 5 seconds #}
|
|
409
|
+
{% live "feed" sse %}...{% endlive %} {# Server-Sent Events stream #}
|
|
410
|
+
{% live "chat" ws "/ws/chat" %}...{% endlive %} {# WebSocket on /ws/chat #}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
`poll N` pulls every `N` seconds. `sse` and `ws` push: the server decides when to send. All
|
|
414
|
+
three render the same server-side body. Only the delivery changes.
|
|
415
|
+
|
|
416
|
+
### The data source
|
|
417
|
+
|
|
418
|
+
A live block needs data on every refresh, not just the first paint. Register a provider by
|
|
419
|
+
name with `@live_source`. It runs on each refresh with the live request, so the block
|
|
420
|
+
re-renders against fresh data and re-applies auth every time. An unauthenticated caller never
|
|
421
|
+
sees another user's numbers.
|
|
422
|
+
|
|
423
|
+
```python
|
|
424
|
+
from tina4_python.frond import live_source
|
|
425
|
+
|
|
426
|
+
@live_source("cart")
|
|
427
|
+
def cart_data(request):
|
|
428
|
+
user = request.session.get("user_id")
|
|
429
|
+
return {"count": cart_count(user), "total": cart_total(user)}
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
The provider feeds the always-on endpoint `GET /__frond/live/{name}`. There is no route to
|
|
433
|
+
write. The block name is the route.
|
|
434
|
+
|
|
435
|
+
### Pushing updates
|
|
436
|
+
|
|
437
|
+
A `ws` block can update the moment data changes, without waiting for the next poll. Call
|
|
438
|
+
`push_live(name, data)`. It re-renders the block and broadcasts the new HTML to every client
|
|
439
|
+
connected on the block's WebSocket path.
|
|
440
|
+
|
|
441
|
+
```python
|
|
442
|
+
from tina4_python.frond import push_live
|
|
443
|
+
|
|
444
|
+
# after an order lands:
|
|
445
|
+
push_live("cart", {"count": 3, "total": 59.97})
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### Same-origin only
|
|
449
|
+
|
|
450
|
+
A block can point at your own route with `src` instead of the auto endpoint:
|
|
451
|
+
|
|
452
|
+
```twig
|
|
453
|
+
{% live "cart" poll 5 src "/fragments/cart" %}...{% endlive %}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
`src` must be a same-origin path. An absolute URL is rejected at render time, so a live block
|
|
457
|
+
never fetches from a host you did not write. Nested live blocks are rejected too.
|
|
458
|
+
|
|
459
|
+
The marker element is byte-identical across Python, PHP, Ruby, and Node, so the shared
|
|
460
|
+
`frond.js` drives every backend the same way. Write the block once. It renders anywhere
|
|
461
|
+
Tina4 runs.
|
|
462
|
+
|
|
463
|
+
---
|
|
464
|
+
|
|
386
465
|
## Comments
|
|
387
466
|
|
|
388
467
|
```twig
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Tina4 Frond — Zero-dependency template engine.
|
|
2
|
+
"""
|
|
3
|
+
Twig-like template engine built from scratch.
|
|
4
|
+
|
|
5
|
+
from tina4_python.frond import Frond
|
|
6
|
+
|
|
7
|
+
engine = Frond(template_dir="src/templates")
|
|
8
|
+
output = engine.render("page.html", {"name": "World"})
|
|
9
|
+
"""
|
|
10
|
+
import inspect
|
|
11
|
+
|
|
12
|
+
from tina4_python.frond.engine import Frond
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def live_endpoint(name, request, response):
|
|
16
|
+
"""GET /__frond/live/{name} - re-render a registered {% live %} fragment.
|
|
17
|
+
|
|
18
|
+
Resolves the @live_source provider for <name>, runs it with the LIVE request
|
|
19
|
+
so auth and session scoping re-apply on every refresh (a live "my cart"
|
|
20
|
+
cannot leak another user's data), renders the registered fragment, and
|
|
21
|
+
returns the HTML. 404 when the name is unknown or its fragment has not been
|
|
22
|
+
registered yet (the page carrying the block has not rendered).
|
|
23
|
+
"""
|
|
24
|
+
provider = Frond._class_live_sources.get(name)
|
|
25
|
+
if name not in Frond._class_live_fragments and provider is None:
|
|
26
|
+
return response("live block not found: " + str(name), 404)
|
|
27
|
+
context = {}
|
|
28
|
+
if provider is not None:
|
|
29
|
+
result = provider(request)
|
|
30
|
+
if inspect.isawaitable(result):
|
|
31
|
+
result = await result
|
|
32
|
+
context = result or {}
|
|
33
|
+
html = Frond.render_live(name, context)
|
|
34
|
+
if html is None:
|
|
35
|
+
return response("live fragment not registered yet: " + str(name), 404)
|
|
36
|
+
return response(html)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
async def push_live(name, data=None):
|
|
40
|
+
"""Re-render the '<name>' live fragment and push it to connected clients.
|
|
41
|
+
|
|
42
|
+
Renders via Frond.render_live(name, data) and broadcasts a {type, name, html}
|
|
43
|
+
envelope over WebSocket: to the ws path the block declared (data-ws), or to a
|
|
44
|
+
room named <name> if the block declared none. Apps call this after a state
|
|
45
|
+
change (a new chat message, an order update). Returns the rendered HTML, or
|
|
46
|
+
None when the fragment is not registered (its page has not rendered).
|
|
47
|
+
"""
|
|
48
|
+
html = Frond.render_live(name, data or {})
|
|
49
|
+
if html is None:
|
|
50
|
+
return None
|
|
51
|
+
import json
|
|
52
|
+
envelope = json.dumps({"type": "live", "name": name, "html": html})
|
|
53
|
+
try:
|
|
54
|
+
from tina4_python.core.server import _ws_manager
|
|
55
|
+
ws_path = Frond._class_live_ws_paths.get(name)
|
|
56
|
+
if ws_path:
|
|
57
|
+
await _ws_manager.broadcast(envelope, path=ws_path)
|
|
58
|
+
else:
|
|
59
|
+
await _ws_manager.broadcast_to_room(name, envelope)
|
|
60
|
+
except Exception as exc:
|
|
61
|
+
try:
|
|
62
|
+
from tina4_python.debug import Log
|
|
63
|
+
Log.error("push_live(" + str(name) + ") broadcast failed: " + str(exc))
|
|
64
|
+
except Exception:
|
|
65
|
+
pass
|
|
66
|
+
return html
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def live_source(name: str):
|
|
70
|
+
"""Register a data provider for a {% live %} block.
|
|
71
|
+
|
|
72
|
+
The decorated function receives the request and returns the context dict
|
|
73
|
+
used to re-render the fragment on refresh:
|
|
74
|
+
|
|
75
|
+
@live_source("notifications")
|
|
76
|
+
async def notifications(request):
|
|
77
|
+
return {"items": Notification.where("user_id = ?", [request.session["uid"]])}
|
|
78
|
+
"""
|
|
79
|
+
def _wrap(fn):
|
|
80
|
+
Frond._class_live_sources[name] = fn
|
|
81
|
+
return fn
|
|
82
|
+
return _wrap
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
__all__ = ["Frond", "live_source", "live_endpoint", "push_live"]
|
|
@@ -117,6 +117,15 @@ _FILTER_CMP_RE = re.compile(r"(\w+)\s*(!=|==|>=|<=|>|<)\s*(.+)")
|
|
|
117
117
|
_FOR_RE = re.compile(r"for\s+(\w+)(?:\s*,\s*(\w+))?\s+in\s+(.+)")
|
|
118
118
|
_SET_RE = re.compile(r"set\s+(\w+)\s*=\s*(.+)", re.DOTALL)
|
|
119
119
|
_INCLUDE_RE = re.compile(r'include\s+["\'](.+?)["\'](?:\s+with\s+(.+))?')
|
|
120
|
+
_LIVE_RE = re.compile(r'live\s+["\'](?P<name>[^"\']+)["\'](?P<rest>.*)$', re.DOTALL)
|
|
121
|
+
_LIVE_WS_RE = re.compile(r'ws\s+["\']([^"\']+)["\']')
|
|
122
|
+
_LIVE_SRC_RE = re.compile(r'src\s+["\']([^"\']+)["\']')
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _live_attr(value) -> str:
|
|
126
|
+
"""Escape a value for use inside an HTML attribute on a live marker."""
|
|
127
|
+
return (str(value).replace("&", "&").replace('"', """)
|
|
128
|
+
.replace("<", "<").replace(">", ">"))
|
|
120
129
|
_MACRO_RE = re.compile(r"macro\s+(\w+)\s*\(([^)]*)\)")
|
|
121
130
|
_FROM_IMPORT_RE = re.compile(r'from\s+["\'](.+?)["\']\s+import\s+(.+)')
|
|
122
131
|
_IMPORT_AS_RE = re.compile(r'import\s+["\'](.+?)["\']\s+as\s+(\w+)')
|
|
@@ -1309,6 +1318,11 @@ class Frond:
|
|
|
1309
1318
|
_class_globals: dict = {}
|
|
1310
1319
|
_class_filters: dict = {}
|
|
1311
1320
|
_class_tests: dict = {}
|
|
1321
|
+
# Live blocks: name -> body template source (registered when a {% live %}
|
|
1322
|
+
# block renders); name -> data provider (registered via @live_source).
|
|
1323
|
+
_class_live_fragments: dict = {}
|
|
1324
|
+
_class_live_sources: dict = {}
|
|
1325
|
+
_class_live_ws_paths: dict = {}
|
|
1312
1326
|
|
|
1313
1327
|
@classmethod
|
|
1314
1328
|
def clear_registry(cls):
|
|
@@ -1320,6 +1334,9 @@ class Frond:
|
|
|
1320
1334
|
cls._class_globals.clear()
|
|
1321
1335
|
cls._class_filters.clear()
|
|
1322
1336
|
cls._class_tests.clear()
|
|
1337
|
+
cls._class_live_fragments.clear()
|
|
1338
|
+
cls._class_live_sources.clear()
|
|
1339
|
+
cls._class_live_ws_paths.clear()
|
|
1323
1340
|
|
|
1324
1341
|
def __init__(self, template_dir: str = "src/templates"):
|
|
1325
1342
|
self.template_dir = Path(template_dir)
|
|
@@ -1766,6 +1783,11 @@ class Frond:
|
|
|
1766
1783
|
output.append(result)
|
|
1767
1784
|
i = skip
|
|
1768
1785
|
|
|
1786
|
+
elif tag == "live":
|
|
1787
|
+
result, skip = self._handle_live(tokens, i, context)
|
|
1788
|
+
output.append(result)
|
|
1789
|
+
i = skip
|
|
1790
|
+
|
|
1769
1791
|
elif tag in ("block", "endblock", "extends"):
|
|
1770
1792
|
i += 1 # Already handled
|
|
1771
1793
|
|
|
@@ -2396,6 +2418,93 @@ class Frond:
|
|
|
2396
2418
|
self._fragment_cache[cache_key] = (rendered, time.time() + ttl)
|
|
2397
2419
|
return rendered, i
|
|
2398
2420
|
|
|
2421
|
+
def _handle_live(self, tokens: list, start: int, context: dict) -> tuple[str, int]:
|
|
2422
|
+
"""Handle {% live "name" poll N | sse | ws "path" [src "url"] %}...{% endlive %}.
|
|
2423
|
+
|
|
2424
|
+
Server-rendered live region. The body renders once for first paint, is
|
|
2425
|
+
registered under <name> so the /__frond/live/<name> endpoint (or a
|
|
2426
|
+
@live_source provider) can re-render it, and is wrapped in a marker
|
|
2427
|
+
element that frond.js wires to the chosen transport (poll / sse / ws).
|
|
2428
|
+
"""
|
|
2429
|
+
content, _, _ = _strip_tag(tokens[start][1])
|
|
2430
|
+
m = _LIVE_RE.match(content)
|
|
2431
|
+
if not m:
|
|
2432
|
+
raise ValueError('live: expected {% live "name" poll N | sse | ws "path" %}')
|
|
2433
|
+
name = m.group("name")
|
|
2434
|
+
rest = (m.group("rest") or "").strip()
|
|
2435
|
+
parts = rest.split()
|
|
2436
|
+
mode = parts[0] if parts else ""
|
|
2437
|
+
|
|
2438
|
+
src = None
|
|
2439
|
+
sm = _LIVE_SRC_RE.search(rest)
|
|
2440
|
+
if sm:
|
|
2441
|
+
src = sm.group(1)
|
|
2442
|
+
if src and (src.startswith("http://") or src.startswith("https://") or src.startswith("//")):
|
|
2443
|
+
raise ValueError("live: src must be a same-origin path, not an absolute URL")
|
|
2444
|
+
|
|
2445
|
+
interval = None
|
|
2446
|
+
ws_path = None
|
|
2447
|
+
if mode == "poll":
|
|
2448
|
+
if len(parts) < 2 or not parts[1].isdigit():
|
|
2449
|
+
raise ValueError('live: poll requires seconds, e.g. {% live "x" poll 5 %}')
|
|
2450
|
+
interval = int(parts[1])
|
|
2451
|
+
elif mode == "sse":
|
|
2452
|
+
pass
|
|
2453
|
+
elif mode == "ws":
|
|
2454
|
+
wm = _LIVE_WS_RE.search(rest)
|
|
2455
|
+
if not wm:
|
|
2456
|
+
raise ValueError('live: ws requires a path, e.g. {% live "x" ws "/ws/x" %}')
|
|
2457
|
+
ws_path = wm.group(1)
|
|
2458
|
+
else:
|
|
2459
|
+
raise ValueError(f'live: unknown transport "{mode}" (use poll N, sse, or ws "path")')
|
|
2460
|
+
|
|
2461
|
+
# Collect body tokens up to {% endlive %}. Nested live is unsupported.
|
|
2462
|
+
body_tokens = []
|
|
2463
|
+
i = start + 1
|
|
2464
|
+
while i < len(tokens):
|
|
2465
|
+
if tokens[i][0] == BLOCK:
|
|
2466
|
+
tag_content, _, _ = _strip_tag(tokens[i][1])
|
|
2467
|
+
tag = tag_content.split()[0] if tag_content.split() else ""
|
|
2468
|
+
if tag == "live":
|
|
2469
|
+
raise ValueError("live: nested live blocks are not supported")
|
|
2470
|
+
if tag == "endlive":
|
|
2471
|
+
i += 1
|
|
2472
|
+
break
|
|
2473
|
+
body_tokens.append(tokens[i])
|
|
2474
|
+
else:
|
|
2475
|
+
body_tokens.append(tokens[i])
|
|
2476
|
+
i += 1
|
|
2477
|
+
|
|
2478
|
+
# Register the body source so the auto endpoint can re-render it.
|
|
2479
|
+
Frond._class_live_fragments[name] = "".join(raw for (_t, raw) in body_tokens)
|
|
2480
|
+
|
|
2481
|
+
endpoint = src or ("/__frond/live/" + name)
|
|
2482
|
+
attrs = [f'data-frond-live="{_live_attr(name)}"', f'id="live-{_live_attr(name)}"']
|
|
2483
|
+
if mode == "poll":
|
|
2484
|
+
attrs += ['data-mode="poll"', f'data-interval="{interval}"',
|
|
2485
|
+
f'data-src="{_live_attr(endpoint)}"']
|
|
2486
|
+
elif mode == "sse":
|
|
2487
|
+
attrs += ['data-mode="sse"', f'data-src="{_live_attr(endpoint)}"']
|
|
2488
|
+
elif mode == "ws":
|
|
2489
|
+
Frond._class_live_ws_paths[name] = ws_path
|
|
2490
|
+
attrs += ['data-mode="ws"', f'data-ws="{_live_attr(ws_path)}"']
|
|
2491
|
+
|
|
2492
|
+
first_paint = self._render_tokens(list(body_tokens), context)
|
|
2493
|
+
return f'<div {" ".join(attrs)}>{first_paint}</div>', i
|
|
2494
|
+
|
|
2495
|
+
@classmethod
|
|
2496
|
+
def render_live(cls, name: str, data: dict = None):
|
|
2497
|
+
"""Re-render a registered {% live %} fragment by name.
|
|
2498
|
+
|
|
2499
|
+
Returns the rendered HTML fragment, or None if no fragment is registered
|
|
2500
|
+
under that name yet (the page carrying the block has not rendered). The
|
|
2501
|
+
/__frond/live/<name> endpoint calls this after resolving the provider data.
|
|
2502
|
+
"""
|
|
2503
|
+
source = cls._class_live_fragments.get(name)
|
|
2504
|
+
if source is None:
|
|
2505
|
+
return None
|
|
2506
|
+
return cls().render_string(source, data or {})
|
|
2507
|
+
|
|
2399
2508
|
def _handle_spaceless(self, tokens: list, start: int, context: dict) -> tuple[str, int]:
|
|
2400
2509
|
"""Handle {% spaceless %}...{% endspaceless %}.
|
|
2401
2510
|
|
|
@@ -558,6 +558,132 @@ var _frondModule = (() => {
|
|
|
558
558
|
}
|
|
559
559
|
});
|
|
560
560
|
}
|
|
561
|
+
function _liveKey(el) {
|
|
562
|
+
const d = el.dataset;
|
|
563
|
+
return d && d.key ? d.key : null;
|
|
564
|
+
}
|
|
565
|
+
function _liveSyncAttrs(oldNode, newNode) {
|
|
566
|
+
const na = newNode.attributes;
|
|
567
|
+
for (let i = 0; i < na.length; i++) {
|
|
568
|
+
const a = na[i];
|
|
569
|
+
if (oldNode.getAttribute(a.name) !== a.value) oldNode.setAttribute(a.name, a.value);
|
|
570
|
+
}
|
|
571
|
+
const oa = Array.prototype.slice.call(oldNode.attributes);
|
|
572
|
+
oa.forEach(function(a) {
|
|
573
|
+
if (!newNode.hasAttribute(a.name)) oldNode.removeAttribute(a.name);
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
function _liveMorphNode(oldNode, newNode) {
|
|
577
|
+
const tag = newNode.tagName;
|
|
578
|
+
if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return;
|
|
579
|
+
_liveSyncAttrs(oldNode, newNode);
|
|
580
|
+
if (oldNode.children.length || newNode.children.length) {
|
|
581
|
+
_liveReconcile(oldNode, newNode);
|
|
582
|
+
} else if (oldNode.innerHTML !== newNode.innerHTML) {
|
|
583
|
+
oldNode.innerHTML = newNode.innerHTML;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
function _liveReconcile(parent, next) {
|
|
587
|
+
const oldKids = Array.prototype.slice.call(parent.children);
|
|
588
|
+
const newKids = Array.prototype.slice.call(next.children);
|
|
589
|
+
const oldByKey = {};
|
|
590
|
+
oldKids.forEach(function(c) {
|
|
591
|
+
const k = _liveKey(c);
|
|
592
|
+
if (k) oldByKey[k] = c;
|
|
593
|
+
});
|
|
594
|
+
const order = [];
|
|
595
|
+
for (let i = 0; i < newKids.length; i++) {
|
|
596
|
+
const nk = newKids[i];
|
|
597
|
+
const k = _liveKey(nk);
|
|
598
|
+
let match = null;
|
|
599
|
+
if (k && oldByKey[k]) {
|
|
600
|
+
match = oldByKey[k];
|
|
601
|
+
} else if (!k && oldKids[i] && !_liveKey(oldKids[i]) && oldKids[i].tagName === nk.tagName) {
|
|
602
|
+
match = oldKids[i];
|
|
603
|
+
}
|
|
604
|
+
if (match && match.tagName === nk.tagName) {
|
|
605
|
+
_liveMorphNode(match, nk);
|
|
606
|
+
reused.push(match);
|
|
607
|
+
order.push(match);
|
|
608
|
+
} else {
|
|
609
|
+
order.push(nk);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
let cursor = parent.firstElementChild;
|
|
613
|
+
for (let i = 0; i < order.length; i++) {
|
|
614
|
+
const node = order[i];
|
|
615
|
+
if (node === cursor) {
|
|
616
|
+
cursor = cursor.nextElementSibling;
|
|
617
|
+
} else {
|
|
618
|
+
parent.insertBefore(node, cursor);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
oldKids.forEach(function(c) {
|
|
622
|
+
if (order.indexOf(c) === -1 && c.parentNode === parent) parent.removeChild(c);
|
|
623
|
+
});
|
|
624
|
+
void reused;
|
|
625
|
+
}
|
|
626
|
+
function _liveSwap(container, html) {
|
|
627
|
+
const tmp = document.createElement("div");
|
|
628
|
+
tmp.innerHTML = html;
|
|
629
|
+
if (!tmp.children.length || !container.children.length) {
|
|
630
|
+
container.innerHTML = html;
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
_liveReconcile(container, tmp);
|
|
634
|
+
}
|
|
635
|
+
function _liveWsUrl(path) {
|
|
636
|
+
if (/^wss?:\/\//.test(path)) return path;
|
|
637
|
+
const proto = typeof location !== "undefined" && location.protocol === "https:" ? "wss" : "ws";
|
|
638
|
+
return proto + "://" + location.host + path;
|
|
639
|
+
}
|
|
640
|
+
function _liveExtract(msg, name) {
|
|
641
|
+
if (msg && typeof msg === "object") {
|
|
642
|
+
if (msg.type === "live") {
|
|
643
|
+
if (name && msg.name && msg.name !== name) return null;
|
|
644
|
+
return msg.html != null ? String(msg.html) : null;
|
|
645
|
+
}
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
return typeof msg === "string" ? msg : null;
|
|
649
|
+
}
|
|
650
|
+
function liveInit(root) {
|
|
651
|
+
if (typeof document === "undefined") return;
|
|
652
|
+
const scope = root || document;
|
|
653
|
+
const blocks = scope.querySelectorAll("[data-frond-live]");
|
|
654
|
+
Array.prototype.slice.call(blocks).forEach(function(el) {
|
|
655
|
+
if (el.__frondLive) return;
|
|
656
|
+
el.__frondLive = true;
|
|
657
|
+
const mode = el.getAttribute("data-mode");
|
|
658
|
+
const name = el.getAttribute("data-frond-live");
|
|
659
|
+
if (mode === "poll") {
|
|
660
|
+
const src = el.getAttribute("data-src");
|
|
661
|
+
const interval = (parseInt(el.getAttribute("data-interval"), 10) || 5) * 1e3;
|
|
662
|
+
const timer = setInterval(function() {
|
|
663
|
+
if (typeof document !== "undefined" && document.hidden) return;
|
|
664
|
+
request(src, { method: "GET", onSuccess: function(data) {
|
|
665
|
+
_liveSwap(el, typeof data === "string" ? data : String(data));
|
|
666
|
+
} });
|
|
667
|
+
}, interval);
|
|
668
|
+
el.__frondLiveStop = function() {
|
|
669
|
+
clearInterval(timer);
|
|
670
|
+
};
|
|
671
|
+
} else if (mode === "ws") {
|
|
672
|
+
const sock = wsConnect(_liveWsUrl(el.getAttribute("data-ws")));
|
|
673
|
+
sock.on("message", function(msg) {
|
|
674
|
+
const h = _liveExtract(msg, name);
|
|
675
|
+
if (h !== null) _liveSwap(el, h);
|
|
676
|
+
});
|
|
677
|
+
el.__frondLiveStop = function() {
|
|
678
|
+
sock.close();
|
|
679
|
+
};
|
|
680
|
+
} else if (mode === "sse") {
|
|
681
|
+
if (typeof console !== "undefined" && console.warn) {
|
|
682
|
+
console.warn("[frond.live] sse transport is not wired yet (v1 supports poll and ws); block '" + name + "' shows first paint only. Use poll or ws.");
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
}
|
|
561
687
|
var frond = {
|
|
562
688
|
/** Core HTTP request. */
|
|
563
689
|
request,
|
|
@@ -573,6 +699,8 @@ var _frondModule = (() => {
|
|
|
573
699
|
ws: wsConnect,
|
|
574
700
|
/** Server-Sent Events with auto-reconnect. */
|
|
575
701
|
sse: sseConnect,
|
|
702
|
+
/** Wire {% live %} blocks (poll/ws) with keyed morph. Auto-runs on DOMContentLoaded. */
|
|
703
|
+
live: liveInit,
|
|
576
704
|
/** Cookie helpers: get, set, remove. */
|
|
577
705
|
cookie,
|
|
578
706
|
/** Display alert message in #message element. */
|
|
@@ -593,8 +721,17 @@ var _frondModule = (() => {
|
|
|
593
721
|
};
|
|
594
722
|
if (typeof window !== "undefined") {
|
|
595
723
|
window.frond = frond;
|
|
724
|
+
if (typeof document !== "undefined") {
|
|
725
|
+
if (document.readyState === "loading") {
|
|
726
|
+
document.addEventListener("DOMContentLoaded", function() {
|
|
727
|
+
liveInit();
|
|
728
|
+
});
|
|
729
|
+
} else {
|
|
730
|
+
liveInit();
|
|
731
|
+
}
|
|
732
|
+
}
|
|
596
733
|
}
|
|
597
734
|
return __toCommonJS(frond_exports);
|
|
598
735
|
})();
|
|
599
|
-
/* Frond v2.
|
|
736
|
+
/* Frond v2.2.0 - tina4.com */
|
|
600
737
|
//# sourceMappingURL=frond.js.map
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var _frondModule=(()=>{var E=Object.defineProperty;var O=Object.getOwnPropertyDescriptor;var A=Object.getOwnPropertyNames;var H=Object.prototype.hasOwnProperty;var q=(t,s)=>{for(var e in s)E(t,e,{get:s[e],enumerable:!0})},D=(t,s,e,o)=>{if(s&&typeof s=="object"||typeof s=="function")for(let n of A(s))!H.call(t,n)&&n!==e&&E(t,n,{get:()=>s[n],enumerable:!(o=O(s,n))||o.enumerable});return t};var W=t=>D(E({},"__esModule",{value:!0}),t);var z={};q(z,{frond:()=>L});var y=null;function v(t,s){let e;typeof s=="function"?e={onSuccess:s}:e=s||{};let o=(e.method||"GET").toUpperCase(),n=new XMLHttpRequest;if(n.open(o,t,!0),y!==null&&n.setRequestHeader("Authorization","Bearer "+y),e.headers)for(let u in e.headers)Object.prototype.hasOwnProperty.call(e.headers,u)&&n.setRequestHeader(u,e.headers[u]);let c=null;e.body!==void 0&&e.body!==null&&(e.body instanceof FormData?c=e.body:typeof e.body=="object"?(c=JSON.stringify(e.body),n.setRequestHeader("Content-Type","application/json; charset=UTF-8")):typeof e.body=="string"&&(c=e.body,n.setRequestHeader("Content-Type","text/plain; charset=UTF-8"))),n.onload=function(){let u=n.getResponseHeader("FreshToken");u&&u!==""&&(y=u);let r=n.response;try{r=JSON.parse(r)}catch{}if(n.responseURL){let i=new URL(t,window.location.href).href;if(n.responseURL!==i){window.location.href=n.responseURL;return}}n.status>=200&&n.status<400?e.onSuccess&&e.onSuccess(r,n.status,n):e.onError&&e.onError(n.status,n)},n.onerror=function(){e.onError&&e.onError(n.status,n)},n.send(c)}function h(t,s){if(!t)return"";let e=new DOMParser,o=t.includes("<html>")?t:"<body>"+t+"</body></html>",c=e.parseFromString(o,"text/html").querySelector("body"),u=c.querySelectorAll("script");if(u.forEach(function(r){r.remove()}),s!==null){let r=document.getElementById(s);return r&&(c.children.length>0?r.replaceChildren.apply(r,Array.from(c.children)):r.innerHTML=c.innerHTML,u.forEach(function(i){let l=document.createElement("script");l.type="text/javascript",l.async=!0,i.src?l.src=i.src:l.textContent=i.textContent,r.appendChild(l)})),""}return u.forEach(function(r){let i=document.createElement("script");i.type="text/javascript",i.async=!0,i.textContent=r.textContent,document.body.appendChild(i)}),c.innerHTML}function _(t,s,e){let o=s||"content";v(t,{method:"GET",onSuccess:function(n,c){if(document.getElementById(o)){let u=h(n,o);e&&e(u,n)}else e&&e(n)}})}function R(t,s,e,o){let n=e||"content";v(t,{method:"POST",body:s,onSuccess:function(c){let u="";if(c&&c.message!==void 0)u=h(c.message,n);else if(document.getElementById(n))u=h(c,n);else{o&&o(c);return}o&&o(u,c)}})}var x={collect:function(t){let s=new FormData,e=document.querySelectorAll("#"+t+" select, #"+t+" input, #"+t+" textarea");for(let o=0;o<e.length;o++){let n=e[o];if(n.name==="formToken"&&y!==null&&(n.value=y),!!n.name)if(n.type==="file"){let c=n.files;if(c)for(let u=0;u<c.length;u++){let r=c[u];if(r!==void 0){let i=n.name;c.length>1&&!i.includes("[")&&(i=i+"[]"),s.append(i,r,r.name)}}}else n.type==="checkbox"||n.type==="radio"?n.checked?s.append(n.name,n.value):n.type!=="radio"&&s.append(n.name,"0"):s.append(n.name,n.value===""?"":n.value)}return s},submit:function(t,s,e,o){let n=x.collect(t);R(s,n,e||"message",o)},show:function(t,s,e,o){let n=t.toUpperCase();(t==="create"||t==="edit")&&(n="GET"),t==="delete"&&(n="DELETE");let c=e||"form";v(s,{method:n,onSuccess:function(u){let r="";if(u&&u.message!==void 0)r=h(u.message,c);else if(document.getElementById(c))r=h(u,c);else{o&&o(u);return}o&&o(r)}})}};function M(t,s){let e={reconnect:!0,reconnectDelay:1e3,maxReconnectDelay:3e4,maxReconnectAttempts:1/0,protocols:[],onOpen:function(){},onClose:function(){},onError:function(){},...s||{}},o=null,n=!1,c=e.reconnectDelay,u=0,r=null,i={message:[],open:[],close:[],error:[]},l={status:"connecting",send:function(f){if(!o||o.readyState!==WebSocket.OPEN)throw new Error("[frond] WebSocket is not connected");o.send(typeof f=="string"?f:JSON.stringify(f))},on:function(f,a){return i[f]||(i[f]=[]),i[f].push(a),function(){let d=i[f],g=d.indexOf(a);g>=0&&d.splice(g,1)}},close:function(f,a){n=!0,r&&(clearTimeout(r),r=null),o&&o.close(f||1e3,a||""),l.status="closed"}};function p(f){if(typeof f!="string")return f;try{return JSON.parse(f)}catch{return f}}function m(){!e.reconnect||u>=e.maxReconnectAttempts||(u++,l.status="reconnecting",r=setTimeout(function(){r=null,w()},c),c=Math.min(c*2,e.maxReconnectDelay))}function w(){l.status=u>0?"reconnecting":"connecting";try{o=new WebSocket(t,e.protocols)}catch{l.status="closed";return}o.onopen=function(){l.status="open",u=0,c=e.reconnectDelay,e.onOpen();for(let f of i.open)f()},o.onmessage=function(f){let a=p(f.data);for(let d of i.message)d(a)},o.onclose=function(f){l.status="closed",e.onClose(f.code,f.reason);for(let a of i.close)a(f.code,f.reason);n||m()},o.onerror=function(f){e.onError(f);for(let a of i.error)a(f)}}return w(),l}function I(t,s){let e={reconnect:!0,reconnectDelay:1e3,maxReconnectDelay:3e4,maxReconnectAttempts:1/0,events:[],json:!0,onOpen:function(){},onClose:function(){},onError:function(){},...s||{}},o=null,n=!1,c=e.reconnectDelay,u=0,r=null,i={message:[],open:[],close:[],error:[]},l={status:"connecting",on:function(a,d){return i[a]||(i[a]=[]),i[a].push(d),function(){let g=i[a],T=g.indexOf(d);T>=0&&g.splice(T,1)}},close:function(){n=!0,r&&(clearTimeout(r),r=null),o&&(o.close(),o=null),l.status="closed"}};function p(a){if(!e.json)return a;try{return JSON.parse(a)}catch{return a}}function m(a,d){for(let g of i.message)g(a,d||void 0)}function w(){!e.reconnect||u>=e.maxReconnectAttempts||(u++,l.status="reconnecting",r=setTimeout(function(){r=null,f()},c),c=Math.min(c*2,e.maxReconnectDelay))}function f(){l.status=u>0?"reconnecting":"connecting";try{o=new EventSource(t)}catch{l.status="closed";return}o.onopen=function(){l.status="open",u=0,c=e.reconnectDelay,e.onOpen();for(let a of i.open)a(null)},o.onmessage=function(a){m(p(a.data),null)};for(let a of e.events)o.addEventListener(a,function(d){m(p(d.data),a)});o.onerror=function(a){e.onError(a);for(let d of i.error)d(a);if(o&&o.readyState===2){o=null,l.status="closed",e.onClose();for(let d of i.close)d(null);n||w()}}}return f(),l}var U={set:function(t,s,e){let o="";if(e){let n=new Date;n.setTime(n.getTime()+e*24*60*60*1e3),o="; expires="+n.toUTCString()}document.cookie=t+"="+(s||"")+o+"; path=/"},get:function(t){let s=t+"=",e=document.cookie.split(";");for(let o=0;o<e.length;o++){let n=e[o];for(;n.charAt(0)===" ";)n=n.substring(1);if(n.indexOf(s)===0)return n.substring(s.length)}return null},remove:function(t){document.cookie=t+"=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/"}};function j(t,s){let e=document.getElementById("message");if(!e)return;let o=s||"info";e.innerHTML='<div class="alert alert-'+o+' alert-dismissible">'+t+'<button type="button" class="btn-close" data-t4-dismiss="alert">×</button></div>'}function B(t,s,e,o){let n=window.screenLeft!==void 0?window.screenLeft:window.screenX,c=window.screenTop!==void 0?window.screenTop:window.screenY,u=window.innerWidth||document.documentElement.clientWidth||screen.width,r=window.innerHeight||document.documentElement.clientHeight||screen.height,i=u/window.screen.availWidth,l=(u-e)/2/i+n,p=(r-o)/2/i+c,m=window.open(t,s,"directories=no,toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes,width="+e/i+",height="+o/i+",top="+p+",left="+l);return window.focus&&m&&m.focus(),m}function P(t){if(t.indexOf("No data available")>=0){window.alert("No data available for this report.");return}window.open(t,"_blank","toolbar=no,scrollbars=yes,resizable=yes,width=800,height=600,top=0,left=0")}function F(t,s,e,o){v(t,{method:"POST",body:{query:s,variables:e||{}},onSuccess:function(n){o&&o(n.data||null,n.errors||void 0)},onError:function(n){o&&o(null,[{message:"GraphQL request failed with status "+n}])}})}function b(t){let s=t.dataset;return s&&s.key?s.key:null}function X(t,s){let e=s.attributes;for(let n=0;n<e.length;n++){let c=e[n];t.getAttribute(c.name)!==c.value&&t.setAttribute(c.name,c.value)}Array.prototype.slice.call(t.attributes).forEach(function(n){s.hasAttribute(n.name)||t.removeAttribute(n.name)})}function G(t,s){let e=s.tagName;e==="INPUT"||e==="TEXTAREA"||e==="SELECT"||(X(t,s),t.children.length||s.children.length?C(t,s):t.innerHTML!==s.innerHTML&&(t.innerHTML=s.innerHTML))}function C(t,s){let e=Array.prototype.slice.call(t.children),o=Array.prototype.slice.call(s.children),n={};e.forEach(function(r){let i=b(r);i&&(n[i]=r)});let c=[];for(let r=0;r<o.length;r++){let i=o[r],l=b(i),p=null;l&&n[l]?p=n[l]:!l&&e[r]&&!b(e[r])&&e[r].tagName===i.tagName&&(p=e[r]),p&&p.tagName===i.tagName?(G(p,i),reused.push(p),c.push(p)):c.push(i)}let u=t.firstElementChild;for(let r=0;r<c.length;r++){let i=c[r];i===u?u=u.nextElementSibling:t.insertBefore(i,u)}e.forEach(function(r){c.indexOf(r)===-1&&r.parentNode===t&&t.removeChild(r)}),reused}function k(t,s){let e=document.createElement("div");if(e.innerHTML=s,!e.children.length||!t.children.length){t.innerHTML=s;return}C(t,e)}function J(t){return/^wss?:\/\//.test(t)?t:(typeof location<"u"&&location.protocol==="https:"?"wss":"ws")+"://"+location.host+t}function N(t,s){return t&&typeof t=="object"?t.type==="live"?s&&t.name&&t.name!==s?null:t.html!=null?String(t.html):null:null:typeof t=="string"?t:null}function S(t){if(typeof document>"u")return;let e=(t||document).querySelectorAll("[data-frond-live]");Array.prototype.slice.call(e).forEach(function(o){if(o.__frondLive)return;o.__frondLive=!0;let n=o.getAttribute("data-mode"),c=o.getAttribute("data-frond-live");if(n==="poll"){let u=o.getAttribute("data-src"),r=(parseInt(o.getAttribute("data-interval"),10)||5)*1e3,i=setInterval(function(){typeof document<"u"&&document.hidden||v(u,{method:"GET",onSuccess:function(l){k(o,typeof l=="string"?l:String(l))}})},r);o.__frondLiveStop=function(){clearInterval(i)}}else if(n==="ws"){let u=M(J(o.getAttribute("data-ws")));u.on("message",function(r){let i=N(r,c);i!==null&&k(o,i)}),o.__frondLiveStop=function(){u.close()}}else n==="sse"&&typeof console<"u"&&console.warn&&console.warn("[frond.live] sse transport is not wired yet (v1 supports poll and ws); block '"+c+"' shows first paint only. Use poll or ws.")})}var L={request:v,load:_,post:R,inject:h,form:x,ws:M,sse:I,live:S,cookie:U,message:j,popup:B,report:P,graphql:F,get token(){return y},set token(t){y=t}};typeof window<"u"&&(window.frond=L,typeof document<"u"&&(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){S()}):S()));return W(z);})();
|
|
2
|
+
/* Frond v2.2.0 - tina4.com */
|
|
@@ -348,7 +348,13 @@ def _eval_math(scss: str) -> str:
|
|
|
348
348
|
|
|
349
349
|
|
|
350
350
|
def _resolve_color_functions(scss: str, variables: dict) -> str:
|
|
351
|
-
"""Resolve lighten(), darken(), rgba() functions.
|
|
351
|
+
"""Resolve lighten(), darken(), rgba()/rgb(), and mix() color functions.
|
|
352
|
+
|
|
353
|
+
``rgba(<hex>, <alpha>)`` is the damaging case (issue #124): the functional
|
|
354
|
+
``rgba()`` notation cannot take a hex, so ``rgba(#0f3460, 0.12)`` is invalid
|
|
355
|
+
CSS and browsers drop the whole declaration. We convert the hex to its
|
|
356
|
+
``r, g, b`` components so the output is valid ``rgba(15, 52, 96, 0.12)``.
|
|
357
|
+
"""
|
|
352
358
|
def _lighten(m):
|
|
353
359
|
color = m.group(1).strip()
|
|
354
360
|
amount = float(m.group(2).strip().rstrip("%")) / 100
|
|
@@ -359,11 +365,53 @@ def _resolve_color_functions(scss: str, variables: dict) -> str:
|
|
|
359
365
|
amount = float(m.group(2).strip().rstrip("%")) / 100
|
|
360
366
|
return _adjust_lightness(color, -amount)
|
|
361
367
|
|
|
368
|
+
def _rgba(m):
|
|
369
|
+
rgb = _hex_to_rgb(m.group(1))
|
|
370
|
+
if rgb is None:
|
|
371
|
+
return m.group(0) # not a hex colour — leave verbatim
|
|
372
|
+
r, g, b = rgb
|
|
373
|
+
return f"rgba({r}, {g}, {b}, {m.group(2).strip()})"
|
|
374
|
+
|
|
375
|
+
def _rgb(m):
|
|
376
|
+
rgb = _hex_to_rgb(m.group(1))
|
|
377
|
+
if rgb is None:
|
|
378
|
+
return m.group(0)
|
|
379
|
+
r, g, b = rgb
|
|
380
|
+
return f"rgb({r}, {g}, {b})"
|
|
381
|
+
|
|
382
|
+
def _mix(m):
|
|
383
|
+
c1, c2 = _hex_to_rgb(m.group(1)), _hex_to_rgb(m.group(2))
|
|
384
|
+
if c1 is None or c2 is None:
|
|
385
|
+
return m.group(0)
|
|
386
|
+
w = (float(m.group(3).strip().rstrip("%")) / 100) if m.group(3) else 0.5
|
|
387
|
+
mixed = tuple(round(a * w + b * (1 - w)) for a, b in zip(c1, c2))
|
|
388
|
+
return f"#{mixed[0]:02x}{mixed[1]:02x}{mixed[2]:02x}"
|
|
389
|
+
|
|
362
390
|
scss = re.sub(r'lighten\(\s*([^,]+)\s*,\s*([^)]+)\s*\)', _lighten, scss)
|
|
363
391
|
scss = re.sub(r'darken\(\s*([^,]+)\s*,\s*([^)]+)\s*\)', _darken, scss)
|
|
392
|
+
# rgba(<hex>, <alpha>) — only the two-arg hex form; leave rgba(r,g,b,a) alone.
|
|
393
|
+
scss = re.sub(r'rgba\(\s*(#[0-9a-fA-F]{3,8})\s*,\s*([\d.]+)\s*\)', _rgba, scss)
|
|
394
|
+
scss = re.sub(r'rgb\(\s*(#[0-9a-fA-F]{3,8})\s*\)', _rgb, scss)
|
|
395
|
+
# mix(<c1>, <c2>[, <weight>]) — Sass weight is c1's proportion (default 50%).
|
|
396
|
+
scss = re.sub(
|
|
397
|
+
r'mix\(\s*(#[0-9a-fA-F]{3,8})\s*,\s*(#[0-9a-fA-F]{3,8})\s*(?:,\s*([\d.]+%?)\s*)?\)',
|
|
398
|
+
_mix, scss)
|
|
364
399
|
return scss
|
|
365
400
|
|
|
366
401
|
|
|
402
|
+
def _hex_to_rgb(color: str):
|
|
403
|
+
"""Parse a #rgb / #rrggbb hex string into an (r, g, b) int tuple, or None."""
|
|
404
|
+
color = color.strip().lstrip("#")
|
|
405
|
+
if len(color) == 3:
|
|
406
|
+
color = "".join(c * 2 for c in color)
|
|
407
|
+
if len(color) != 6:
|
|
408
|
+
return None
|
|
409
|
+
try:
|
|
410
|
+
return int(color[0:2], 16), int(color[2:4], 16), int(color[4:6], 16)
|
|
411
|
+
except ValueError:
|
|
412
|
+
return None
|
|
413
|
+
|
|
414
|
+
|
|
367
415
|
def _adjust_lightness(color: str, amount: float) -> str:
|
|
368
416
|
"""Adjust the lightness of a hex color."""
|
|
369
417
|
color = color.strip().lstrip("#")
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
# Tina4 Frond — Zero-dependency template engine.
|
|
2
|
-
"""
|
|
3
|
-
Twig-like template engine built from scratch.
|
|
4
|
-
|
|
5
|
-
from tina4_python.frond import Frond
|
|
6
|
-
|
|
7
|
-
engine = Frond(template_dir="src/templates")
|
|
8
|
-
output = engine.render("page.html", {"name": "World"})
|
|
9
|
-
"""
|
|
10
|
-
from tina4_python.frond.engine import Frond
|
|
11
|
-
|
|
12
|
-
__all__ = ["Frond"]
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
var _frondModule=(()=>{var b=Object.defineProperty;var k=Object.getOwnPropertyDescriptor;var x=Object.getOwnPropertyNames;var C=Object.prototype.hasOwnProperty;var O=(o,s)=>{for(var e in s)b(o,e,{get:s[e],enumerable:!0})},M=(o,s,e,t)=>{if(s&&typeof s=="object"||typeof s=="function")for(let n of x(s))!C.call(o,n)&&n!==e&&b(o,n,{get:()=>s[n],enumerable:!(t=k(s,n))||t.enumerable});return o};var q=o=>M(b({},"__esModule",{value:!0}),o);var j={};O(j,{frond:()=>R});var g=null;function w(o,s){let e;typeof s=="function"?e={onSuccess:s}:e=s||{};let t=(e.method||"GET").toUpperCase(),n=new XMLHttpRequest;if(n.open(t,o,!0),g!==null&&n.setRequestHeader("Authorization","Bearer "+g),e.headers)for(let r in e.headers)Object.prototype.hasOwnProperty.call(e.headers,r)&&n.setRequestHeader(r,e.headers[r]);let i=null;e.body!==void 0&&e.body!==null&&(e.body instanceof FormData?i=e.body:typeof e.body=="object"?(i=JSON.stringify(e.body),n.setRequestHeader("Content-Type","application/json; charset=UTF-8")):typeof e.body=="string"&&(i=e.body,n.setRequestHeader("Content-Type","text/plain; charset=UTF-8"))),n.onload=function(){let r=n.getResponseHeader("FreshToken");r&&r!==""&&(g=r);let u=n.response;try{u=JSON.parse(u)}catch{}if(n.responseURL){let c=new URL(o,window.location.href).href;if(n.responseURL!==c){window.location.href=n.responseURL;return}}n.status>=200&&n.status<400?e.onSuccess&&e.onSuccess(u,n.status,n):e.onError&&e.onError(n.status,n)},n.onerror=function(){e.onError&&e.onError(n.status,n)},n.send(i)}function h(o,s){if(!o)return"";let e=new DOMParser,t=o.includes("<html>")?o:"<body>"+o+"</body></html>",i=e.parseFromString(t,"text/html").querySelector("body"),r=i.querySelectorAll("script");if(r.forEach(function(u){u.remove()}),s!==null){let u=document.getElementById(s);return u&&(i.children.length>0?u.replaceChildren.apply(u,Array.from(i.children)):u.innerHTML=i.innerHTML,r.forEach(function(c){let d=document.createElement("script");d.type="text/javascript",d.async=!0,c.src?d.src=c.src:d.textContent=c.textContent,u.appendChild(d)})),""}return r.forEach(function(u){let c=document.createElement("script");c.type="text/javascript",c.async=!0,c.textContent=u.textContent,document.body.appendChild(c)}),i.innerHTML}function H(o,s,e){let t=s||"content";w(o,{method:"GET",onSuccess:function(n,i){if(document.getElementById(t)){let r=h(n,t);e&&e(r,n)}else e&&e(n)}})}function S(o,s,e,t){let n=e||"content";w(o,{method:"POST",body:s,onSuccess:function(i){let r="";if(i&&i.message!==void 0)r=h(i.message,n);else if(document.getElementById(n))r=h(i,n);else{t&&t(i);return}t&&t(r,i)}})}var T={collect:function(o){let s=new FormData,e=document.querySelectorAll("#"+o+" select, #"+o+" input, #"+o+" textarea");for(let t=0;t<e.length;t++){let n=e[t];if(n.name==="formToken"&&g!==null&&(n.value=g),!!n.name)if(n.type==="file"){let i=n.files;if(i)for(let r=0;r<i.length;r++){let u=i[r];if(u!==void 0){let c=n.name;i.length>1&&!c.includes("[")&&(c=c+"[]"),s.append(c,u,u.name)}}}else n.type==="checkbox"||n.type==="radio"?n.checked?s.append(n.name,n.value):n.type!=="radio"&&s.append(n.name,"0"):s.append(n.name,n.value===""?"":n.value)}return s},submit:function(o,s,e,t){let n=T.collect(o);S(s,n,e||"message",t)},show:function(o,s,e,t){let n=o.toUpperCase();(o==="create"||o==="edit")&&(n="GET"),o==="delete"&&(n="DELETE");let i=e||"form";w(s,{method:n,onSuccess:function(r){let u="";if(r&&r.message!==void 0)u=h(r.message,i);else if(document.getElementById(i))u=h(r,i);else{t&&t(r);return}t&&t(u)}})}};function L(o,s){let e={reconnect:!0,reconnectDelay:1e3,maxReconnectDelay:3e4,maxReconnectAttempts:1/0,protocols:[],onOpen:function(){},onClose:function(){},onError:function(){},...s||{}},t=null,n=!1,i=e.reconnectDelay,r=0,u=null,c={message:[],open:[],close:[],error:[]},d={status:"connecting",send:function(l){if(!t||t.readyState!==WebSocket.OPEN)throw new Error("[frond] WebSocket is not connected");t.send(typeof l=="string"?l:JSON.stringify(l))},on:function(l,a){return c[l]||(c[l]=[]),c[l].push(a),function(){let f=c[l],m=f.indexOf(a);m>=0&&f.splice(m,1)}},close:function(l,a){n=!0,u&&(clearTimeout(u),u=null),t&&t.close(l||1e3,a||""),d.status="closed"}};function y(l){if(typeof l!="string")return l;try{return JSON.parse(l)}catch{return l}}function p(){!e.reconnect||r>=e.maxReconnectAttempts||(r++,d.status="reconnecting",u=setTimeout(function(){u=null,v()},i),i=Math.min(i*2,e.maxReconnectDelay))}function v(){d.status=r>0?"reconnecting":"connecting";try{t=new WebSocket(o,e.protocols)}catch{d.status="closed";return}t.onopen=function(){d.status="open",r=0,i=e.reconnectDelay,e.onOpen();for(let l of c.open)l()},t.onmessage=function(l){let a=y(l.data);for(let f of c.message)f(a)},t.onclose=function(l){d.status="closed",e.onClose(l.code,l.reason);for(let a of c.close)a(l.code,l.reason);n||p()},t.onerror=function(l){e.onError(l);for(let a of c.error)a(l)}}return v(),d}function D(o,s){let e={reconnect:!0,reconnectDelay:1e3,maxReconnectDelay:3e4,maxReconnectAttempts:1/0,events:[],json:!0,onOpen:function(){},onClose:function(){},onError:function(){},...s||{}},t=null,n=!1,i=e.reconnectDelay,r=0,u=null,c={message:[],open:[],close:[],error:[]},d={status:"connecting",on:function(a,f){return c[a]||(c[a]=[]),c[a].push(f),function(){let m=c[a],E=m.indexOf(f);E>=0&&m.splice(E,1)}},close:function(){n=!0,u&&(clearTimeout(u),u=null),t&&(t.close(),t=null),d.status="closed"}};function y(a){if(!e.json)return a;try{return JSON.parse(a)}catch{return a}}function p(a,f){for(let m of c.message)m(a,f||void 0)}function v(){!e.reconnect||r>=e.maxReconnectAttempts||(r++,d.status="reconnecting",u=setTimeout(function(){u=null,l()},i),i=Math.min(i*2,e.maxReconnectDelay))}function l(){d.status=r>0?"reconnecting":"connecting";try{t=new EventSource(o)}catch{d.status="closed";return}t.onopen=function(){d.status="open",r=0,i=e.reconnectDelay,e.onOpen();for(let a of c.open)a(null)},t.onmessage=function(a){p(y(a.data),null)};for(let a of e.events)t.addEventListener(a,function(f){p(y(f.data),a)});t.onerror=function(a){e.onError(a);for(let f of c.error)f(a);if(t&&t.readyState===2){t=null,d.status="closed",e.onClose();for(let f of c.close)f(null);n||v()}}}return l(),d}var W={set:function(o,s,e){let t="";if(e){let n=new Date;n.setTime(n.getTime()+e*24*60*60*1e3),t="; expires="+n.toUTCString()}document.cookie=o+"="+(s||"")+t+"; path=/"},get:function(o){let s=o+"=",e=document.cookie.split(";");for(let t=0;t<e.length;t++){let n=e[t];for(;n.charAt(0)===" ";)n=n.substring(1);if(n.indexOf(s)===0)return n.substring(s.length)}return null},remove:function(o){document.cookie=o+"=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/"}};function A(o,s){let e=document.getElementById("message");if(!e)return;let t=s||"info";e.innerHTML='<div class="alert alert-'+t+' alert-dismissible">'+o+'<button type="button" class="btn-close" data-t4-dismiss="alert">×</button></div>'}function I(o,s,e,t){let n=window.screenLeft!==void 0?window.screenLeft:window.screenX,i=window.screenTop!==void 0?window.screenTop:window.screenY,r=window.innerWidth||document.documentElement.clientWidth||screen.width,u=window.innerHeight||document.documentElement.clientHeight||screen.height,c=r/window.screen.availWidth,d=(r-e)/2/c+n,y=(u-t)/2/c+i,p=window.open(o,s,"directories=no,toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes,width="+e/c+",height="+t/c+",top="+y+",left="+d);return window.focus&&p&&p.focus(),p}function N(o){if(o.indexOf("No data available")>=0){window.alert("No data available for this report.");return}window.open(o,"_blank","toolbar=no,scrollbars=yes,resizable=yes,width=800,height=600,top=0,left=0")}function U(o,s,e,t){w(o,{method:"POST",body:{query:s,variables:e||{}},onSuccess:function(n){t&&t(n.data||null,n.errors||void 0)},onError:function(n){t&&t(null,[{message:"GraphQL request failed with status "+n}])}})}var R={request:w,load:H,post:S,inject:h,form:T,ws:L,sse:D,cookie:W,message:A,popup:I,report:N,graphql:U,get token(){return g},set token(o){g=o}};typeof window<"u"&&(window.frond=R);return q(j);})();
|
|
2
|
-
/* Frond v2.1.3 — tina4.com */
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|