tina4-python 3.13.24__tar.gz → 3.13.27__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.
Files changed (157) hide show
  1. {tina4_python-3.13.24 → tina4_python-3.13.27}/PKG-INFO +1 -1
  2. {tina4_python-3.13.24 → tina4_python-3.13.27}/pyproject.toml +1 -1
  3. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/__init__.py +1 -1
  4. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/database/adapter.py +7 -3
  5. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/frond/engine.py +62 -11
  6. {tina4_python-3.13.24 → tina4_python-3.13.27}/.gitignore +0 -0
  7. {tina4_python-3.13.24 → tina4_python-3.13.27}/README.md +0 -0
  8. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/CLAUDE.md +0 -0
  9. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/HtmlElement.py +0 -0
  10. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/Testing.py +0 -0
  11. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/ai/__init__.py +0 -0
  12. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/api/__init__.py +0 -0
  13. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/auth/__init__.py +0 -0
  14. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/cache/__init__.py +0 -0
  15. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/cli/__init__.py +0 -0
  16. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/container/__init__.py +0 -0
  17. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/core/__init__.py +0 -0
  18. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/core/cache.py +0 -0
  19. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/core/constants.py +0 -0
  20. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/core/events.py +0 -0
  21. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/core/middleware.py +0 -0
  22. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/core/rate_limiter.py +0 -0
  23. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/core/request.py +0 -0
  24. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/core/response.py +0 -0
  25. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/core/router.py +0 -0
  26. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/core/server.py +0 -0
  27. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/crud/__init__.py +0 -0
  28. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/database/__init__.py +0 -0
  29. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/database/connection.py +0 -0
  30. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/database/firebird.py +0 -0
  31. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/database/mongodb.py +0 -0
  32. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/database/mssql.py +0 -0
  33. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/database/mysql.py +0 -0
  34. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/database/odbc.py +0 -0
  35. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/database/postgres.py +0 -0
  36. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/database/sqlite.py +0 -0
  37. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/debug/__init__.py +0 -0
  38. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/debug/error_overlay.py +0 -0
  39. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/dev_admin/__init__.py +0 -0
  40. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/dev_admin/metrics.py +0 -0
  41. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/dev_admin/plan.py +0 -0
  42. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/dev_admin/project_index.py +0 -0
  43. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/docs.py +0 -0
  44. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/dotenv/__init__.py +0 -0
  45. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/env.py +0 -0
  46. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/frond/FROND.md +0 -0
  47. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/frond/__init__.py +0 -0
  48. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/gallery/auth/meta.json +0 -0
  49. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  50. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/gallery/database/meta.json +0 -0
  51. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  52. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/gallery/error-overlay/meta.json +0 -0
  53. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  54. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/gallery/orm/meta.json +0 -0
  55. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  56. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  57. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/gallery/queue/meta.json +0 -0
  58. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  59. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/gallery/rest-api/meta.json +0 -0
  60. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  61. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/gallery/templates/meta.json +0 -0
  62. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  63. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  64. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/graphql/__init__.py +0 -0
  65. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/i18n/__init__.py +0 -0
  66. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/mcp/__init__.py +0 -0
  67. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/mcp/protocol.py +0 -0
  68. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/mcp/tools.py +0 -0
  69. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/messenger/__init__.py +0 -0
  70. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/migration/__init__.py +0 -0
  71. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/migration/runner.py +0 -0
  72. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/orm/__init__.py +0 -0
  73. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/orm/fields.py +0 -0
  74. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/orm/model.py +0 -0
  75. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/public/__feedback/widget.js +0 -0
  76. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/public/css/tina4.css +0 -0
  77. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/public/css/tina4.min.css +0 -0
  78. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/public/favicon.ico +0 -0
  79. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/public/images/logo.svg +0 -0
  80. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  81. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/public/js/frond.js +0 -0
  82. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/public/js/frond.min.js +0 -0
  83. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/public/js/tina4-dev-admin.js +0 -0
  84. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  85. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/public/js/tina4.min.js +0 -0
  86. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/public/js/tina4js.min.js +0 -0
  87. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/public/swagger/index.html +0 -0
  88. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  89. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/query_builder/__init__.py +0 -0
  90. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/queue/__init__.py +0 -0
  91. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/queue/job.py +0 -0
  92. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/queue/kafka_backend.py +0 -0
  93. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/queue/lite_backend.py +0 -0
  94. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/queue/mongo_backend.py +0 -0
  95. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/queue/rabbitmq_backend.py +0 -0
  96. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/queue_backends/__init__.py +0 -0
  97. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/queue_backends/kafka_backend.py +0 -0
  98. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/queue_backends/mongo_backend.py +0 -0
  99. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  100. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/scss/__init__.py +0 -0
  101. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  102. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/scss/tina4css/_badges.scss +0 -0
  103. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  104. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/scss/tina4css/_cards.scss +0 -0
  105. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/scss/tina4css/_forms.scss +0 -0
  106. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/scss/tina4css/_grid.scss +0 -0
  107. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/scss/tina4css/_modals.scss +0 -0
  108. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/scss/tina4css/_nav.scss +0 -0
  109. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/scss/tina4css/_reset.scss +0 -0
  110. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/scss/tina4css/_tables.scss +0 -0
  111. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/scss/tina4css/_typography.scss +0 -0
  112. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  113. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/scss/tina4css/_variables.scss +0 -0
  114. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/scss/tina4css/base.scss +0 -0
  115. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/scss/tina4css/colors.scss +0 -0
  116. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/scss/tina4css/tina4.scss +0 -0
  117. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/seeder/__init__.py +0 -0
  118. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/service/__init__.py +0 -0
  119. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/session/__init__.py +0 -0
  120. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/session_handlers/__init__.py +0 -0
  121. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  122. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/session_handlers/redis_handler.py +0 -0
  123. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/session_handlers/valkey_handler.py +0 -0
  124. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/swagger/__init__.py +0 -0
  125. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/templates/components/crud.twig +0 -0
  126. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  127. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  128. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/templates/docker/python/Dockerfile +0 -0
  129. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  130. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/templates/errors/302.twig +0 -0
  131. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/templates/errors/401.twig +0 -0
  132. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/templates/errors/403.twig +0 -0
  133. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/templates/errors/404.twig +0 -0
  134. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/templates/errors/500.twig +0 -0
  135. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/templates/errors/502.twig +0 -0
  136. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/templates/errors/503.twig +0 -0
  137. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/templates/errors/base.twig +0 -0
  138. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/templates/frontend/README.md +0 -0
  139. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/templates/readme.md +0 -0
  140. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/test/__init__.py +0 -0
  141. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/test_client/__init__.py +0 -0
  142. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  143. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  144. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  145. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  146. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  147. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  148. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  149. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  150. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  151. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  152. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  153. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  154. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/validator/__init__.py +0 -0
  155. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/websocket/__init__.py +0 -0
  156. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/websocket/backplane.py +0 -0
  157. {tina4_python-3.13.24 → tina4_python-3.13.27}/tina4_python/wsdl/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tina4-python
3
- Version: 3.13.24
3
+ Version: 3.13.27
4
4
  Summary: Tina4 Python v3 — Zero-dependency, lightweight web framework
5
5
  Author-email: Andre van Zuydam <andrevanzuydam@gmail.com>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tina4-python"
3
- version = "3.13.24"
3
+ version = "3.13.27"
4
4
  description = "Tina4 Python v3 — Zero-dependency, lightweight web framework"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam", email = "andrevanzuydam@gmail.com"}
@@ -8,7 +8,7 @@ Tina4 Python v3.0 — Zero-dependency, lightweight web framework.
8
8
 
9
9
  One import, everything works.
10
10
  """
11
- __version__ = "3.13.24"
11
+ __version__ = "3.13.27"
12
12
 
13
13
  # ── Route decorators ──
14
14
  from tina4_python.core.router import ( # noqa: E402, F401
@@ -308,14 +308,18 @@ class DatabaseAdapter:
308
308
  Every method raises NotImplementedError — drivers must implement all of them.
309
309
  The interface is deliberately minimal: 13 methods cover everything.
310
310
 
311
- Autocommit is OFF by default. Set TINA4_AUTOCOMMIT=true in .env to enable.
312
- Without autocommit, you must call commit() explicitly after write operations.
311
+ Autocommit is ON by default: a standalone write (execute/insert/update/delete
312
+ made outside an explicit transaction) commits on its own connection before
313
+ returning. Inside start_transaction()/commit()/rollback() the commit is
314
+ deferred — the per-statement commit branches are gated on `not self._in_transaction`
315
+ so explicit transactions stay atomic. Set TINA4_AUTOCOMMIT=false in .env for
316
+ strict manual mode (every write needs an explicit commit()).
313
317
  """
314
318
 
315
319
  def __init__(self):
316
320
  import os
317
321
  self._autocommit = os.environ.get(
318
- "TINA4_AUTOCOMMIT", "false"
322
+ "TINA4_AUTOCOMMIT", "true"
319
323
  ).lower() in ("true", "1", "yes")
320
324
 
321
325
  @property
@@ -989,9 +989,26 @@ def _coerce_arg(s: str):
989
989
  return s
990
990
 
991
991
 
992
+ class _Ref:
993
+ """A filter argument that was an UNQUOTED bareword — i.e. a variable
994
+ reference (or dotted/bracket path), not a literal. Resolved against the
995
+ render context at apply-time so e.g. `{{ '%.2f' | format(price) }}` binds
996
+ `price` to its value. Quoted literals (`default('fb')`) are kept as plain
997
+ strings and never resolved."""
998
+
999
+ __slots__ = ("name",)
1000
+
1001
+ def __init__(self, name: str):
1002
+ self.name = name
1003
+
1004
+
992
1005
  def _parse_args(raw: str) -> list:
993
- """Parse filter arguments, respecting quoted strings, braces, and backslash escapes."""
994
- args = []
1006
+ """Parse filter arguments, respecting quoted strings, braces, and backslash escapes.
1007
+
1008
+ Quoted args become literal strings; numbers/bools/null/JSON are coerced to
1009
+ their Python types; an unquoted bareword becomes a `_Ref` (a variable
1010
+ reference resolved against the context when the filter runs)."""
1011
+ tokens = []
995
1012
  current = ""
996
1013
  in_quote = None
997
1014
  depth = 0
@@ -1006,15 +1023,27 @@ def _parse_args(raw: str) -> list:
1006
1023
  elif ch in (")", "}", "]") and not in_quote:
1007
1024
  depth -= 1
1008
1025
  elif ch == "," and not in_quote and depth == 0:
1009
- args.append(_strip_outer_quotes(current.strip()))
1026
+ tokens.append(current.strip())
1010
1027
  current = ""
1011
1028
  continue
1012
1029
  current += ch
1013
1030
 
1014
1031
  if current.strip():
1015
- args.append(_strip_outer_quotes(current.strip()))
1032
+ tokens.append(current.strip())
1016
1033
 
1017
- return [_coerce_arg(a) for a in args]
1034
+ out = []
1035
+ for tok in tokens:
1036
+ if len(tok) >= 2 and tok[0] == tok[-1] and tok[0] in ('"', "'"):
1037
+ out.append(_strip_outer_quotes(tok)) # quoted literal → string
1038
+ else:
1039
+ coerced = _coerce_arg(tok)
1040
+ # A bareword that stayed a string is a variable/expression
1041
+ # reference (e.g. `n`, `user.age`) — resolve it at apply-time.
1042
+ if isinstance(coerced, str) and tok:
1043
+ out.append(_Ref(tok))
1044
+ else:
1045
+ out.append(coerced)
1046
+ return out
1018
1047
 
1019
1048
 
1020
1049
  def _strip_outer_quotes(s: str) -> str:
@@ -1071,16 +1100,16 @@ _BUILTIN_FILTERS = {
1071
1100
  "default": lambda v, *a: v if v is not None and v != "" else (a[0] if a else ""),
1072
1101
  "raw": lambda v, *a: v, # Mark as safe (no escaping)
1073
1102
  "safe": lambda v, *a: v,
1074
- "escape": lambda v, *a: html.escape(str(v)),
1075
- "e": lambda v, *a: html.escape(str(v)),
1103
+ "escape": lambda v, *a: SafeString(html.escape(str(v))),
1104
+ "e": lambda v, *a: SafeString(html.escape(str(v))),
1076
1105
  "striptags": lambda v, *a: _STRIPTAGS_RE.sub("", str(v)),
1077
- "nl2br": lambda v, *a: str(v).replace("\n", "<br>\n"),
1106
+ "nl2br": lambda v, *a: SafeString(html.escape(str(v)).replace("\n", "<br />\n")),
1078
1107
  "abs": lambda v, *a: abs(v) if isinstance(v, (int, float)) else v,
1079
- "round": lambda v, *a: round(float(v), int(a[0]) if a else 0),
1108
+ "round": lambda v, *a: round(float(v), int(a[0])) if a else int(round(float(v))),
1080
1109
  "int": lambda v, *a: int(v) if v else 0,
1081
1110
  "float": lambda v, *a: float(v) if v else 0.0,
1082
1111
  "string": lambda v, *a: str(v),
1083
- "json_encode": lambda v, *a: json.dumps(v),
1112
+ "json_encode": lambda v, *a: json.dumps(v, separators=(",", ":")),
1084
1113
  "to_json": lambda v, *a: SafeString(json.dumps(v, default=str, separators=(",", ":")).replace("<", "\\u003c").replace(">", "\\u003e").replace("&", "\\u0026")),
1085
1114
  "tojson": lambda v, *a: SafeString(json.dumps(v, default=str, separators=(",", ":")).replace("<", "\\u003c").replace(">", "\\u003e").replace("&", "\\u0026")),
1086
1115
  "js_escape": lambda v, *a: SafeString(str(v).replace("\\", "\\\\").replace("'", "\\'").replace('"', '\\"').replace("\n", "\\n").replace("\r", "\\r")),
@@ -1633,6 +1662,21 @@ class Frond:
1633
1662
 
1634
1663
  def _render_tokens(self, tokens: list, context: dict) -> str:
1635
1664
  """Render a list of tokens to string."""
1665
+ # Whitespace control pre-pass: every {{- / -}} and {%- / -%} marker
1666
+ # trims its neighbouring TEXT token. Doing this up-front over the whole
1667
+ # token list (not per-handler) means the markers on closing tags
1668
+ # (endif/endfor) and the body boundaries are honoured too — a block tag
1669
+ # consumes its own end tag during dispatch, so the inline pass below
1670
+ # never sees it. Idempotent, so recursion into block bodies is safe.
1671
+ for _idx in range(len(tokens)):
1672
+ _tt, _raw = tokens[_idx]
1673
+ if _tt in (VAR, BLOCK):
1674
+ _, _sb, _sa = _strip_tag(_raw)
1675
+ if _sb and _idx > 0 and tokens[_idx - 1][0] == TEXT:
1676
+ tokens[_idx - 1] = (TEXT, tokens[_idx - 1][1].rstrip())
1677
+ if _sa and _idx + 1 < len(tokens) and tokens[_idx + 1][0] == TEXT:
1678
+ tokens[_idx + 1] = (TEXT, tokens[_idx + 1][1].lstrip())
1679
+
1636
1680
  output = []
1637
1681
  i = 0
1638
1682
 
@@ -1835,6 +1879,13 @@ class Frond:
1835
1879
  is_safe = True
1836
1880
  continue
1837
1881
 
1882
+ # Resolve any unquoted-bareword args against the context (a quoted
1883
+ # literal stays a plain string). Lets `{{ '%.2f' | format(n) }}`
1884
+ # bind `n` to its value instead of the literal text "n".
1885
+ if args and any(isinstance(a, _Ref) for a in args):
1886
+ args = [_eval_expr(a.name, context) if isinstance(a, _Ref) else a
1887
+ for a in args]
1888
+
1838
1889
  # Sandbox: check filter access
1839
1890
  if self._sandbox and self._allowed_filters is not None:
1840
1891
  if fname not in self._allowed_filters:
@@ -1885,7 +1936,7 @@ class Frond:
1885
1936
  value = int(value) if value else 0
1886
1937
  continue
1887
1938
  if fname in ("e", "escape"):
1888
- value = html.escape(str(value))
1939
+ value = SafeString(html.escape(str(value)))
1889
1940
  continue
1890
1941
 
1891
1942
  fn = self._filters.get(fname)
File without changes