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
@@ -1,3 +1,5 @@
1
+
2
+
1
3
  class Scriptorium::Repo
2
4
  include Scriptorium::Exceptions
3
5
  extend Scriptorium::Exceptions
@@ -22,7 +24,8 @@ class Scriptorium::Repo
22
24
  end
23
25
 
24
26
  def self.create(path = nil, testmode: false)
25
- assume { path.nil? || path.is_a?(String) }
27
+ msg = "path must be nil or String, got #{path.class}"
28
+ assume(msg) { path.nil? || path.is_a?(String) }
26
29
  # Handle backward compatibility: boolean true means testing mode
27
30
  if testmode == true
28
31
  Scriptorium::Repo.testing = path
@@ -32,12 +35,9 @@ class Scriptorium::Repo
32
35
  home = ENV['HOME']
33
36
  @predef = Scriptorium::StandardFiles.new
34
37
  @root = path || "#{home}/.scriptorium"
35
- parent = path ? "." : home
36
- file = path || ".scriptorium"
37
- @root = parent/file
38
38
  raise self.RepoDirAlreadyExists(@root) if Dir.exist?(@root)
39
- make_tree(parent, <<~EOS)
40
- #@root
39
+ make_tree(@root, <<~EOS)
40
+ .
41
41
  ├── config/ # Global config files
42
42
  ├── views/ # Views
43
43
  ├── drafts/ # Draft posts (global)
@@ -50,10 +50,18 @@ class Scriptorium::Repo
50
50
  postnum_file = "#@root/config/last_post_num.txt"
51
51
  write_file(postnum_file, "0")
52
52
  write_file(@root/:config/"global-head.txt", @predef.html_head_content)
53
- write_file(@root/:config/"bootstrap_js.txt", @predef.bootstrap_js)
54
- write_file(@root/:config/"bootstrap_css.txt", @predef.bootstrap_css)
53
+ copy_support_file('bootstrap/js.txt', @root/:config/"bootstrap_js.txt")
54
+ copy_support_file('bootstrap/css.txt', @root/:config/"bootstrap_css.txt")
55
55
  write_file(@root/:config/"common.js", @predef.common_js)
56
56
  write_file(@root/:config/"widgets.txt", @predef.available_widgets)
57
+ copy_support_file('post_index/config.txt', @root/:config/"post_index_defaults.txt")
58
+
59
+ # Create global credentials directory and template
60
+ FileUtils.mkdir_p(@root/:credentials)
61
+ copy_support_file('config/reddit_template.txt', @root/:credentials/"reddit.txt")
62
+
63
+
64
+
57
65
  Scriptorium::Theme.create_standard(@root) # Theme: templates, etc.
58
66
 
59
67
  # Copy application-wide gem assets to library
@@ -63,20 +71,22 @@ class Scriptorium::Repo
63
71
  generate_os_helpers(@root)
64
72
 
65
73
  @repo = self.open(@root)
66
- Scriptorium::View.create_sample_view(repo)
74
+ Scriptorium::View.create_sample_view(@repo)
67
75
  verify { @repo.is_a?(Scriptorium::Repo) }
68
- return repo
76
+ return @repo
69
77
  end
70
78
 
71
79
  def self.open(root)
72
- assume { root.is_a?(String) && !root.empty? }
80
+ msg = "root must be a non-empty String, got #{root.class} (#{root.inspect})"
81
+ assume(msg) { root.is_a?(String) && !root.empty? }
73
82
  repo = Scriptorium::Repo.new(root)
74
83
  verify { repo.is_a?(Scriptorium::Repo) }
75
84
  repo
76
85
  end
77
86
 
78
87
  def self.destroy
79
- assume { Scriptorium::Repo.testing }
88
+ msg = "Repo.testing must be true in test mode"
89
+ assume(msg) { Scriptorium::Repo.testing }
80
90
  raise self.TestModeOnly unless Scriptorium::Repo.testing
81
91
  system!("rm -rf #@root", "destroying repository")
82
92
  verify { !Dir.exist?(@root) }
@@ -94,7 +104,8 @@ class Scriptorium::Repo
94
104
  end
95
105
 
96
106
  def initialize(root) # repo
97
- assume { root.is_a?(String) && !root.empty? }
107
+ msg = "root must be a non-empty String, got #{root.class} (#{root.inspect})"
108
+ assume(msg) { root.is_a?(String) && !root.empty? }
98
109
  @root = root
99
110
  @predef = Scriptorium::StandardFiles.new
100
111
  # Scriptorium::Repo.class_eval { @root, @repo = root, self }
@@ -117,7 +128,7 @@ class Scriptorium::Repo
117
128
  view_name = read_file(cview_file).chomp
118
129
  begin
119
130
  @current_view = lookup_view(view_name)
120
- rescue => e
131
+ rescue
121
132
  # If the saved view doesn't exist, just leave current_view as nil
122
133
  # It will be set when a view is created or selected
123
134
  end
@@ -138,9 +149,14 @@ class Scriptorium::Repo
138
149
  end
139
150
 
140
151
  private def validate_view_target(target)
141
- raise CannotLookupViewTargetNil if target.nil?
152
+ raise ViewTargetNil if target.nil?
153
+
154
+ raise ViewTargetEmpty if target.to_s.strip.empty?
142
155
 
143
- raise CannotLookupViewTargetEmpty if target.to_s.strip.empty?
156
+ # Validate that target is a valid view name (alphanumeric, hyphen, underscore)
157
+ unless target.match?(/^[a-zA-Z0-9_-]+$/)
158
+ raise ViewTargetInvalid(target)
159
+ end
144
160
  end
145
161
 
146
162
  def view(change = nil) # get/set current view
@@ -160,19 +176,21 @@ class Scriptorium::Repo
160
176
  end
161
177
 
162
178
  def create_view(name, title, subtitle = "", theme: "standard")
163
- assume { name.is_a?(String) }
164
- assume { title.is_a?(String) }
179
+ msg = "name must be a String, got #{name.class}"
180
+ assume(msg) { name.is_a?(String) }
181
+ msg = "title must be a String, got #{title.class}"
182
+ assume(msg) { title.is_a?(String) }
165
183
  validate_view_name(name)
166
184
  validate_view_title(title)
167
185
 
168
186
  # Validate name format (only allow alphanumeric, hyphen, underscore)
169
187
  unless name.match?(/^[a-zA-Z0-9_-]+$/)
170
- raise CannotCreateViewNameInvalid(name)
188
+ raise ViewNameInvalid(name)
171
189
  end
172
190
 
173
191
  raise ViewDirAlreadyExists(name) if view_exist?(name)
174
- make_tree(@root/:views, <<~EOS)
175
- #{name}/
192
+ make_tree(@root/:views/name, <<~EOS)
193
+ .
176
194
  ├── config/ # View-specific config files
177
195
  │ ├── layout.txt # Overall layout for front page
178
196
  │ ├── footer.txt # Content for footer.html
@@ -193,6 +211,9 @@ class Scriptorium::Repo
193
211
  │ │ ├── main.html # Generated from main.txt
194
212
  │ │ └── right.html # Generated from right.txt
195
213
  │ └── posts/ # Generated posts for view (slug.html)
214
+ ├── posts/ # Post state tracking
215
+ │ ├── unpublished.txt # Posts NOT published in this view (empty = all published)
216
+ │ └── undeployed.txt # Posts NOT published in this view (empty = all deployed)
196
217
  ├── widgets/ # Widgets for view
197
218
  └── staging/ # Staging area prior to deployment
198
219
  EOS
@@ -200,19 +221,62 @@ class Scriptorium::Repo
200
221
  ###
201
222
 
202
223
  dir = "#@root/views/#{name}"
203
- write_file!(dir/"config.txt",
204
- "title #{title}",
205
- "subtitle #{subtitle}",
206
- "theme #{theme}")
207
- write_file(dir/:config/"global-head.txt", @predef.html_head_content(true)) # true = view-specific
208
- write_file(dir/:config/"bootstrap_js.txt", @predef.bootstrap_js)
209
- write_file(dir/:config/"bootstrap_css.txt", @predef.bootstrap_css)
210
- write_file(dir/:config/"common.js", @predef.common_js)
211
- write_file(dir/:config/"social.txt", @predef.social_config)
212
- write_file(dir/:config/"reddit.txt", @predef.reddit_config)
213
- write_file(dir/:config/"deploy.txt", @predef.deploy_text % {view: name, domain: "example.com"})
214
- write_file(dir/:config/"status.txt", @predef.status_txt)
215
- view = open_view(name)
224
+
225
+ begin
226
+ write_file!(dir/"config.txt",
227
+ "title #{title}",
228
+ "subtitle #{subtitle}",
229
+ "theme #{theme}")
230
+
231
+ write_file(dir/:config/"global-head.txt", @predef.html_head_content(true)) # true = view-specific
232
+ copy_support_file('bootstrap/js.txt', dir/:config/"bootstrap_js.txt")
233
+ copy_support_file('bootstrap/css.txt', dir/:config/"bootstrap_css.txt")
234
+ # Highlight.js config files (renamed from prism_* for clarity)
235
+ copy_support_file('highlight/js.txt', dir/:config/"highlight_js.txt")
236
+ write_file(dir/:config/"highlight_ruby_js.txt", @predef.highlight_ruby_js)
237
+ copy_support_file('highlight/css.txt', dir/:config/"highlight_css.txt")
238
+ write_file(dir/:config/"common.js", @predef.common_js)
239
+ copy_support_file('config/social.txt', dir/:config/"social.txt")
240
+ copy_support_file('config/reddit.txt', dir/:config/"reddit.txt")
241
+ write_file(dir/:config/"deploy.txt", @predef.deploy_text % {view: name, domain: "example.com"})
242
+ write_file(dir/:config/"status.txt", @predef.status_txt)
243
+ copy_support_file('post_index/config.txt', dir/:config/"post_index.txt")
244
+
245
+ # Create view credentials directory and template
246
+ FileUtils.mkdir_p(dir/:credentials)
247
+ copy_support_file('config/reddit_template.txt', dir/:credentials/"reddit.txt")
248
+
249
+ # Copy essential icons to view assets directory
250
+ view_assets_dir = dir/:assets
251
+ FileUtils.mkdir_p(view_assets_dir)
252
+ FileUtils.mkdir_p(view_assets_dir/"icons"/"ui")
253
+ FileUtils.mkdir_p(view_assets_dir/"icons"/"social")
254
+
255
+ # Copy UI icons
256
+ if File.exist?(@root/:assets/"icons"/"ui"/"back.png")
257
+ FileUtils.cp(@root/:assets/"icons"/"ui"/"back.png", view_assets_dir/"icons"/"ui"/"back.png")
258
+ end
259
+
260
+ # Copy social icons
261
+ if File.exist?(@root/:assets/"icons"/"social"/"reddit.png")
262
+ FileUtils.cp(@root/:assets/"icons"/"social"/"reddit.png", view_assets_dir/"icons"/"social"/"reddit.png")
263
+ end
264
+
265
+ # Copy missing image placeholder
266
+ if File.exist?(@root/:assets/"imagenotfound.jpg")
267
+ FileUtils.cp(@root/:assets/"imagenotfound.jpg", view_assets_dir/"imagenotfound.jpg")
268
+ end
269
+
270
+ # Create post state tracking files
271
+ write_file(dir/:posts/"unpublished.txt", "") # Empty = all posts published
272
+ write_file(dir/:posts/"undeployed.txt", "") # Empty = all posts deployed
273
+
274
+ view = open_view(name)
275
+ rescue => e
276
+ # Clean up partial view directory if creation fails
277
+ FileUtils.rm_rf(dir) if Dir.exist?(dir)
278
+ raise CannotCreateView("Failed to create view '#{name}': #{e.message}")
279
+ end
216
280
  @views -= [view]
217
281
  @views << view
218
282
  @current_view = view
@@ -221,13 +285,18 @@ class Scriptorium::Repo
221
285
  theme_config = @root/:themes/theme/:layout/:config
222
286
  containers = %w[header.txt footer.txt left.txt right.txt main.txt]
223
287
  containers.each { |container| FileUtils.cp(theme_config/container, cfg/container) } # from theme to view
288
+
289
+ # Create default SVG configuration using standard files
290
+ write_file(cfg/"svg.txt", @predef.svg_txt)
291
+
224
292
  view.apply_theme(theme)
225
293
  verify { view.is_a?(Scriptorium::View) }
226
294
  return view
227
295
  end
228
296
 
229
297
  def open_view(name)
230
- vhash = getvars(view_dir(name)/"config.txt")
298
+ config_file = view_dir(name)/"config.txt"
299
+ vhash = getvars(config_file)
231
300
  title, subtitle, theme = vhash.values_at(:title, :subtitle, :theme)
232
301
  view = Scriptorium::View.new(name, title, subtitle, theme)
233
302
  @views -= [view]
@@ -245,18 +314,16 @@ class Scriptorium::Repo
245
314
 
246
315
  # Whoa - what if different views have different themes??? FIXME
247
316
  # Maybe solution is as simple as: Initial post is not theme-dependent
248
- theme = @current_view.theme
249
317
  views ||= @current_view.name # initial_post wants a String!
250
318
  views, tags = Array(views), Array(tags)
251
- id = incr_post_num
252
319
 
253
320
  # Create content file (no ID, no created date)
254
- content = @predef.initial_post_content(title: title, blurb: blurb,
255
- views: views, tags: tags, body: body)
321
+ content = @predef.initial_post(:filled, title: title, blurb: blurb,
322
+ views: views, tags: tags, body: body)
256
323
  write_file(content_name, content)
257
324
 
258
- # Create metadata file (with ID and created date)
259
- metadata = @predef.initial_post_metadata(num: id, title: title, blurb: blurb,
325
+ # Create metadata file (no ID for drafts)
326
+ metadata = @predef.initial_post_metadata(title: title, blurb: blurb,
260
327
  views: views, tags: tags)
261
328
  write_file(metadata_name, metadata)
262
329
 
@@ -275,7 +342,7 @@ class Scriptorium::Repo
275
342
  end
276
343
 
277
344
  def finish_draft(name)
278
- id = last_post_num
345
+ id = incr_post_num
279
346
  id4 = d4(id)
280
347
  posts = @root/:posts
281
348
  make_dir(posts/id4)
@@ -297,28 +364,6 @@ class Scriptorium::Repo
297
364
  end
298
365
 
299
366
 
300
- private def copy_post_assets_to_view(num, view)
301
- id4 = d4(num)
302
- post_assets_dir = @root/:posts/id4/"assets"
303
- view_assets_dir = view.dir/:output/"assets"
304
-
305
- # Only copy if post has assets
306
- return unless Dir.exist?(post_assets_dir)
307
-
308
- # Create view assets directory if it doesn't exist
309
- make_dir(view_assets_dir)
310
-
311
- # Copy all files from post assets to view assets
312
- Dir.glob(post_assets_dir/"*").each do |file|
313
- next unless File.file?(file)
314
- filename = File.basename(file)
315
- target_file = view_assets_dir/filename
316
-
317
- # Copy file, overwriting if it exists (post assets take precedence)
318
- FileUtils.cp(file, target_file)
319
- end
320
- end
321
-
322
367
  private def write_post_metadata(data, view)
323
368
  num, title = data.values_at(:"post.id", :"post.title")
324
369
  metadata_file = @root/:posts/d4(num)/"meta.txt"
@@ -356,83 +401,269 @@ class Scriptorium::Repo
356
401
  # view/.../output/permalink/0123-this-is-me.html (for direct access)
357
402
  permalink_path = view.dir/:output/:permalink/slug
358
403
  make_dir(File.dirname(permalink_path))
404
+ # Remove the Back-to-index block from permalink content
405
+ cleaned_final = final.gsub(/<div align='right'>.*?Back to index<\/a>\s*<\/div>\s*/m, "")
359
406
  # Write the permalink version with "Visit Blog" link and "Copy link" button
360
- permalink_content = final + "\n<div style=\"text-align: center; margin-top: 20px;\">\n<a href=\"../index.html\">Visit Blog</a>\n</div>\n<div style=\"text-align: center; margin-top: 10px;\">\n<button onclick=\"copyPermalinkToClipboard()\" style=\"padding: 8px 16px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer;\">Copy link</button>\n</div>\n<script>\nfunction copyPermalinkToClipboard() {\n navigator.clipboard.writeText(window.location.href).then(function() {\n // Change button text temporarily to show success\n const button = event.target;\n const originalText = button.textContent;\n button.textContent = 'Copied!';\n button.style.background = '#28a745';\n setTimeout(function() {\n button.textContent = originalText;\n button.style.background = '#007bff';\n }, 2000);\n }).catch(function(err) {\n console.error('Failed to copy: ', err);\n alert('Failed to copy link to clipboard');\n });\n}\n</script>"
407
+ permalink_content = cleaned_final + "\n<div style=\"text-align: center; margin-top: 20px;\">\n<a href=\"../index.html\">Visit Blog</a>\n</div>\n<div style=\"text-align: center; margin-top: 10px;\">\n<button onclick=\"copyPermalinkToClipboard()\" style=\"padding: 8px 16px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer;\">Copy link</button>\n</div>\n<script>\nfunction copyPermalinkToClipboard() {\n navigator.clipboard.writeText(window.location.href).then(function() {\n const button = event.target;\n const originalText = button.textContent;\n button.textContent = 'Copied!';\n button.style.background = '#28a745';\n setTimeout(function() {\n button.textContent = originalText;\n button.style.background = '#007bff';\n }, 2000);\n }).catch(function(err) {\n console.error('Failed to copy: ', err);\n alert('Failed to copy link to clipboard');\n });\n}\n</script>"
361
408
  write_file(permalink_path, permalink_content)
362
409
 
363
- # Create symlink for clean URL (without numeric prefix)
410
+ # Create copy for clean URL (without numeric prefix)
364
411
  clean_slug = clean_slugify(title) + ".html"
365
- clean_symlink_path = view.dir/:output/:permalink/clean_slug
412
+ clean_copy_path = view.dir/:output/:permalink/clean_slug
366
413
 
367
- # Remove existing symlink if it exists
368
- File.delete(clean_symlink_path) if File.exist?(clean_symlink_path) && File.symlink?(clean_symlink_path)
369
-
370
- # Create symlink (relative path from clean_symlink_path to slug)
371
- begin
372
- File.symlink(slug, clean_symlink_path)
373
- rescue Errno::EEXIST => e
374
- # If symlink already exists (not a symlink), remove it and try again
375
- File.delete(clean_symlink_path) if File.exist?(clean_symlink_path)
376
- File.symlink(slug, clean_symlink_path)
377
- end
414
+ # Remove existing file if it exists
415
+ File.delete(clean_copy_path) if File.exist?(clean_copy_path)
378
416
 
379
- # Copy post-specific assets to view output directory for deployment
380
- copy_post_assets_to_view(num, view)
417
+ # Copy the permalink file to create clean URL
418
+ FileUtils.cp(permalink_path, clean_copy_path)
381
419
  end
382
420
 
383
421
  def create_post(title: nil, views: nil, tags: nil, body: nil, blurb: nil)
384
- assume { title.nil? || title.is_a?(String) }
385
- assume { views.nil? || views.is_a?(Array) || views.is_a?(String) }
386
- assume { tags.nil? || tags.is_a?(Array) || tags.is_a?(String) }
387
- assume { body.nil? || body.is_a?(String) }
388
- assume { blurb.nil? || blurb.is_a?(String) }
422
+ msg = "title must be nil or String, got #{title.class}"
423
+ assume(msg) { title.nil? || title.is_a?(String) }
424
+ msg = "views must be nil, Array, or String, got #{views.class}"
425
+ assume(msg) { views.nil? || views.is_a?(Array) || views.is_a?(String) }
426
+ msg = "tags must be nil, Array, or String, got #{tags.class}"
427
+ assume(msg) { tags.nil? || tags.is_a?(Array) || tags.is_a?(String) }
428
+ msg = "body must be nil or String, got #{body.class}"
429
+ assume(msg) { body.nil? || body.is_a?(String) }
430
+ msg = "blurb must be nil or String, got #{blurb.class}"
431
+ assume(msg) { blurb.nil? || blurb.is_a?(String) }
389
432
  name = create_draft(title: title, views: views, tags: tags, body: body, blurb: blurb)
390
433
  num = finish_draft(name)
434
+
435
+ # Add post to unpublished and undeployed lists for all views it belongs to
436
+ metadata_file = @root/:posts/d4(num)/"meta.txt"
437
+ if File.exist?(metadata_file)
438
+ metadata = getvars(metadata_file)
439
+ views = metadata[:"post.views"]&.strip&.split(/\s+/) || ["sample"]
440
+ views.each do |view_name|
441
+ view_obj = lookup_view(view_name)
442
+ unpublished_file = view_obj.dir/:posts/"unpublished.txt"
443
+ undeployed_file = view_obj.dir/:posts/"undeployed.txt"
444
+ add_post_to_state_file(unpublished_file, num)
445
+ add_post_to_state_file(undeployed_file, num)
446
+ end
447
+ end
448
+
391
449
  generate_post(num)
392
450
  post = self.post(num) # Return the Post object
393
451
  verify { post.is_a?(Scriptorium::Post) }
394
452
  post
395
453
  end
396
454
 
397
- def publish_post(num)
455
+ def publish_post(num, view = nil)
398
456
  validate_post_id(num)
457
+
458
+ # Check if post exists in normal location first
399
459
  metadata_file = @root/:posts/d4(num)/"meta.txt"
460
+ unless File.exist?(metadata_file)
461
+ # Check if post is deleted
462
+ if post_deleted?(num)
463
+ raise PostDeleted(num)
464
+ else
465
+ raise CannotGetPost("Post #{num} not found")
466
+ end
467
+ end
400
468
 
401
- # Read current metadata if it exists
402
- metadata = {}
403
- metadata = getvars(metadata_file) if File.exist?(metadata_file)
469
+ # Read current metadata
470
+ metadata = getvars(metadata_file)
404
471
 
405
- # Check if already published
406
- if metadata[:"post.published"] != "no" && metadata[:"post.published"] != nil
407
- raise "Post #{num} is already published"
472
+ if view.nil?
473
+ # Use current view if no view specified
474
+ view = @current_view&.name || "sample"
408
475
  end
409
476
 
410
- # Update published timestamp
411
- metadata[:"post.published"] = ymdhms
477
+ # Check if already published in this view
478
+ view_obj = lookup_view(view)
479
+ unpublished_file = view_obj.dir/:posts/"unpublished.txt"
480
+ if !post_in_state_file?(unpublished_file, num)
481
+ raise PostAlreadyPublished(num)
482
+ end
412
483
 
413
- # Write updated metadata
414
- lines = metadata.map { |k, v| sprintf("%-18s %s", k, v) }
415
- write_file(metadata_file, lines.join("\n"))
484
+ # View-specific publish - only update the specified view's state
485
+ remove_post_from_state_file(unpublished_file, num)
416
486
 
417
- # Generate the post (this will preserve the updated metadata)
418
- generate_post(num)
487
+ # If this is the first time publishing this post, update global metadata
488
+ if metadata[:"post.published"] == "no" || metadata[:"post.published"].nil?
489
+ metadata[:"post.published"] = ymdhms
490
+
491
+ # Write updated metadata
492
+ lines = metadata.map { |k, v| sprintf("%-18s %s", k, v) }
493
+ write_file(metadata_file, lines.join("\n"))
494
+ end
419
495
 
420
496
  self.post(num)
421
497
  end
422
-
423
- def post_published?(num)
498
+
499
+ def mark_post_deployed(num, view = nil)
424
500
  validate_post_id(num)
501
+
502
+ # Check if post exists in normal location first
425
503
  metadata_file = @root/:posts/d4(num)/"meta.txt"
426
- return false unless File.exist?(metadata_file)
504
+ unless File.exist?(metadata_file)
505
+ # Check if post is deleted
506
+ if post_deleted?(num)
507
+ raise PostDeleted(num)
508
+ else
509
+ raise CannotGetPost("Post #{num} not found")
510
+ end
511
+ end
427
512
 
428
- metadata = getvars(metadata_file)
429
- result = metadata[:"post.published"] != "no"
430
- result
513
+ if view.nil?
514
+ # Use current view if no view specified
515
+ view = @current_view&.name || "sample"
516
+ end
517
+
518
+ # Check if already deployed in this view
519
+ view_obj = lookup_view(view)
520
+ undeployed_file = view_obj.dir/:posts/"undeployed.txt"
521
+ if !post_in_state_file?(undeployed_file, num)
522
+ raise PostAlreadyDeployed(num)
523
+ end
524
+
525
+ # Validate that only published posts can be deployed
526
+ unless post_published?(num, view)
527
+ raise PostNotPublished(num)
528
+ end
529
+
530
+ # Remove from undeployed list for this view
531
+ remove_post_from_state_file(undeployed_file, num)
532
+
533
+ # Update global metadata if this is the first deployment
534
+ metadata_file = @root/:posts/d4(num)/"meta.txt"
535
+ if File.exist?(metadata_file)
536
+ metadata = getvars(metadata_file)
537
+ if metadata[:"post.deployed"] == "no" || metadata[:"post.deployed"].nil?
538
+ metadata[:"post.deployed"] = ymdhms
539
+ lines = metadata.map { |k, v| sprintf("%-18s %s", k, v) }
540
+ write_file(metadata_file, lines.join("\n"))
541
+ end
542
+ else
543
+ raise CannotGetPost("Post #{num} metadata not found")
544
+ end
431
545
  end
432
-
433
- def get_published_posts(view = nil)
546
+
547
+ def mark_post_undeployed(num, view = nil)
548
+ validate_post_id(num)
549
+
550
+ if view.nil?
551
+ # Use current view if no view specified
552
+ view = @current_view&.name || "sample"
553
+ end
554
+
555
+ # Add to undeployed list for this view
556
+ view_obj = lookup_view(view)
557
+ undeployed_file = view_obj.dir/:posts/"undeployed.txt"
558
+ add_post_to_state_file(undeployed_file, num)
559
+
560
+ # Update global metadata
561
+ metadata_file = @root/:posts/d4(num)/"meta.txt"
562
+ if File.exist?(metadata_file)
563
+ metadata = getvars(metadata_file)
564
+ metadata[:"post.deployed"] = "no"
565
+ lines = metadata.map { |k, v| sprintf("%-18s %s", k, v) }
566
+ write_file(metadata_file, lines.join("\n"))
567
+ end
568
+ end
569
+
570
+ def post_deployed?(num, view = nil)
571
+ validate_post_id(num)
572
+
573
+ # If no specific view, check global metadata (for backward compatibility)
574
+ if view.nil?
575
+ metadata_file = @root/:posts/d4(num)/"meta.txt"
576
+ return false unless File.exist?(metadata_file)
577
+
578
+ metadata = getvars(metadata_file)
579
+ return metadata[:"post.deployed"] != "no"
580
+ end
581
+
582
+ # Check view-specific deployed status
583
+ view = lookup_view(view)
584
+ undeployed_file = view.dir/:posts/"undeployed.txt"
585
+ !post_in_state_file?(undeployed_file, num)
586
+ end
587
+
588
+ def get_deployed_posts(view = nil)
434
589
  all_posts = all_posts(view)
435
- all_posts.select { |post| post_published?(post.id) }
590
+
591
+ if view.nil?
592
+ # If no specific view, use global metadata (for backward compatibility)
593
+ all_posts.select { |post| post_deployed?(post.id) }
594
+ else
595
+ # Use view-specific deployed status
596
+ view = lookup_view(view)
597
+ undeployed_file = view.dir/:posts/"undeployed.txt"
598
+ all_posts.reject { |post| post_in_state_file?(undeployed_file, post.id) }
599
+ end
600
+ end
601
+
602
+ def unpublish_post(num, view = nil)
603
+ validate_post_id(num)
604
+
605
+ # Check if post is deployed in any view (including current view if none specified)
606
+ if view.nil?
607
+ view = @current_view&.name || "sample"
608
+ end
609
+
610
+ # Check if post is deployed in this view
611
+ view_obj = lookup_view(view)
612
+ if post_deployed?(num, view)
613
+ raise PostAlreadyDeployed(num)
614
+ end
615
+
616
+ # Always use view-specific logic when a specific view is provided
617
+ # Add to view-specific unpublished list
618
+ unpublished_file = view_obj.dir/:posts/"unpublished.txt"
619
+ add_post_to_state_file(unpublished_file, num)
620
+
621
+ # Also update global metadata to "no" if this was the first published view
622
+ metadata_file = @root/:posts/d4(num)/"meta.txt"
623
+ if File.exist?(metadata_file)
624
+ metadata = getvars(metadata_file)
625
+ metadata[:"post.published"] = "no"
626
+ lines = metadata.map { |k, v| sprintf("%-18s %s", k, v) }
627
+ write_file(metadata_file, lines.join("\n"))
628
+ end
629
+
630
+ # Regenerate the post
631
+ generate_post(num)
632
+ end
633
+
634
+ def post_published?(num, view = nil)
635
+ validate_post_id(num)
636
+
637
+ # If no specific view, check global metadata (for backward compatibility)
638
+ if view.nil?
639
+ metadata_file = @root/:posts/d4(num)/"meta.txt"
640
+ return false unless File.exist?(metadata_file)
641
+
642
+ metadata = getvars(metadata_file)
643
+ return metadata[:"post.published"] != "no"
644
+ end
645
+
646
+ # Check view-specific published status
647
+ view = lookup_view(view)
648
+ unpublished_file = view.dir/:posts/"unpublished.txt"
649
+ !post_in_state_file?(unpublished_file, num)
650
+ end
651
+
652
+ def post_deleted?(num)
653
+ validate_post_id(num)
654
+
655
+ # Check deleted location first (with underscore prefix)
656
+ deleted_metadata_file = @root/:posts/"_#{d4(num)}"/"meta.txt"
657
+ if File.exist?(deleted_metadata_file)
658
+ return true
659
+ end
660
+
661
+ # Check normal location
662
+ metadata_file = @root/:posts/d4(num)/"meta.txt"
663
+ return false unless File.exist?(metadata_file)
664
+
665
+ metadata = getvars(metadata_file)
666
+ metadata[:"post.deleted"] == "yes"
436
667
  end
437
668
 
438
669
  def generate_post(num)
@@ -443,43 +674,54 @@ class Scriptorium::Repo
443
674
 
444
675
  # Read content file
445
676
  vars = { View: @current_view.name, :"post.id" => num }
446
- # live = Livetext.customize(mix: "lt3scriptor", call: ".nopara", vars: vars)
447
- # text = live.xform_file(content_file)
448
- # vars, _body = live.vars.vars, live.body
677
+ # Mark transform as post-context
678
+ vars[:"post.context"] = "post"
449
679
 
450
- live = Livetext.customize(mix: "lt3scriptor", call: ".nopara", vars: vars)
451
- body, vars = live.process(file: content_file)
452
-
453
- # Create or update metadata from post content
680
+ # Merge metadata into vars if metadata file exists
454
681
  if File.exist?(metadata_file)
455
- # Preserve existing metadata (like post.published timestamp)
456
- existing_metadata = getvars(metadata_file)
457
- metadata_vars = create_metadata_from_content(num, vars)
458
- # Merge existing metadata over defaults
459
- existing_metadata.each do |key, value|
460
- metadata_vars[key] = value
461
- end
462
- else
463
- # Create new metadata
464
- metadata_vars = create_metadata_from_content(num, vars)
682
+ metadata = getvars(metadata_file)
683
+ vars.merge!(metadata)
465
684
  end
466
685
 
686
+ live = Livetext.customize(mix: "lt3scriptor", call: ".nopara", vars: vars)
687
+ body, vars = live.process(file: content_file)
688
+
689
+ # Debug: Write vars to file
690
+ File.open("/tmp/debug_vars_#{num}.txt", "w") do |f|
691
+ f.puts "Vars after LiveText processing:"
692
+ vars.each { |k, v| f.puts "#{k} = #{v.inspect}" }
693
+ end
694
+
695
+ # Use metadata from LiveText processing
696
+ # Filter vars to only include post.* fields for metadata
697
+ metadata_vars = vars.select {|k,v| k.to_s.start_with?("post.") }
698
+ metadata_vars.delete(:"post.body")
699
+ metadata_vars[:"post.slug"] = slugify(num, vars[:"post.title"]) + ".html"
700
+ metadata_vars[:"post.published"] = "no"
701
+ metadata_vars[:"post.deployed"] = "no"
702
+
703
+ if vars[:"post.created"]
704
+ time = Time.parse(vars[:"post.created"])
705
+ metadata_vars["post.pubdate"] = time.strftime("%Y-%m-%d %H:%M:%S")
706
+ metadata_vars["post.pubdate.month"] = time.strftime("%B")
707
+ metadata_vars["post.pubdate.day"] = time.strftime("%e")
708
+ metadata_vars["post.pubdate.year"] = time.strftime("%Y")
709
+ end
710
+
467
711
  # Write metadata file
468
712
  lines = metadata_vars.map { |k, v| sprintf("%-18s %s", k, v) }
469
713
  write_file(metadata_file, lines.join("\n"))
470
714
 
471
- # Merge metadata into vars, but don't override content vars
472
- metadata_vars.each { |key, value| vars[key] = value unless vars.key?(key) }
473
-
474
- views = vars[:"post.views"].strip.split(/\s+/)
715
+ views = (vars[:"post.views"] || "").strip.split(/\s+/)
475
716
  vars[:"post.views"] = views.join(" ") # Ensure post.views is set in vars
476
717
  views.each do |view|
477
718
  view = lookup_view(view)
478
- theme = view.theme
479
719
  vars[:"post.id"] = num.to_s # Always use the post number as ID
480
720
  vars[:"post.body"] = body
481
- template = @predef.post_template("standard")
482
- set_pubdate(vars)
721
+ vars[:"post.date"] = self.post(num).date # Set post.date for templates
722
+
723
+
724
+ template = support_data('templates/post.lt3')
483
725
  # Add Reddit button if enabled
484
726
  vars[:"reddit_button"] = view.generate_reddit_button(vars)
485
727
  final = substitute(vars, template)
@@ -487,31 +729,7 @@ class Scriptorium::Repo
487
729
  end
488
730
  end
489
731
 
490
- private def create_metadata_from_content(num, vars)
491
- metadata = {}
492
-
493
- # Set required fields
494
- metadata[:"post.id"] = d4(num)
495
- metadata[:"post.created"] = ymdhms
496
- metadata[:"post.published"] = "no" # Default to unpublished
497
- metadata[:"post.deployed"] = "no"
498
-
499
- # Copy fields from content vars
500
- metadata[:"post.title"] = vars[:"post.title"] || "ADD TITLE HERE"
501
- metadata[:"post.blurb"] = vars[:"post.blurb"] || "ADD BLURB HERE"
502
- metadata[:"post.views"] = vars[:"post.views"] || "sample"
503
- metadata[:"post.tags"] = vars[:"post.tags"] || ""
504
-
505
- metadata
506
- end
507
732
 
508
- private def set_pubdate(vars) # Not Post#set_pubdate
509
- t = Time.now
510
- vars[:"post.pubdate"] = t.strftime("%Y-%m-%d %H:%M:%S")
511
- vars[:"post.pubdate.month"] = t.strftime("%B")
512
- vars[:"post.pubdate.day"] = t.strftime("%d")
513
- vars[:"post.pubdate.year"] = t.strftime("%Y")
514
- end
515
733
 
516
734
  def all_posts(view = nil)
517
735
  posts = []
@@ -525,6 +743,31 @@ class Scriptorium::Repo
525
743
  view = lookup_view(view)
526
744
  posts.select {|x| x.views.include?(view.name) }
527
745
  end
746
+
747
+ def all_posts_including_deleted(view = nil)
748
+ posts = []
749
+ dirs = Dir.children(@root/:posts)
750
+ dirs.each do |id4|
751
+ # Include both normal and deleted posts
752
+ if id4.start_with?('_')
753
+ # Deleted post - remove underscore prefix and pass deleted: true
754
+ original_id = id4[1..-1]
755
+ posts << Scriptorium::Post.read(self, original_id, deleted: true)
756
+ else
757
+ posts << Scriptorium::Post.read(self, id4)
758
+ end
759
+ end
760
+ return posts if view.nil?
761
+ view = lookup_view(view)
762
+ posts.select {|x|
763
+ views_str = x.views
764
+ if views_str.nil? || views_str.strip.empty?
765
+ false
766
+ else
767
+ views_str.strip.split(/\s+/).include?(view.name)
768
+ end
769
+ }
770
+ end
528
771
 
529
772
  def generate_post_index(view)
530
773
  view = lookup_view(view)
@@ -543,16 +786,81 @@ class Scriptorium::Repo
543
786
  return Scriptorium::Post.new(self, id) if File.exist?(deleted_meta)
544
787
 
545
788
  # Post not found in either location
546
- nil
789
+ raise CannotGetPost("Post with ID #{id} not found")
790
+ end
791
+
792
+ def delete_post(num)
793
+ validate_post_id(num)
794
+
795
+ # Check if post exists
796
+ metadata_file = @root/:posts/d4(num)/"meta.txt"
797
+ unless File.exist?(metadata_file)
798
+ raise CannotGetPost("Post #{num} not found")
799
+ end
800
+
801
+ # Check if already deleted
802
+ if post_deleted?(num)
803
+ raise PostAlreadyDeleted(num)
804
+ end
805
+
806
+ # Mark as deleted in metadata
807
+ metadata = getvars(metadata_file)
808
+ metadata[:"post.deleted"] = "yes"
809
+ metadata[:"post.deleted_at"] = ymdhms
810
+
811
+ # Write updated metadata
812
+ lines = metadata.map { |k, v| sprintf("%-18s %s", k, v) }
813
+ write_file(metadata_file, lines.join("\n"))
814
+
815
+ # Move post directory to deleted location (with underscore prefix)
816
+ post_dir = @root/:posts/d4(num)
817
+ deleted_dir = @root/:posts/"_#{d4(num)}"
818
+
819
+ if Dir.exist?(post_dir)
820
+ FileUtils.mv(post_dir, deleted_dir)
821
+ end
822
+ end
823
+
824
+ def undelete_post(num)
825
+ validate_post_id(num)
826
+
827
+ # Check if post exists in deleted location
828
+ deleted_dir = @root/:posts/"_#{d4(num)}"
829
+ unless Dir.exist?(deleted_dir)
830
+ raise CannotGetPost("Deleted post #{num} not found")
831
+ end
832
+
833
+ # Check if already undeleted
834
+ unless post_deleted?(num)
835
+ raise PostNotDeleted(num)
836
+ end
837
+
838
+ # Move post directory back to normal location
839
+ post_dir = @root/:posts/d4(num)
840
+ FileUtils.mv(deleted_dir, post_dir)
841
+
842
+ # Remove deleted flag from metadata
843
+ metadata_file = @root/:posts/d4(num)/"meta.txt"
844
+ if File.exist?(metadata_file)
845
+ metadata = getvars(metadata_file)
846
+ metadata.delete(:"post.deleted")
847
+ metadata.delete(:"post.deleted_at")
848
+
849
+ # Write updated metadata
850
+ lines = metadata.map { |k, v| sprintf("%-18s %s", k, v) }
851
+ write_file(metadata_file, lines.join("\n"))
852
+ end
547
853
  end
854
+
855
+
548
856
 
549
857
  private def validate_post_id(id)
550
- raise CannotGetPostIdNil if id.nil?
858
+ raise PostIdNil if id.nil?
551
859
 
552
- raise CannotGetPostIdEmpty if id.to_s.strip.empty?
860
+ raise PostIdEmpty if id.to_s.strip.empty?
553
861
 
554
862
  unless id.to_s.match?(/^\d+$/)
555
- raise CannotGetPostIdInvalid(id)
863
+ raise PostIdInvalid(id)
556
864
  end
557
865
  end
558
866
 
@@ -574,17 +882,23 @@ class Scriptorium::Repo
574
882
  reddit.configured?
575
883
  end
576
884
 
885
+
886
+
577
887
  private def validate_view_name(name)
578
- raise CannotCreateViewNameNil if name.nil?
888
+ raise ViewNameNil if name.nil?
579
889
 
580
- raise CannotCreateViewNameEmpty if name.to_s.strip.empty?
890
+ raise ViewNameEmpty if name.to_s.strip.empty?
581
891
  end
582
892
 
893
+
894
+
583
895
  private def validate_view_title(title)
584
- raise CannotCreateViewTitleNil if title.nil?
896
+ raise ViewTitleNil if title.nil?
585
897
 
586
- raise CannotCreateViewTitleEmpty if title.to_s.strip.empty?
898
+ raise ViewTitleEmpty if title.to_s.strip.empty?
587
899
  end
900
+
901
+
588
902
 
589
903
  def self.generate_os_helpers(root)
590
904
  os_code = case RbConfig::CONFIG['host_os']