scriptorium 0.6.1 → 0.7.2

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 (358) hide show
  1. checksums.yaml +4 -4
  2. data/assets/icons/social/reddit.png +0 -0
  3. data/assets/icons/social/x-logo.png +0 -0
  4. data/assets/imagenotfound.jpg +0 -0
  5. data/bin/sblog +84 -5
  6. data/bin/scriptorium +1 -0
  7. data/doc/anti-amnesia/20250727-054000-scriptorium-overview.md +0 -1
  8. data/doc/anti-amnesia/20250727-123000-anti-amnesia-conventions.md +0 -29
  9. data/doc/anti-amnesia/20250727-172600-cursor-rbenv-ruby-version-mystery.md +0 -19
  10. data/doc/anti-amnesia/20250727-172900-ai-cognitive-assessment-capabilities.md +1 -1
  11. data/doc/anti-amnesia/20250728-124243-aaa-syntax-clarification.md +1 -1
  12. data/doc/anti-amnesia/20250729-210000-reddit-autopost-integration-complete.md +1 -1
  13. data/doc/anti-amnesia/20250804-190500-cognitive-loop-bug.md +0 -10
  14. data/doc/anti-amnesia/20250804-190700-anti-amnesia-timestamping-fix.md +1 -4
  15. data/doc/anti-amnesia/20250901-211714-codemirror-integration-and-web-tests.md +172 -0
  16. data/doc/anti-amnesia/20250902-002402-backup-restore-system.md +126 -0
  17. data/doc/anti-amnesia/20250907-203339-backup-metadata-implementation.md +66 -0
  18. data/doc/imported/0001-elixir-conf-2014/metadata.txt +7 -0
  19. data/doc/imported/0001-elixir-conf-2014/post.html +37 -0
  20. data/doc/imported/0001-elixir-conf-2014/source.lt3 +22 -0
  21. data/doc/imported/0002-programmers-and-word-processing/metadata.txt +7 -0
  22. data/doc/imported/0002-programmers-and-word-processing/post.html +192 -0
  23. data/doc/imported/0002-programmers-and-word-processing/source.lt3 +146 -0
  24. data/doc/imported/0003-how-to-turn-your-brain-sideways/metadata.txt +7 -0
  25. data/doc/imported/0003-how-to-turn-your-brain-sideways/post.html +60 -0
  26. data/doc/imported/0003-how-to-turn-your-brain-sideways/source.lt3 +40 -0
  27. data/doc/imported/0004-upcoming-lone-star-ruby-conference/metadata.txt +7 -0
  28. data/doc/imported/0004-upcoming-lone-star-ruby-conference/post.html +42 -0
  29. data/doc/imported/0004-upcoming-lone-star-ruby-conference/source.lt3 +24 -0
  30. data/doc/imported/0005-elixir-conf-2015-announced/metadata.txt +7 -0
  31. data/doc/imported/0005-elixir-conf-2015-announced/post.html +30 -0
  32. data/doc/imported/0005-elixir-conf-2015-announced/source.lt3 +16 -0
  33. data/doc/imported/0006-ruby-for-dinosaurs/metadata.txt +7 -0
  34. data/doc/imported/0006-ruby-for-dinosaurs/post.html +43 -0
  35. data/doc/imported/0006-ruby-for-dinosaurs/source.lt3 +27 -0
  36. data/doc/imported/0007-phoenix-isnt-rails/metadata.txt +7 -0
  37. data/doc/imported/0007-phoenix-isnt-rails/post.html +116 -0
  38. data/doc/imported/0007-phoenix-isnt-rails/source.lt3 +87 -0
  39. data/doc/imported/0008-concerning-the-term-monkeypatching/metadata.txt +7 -0
  40. data/doc/imported/0008-concerning-the-term-monkeypatching/post.html +129 -0
  41. data/doc/imported/0008-concerning-the-term-monkeypatching/source.lt3 +92 -0
  42. data/doc/imported/0009-announcement-coming-soon/metadata.txt +7 -0
  43. data/doc/imported/0009-announcement-coming-soon/post.html +33 -0
  44. data/doc/imported/0009-announcement-coming-soon/source.lt3 +19 -0
  45. data/doc/imported/0010-immutable-data-ditching-the-wax-tablet/metadata.txt +7 -0
  46. data/doc/imported/0010-immutable-data-ditching-the-wax-tablet/post.html +175 -0
  47. data/doc/imported/0010-immutable-data-ditching-the-wax-tablet/source.lt3 +139 -0
  48. data/doc/imported/0011-computer-science-as-a-lost-art/metadata.txt +7 -0
  49. data/doc/imported/0011-computer-science-as-a-lost-art/post.html +139 -0
  50. data/doc/imported/0011-computer-science-as-a-lost-art/source.lt3 +104 -0
  51. data/doc/imported/0012-ruby-day-in-turin-italy/metadata.txt +7 -0
  52. data/doc/imported/0012-ruby-day-in-turin-italy/post.html +42 -0
  53. data/doc/imported/0012-ruby-day-in-turin-italy/source.lt3 +24 -0
  54. data/doc/imported/0013-rubyday-was-a-success/metadata.txt +7 -0
  55. data/doc/imported/0013-rubyday-was-a-success/post.html +44 -0
  56. data/doc/imported/0013-rubyday-was-a-success/source.lt3 +27 -0
  57. data/doc/imported/0014-working-on-the-blogging-software/metadata.txt +7 -0
  58. data/doc/imported/0014-working-on-the-blogging-software/post.html +63 -0
  59. data/doc/imported/0014-working-on-the-blogging-software/source.lt3 +41 -0
  60. data/doc/imported/0015-ok-its-not-really-a-lost-art/metadata.txt +7 -0
  61. data/doc/imported/0015-ok-its-not-really-a-lost-art/post.html +172 -0
  62. data/doc/imported/0015-ok-its-not-really-a-lost-art/source.lt3 +134 -0
  63. data/doc/imported/0016-an-in-operator-for-ruby/metadata.txt +7 -0
  64. data/doc/imported/0016-an-in-operator-for-ruby/post.html +155 -0
  65. data/doc/imported/0016-an-in-operator-for-ruby/source.lt3 +106 -0
  66. data/doc/imported/0017-the-forgotten-mathematician/metadata.txt +7 -0
  67. data/doc/imported/0017-the-forgotten-mathematician/post.html +161 -0
  68. data/doc/imported/0017-the-forgotten-mathematician/source.lt3 +119 -0
  69. data/doc/imported/0018-ruby-puns/metadata.txt +7 -0
  70. data/doc/imported/0018-ruby-puns/post.html +46 -0
  71. data/doc/imported/0018-ruby-puns/source.lt3 +28 -0
  72. data/doc/imported/0019-custom-exceptions-via-metaprogramming/metadata.txt +7 -0
  73. data/doc/imported/0019-custom-exceptions-via-metaprogramming/post.html +138 -0
  74. data/doc/imported/0019-custom-exceptions-via-metaprogramming/source.lt3 +101 -0
  75. data/doc/imported/0020-fffff/metadata.txt +7 -0
  76. data/doc/imported/0020-fffff/post.html +24 -0
  77. data/doc/imported/0020-fffff/source.lt3 +12 -0
  78. data/doc/imported/0021-trying-ror-yet-again/metadata.txt +7 -0
  79. data/doc/imported/0021-trying-ror-yet-again/post.html +26 -0
  80. data/doc/imported/0021-trying-ror-yet-again/source.lt3 +12 -0
  81. data/doc/imported/0023-doctor-sleep/metadata.txt +7 -0
  82. data/doc/imported/0023-doctor-sleep/post.html +63 -0
  83. data/doc/imported/0023-doctor-sleep/source.lt3 +44 -0
  84. data/doc/imported/0024-just-a-test/metadata.txt +7 -0
  85. data/doc/imported/0024-just-a-test/post.html +24 -0
  86. data/doc/imported/0024-just-a-test/source.lt3 +12 -0
  87. data/doc/imported/import_summary.txt +98 -0
  88. data/doc/livetext-informal-spec.txt +65 -0
  89. data/doc/myuserdoc/ch-0.lt3 +31 -0
  90. data/doc/myuserdoc/ch-1.lt3 +37 -0
  91. data/doc/myuserdoc/ch-10.lt3 +22 -0
  92. data/doc/myuserdoc/ch-2.lt3 +37 -0
  93. data/doc/myuserdoc/ch-3.lt3 +19 -0
  94. data/doc/myuserdoc/ch-4.lt3 +43 -0
  95. data/doc/myuserdoc/ch-5.lt3 +22 -0
  96. data/doc/myuserdoc/ch-6.lt3 +19 -0
  97. data/doc/myuserdoc/ch-7.lt3 +16 -0
  98. data/doc/myuserdoc/ch-8.lt3 +13 -0
  99. data/doc/myuserdoc/ch-9.lt3 +19 -0
  100. data/doc/myuserdoc/tweak.rb +18 -0
  101. data/doc/{userdoc-toc.txt → myuserdoc/userdoc-toc.txt} +27 -27
  102. data/doc/old-posts/0001-elixir-conf-2014.lt3 +24 -0
  103. data/doc/old-posts/0002-programmers-and-word-processing.lt3 +150 -0
  104. data/doc/old-posts/0003-how-to-turn-your-brain-sideways.lt3 +43 -0
  105. data/doc/old-posts/0004-upcoming-lone-star-ruby-conference.lt3 +26 -0
  106. data/doc/old-posts/0005-elixir-conf-2015-announced.lt3 +17 -0
  107. data/doc/old-posts/0006-ruby-for-dinosaurs.lt3 +30 -0
  108. data/doc/old-posts/0007-phoenix-isnt-rails.lt3 +90 -0
  109. data/doc/old-posts/0008-concerning-the-term-monkeypatching.lt3 +105 -0
  110. data/doc/old-posts/0009-announcement-coming-soon.lt3 +20 -0
  111. data/doc/old-posts/0010-immutable-data-ditching-the-wax-tablet.lt3 +142 -0
  112. data/doc/old-posts/0011-computer-science-as-a-lost-art.lt3 +117 -0
  113. data/doc/old-posts/0012-ruby-day-in-turin-italy.lt3 +26 -0
  114. data/doc/old-posts/0013-rubyday-was-a-success.lt3 +28 -0
  115. data/doc/old-posts/0014-working-on-the-blogging-software.lt3 +42 -0
  116. data/doc/old-posts/0015-ok-its-not-really-a-lost-art.lt3 +137 -0
  117. data/doc/old-posts/0016-an-in-operator-for-ruby.lt3 +142 -0
  118. data/doc/old-posts/0017-the-forgotten-mathematician.lt3 +129 -0
  119. data/doc/old-posts/0018-ruby-puns.lt3 +31 -0
  120. data/doc/old-posts/0019-custom-exceptions-via-metaprogramming.lt3 +116 -0
  121. data/doc/old-posts/0021-trying-ror-yet-again.lt3 +35 -0
  122. data/doc/old-posts/0023-doctor-sleep.lt3 +43 -0
  123. data/doc/old-posts/0024-just-a-test.lt3 +12 -0
  124. data/doc/old-posts/0025-trying-another-post.lt3 +12 -0
  125. data/doc/old-repo +1 -0
  126. data/doc/reddit_integration.md +2 -2
  127. data/doc/user.lt3 +0 -3
  128. data/lib/scriptorium/api.rb +1811 -78
  129. data/lib/scriptorium/banner_svg.rb +55 -68
  130. data/lib/scriptorium/contract.rb +3 -2
  131. data/lib/scriptorium/exceptions.rb +133 -102
  132. data/lib/scriptorium/helpers.rb +282 -82
  133. data/lib/scriptorium/post.rb +81 -17
  134. data/lib/scriptorium/reddit.rb +1 -1
  135. data/lib/scriptorium/repo.rb +478 -164
  136. data/lib/scriptorium/standard_files.rb +30 -396
  137. data/lib/scriptorium/support/common_js/clipboard.js +35 -0
  138. data/lib/scriptorium/support/common_js/content-loader.js +187 -0
  139. data/lib/scriptorium/support/common_js/navigation.js +52 -0
  140. data/lib/scriptorium/support/common_js/syntax-highlighting.js +27 -0
  141. data/lib/scriptorium/support/config/reddit_template.txt +17 -0
  142. data/{test/scriptorium-TEST-1754622690-146/views/sample → lib/scriptorium/support}/config/social.txt +1 -0
  143. data/lib/scriptorium/support/highlight/css.txt +2 -0
  144. data/lib/scriptorium/support/highlight/custom.css +119 -0
  145. data/lib/scriptorium/support/highlight/js.txt +1 -0
  146. data/lib/scriptorium/support/post_index/config.txt +15 -0
  147. data/lib/scriptorium/support/post_index/style.css +55 -0
  148. data/lib/scriptorium/support/templates/index_entry.lt3 +16 -0
  149. data/{test/scriptorium-TEST-1754622690-146/themes/standard/initial/post.lt3 → lib/scriptorium/support/templates/initial_post.lt3} +5 -5
  150. data/lib/scriptorium/support/templates/post.lt3 +104 -0
  151. data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/header.txt → lib/scriptorium/support/theme/header.lt3} +1 -1
  152. data/lib/scriptorium/theme.rb +83 -70
  153. data/lib/scriptorium/version.rb +2 -2
  154. data/lib/scriptorium/view.rb +194 -149
  155. data/lib/scriptorium.rb +24 -1
  156. data/lib/skeleton.rb +4 -1
  157. data/scriptorium.gemspec +2 -1
  158. data/test/WEB_INTEGRATION_README.md +196 -0
  159. data/test/all +40 -0
  160. data/test/banner_svg/unit.rb +267 -35
  161. data/test/config/deployment.txt +5 -0
  162. data/test/integration/integration_test.rb +7 -7
  163. data/test/integration/preview_flow_test.rb +94 -0
  164. data/test/livetext_plugin_test.rb +453 -182
  165. data/test/manual/banner-tests/test01.html +82 -18
  166. data/test/manual/banner-tests/test02.html +82 -18
  167. data/test/manual/banner-tests/test03.html +82 -18
  168. data/test/manual/banner-tests/test04.html +89 -25
  169. data/test/manual/banner-tests/test05.html +89 -25
  170. data/test/manual/banner-tests/test06.html +89 -25
  171. data/test/manual/banner-tests/test07.html +89 -25
  172. data/test/manual/banner-tests/test08.html +82 -18
  173. data/test/manual/banner-tests/test09.html +82 -18
  174. data/test/manual/banner-tests/test10.html +82 -18
  175. data/test/manual/banner-tests/test11.html +82 -18
  176. data/test/manual/banner-tests/test12.html +82 -18
  177. data/test/manual/banner-tests/test13.html +82 -18
  178. data/test/manual/banner-tests/test14.html +82 -18
  179. data/test/manual/banner-tests/test15.html +82 -18
  180. data/test/manual/banner-tests/test16.html +82 -18
  181. data/test/manual/banner-tests/test17.html +82 -18
  182. data/test/manual/banner-tests/test18.html +90 -26
  183. data/test/manual/banner-tests/test19.html +90 -26
  184. data/test/manual/banner-tests/test20.html +90 -26
  185. data/test/manual/banner-tests/test21.html +90 -26
  186. data/test/manual/banner-tests/test22.html +90 -26
  187. data/test/manual/banner-tests/test23.html +90 -26
  188. data/test/manual/banner-tests/test24.html +90 -26
  189. data/test/manual/banner-tests/test25.html +89 -25
  190. data/test/manual/banner_environment.rb +15 -2
  191. data/test/manual/codemirror_demo.html +773 -0
  192. data/test/manual/create_posts_for_web.rb +114 -0
  193. data/test/manual/preview_manual_test.rb +129 -0
  194. data/test/manual/test_banner_features.rb +14 -14
  195. data/test/manual/test_banner_integration.rb +115 -0
  196. data/test/manual/test_banner_radial.rb +87 -0
  197. data/test/manual/test_syntax_highlighting.rb +60 -40
  198. data/test/support/preview_utils.rb +88 -0
  199. data/test/test_gem_assets.rb +48 -0
  200. data/test/test_helpers.rb +10 -0
  201. data/test/tui_editor_integration_test.rb +15 -15
  202. data/test/tui_integration_test.rb +687 -441
  203. data/test/unit/api.rb +757 -37
  204. data/test/unit/asset_management.rb +195 -221
  205. data/test/unit/backup_test.rb +451 -0
  206. data/test/unit/contract_test.rb +1 -23
  207. data/test/unit/core.rb +415 -61
  208. data/test/unit/deploy_config_test.rb +248 -0
  209. data/test/unit/deploy_test.rb +312 -21
  210. data/test/unit/edit_post_test.rb +168 -0
  211. data/test/unit/gem_asset_management.rb +36 -42
  212. data/test/unit/livetext_basic.rb +23 -35
  213. data/test/unit/livetext_compatibility.rb +7 -14
  214. data/test/unit/parse_cmd_test.rb +260 -0
  215. data/test/unit/{symlink_test.rb → permalink_copy_test.rb} +47 -49
  216. data/test/unit/post.rb +91 -26
  217. data/test/unit/post_index_config_test.rb +258 -0
  218. data/test/unit/post_state_helpers_test.rb +137 -0
  219. data/test/unit/read_commented_file_test.rb +8 -6
  220. data/test/unit/repo.rb +75 -54
  221. data/test/unit/social_test.rb +41 -44
  222. data/test/unit/syntax_highlighting.rb +70 -0
  223. data/test/unit/theme_management_test.rb +91 -0
  224. data/test/unit/view.rb +79 -12
  225. data/test/unit/widgets.rb +8 -8
  226. data/test/web_integration_test.rb +231 -0
  227. data/test/web_test_helper.rb +218 -0
  228. data/test/web_workflow_test.rb +527 -0
  229. data/ui/tui/bin/scriptorium +885 -415
  230. data/ui/web/app/app.rb +1398 -176
  231. data/ui/web/app/assets/livetext_mode.js +244 -0
  232. data/ui/web/app/error_helpers.rb +16 -16
  233. data/ui/web/app/views/advanced_config.erb +8 -2
  234. data/ui/web/app/views/asset_management.erb +56 -0
  235. data/ui/web/app/views/backup_management.erb +238 -0
  236. data/ui/web/app/views/config_widget.erb +232 -0
  237. data/ui/web/app/views/dashboard.erb +64 -72
  238. data/ui/web/app/views/deploy_config.erb +3 -0
  239. data/ui/web/app/views/edit_pages.erb +170 -2
  240. data/ui/web/app/views/edit_post.erb +130 -9
  241. data/ui/web/app/views/edit_theme.erb +73 -0
  242. data/ui/web/app/views/edit_theme_file.erb +74 -0
  243. data/ui/web/app/views/theme_management.erb +130 -0
  244. data/ui/web/app/views/view_dashboard.erb +666 -25
  245. data/ui/web/app/views/widgets.erb +249 -0
  246. data/ui/web/bin/scriptorium-web +35 -24
  247. data/ui/web/tmp/timing.log +17 -0
  248. data/ui/web/tmp/web_server.log +0 -5
  249. metadata +190 -116
  250. data/assets/back-icon.png +0 -0
  251. data/assets/icons/facebook.svg +0 -1
  252. data/assets/icons/github.svg +0 -1
  253. data/assets/icons/instagram.svg +0 -1
  254. data/assets/icons/reddit.svg +0 -1
  255. data/assets/icons/x.svg +0 -1
  256. data/assets/icons/youtube.svg +0 -1
  257. data/bin/scriptorium +0 -1511
  258. data/doc/anti-amnesia/20250727-060000-api-design-tui-planning.md +0 -34
  259. data/doc/anti-amnesia/20250727-061000-runeblog-tui-analysis.md +0 -50
  260. data/doc/anti-amnesia/20250727-154000-livetext-plugin-file-stats.md +0 -73
  261. data/doc/anti-amnesia/20250727-172600-unified-minitest-framework.md +0 -70
  262. data/doc/anti-amnesia/20250727-173000-widget-testing-achievement.md +0 -110
  263. data/doc/anti-amnesia/20250727-180000-post-id-num-refactoring.md +0 -73
  264. data/doc/anti-amnesia/20250728-124421-conversation-summary-concise.md +0 -124
  265. data/doc/anti-amnesia/20250729-190000-scriptorium-tui-testing-complete.md +0 -46
  266. data/doc/anti-amnesia/20250729-200000-scriptorium-tui-testing-edit-file-workflow.md +0 -97
  267. data/doc/anti-amnesia/20250729-211500-dependency-management-system.md +0 -211
  268. data/doc/anti-amnesia/20250729-213000-python-virtual-environment-setup.md +0 -141
  269. data/doc/anti-amnesia/20250729-214500-theme-management-commands.md +0 -211
  270. data/doc/anti-amnesia/20250729-215000-version-update-to-0.6.0.md +0 -134
  271. data/doc/anti-amnesia/20250729-220000-user-guide-complete.md +0 -41
  272. data/doc/anti-amnesia/20250804-213700-publishing-test-fix.md +0 -49
  273. data/doc/anti-amnesia/20250804-214400-additional-test-fixes.md +0 -46
  274. data/doc/anti-amnesia/20250804-220000-asset-function-logic-clarification.md +0 -41
  275. data/doc/anti-amnesia/20250806-202032-asset-function-logic-clarification.md +0 -41
  276. data/doc/anti-amnesia/20250813-082428-syntax-highlighting-and-navigation-improvements.md +0 -256
  277. data/lib/scriptorium/syntax_highlighter.rb +0 -234
  278. data/test/manual/deploy_symlink_demo.rb +0 -142
  279. data/test/manual/symlink_demo.rb +0 -117
  280. data/test/manual/test2.rb +0 -12
  281. data/test/manual/test_banner_from_file.rb +0 -150
  282. data/test/manual/test_banner_in_header.rb +0 -35
  283. data/test/manual/test_code_highlighting.rb +0 -68
  284. data/test/manual/test_complex_header.rb +0 -74
  285. data/test/manual/test_empty_header.rb +0 -32
  286. data/test/manual/test_radial_custom.rb +0 -58
  287. data/test/manual/test_radial_large_radius.rb +0 -52
  288. data/test/manual/test_svg_debug.rb +0 -47
  289. data/test/pages-demo/config/currentview.txt +0 -1
  290. data/test/pages-demo/views/demo/config/common.js +0 -57
  291. data/test/pages-demo/views/demo/config/footer.txt +0 -1
  292. data/test/pages-demo/views/demo/config/global-head.txt +0 -8
  293. data/test/pages-demo/views/demo/config/header.txt +0 -1
  294. data/test/pages-demo/views/demo/config/layout.txt +0 -1
  295. data/test/pages-demo/views/demo/config/left.txt +0 -1
  296. data/test/pages-demo/views/demo/config/main.txt +0 -1
  297. data/test/pages-demo/views/demo/config/right.txt +0 -1
  298. data/test/pages-demo/views/demo/config.txt +0 -3
  299. data/test/pages-demo/views/demo/output/panes/footer.html +0 -1
  300. data/test/pages-demo/views/demo/output/panes/header.html +0 -1
  301. data/test/pages-demo/views/demo/output/panes/left.html +0 -1
  302. data/test/pages-demo/views/demo/output/panes/main.html +0 -1
  303. data/test/pages-demo/views/demo/output/panes/right.html +0 -1
  304. data/test/scriptorium-TEST-1754622690-146/config/bootstrap_css.txt +0 -5
  305. data/test/scriptorium-TEST-1754622690-146/config/bootstrap_js.txt +0 -4
  306. data/test/scriptorium-TEST-1754622690-146/config/common.js +0 -57
  307. data/test/scriptorium-TEST-1754622690-146/config/currentview.txt +0 -1
  308. data/test/scriptorium-TEST-1754622690-146/config/global-head.txt +0 -9
  309. data/test/scriptorium-TEST-1754622690-146/config/last_post_num.txt +0 -1
  310. data/test/scriptorium-TEST-1754622690-146/config/os_helpers.rb +0 -4
  311. data/test/scriptorium-TEST-1754622690-146/config/widgets.txt +0 -3
  312. data/test/scriptorium-TEST-1754622690-146/posts/0001/meta.txt +0 -8
  313. data/test/scriptorium-TEST-1754622690-146/posts/0001/source.lt3 +0 -6
  314. data/test/scriptorium-TEST-1754622690-146/themes/standard/README.txt +0 -1
  315. data/test/scriptorium-TEST-1754622690-146/themes/standard/config.txt +0 -1
  316. data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/gen/text.css +0 -1
  317. data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/index.lt3 +0 -1
  318. data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/index_entry.lt3 +0 -14
  319. data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/post.lt3 +0 -13
  320. data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/widget.lt3 +0 -1
  321. data/test/scriptorium-TEST-1754622690-146/views/sample/config/bootstrap_css.txt +0 -5
  322. data/test/scriptorium-TEST-1754622690-146/views/sample/config/bootstrap_js.txt +0 -4
  323. data/test/scriptorium-TEST-1754622690-146/views/sample/config/common.js +0 -57
  324. data/test/scriptorium-TEST-1754622690-146/views/sample/config/deploy.txt +0 -5
  325. data/test/scriptorium-TEST-1754622690-146/views/sample/config/footer.txt +0 -2
  326. data/test/scriptorium-TEST-1754622690-146/views/sample/config/global-head.txt +0 -9
  327. data/test/scriptorium-TEST-1754622690-146/views/sample/config/header.txt +0 -4
  328. data/test/scriptorium-TEST-1754622690-146/views/sample/config/layout.txt +0 -5
  329. data/test/scriptorium-TEST-1754622690-146/views/sample/config/left.txt +0 -3
  330. data/test/scriptorium-TEST-1754622690-146/views/sample/config/main.txt +0 -5
  331. data/test/scriptorium-TEST-1754622690-146/views/sample/config/right.txt +0 -3
  332. data/test/scriptorium-TEST-1754622690-146/views/sample/config/status.txt +0 -7
  333. data/test/scriptorium-TEST-1754622690-146/views/sample/config.txt +0 -3
  334. data/test/scriptorium-TEST-1754622690-146/views/sample/layout/footer.html +0 -3
  335. data/test/scriptorium-TEST-1754622690-146/views/sample/layout/header.html +0 -3
  336. data/test/scriptorium-TEST-1754622690-146/views/sample/layout/left.html +0 -3
  337. data/test/scriptorium-TEST-1754622690-146/views/sample/layout/main.html +0 -3
  338. data/test/scriptorium-TEST-1754622690-146/views/sample/layout/right.html +0 -3
  339. data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/footer.html +0 -1
  340. data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/header.html +0 -1
  341. data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/left.html +0 -1
  342. data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/main.html +0 -1
  343. data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/right.html +0 -1
  344. data/ui/web/tmp/web_server.pid +0 -1
  345. /data/{test/pages-demo/views/demo/config/bootstrap_css.txt → lib/scriptorium/support/bootstrap/css.txt} +0 -0
  346. /data/{test/pages-demo/views/demo/config/bootstrap_js.txt → lib/scriptorium/support/bootstrap/js.txt} +0 -0
  347. /data/{test/scriptorium-TEST-1754622690-146/views/sample → lib/scriptorium/support}/config/reddit.txt +0 -0
  348. /data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout → lib/scriptorium/support/templates}/layout.txt +0 -0
  349. /data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/footer.txt → lib/scriptorium/support/theme/footer.lt3} +0 -0
  350. /data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/left.txt → lib/scriptorium/support/theme/left.lt3} +0 -0
  351. /data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/main.txt → lib/scriptorium/support/theme/main.lt3} +0 -0
  352. /data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/right.txt → lib/scriptorium/support/theme/right.lt3} +0 -0
  353. /data/test/manual/banner-tests/{config.txt → svg.txt} +0 -0
  354. /data/test/manual/{test6.rb → test_advanced_widgets.rb} +0 -0
  355. /data/test/manual/{test1.rb → test_basic_posts.rb} +0 -0
  356. /data/test/manual/{test4.rb → test_layout_widgets.rb} +0 -0
  357. /data/test/manual/{test5.rb → test_pagination.rb} +0 -0
  358. /data/test/manual/{test3.rb → test_random_posts.rb} +0 -0
data/test/unit/api.rb CHANGED
@@ -51,7 +51,24 @@ class TestScriptoriumAPI < Minitest::Test
51
51
  assert_equal "test", post.tags
52
52
  end
53
53
 
54
- def test_004_posts
54
+ def test_004_create_page
55
+ @api.create_view("test_view", "Test View")
56
+
57
+ page_name = @api.create_page("test_view", "about", "About Us", "This is our about page content.")
58
+
59
+ assert_equal "about", page_name
60
+
61
+ # Check that the page file was created
62
+ page_file = "test/scriptorium-TEST/views/test_view/pages/about.lt3"
63
+ assert File.exist?(page_file), "Page file should exist"
64
+
65
+ # Check the content
66
+ content = read_file(page_file)
67
+ assert_includes content, ".title About Us"
68
+ assert_includes content, "This is our about page content."
69
+ end
70
+
71
+ def test_005_posts
55
72
  @api.create_view("test_view", "Test View")
56
73
  @api.create_post("Post 1", "Body 1")
57
74
  @api.create_post("Post 2", "Body 2")
@@ -152,10 +169,54 @@ class TestScriptoriumAPI < Minitest::Test
152
169
  def test_014_themes_available
153
170
  themes = @api.themes_available
154
171
  assert_instance_of Array, themes
155
- assert_includes themes, "standard" # Should have the standard theme
172
+
173
+ # Should have the standard theme
174
+ assert_includes themes, "standard"
175
+
176
+ # Check system vs user themes
177
+ system_themes = @api.system_themes
178
+ user_themes = @api.user_themes
179
+
180
+ assert_includes system_themes, "standard"
181
+ assert_empty user_themes # No user themes yet
182
+ end
183
+
184
+ def test_015_clone_theme
185
+ # Clone the standard theme
186
+ result = @api.clone_theme("standard", "my-custom")
187
+ assert_equal "my-custom", result
188
+
189
+ # Check that the new theme exists
190
+ themes = @api.themes_available
191
+ assert_includes themes, "my-custom"
192
+
193
+ # Check that it's now a user theme
194
+ user_themes = @api.user_themes
195
+ assert_includes user_themes, "my-custom"
196
+
197
+ # Check that standard is still a system theme
198
+ system_themes = @api.system_themes
199
+ assert_includes system_themes, "standard"
200
+ end
201
+
202
+ def test_016_clone_theme_validation
203
+ # Try to clone to existing theme name
204
+ assert_raises(ThemeAlreadyExists) do
205
+ @api.clone_theme("standard", "standard")
206
+ end
207
+
208
+ # Try to clone from non-existent theme
209
+ assert_raises(ThemeNotFound) do
210
+ @api.clone_theme("nonexistent", "new-theme")
211
+ end
212
+
213
+ # Try to clone with invalid name
214
+ assert_raises(ThemeNameInvalid) do
215
+ @api.clone_theme("standard", "invalid name with spaces")
216
+ end
156
217
  end
157
218
 
158
- def test_015_widgets_available
219
+ def test_017_widgets_available
159
220
  widgets = @api.widgets_available
160
221
  assert_instance_of Array, widgets
161
222
  # Should return available widgets from widgets.txt
@@ -185,7 +246,7 @@ class TestScriptoriumAPI < Minitest::Test
185
246
  # Clear the current view directly
186
247
  @api.repo.instance_variable_set(:@current_view, nil)
187
248
 
188
- assert_raises(RuntimeError) do
249
+ assert_raises(ViewTargetNil) do
189
250
  @api.create_post("Test Post", "Test body")
190
251
  end
191
252
  end
@@ -489,12 +550,12 @@ class TestScriptoriumAPI < Minitest::Test
489
550
  @api.create_view("test_view", "Test View")
490
551
 
491
552
  # Test with non-draft file
492
- assert_raises(RuntimeError) do
553
+ assert_raises(DraftFileInvalid) do
493
554
  @api.delete_draft("not-a-draft.txt")
494
555
  end
495
556
 
496
557
  # Test with non-existent file
497
- assert_raises(RuntimeError) do
558
+ assert_raises(DraftFileNotFound) do
498
559
  @api.delete_draft("nonexistent-draft.lt3")
499
560
  end
500
561
  end
@@ -532,17 +593,17 @@ class TestScriptoriumAPI < Minitest::Test
532
593
  @api.create_view("test_view", "Test View")
533
594
 
534
595
  # Test with invalid widget name
535
- assert_raises(RuntimeError) do
596
+ assert_raises(WidgetNameInvalid) do
536
597
  @api.generate_widget("invalid-widget")
537
598
  end
538
599
 
539
600
  # Test with nil
540
- assert_raises(RuntimeError) do
601
+ assert_raises(WidgetNameNil) do
541
602
  @api.generate_widget(nil)
542
603
  end
543
604
 
544
605
  # Test with empty string
545
- assert_raises(RuntimeError) do
606
+ assert_raises(WidgetsArgEmpty) do
546
607
  @api.generate_widget("")
547
608
  end
548
609
  end
@@ -551,7 +612,7 @@ class TestScriptoriumAPI < Minitest::Test
551
612
  @api.create_view("test_view", "Test View")
552
613
 
553
614
  # Test with non-existent widget class
554
- assert_raises(RuntimeError) do
615
+ assert_raises(CannotBuildWidget) do
555
616
  @api.generate_widget("nonexistent")
556
617
  end
557
618
  end
@@ -628,7 +689,7 @@ class TestScriptoriumAPI < Minitest::Test
628
689
  # Create a post so the search actually processes something
629
690
  @api.create_post("Test Post", "Test body")
630
691
 
631
- assert_raises(RuntimeError) do
692
+ assert_raises(UnknownSearchField) do
632
693
  @api.search_posts(unknown_field: "value")
633
694
  end
634
695
  end
@@ -723,19 +784,19 @@ class TestScriptoriumAPI < Minitest::Test
723
784
 
724
785
  # edit_file tests
725
786
  def test_048_edit_file_validation_nil_path
726
- assert_raises(CannotEditFilePathNil) do
787
+ assert_raises(EditFilePathNil) do
727
788
  @api.edit_file(nil)
728
789
  end
729
790
  end
730
791
 
731
792
  def test_049_edit_file_validation_empty_path
732
- assert_raises(CannotEditFilePathEmpty) do
793
+ assert_raises(EditFilePathEmpty) do
733
794
  @api.edit_file("")
734
795
  end
735
796
  end
736
797
 
737
798
  def test_050_edit_file_validation_whitespace_path
738
- assert_raises(CannotEditFilePathEmpty) do
799
+ assert_raises(EditFilePathEmpty) do
739
800
  @api.edit_file(" ")
740
801
  end
741
802
  end
@@ -801,7 +862,7 @@ class TestScriptoriumAPI < Minitest::Test
801
862
  # Clear the current view
802
863
  @api.repo.instance_variable_set(:@current_view, nil)
803
864
 
804
- assert_raises(RuntimeError, "No view specified and no current view set") do
865
+ assert_raises(ViewTargetNil, "No view specified and no current view set") do
805
866
  @api.edit_layout
806
867
  end
807
868
  end
@@ -833,7 +894,7 @@ class TestScriptoriumAPI < Minitest::Test
833
894
  # Clear the current view
834
895
  @api.repo.instance_variable_set(:@current_view, nil)
835
896
 
836
- assert_raises(RuntimeError, "No view specified and no current view set") do
897
+ assert_raises(ViewTargetNil, "No view specified and no current view set") do
837
898
  @api.edit_config
838
899
  end
839
900
  end
@@ -864,7 +925,7 @@ class TestScriptoriumAPI < Minitest::Test
864
925
  def test_058_edit_widget_data_nil_widget
865
926
  @api.create_view("test_view", "Test View")
866
927
 
867
- assert_raises(RuntimeError, "Widget name cannot be nil") do
928
+ assert_raises(WidgetNameNil, "Widget name cannot be nil") do
868
929
  @api.edit_widget_data(nil, nil)
869
930
  end
870
931
  end
@@ -892,7 +953,7 @@ class TestScriptoriumAPI < Minitest::Test
892
953
  post = @api.create_post("Test Post", "Test body")
893
954
 
894
955
  # Create source.lt3 file to test smart selection
895
- source_path = "posts/#{post.num}/source.lt3"
956
+ source_path = "#{@test_dir}/posts/#{post.num}/source.lt3"
896
957
  write_file(source_path, "Test source content")
897
958
 
898
959
  # Mock edit_file to track calls
@@ -907,20 +968,18 @@ class TestScriptoriumAPI < Minitest::Test
907
968
  @api.create_view("test_view", "Test View")
908
969
  post = @api.create_post("Test Post", "Test body")
909
970
 
910
- # Ensure source.lt3 doesn't exist, only body.html
911
- source_path = "posts/#{post.num}/source.lt3"
971
+ # Ensure source.lt3 doesn't exist
972
+ source_path = "#{@test_dir}/posts/#{post.num}/source.lt3"
912
973
  File.delete(source_path) if File.exist?(source_path)
913
974
 
914
- # Mock edit_file to track calls
915
- called_path = nil
916
- @api.stub :edit_file, ->(path) { called_path = path } do
975
+ # Should raise error since source.lt3 is required
976
+ assert_raises(RuntimeError) do
917
977
  @api.edit_post(post.id)
918
- assert_equal "posts/#{post.num}/body.html", called_path
919
978
  end
920
979
  end
921
980
 
922
981
  def test_064_edit_post_nonexistent
923
- assert_raises(NoMethodError) do
982
+ assert_raises(CannotGetPost) do
924
983
  @api.edit_post(999)
925
984
  end
926
985
  end
@@ -948,22 +1007,39 @@ class TestScriptoriumAPI < Minitest::Test
948
1007
 
949
1008
  def test_069_publish_post_already_published
950
1009
  @api.create_view("test_view", "Test View")
1010
+
1011
+ # Set test_view as current view
1012
+ @api.repo.instance_variable_set(:@current_view, @api.repo.lookup_view("test_view"))
1013
+
951
1014
  post = @api.create_post("Test Post", "Test body")
952
1015
 
953
1016
  # Publish once
954
1017
  @api.publish_post(post.id)
955
1018
 
956
1019
  # Try to publish again
957
- assert_raises(RuntimeError, "Post #{post.id} is already published") do
1020
+ assert_raises(PostAlreadyPublished, "Post #{post.id} is already published") do
958
1021
  @api.publish_post(post.id)
959
1022
  end
960
1023
  end
961
1024
 
962
1025
  def test_070_publish_post_nonexistent
963
- assert_raises(RequiredFileNotFound) do
1026
+ assert_raises(CannotGetPost) do
964
1027
  @api.publish_post(999)
965
1028
  end
966
1029
  end
1030
+
1031
+ def test_070_5_unpublish_post
1032
+ @api.create_view("test_view", "Test View")
1033
+ post = @api.create_post("Test Post", "Test body")
1034
+
1035
+ # Publish the post
1036
+ @api.publish_post(post.id)
1037
+ assert @api.post_published?(post.id)
1038
+
1039
+ # Unpublish the post
1040
+ @api.unpublish_post(post.id)
1041
+ refute @api.post_published?(post.id)
1042
+ end
967
1043
 
968
1044
  def test_071_post_published_status
969
1045
  @api.create_view("test_view", "Test View")
@@ -979,7 +1055,7 @@ class TestScriptoriumAPI < Minitest::Test
979
1055
  assert @api.post_published?(post.id)
980
1056
  end
981
1057
 
982
- def test_072_get_published_posts
1058
+ def test_072_posts_with_published_parameter
983
1059
  @api.create_view("test_view", "Test View")
984
1060
 
985
1061
  # Create multiple posts
@@ -988,7 +1064,7 @@ class TestScriptoriumAPI < Minitest::Test
988
1064
  post3 = @api.create_post("Post 3", "Body 3")
989
1065
 
990
1066
  # Initially no published posts
991
- published_posts = @api.get_published_posts
1067
+ published_posts = @api.posts(published: true)
992
1068
  assert_equal 0, published_posts.length
993
1069
 
994
1070
  # Publish two posts
@@ -996,33 +1072,296 @@ class TestScriptoriumAPI < Minitest::Test
996
1072
  @api.publish_post(post3.id)
997
1073
 
998
1074
  # Should have 2 published posts
999
- published_posts = @api.get_published_posts
1075
+ published_posts = @api.posts(published: true)
1000
1076
  assert_equal 2, published_posts.length
1001
1077
  assert_includes published_posts.map(&:id), post1.id
1002
1078
  assert_includes published_posts.map(&:id), post3.id
1003
1079
  refute_includes published_posts.map(&:id), post2.id
1004
1080
  end
1005
1081
 
1006
- def test_073_get_published_posts_with_view
1082
+ def test_073_posts_with_published_parameter_and_view
1007
1083
  @api.create_view("test_view", "Test View")
1008
1084
  @api.create_view("other_view", "Other View")
1009
1085
 
1086
+ # Set test_view as current view
1087
+ @api.repo.instance_variable_set(:@current_view, @api.repo.lookup_view("test_view"))
1088
+
1010
1089
  # Create posts in different views
1011
1090
  post1 = @api.create_post("Post 1", "Body 1", views: "test_view")
1012
1091
  post2 = @api.create_post("Post 2", "Body 2", views: "other_view")
1013
1092
 
1014
- # Publish both
1093
+ # Publish post1 in test_view (current view)
1015
1094
  @api.publish_post(post1.id)
1016
- @api.publish_post(post2.id)
1017
1095
 
1018
1096
  # Get published posts for specific view
1019
- test_view_posts = @api.get_published_posts("test_view")
1097
+ test_view_posts = @api.posts("test_view", published: true)
1020
1098
  assert_equal 1, test_view_posts.length
1021
1099
  assert_equal post1.id, test_view_posts.first.id
1022
1100
 
1023
- other_view_posts = @api.get_published_posts("other_view")
1024
- assert_equal 1, other_view_posts.length
1025
- assert_equal post2.id, other_view_posts.first.id
1101
+ other_view_posts = @api.posts("other_view", published: true)
1102
+ assert_equal 0, other_view_posts.length # post2 not published
1103
+ end
1104
+
1105
+ def test_073_5_view_specific_publishing
1106
+ @api.create_view("test_view", "Test View")
1107
+ @api.create_view("other_view", "Other View")
1108
+
1109
+ # Set test_view as current view
1110
+ @api.repo.instance_variable_set(:@current_view, @api.repo.lookup_view("test_view"))
1111
+
1112
+ # Create a post that belongs to both views
1113
+ post = @api.create_post("Test Post", "Test body", views: "test_view other_view")
1114
+
1115
+ # Initially unpublished in both views
1116
+ refute @api.post_published?(post.id, "test_view")
1117
+ refute @api.post_published?(post.id, "other_view")
1118
+
1119
+ # Publish in test_view only (current view)
1120
+ @api.publish_post(post.id)
1121
+
1122
+ # Should be published in test_view, unpublished in other_view
1123
+ assert @api.post_published?(post.id, "test_view")
1124
+ refute @api.post_published?(post.id, "other_view")
1125
+
1126
+ # Publish in other_view
1127
+ @api.publish_post(post.id, "other_view")
1128
+
1129
+ # Should be published in both views
1130
+ assert @api.post_published?(post.id, "test_view")
1131
+ assert @api.post_published?(post.id, "other_view")
1132
+ end
1133
+
1134
+ def test_073_6_deployment_state_management
1135
+ @api.create_view("test_view", "Test View")
1136
+ @api.create_view("other_view", "Other View")
1137
+
1138
+ # Set test_view as current view
1139
+ @api.repo.instance_variable_set(:@current_view, @api.repo.lookup_view("test_view"))
1140
+
1141
+ # Create a post that belongs to both views
1142
+ post = @api.create_post("Test Post", "Test body", views: "test_view other_view")
1143
+
1144
+ # Initially unpublished and undeployed in both views
1145
+ refute @api.post_published?(post.id, "test_view")
1146
+ refute @api.post_deployed?(post.id, "test_view")
1147
+ refute @api.post_published?(post.id, "other_view")
1148
+ refute @api.post_deployed?(post.id, "other_view")
1149
+
1150
+ # Publish in test_view only
1151
+ @api.publish_post(post.id)
1152
+
1153
+ # Should be published but still undeployed in test_view
1154
+ assert @api.post_published?(post.id, "test_view")
1155
+ refute @api.post_deployed?(post.id, "test_view")
1156
+
1157
+ # Deploy in test_view
1158
+ @api.mark_post_deployed(post.id)
1159
+
1160
+ # Should be published and deployed in test_view
1161
+ assert @api.post_published?(post.id, "test_view")
1162
+ assert @api.post_deployed?(post.id, "test_view")
1163
+
1164
+ # Should still be unpublished and undeployed in other_view
1165
+ refute @api.post_published?(post.id, "other_view")
1166
+ refute @api.post_deployed?(post.id, "other_view")
1167
+ end
1168
+
1169
+ def test_073_7_deployment_state_workflow
1170
+ @api.create_view("test_view", "Test View")
1171
+
1172
+ # Set test_view as current view
1173
+ @api.repo.instance_variable_set(:@current_view, @api.repo.lookup_view("test_view"))
1174
+
1175
+ # Create multiple posts
1176
+ post1 = @api.create_post("Post 1", "Body 1")
1177
+ post2 = @api.create_post("Post 2", "Body 2")
1178
+ post3 = @api.create_post("Post 3", "Body 3")
1179
+
1180
+ # Initially all posts are unpublished and undeployed
1181
+ [post1, post2, post3].each do |post|
1182
+ refute @api.post_published?(post.id)
1183
+ refute @api.post_deployed?(post.id)
1184
+ end
1185
+
1186
+ # Publish post1 and post3
1187
+ @api.publish_post(post1.id)
1188
+ @api.publish_post(post3.id)
1189
+
1190
+ # Deploy post1
1191
+ @api.mark_post_deployed(post1.id)
1192
+
1193
+ # Check states
1194
+ assert @api.post_published?(post1.id)
1195
+ assert @api.post_deployed?(post1.id)
1196
+ refute @api.post_published?(post2.id)
1197
+ refute @api.post_deployed?(post2.id)
1198
+ assert @api.post_published?(post3.id)
1199
+ refute @api.post_deployed?(post3.id)
1200
+
1201
+ # Get deployed posts
1202
+ deployed_posts = @api.get_deployed_posts
1203
+ assert_equal 1, deployed_posts.length
1204
+ assert_equal post1.id, deployed_posts.first.id
1205
+
1206
+ # Mark post1 as undeployed
1207
+ @api.mark_post_undeployed(post1.id)
1208
+ refute @api.post_deployed?(post1.id)
1209
+
1210
+ # Get deployed posts again
1211
+ deployed_posts = @api.get_deployed_posts
1212
+ assert_equal 0, deployed_posts.length
1213
+ end
1214
+
1215
+ def test_073_8_deployment_workflow_integration
1216
+ @api.create_view("test_view", "Test View")
1217
+
1218
+ # Set test_view as current view
1219
+ @api.repo.instance_variable_set(:@current_view, @api.repo.lookup_view("test_view"))
1220
+
1221
+ # Create a post (don't publish it yet)
1222
+ post = @api.create_post("Test Post", "Test body")
1223
+
1224
+ # Post should be unpublished and undeployed
1225
+ refute @api.post_published?(post.id)
1226
+ refute @api.post_deployed?(post.id)
1227
+
1228
+ # Try to deploy unpublished post (should fail)
1229
+ assert_raises(PostNotPublished) do
1230
+ @api.mark_post_deployed(post.id)
1231
+ end
1232
+
1233
+ # Now publish the post
1234
+ @api.publish_post(post.id)
1235
+
1236
+ # Post should be published but not deployed
1237
+ assert @api.post_published?(post.id)
1238
+ refute @api.post_deployed?(post.id)
1239
+
1240
+ # Now deploy the published post
1241
+ @api.mark_post_deployed(post.id)
1242
+
1243
+ # Post should be both published and deployed
1244
+ assert @api.post_published?(post.id)
1245
+ assert @api.post_deployed?(post.id)
1246
+ end
1247
+
1248
+ def test_073_9_post_states_display
1249
+ @api.create_view("test_view", "Test View")
1250
+
1251
+ # Set test_view as current view
1252
+ @api.repo.instance_variable_set(:@current_view, @api.repo.lookup_view("test_view"))
1253
+
1254
+ # Create multiple posts
1255
+ post1 = @api.create_post("Post 1", "Body 1")
1256
+ post2 = @api.create_post("Post 2", "Body 2")
1257
+ post3 = @api.create_post("Post 3", "Body 3")
1258
+
1259
+ # Get initial states
1260
+ states = @api.get_post_states
1261
+ assert_equal 3, states.length
1262
+
1263
+ # Check initial state (should be "-" for unpublished/undeployed)
1264
+ assert_equal "-", states[post1.id][:state]
1265
+ assert_equal "-", states[post2.id][:state]
1266
+ assert_equal "-", states[post3.id][:state]
1267
+
1268
+ # Publish post1
1269
+ @api.publish_post(post1.id)
1270
+ states = @api.get_post_states
1271
+ assert_equal "P", states[post1.id][:state] # Published only
1272
+
1273
+ # Deploy post1
1274
+ @api.mark_post_deployed(post1.id)
1275
+ states = @api.get_post_states
1276
+ assert_equal "PD", states[post1.id][:state] # Published and deployed
1277
+
1278
+ # Publish post3
1279
+ @api.publish_post(post3.id)
1280
+ states = @api.get_post_states
1281
+ assert_equal "PD", states[post1.id][:state] # Published and deployed
1282
+ assert_equal "P", states[post3.id][:state] # Published only
1283
+ assert_equal "-", states[post2.id][:state] # Neither
1284
+ end
1285
+
1286
+ def test_073_10_state_validation_rules
1287
+ @api.create_view("test_view", "Test View")
1288
+
1289
+ # Set test_view as current view
1290
+ @api.repo.instance_variable_set(:@current_view, @api.repo.lookup_view("test_view"))
1291
+
1292
+ # Create a post
1293
+ post = @api.create_post("Test Post", "Test body")
1294
+
1295
+ # Test: Cannot publish deleted post
1296
+ @api.delete_post(post.id)
1297
+ assert_raises(PostDeleted) do
1298
+ @api.publish_post(post.id)
1299
+ end
1300
+
1301
+ # Test: Cannot deploy deleted post
1302
+ assert_raises(PostDeleted) do
1303
+ @api.mark_post_deployed(post.id)
1304
+ end
1305
+
1306
+ # Test: Cannot unpublish deployed post
1307
+ @api.undelete_post(post.id)
1308
+ @api.publish_post(post.id)
1309
+ @api.mark_post_deployed(post.id)
1310
+ assert_raises(PostAlreadyDeployed) do
1311
+ @api.unpublish_post(post.id)
1312
+ end
1313
+ end
1314
+
1315
+ def test_073_11_delete_undelete_workflow
1316
+ @api.create_view("test_view", "Test View")
1317
+
1318
+ # Set test_view as current view
1319
+ @api.repo.instance_variable_set(:@current_view, @api.repo.lookup_view("test_view"))
1320
+
1321
+ # Create a post
1322
+ post = @api.create_post("Test Post", "Test body")
1323
+
1324
+ # Post should not be deleted initially
1325
+ refute @api.post_deleted?(post.id)
1326
+
1327
+ # Delete the post
1328
+ @api.delete_post(post.id)
1329
+ assert @api.post_deleted?(post.id)
1330
+
1331
+ # Post should be in deleted state (X)
1332
+ states = @api.get_post_states
1333
+ assert_equal "X", states[post.id][:state]
1334
+
1335
+ # Undelete the post
1336
+ @api.undelete_post(post.id)
1337
+ refute @api.post_deleted?(post.id)
1338
+
1339
+ # Post should be back to normal state (-)
1340
+ states = @api.get_post_states
1341
+ assert_equal "-", states[post.id][:state]
1342
+ end
1343
+
1344
+ def test_073_12_edit_state_transition
1345
+ @api.create_view("test_view", "Test View")
1346
+
1347
+ # Set test_view as current view
1348
+ @api.repo.instance_variable_set(:@current_view, @api.repo.lookup_view("test_view"))
1349
+
1350
+ # Create and publish a post
1351
+ post = @api.create_post("Test Post", "Test body")
1352
+ @api.publish_post(post.id)
1353
+ @api.mark_post_deployed(post.id)
1354
+
1355
+ # Post should be published and deployed
1356
+ assert @api.post_published?(post.id)
1357
+ assert @api.post_deployed?(post.id)
1358
+
1359
+ # Regenerate the post - should reset state to unpublished/undeployed
1360
+ @api.repo.generate_post(post.id)
1361
+
1362
+ # Post should now be unpublished and undeployed (content changed)
1363
+ refute @api.post_published?(post.id)
1364
+ refute @api.post_deployed?(post.id)
1026
1365
  end
1027
1366
 
1028
1367
  def test_074_create_post_with_generation
@@ -1053,4 +1392,385 @@ class TestScriptoriumAPI < Minitest::Test
1053
1392
  assert_match /post\.published\s+no/, metadata_content
1054
1393
  refute @api.post_published?(post.id)
1055
1394
  end
1395
+
1396
+ def test_999_placeholder
1397
+ # This test ensures the file has at least one test method
1398
+ assert true
1399
+ end
1400
+
1401
+ # Asset management tests
1402
+
1403
+ def test_1000_list_assets_global
1404
+ # @api is already set up in setup method
1405
+
1406
+ # Create some test assets
1407
+ write_file("test/scriptorium-TEST/assets/test1.jpg", "Test image 1")
1408
+ write_file("test/scriptorium-TEST/assets/test2.png", "Test image 2")
1409
+
1410
+ assets = @api.list_assets(target: 'global')
1411
+
1412
+ assert_equal 3, assets.length
1413
+ # Check that our test assets are present (imagenotfound.jpg will also be there)
1414
+ filenames = assets.map { |a| a[:filename] }
1415
+ assert_includes filenames, "test1.jpg"
1416
+ assert_includes filenames, "test2.png"
1417
+ assert_includes filenames, "imagenotfound.jpg"
1418
+ assert_equal "image", assets[0][:type]
1419
+ assert assets[0][:size] > 0
1420
+ end
1421
+
1422
+ def test_1001_list_assets_library
1423
+ # @api is already set up in setup method
1424
+
1425
+ # Create library assets
1426
+ write_file("test/scriptorium-TEST/assets/library/sample1.jpg", "Sample 1")
1427
+ write_file("test/scriptorium-TEST/assets/library/sample2.txt", "Sample 2")
1428
+
1429
+ assets = @api.list_assets(target: 'library')
1430
+
1431
+ # There might be existing assets from setup, so check that our test assets are included
1432
+ assert assets.length >= 2, "Should have at least our 2 test assets"
1433
+ filenames = assets.map { |a| a[:filename] }
1434
+ assert_includes filenames, "sample1.jpg"
1435
+ assert_includes filenames, "sample2.txt"
1436
+
1437
+ # Find our specific test assets
1438
+ sample1 = assets.find { |a| a[:filename] == "sample1.jpg" }
1439
+ sample2 = assets.find { |a| a[:filename] == "sample2.txt" }
1440
+
1441
+ assert_equal "image", sample1[:type]
1442
+ assert_equal "document", sample2[:type]
1443
+ end
1444
+
1445
+ def test_1002_list_assets_view
1446
+ # @api is already set up in setup method
1447
+ @api.create_view("testview", "Test View", "Test Subtitle")
1448
+
1449
+ # Create view assets
1450
+ write_file("test/scriptorium-TEST/views/testview/assets/view1.jpg", "View asset 1")
1451
+ write_file("test/scriptorium-TEST/views/testview/assets/view2.svg", "View asset 2")
1452
+
1453
+ assets = @api.list_assets(target: 'view', view: 'testview')
1454
+
1455
+ assert_equal 3, assets.length
1456
+ # Check that our test assets are present (imagenotfound.jpg will also be there)
1457
+ filenames = assets.map { |a| a[:filename] }
1458
+ assert_includes filenames, "view1.jpg"
1459
+ assert_includes filenames, "view2.svg"
1460
+ assert_includes filenames, "imagenotfound.jpg"
1461
+ assert_equal "image", assets[0][:type]
1462
+ end
1463
+
1464
+ def test_1003_list_assets_gem
1465
+ # @api is already set up in setup method
1466
+
1467
+ # Test gem assets (should work in development environment)
1468
+ assets = @api.list_assets(target: 'gem')
1469
+
1470
+ # In development environment, we should find assets from the working directory
1471
+ # If no gem assets are found, that's also acceptable
1472
+ if assets.length > 0
1473
+ assert assets.all? { |asset| asset[:type] == 'image' || asset[:type] == 'other' }
1474
+ end
1475
+ end
1476
+
1477
+ def test_1004_get_asset_info
1478
+ # @api is already set up in setup method
1479
+
1480
+ # Create test asset
1481
+ write_file("test/scriptorium-TEST/assets/test_info.jpg", "Test info image")
1482
+
1483
+ asset_info = @api.get_asset_info("test_info.jpg", target: 'global')
1484
+
1485
+ assert_equal "test_info.jpg", asset_info[:filename]
1486
+ assert_equal "image", asset_info[:type]
1487
+ assert asset_info[:size] > 0
1488
+ assert asset_info[:path].include?("test_info.jpg")
1489
+ end
1490
+
1491
+ def test_1005_asset_exists
1492
+ # @api is already set up in setup method
1493
+
1494
+ # Create test asset
1495
+ write_file("test/scriptorium-TEST/assets/exists.jpg", "Exists")
1496
+
1497
+ assert @api.asset_exists?("exists.jpg", target: 'global')
1498
+ refute @api.asset_exists?("missing.jpg", target: 'global')
1499
+ end
1500
+
1501
+ def test_1006_copy_asset_global_to_view
1502
+ # @api is already set up in setup method
1503
+ @api.create_view("testview", "Test View", "Test Subtitle")
1504
+
1505
+ # Create source asset
1506
+ write_file("test/scriptorium-TEST/assets/source.jpg", "Source image")
1507
+
1508
+ # Copy asset
1509
+ target_path = @api.copy_asset("source.jpg", from: 'global', to: 'view', view: 'testview')
1510
+
1511
+ # Verify copy
1512
+ assert File.exist?(target_path)
1513
+ assert File.exist?("test/scriptorium-TEST/views/testview/assets/source.jpg")
1514
+ assert_equal "Source image", read_file("test/scriptorium-TEST/views/testview/assets/source.jpg").chomp
1515
+ end
1516
+
1517
+ def test_1007_copy_asset_gem_to_global
1518
+ # @api is already set up in setup method
1519
+
1520
+ # Find a gem asset to copy
1521
+ gem_assets = @api.list_assets(target: 'gem')
1522
+ skip "No gem assets available for testing" if gem_assets.empty?
1523
+
1524
+ gem_filename = gem_assets.first[:filename]
1525
+
1526
+ # Copy from gem to global
1527
+ target_path = @api.copy_asset(gem_filename, from: 'gem', to: 'global')
1528
+
1529
+ # Verify copy
1530
+ assert File.exist?(target_path)
1531
+ assert File.exist?("test/scriptorium-TEST/assets/#{gem_filename}")
1532
+ end
1533
+
1534
+ def test_1008_copy_asset_library_to_view
1535
+ # @api is already set up in setup method
1536
+ @api.create_view("testview", "Test View", "Test Subtitle")
1537
+
1538
+ # Create library asset
1539
+ write_file("test/scriptorium-TEST/assets/library/lib.jpg", "Library image")
1540
+
1541
+ # Copy to view
1542
+ target_path = @api.copy_asset("lib.jpg", from: 'library', to: 'view', view: 'testview')
1543
+
1544
+ # Verify copy
1545
+ assert File.exist?(target_path)
1546
+ assert File.exist?("test/scriptorium-TEST/views/testview/assets/lib.jpg")
1547
+ end
1548
+
1549
+ def test_1009_upload_asset
1550
+ # @api is already set up in setup method
1551
+
1552
+ # Create temporary source file
1553
+ source_file = "test/temp_source.jpg"
1554
+ write_file(source_file, "Temporary source image")
1555
+
1556
+ # Upload to global
1557
+ target_path = @api.upload_asset(source_file, target: 'global')
1558
+
1559
+ # Verify upload
1560
+ assert File.exist?(target_path)
1561
+ assert File.exist?("test/scriptorium-TEST/assets/temp_source.jpg")
1562
+ assert_equal "Temporary source image", read_file("test/scriptorium-TEST/assets/temp_source.jpg").chomp
1563
+
1564
+ # Cleanup
1565
+ File.delete(source_file)
1566
+ end
1567
+
1568
+ def test_1010_upload_asset_to_view
1569
+ # @api is already set up in setup method
1570
+ @api.create_view("testview", "Test View", "Test Subtitle")
1571
+
1572
+ # Create temporary source file
1573
+ source_file = "test/temp_view_source.jpg"
1574
+ write_file(source_file, "View source image")
1575
+
1576
+ # Upload to view
1577
+ target_path = @api.upload_asset(source_file, target: 'view', view: 'testview')
1578
+
1579
+ # Verify upload
1580
+ assert File.exist?(target_path)
1581
+ assert File.exist?("test/scriptorium-TEST/views/testview/assets/temp_view_source.jpg")
1582
+
1583
+ # Cleanup
1584
+ File.delete(source_file)
1585
+ end
1586
+
1587
+ def test_1011_delete_asset
1588
+ # @api is already set up in setup method
1589
+
1590
+ # Create test asset
1591
+ write_file("test/scriptorium-TEST/assets/to_delete.jpg", "Delete me")
1592
+
1593
+ # Verify it exists
1594
+ assert File.exist?("test/scriptorium-TEST/assets/to_delete.jpg")
1595
+
1596
+ # Delete it
1597
+ result = @api.delete_asset("to_delete.jpg", target: 'global')
1598
+
1599
+ # Verify deletion
1600
+ assert result
1601
+ refute File.exist?("test/scriptorium-TEST/assets/to_delete.jpg")
1602
+ end
1603
+
1604
+ def test_1012_get_asset_path
1605
+ # @api is already set up in setup method
1606
+
1607
+ # Create test asset
1608
+ write_file("test/scriptorium-TEST/assets/path_test.jpg", "Path test")
1609
+
1610
+ # Get path
1611
+ path = @api.get_asset_path("path_test.jpg", target: 'global')
1612
+
1613
+ assert path.include?("path_test.jpg")
1614
+ assert path.include?("assets")
1615
+ end
1616
+
1617
+ def test_1013_get_asset_dimensions
1618
+ # @api is already set up in setup method
1619
+
1620
+ # Create test image (we'll use a placeholder)
1621
+ write_file("test/scriptorium-TEST/assets/dimensions.jpg", "Image data")
1622
+
1623
+ # Get dimensions (may be nil if FastImage not available)
1624
+ dimensions = @api.get_asset_dimensions("dimensions.jpg", target: 'global')
1625
+
1626
+ # Just verify the method doesn't crash
1627
+ assert true
1628
+ end
1629
+
1630
+ def test_1014_get_asset_size
1631
+ # @api is already set up in setup method
1632
+
1633
+ # Create test asset
1634
+ content = "Test content for size measurement"
1635
+ write_file("test/scriptorium-TEST/assets/size_test.txt", content)
1636
+
1637
+ # Get size (write_file adds a newline, so size will be content.length + 1)
1638
+ size = @api.get_asset_size("size_test.txt", target: 'global')
1639
+
1640
+ assert_equal content.length + 1, size
1641
+ end
1642
+
1643
+ def test_1015_get_asset_type
1644
+ # @api is already set up in setup method
1645
+
1646
+ # Test various file types
1647
+ assert_equal "image", @api.get_asset_type("test.jpg")
1648
+ assert_equal "image", @api.get_asset_type("test.png")
1649
+ assert_equal "image", @api.get_asset_type("test.svg")
1650
+ assert_equal "document", @api.get_asset_type("test.txt")
1651
+ assert_equal "document", @api.get_asset_type("test.md")
1652
+ assert_equal "video", @api.get_asset_type("test.mp4")
1653
+ assert_equal "audio", @api.get_asset_type("test.mp3")
1654
+ assert_equal "other", @api.get_asset_type("test.xyz")
1655
+ assert_nil @api.get_asset_type(nil)
1656
+ end
1657
+
1658
+ def test_1016_bulk_copy_assets
1659
+ # @api is already set up in setup method
1660
+ @api.create_view("testview", "Test View", "Test Subtitle")
1661
+
1662
+ # Create multiple source assets
1663
+ write_file("test/scriptorium-TEST/assets/bulk1.jpg", "Bulk 1")
1664
+ write_file("test/scriptorium-TEST/assets/bulk2.png", "Bulk 2")
1665
+ write_file("test/scriptorium-TEST/assets/bulk3.txt", "Bulk 3")
1666
+
1667
+ filenames = ["bulk1.jpg", "bulk2.png", "bulk3.txt"]
1668
+
1669
+ # Bulk copy
1670
+ results = @api.bulk_copy_assets(filenames, from: 'global', to: 'view', view: 'testview')
1671
+
1672
+ # Verify results
1673
+ assert_equal 3, results.length
1674
+ assert results.all? { |r| r[:success] }
1675
+
1676
+ # Verify files were copied
1677
+ filenames.each do |filename|
1678
+ assert File.exist?("test/scriptorium-TEST/views/testview/assets/#{filename}")
1679
+ end
1680
+ end
1681
+
1682
+ def test_1017_copy_asset_invalid_source
1683
+ # @api is already set up in setup method
1684
+
1685
+ # Try to copy from invalid source
1686
+ assert_raises(InvalidFormatError) do
1687
+ @api.copy_asset("test.jpg", from: 'invalid', to: 'global')
1688
+ end
1689
+ end
1690
+
1691
+ def test_1018_copy_asset_invalid_target
1692
+ # @api is already set up in setup method
1693
+
1694
+ # Try to copy to invalid target
1695
+ assert_raises(InvalidFormatError) do
1696
+ @api.copy_asset("test.jpg", from: 'global', to: 'invalid')
1697
+ end
1698
+ end
1699
+
1700
+ def test_1019_copy_asset_source_not_found
1701
+ # @api is already set up in setup method
1702
+
1703
+ # Try to copy non-existent asset
1704
+ assert_raises(FileNotFoundError) do
1705
+ @api.copy_asset("missing.jpg", from: 'global', to: 'global')
1706
+ end
1707
+ end
1708
+
1709
+ def test_1020_list_assets_no_view_specified
1710
+ # @api is already set up in setup method
1711
+
1712
+ # Should work for global assets
1713
+ assets = @api.list_assets(target: 'global')
1714
+ assert assets.is_a?(Array)
1715
+
1716
+ # Clear the current view to test the error case
1717
+ @api.repo.instance_variable_set(:@current_view, nil)
1718
+
1719
+ # Should fail for view assets without view
1720
+ assert_raises(ViewTargetNil) do
1721
+ @api.list_assets(target: 'view')
1722
+ end
1723
+ end
1724
+
1725
+ def test_999_creates_posts_with_sequential_ids
1726
+ # Create three posts
1727
+ post1 = @api.create_post(
1728
+ "First Post",
1729
+ "This is the first post content.",
1730
+ views: "sample",
1731
+ tags: "test"
1732
+ )
1733
+
1734
+ post2 = @api.create_post(
1735
+ "Second Post",
1736
+ "This is the second post content.",
1737
+ views: "sample",
1738
+ tags: "test"
1739
+ )
1740
+
1741
+ post3 = @api.create_post(
1742
+ "Third Post",
1743
+ "This is the third post content.",
1744
+ views: "sample",
1745
+ tags: "test"
1746
+ )
1747
+
1748
+ # Verify they have sequential IDs
1749
+ assert_equal 1, post1.id, "First post should have ID 1"
1750
+ assert_equal 2, post2.id, "Second post should have ID 2"
1751
+ assert_equal 3, post3.id, "Third post should have ID 3"
1752
+
1753
+ # Verify the posts exist in the repository
1754
+ assert_equal "First Post", @api.post(1).title
1755
+ assert_equal "Second Post", @api.post(2).title
1756
+ assert_equal "Third Post", @api.post(3).title
1757
+ end
1758
+
1759
+ def test_998_post_counter_is_correctly_updated
1760
+ # Check initial counter
1761
+ initial_counter = @api.instance_variable_get(:@repo).last_post_num
1762
+ assert_equal 0, initial_counter, "Initial counter should be 0"
1763
+
1764
+ # Create a post
1765
+ post = @api.create_post(
1766
+ "Test Post",
1767
+ "Test content",
1768
+ views: "sample"
1769
+ )
1770
+
1771
+ # Check counter after creation
1772
+ updated_counter = @api.instance_variable_get(:@repo).last_post_num
1773
+ assert_equal 1, updated_counter, "Counter should be 1 after creating one post"
1774
+ assert_equal 1, post.id, "Post should have ID 1"
1775
+ end
1056
1776
  end