tina4-python 3.13.87__tar.gz → 3.13.88__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 (172) hide show
  1. {tina4_python-3.13.87 → tina4_python-3.13.88}/PKG-INFO +1 -1
  2. {tina4_python-3.13.87 → tina4_python-3.13.88}/pyproject.toml +1 -1
  3. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/frond/engine.py +81 -3
  4. {tina4_python-3.13.87 → tina4_python-3.13.88}/.gitignore +0 -0
  5. {tina4_python-3.13.87 → tina4_python-3.13.88}/README.md +0 -0
  6. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/CLAUDE.md +0 -0
  7. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/HtmlElement.py +0 -0
  8. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/Testing.py +0 -0
  9. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/__init__.py +0 -0
  10. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/ai/__init__.py +0 -0
  11. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/api/__init__.py +0 -0
  12. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/auth/__init__.py +0 -0
  13. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/cache/__init__.py +0 -0
  14. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/cli/__init__.py +0 -0
  15. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/container/__init__.py +0 -0
  16. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/context/__init__.py +0 -0
  17. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/context/chunker.py +0 -0
  18. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/core/__init__.py +0 -0
  19. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/core/cache.py +0 -0
  20. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/core/constants.py +0 -0
  21. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/core/events.py +0 -0
  22. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/core/middleware.py +0 -0
  23. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/core/rate_limiter.py +0 -0
  24. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/core/request.py +0 -0
  25. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/core/response.py +0 -0
  26. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/core/router.py +0 -0
  27. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/core/server.py +0 -0
  28. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/crud/__init__.py +0 -0
  29. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/database/__init__.py +0 -0
  30. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/database/adapter.py +0 -0
  31. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/database/connection.py +0 -0
  32. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/database/firebird.py +0 -0
  33. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/database/mongodb.py +0 -0
  34. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/database/mssql.py +0 -0
  35. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/database/mysql.py +0 -0
  36. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/database/odbc.py +0 -0
  37. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/database/postgres.py +0 -0
  38. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/database/sqlite.py +0 -0
  39. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/debug/__init__.py +0 -0
  40. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/debug/error_overlay.py +0 -0
  41. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/dev_admin/__init__.py +0 -0
  42. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/dev_admin/metrics.py +0 -0
  43. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/dev_admin/plan.py +0 -0
  44. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/dev_admin/project_index.py +0 -0
  45. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/docs.py +0 -0
  46. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/docstore/__init__.py +0 -0
  47. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/dotenv/__init__.py +0 -0
  48. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/env.py +0 -0
  49. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/frond/FROND.md +0 -0
  50. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/frond/__init__.py +0 -0
  51. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/frond/compiler.py +0 -0
  52. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/frond/parser.py +0 -0
  53. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/gallery/auth/meta.json +0 -0
  54. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  55. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/gallery/database/meta.json +0 -0
  56. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  57. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/gallery/error-overlay/meta.json +0 -0
  58. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  59. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/gallery/orm/meta.json +0 -0
  60. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  61. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  62. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/gallery/queue/meta.json +0 -0
  63. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  64. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/gallery/rest-api/meta.json +0 -0
  65. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  66. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/gallery/templates/meta.json +0 -0
  67. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  68. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  69. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/graphql/__init__.py +0 -0
  70. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/i18n/__init__.py +0 -0
  71. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/mcp/__init__.py +0 -0
  72. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/mcp/protocol.py +0 -0
  73. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/mcp/tools.py +0 -0
  74. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/messenger/__init__.py +0 -0
  75. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/migration/__init__.py +0 -0
  76. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/migration/runner.py +0 -0
  77. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/mqtt/__init__.py +0 -0
  78. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/mqtt/message.py +0 -0
  79. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/orm/__init__.py +0 -0
  80. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/orm/fields.py +0 -0
  81. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/orm/model.py +0 -0
  82. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/public/__feedback/widget.js +0 -0
  83. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/public/css/tina4.css +0 -0
  84. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/public/css/tina4.min.css +0 -0
  85. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/public/favicon.ico +0 -0
  86. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/public/images/logo.svg +0 -0
  87. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  88. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/public/js/frond.js +0 -0
  89. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/public/js/frond.min.js +0 -0
  90. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  91. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/public/js/tina4.min.js +0 -0
  92. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/public/js/tina4js.min.js +0 -0
  93. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/public/swagger/index.html +0 -0
  94. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  95. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/query_builder/__init__.py +0 -0
  96. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/queue/__init__.py +0 -0
  97. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/queue/job.py +0 -0
  98. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/queue/kafka_backend.py +0 -0
  99. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/queue/lite_backend.py +0 -0
  100. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/queue/mongo_backend.py +0 -0
  101. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/queue/rabbitmq_backend.py +0 -0
  102. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/queue_backends/__init__.py +0 -0
  103. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/queue_backends/kafka_backend.py +0 -0
  104. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/queue_backends/mongo_backend.py +0 -0
  105. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  106. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/realtime/__init__.py +0 -0
  107. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/realtime/models/Attachment.py +0 -0
  108. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/realtime/models/Channel.py +0 -0
  109. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/realtime/models/ChannelMember.py +0 -0
  110. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/realtime/models/Message.py +0 -0
  111. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/realtime/models/Workspace.py +0 -0
  112. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/realtime/models/__init__.py +0 -0
  113. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/realtime/storage.py +0 -0
  114. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/scss/__init__.py +0 -0
  115. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  116. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/scss/tina4css/_badges.scss +0 -0
  117. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  118. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/scss/tina4css/_cards.scss +0 -0
  119. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/scss/tina4css/_forms.scss +0 -0
  120. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/scss/tina4css/_grid.scss +0 -0
  121. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/scss/tina4css/_modals.scss +0 -0
  122. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/scss/tina4css/_nav.scss +0 -0
  123. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/scss/tina4css/_pagination.scss +0 -0
  124. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/scss/tina4css/_reset.scss +0 -0
  125. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/scss/tina4css/_tables.scss +0 -0
  126. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/scss/tina4css/_typography.scss +0 -0
  127. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  128. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/scss/tina4css/_variables.scss +0 -0
  129. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/scss/tina4css/base.scss +0 -0
  130. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/scss/tina4css/colors.scss +0 -0
  131. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/scss/tina4css/tina4.scss +0 -0
  132. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/seeder/__init__.py +0 -0
  133. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/service/__init__.py +0 -0
  134. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/session/__init__.py +0 -0
  135. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/session_handlers/__init__.py +0 -0
  136. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  137. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/session_handlers/redis_handler.py +0 -0
  138. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/session_handlers/valkey_handler.py +0 -0
  139. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/swagger/__init__.py +0 -0
  140. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/templates/components/crud.twig +0 -0
  141. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  142. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  143. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/templates/docker/python/Dockerfile +0 -0
  144. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  145. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/templates/errors/302.twig +0 -0
  146. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/templates/errors/401.twig +0 -0
  147. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/templates/errors/403.twig +0 -0
  148. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/templates/errors/404.twig +0 -0
  149. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/templates/errors/500.twig +0 -0
  150. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/templates/errors/502.twig +0 -0
  151. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/templates/errors/503.twig +0 -0
  152. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/templates/errors/base.twig +0 -0
  153. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/templates/frontend/README.md +0 -0
  154. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/templates/readme.md +0 -0
  155. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/test/__init__.py +0 -0
  156. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/test_client/__init__.py +0 -0
  157. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  158. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  159. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  160. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  161. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  162. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  163. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  164. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  165. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  166. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  167. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  168. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  169. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/validator/__init__.py +0 -0
  170. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/websocket/__init__.py +0 -0
  171. {tina4_python-3.13.87 → tina4_python-3.13.88}/tina4_python/websocket/backplane.py +0 -0
  172. {tina4_python-3.13.87 → tina4_python-3.13.88}/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.87
3
+ Version: 3.13.88
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.87"
3
+ version = "3.13.88"
4
4
  description = "Tina4 Python v3 — Zero-dependency, lightweight web framework"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam", email = "andrevanzuydam@gmail.com"}
@@ -14,6 +14,7 @@ import re
14
14
  import html
15
15
  import hashlib
16
16
  import json
17
+ import math
17
18
  import secrets
18
19
  import time
19
20
  from functools import lru_cache
@@ -1187,6 +1188,83 @@ def _dict_replace(s: str, mapping: dict) -> str:
1187
1188
 
1188
1189
 
1189
1190
  # Built-in filters
1191
+ def _json_safe(value) -> "SafeString":
1192
+ """Serialize to JSON that is valid JSON, valid JavaScript, and safe in HTML.
1193
+
1194
+ THE cross-framework contract for ``json_encode`` / ``to_json`` / ``tojson``.
1195
+ Keep the four implementations byte-identical; ``frond_expression_corpus.txt``
1196
+ locks it.
1197
+
1198
+ Three things this must never do, each of which was a real bug:
1199
+
1200
+ 1. **Never emit a non-finite literal.** ``json.dumps`` writes bare
1201
+ ``Infinity``/``NaN`` by default. Neither is JSON -- ``JSON.parse`` throws
1202
+ -- so the payload only survives when inlined straight into a ``<script>``.
1203
+ The JSON spec has no infinity, so a non-finite number serializes as
1204
+ ``null``, which is what ``JSON.stringify`` has always done. Reported as
1205
+ tina4-php#184 by justin-k-bruce, who found it in production: two ``INF``
1206
+ sort keys in a 657-row grid blanked the whole screen.
1207
+ 2. **Never emit nothing.** A serializer that fails must not collapse to an
1208
+ empty string (or, worse, to something that still parses and means
1209
+ something else). ``var ROWS = ;`` is at least a loud SyntaxError;
1210
+ ``var ROWS = false;`` is valid JS carrying wrong data. Same fail-loud
1211
+ contract the DB layer got: no swallow-to-empty.
1212
+ 3. **Never HTML-escape it.** Entity-encoding JSON produces
1213
+ ``{&quot;a&quot;:1}``, which is a SyntaxError inside ``<script>`` -- it
1214
+ breaks the filter's primary use. Instead escape only the characters that
1215
+ are dangerous in HTML, as JSON ``\\uXXXX`` escapes, which keeps the output
1216
+ valid JSON AND valid JS: ``</script>`` cannot terminate the block, and the
1217
+ result is safe inside a single-quoted attribute. This is exactly what
1218
+ Jinja2's ``tojson`` does, and it is why the result is marked SafeString.
1219
+
1220
+ U+2028 and U+2029 join that escape set. Both are legal inside a JSON string
1221
+ and both were illegal inside a JavaScript string literal before ES2019, so an
1222
+ unescaped one turns a valid payload into a SyntaxError on an older engine.
1223
+
1224
+ ``ensure_ascii=False`` keeps non-ASCII text as itself. PHP, Ruby and Node all
1225
+ emit raw UTF-8; Python was the only one escaping to ``\\uXXXX``, so the four
1226
+ disagreed byte-for-byte on any accented character. Raw wins on ADR-0004 -- it
1227
+ is shorter, readable, and still valid JSON.
1228
+
1229
+ The sanitising walk runs ONLY on the failure path, so a well-formed payload
1230
+ pays nothing beyond the escape pass.
1231
+ """
1232
+ try:
1233
+ # allow_nan=False turns a non-finite into a raised ValueError instead of
1234
+ # a bare Infinity token, so the happy path stays a single dumps() call.
1235
+ text = json.dumps(
1236
+ value, separators=(",", ":"), allow_nan=False, ensure_ascii=False, default=str
1237
+ )
1238
+ except (ValueError, TypeError):
1239
+ text = json.dumps(
1240
+ _json_sanitize(value), separators=(",", ":"), ensure_ascii=False, default=str
1241
+ )
1242
+ return SafeString(
1243
+ text.replace("<", "\\u003c")
1244
+ .replace(">", "\\u003e")
1245
+ .replace("&", "\\u0026")
1246
+ .replace("'", "\\u0027")
1247
+ .replace("\u2028", "\\u2028")
1248
+ .replace("\u2029", "\\u2029")
1249
+ )
1250
+
1251
+
1252
+ def _json_sanitize(value):
1253
+ """Replace anything json.dumps refuses with a JSON-representable stand-in.
1254
+
1255
+ Only reached when the happy path raised. Non-finite floats become ``None``
1256
+ (the JSON spec has no Infinity/NaN); anything else unencodable is left for
1257
+ ``default=str`` to stringify.
1258
+ """
1259
+ if isinstance(value, float):
1260
+ return value if math.isfinite(value) else None
1261
+ if isinstance(value, dict):
1262
+ return {k: _json_sanitize(v) for k, v in value.items()}
1263
+ if isinstance(value, (list, tuple)):
1264
+ return [_json_sanitize(v) for v in value]
1265
+ return value
1266
+
1267
+
1190
1268
  _BUILTIN_FILTERS = {
1191
1269
  "upper": lambda v, *a: str(v).upper(),
1192
1270
  "lower": lambda v, *a: str(v).lower(),
@@ -1220,9 +1298,9 @@ _BUILTIN_FILTERS = {
1220
1298
  "int": lambda v, *a: int(v) if v else 0,
1221
1299
  "float": lambda v, *a: float(v) if v else 0.0,
1222
1300
  "string": lambda v, *a: str(v),
1223
- "json_encode": lambda v, *a: json.dumps(v, separators=(",", ":")),
1224
- "to_json": lambda v, *a: SafeString(json.dumps(v, default=str, separators=(",", ":")).replace("<", "\\u003c").replace(">", "\\u003e").replace("&", "\\u0026")),
1225
- "tojson": lambda v, *a: SafeString(json.dumps(v, default=str, separators=(",", ":")).replace("<", "\\u003c").replace(">", "\\u003e").replace("&", "\\u0026")),
1301
+ "json_encode": lambda v, *a: _json_safe(v),
1302
+ "to_json": lambda v, *a: _json_safe(v),
1303
+ "tojson": lambda v, *a: _json_safe(v),
1226
1304
  "js_escape": lambda v, *a: SafeString(str(v).replace("\\", "\\\\").replace("'", "\\'").replace('"', '\\"').replace("\n", "\\n").replace("\r", "\\r")),
1227
1305
  "json_decode": lambda v, *a: json.loads(v) if isinstance(v, str) else v,
1228
1306
  "keys": lambda v, *a: list(v.keys()) if isinstance(v, dict) else [],
File without changes