scriptorium 0.0.3 → 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 (353) hide show
  1. checksums.yaml +4 -4
  2. data/README.lt3 +324 -0
  3. data/README.md +3155 -1
  4. data/assets/.DS_Store +0 -0
  5. data/assets/README.md +44 -0
  6. data/assets/icons/social/reddit.png +0 -0
  7. data/assets/icons/social/x-logo.png +0 -0
  8. data/assets/icons/ui/.DS_Store +0 -0
  9. data/assets/icons/ui/back.png +0 -0
  10. data/assets/icons/ui/copy.png +0 -0
  11. data/assets/icons/ui/down.png +0 -0
  12. data/assets/icons/ui/end.png +0 -0
  13. data/assets/icons/ui/exit.png +0 -0
  14. data/assets/icons/ui/foo +10 -0
  15. data/assets/icons/ui/home.png +0 -0
  16. data/assets/icons/ui/left.png +0 -0
  17. data/assets/icons/ui/next.png +0 -0
  18. data/assets/icons/ui/right.png +0 -0
  19. data/assets/icons/ui/start.png +0 -0
  20. data/assets/icons/ui/up.png +0 -0
  21. data/assets/imagenotfound.jpg +0 -0
  22. data/assets/samples/placeholder.svg +9 -0
  23. data/assets/themes/standard/favicon.svg +6 -0
  24. data/bin/sblog +84 -5
  25. data/bin/scriptorium +1 -0
  26. data/doc/README.txt +6 -0
  27. data/doc/anti-amnesia/20250727-054000-scriptorium-overview.md +94 -0
  28. data/doc/anti-amnesia/20250727-123000-anti-amnesia-conventions.md +2 -0
  29. data/doc/anti-amnesia/20250727-172600-cursor-rbenv-ruby-version-mystery.md +45 -0
  30. data/doc/anti-amnesia/20250727-172900-ai-cognitive-assessment-capabilities.md +40 -0
  31. data/doc/anti-amnesia/20250728-124243-aaa-syntax-clarification.md +46 -0
  32. data/doc/anti-amnesia/20250729-210000-reddit-autopost-integration-complete.md +158 -0
  33. data/doc/anti-amnesia/20250804-190500-cognitive-loop-bug.md +35 -0
  34. data/doc/anti-amnesia/20250804-190700-anti-amnesia-timestamping-fix.md +27 -0
  35. data/doc/anti-amnesia/20250807-213025.md +116 -0
  36. data/doc/anti-amnesia/20250901-211714-codemirror-integration-and-web-tests.md +172 -0
  37. data/doc/anti-amnesia/20250902-002402-backup-restore-system.md +126 -0
  38. data/doc/anti-amnesia/20250907-203339-backup-metadata-implementation.md +66 -0
  39. data/doc/banner_svg_config.md +114 -0
  40. data/doc/contrib.lt3 +8 -0
  41. data/doc/dependencies.md +281 -0
  42. data/doc/hacker.lt3 +5 -0
  43. data/doc/imported/0001-elixir-conf-2014/metadata.txt +7 -0
  44. data/doc/imported/0001-elixir-conf-2014/post.html +37 -0
  45. data/doc/imported/0001-elixir-conf-2014/source.lt3 +22 -0
  46. data/doc/imported/0002-programmers-and-word-processing/metadata.txt +7 -0
  47. data/doc/imported/0002-programmers-and-word-processing/post.html +192 -0
  48. data/doc/imported/0002-programmers-and-word-processing/source.lt3 +146 -0
  49. data/doc/imported/0003-how-to-turn-your-brain-sideways/metadata.txt +7 -0
  50. data/doc/imported/0003-how-to-turn-your-brain-sideways/post.html +60 -0
  51. data/doc/imported/0003-how-to-turn-your-brain-sideways/source.lt3 +40 -0
  52. data/doc/imported/0004-upcoming-lone-star-ruby-conference/metadata.txt +7 -0
  53. data/doc/imported/0004-upcoming-lone-star-ruby-conference/post.html +42 -0
  54. data/doc/imported/0004-upcoming-lone-star-ruby-conference/source.lt3 +24 -0
  55. data/doc/imported/0005-elixir-conf-2015-announced/metadata.txt +7 -0
  56. data/doc/imported/0005-elixir-conf-2015-announced/post.html +30 -0
  57. data/doc/imported/0005-elixir-conf-2015-announced/source.lt3 +16 -0
  58. data/doc/imported/0006-ruby-for-dinosaurs/metadata.txt +7 -0
  59. data/doc/imported/0006-ruby-for-dinosaurs/post.html +43 -0
  60. data/doc/imported/0006-ruby-for-dinosaurs/source.lt3 +27 -0
  61. data/doc/imported/0007-phoenix-isnt-rails/metadata.txt +7 -0
  62. data/doc/imported/0007-phoenix-isnt-rails/post.html +116 -0
  63. data/doc/imported/0007-phoenix-isnt-rails/source.lt3 +87 -0
  64. data/doc/imported/0008-concerning-the-term-monkeypatching/metadata.txt +7 -0
  65. data/doc/imported/0008-concerning-the-term-monkeypatching/post.html +129 -0
  66. data/doc/imported/0008-concerning-the-term-monkeypatching/source.lt3 +92 -0
  67. data/doc/imported/0009-announcement-coming-soon/metadata.txt +7 -0
  68. data/doc/imported/0009-announcement-coming-soon/post.html +33 -0
  69. data/doc/imported/0009-announcement-coming-soon/source.lt3 +19 -0
  70. data/doc/imported/0010-immutable-data-ditching-the-wax-tablet/metadata.txt +7 -0
  71. data/doc/imported/0010-immutable-data-ditching-the-wax-tablet/post.html +175 -0
  72. data/doc/imported/0010-immutable-data-ditching-the-wax-tablet/source.lt3 +139 -0
  73. data/doc/imported/0011-computer-science-as-a-lost-art/metadata.txt +7 -0
  74. data/doc/imported/0011-computer-science-as-a-lost-art/post.html +139 -0
  75. data/doc/imported/0011-computer-science-as-a-lost-art/source.lt3 +104 -0
  76. data/doc/imported/0012-ruby-day-in-turin-italy/metadata.txt +7 -0
  77. data/doc/imported/0012-ruby-day-in-turin-italy/post.html +42 -0
  78. data/doc/imported/0012-ruby-day-in-turin-italy/source.lt3 +24 -0
  79. data/doc/imported/0013-rubyday-was-a-success/metadata.txt +7 -0
  80. data/doc/imported/0013-rubyday-was-a-success/post.html +44 -0
  81. data/doc/imported/0013-rubyday-was-a-success/source.lt3 +27 -0
  82. data/doc/imported/0014-working-on-the-blogging-software/metadata.txt +7 -0
  83. data/doc/imported/0014-working-on-the-blogging-software/post.html +63 -0
  84. data/doc/imported/0014-working-on-the-blogging-software/source.lt3 +41 -0
  85. data/doc/imported/0015-ok-its-not-really-a-lost-art/metadata.txt +7 -0
  86. data/doc/imported/0015-ok-its-not-really-a-lost-art/post.html +172 -0
  87. data/doc/imported/0015-ok-its-not-really-a-lost-art/source.lt3 +134 -0
  88. data/doc/imported/0016-an-in-operator-for-ruby/metadata.txt +7 -0
  89. data/doc/imported/0016-an-in-operator-for-ruby/post.html +155 -0
  90. data/doc/imported/0016-an-in-operator-for-ruby/source.lt3 +106 -0
  91. data/doc/imported/0017-the-forgotten-mathematician/metadata.txt +7 -0
  92. data/doc/imported/0017-the-forgotten-mathematician/post.html +161 -0
  93. data/doc/imported/0017-the-forgotten-mathematician/source.lt3 +119 -0
  94. data/doc/imported/0018-ruby-puns/metadata.txt +7 -0
  95. data/doc/imported/0018-ruby-puns/post.html +46 -0
  96. data/doc/imported/0018-ruby-puns/source.lt3 +28 -0
  97. data/doc/imported/0019-custom-exceptions-via-metaprogramming/metadata.txt +7 -0
  98. data/doc/imported/0019-custom-exceptions-via-metaprogramming/post.html +138 -0
  99. data/doc/imported/0019-custom-exceptions-via-metaprogramming/source.lt3 +101 -0
  100. data/doc/imported/0020-fffff/metadata.txt +7 -0
  101. data/doc/imported/0020-fffff/post.html +24 -0
  102. data/doc/imported/0020-fffff/source.lt3 +12 -0
  103. data/doc/imported/0021-trying-ror-yet-again/metadata.txt +7 -0
  104. data/doc/imported/0021-trying-ror-yet-again/post.html +26 -0
  105. data/doc/imported/0021-trying-ror-yet-again/source.lt3 +12 -0
  106. data/doc/imported/0023-doctor-sleep/metadata.txt +7 -0
  107. data/doc/imported/0023-doctor-sleep/post.html +63 -0
  108. data/doc/imported/0023-doctor-sleep/source.lt3 +44 -0
  109. data/doc/imported/0024-just-a-test/metadata.txt +7 -0
  110. data/doc/imported/0024-just-a-test/post.html +24 -0
  111. data/doc/imported/0024-just-a-test/source.lt3 +12 -0
  112. data/doc/imported/import_summary.txt +98 -0
  113. data/doc/livetext-informal-spec.txt +65 -0
  114. data/doc/myuserdoc/ch-0.lt3 +31 -0
  115. data/doc/myuserdoc/ch-1.lt3 +37 -0
  116. data/doc/myuserdoc/ch-10.lt3 +22 -0
  117. data/doc/myuserdoc/ch-2.lt3 +37 -0
  118. data/doc/myuserdoc/ch-3.lt3 +19 -0
  119. data/doc/myuserdoc/ch-4.lt3 +43 -0
  120. data/doc/myuserdoc/ch-5.lt3 +22 -0
  121. data/doc/myuserdoc/ch-6.lt3 +19 -0
  122. data/doc/myuserdoc/ch-7.lt3 +16 -0
  123. data/doc/myuserdoc/ch-8.lt3 +13 -0
  124. data/doc/myuserdoc/ch-9.lt3 +19 -0
  125. data/doc/myuserdoc/tweak.rb +18 -0
  126. data/doc/myuserdoc/userdoc-toc.txt +88 -0
  127. data/doc/old-posts/0001-elixir-conf-2014.lt3 +24 -0
  128. data/doc/old-posts/0002-programmers-and-word-processing.lt3 +150 -0
  129. data/doc/old-posts/0003-how-to-turn-your-brain-sideways.lt3 +43 -0
  130. data/doc/old-posts/0004-upcoming-lone-star-ruby-conference.lt3 +26 -0
  131. data/doc/old-posts/0005-elixir-conf-2015-announced.lt3 +17 -0
  132. data/doc/old-posts/0006-ruby-for-dinosaurs.lt3 +30 -0
  133. data/doc/old-posts/0007-phoenix-isnt-rails.lt3 +90 -0
  134. data/doc/old-posts/0008-concerning-the-term-monkeypatching.lt3 +105 -0
  135. data/doc/old-posts/0009-announcement-coming-soon.lt3 +20 -0
  136. data/doc/old-posts/0010-immutable-data-ditching-the-wax-tablet.lt3 +142 -0
  137. data/doc/old-posts/0011-computer-science-as-a-lost-art.lt3 +117 -0
  138. data/doc/old-posts/0012-ruby-day-in-turin-italy.lt3 +26 -0
  139. data/doc/old-posts/0013-rubyday-was-a-success.lt3 +28 -0
  140. data/doc/old-posts/0014-working-on-the-blogging-software.lt3 +42 -0
  141. data/doc/old-posts/0015-ok-its-not-really-a-lost-art.lt3 +137 -0
  142. data/doc/old-posts/0016-an-in-operator-for-ruby.lt3 +142 -0
  143. data/doc/old-posts/0017-the-forgotten-mathematician.lt3 +129 -0
  144. data/doc/old-posts/0018-ruby-puns.lt3 +31 -0
  145. data/doc/old-posts/0019-custom-exceptions-via-metaprogramming.lt3 +116 -0
  146. data/doc/old-posts/0021-trying-ror-yet-again.lt3 +35 -0
  147. data/doc/old-posts/0023-doctor-sleep.lt3 +43 -0
  148. data/doc/old-posts/0024-just-a-test.lt3 +12 -0
  149. data/doc/old-posts/0025-trying-another-post.lt3 +12 -0
  150. data/doc/old-repo +1 -0
  151. data/doc/reddit_credentials_template.json +8 -0
  152. data/doc/reddit_integration.md +207 -0
  153. data/doc/user.lt3 +35 -0
  154. data/doc/user_guide_section_1.md +137 -0
  155. data/doc/user_guide_section_10.md +515 -0
  156. data/doc/user_guide_section_11.md +708 -0
  157. data/doc/user_guide_section_2.md +233 -0
  158. data/doc/user_guide_section_3.md +5 -0
  159. data/doc/user_guide_section_4.md +221 -0
  160. data/doc/user_guide_section_5.md +243 -0
  161. data/doc/user_guide_section_6.md +147 -0
  162. data/doc/user_guide_section_7.md +311 -0
  163. data/doc/user_guide_section_8.md +224 -0
  164. data/doc/user_guide_section_9.md +375 -0
  165. data/lib/rouge/lexers/livetext.rb +74 -0
  166. data/lib/scriptorium/api.rb +2373 -0
  167. data/lib/scriptorium/banner_svg.rb +729 -0
  168. data/lib/scriptorium/contract.rb +34 -0
  169. data/lib/scriptorium/exceptions.rb +201 -1
  170. data/lib/scriptorium/helpers.rb +675 -0
  171. data/lib/scriptorium/post.rb +259 -0
  172. data/lib/scriptorium/reddit.rb +83 -0
  173. data/lib/scriptorium/repo.rb +938 -0
  174. data/lib/scriptorium/standard_files.rb +149 -0
  175. data/lib/scriptorium/support/bootstrap/css.txt +5 -0
  176. data/lib/scriptorium/support/bootstrap/js.txt +4 -0
  177. data/lib/scriptorium/support/common_js/clipboard.js +35 -0
  178. data/lib/scriptorium/support/common_js/content-loader.js +187 -0
  179. data/lib/scriptorium/support/common_js/navigation.js +52 -0
  180. data/lib/scriptorium/support/common_js/syntax-highlighting.js +27 -0
  181. data/lib/scriptorium/support/config/reddit.txt +10 -0
  182. data/lib/scriptorium/support/config/reddit_template.txt +17 -0
  183. data/lib/scriptorium/support/config/social.txt +8 -0
  184. data/lib/scriptorium/support/highlight/css.txt +2 -0
  185. data/lib/scriptorium/support/highlight/custom.css +119 -0
  186. data/lib/scriptorium/support/highlight/js.txt +1 -0
  187. data/lib/scriptorium/support/post_index/config.txt +15 -0
  188. data/lib/scriptorium/support/post_index/style.css +55 -0
  189. data/lib/scriptorium/support/templates/index_entry.lt3 +16 -0
  190. data/lib/scriptorium/support/templates/initial_post.lt3 +12 -0
  191. data/lib/scriptorium/support/templates/layout.txt +5 -0
  192. data/lib/scriptorium/support/templates/post.lt3 +104 -0
  193. data/lib/scriptorium/support/theme/footer.lt3 +2 -0
  194. data/lib/scriptorium/support/theme/header.lt3 +4 -0
  195. data/lib/scriptorium/support/theme/left.lt3 +3 -0
  196. data/lib/scriptorium/support/theme/main.lt3 +5 -0
  197. data/lib/scriptorium/support/theme/right.lt3 +3 -0
  198. data/lib/scriptorium/theme.rb +192 -0
  199. data/lib/scriptorium/version.rb +1 -1
  200. data/lib/scriptorium/view.rb +1021 -0
  201. data/lib/scriptorium/widgets/featured_posts.rb +149 -0
  202. data/lib/scriptorium/widgets/links.rb +112 -0
  203. data/lib/scriptorium/widgets/pages.rb +133 -0
  204. data/lib/scriptorium/widgets/widget.rb +133 -0
  205. data/lib/scriptorium.rb +38 -34
  206. data/lib/skeleton.rb +10 -1
  207. data/scriptorium.gemspec +17 -5
  208. data/test/README.md +69 -0
  209. data/test/WEB_INTEGRATION_README.md +196 -0
  210. data/test/all +83 -0
  211. data/test/api_demo.rb +99 -0
  212. data/test/assets/imagenotfound.jpg +0 -0
  213. data/test/assets/images/.DS_Store +0 -0
  214. data/test/assets/images/README.md +27 -0
  215. data/test/assets/images/odd_aspect.png +0 -0
  216. data/test/assets/images/perfect.png +0 -0
  217. data/test/assets/images/small.png +0 -0
  218. data/test/assets/images/tall.png +0 -0
  219. data/test/assets/images/very_tall.png +0 -0
  220. data/test/assets/images/very_wide.png +0 -0
  221. data/test/assets/images/wide.png +0 -0
  222. data/test/assets/testbanner.jpg +0 -0
  223. data/test/banner_svg/simple_helpers.rb +13 -0
  224. data/test/banner_svg/unit.rb +1000 -0
  225. data/test/config/deployment.txt +5 -0
  226. data/test/ed_test.rb +204 -0
  227. data/test/integration/cursor_banner_combinations.rb +193 -0
  228. data/test/integration/cursor_banner_features.rb +374 -0
  229. data/test/integration/integration_test.rb +326 -0
  230. data/test/integration/preview_flow_test.rb +94 -0
  231. data/test/livetext_plugin_test.rb +500 -0
  232. data/test/manual/asset_mgmt.rb +67 -0
  233. data/test/manual/banner-tests/index.html +45 -0
  234. data/test/manual/banner-tests/svg.txt +3 -0
  235. data/test/manual/banner-tests/test01.html +122 -0
  236. data/test/manual/banner-tests/test02.html +122 -0
  237. data/test/manual/banner-tests/test03.html +122 -0
  238. data/test/manual/banner-tests/test04.html +129 -0
  239. data/test/manual/banner-tests/test05.html +129 -0
  240. data/test/manual/banner-tests/test06.html +129 -0
  241. data/test/manual/banner-tests/test07.html +129 -0
  242. data/test/manual/banner-tests/test08.html +123 -0
  243. data/test/manual/banner-tests/test09.html +123 -0
  244. data/test/manual/banner-tests/test10.html +123 -0
  245. data/test/manual/banner-tests/test11.html +123 -0
  246. data/test/manual/banner-tests/test12.html +123 -0
  247. data/test/manual/banner-tests/test13.html +123 -0
  248. data/test/manual/banner-tests/test14.html +123 -0
  249. data/test/manual/banner-tests/test15.html +122 -0
  250. data/test/manual/banner-tests/test16.html +122 -0
  251. data/test/manual/banner-tests/test17.html +122 -0
  252. data/test/manual/banner-tests/test18.html +132 -0
  253. data/test/manual/banner-tests/test19.html +132 -0
  254. data/test/manual/banner-tests/test20.html +132 -0
  255. data/test/manual/banner-tests/test21.html +132 -0
  256. data/test/manual/banner-tests/test22.html +132 -0
  257. data/test/manual/banner-tests/test23.html +132 -0
  258. data/test/manual/banner-tests/test24.html +132 -0
  259. data/test/manual/banner-tests/test25.html +131 -0
  260. data/test/manual/banner_environment.rb +205 -0
  261. data/test/manual/codemirror_demo.html +773 -0
  262. data/test/manual/create_posts_for_web.rb +114 -0
  263. data/test/manual/environment.rb +67 -0
  264. data/test/manual/make_banner.rb +153 -0
  265. data/test/manual/preview_manual_test.rb +129 -0
  266. data/test/manual/sample_banner_config.txt +12 -0
  267. data/test/manual/test_advanced_widgets.rb +73 -0
  268. data/test/manual/test_banner_combinations.rb +120 -0
  269. data/test/manual/test_banner_features.rb +306 -0
  270. data/test/manual/test_banner_integration.rb +115 -0
  271. data/test/manual/test_banner_radial.rb +87 -0
  272. data/test/manual/test_basic_posts.rb +47 -0
  273. data/test/manual/test_layout_widgets.rb +40 -0
  274. data/test/manual/test_pagination.rb +24 -0
  275. data/test/manual/test_random_posts.rb +38 -0
  276. data/test/manual/test_syntax_highlighting.rb +167 -0
  277. data/test/rubytext/rubytext_comprehensive_test.rb +307 -0
  278. data/test/rubytext/rubytext_demo_test.rb +42 -0
  279. data/test/rubytext/rubytext_testing_guide.md +277 -0
  280. data/test/run_automated_tests.rb +45 -0
  281. data/test/staging/.DS_Store +0 -0
  282. data/test/support/preview_utils.rb +88 -0
  283. data/test/syntax_highlighting_test.lt3 +124 -0
  284. data/test/test_gem_assets.rb +48 -0
  285. data/test/test_helpers.rb +240 -0
  286. data/test/tui_editor_integration_test.rb +296 -0
  287. data/test/tui_integration_test.rb +883 -0
  288. data/test/unit/api.rb +1776 -0
  289. data/test/unit/asset_management.rb +219 -0
  290. data/test/unit/backup_test.rb +451 -0
  291. data/test/unit/clipboard_test.rb +60 -0
  292. data/test/unit/contract_test.rb +69 -0
  293. data/test/unit/core.rb +1211 -0
  294. data/test/unit/deploy_config_test.rb +248 -0
  295. data/test/unit/deploy_test.rb +478 -0
  296. data/test/unit/edit_post_test.rb +168 -0
  297. data/test/unit/gem_asset_management.rb +183 -0
  298. data/test/unit/livetext_basic.rb +57 -0
  299. data/test/unit/livetext_compatibility.rb +82 -0
  300. data/test/unit/parse_cmd_test.rb +260 -0
  301. data/test/unit/permalink_copy_test.rb +211 -0
  302. data/test/unit/post.rb +309 -0
  303. data/test/unit/post_index_config_test.rb +258 -0
  304. data/test/unit/post_state_helpers_test.rb +137 -0
  305. data/test/unit/read_commented_file_test.rb +278 -0
  306. data/test/unit/reddit_test.rb +235 -0
  307. data/test/unit/repo.rb +569 -0
  308. data/test/unit/social_test.rb +366 -0
  309. data/test/unit/syntax_highlighting.rb +70 -0
  310. data/test/unit/theme_management_test.rb +91 -0
  311. data/test/unit/view.rb +498 -0
  312. data/test/unit/widgets.rb +669 -0
  313. data/test/web_integration_test.rb +231 -0
  314. data/test/web_test_helper.rb +218 -0
  315. data/test/web_workflow_test.rb +527 -0
  316. data/test/wizard_test.rb +123 -0
  317. data/ui/README.md +67 -0
  318. data/ui/common/lib/ui_common.rb +8 -0
  319. data/ui/rubytext/README.md +191 -0
  320. data/ui/rubytext/bin/scriptorium-rubytext +402 -0
  321. data/ui/rubytext/lib/rubytext_ui.rb +300 -0
  322. data/ui/tui/bin/scriptorium +1890 -0
  323. data/ui/tui/test/tui_test.rb +23 -0
  324. data/ui/web/app/app.rb +2600 -0
  325. data/ui/web/app/assets/livetext_mode.js +244 -0
  326. data/ui/web/app/error_helpers.rb +150 -0
  327. data/ui/web/app/views/advanced_config.erb +196 -0
  328. data/ui/web/app/views/asset_management.erb +645 -0
  329. data/ui/web/app/views/backup_management.erb +238 -0
  330. data/ui/web/app/views/banner_config.erb +200 -0
  331. data/ui/web/app/views/config_widget.erb +232 -0
  332. data/ui/web/app/views/configure_view.erb +401 -0
  333. data/ui/web/app/views/dashboard.erb +154 -0
  334. data/ui/web/app/views/deploy_config.erb +149 -0
  335. data/ui/web/app/views/edit_pages.erb +363 -0
  336. data/ui/web/app/views/edit_post.erb +175 -0
  337. data/ui/web/app/views/edit_theme.erb +73 -0
  338. data/ui/web/app/views/edit_theme_file.erb +74 -0
  339. data/ui/web/app/views/error_page.erb +29 -0
  340. data/ui/web/app/views/header_config.erb +155 -0
  341. data/ui/web/app/views/layout_config.erb +147 -0
  342. data/ui/web/app/views/navbar_config.erb +411 -0
  343. data/ui/web/app/views/theme_management.erb +130 -0
  344. data/ui/web/app/views/view_dashboard.erb +779 -0
  345. data/ui/web/app/views/widgets.erb +249 -0
  346. data/ui/web/bin/scriptorium-web +164 -0
  347. data/ui/web/test/web_basic_test.rb +38 -0
  348. data/ui/web/test_navbar.txt +7 -0
  349. data/ui/web/tmp/timing.log +17 -0
  350. data/ui/web/tmp/web_server.log +0 -0
  351. metadata +434 -8
  352. data/lib/scriptorium/engine.rb +0 -22
  353. data/test/engine/unit.rb +0 -44
data/test/unit/core.rb ADDED
@@ -0,0 +1,1211 @@
1
+ # This test file primarily tests the core helper methods from lib/scriptorium/helpers.rb
2
+ # including file I/O operations, directory creation, system commands, and existence validation
3
+ # test/unit/core.rb
4
+
5
+ require 'minitest/autorun'
6
+ require_relative '../../lib/scriptorium'
7
+ require_relative '../test_helpers'
8
+ require 'fileutils'
9
+ require 'tempfile'
10
+
11
+ class TestReadWrite < Minitest::Test
12
+ include Scriptorium::Helpers
13
+ include Scriptorium::Exceptions
14
+ include TestHelpers
15
+
16
+ def setup
17
+ @test_dir = "test/core_test_files"
18
+ FileUtils.mkdir_p(@test_dir)
19
+ Scriptorium::Repo.testing = true # Ensure testing mode is enabled
20
+ end
21
+
22
+ def teardown
23
+ FileUtils.rm_rf(@test_dir) if File.exist?(@test_dir)
24
+ end
25
+
26
+ # ========================================
27
+ # need() tests
28
+ # ========================================
29
+
30
+ def test_001_need_file_basic_functionality
31
+ file_path = "#{@test_dir}/test_file.txt"
32
+ write_file(file_path, "test content")
33
+
34
+ result = need(:file, file_path)
35
+ assert_equal file_path, result
36
+ end
37
+
38
+ def test_002_need_dir_basic_functionality
39
+ dir_path = "#{@test_dir}/test_dir"
40
+ Dir.mkdir(dir_path)
41
+
42
+ result = need(:dir, dir_path)
43
+ assert_equal dir_path, result
44
+ end
45
+
46
+ def test_003_need_with_custom_exception
47
+ file_path = "#{@test_dir}/nonexistent.txt"
48
+
49
+ # Create a simple custom exception for testing
50
+ custom_error_class = Class.new(StandardError) do
51
+ def self.call(path)
52
+ new("Custom error for: #{path}")
53
+ end
54
+ end
55
+
56
+ begin
57
+ need(:file, file_path, custom_error_class)
58
+ flunk "Expected need() to raise an error"
59
+ rescue custom_error_class => e
60
+ assert_match /Custom error for: #{file_path}/, e.message
61
+ end
62
+ end
63
+
64
+ def test_004_need_nil_path_raises_error
65
+ assert_raises(RequirePathNil) do
66
+ need(:file, nil)
67
+ end
68
+ end
69
+
70
+ def test_005_need_empty_path_raises_error
71
+ assert_raises(RequirePathEmpty) do
72
+ need(:file, "")
73
+ end
74
+ end
75
+
76
+ def test_006_need_whitespace_path_raises_error
77
+ assert_raises(RequirePathEmpty) do
78
+ need(:file, " ")
79
+ end
80
+ end
81
+
82
+ def test_007_need_invalid_type_raises_error
83
+ assert_raises(InvalidType) do
84
+ need(:invalid, "some/path")
85
+ end
86
+ end
87
+
88
+ def test_008_need_file_nonexistent_raises_error
89
+ file_path = "#{@test_dir}/nonexistent.txt"
90
+
91
+ assert_raises(RequiredFileNotFound) do
92
+ need(:file, file_path)
93
+ end
94
+ end
95
+
96
+ def test_009_need_dir_nonexistent_raises_error
97
+ dir_path = "#{@test_dir}/nonexistent_dir"
98
+
99
+ assert_raises(RequiredFileNotFound) do
100
+ need(:dir, dir_path)
101
+ end
102
+ end
103
+
104
+ # ========================================
105
+ # system! tests
106
+ # ========================================
107
+
108
+ def test_010_system_basic_functionality
109
+ # Test a simple command that should succeed
110
+ result = system!("echo 'test' > /dev/null", "basic echo test")
111
+ assert result
112
+ end
113
+
114
+ def test_011_system_with_description
115
+ # Test that description is included in error message
116
+ system!("false", "testing failure")
117
+ flunk "Expected system! to raise an error"
118
+ rescue CommandFailedWithDesc => e
119
+ assert_match /testing failure/, e.message
120
+ assert_match /Command failed/, e.message
121
+ end
122
+
123
+ def test_012_system_nil_command_raises_error
124
+ assert_raises(CommandNil) do
125
+ system!(nil)
126
+ end
127
+ end
128
+
129
+ def test_013_system_empty_command_raises_error
130
+ assert_raises(CommandEmpty) do
131
+ system!("")
132
+ end
133
+ end
134
+
135
+ def test_014_system_whitespace_command_raises_error
136
+ assert_raises(CommandEmpty) do
137
+ system!(" ")
138
+ end
139
+ end
140
+
141
+ def test_015_system_failing_command_raises_error
142
+ assert_raises(CommandFailedWithDesc) do
143
+ system!("false")
144
+ end
145
+ end
146
+
147
+ def test_016_system_nonexistent_command_raises_error
148
+ assert_raises(CommandFailedWithDesc) do
149
+ system!("nonexistent_command_that_should_fail")
150
+ end
151
+ end
152
+
153
+ # ========================================
154
+ # make_dir tests
155
+ # ========================================
156
+
157
+ def test_017_make_dir_basic_functionality
158
+ dir_path = "#{@test_dir}/new_dir"
159
+
160
+ make_dir(dir_path)
161
+
162
+ assert Dir.exist?(dir_path)
163
+ end
164
+
165
+ def test_018_make_dir_with_parents
166
+ dir_path = "#{@test_dir}/nested/deep/path"
167
+
168
+ make_dir(dir_path, create_parents: true)
169
+
170
+ assert Dir.exist?(dir_path)
171
+ assert Dir.exist?("#{@test_dir}/nested")
172
+ assert Dir.exist?("#{@test_dir}/nested/deep")
173
+ end
174
+
175
+ def test_019_make_dir_existing_directory
176
+ dir_path = "#{@test_dir}/existing_dir"
177
+ Dir.mkdir(dir_path)
178
+
179
+ # Should not raise an error
180
+ make_dir(dir_path)
181
+
182
+ assert Dir.exist?(dir_path)
183
+ end
184
+
185
+ def test_020_make_dir_nil_path_raises_error
186
+ assert_raises(DirectoryPathNil) do
187
+ make_dir(nil)
188
+ end
189
+ end
190
+
191
+ def test_021_make_dir_empty_path_raises_error
192
+ assert_raises(DirectoryPathEmpty) do
193
+ make_dir("")
194
+ end
195
+ end
196
+
197
+ def test_022_make_dir_whitespace_path_raises_error
198
+ assert_raises(DirectoryPathEmpty) do
199
+ make_dir(" ")
200
+ end
201
+ end
202
+
203
+ def test_023_make_dir_permission_denied_simulation
204
+ # Create a read-only directory
205
+ read_only_dir = "#{@test_dir}/readonly"
206
+ Dir.mkdir(read_only_dir)
207
+ FileUtils.chmod(0444, read_only_dir)
208
+
209
+ file_path = "#{read_only_dir}/test_dir"
210
+
211
+ # This should raise a permission denied error
212
+ assert_raises(DirectoryPermissionDenied) do
213
+ make_dir(file_path)
214
+ end
215
+
216
+ # Clean up
217
+ FileUtils.chmod(0755, read_only_dir)
218
+ end
219
+
220
+ # ========================================
221
+ # write_file tests
222
+ # ========================================
223
+
224
+ def test_024_write_file_basic_functionality
225
+ file_path = "#{@test_dir}/basic.txt"
226
+ content = ["line1", "line2", "line3"]
227
+
228
+ write_file(file_path, content.join("\n"))
229
+
230
+ assert File.exist?(file_path)
231
+ assert_equal content.join("\n") + "\n", read_file(file_path)
232
+ end
233
+
234
+ def test_025_write_file_creates_parent_directories
235
+ file_path = "#{@test_dir}/nested/deep/path/file.txt"
236
+ content = ["test content"]
237
+
238
+ write_file(file_path, content.join("\n"))
239
+
240
+ assert File.exist?(file_path)
241
+ assert File.exist?("#{@test_dir}/nested/deep/path")
242
+ end
243
+
244
+ def test_026_write_file_empty_content
245
+ file_path = "#{@test_dir}/empty.txt"
246
+
247
+ write_file(file_path, "")
248
+
249
+ assert File.exist?(file_path)
250
+ assert_equal "\n", read_file(file_path)
251
+ end
252
+
253
+ def test_027_write_file_single_line
254
+ file_path = "#{@test_dir}/single.txt"
255
+ content = "single line content"
256
+
257
+ write_file(file_path, content)
258
+
259
+ assert File.exist?(file_path)
260
+ assert_equal content + "\n", read_file(file_path)
261
+ end
262
+
263
+ def test_028_write_file_multiple_lines
264
+ file_path = "#{@test_dir}/multi.txt"
265
+ content = ["first line", "second line", "third line"]
266
+
267
+ write_file(file_path, content.join("\n"))
268
+
269
+ assert File.exist?(file_path)
270
+ expected = content.join("\n") + "\n"
271
+ assert_equal expected, read_file(file_path)
272
+ end
273
+
274
+ def test_029_write_file_nil_path_raises_error
275
+ assert_raises(FilePathNil) do
276
+ write_file(nil, "content")
277
+ end
278
+ end
279
+
280
+ def test_030_write_file_empty_path_raises_error
281
+ assert_raises(FilePathEmpty) do
282
+ write_file("", "content")
283
+ end
284
+ end
285
+
286
+ def test_031_write_file_whitespace_path_raises_error
287
+ assert_raises(FilePathEmpty) do
288
+ write_file(" ", "content")
289
+ end
290
+ end
291
+
292
+ def test_032_write_file_permission_denied_simulation
293
+ # Create a read-only directory
294
+ read_only_dir = "#{@test_dir}/readonly"
295
+ FileUtils.mkdir_p(read_only_dir)
296
+ FileUtils.chmod(0444, read_only_dir)
297
+
298
+ file_path = "#{read_only_dir}/test.txt"
299
+
300
+ # This should raise a permission denied error
301
+ assert_raises(FilePermissionDenied) do
302
+ write_file(file_path, "content")
303
+ end
304
+
305
+ # Clean up
306
+ FileUtils.chmod(0755, read_only_dir)
307
+ end
308
+
309
+ def test_033_write_file_empty_true_creates_file
310
+ file_path = "#{@test_dir}/empty_true.txt"
311
+
312
+ # Should create file with empty: true even for empty content
313
+ write_file(file_path, "", empty: true)
314
+
315
+ assert File.exist?(file_path)
316
+ assert_equal "", read_file(file_path) # No newline added
317
+ end
318
+
319
+ def test_034_write_file_empty_true_nil_content
320
+ file_path = "#{@test_dir}/empty_true_nil.txt"
321
+
322
+ # Should create file with empty: true even for nil content
323
+ write_file(file_path, nil, empty: true)
324
+
325
+ assert File.exist?(file_path)
326
+ assert_equal "", read_file(file_path) # No newline added
327
+ end
328
+
329
+ def test_035_write_file_empty_true_whitespace_content
330
+ file_path = "#{@test_dir}/empty_true_whitespace.txt"
331
+
332
+ # Should create file with empty: true for whitespace-only content
333
+ write_file(file_path, " ", empty: true)
334
+
335
+ assert File.exist?(file_path)
336
+ assert_equal "", read_file(file_path) # No newline added
337
+ end
338
+
339
+ def test_036_write_file_empty_true_existing_file
340
+ file_path = "#{@test_dir}/empty_true_existing.txt"
341
+
342
+ # Create file with content first
343
+ write_file(file_path, "original content")
344
+ original_size = File.size(file_path)
345
+
346
+ # Now write empty content with empty: true
347
+ write_file(file_path, "", empty: true)
348
+
349
+ assert File.exist?(file_path)
350
+ assert_equal "original content\n", read_file(file_path) # File should preserve original content
351
+ assert_equal original_size, File.size(file_path) # File size should be unchanged
352
+ end
353
+
354
+ def test_037_write_file_empty_true_with_content
355
+ file_path = "#{@test_dir}/empty_true_with_content.txt"
356
+ content = "some content"
357
+
358
+ # empty: true should be ignored when there's actual content
359
+ write_file(file_path, content, empty: true)
360
+
361
+ assert File.exist?(file_path)
362
+ assert_equal content + "\n", read_file(file_path) # Normal behavior
363
+ end
364
+
365
+ # ========================================
366
+ # write_file! tests
367
+ # ========================================
368
+
369
+ def test_038_write_file_bang_basic_functionality
370
+ file_path = "#{@test_dir}/bang_basic.txt"
371
+
372
+ write_file!(file_path, "line1", "line2", "line3")
373
+
374
+ assert File.exist?(file_path)
375
+ assert_equal "line1\nline2\nline3\n", read_file(file_path)
376
+ end
377
+
378
+ def test_039_write_file_bang_empty_true_no_lines
379
+ file_path = "#{@test_dir}/bang_empty_true.txt"
380
+
381
+ # Should create file with empty: true when no lines provided
382
+ write_file!(file_path, empty: true)
383
+
384
+ assert File.exist?(file_path)
385
+ assert_equal "", read_file(file_path) # No newline added
386
+ end
387
+
388
+ def test_040_write_file_bang_empty_true_nil_lines
389
+ file_path = "#{@test_dir}/bang_empty_true_nil.txt"
390
+
391
+ # Should create file with empty: true when all lines are nil
392
+ write_file!(file_path, nil, nil, nil, empty: true)
393
+
394
+ assert File.exist?(file_path)
395
+ assert_equal "", read_file(file_path) # No newline added
396
+ end
397
+
398
+ def test_041_write_file_bang_empty_true_whitespace_lines
399
+ file_path = "#{@test_dir}/bang_empty_true_whitespace.txt"
400
+
401
+ # Should create file with empty: true when all lines are whitespace
402
+ write_file!(file_path, "", " ", "", empty: true)
403
+
404
+ assert File.exist?(file_path)
405
+ assert_equal "", read_file(file_path) # No newline added
406
+ end
407
+
408
+ def test_042_write_file_bang_empty_true_with_content
409
+ file_path = "#{@test_dir}/bang_empty_true_with_content.txt"
410
+
411
+ # empty: true should be ignored when there's actual content
412
+ write_file!(file_path, "line1", "", "line3", empty: true)
413
+
414
+ assert File.exist?(file_path)
415
+ assert_equal "line1\n\nline3\n", read_file(file_path) # Normal behavior
416
+ end
417
+
418
+ # ========================================
419
+ # read_file tests
420
+ # ========================================
421
+
422
+ def test_033_read_file_basic_functionality
423
+ file_path = "#{@test_dir}/read_test.txt"
424
+ content = "line1\nline2\nline3"
425
+ write_file(file_path, content)
426
+
427
+ result = read_file(file_path)
428
+
429
+ assert_equal content + "\n", result
430
+ end
431
+
432
+ def test_034_read_file_as_lines
433
+ file_path = "#{@test_dir}/lines_test.txt"
434
+ content = "line1\nline2\nline3"
435
+ write_file(file_path, content)
436
+
437
+ result = read_file(file_path, lines: true)
438
+
439
+ assert_equal ["line1\n", "line2\n", "line3\n"], result
440
+ end
441
+
442
+ def test_035_read_file_as_lines_with_chomp
443
+ file_path = "#{@test_dir}/chomp_test.txt"
444
+ content = "line1\nline2\nline3\n"
445
+ write_file(file_path, content)
446
+
447
+ result = read_file(file_path, lines: true, chomp: true)
448
+
449
+ assert_equal ["line1", "line2", "line3"], result
450
+ end
451
+
452
+ def test_036_read_file_as_lines_without_chomp
453
+ file_path = "#{@test_dir}/no_chomp_test.txt"
454
+ content = "line1\nline2\nline3\n"
455
+ write_file(file_path, content)
456
+
457
+ result = read_file(file_path, lines: true, chomp: false)
458
+
459
+ assert_equal ["line1\n", "line2\n", "line3\n"], result
460
+ end
461
+
462
+ def test_037_read_file_missing_file_with_fallback
463
+ file_path = "#{@test_dir}/nonexistent.txt"
464
+ fallback = "fallback content"
465
+
466
+ result = read_file(file_path, missing_fallback: fallback)
467
+
468
+ assert_equal fallback, result
469
+ end
470
+
471
+ def test_038_read_file_missing_file_without_fallback_raises_error
472
+ file_path = "#{@test_dir}/nonexistent.txt"
473
+
474
+ assert_raises(ReadFileNotFound) do
475
+ read_file(file_path)
476
+ end
477
+ end
478
+
479
+ def test_039_read_file_nil_path_raises_error
480
+ assert_raises(ReadFilePathNil) do
481
+ read_file(nil)
482
+ end
483
+ end
484
+
485
+ def test_040_read_file_empty_path_raises_error
486
+ assert_raises(ReadFilePathEmpty) do
487
+ read_file("")
488
+ end
489
+ end
490
+
491
+ def test_041_read_file_whitespace_path_raises_error
492
+ assert_raises(ReadFilePathEmpty) do
493
+ read_file(" ")
494
+ end
495
+ end
496
+
497
+ def test_042_read_file_permission_denied_simulation
498
+ # Create a file and make it unreadable
499
+ file_path = "#{@test_dir}/unreadable.txt"
500
+ write_file(file_path, "content")
501
+ FileUtils.chmod(0000, file_path)
502
+
503
+ assert_raises(ReadFilePermissionDenied) do
504
+ read_file(file_path)
505
+ end
506
+
507
+ # Clean up
508
+ FileUtils.chmod(0644, file_path)
509
+ end
510
+
511
+ def test_043_read_file_empty_file
512
+ file_path = "#{@test_dir}/empty.txt"
513
+ write_file(file_path, "")
514
+
515
+ result = read_file(file_path)
516
+
517
+ assert_equal "\n", result
518
+ end
519
+
520
+ def test_044_read_file_empty_file_as_lines
521
+ file_path = "#{@test_dir}/empty_lines.txt"
522
+ write_file(file_path, "")
523
+
524
+ result = read_file(file_path, lines: true)
525
+
526
+ assert_equal ["\n"], result
527
+ end
528
+
529
+ def test_045_read_file_single_line
530
+ file_path = "#{@test_dir}/single_line.txt"
531
+ content = "single line"
532
+ write_file(file_path, content)
533
+
534
+ result = read_file(file_path)
535
+
536
+ assert_equal content + "\n", result
537
+ end
538
+
539
+ def test_046_read_file_single_line_as_lines
540
+ file_path = "#{@test_dir}/single_line.txt"
541
+ content = "single line"
542
+ write_file(file_path, content)
543
+
544
+ result = read_file(file_path, lines: true)
545
+
546
+ assert_equal [content + "\n"], result
547
+ end
548
+
549
+ def test_047_read_file_with_trailing_newline
550
+ file_path = "#{@test_dir}/trailing_newline.txt"
551
+ content = "line1\nline2\n"
552
+ write_file(file_path, content)
553
+
554
+ result = read_file(file_path)
555
+
556
+ assert_equal content, result
557
+ end
558
+
559
+ def test_048_read_file_with_trailing_newline_as_lines
560
+ file_path = "#{@test_dir}/trailing_newline.txt"
561
+ content = "line1\nline2\n"
562
+ write_file(file_path, content)
563
+
564
+ result = read_file(file_path, lines: true)
565
+
566
+ assert_equal ["line1\n", "line2\n"], result
567
+ end
568
+
569
+ def test_049_read_file_with_trailing_newline_as_lines_no_chomp
570
+ file_path = "#{@test_dir}/trailing_newline.txt"
571
+ content = "line1\nline2\n"
572
+ write_file(file_path, content)
573
+
574
+ result = read_file(file_path, lines: true, chomp: false)
575
+
576
+ assert_equal ["line1\n", "line2\n"], result
577
+ end
578
+
579
+ # ========================================
580
+ # Integration tests
581
+ # ========================================
582
+
583
+ def test_050_write_then_read_cycle
584
+ file_path = "#{@test_dir}/cycle.txt"
585
+ original_content = ["first line", "second line", "third line"]
586
+
587
+ # Write content
588
+ write_file(file_path, original_content.join("\n"))
589
+
590
+ # Read it back
591
+ read_content = read_file(file_path)
592
+
593
+ # Verify
594
+ expected_content = original_content.join("\n") + "\n"
595
+ assert_equal expected_content, read_content
596
+ end
597
+
598
+ def test_051_write_then_read_as_lines_cycle
599
+ file_path = "#{@test_dir}/cycle_lines.txt"
600
+ original_content = ["first line", "second line", "third line"]
601
+
602
+ # Write content
603
+ write_file(file_path, original_content.join("\n"))
604
+
605
+ # Read it back as lines
606
+ read_content = read_file(file_path, lines: true)
607
+
608
+ # Verify
609
+ assert_equal ["first line\n", "second line\n", "third line\n"], read_content
610
+ end
611
+
612
+ def test_052_multiple_write_operations
613
+ file_path = "#{@test_dir}/multiple.txt"
614
+
615
+ # First write
616
+ write_file(file_path, "first content")
617
+ assert_equal "first content\n", read_file(file_path)
618
+
619
+ # Second write (should overwrite)
620
+ write_file(file_path, "second content")
621
+ assert_equal "second content\n", read_file(file_path)
622
+ end
623
+
624
+ def test_053_write_file_with_special_characters
625
+ file_path = "#{@test_dir}/special.txt"
626
+ content = ["line with spaces", "line\twith\ttabs", "line with # comments"]
627
+
628
+ write_file(file_path, content.join("\n"))
629
+
630
+ result = read_file(file_path, lines: true)
631
+ assert_equal ["line with spaces\n", "line\twith\ttabs\n", "line with # comments\n"], result
632
+ end
633
+
634
+ def test_054_read_file_with_unicode_content
635
+ file_path = "#{@test_dir}/unicode.txt"
636
+ content = ["café", "naïve", "résumé", "über"]
637
+
638
+ write_file(file_path, content.join("\n"))
639
+
640
+ result = read_file(file_path, lines: true)
641
+ assert_equal ["café\n", "naïve\n", "résumé\n", "über\n"], result
642
+ end
643
+
644
+ # ========================================
645
+ # Edge case tests
646
+ # ========================================
647
+
648
+ def test_055_write_file_with_nil_lines
649
+ file_path = "#{@test_dir}/nil_lines.txt"
650
+
651
+ write_file!(file_path, nil, "content", nil)
652
+
653
+ result = read_file(file_path, lines: true)
654
+ assert_equal ["\n", "content\n", "\n"], result
655
+ end
656
+
657
+ def test_056_write_file_with_empty_strings
658
+ file_path = "#{@test_dir}/empty_strings.txt"
659
+
660
+ write_file!(file_path, "", "content", "")
661
+
662
+ result = read_file(file_path, lines: true)
663
+ assert_equal ["\n", "content\n", "\n"], result
664
+ end
665
+
666
+ def test_057_read_file_missing_fallback_with_lines_option
667
+ file_path = "#{@test_dir}/nonexistent.txt"
668
+ fallback = ["fallback", "lines"]
669
+
670
+ result = read_file(file_path, lines: true, missing_fallback: fallback)
671
+
672
+ assert_equal fallback, result
673
+ end
674
+
675
+ def test_058_read_file_missing_fallback_with_chomp_option
676
+ file_path = "#{@test_dir}/nonexistent.txt"
677
+ fallback = ["fallback", "lines"]
678
+
679
+ result = read_file(file_path, lines: true, chomp: true, missing_fallback: fallback)
680
+
681
+ assert_equal fallback, result
682
+ end
683
+
684
+ def test_059_write_file_very_long_line
685
+ file_path = "#{@test_dir}/long_line.txt"
686
+ long_line = "x" * 10000
687
+
688
+ write_file(file_path, long_line)
689
+
690
+ result = read_file(file_path)
691
+ assert_equal long_line + "\n", result
692
+ end
693
+
694
+ def test_060_write_file_many_lines
695
+ file_path = "#{@test_dir}/many_lines.txt"
696
+ many_lines = (1..1000).map { |i| "line #{i}" }
697
+
698
+ write_file(file_path, many_lines.join("\n"))
699
+
700
+ result = read_file(file_path, lines: true)
701
+ expected_lines = many_lines.map { |line| line + "\n" }
702
+ assert_equal expected_lines, result
703
+ end
704
+
705
+ # ========================================
706
+ # Moved helper method tests (3 tests)
707
+ # ========================================
708
+
709
+ def test_061_change_config
710
+ cfg_file = "#{@test_dir}/myconfig.txt"
711
+ File.open(cfg_file, "w") do |f|
712
+ f.puts <<~EOS
713
+ alpha foo # nothing much
714
+ beta bar # meh again
715
+ gamma baz # whatever
716
+ EOS
717
+ end
718
+ change_config(cfg_file, "beta", "new-value")
719
+ lines = File.readlines(cfg_file).map(&:chomp)
720
+ assert lines[0] == "alpha foo # nothing much", "Expected alpha text"
721
+ assert lines[1] == "beta new-value # meh again", "Expected beta text"
722
+ assert lines[2] == "gamma baz # whatever", "Expected gamma text"
723
+ end
724
+
725
+ def test_062_read_commented_file
726
+ # Setup: Create a temporary test config file
727
+ test_file = "#{@test_dir}/test_config.txt"
728
+ File.open(test_file, "w") do |f|
729
+ f.puts "# This is a comment"
730
+ f.puts ""
731
+ f.puts "header 20% # This is a header line with a comment"
732
+ f.puts "footer # This is a footer line with another comment"
733
+ f.puts "# Another full-line comment"
734
+ f.puts "main # Main content area"
735
+ end
736
+
737
+ # Expected result: an array of non-comment lines, with comments stripped
738
+ expected_result = ["header 20%", "footer", "main"]
739
+
740
+ # Run the method
741
+ result = read_commented_file(test_file)
742
+
743
+ # Assert the result matches the expected array
744
+ assert_equal expected_result, result
745
+ end
746
+
747
+ def test_063_get_asset_path
748
+ name = "back.png"
749
+ result = get_asset_path(name)
750
+ assert result == "assets/icons/ui/#{name}", "Expected #{name} to be found recursively (got #{result})"
751
+ assert_raises(AssetNotFound) { get_asset_path("nonexistent.png") }
752
+ end
753
+
754
+ # ========================================
755
+ # New helper method tests
756
+ # ========================================
757
+
758
+ def test_064_slugify_basic
759
+ result = slugify(42, "My Test Post")
760
+ assert_equal "0042-my-test-post", result
761
+ end
762
+
763
+ def test_065_slugify_with_special_characters
764
+ result = slugify(1, "Post with & < > \" ' characters!")
765
+ assert_equal "0001-post-with-characters", result
766
+ end
767
+
768
+ def test_066_slugify_with_underscores_and_hyphens
769
+ result = slugify(123, "Post with_underscores-and-hyphens")
770
+ assert_equal "0123-post-with-underscores-and-hyphens", result
771
+ end
772
+
773
+ def test_067_slugify_with_multiple_spaces
774
+ result = slugify(5, "Post with multiple spaces")
775
+ assert_equal "0005-post-with-multiple-spaces", result
776
+ end
777
+
778
+ def test_068_slugify_with_leading_trailing_hyphens
779
+ result = slugify(99, "-Post with leading/trailing hyphens-")
780
+ assert_equal "0099-post-with-leadingtrailing-hyphens", result
781
+ end
782
+
783
+ # ========================================
784
+ # clean_slugify tests
785
+ # ========================================
786
+
787
+ def test_083_clean_slugify_basic
788
+ result = clean_slugify("My Test Post")
789
+ assert_equal "my-test-post", result
790
+ end
791
+
792
+ def test_084_clean_slugify_with_special_characters
793
+ result = clean_slugify("Post with & < > \" ' characters!")
794
+ assert_equal "post-with-characters", result
795
+ end
796
+
797
+ def test_085_clean_slugify_with_underscores_and_hyphens
798
+ result = clean_slugify("Post with_underscores-and-hyphens")
799
+ assert_equal "post-with-underscores-and-hyphens", result
800
+ end
801
+
802
+ def test_086_clean_slugify_with_multiple_spaces
803
+ result = clean_slugify("Post with multiple spaces")
804
+ assert_equal "post-with-multiple-spaces", result
805
+ end
806
+
807
+ def test_087_clean_slugify_with_leading_trailing_hyphens
808
+ result = clean_slugify("-Post with leading/trailing hyphens-")
809
+ assert_equal "post-with-leadingtrailing-hyphens", result
810
+ end
811
+
812
+ def test_088_clean_slugify_empty_string
813
+ result = clean_slugify("")
814
+ assert_equal "", result
815
+ end
816
+
817
+ def test_089_clean_slugify_nil_string
818
+ result = clean_slugify(nil)
819
+ assert_equal "title-is-missing", result
820
+ end
821
+
822
+ def test_069_escape_html_basic
823
+ result = escape_html("<script>alert('test')</script>")
824
+ assert_equal "&lt;script&gt;alert(&#39;test&#39;)&lt;/script&gt;", result
825
+ end
826
+
827
+ def test_070_escape_html_with_quotes
828
+ result = escape_html('He said "Hello" and she said \'Hi\'')
829
+ assert_equal "He said &quot;Hello&quot; and she said &#39;Hi&#39;", result
830
+ end
831
+
832
+ def test_071_escape_html_with_ampersand
833
+ result = escape_html("Fish & Chips")
834
+ assert_equal "Fish &amp; Chips", result
835
+ end
836
+
837
+ def test_072_escape_html_with_mixed_content
838
+ result = escape_html('<a href="test">Link & Text</a>')
839
+ assert_equal "&lt;a href=&quot;test&quot;&gt;Link &amp; Text&lt;/a&gt;", result
840
+ end
841
+
842
+ def test_073_getvars_basic
843
+ config_file = "#{@test_dir}/config.txt"
844
+ write_file(config_file, <<~EOS)
845
+ title My Blog
846
+ subtitle A test blog
847
+ theme standard
848
+ EOS
849
+
850
+ result = getvars(config_file)
851
+ assert_equal "My Blog", result[:title]
852
+ assert_equal "A test blog", result[:subtitle]
853
+ assert_equal "standard", result[:theme]
854
+ end
855
+
856
+ def test_074_getvars_with_comments
857
+ config_file = "#{@test_dir}/config_with_comments.txt"
858
+ write_file(config_file, <<~EOS)
859
+ # This is a comment
860
+ title My Blog # Another comment
861
+ subtitle A test blog
862
+ # Empty line below
863
+
864
+ theme standard
865
+ EOS
866
+
867
+ result = getvars(config_file)
868
+ assert_equal "My Blog", result[:title]
869
+ assert_equal "A test blog", result[:subtitle]
870
+ assert_equal "standard", result[:theme]
871
+ assert_equal 3, result.size # Only valid key-value pairs
872
+ end
873
+
874
+ def test_075_getvars_with_empty_values
875
+ config_file = "#{@test_dir}/config_empty.txt"
876
+ write_file(config_file, <<~EOS)
877
+ title My Blog
878
+ subtitle
879
+ theme standard
880
+ EOS
881
+
882
+ result = getvars(config_file)
883
+ assert_equal "My Blog", result[:title]
884
+ assert_equal "", result[:subtitle] # When no value after key, split returns empty string
885
+ assert_equal "standard", result[:theme]
886
+ end
887
+
888
+ def test_076_d4_basic
889
+ assert_equal "0001", d4(1)
890
+ assert_equal "0042", d4(42)
891
+ assert_equal "0123", d4(123)
892
+ assert_equal "9999", d4(9999)
893
+ end
894
+
895
+ def test_077_d4_with_large_numbers
896
+ assert_equal "10000", d4(10000)
897
+ assert_equal "12345", d4(12345)
898
+ end
899
+
900
+ def test_078_substitute_with_hash
901
+ vars = {title: "My Title", content: "My Content"}
902
+ template = "Title: %{title}\nContent: %{content}"
903
+
904
+ result = substitute(vars, template)
905
+ assert_equal "Title: My Title\nContent: My Content", result
906
+ end
907
+
908
+ def test_079_substitute_with_object
909
+ # Create a mock object with vars method
910
+ mock_obj = Object.new
911
+ def mock_obj.vars
912
+ {title: "My Title", content: "My Content"}
913
+ end
914
+
915
+ template = "Title: %{title}\nContent: %{content}"
916
+ result = substitute(mock_obj, template)
917
+ assert_equal "Title: My Title\nContent: My Content", result
918
+ end
919
+
920
+ def test_080_substitute_with_missing_vars
921
+ vars = {title: "My Title"}
922
+ template = "Title: %{title}\nContent: %{content}"
923
+
924
+ # The substitute method raises KeyError for missing keys
925
+ assert_raises(KeyError) do
926
+ substitute(vars, template)
927
+ end
928
+ end
929
+
930
+ def test_081_make_tree_basic
931
+ tree_text = <<~EOS
932
+ .
933
+ ├── file1.txt
934
+ ├── dir1/
935
+ │ ├── file2.txt
936
+ │ └── subdir/
937
+ │ └── file3.txt
938
+ └── file4.txt
939
+ EOS
940
+
941
+ make_tree("#{@test_dir}/test_tree", tree_text)
942
+
943
+ assert File.exist?("#{@test_dir}/test_tree/file1.txt")
944
+ assert File.exist?("#{@test_dir}/test_tree/dir1/file2.txt")
945
+ assert File.exist?("#{@test_dir}/test_tree/dir1/subdir/file3.txt")
946
+ assert File.exist?("#{@test_dir}/test_tree/file4.txt")
947
+ end
948
+
949
+ def test_082_make_tree_with_comments
950
+ tree_text = <<~EOS
951
+ .
952
+ ├── file1.txt # This is a comment
953
+ ├── dir1/ # Another comment
954
+ │ └── file2.txt
955
+ └── file3.txt
956
+ EOS
957
+
958
+ make_tree("#{@test_dir}/test_tree_with_comments", tree_text)
959
+
960
+ assert File.exist?("#{@test_dir}/test_tree_with_comments/file1.txt")
961
+ assert File.exist?("#{@test_dir}/test_tree_with_comments/dir1/file2.txt")
962
+ assert File.exist?("#{@test_dir}/test_tree_with_comments/file3.txt")
963
+ end
964
+
965
+ def test_083_make_tree_simplified
966
+ tree_text = <<~EOS
967
+ .
968
+ ├── config/
969
+ ├── views/
970
+ ├── drafts/
971
+ ├── posts/
972
+ ├── assets/
973
+ │ └── library/
974
+ └── themes/
975
+ EOS
976
+
977
+ target_dir = "#{@test_dir}/simplified_tree"
978
+ make_tree(target_dir, tree_text)
979
+
980
+ assert File.exist?("#{target_dir}/config")
981
+ assert File.exist?("#{target_dir}/views")
982
+ assert File.exist?("#{target_dir}/drafts")
983
+ assert File.exist?("#{target_dir}/posts")
984
+ assert File.exist?("#{target_dir}/assets/library")
985
+ assert File.exist?("#{target_dir}/themes")
986
+ end
987
+
988
+ # ========================================
989
+ # Additional exception tests for untested exceptions
990
+ # ========================================
991
+
992
+ def test_093_general_validation_exceptions
993
+ # Test that exception classes exist
994
+ assert NilValueError
995
+ assert EmptyValueError
996
+ assert InvalidFormatError
997
+
998
+ # Test actual exception raising for some that we can trigger
999
+ assert_raises(InvalidType) do
1000
+ need(:invalid_type, "some/path")
1001
+ end
1002
+
1003
+ # Test require path exceptions
1004
+ assert_raises(RequirePathNil) do
1005
+ need(:file, nil)
1006
+ end
1007
+
1008
+ assert_raises(RequirePathEmpty) do
1009
+ need(:file, "")
1010
+ end
1011
+ end
1012
+
1013
+ def test_094_repository_exceptions
1014
+ # Test RepoDirAlreadyExists
1015
+ assert RepoDirAlreadyExists
1016
+
1017
+ # Test ViewDirDoesntExist
1018
+ assert ViewDirDoesntExist
1019
+
1020
+ # Test ThemeFileNotFound
1021
+ assert ThemeFileNotFound
1022
+
1023
+ # Test NoGemPath
1024
+ assert NoGemPath
1025
+ end
1026
+
1027
+ def test_095_file_io_exceptions
1028
+ # Test that exception classes exist
1029
+ assert FileNotFoundError
1030
+ assert DirectoryNotFoundError
1031
+ assert FileDiskFull
1032
+ assert FileDirectoryNotFound
1033
+ assert CannotWriteFileError
1034
+ assert DirectoryParentNotFound
1035
+ assert DirectoryDiskFull
1036
+ assert DirectoryError
1037
+ assert ReadFileError
1038
+
1039
+ # Test actual exception raising for some that we can trigger
1040
+ assert_raises(ReadFilePathNil) do
1041
+ read_file(nil)
1042
+ end
1043
+
1044
+ assert_raises(ReadFilePathEmpty) do
1045
+ read_file("")
1046
+ end
1047
+
1048
+ assert_raises(FilePathNil) do
1049
+ write_file(nil, "content")
1050
+ end
1051
+
1052
+ assert_raises(FilePathEmpty) do
1053
+ write_file("", "content")
1054
+ end
1055
+
1056
+ # Test directory-related exceptions
1057
+ assert_raises(DirectoryPathNil) do
1058
+ make_dir(nil)
1059
+ end
1060
+
1061
+ assert_raises(DirectoryPathEmpty) do
1062
+ make_dir("")
1063
+ end
1064
+ end
1065
+
1066
+ def test_096_command_system_exceptions
1067
+ # Test that exception classes exist
1068
+ assert CommandFailed
1069
+ assert CannotExecuteCommand
1070
+ assert SectionOutputError
1071
+ assert WriteFrontPageError
1072
+
1073
+ # Test actual exception raising for some that we can trigger
1074
+ assert_raises(CommandNil) do
1075
+ system!(nil)
1076
+ end
1077
+
1078
+ assert_raises(CommandEmpty) do
1079
+ system!("")
1080
+ end
1081
+ end
1082
+
1083
+ def test_097_validation_exceptions
1084
+ # Test ViewTargetNil
1085
+ assert ViewTargetNil
1086
+
1087
+ # Test ViewTargetEmpty
1088
+ assert ViewTargetEmpty
1089
+
1090
+ # Test ViewTargetInvalid
1091
+ assert ViewTargetInvalid
1092
+
1093
+ # Test PostIdNil
1094
+ assert PostIdNil
1095
+
1096
+ # Test PostIdEmpty
1097
+ assert PostIdEmpty
1098
+
1099
+ # Test PostIdInvalid
1100
+ assert PostIdInvalid
1101
+
1102
+ # Test CannotCreateView
1103
+ assert CannotCreateView
1104
+
1105
+ # Test CannotBuildWidget
1106
+ assert CannotBuildWidget
1107
+
1108
+ # Test CannotGetPost
1109
+ assert CannotGetPost
1110
+
1111
+ # Test CannotSetPubdate
1112
+ assert CannotSetPubdate
1113
+ end
1114
+
1115
+ # ========================================
1116
+ # format_date() tests
1117
+ # ========================================
1118
+
1119
+ def test_001_format_date_basic_functionality
1120
+ # Test basic date formatting
1121
+ result = format_date("month dd yyyy", Date.new(2025, 2, 22))
1122
+ assert_equal "February 22 2025", result
1123
+ end
1124
+
1125
+ def test_002_format_date_with_ordinal
1126
+ # Test ordinal day formatting
1127
+ result = format_date("month day, yyyy", Date.new(2025, 2, 22))
1128
+ assert_equal "February 22nd, 2025", result
1129
+ end
1130
+
1131
+ def test_003_format_date_with_break
1132
+ # Test line break formatting
1133
+ result = format_date("month dd break yyyy", Date.new(2025, 2, 22))
1134
+ assert_equal "February 22<br>2025", result
1135
+ end
1136
+
1137
+ def test_004_format_date_string_input
1138
+ # Test string date input
1139
+ result = format_date("dd month yyyy", "2025-02-22")
1140
+ assert_equal "22 February 2025", result
1141
+ end
1142
+
1143
+ def test_005_format_date_nil_input
1144
+ # Test nil date input (should return original format)
1145
+ result = format_date("month dd yyyy", nil)
1146
+ assert_equal "month dd yyyy", result
1147
+ end
1148
+
1149
+ def test_006_format_date_invalid_string
1150
+ # Test invalid date string (should return original format)
1151
+ result = format_date("month dd yyyy", "invalid-date")
1152
+ assert_equal "month dd yyyy", result
1153
+ end
1154
+
1155
+ def test_007_format_date_ordinal_edge_cases
1156
+ # Test ordinal edge cases
1157
+ assert_equal "1st", ordinalize(1)
1158
+ assert_equal "2nd", ordinalize(2)
1159
+ assert_equal "3rd", ordinalize(3)
1160
+ assert_equal "4th", ordinalize(4)
1161
+ assert_equal "11th", ordinalize(11)
1162
+ assert_equal "12th", ordinalize(12)
1163
+ assert_equal "13th", ordinalize(13)
1164
+ assert_equal "21st", ordinalize(21)
1165
+ assert_equal "22nd", ordinalize(22)
1166
+ assert_equal "23rd", ordinalize(23)
1167
+ end
1168
+
1169
+ def test_008_format_date_various_formats
1170
+ # Test various format combinations
1171
+ date = Date.new(2025, 2, 22)
1172
+
1173
+ assert_equal "02/22/2025", format_date("mm/dd/yyyy", date)
1174
+ assert_equal "22/02/25", format_date("dd/mm/yy", date)
1175
+ assert_equal "February 22nd, 2025", format_date("month day, yyyy", date)
1176
+ assert_equal "22 February 2025", format_date("dd month yyyy", date)
1177
+ assert_equal "February 22<br>2025", format_date("month dd break yyyy", date)
1178
+ end
1179
+
1180
+ def test_009_format_date_smart_padding
1181
+ # Test smart padding logic
1182
+ date = Date.new(2025, 7, 4)
1183
+
1184
+ # Should pad when surrounded by punctuation
1185
+ assert_equal "04/07/2025", format_date("dd/mm/yyyy", date)
1186
+ assert_equal "07/04/2025", format_date("mm/dd/yyyy", date)
1187
+ assert_equal "2025-07-04", format_date("yyyy-mm-dd", date)
1188
+ assert_equal "04.07.2025", format_date("dd.mm.yyyy", date)
1189
+
1190
+ # Should not pad when surrounded by spaces
1191
+ assert_equal "July 4, 2025", format_date("month dd, yyyy", date)
1192
+ assert_equal "4 July 2025", format_date("dd month yyyy", date)
1193
+ assert_equal "July 4 2025", format_date("month dd yyyy", date)
1194
+ end
1195
+
1196
+ private
1197
+
1198
+ def ordinalize(number)
1199
+ case number % 100
1200
+ when 11, 12, 13
1201
+ "#{number}th"
1202
+ else
1203
+ case number % 10
1204
+ when 1 then "#{number}st"
1205
+ when 2 then "#{number}nd"
1206
+ when 3 then "#{number}rd"
1207
+ else "#{number}th"
1208
+ end
1209
+ end
1210
+ end
1211
+ end