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
@@ -18,6 +18,17 @@ Symbol.include(PathSep)
18
18
 
19
19
  module Scriptorium::Helpers
20
20
  include Scriptorium::Exceptions
21
+
22
+ # Post comparison for sorting (uses post.date which handles fallback)
23
+ def post_compare(a, b)
24
+ # Primary sort: date (newest first)
25
+ date_compare = b.date <=> a.date
26
+ return date_compare unless date_compare == 0
27
+
28
+ # Secondary sort: post number (ascending) for stable ordering when dates are identical
29
+ a.num <=> b.num
30
+ end
31
+
21
32
  def getvars(file)
22
33
  lines = read_file(file, lines: true)
23
34
  lines.map! {|line| line.sub(/# .*$/, "").strip }
@@ -25,6 +36,8 @@ module Scriptorium::Helpers
25
36
  vhash = Hash.new("")
26
37
  lines.each do |line|
27
38
  var, val = line.split(" ", 2)
39
+ # Fix: treat nil values as empty strings
40
+ val = "" if val.nil?
28
41
  vhash[var.to_sym] = val
29
42
  end
30
43
  vhash
@@ -38,11 +51,21 @@ module Scriptorium::Helpers
38
51
  @root/:views/name
39
52
  end
40
53
 
41
- def write_file(file, content)
54
+ def write_file(file, content, empty: false)
42
55
  # Input validation
43
- raise CannotWriteFilePathNil if file.nil?
56
+ raise FilePathNil if file.nil?
57
+
58
+ raise FilePathEmpty if file.to_s.strip.empty?
44
59
 
45
- raise CannotWriteFilePathEmpty if file.to_s.strip.empty?
60
+ # Handle empty content if empty: true is specified
61
+ if empty && (content.nil? || content.to_s.strip.empty?)
62
+ # If file exists, do nothing; if it doesn't exist, touch it
63
+ unless File.exist?(file)
64
+ FileUtils.mkdir_p(File.dirname(file))
65
+ FileUtils.touch(file)
66
+ end
67
+ return
68
+ end
46
69
 
47
70
  # Ensure parent directory exists
48
71
  FileUtils.mkdir_p(File.dirname(file))
@@ -53,30 +76,30 @@ module Scriptorium::Helpers
53
76
  f.puts content
54
77
  end
55
78
  rescue Errno::ENOSPC => e
56
- raise CannotWriteFileDiskFull(file, e.message)
79
+ raise FileDiskFull(file, e.message)
57
80
  rescue Errno::EACCES => e
58
- raise CannotWriteFilePermissionDenied(file, e.message)
81
+ raise FilePermissionDenied(file, e.message)
59
82
  rescue Errno::ENOENT => e
60
- raise CannotWriteFileDirectoryNotFound(file, e.message)
83
+ raise FileDirectoryNotFound(file, e.message)
61
84
  rescue => e
62
- raise CannotWriteFileError(file, e.message)
85
+ raise WriteFileError(file, e.message)
63
86
  end
64
87
  end
65
88
 
66
- def write_file!(file, *lines)
89
+ def write_file!(file, *lines, empty: false)
67
90
  # Convert nil values to empty strings for proper joining
68
91
  processed_lines = lines.map { |line| line.nil? ? "" : line.to_s }
69
92
  content = processed_lines.join("\n")
70
93
  # Always add a newline at the end to ensure there's an empty line
71
94
  content += "\n"
72
- write_file(file, content)
95
+ write_file(file, content, empty: empty)
73
96
  end
74
97
 
75
98
  def make_dir(dir, create_parents = false)
76
99
  # Input validation
77
- raise CannotCreateDirectoryPathNil if dir.nil?
78
-
79
- raise CannotCreateDirectoryPathEmpty if dir.to_s.strip.empty?
100
+ raise DirectoryPathNil if dir.nil?
101
+
102
+ raise DirectoryPathEmpty if dir.to_s.strip.empty?
80
103
 
81
104
  # Create parent directories if requested
82
105
  if create_parents
@@ -86,25 +109,25 @@ module Scriptorium::Helpers
86
109
  begin
87
110
  Dir.mkdir(dir)
88
111
  rescue Errno::ENOSPC => e
89
- raise CannotCreateDirectoryDiskFull(dir, e.message)
112
+ raise DirectoryDiskFull(dir, e.message)
90
113
  rescue Errno::EACCES => e
91
- raise CannotCreateDirectoryPermissionDenied(dir, e.message)
114
+ raise DirectoryPermissionDenied(dir, e.message)
92
115
  rescue Errno::ENOENT => e
93
- raise CannotCreateDirectoryParentNotFound(dir, e.message)
116
+ raise DirectoryParentNotFound(dir, e.message)
94
117
  rescue Errno::EEXIST => e
95
118
  # Directory already exists - this is usually not an error
96
119
  # But we could make this configurable if needed
97
120
  rescue => e
98
- raise CannotCreateDirectoryError(dir, e.message)
121
+ raise CreateDirectoryError(dir, e.message)
99
122
  end
100
123
  end
101
124
  end
102
125
 
103
126
  def system!(command, description = nil)
104
127
  # Input validation
105
- raise CannotExecuteCommandNil if command.nil?
106
-
107
- raise CannotExecuteCommandEmpty if command.to_s.strip.empty?
128
+ raise CommandNil if command.nil?
129
+
130
+ raise CommandEmpty if command.to_s.strip.empty?
108
131
 
109
132
  # Execute command with error handling
110
133
  success = system(command)
@@ -119,9 +142,9 @@ module Scriptorium::Helpers
119
142
 
120
143
  def need(type, path, exception_class = RuntimeError)
121
144
  # Input validation
122
- raise CannotRequirePathNil(type) if path.nil?
123
-
124
- raise CannotRequirePathEmpty(type) if path.to_s.strip.empty?
145
+ raise RequirePathNil(type) if path.nil?
146
+
147
+ raise RequirePathEmpty(type) if path.to_s.strip.empty?
125
148
 
126
149
  # Check if file/directory exists
127
150
  exists = case type
@@ -146,9 +169,9 @@ module Scriptorium::Helpers
146
169
 
147
170
  def read_file(file, options = {})
148
171
  # Input validation
149
- raise CannotReadFilePathNil if file.nil?
150
-
151
- raise CannotReadFilePathEmpty if file.to_s.strip.empty?
172
+ raise ReadFilePathNil if file.nil?
173
+
174
+ raise ReadFilePathEmpty if file.to_s.strip.empty?
152
175
 
153
176
  # Handle missing file with fallback
154
177
  if options[:missing_fallback]
@@ -172,12 +195,12 @@ module Scriptorium::Helpers
172
195
  if options[:missing_fallback]
173
196
  return options[:missing_fallback]
174
197
  else
175
- raise CannotReadFileNotFound(file, e.message)
198
+ raise ReadFileNotFound(file, e.message)
176
199
  end
177
200
  rescue Errno::EACCES => e
178
- raise CannotReadFilePermissionDenied(file, e.message)
201
+ raise ReadFilePermissionDenied(file, e.message)
179
202
  rescue => e
180
- raise CannotReadFileError(file, e.message)
203
+ raise ReadFileError(file, e.message)
181
204
  end
182
205
  end
183
206
 
@@ -207,6 +230,8 @@ module Scriptorium::Helpers
207
230
  end
208
231
 
209
232
  def slugify(id, title)
233
+ return "#{d4(id)}-untitled" if title.nil? || title.strip.empty?
234
+
210
235
  slug = title.downcase.strip
211
236
  .gsub(/[^a-z0-9\s_-]/, '') # remove punctuation
212
237
  .gsub(/[\s_-]+/, '-') # replace spaces and underscores with hyphen
@@ -225,7 +250,11 @@ module Scriptorium::Helpers
225
250
  end
226
251
 
227
252
  def ymdhms
228
- Time.now.strftime("%Y-%m-%d-%H-%M-%S")
253
+ Time.now.strftime("%Y-%m-%d %H:%M:%S")
254
+ end
255
+
256
+ def ymdhms_filename
257
+ Time.now.strftime("%Y%m%d-%H%M%S")
229
258
  end
230
259
 
231
260
  def see_file(file) # Really from TestHelpers
@@ -243,16 +272,14 @@ module Scriptorium::Helpers
243
272
  lines.each {|line| line.gsub!(/ *#.*$/, "") }
244
273
  entries = []
245
274
 
246
- # Determine the root name
247
- first_line = lines.shift
248
- root = first_line.strip.sub(/\/$/, "") # remove trailing slash
249
- root_path = File.join(base, root)
250
- make_dir(root_path) unless File.exist?(root_path)
251
-
252
- # Prepare stack starting from root
253
- stack = [root_path]
275
+ # Always throw away the first line (for backward compatibility)
276
+ lines.shift
277
+
278
+ # Create the base directory and start stack there
279
+ make_dir(base) unless File.exist?(base)
280
+ stack = [base]
254
281
 
255
- # Parse the remaining lines
282
+ # Parse all lines as structure
256
283
  lines.each do |line|
257
284
  if (i = line.index(/ [a-zA-Z0-9_.]/))
258
285
  name = line[(i + 1)..-1]
@@ -301,6 +328,17 @@ module Scriptorium::Helpers
301
328
  end
302
329
  lines # Return cleaned lines
303
330
  end
331
+
332
+ def parse_commented_file(file_path)
333
+ config = {}
334
+ read_commented_file(file_path).each do |line|
335
+ if line.include?(' ')
336
+ key, value = line.split(/\s+/, 2)
337
+ config[key] = config[key.to_sym] = value
338
+ end
339
+ end
340
+ config
341
+ end
304
342
 
305
343
  def cf_time(t1, t2)
306
344
  t1 = t1.split(/- :/, 6)
@@ -310,32 +348,82 @@ module Scriptorium::Helpers
310
348
  t1 <=> t2
311
349
  end
312
350
 
351
+ # Post state management helpers
352
+
353
+ def read_post_state_file(file_path)
354
+ return [] unless File.exist?(file_path)
355
+ content = read_file(file_path)
356
+ return [] if content.strip.empty?
357
+ content.lines.map { |line| line.strip.to_i }.reject { |id| id == 0 }
358
+ end
359
+
360
+ def write_post_state_file(file_path, post_ids)
361
+ if post_ids.empty?
362
+ # Write empty file (empty = all posts are in that state)
363
+ # For state files, empty means "all posts are in this state", so we need to clear the file
364
+ write_file(file_path, "")
365
+ else
366
+ content = post_ids.sort.uniq.join("\n") + "\n"
367
+ write_file(file_path, content)
368
+ end
369
+ end
370
+
371
+ def add_post_to_state_file(file_path, post_id)
372
+ post_ids = read_post_state_file(file_path)
373
+ post_ids << post_id unless post_ids.include?(post_id)
374
+ write_post_state_file(file_path, post_ids)
375
+ end
376
+
377
+ def remove_post_from_state_file(file_path, post_id)
378
+ post_ids = read_post_state_file(file_path)
379
+ post_ids.delete(post_id)
380
+ write_post_state_file(file_path, post_ids)
381
+ end
382
+
383
+ def post_in_state_file?(file_path, post_id)
384
+ post_ids = read_post_state_file(file_path)
385
+ post_ids.include?(post_id)
386
+ end
387
+
388
+ # Helper method to find asset recursively in a directory
389
+ def find_asset(base_dir, asset_name)
390
+ # First try exact path
391
+ exact_path = "#{base_dir}/#{asset_name}"
392
+ return exact_path if File.exist?(exact_path)
393
+
394
+ # Then search recursively
395
+ Dir.glob("#{base_dir}/**/*").each do |file|
396
+ next unless File.file?(file)
397
+ next unless File.basename(file) == File.basename(asset_name)
398
+ return file
399
+ end
400
+
401
+ nil
402
+ end
403
+
313
404
  def get_asset_path(name)
314
405
  if Scriptorium::Repo.testing
315
406
  # Development/testing: Check dev_assets first, then local assets
316
- if File.exist?("dev_assets/#{name}")
317
- return "dev_assets/#{name}"
318
- elsif File.exist?("assets/#{name}")
319
- return "assets/#{name}"
320
- else
321
- raise AssetNotFound(name)
322
- end
407
+ dev_asset = find_asset("dev_assets", name)
408
+ return dev_asset if dev_asset
409
+
410
+ local_asset = find_asset("assets", name)
411
+ return local_asset if local_asset
412
+
413
+ raise AssetNotFound(name)
323
414
  else # Production
324
415
  # Production: Check user assets first, then gem assets
325
416
 
326
- # Check user assets first (highest priority)
327
- if File.exist?("assets/#{name}")
328
- return "assets/#{name}"
329
- end
417
+ # Check user assets first (highest priority) - recursively
418
+ user_asset = find_asset("assets", name)
419
+ return user_asset if user_asset
330
420
 
331
- # Then check gem assets (fallback)
421
+ # Then check gem assets (fallback) - recursively
332
422
  begin
333
423
  gem_spec = Gem.loaded_specs['scriptorium']
334
424
  if gem_spec
335
- gem_asset_path = "#{gem_spec.full_gem_path}/assets/#{name}"
336
- if File.exist?(gem_asset_path)
337
- return gem_asset_path
338
- end
425
+ gem_asset = find_asset("#{gem_spec.full_gem_path}/assets", name)
426
+ return gem_asset if gem_asset
339
427
  end
340
428
  rescue => e
341
429
  # If gem lookup fails, continue without gem assets
@@ -385,43 +473,41 @@ module Scriptorium::Helpers
385
473
 
386
474
  def list_gem_assets
387
475
  assets = []
388
- begin
389
- gem_spec = Gem.loaded_specs['scriptorium']
390
- if gem_spec
391
- gem_assets_dir = "#{gem_spec.full_gem_path}/assets"
392
- if Dir.exist?(gem_assets_dir)
393
- Dir.glob("#{gem_assets_dir}/**/*").each do |file|
394
- next unless File.file?(file)
395
- relative_path = file.sub("#{gem_assets_dir}/", "")
396
- assets << relative_path
397
- end
476
+ gem_spec = Gem.loaded_specs['scriptorium']
477
+ if gem_spec
478
+ gem_assets_dir = "#{gem_spec.full_gem_path}/assets"
479
+ if Dir.exist?(gem_assets_dir)
480
+ Dir.glob("#{gem_assets_dir}/**/*").each do |file|
481
+ next unless File.file?(file)
482
+ relative_path = file.sub("#{gem_assets_dir}/", "")
483
+ assets << relative_path
398
484
  end
399
485
  end
400
- rescue => e
401
- # If gem lookup fails, return empty array
402
486
  end
403
487
  assets.sort
488
+ rescue => e
489
+ # If gem lookup fails, return empty array
490
+ []
404
491
  end
405
492
 
406
493
  def copy_gem_asset_to_user(asset_name, target_dir = "assets")
407
- begin
408
- gem_spec = Gem.loaded_specs['scriptorium']
409
- if gem_spec
410
- gem_asset_path = "#{gem_spec.full_gem_path}/assets/#{asset_name}"
411
- if File.exist?(gem_asset_path)
412
- # Create target directory if it doesn't exist
413
- FileUtils.mkdir_p(target_dir) unless Dir.exist?(target_dir)
414
-
415
- # Copy the asset
416
- target_path = "#{target_dir}/#{File.basename(asset_name)}"
417
- FileUtils.cp(gem_asset_path, target_path)
418
- return target_path
419
- end
494
+ gem_spec = Gem.loaded_specs['scriptorium']
495
+ if gem_spec
496
+ gem_asset_path = "#{gem_spec.full_gem_path}/assets/#{asset_name}"
497
+ if File.exist?(gem_asset_path)
498
+ # Create target directory if it doesn't exist
499
+ FileUtils.mkdir_p(target_dir) unless Dir.exist?(target_dir)
500
+
501
+ # Copy the asset
502
+ target_path = "#{target_dir}/#{File.basename(asset_name)}"
503
+ FileUtils.cp(gem_asset_path, target_path)
504
+ return target_path
420
505
  end
421
- rescue => e
422
- # If gem lookup fails, return nil
423
506
  end
424
507
  nil
508
+ rescue => e
509
+ # If gem lookup fails, return nil
510
+ nil
425
511
  end
426
512
 
427
513
  # Clipboard helper methods
@@ -471,5 +557,119 @@ module Scriptorium::Helpers
471
557
  nil
472
558
  end
473
559
  end
474
- end
475
560
 
561
+ def tty(str)
562
+ File.open('/dev/tty', 'w') { |f| f.puts str }
563
+ end
564
+
565
+ def format_date(format, day = Time.now.to_date)
566
+ # Handle nil dates
567
+ return format if day.nil?
568
+
569
+ # Handle string dates by parsing them
570
+ if day.is_a?(String)
571
+ begin
572
+ day = Date.parse(day)
573
+ rescue Date::Error
574
+ return format # Return original format if date is invalid
575
+ end
576
+ end
577
+
578
+ # Parse the format string and replace tokens
579
+ result = format.dup
580
+
581
+ # Month name vs number
582
+ result.gsub!(/\bmonth\b/, day.strftime("%B"))
583
+ result.gsub!(/\bmm\b/, day.strftime("%m"))
584
+
585
+ # Day ordinal vs number - use smart padding
586
+ result.gsub!(/\bday\b/, ordinalize(day.day))
587
+ result.gsub!(/\bdd\b/) { smart_pad_day(day.day, result, $~.offset(0)[0]) }
588
+
589
+ # Year formats
590
+ result.gsub!(/\byyyy\b/, day.strftime("%Y"))
591
+ result.gsub!(/\byy\b/, day.strftime("%y"))
592
+
593
+ # Line breaks - handle spaces around break
594
+ result.gsub!(/\s+break\s+/, "<br>")
595
+ result.gsub!(/\s+break\b/, "<br>")
596
+ result.gsub!(/\bbreak\s+/, "<br>")
597
+ result.gsub!(/\bbreak\b/, "<br>")
598
+
599
+ result
600
+ end
601
+
602
+ private
603
+
604
+ def smart_pad_day(day, format_string, position)
605
+ # Look at the context around the dd token to determine padding
606
+ before = format_string[0...position]
607
+ after = format_string[position + 2..-1] || ""
608
+
609
+ # Check if we should pad based on context
610
+ should_pad = false
611
+
612
+ # Rule 1: Initial number followed immediately by punctuation
613
+ if position == 0 && after.match?(/^[^a-zA-Z\s]/)
614
+ should_pad = true
615
+ end
616
+
617
+ # Rule 2: Number enclosed in punctuation (before and after)
618
+ if before.match?(/[^a-zA-Z\s]$/) && after.match?(/^[^a-zA-Z\s]/)
619
+ should_pad = true
620
+ end
621
+
622
+ # Rule 3: Terminal number preceded by punctuation
623
+ if after.empty? && before.match?(/[^a-zA-Z\s]$/)
624
+ should_pad = true
625
+ end
626
+
627
+ # Rule 4: Middle number with spaces - don't pad
628
+ if before.match?(/\s$/) || after.match?(/^\s/)
629
+ should_pad = false
630
+ end
631
+
632
+ should_pad ? day.to_s.rjust(2, '0') : day.to_s
633
+ end
634
+
635
+ def ordinalize(number)
636
+ case number % 100
637
+ when 11, 12, 13
638
+ "#{number}th"
639
+ else
640
+ case number % 10
641
+ when 1 then "#{number}st"
642
+ when 2 then "#{number}nd"
643
+ when 3 then "#{number}rd"
644
+ else "#{number}th"
645
+ end
646
+ end
647
+ end
648
+
649
+ def support_file(relative_path)
650
+ # Get the path to support files, handling both dev and gem environments
651
+ begin
652
+ gem_spec = Gem.loaded_specs['scriptorium']
653
+ if gem_spec
654
+ # Production: use gem path
655
+ "#{gem_spec.full_gem_path}/lib/scriptorium/support/#{relative_path}"
656
+ else
657
+ # Development: use project root (not current working directory)
658
+ project_root = File.expand_path(File.join(File.dirname(__FILE__), '..', '..'))
659
+ File.expand_path("lib/scriptorium/support/#{relative_path}", project_root)
660
+ end
661
+ rescue => e
662
+ # Fallback to development path if gem lookup fails
663
+ project_root = File.expand_path(File.join(File.dirname(__FILE__), '..', '..'))
664
+ File.expand_path("lib/scriptorium/support/#{relative_path}", project_root)
665
+ end
666
+ end
667
+
668
+ def support_data(relative_path)
669
+ read_file(support_file(relative_path))
670
+ end
671
+
672
+ def copy_support_file(relative_path, target_path)
673
+ FileUtils.cp(support_file(relative_path), target_path)
674
+ end
675
+ end
@@ -22,10 +22,10 @@ class Scriptorium::Post
22
22
  end
23
23
 
24
24
  private def validate_initialization(repo, num)
25
- raise CannotCreatePostRepoNil if repo.nil?
26
- raise CannotCreatePostNumNil if num.nil?
27
- raise CannotCreatePostNumEmpty if num.to_s.strip.empty?
28
- raise CannotCreatePostNumInvalid(num) unless num.to_s.match?(/^\d+$/)
25
+ raise PostRepoNil if repo.nil?
26
+ raise PostNumNil if num.nil?
27
+ raise PostNumEmpty if num.to_s.strip.empty?
28
+ raise PostNumInvalid(num) unless num.to_s.match?(/^\d+$/)
29
29
  end
30
30
 
31
31
  def dir
@@ -33,16 +33,40 @@ class Scriptorium::Post
33
33
  end
34
34
 
35
35
  def meta_file
36
- # Check if post is in deleted directory (with underscore prefix)
37
- deleted_meta = @repo.root/:posts/"_#{@num}"/"meta.txt"
38
- return deleted_meta if File.exist?(deleted_meta)
36
+ # Check what directory actually exists
37
+ normal_dir = @repo.root/:posts/@num
38
+ deleted_dir = @repo.root/:posts/"_#{@num}"
39
39
 
40
- # Otherwise use normal directory
41
- @repo.root/:posts/@num/"meta.txt"
40
+ if Dir.exist?(normal_dir)
41
+ # Normal post directory exists
42
+ normal_dir/"meta.txt"
43
+ elsif Dir.exist?(deleted_dir)
44
+ # Deleted post directory exists
45
+ deleted_dir/"meta.txt"
46
+ else
47
+ # Neither exists - post never existed
48
+ raise "Post directory for #{@num} not found"
49
+ end
42
50
  end
43
-
44
-
45
51
 
52
+ def validate_metadata_consistency
53
+ return unless File.exist?(meta_file)
54
+
55
+ normal_dir = @repo.root/:posts/@num
56
+ deleted_dir = @repo.root/:posts/"_#{@num}"
57
+ metadata_deleted = meta["post.deleted"] == "true"
58
+
59
+ if Dir.exist?(normal_dir) && metadata_deleted
60
+ raise "Inconsistency: Post #{@num} has normal directory but metadata shows deleted=true"
61
+ elsif Dir.exist?(deleted_dir) && !metadata_deleted
62
+ raise "Inconsistency: Post #{@num} has deleted directory but metadata shows deleted=false"
63
+ end
64
+ end
65
+
66
+ def blurb_text
67
+ meta["post.blurb"]
68
+ end
69
+
46
70
  def blurb
47
71
  meta["post.blurb"]
48
72
  end
@@ -58,6 +82,14 @@ class Scriptorium::Post
58
82
  def pubdate
59
83
  meta["post.pubdate"]
60
84
  end
85
+
86
+ def created
87
+ meta["post.created"]
88
+ end
89
+
90
+ def date
91
+ pubdate || created
92
+ end
61
93
 
62
94
  def set_pubdate(ymd, seconds: 0)
63
95
  check_invariants
@@ -80,12 +112,12 @@ class Scriptorium::Post
80
112
  end
81
113
 
82
114
  private def validate_date_format(date)
83
- raise CannotSetPubdateYmdNil if date.nil?
84
- raise CannotSetPubdateYmdEmpty if date.to_s.strip.empty?
85
- raise CannotSetPubdateInvalidFormat(date) unless date.to_s.match?(/^\d{4}-\d{2}-\d{2}$/)
115
+ raise PubdateYmdNil if date.nil?
116
+ raise PubdateYmdEmpty if date.to_s.strip.empty?
117
+ raise PubdateInvalidFormat(date) unless date.to_s.match?(/^\d{4}-\d{2}-\d{2}$/)
86
118
  end
87
119
 
88
- private def update_pubdate_metadata(time)
120
+ def update_pubdate_metadata(time)
89
121
  meta["post.pubdate"] = time.strftime("%Y-%m-%d %H:%M:%S")
90
122
  meta["post.pubdate.month"] = time.strftime("%B")
91
123
  meta["post.pubdate.day"] = time.strftime("%e")
@@ -123,8 +155,25 @@ class Scriptorium::Post
123
155
  meta["post.tags"]
124
156
  end
125
157
 
158
+ def tags_array
159
+ tags_str = meta["post.tags"]
160
+ return [] if tags_str.nil? || tags_str.strip.empty?
161
+ tags_str.strip.split(/,\s*/)
162
+ end
163
+
126
164
  def deleted
127
- meta["post.deleted"] == "true"
165
+ # Check what directory actually exists
166
+ normal_dir = @repo.root/:posts/@num
167
+ deleted_dir = @repo.root/:posts/"_#{@num}"
168
+
169
+ if Dir.exist?(deleted_dir)
170
+ true
171
+ elsif Dir.exist?(normal_dir)
172
+ false
173
+ else
174
+ # Neither exists - post never existed
175
+ raise "Post directory for #{@num} not found"
176
+ end
128
177
  end
129
178
 
130
179
  def deleted=(value)
@@ -144,9 +193,10 @@ class Scriptorium::Post
144
193
  end
145
194
 
146
195
  # New class method to read metadata and initialize the Post
147
- def self.read(repo, num)
196
+ def self.read(repo, num, deleted: false)
148
197
  post = new(repo, num)
149
198
  post.load_metadata
199
+ post.validate_metadata_consistency
150
200
  post
151
201
  end
152
202
 
@@ -159,6 +209,20 @@ class Scriptorium::Post
159
209
  return @vars if defined?(@vars)
160
210
  @vars = Hash.new("")
161
211
  meta.each_pair {|k,v| @vars[k.to_sym] = v }
212
+
213
+ # Add computed date components for template access
214
+ date_value = pubdate || created
215
+ if date_value
216
+ begin
217
+ date_obj = Date.parse(date_value)
218
+ @vars[:"post.pubdate.month"] = date_obj.strftime("%B")
219
+ @vars[:"post.pubdate.day"] = date_obj.strftime("%e").strip
220
+ @vars[:"post.pubdate.year"] = date_obj.strftime("%Y")
221
+ rescue Date::Error
222
+ # If date is invalid, leave the computed fields empty
223
+ end
224
+ end
225
+
162
226
  @vars
163
227
  end
164
228
 
@@ -3,7 +3,7 @@ require 'redd'
3
3
  require_relative 'exceptions'
4
4
  require_relative 'helpers'
5
5
 
6
- module Scriptorium
6
+ class Scriptorium
7
7
  class Reddit
8
8
  include Scriptorium::Exceptions
9
9
  include Scriptorium::Helpers