cuju-render 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (382) hide show
  1. cuju/render/__init__.py +759 -0
  2. cuju/render/__main__.py +1065 -0
  3. cuju/render/_export/__init__.py +3 -0
  4. cuju/render/_export/feed.py +542 -0
  5. cuju/render/_export/sizes.py +157 -0
  6. cuju/render/_export/video.py +1618 -0
  7. cuju/render/_export/video_jobs.py +408 -0
  8. cuju/render/_format/__init__.py +3 -0
  9. cuju/render/_format/annotations.py +235 -0
  10. cuju/render/_format/codec.py +307 -0
  11. cuju/render/_format/keep.py +121 -0
  12. cuju/render/_format/recording.py +860 -0
  13. cuju/render/_format/writer.py +54 -0
  14. cuju/render/_guide/__init__.py +3 -0
  15. cuju/render/_guide/doctor.py +523 -0
  16. cuju/render/_guide/help.py +62 -0
  17. cuju/render/_guide/tutorial.py +254 -0
  18. cuju/render/_http/__init__.py +3 -0
  19. cuju/render/_http/commands.py +57 -0
  20. cuju/render/_http/headers.py +103 -0
  21. cuju/render/_http/launch.py +500 -0
  22. cuju/render/_http/launchers.py +408 -0
  23. cuju/render/_http/pwa.py +128 -0
  24. cuju/render/_http/server.py +2105 -0
  25. cuju/render/_http/window.py +360 -0
  26. cuju/render/_moments/__init__.py +3 -0
  27. cuju/render/_moments/captions.py +284 -0
  28. cuju/render/_moments/events.py +1123 -0
  29. cuju/render/_moments/search.py +694 -0
  30. cuju/render/_packages/__init__.py +3 -0
  31. cuju/render/_packages/glsl.py +746 -0
  32. cuju/render/_packages/loudness.py +334 -0
  33. cuju/render/_packages/pack.py +1959 -0
  34. cuju/render/_packages/presentation.py +1376 -0
  35. cuju/render/_packages/vocabulary.py +609 -0
  36. cuju/render/_playing/__init__.py +3 -0
  37. cuju/render/_playing/achievements.py +463 -0
  38. cuju/render/_playing/bench.py +609 -0
  39. cuju/render/_playing/challenges.py +1813 -0
  40. cuju/render/_playing/demo.py +323 -0
  41. cuju/render/_playing/draw.py +211 -0
  42. cuju/render/_playing/errors.py +20 -0
  43. cuju/render/_playing/game.py +645 -0
  44. cuju/render/_playing/limits.py +339 -0
  45. cuju/render/_playing/play.py +926 -0
  46. cuju/render/_playing/policy_worker.py +250 -0
  47. cuju/render/_playing/strict.py +810 -0
  48. cuju/render/_registry/__init__.py +3 -0
  49. cuju/render/_registry/clips.py +394 -0
  50. cuju/render/_registry/joints.py +108 -0
  51. cuju/render/_registry/teams.py +2058 -0
  52. cuju/render/_store/__init__.py +3 -0
  53. cuju/render/_store/home.py +70 -0
  54. cuju/render/_store/library.py +604 -0
  55. cuju/render/_store/samples.py +163 -0
  56. cuju/render/_text/__init__.py +3 -0
  57. cuju/render/_text/languages.py +637 -0
  58. cuju/render/_text/messages.py +1396 -0
  59. cuju/render/_tournaments/__init__.py +3 -0
  60. cuju/render/_tournaments/results.py +547 -0
  61. cuju/render/_tournaments/tournament.py +2742 -0
  62. cuju/render/_tournaments/tournament_cli.py +587 -0
  63. cuju/render/_tournaments/tournament_runner.py +1975 -0
  64. cuju/render/_version.py +6 -0
  65. cuju/render/_watch/__init__.py +3 -0
  66. cuju/render/_watch/grid.py +265 -0
  67. cuju/render/_watch/live.py +664 -0
  68. cuju/render/_watch/notebook.py +79 -0
  69. cuju/render/_watch/show.py +171 -0
  70. cuju/render/samples/sample-11v11.cjr +0 -0
  71. cuju/render/samples/sample-5v5.cjr +0 -0
  72. cuju/render/samples/teams/starter/main.py +33 -0
  73. cuju/render/scenarios/benchmark_full_game_11_vs_11_easy.py +46 -0
  74. cuju/render/scenarios/benchmark_full_game_11_vs_11_hard.py +46 -0
  75. cuju/render/scenarios/benchmark_full_game_11_vs_11_medium.py +46 -0
  76. cuju/render/scenarios/benchmark_full_game_5_vs_5_easy.py +34 -0
  77. cuju/render/scenarios/benchmark_full_game_5_vs_5_hard.py +34 -0
  78. cuju/render/scenarios/benchmark_full_game_5_vs_5_medium.py +34 -0
  79. cuju/render/web/assets/adboard_polygon.webp +0 -0
  80. cuju/render/web/assets/adboard_your_ad_here.webp +0 -0
  81. cuju/render/web/assets/animations/kaykit-rig-medium-combatmelee.glb +0 -0
  82. cuju/render/web/assets/animations/kaykit-rig-medium-general.glb +0 -0
  83. cuju/render/web/assets/animations/kaykit-rig-medium-movementadvanced.glb +0 -0
  84. cuju/render/web/assets/animations/kaykit-rig-medium-movementbasic.glb +0 -0
  85. cuju/render/web/assets/animations/kaykit-rig-medium-simulation.glb +0 -0
  86. cuju/render/web/assets/animations/kaykit-rig-medium-special.glb +0 -0
  87. cuju/render/web/assets/ball.glb +0 -0
  88. cuju/render/web/assets/challenges/3_vs_1_with_keeper.mp4 +0 -0
  89. cuju/render/web/assets/challenges/3_vs_1_with_keeper.webp +0 -0
  90. cuju/render/web/assets/challenges/corner.mp4 +0 -0
  91. cuju/render/web/assets/challenges/corner.webp +0 -0
  92. cuju/render/web/assets/challenges/counterattack_easy.mp4 +0 -0
  93. cuju/render/web/assets/challenges/counterattack_easy.webp +0 -0
  94. cuju/render/web/assets/challenges/counterattack_hard.mp4 +0 -0
  95. cuju/render/web/assets/challenges/counterattack_hard.webp +0 -0
  96. cuju/render/web/assets/challenges/empty_goal.mp4 +0 -0
  97. cuju/render/web/assets/challenges/empty_goal.webp +0 -0
  98. cuju/render/web/assets/challenges/empty_goal_close.mp4 +0 -0
  99. cuju/render/web/assets/challenges/empty_goal_close.webp +0 -0
  100. cuju/render/web/assets/challenges/manifest.json +375 -0
  101. cuju/render/web/assets/challenges/one_on_one.mp4 +0 -0
  102. cuju/render/web/assets/challenges/one_on_one.webp +0 -0
  103. cuju/render/web/assets/challenges/one_on_one_hard.mp4 +0 -0
  104. cuju/render/web/assets/challenges/one_on_one_hard.webp +0 -0
  105. cuju/render/web/assets/challenges/pass_and_shoot_with_keeper.mp4 +0 -0
  106. cuju/render/web/assets/challenges/pass_and_shoot_with_keeper.webp +0 -0
  107. cuju/render/web/assets/challenges/run_pass_and_shoot_with_keeper.mp4 +0 -0
  108. cuju/render/web/assets/challenges/run_pass_and_shoot_with_keeper.webp +0 -0
  109. cuju/render/web/assets/challenges/run_to_score.mp4 +0 -0
  110. cuju/render/web/assets/challenges/run_to_score.webp +0 -0
  111. cuju/render/web/assets/challenges/run_to_score_with_keeper.mp4 +0 -0
  112. cuju/render/web/assets/challenges/run_to_score_with_keeper.webp +0 -0
  113. cuju/render/web/assets/challenges/single_goal_versus_lazy.mp4 +0 -0
  114. cuju/render/web/assets/challenges/single_goal_versus_lazy.webp +0 -0
  115. cuju/render/web/assets/crowd.webp +0 -0
  116. cuju/render/web/assets/crowd_normal.webp +0 -0
  117. cuju/render/web/assets/cuju-logo-384.png +0 -0
  118. cuju/render/web/assets/cuju-logo.png +0 -0
  119. cuju/render/web/assets/cuju-logo.svg +46 -0
  120. cuju/render/web/assets/cuju-mark-light.svg +33 -0
  121. cuju/render/web/assets/cuju-mark.svg +33 -0
  122. cuju/render/web/assets/goal.glb +0 -0
  123. cuju/render/web/assets/grass.webp +0 -0
  124. cuju/render/web/assets/hair_black.png +0 -0
  125. cuju/render/web/assets/hair_blonde.png +0 -0
  126. cuju/render/web/assets/hair_brown.png +0 -0
  127. cuju/render/web/assets/hair_darkblonde.png +0 -0
  128. cuju/render/web/assets/hair_long01.glb +0 -0
  129. cuju/render/web/assets/hair_long02.glb +0 -0
  130. cuju/render/web/assets/hair_medium01.glb +0 -0
  131. cuju/render/web/assets/hair_red.png +0 -0
  132. cuju/render/web/assets/hair_short01.glb +0 -0
  133. cuju/render/web/assets/hair_short02.glb +0 -0
  134. cuju/render/web/assets/icons/cuju-192.png +0 -0
  135. cuju/render/web/assets/icons/cuju-512.png +0 -0
  136. cuju/render/web/assets/icons/cuju-maskable-512.png +0 -0
  137. cuju/render/web/assets/kit_goalkeeper.webp +0 -0
  138. cuju/render/web/assets/kit_mask.png +0 -0
  139. cuju/render/web/assets/kit_normal.webp +0 -0
  140. cuju/render/web/assets/manifest.json +601 -0
  141. cuju/render/web/assets/models/kaykit-barbarian.glb +0 -0
  142. cuju/render/web/assets/models/kaykit-barbarian.json +23 -0
  143. cuju/render/web/assets/models/kaykit-knight.glb +0 -0
  144. cuju/render/web/assets/models/kaykit-knight.json +23 -0
  145. cuju/render/web/assets/models/kaykit-mage.glb +0 -0
  146. cuju/render/web/assets/models/kaykit-mage.json +23 -0
  147. cuju/render/web/assets/models/kaykit-mannequin-medium.glb +0 -0
  148. cuju/render/web/assets/models/kaykit-mannequin-medium.json +23 -0
  149. cuju/render/web/assets/models/kaykit-ranger.glb +0 -0
  150. cuju/render/web/assets/models/kaykit-ranger.json +23 -0
  151. cuju/render/web/assets/models/kaykit-rogue-hooded.glb +0 -0
  152. cuju/render/web/assets/models/kaykit-rogue-hooded.json +23 -0
  153. cuju/render/web/assets/models/kaykit-rogue.glb +0 -0
  154. cuju/render/web/assets/models/kaykit-rogue.json +23 -0
  155. cuju/render/web/assets/models/kaykit-skeleton-mage.glb +0 -0
  156. cuju/render/web/assets/models/kaykit-skeleton-mage.json +23 -0
  157. cuju/render/web/assets/models/kaykit-skeleton-minion.glb +0 -0
  158. cuju/render/web/assets/models/kaykit-skeleton-minion.json +23 -0
  159. cuju/render/web/assets/models/kaykit-skeleton-rogue.glb +0 -0
  160. cuju/render/web/assets/models/kaykit-skeleton-rogue.json +23 -0
  161. cuju/render/web/assets/models/kaykit-skeleton-warrior.glb +0 -0
  162. cuju/render/web/assets/models/kaykit-skeleton-warrior.json +23 -0
  163. cuju/render/web/assets/pitch.webp +0 -0
  164. cuju/render/web/assets/pitch_normal.webp +0 -0
  165. cuju/render/web/assets/player.glb +0 -0
  166. cuju/render/web/assets/shoe.webp +0 -0
  167. cuju/render/web/assets/shoe_sole.png +0 -0
  168. cuju/render/web/assets/skin_01.png +0 -0
  169. cuju/render/web/assets/skin_02.png +0 -0
  170. cuju/render/web/assets/skin_03.png +0 -0
  171. cuju/render/web/assets/skin_04.png +0 -0
  172. cuju/render/web/assets/sounds/applause.ogg +0 -0
  173. cuju/render/web/assets/sounds/beach-wave-1.ogg +0 -0
  174. cuju/render/web/assets/sounds/beach-wave-2.ogg +0 -0
  175. cuju/render/web/assets/sounds/beach-wave-3.ogg +0 -0
  176. cuju/render/web/assets/sounds/beach-wave-4.ogg +0 -0
  177. cuju/render/web/assets/sounds/city-horn.ogg +0 -0
  178. cuju/render/web/assets/sounds/city-loop.ogg +0 -0
  179. cuju/render/web/assets/sounds/city-siren.ogg +0 -0
  180. cuju/render/web/assets/sounds/crowd-cheer.ogg +0 -0
  181. cuju/render/web/assets/sounds/crowd-loop.ogg +0 -0
  182. cuju/render/web/assets/sounds/crowd-ooh.ogg +0 -0
  183. cuju/render/web/assets/sounds/gulls.ogg +0 -0
  184. cuju/render/web/assets/sounds/neon-sting.ogg +0 -0
  185. cuju/render/web/assets/sounds/rain-loop.ogg +0 -0
  186. cuju/render/web/assets/sounds/sounds.json +46 -0
  187. cuju/render/web/assets/sounds/space-beep.ogg +0 -0
  188. cuju/render/web/assets/sounds/space-hum.ogg +0 -0
  189. cuju/render/web/assets/sounds/space-radio.ogg +0 -0
  190. cuju/render/web/assets/sounds/whistle-long.ogg +0 -0
  191. cuju/render/web/assets/sounds/whistle-short.ogg +0 -0
  192. cuju/render/web/assets/sounds/wind-loop.ogg +0 -0
  193. cuju/render/web/easy.css +357 -0
  194. cuju/render/web/export.html +23 -0
  195. cuju/render/web/feed.css +262 -0
  196. cuju/render/web/feed.html +28 -0
  197. cuju/render/web/flat.css +120 -0
  198. cuju/render/web/home.css +837 -0
  199. cuju/render/web/index.html +373 -0
  200. cuju/render/web/js/boot.js +50 -0
  201. cuju/render/web/js/broadcast/audio.js +2141 -0
  202. cuju/render/web/js/broadcast/celebrate.js +1238 -0
  203. cuju/render/web/js/broadcast/decisions.js +609 -0
  204. cuju/render/web/js/broadcast/hud.js +352 -0
  205. cuju/render/web/js/broadcast/show.js +3614 -0
  206. cuju/render/web/js/broadcast/transition.js +2231 -0
  207. cuju/render/web/js/broadcast/voices.js +153 -0
  208. cuju/render/web/js/capture.js +101 -0
  209. cuju/render/web/js/core/accessibility.js +90 -0
  210. cuju/render/web/js/core/api.js +429 -0
  211. cuju/render/web/js/core/comfort.js +135 -0
  212. cuju/render/web/js/core/i18n.js +705 -0
  213. cuju/render/web/js/core/icons.js +167 -0
  214. cuju/render/web/js/core/input-settings.js +156 -0
  215. cuju/render/web/js/core/keys.js +277 -0
  216. cuju/render/web/js/core/language-ui.js +125 -0
  217. cuju/render/web/js/core/menu.js +853 -0
  218. cuju/render/web/js/core/names.js +113 -0
  219. cuju/render/web/js/core/page-structure.js +110 -0
  220. cuju/render/web/js/core/scenarios.js +109 -0
  221. cuju/render/web/js/core/spoilers.js +236 -0
  222. cuju/render/web/js/core/typeahead.js +38 -0
  223. cuju/render/web/js/export/dom-picture.js +330 -0
  224. cuju/render/web/js/export/export-host.js +97 -0
  225. cuju/render/web/js/export/feed-dialog.js +191 -0
  226. cuju/render/web/js/export/feed-params.js +266 -0
  227. cuju/render/web/js/export/feed.js +363 -0
  228. cuju/render/web/js/export/mp4-mux.js +295 -0
  229. cuju/render/web/js/export/photo-core.js +313 -0
  230. cuju/render/web/js/export/photo.js +1314 -0
  231. cuju/render/web/js/export/safe-frame.js +59 -0
  232. cuju/render/web/js/export/size-fields.js +103 -0
  233. cuju/render/web/js/export/sizes.js +178 -0
  234. cuju/render/web/js/export/video-boot.js +55 -0
  235. cuju/render/web/js/export/video-browser.js +325 -0
  236. cuju/render/web/js/export/video-export.js +417 -0
  237. cuju/render/web/js/export/video-plan.js +207 -0
  238. cuju/render/web/js/export/video-range.js +185 -0
  239. cuju/render/web/js/export/webm-mux.js +198 -0
  240. cuju/render/web/js/feed-boot.js +151 -0
  241. cuju/render/web/js/feed-frame.js +101 -0
  242. cuju/render/web/js/help/doctor.js +488 -0
  243. cuju/render/web/js/help/firstrun.js +379 -0
  244. cuju/render/web/js/help/help-center.js +438 -0
  245. cuju/render/web/js/help/install-offer.js +164 -0
  246. cuju/render/web/js/help/install-state.js +104 -0
  247. cuju/render/web/js/help/lessons-core.js +207 -0
  248. cuju/render/web/js/help/lessons.js +463 -0
  249. cuju/render/web/js/help/palette.js +584 -0
  250. cuju/render/web/js/help/pwa.js +257 -0
  251. cuju/render/web/js/help/tour-core.js +241 -0
  252. cuju/render/web/js/help/tour.js +461 -0
  253. cuju/render/web/js/main.js +2513 -0
  254. cuju/render/web/js/pages/draw.js +188 -0
  255. cuju/render/web/js/pages/fieldpage.js +530 -0
  256. cuju/render/web/js/pages/home.js +521 -0
  257. cuju/render/web/js/pages/matches.js +779 -0
  258. cuju/render/web/js/pages/moments-ui.js +359 -0
  259. cuju/render/web/js/pages/queue-filters.js +63 -0
  260. cuju/render/web/js/pages/recording-groups.js +80 -0
  261. cuju/render/web/js/pages/recordings.js +1069 -0
  262. cuju/render/web/js/play/bench.js +347 -0
  263. cuju/render/web/js/play/challenges.js +1302 -0
  264. cuju/render/web/js/play/medals.js +127 -0
  265. cuju/render/web/js/play/play.js +1953 -0
  266. cuju/render/web/js/play/teasers.js +196 -0
  267. cuju/render/web/js/play/touch.js +350 -0
  268. cuju/render/web/js/play/tournament-layout.js +487 -0
  269. cuju/render/web/js/play/tournament-results.js +441 -0
  270. cuju/render/web/js/play/tournament-strict.js +560 -0
  271. cuju/render/web/js/play/tournament.js +2585 -0
  272. cuju/render/web/js/players/animate.js +972 -0
  273. cuju/render/web/js/players/clips.js +224 -0
  274. cuju/render/web/js/players/kaykit.js +162 -0
  275. cuju/render/web/js/players/kits.js +676 -0
  276. cuju/render/web/js/players/number-patch.js +82 -0
  277. cuju/render/web/js/players/players.js +2694 -0
  278. cuju/render/web/js/players/recolor.js +893 -0
  279. cuju/render/web/js/players/retarget.js +325 -0
  280. cuju/render/web/js/players/rig-numbers.js +174 -0
  281. cuju/render/web/js/presentation/animator.js +231 -0
  282. cuju/render/web/js/presentation/assign.js +85 -0
  283. cuju/render/web/js/presentation/behaviors.js +440 -0
  284. cuju/render/web/js/presentation/budget.js +209 -0
  285. cuju/render/web/js/presentation/catalog.js +1674 -0
  286. cuju/render/web/js/presentation/clocks.js +72 -0
  287. cuju/render/web/js/presentation/commands.js +301 -0
  288. cuju/render/web/js/presentation/contract.js +240 -0
  289. cuju/render/web/js/presentation/effects.js +904 -0
  290. cuju/render/web/js/presentation/flash.js +605 -0
  291. cuju/render/web/js/presentation/freeze-worklet.js +22 -0
  292. cuju/render/web/js/presentation/glsl.js +464 -0
  293. cuju/render/web/js/presentation/host.js +323 -0
  294. cuju/render/web/js/presentation/index.js +2066 -0
  295. cuju/render/web/js/presentation/loudness-worklet.js +235 -0
  296. cuju/render/web/js/presentation/loudness.js +740 -0
  297. cuju/render/web/js/presentation/moments.js +536 -0
  298. cuju/render/web/js/presentation/post.js +327 -0
  299. cuju/render/web/js/presentation/preview.js +180 -0
  300. cuju/render/web/js/presentation/protocol.js +174 -0
  301. cuju/render/web/js/presentation/reach.js +158 -0
  302. cuju/render/web/js/presentation/rigging.js +130 -0
  303. cuju/render/web/js/presentation/sandbox.js +157 -0
  304. cuju/render/web/js/presentation/shaders.js +235 -0
  305. cuju/render/web/js/presentation/stage.js +402 -0
  306. cuju/render/web/js/presentation/teamaudio.js +722 -0
  307. cuju/render/web/js/presentation/tracks.js +197 -0
  308. cuju/render/web/js/presentation/values.js +81 -0
  309. cuju/render/web/js/recording/annotation-data.js +203 -0
  310. cuju/render/web/js/recording/captions.js +491 -0
  311. cuju/render/web/js/recording/codec.js +185 -0
  312. cuju/render/web/js/recording/decode-worker.js +70 -0
  313. cuju/render/web/js/recording/decode.js +94 -0
  314. cuju/render/web/js/recording/episodes.js +46 -0
  315. cuju/render/web/js/recording/format.js +461 -0
  316. cuju/render/web/js/recording/live.js +348 -0
  317. cuju/render/web/js/recording/moments-query.js +59 -0
  318. cuju/render/web/js/recording/moments.js +149 -0
  319. cuju/render/web/js/recording/pose.js +86 -0
  320. cuju/render/web/js/recording/timeline.js +275 -0
  321. cuju/render/web/js/teams/art.js +50 -0
  322. cuju/render/web/js/teams/clip-editor.js +293 -0
  323. cuju/render/web/js/teams/editor.js +1979 -0
  324. cuju/render/web/js/teams/library.js +344 -0
  325. cuju/render/web/js/teams/painter.js +633 -0
  326. cuju/render/web/js/teams/surprise-ui.js +195 -0
  327. cuju/render/web/js/teams/surprise.js +497 -0
  328. cuju/render/web/js/teams/team-check.js +127 -0
  329. cuju/render/web/js/watch/annotations.js +270 -0
  330. cuju/render/web/js/watch/flat.js +366 -0
  331. cuju/render/web/js/watch/grid.js +436 -0
  332. cuju/render/web/js/watch/pitch2d.js +289 -0
  333. cuju/render/web/js/watch/player-tools.js +192 -0
  334. cuju/render/web/js/watch/presets.js +201 -0
  335. cuju/render/web/js/world/arenas.js +997 -0
  336. cuju/render/web/js/world/ball.js +204 -0
  337. cuju/render/web/js/world/cameras.js +982 -0
  338. cuju/render/web/js/world/crowd.js +376 -0
  339. cuju/render/web/js/world/field.js +227 -0
  340. cuju/render/web/js/world/framing.js +145 -0
  341. cuju/render/web/js/world/occlusion.js +48 -0
  342. cuju/render/web/js/world/pitch.js +703 -0
  343. cuju/render/web/js/world/scene.js +395 -0
  344. cuju/render/web/js/world/sky.js +325 -0
  345. cuju/render/web/js/world/stadium.js +1262 -0
  346. cuju/render/web/js/world/vendor-sky.js +367 -0
  347. cuju/render/web/js/world/weather.js +264 -0
  348. cuju/render/web/languages/en.json +4750 -0
  349. cuju/render/web/lessons.json +137 -0
  350. cuju/render/web/manifest.webmanifest +23 -0
  351. cuju/render/web/style.css +8399 -0
  352. cuju/render/web/sw.js +235 -0
  353. cuju/render/web/vendor/fabric/LICENSE +16 -0
  354. cuju/render/web/vendor/fabric/fabric.min.mjs +1 -0
  355. cuju/render/web/vendor/lucide/LICENSE +43 -0
  356. cuju/render/web/vendor/lucide/icons.js +141 -0
  357. cuju/render/web/vendor/three/LICENSE +21 -0
  358. cuju/render/web/vendor/three/build/three.core.js +60586 -0
  359. cuju/render/web/vendor/three/build/three.module.js +19719 -0
  360. cuju/render/web/vendor/three/examples/jsm/loaders/GLTFLoader.js +4925 -0
  361. cuju/render/web/vendor/three/examples/jsm/postprocessing/EffectComposer.js +365 -0
  362. cuju/render/web/vendor/three/examples/jsm/postprocessing/MaskPass.js +195 -0
  363. cuju/render/web/vendor/three/examples/jsm/postprocessing/OutputPass.js +148 -0
  364. cuju/render/web/vendor/three/examples/jsm/postprocessing/Pass.js +191 -0
  365. cuju/render/web/vendor/three/examples/jsm/postprocessing/RenderPass.js +193 -0
  366. cuju/render/web/vendor/three/examples/jsm/postprocessing/ShaderPass.js +135 -0
  367. cuju/render/web/vendor/three/examples/jsm/postprocessing/UnrealBloomPass.js +541 -0
  368. cuju/render/web/vendor/three/examples/jsm/shaders/CopyShader.js +52 -0
  369. cuju/render/web/vendor/three/examples/jsm/shaders/LuminosityHighPassShader.js +68 -0
  370. cuju/render/web/vendor/three/examples/jsm/shaders/OutputShader.js +103 -0
  371. cuju/render/web/vendor/three/examples/jsm/utils/BufferGeometryUtils.js +1501 -0
  372. cuju/render/web/vendor/three/examples/jsm/utils/SkeletonUtils.js +496 -0
  373. cuju_render-0.1.0.dist-info/METADATA +116 -0
  374. cuju_render-0.1.0.dist-info/RECORD +382 -0
  375. cuju_render-0.1.0.dist-info/WHEEL +4 -0
  376. cuju_render-0.1.0.dist-info/licenses/LICENSE +202 -0
  377. cuju_render-0.1.0.dist-info/licenses/LICENSES/CC-BY-4.0.txt +396 -0
  378. cuju_render-0.1.0.dist-info/licenses/LICENSES/CC0-1.0.txt +127 -0
  379. cuju_render-0.1.0.dist-info/licenses/LICENSES/ISC.txt +16 -0
  380. cuju_render-0.1.0.dist-info/licenses/LICENSES/MIT.txt +24 -0
  381. cuju_render-0.1.0.dist-info/licenses/LICENSES/Unlicense.txt +13 -0
  382. cuju_render-0.1.0.dist-info/licenses/NOTICE +35 -0
@@ -0,0 +1,637 @@
1
+ # Copyright 2026 The cuju Authors
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """The languages of the page, the shipped files and those of the renderer's home folder.
5
+
6
+ A language file is one JSON object keyed by message id, such as `home.title`, whose values are
7
+ texts with placeholders in braces, such as `{count} recordings`, or objects of plural forms keyed
8
+ by the categories of Unicode CLDR, `zero`, `one`, `two`, `few`, `many` and `other`, and by exact
9
+ counts such as `=0`. The key `@language` holds the code, the name, the English name, the locale of
10
+ numbers and dates, the direction of the text and the plural categories of the language.
11
+
12
+ The page ships English in `web/languages`, and a person adds a language by dropping its file,
13
+ named after its code, into the folder `languages` of the home folder, or by importing it on the
14
+ Settings panel. A code is a language tag of BCP 47, such as `de` or `pt-BR`, and two codes that
15
+ differ in case alone name the same language, as BCP 47 has it. English is the reference of every
16
+ other file. `check` reports the messages a file lacks, those English lacks, and malformed ones,
17
+ whose placeholders differ from English or whose plural forms miss a category of the language,
18
+ the categories of the base language before the first hyphen of the code. `template` writes a new
19
+ file from English, and `get`, `post` and `delete` answer `/api/languages`.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import json
25
+ import os
26
+ import re
27
+ import sys
28
+ import time
29
+ from pathlib import Path
30
+ from typing import Any
31
+
32
+ from .._store import home as _home
33
+ from . import messages as _messages
34
+ from .messages import MessageError, counted
35
+
36
+ SHIPPED = Path(__file__).parents[1] / 'web' / 'languages'
37
+ HEADER = '@language'
38
+ # A language file of the home folder holds at most this many bytes.
39
+ MAXIMUM_BYTES = 2 * 1024 * 1024
40
+
41
+ _CODE = re.compile(r'[A-Za-z]{2,3}(-[A-Za-z0-9]{1,8})*')
42
+ _ID = re.compile(r'[A-Za-z0-9_.-]+')
43
+ _PLACEHOLDER = re.compile(r'\{([A-Za-z0-9_]+)\}')
44
+ _EXACT = re.compile(r'=\d+')
45
+ CATEGORIES = ('zero', 'one', 'two', 'few', 'many', 'other')
46
+
47
+ # The plural categories of the cardinal numbers of common languages, by base language, as
48
+ # Intl.PluralRules gives them from Unicode CLDR. A language missing here declares its own.
49
+ _GROUPS = {
50
+ ('one', 'other'): (
51
+ 'af am az bg bn da de el en et eu fa fi fil gl gu hi hu hy is ka kk kn ky mk ml mn mr nb '
52
+ 'ne nl pa ps si sq sv sw ta te tr ur uz zu'
53
+ ),
54
+ ('zero', 'one', 'two', 'few', 'many', 'other'): 'ar cy',
55
+ ('one', 'few', 'many', 'other'): 'be cs lt pl ru sk uk',
56
+ ('one', 'few', 'other'): 'bs hr ro sr',
57
+ ('one', 'many', 'other'): 'ca es fr it pt',
58
+ ('one', 'two', 'few', 'many', 'other'): 'ga',
59
+ ('one', 'two', 'other'): 'he',
60
+ ('other',): 'id ja km ko lo ms my th vi zh',
61
+ ('zero', 'one', 'other'): 'lv',
62
+ ('one', 'two', 'few', 'other'): 'sl',
63
+ }
64
+ PLURALS = {code: list(group) for group, codes in _GROUPS.items() for code in codes.split()}
65
+ # Languages written from right to left.
66
+ _RTL = {'ar', 'fa', 'he', 'ps', 'ur', 'yi', 'dv', 'ug'}
67
+
68
+
69
+ def valid_code(code: Any) -> bool:
70
+ """Returns whether `code` names a language file safely, such as `pt` or `pt-BR`."""
71
+ return isinstance(code, str) and bool(_CODE.fullmatch(code))
72
+
73
+
74
+ def _file(place: Path, code: str) -> Path | None:
75
+ """Returns the file of the language `code` in `place`, whatever the case of its name."""
76
+ exact = place / f'{code}.json'
77
+ if exact.is_file():
78
+ return exact
79
+ for path in place.glob('*.json'):
80
+ if path.stem.lower() == code.lower() and path.is_file():
81
+ return path
82
+ return None
83
+
84
+
85
+ def folder() -> Path:
86
+ """Returns the folder of the languages a person adds, created when missing."""
87
+ return _home.folder('languages')
88
+
89
+
90
+ def _read(path: Path) -> Any:
91
+ return json.loads(path.read_text(encoding='utf-8'))
92
+
93
+
94
+ def english() -> dict[str, Any]:
95
+ """Returns the shipped English file, the reference of every other."""
96
+ return _read(SHIPPED / 'en.json')
97
+
98
+
99
+ # The message of the name a person plays under when they type none.
100
+ _DEFAULT_NAME = 'challenges.name.default'
101
+ # The default names of languages that shipped with the page before, which older runs may hold.
102
+ FORMER_DEFAULT_NAMES = ('Jogador',)
103
+ _DEFAULTS: dict[tuple[tuple[str, int], ...], frozenset[str]] = {}
104
+ # The names are read again at most this often, since a leaderboard asks for them by entry.
105
+ _RECHECK_SECONDS = 2.0
106
+ _CHECKED: dict[str, tuple[float, frozenset[str]]] = {}
107
+
108
+
109
+ def default_names() -> frozenset[str]:
110
+ """Returns the default name of a person in every language that the renderer knows.
111
+
112
+ A person who types no name plays under the default name of the page's language, so a name
113
+ among these counts as no name, and the page shows the default of its own language in its
114
+ place. The names come from English, the languages of the folder and the languages that shipped
115
+ with the page before, and stay cached until a file of them changes, which the function looks
116
+ for every two seconds.
117
+ """
118
+ place = folder()
119
+ now = time.monotonic()
120
+ recent = _CHECKED.get(str(place))
121
+ if recent is not None and now - recent[0] < _RECHECK_SECONDS:
122
+ return recent[1]
123
+ paths = [SHIPPED / 'en.json', *sorted(place.glob('*.json'))]
124
+ key = []
125
+ for path in paths:
126
+ try:
127
+ key.append((str(path), path.stat().st_mtime_ns))
128
+ except OSError:
129
+ continue
130
+ found = _DEFAULTS.get(tuple(key))
131
+ if found is None:
132
+ names = set(FORMER_DEFAULT_NAMES)
133
+ for path, _ in key:
134
+ try:
135
+ value = _read(Path(path)).get(_DEFAULT_NAME)
136
+ except (OSError, ValueError, AttributeError):
137
+ continue
138
+ if isinstance(value, str) and value.strip():
139
+ names.add(value.strip())
140
+ found = frozenset(names)
141
+ _DEFAULTS.clear()
142
+ _DEFAULTS[tuple(key)] = found
143
+ _CHECKED.clear()
144
+ _CHECKED[str(place)] = (now, found)
145
+ return found
146
+
147
+
148
+ def placeholders(value: Any) -> set[str]:
149
+ """Returns the placeholders of a message, of every form of a plural message."""
150
+ if isinstance(value, str):
151
+ return set(_PLACEHOLDER.findall(value))
152
+ if isinstance(value, dict):
153
+ return {
154
+ name
155
+ for form in value.values()
156
+ if isinstance(form, str)
157
+ for name in _PLACEHOLDER.findall(form)
158
+ }
159
+ return set()
160
+
161
+
162
+ def _problem(name: str, **values: Any) -> dict[str, Any]:
163
+ """Returns a problem of a file as a label of the message `server.language.problem.<name>`."""
164
+ return _messages.label(f'server.language.problem.{name}', **values)
165
+
166
+
167
+ def _header_problems(header: Any, code: str | None) -> list[dict[str, Any]]:
168
+ if not isinstance(header, dict):
169
+ return [_problem('header')]
170
+ problems = []
171
+ if not valid_code(header.get('code')):
172
+ problems.append(_problem('code', code=repr(header.get('code'))))
173
+ elif code is not None and header.get('code') != code:
174
+ problems.append(_problem('file_code', code=repr(code), found=repr(header.get('code'))))
175
+ if not isinstance(header.get('name'), str) or not header.get('name').strip():
176
+ problems.append(_problem('name'))
177
+ for key in ('english', 'locale'):
178
+ if key in header and not isinstance(header[key], str):
179
+ problems.append(_problem('text', key=key, value=repr(header[key])))
180
+ if header.get('dir', 'ltr') not in ('ltr', 'rtl'):
181
+ problems.append(_problem('dir', value=repr(header.get('dir'))))
182
+ plurals = header.get('plurals')
183
+ if not isinstance(plurals, list) or not plurals or not all(p in CATEGORIES for p in plurals):
184
+ problems.append(_problem('plurals', categories=', '.join(CATEGORIES), value=repr(plurals)))
185
+ elif 'other' not in plurals:
186
+ problems.append(_problem('other'))
187
+ else:
188
+ base = str(header.get('code', '')).split('-')[0].lower()
189
+ lacking = [p for p in PLURALS.get(base, []) if p not in plurals]
190
+ if lacking:
191
+ problems.append(_problem('categories', language=base, lacking=', '.join(lacking)))
192
+ return problems
193
+
194
+
195
+ def _message_problem(
196
+ key: str, value: Any, reference: Any, plurals: list[str]
197
+ ) -> dict[str, Any] | None:
198
+ """Returns what is malformed about one message against its English as a label, or None."""
199
+ if isinstance(value, str):
200
+ if isinstance(reference, dict) and plurals != ['other']:
201
+ return _problem('forms_expected')
202
+ elif isinstance(value, dict):
203
+ if not value or not all(isinstance(form, str) for form in value.values()):
204
+ return _problem('form_texts')
205
+ if key.startswith('caption.'):
206
+ return _problem('caption')
207
+ unknown = [form for form in value if form not in CATEGORIES and not _EXACT.fullmatch(form)]
208
+ if unknown:
209
+ return _problem('form_names', unknown=', '.join(unknown))
210
+ lacking = [form for form in plurals if form not in value]
211
+ if lacking:
212
+ return _problem('forms_lacking', lacking=', '.join(lacking))
213
+ # A language may count what English leaves alone, when the English text holds {count}.
214
+ if isinstance(reference, str) and 'count' not in placeholders(reference):
215
+ return _problem('text_expected')
216
+ else:
217
+ return _problem('kind')
218
+ if reference is None:
219
+ return None
220
+ own, theirs = placeholders(value), placeholders(reference)
221
+ extra = sorted(own - theirs)
222
+ if extra:
223
+ return _problem('extra', extra=', '.join('{' + p + '}' for p in extra))
224
+ # A plural form may leave the count out, as in "one recording", every other value stays.
225
+ lost = sorted(theirs - own - ({'count'} if isinstance(value, dict) else set()))
226
+ if lost:
227
+ return _problem('lost', lost=', '.join('{' + p + '}' for p in lost))
228
+ # Commands and shortcuts are code and must survive, while bold words are the translator's
229
+ # choice, since each language stresses its own words.
230
+ if _code_spans(value) != _code_spans(reference):
231
+ return _problem('marks')
232
+ return None
233
+
234
+
235
+ def _code_spans(value: Any) -> int:
236
+ """Returns how many code spans a message holds, its commands, names and shortcuts.
237
+
238
+ A plural message counts the spans of its form `other`, which every language has.
239
+ """
240
+ text = value if isinstance(value, str) else (value or {}).get('other', '')
241
+ found = _messages._MARK.findall(text if isinstance(text, str) else '')
242
+ return sum(1 for _, code in found if code)
243
+
244
+
245
+ def check(
246
+ document: Any, code: str | None = None, reference: dict[str, Any] | None = None
247
+ ) -> dict[str, Any]:
248
+ """Checks a language file against English.
249
+
250
+ Args:
251
+ document: The parsed file.
252
+ code: The code the file must name, that of its file name, or None for any.
253
+ reference: The English file, by default the shipped one.
254
+
255
+ Returns:
256
+ A report with `code`, `name`, `messages`, `ok`, and lists `missing` and `unused` of ids,
257
+ `malformed` of objects with `id` and `problem`, the English text of the problem, with its
258
+ message in `problem_id` and `problem_values`, and `untranslated`, the ids whose text is
259
+ the English one, which a template starts with and which is no fault of its own.
260
+ """
261
+ reference = english() if reference is None else reference
262
+ report: dict[str, Any] = {
263
+ 'code': None,
264
+ 'name': None,
265
+ 'messages': 0,
266
+ 'missing': [],
267
+ 'unused': [],
268
+ 'malformed': [],
269
+ 'untranslated': [],
270
+ }
271
+ if not isinstance(document, dict):
272
+ problem = _messages.named('problem', _problem('document'))
273
+ report['malformed'].append({'id': HEADER, **problem})
274
+ report['ok'] = False
275
+ return report
276
+ header = document.get(HEADER)
277
+ if isinstance(header, dict):
278
+ report['code'] = header.get('code')
279
+ report['name'] = header.get('name')
280
+ for problem in _header_problems(header, code):
281
+ report['malformed'].append({'id': HEADER, **_messages.named('problem', problem)})
282
+ plurals = (
283
+ header.get('plurals')
284
+ if isinstance(header, dict) and isinstance(header.get('plurals'), list)
285
+ else ['other']
286
+ )
287
+ for key, value in document.items():
288
+ if key == HEADER:
289
+ continue
290
+ if not _ID.fullmatch(key):
291
+ report['malformed'].append({'id': key, **_messages.named('problem', _problem('id'))})
292
+ continue
293
+ report['messages'] += 1
294
+ if key not in reference:
295
+ report['unused'].append(key)
296
+ problem = _message_problem(key, value, reference.get(key), plurals)
297
+ if problem:
298
+ report['malformed'].append({'id': key, **_messages.named('problem', problem)})
299
+ elif report['code'] != 'en' and value == reference.get(key):
300
+ report['untranslated'].append(key)
301
+ report['missing'] = [key for key in reference if key != HEADER and key not in document]
302
+ report['ok'] = not (report['missing'] or report['unused'] or report['malformed'])
303
+ return report
304
+
305
+
306
+ def template(code: str, name: str | None = None) -> dict[str, Any]:
307
+ """Returns a new language file for `code` with every English message, for a translator.
308
+
309
+ The plural messages take the categories of the language, each filled with the English text of
310
+ the nearest English form, and the header names the language with its code until it is set.
311
+ """
312
+ if not valid_code(code):
313
+ raise MessageError('server.language.code', code=code)
314
+ base = code.split('-')[0].lower()
315
+ plurals = PLURALS.get(base, ['one', 'other'])
316
+ document: dict[str, Any] = {
317
+ HEADER: {
318
+ 'code': code,
319
+ 'name': name or code,
320
+ 'english': '',
321
+ 'locale': code,
322
+ 'dir': 'rtl' if base in _RTL else 'ltr',
323
+ 'plurals': plurals,
324
+ }
325
+ }
326
+ for key, value in english().items():
327
+ if key == HEADER:
328
+ continue
329
+ if isinstance(value, dict):
330
+ forms = {
331
+ form: value.get(
332
+ form, value.get('one' if form in ('zero', 'one') else 'other', value['other'])
333
+ )
334
+ for form in plurals
335
+ }
336
+ document[key] = forms if plurals != ['other'] else value['other']
337
+ else:
338
+ document[key] = value
339
+ return document
340
+
341
+
342
+ def _entry(document: Any, code: str, source: str, reference: dict[str, Any]) -> dict[str, Any]:
343
+ report = check(document, code, reference)
344
+ header = document.get(HEADER) if isinstance(document, dict) else None
345
+ header = header if isinstance(header, dict) else {}
346
+ return {
347
+ 'code': code,
348
+ 'name': header.get('name') if isinstance(header.get('name'), str) else code,
349
+ 'english': header.get('english') if isinstance(header.get('english'), str) else '',
350
+ 'locale': header.get('locale') if isinstance(header.get('locale'), str) else code,
351
+ 'dir': 'rtl' if header.get('dir') == 'rtl' else 'ltr',
352
+ 'source': source,
353
+ 'usable': not report['malformed'],
354
+ 'messages': report['messages'],
355
+ 'missing': len(report['missing']),
356
+ 'problems': [f'{item["id"]}, {item["problem"]}' for item in report['malformed'][:20]],
357
+ }
358
+
359
+
360
+ def listing() -> list[dict[str, Any]]:
361
+ """Returns the languages, the shipped ones first, English first among them, then the own.
362
+
363
+ Each entry names its code, name, English name, locale, direction, source `shipped` or `home`,
364
+ whether the page can use it, the counts of its messages and of the English messages it lacks,
365
+ and the problems that make it unusable.
366
+ """
367
+ reference = english()
368
+ entries = []
369
+ shipped = sorted(SHIPPED.glob('*.json'), key=lambda path: (path.stem != 'en', path.stem))
370
+ for path in shipped:
371
+ entries.append(_entry(_read(path), path.stem, 'shipped', reference))
372
+ names = {entry['code'].lower() for entry in entries}
373
+ for path in sorted(folder().glob('*.json'), key=lambda path: path.stem):
374
+ if path.stem.lower() in names or not valid_code(path.stem):
375
+ continue
376
+ try:
377
+ document = _read(path)
378
+ except (OSError, ValueError) as error:
379
+ entries.append(
380
+ {
381
+ 'code': path.stem,
382
+ 'name': path.stem,
383
+ 'english': '',
384
+ 'locale': path.stem,
385
+ 'dir': 'ltr',
386
+ 'source': 'home',
387
+ 'usable': False,
388
+ 'messages': 0,
389
+ 'missing': 0,
390
+ 'problems': [f'the file is not JSON, {error}'],
391
+ }
392
+ )
393
+ continue
394
+ entries.append(_entry(document, path.stem, 'home', reference))
395
+ return entries
396
+
397
+
398
+ _SLOT = b'<script type="application/json" id="cuju-languages"></script>'
399
+ _INLINE: dict[str, Any] = {'key': None, 'text': b''}
400
+
401
+
402
+ def inline(page: bytes) -> bytes:
403
+ """Fills the empty language block of a page with every shipped language file.
404
+
405
+ The page reads its messages from the block, so it starts without another request. Every `<`
406
+ of the files is escaped, so no text of a message can close the block.
407
+ """
408
+ if _SLOT not in page:
409
+ return page
410
+ paths = sorted(SHIPPED.glob('*.json'))
411
+ key = tuple((path.name, path.stat().st_mtime_ns) for path in paths)
412
+ if _INLINE['key'] != key:
413
+ files = {path.stem: _read(path) for path in paths}
414
+ text = json.dumps(files, ensure_ascii=False, separators=(',', ':')).replace('<', r'\u003c')
415
+ _INLINE.update(key=key, text=text.encode('utf-8'))
416
+ return page.replace(_SLOT, _SLOT.replace(b'></', b'>' + _INLINE['text'] + b'</', 1))
417
+
418
+
419
+ def read(code: str) -> dict[str, Any]:
420
+ """Returns the file of a language, shipped or of the home folder."""
421
+ if not valid_code(code):
422
+ raise MessageError('server.language.code', code=code)
423
+ for place in (SHIPPED, folder()):
424
+ path = _file(place, code)
425
+ if path is not None:
426
+ return _read(path)
427
+ raise MessageError('server.language.none', code=code)
428
+
429
+
430
+ def add(document: Any) -> dict[str, Any]:
431
+ """Keeps a checked language file in the home folder and returns its entry and report.
432
+
433
+ A file whose messages are malformed is refused with the reasons. Missing messages are allowed,
434
+ the page shows them in English, and so are messages English lacks, which the page never reads.
435
+
436
+ Raises:
437
+ MessageError: If the file is malformed or names a shipped language.
438
+ """
439
+ if not isinstance(document, dict) or not isinstance(document.get(HEADER), dict):
440
+ raise MessageError('server.language.json')
441
+ code = document[HEADER].get('code')
442
+ if not valid_code(code):
443
+ raise MessageError('server.language.code', code=code)
444
+ if _file(SHIPPED, code) is not None:
445
+ raise MessageError('server.language.shipped', code=code)
446
+ report = check(document, code)
447
+ if report['malformed']:
448
+ first = report['malformed'][0]
449
+ problem = {
450
+ 'id': first.get('problem_id'),
451
+ 'text': first['problem'],
452
+ 'values': first.get('problem_values') or {},
453
+ }
454
+ place = _messages.label('server.language.problem_at', id=first['id'], problem=problem)
455
+ raise MessageError('server.language.malformed', first=place)
456
+ body = json.dumps(document, ensure_ascii=False, indent=4) + '\n'
457
+ if len(body.encode('utf-8')) > MAXIMUM_BYTES:
458
+ raise MessageError('server.language.size', most=MAXIMUM_BYTES // (1024 * 1024))
459
+ target = folder() / f'{code}.json'
460
+ temporary = target.with_name(f'.{code}.json.tmp')
461
+ temporary.write_text(body, encoding='utf-8', newline='\n')
462
+ # A file of the same language whose name differs in case gives way to the new one.
463
+ # The names are read from the folder, since a file system that ignores case, as those of
464
+ # Windows and macOS usually do, would answer for the new name with the old file's spelling.
465
+ for earlier in folder().glob('*.json'):
466
+ if earlier.stem.lower() == code.lower() and earlier.name != target.name:
467
+ earlier.unlink()
468
+ os.replace(temporary, target)
469
+ entry = _entry(document, code, 'home', english())
470
+ entry['report'] = {key: report[key] for key in ('missing', 'unused', 'untranslated')}
471
+ return entry
472
+
473
+
474
+ def remove(code: str) -> dict[str, Any]:
475
+ """Removes a language of the home folder and returns its code."""
476
+ if not valid_code(code):
477
+ raise MessageError('server.language.code', code=code)
478
+ if _file(SHIPPED, code) is not None:
479
+ raise MessageError('server.language.keep', code=code)
480
+ path = _file(folder(), code)
481
+ if path is None:
482
+ raise MessageError('server.language.none', code=code)
483
+ path.unlink()
484
+ return {'code': code, 'removed': True}
485
+
486
+
487
+ def _refusal(error: MessageError, status: int) -> tuple[dict[str, Any], int]:
488
+ return {'error': str(error), 'error_id': error.message_id, 'error_values': error.values}, status
489
+
490
+
491
+ def get(parts: list[str]) -> tuple[Any, int]:
492
+ """Answers `GET /api/languages` and `GET /api/languages/CODE`."""
493
+ if not parts:
494
+ return {'languages': listing()}, 200
495
+ if len(parts) == 1:
496
+ try:
497
+ return read(parts[0]), 200
498
+ except MessageError as error:
499
+ return _refusal(error, 404 if error.message_id == 'server.language.none' else 400)
500
+ return _refusal(MessageError('server.no_resource'), 404)
501
+
502
+
503
+ def post(parts: list[str], body: bytes) -> tuple[Any, int]:
504
+ """Answers `POST /api/languages` with a language file as its body."""
505
+ if parts:
506
+ return _refusal(MessageError('server.no_resource'), 404)
507
+ if len(body) > MAXIMUM_BYTES:
508
+ return _refusal(
509
+ MessageError('server.language.size', most=MAXIMUM_BYTES // (1024 * 1024)), 413
510
+ )
511
+ try:
512
+ document = json.loads(body.decode('utf-8'))
513
+ except (UnicodeDecodeError, ValueError):
514
+ return _refusal(MessageError('server.language.json'), 400)
515
+ try:
516
+ return add(document), 201
517
+ except MessageError as error:
518
+ return _refusal(error, 400)
519
+
520
+
521
+ def delete(parts: list[str]) -> tuple[Any, int]:
522
+ """Answers `DELETE /api/languages/CODE` for a language of the home folder."""
523
+ if len(parts) != 1:
524
+ return _refusal(MessageError('server.no_resource'), 404)
525
+ try:
526
+ return remove(parts[0]), 200
527
+ except MessageError as error:
528
+ return _refusal(error, 404 if error.message_id == 'server.language.none' else 400)
529
+
530
+
531
+ def add_arguments(parser: Any) -> None:
532
+ """Adds the arguments of `cuju render language` to its parser."""
533
+ parser.add_argument('action', choices=('list', 'new', 'check', 'add', 'remove'))
534
+ parser.add_argument(
535
+ 'target',
536
+ nargs='?',
537
+ help='the code of a new or removed language, or the language file to check or add',
538
+ )
539
+ parser.add_argument('-o', '--out', help='the file that new writes, by default CODE.json')
540
+ parser.add_argument('--name', help='the name of a new language in itself, such as Deutsch')
541
+ parser.add_argument('--json', action='store_true', help='print the result as JSON')
542
+
543
+
544
+ def _print_report(report: dict[str, Any], path: str) -> None:
545
+ name = report.get('name') or report.get('code') or path
546
+ print(f'{path}, {name}, {counted(report["messages"], "message")}')
547
+ for key in report['missing']:
548
+ print(f' missing {key}')
549
+ for key in report['unused']:
550
+ print(f' unused {key}')
551
+ for item in report['malformed']:
552
+ print(f' malformed {item["id"]}, {item["problem"]}')
553
+ if report['untranslated']:
554
+ print(f' {counted(len(report["untranslated"]), "message")} read as in English')
555
+ print('ok' if report['ok'] else 'not ok')
556
+
557
+
558
+ def run(options: Any) -> int:
559
+ """Runs `cuju render language`, and returns the exit status.
560
+
561
+ `new CODE` writes a template from English, `check FILE` reports missing, unused and malformed
562
+ messages and exits with 1 when it finds any, `list` prints the languages, `add FILE` keeps a
563
+ checked file in the home folder and `remove CODE` removes one of them.
564
+ """
565
+ action, target = options.action, options.target
566
+ if action != 'list' and not target:
567
+ print(f'cuju render: expected what language {action} reads or writes', file=sys.stderr)
568
+ return 2
569
+ try:
570
+ if action == 'new':
571
+ document = template(target, options.name)
572
+ path = Path(options.out or f'{target}.json')
573
+ path.write_text(
574
+ json.dumps(document, ensure_ascii=False, indent=4) + '\n',
575
+ encoding='utf-8',
576
+ newline='\n',
577
+ )
578
+ if options.json:
579
+ print(
580
+ json.dumps({'code': target, 'path': str(path), 'messages': len(document) - 1})
581
+ )
582
+ else:
583
+ print(f'wrote {path}, {counted(len(document) - 1, "message")} from English')
584
+ return 0
585
+ if action == 'check':
586
+ path = Path(target)
587
+ try:
588
+ document = json.loads(path.read_text(encoding='utf-8'))
589
+ except ValueError as error:
590
+ report = {
591
+ 'code': None,
592
+ 'name': None,
593
+ 'messages': 0,
594
+ 'missing': [],
595
+ 'unused': [],
596
+ 'malformed': [{'id': HEADER, 'problem': f'the file is not JSON, {error}'}],
597
+ 'untranslated': [],
598
+ 'ok': False,
599
+ }
600
+ else:
601
+ code = path.stem if valid_code(path.stem) else None
602
+ report = check(document, code)
603
+ if options.json:
604
+ print(json.dumps(report, ensure_ascii=False, indent=2))
605
+ else:
606
+ _print_report(report, str(path))
607
+ return 0 if report['ok'] else 1
608
+ if action == 'list':
609
+ entries = listing()
610
+ if options.json:
611
+ print(json.dumps(entries, ensure_ascii=False, indent=2))
612
+ else:
613
+ for entry in entries:
614
+ state = 'usable' if entry['usable'] else 'refused'
615
+ print(f'{entry["code"]} {entry["source"]} {state} {entry["name"]}')
616
+ return 0
617
+ if action == 'add':
618
+ entry = add(json.loads(Path(target).read_text(encoding='utf-8')))
619
+ if options.json:
620
+ print(json.dumps(entry, ensure_ascii=False, indent=2))
621
+ else:
622
+ print(
623
+ f'added {entry["code"]} {entry["name"]}, {entry["missing"]} '
624
+ f'{"message shows" if entry["missing"] == 1 else "messages show"} in English'
625
+ )
626
+ return 0
627
+ result = remove(target)
628
+ print(json.dumps(result) if options.json else f'removed {result["code"]}')
629
+ return 0
630
+ except ValueError as error:
631
+ if options.json:
632
+ print(json.dumps({'error': str(error), 'error_id': getattr(error, 'message_id', None)}))
633
+ print(f'cuju render: {error}', file=sys.stderr)
634
+ return 1
635
+ except OSError as error:
636
+ print(f'cuju render: {error}', file=sys.stderr)
637
+ return 1