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
@@ -0,0 +1,675 @@
1
+ # Path magic
2
+
3
+ module PathSep
4
+ def /(right)
5
+ s1 = self.to_s.dup
6
+ s2 = right.to_s.dup
7
+ s1 << "/" unless s1.end_with?("/") || s2.start_with?("/")
8
+ path = s1 + s2
9
+ path.gsub!("//", "/")
10
+ path
11
+ end
12
+ end
13
+
14
+ String.include(PathSep)
15
+ Symbol.include(PathSep)
16
+
17
+ ## Helpers
18
+
19
+ module Scriptorium::Helpers
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
+
32
+ def getvars(file)
33
+ lines = read_file(file, lines: true)
34
+ lines.map! {|line| line.sub(/# .*$/, "").strip }
35
+ lines.reject! {|line| line.empty? }
36
+ vhash = Hash.new("")
37
+ lines.each do |line|
38
+ var, val = line.split(" ", 2)
39
+ # Fix: treat nil values as empty strings
40
+ val = "" if val.nil?
41
+ vhash[var.to_sym] = val
42
+ end
43
+ vhash
44
+ end
45
+
46
+ def d4(num)
47
+ "%04d" % num
48
+ end
49
+
50
+ def view_dir(name)
51
+ @root/:views/name
52
+ end
53
+
54
+ def write_file(file, content, empty: false)
55
+ # Input validation
56
+ raise FilePathNil if file.nil?
57
+
58
+ raise FilePathEmpty if file.to_s.strip.empty?
59
+
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
69
+
70
+ # Ensure parent directory exists
71
+ FileUtils.mkdir_p(File.dirname(file))
72
+
73
+ # Write the file with error handling
74
+ begin
75
+ File.open(file, "w") do |f|
76
+ f.puts content
77
+ end
78
+ rescue Errno::ENOSPC => e
79
+ raise FileDiskFull(file, e.message)
80
+ rescue Errno::EACCES => e
81
+ raise FilePermissionDenied(file, e.message)
82
+ rescue Errno::ENOENT => e
83
+ raise FileDirectoryNotFound(file, e.message)
84
+ rescue => e
85
+ raise WriteFileError(file, e.message)
86
+ end
87
+ end
88
+
89
+ def write_file!(file, *lines, empty: false)
90
+ # Convert nil values to empty strings for proper joining
91
+ processed_lines = lines.map { |line| line.nil? ? "" : line.to_s }
92
+ content = processed_lines.join("\n")
93
+ # Always add a newline at the end to ensure there's an empty line
94
+ content += "\n"
95
+ write_file(file, content, empty: empty)
96
+ end
97
+
98
+ def make_dir(dir, create_parents = false)
99
+ # Input validation
100
+ raise DirectoryPathNil if dir.nil?
101
+
102
+ raise DirectoryPathEmpty if dir.to_s.strip.empty?
103
+
104
+ # Create parent directories if requested
105
+ if create_parents
106
+ FileUtils.mkdir_p(dir)
107
+ else
108
+ # Create single directory with error handling
109
+ begin
110
+ Dir.mkdir(dir)
111
+ rescue Errno::ENOSPC => e
112
+ raise DirectoryDiskFull(dir, e.message)
113
+ rescue Errno::EACCES => e
114
+ raise DirectoryPermissionDenied(dir, e.message)
115
+ rescue Errno::ENOENT => e
116
+ raise DirectoryParentNotFound(dir, e.message)
117
+ rescue Errno::EEXIST => e
118
+ # Directory already exists - this is usually not an error
119
+ # But we could make this configurable if needed
120
+ rescue => e
121
+ raise CreateDirectoryError(dir, e.message)
122
+ end
123
+ end
124
+ end
125
+
126
+ def system!(command, description = nil)
127
+ # Input validation
128
+ raise CommandNil if command.nil?
129
+
130
+ raise CommandEmpty if command.to_s.strip.empty?
131
+
132
+ # Execute command with error handling
133
+ success = system(command)
134
+
135
+ unless success
136
+ desc = description ? " (#{description})" : ""
137
+ raise CommandFailedWithDesc(desc, command)
138
+ end
139
+
140
+ success
141
+ end
142
+
143
+ def need(type, path, exception_class = RuntimeError)
144
+ # Input validation
145
+ raise RequirePathNil(type) if path.nil?
146
+
147
+ raise RequirePathEmpty(type) if path.to_s.strip.empty?
148
+
149
+ # Check if file/directory exists
150
+ exists = case type
151
+ when :file
152
+ File.exist?(path)
153
+ when :dir
154
+ Dir.exist?(path)
155
+ else
156
+ raise InvalidType(type)
157
+ end
158
+
159
+ unless exists
160
+ raise RequiredFileNotFound(type, path) if exception_class == RuntimeError
161
+
162
+ # Exception class - try to call it as a method first, then as constructor
163
+ raise exception_class.call(path) if exception_class.respond_to?(:call)
164
+ raise exception_class.new(path)
165
+ end
166
+
167
+ path
168
+ end
169
+
170
+ def read_file(file, options = {})
171
+ # Input validation
172
+ raise ReadFilePathNil if file.nil?
173
+
174
+ raise ReadFilePathEmpty if file.to_s.strip.empty?
175
+
176
+ # Handle missing file with fallback
177
+ if options[:missing_fallback]
178
+ return options[:missing_fallback] unless File.exist?(file)
179
+ end
180
+
181
+ # Read the file with error handling
182
+ begin
183
+ if options[:lines]
184
+ # Read as lines
185
+ if options[:chomp]
186
+ File.readlines(file, chomp: true)
187
+ else
188
+ File.readlines(file)
189
+ end
190
+ else
191
+ # Read as content
192
+ File.read(file)
193
+ end
194
+ rescue Errno::ENOENT => e
195
+ if options[:missing_fallback]
196
+ return options[:missing_fallback]
197
+ else
198
+ raise ReadFileNotFound(file, e.message)
199
+ end
200
+ rescue Errno::EACCES => e
201
+ raise ReadFilePermissionDenied(file, e.message)
202
+ rescue => e
203
+ raise ReadFileError(file, e.message)
204
+ end
205
+ end
206
+
207
+
208
+
209
+ def change_config(file_path, target_key, new_value)
210
+ pattern = /
211
+ ^(?<leading>\s*#{Regexp.escape(target_key)}\s+) # key and spacing
212
+ (?<old_value>[^\#]*?) # value (non-greedy up to comment)
213
+ (?<trailing>\s*) # trailing space
214
+ (?<comment>\#.*)?$ # optional comment
215
+ /x
216
+
217
+ lines = read_file(file_path, lines: true)
218
+ updated_lines = lines.map do |line|
219
+ if match = pattern.match(line)
220
+ leading = match[:leading]
221
+ trailing = match[:trailing]
222
+ comment = match[:comment] || ''
223
+ "#{leading}#{new_value}#{trailing}#{comment}\n"
224
+ else
225
+ line
226
+ end
227
+ end
228
+
229
+ write_file(file_path, updated_lines.join)
230
+ end
231
+
232
+ def slugify(id, title)
233
+ return "#{d4(id)}-untitled" if title.nil? || title.strip.empty?
234
+
235
+ slug = title.downcase.strip
236
+ .gsub(/[^a-z0-9\s_-]/, '') # remove punctuation
237
+ .gsub(/[\s_-]+/, '-') # replace spaces and underscores with hyphen
238
+ .gsub(/^-+|-+$/, '') # trim leading/trailing hyphens
239
+ "#{d4(id)}-#{slug}"
240
+ end
241
+
242
+ def clean_slugify(title)
243
+ return "title-is-missing" if title.nil?
244
+
245
+ slug = title.downcase.strip
246
+ .gsub(/[^a-z0-9\s_-]/, '') # remove punctuation
247
+ .gsub(/[\s_-]+/, '-') # replace spaces and underscores with hyphen
248
+ .gsub(/^-+|-+$/, '') # trim leading/trailing hyphens
249
+ slug
250
+ end
251
+
252
+ def ymdhms
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")
258
+ end
259
+
260
+ def see_file(file) # Really from TestHelpers
261
+ puts "----- File: #{file}"
262
+ system!("cat #{file}", "displaying file contents")
263
+ puts "-----"
264
+ end
265
+
266
+ def see(label, var)
267
+ puts "#{label} = \n<<<\n#{var}\n>>>"
268
+ end
269
+
270
+ def make_tree(base, text)
271
+ lines = text.split("\n").map(&:chomp)
272
+ lines.each {|line| line.gsub!(/ *#.*$/, "") }
273
+ entries = []
274
+
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]
281
+
282
+ # Parse all lines as structure
283
+ lines.each do |line|
284
+ if (i = line.index(/ [a-zA-Z0-9_.]/))
285
+ name = line[(i + 1)..-1]
286
+ level = i / 4
287
+ else
288
+ name = line.strip
289
+ level = 0
290
+ end
291
+ entries << [level, name]
292
+ end
293
+
294
+ entries.each do |level, name|
295
+ stack = stack[0..level]
296
+ full_path = File.join(stack.last, name)
297
+
298
+ if name.end_with?("/")
299
+ make_dir(full_path) unless File.exist?(full_path)
300
+ stack << full_path
301
+ else
302
+ write_file(full_path, "Empty file generated at #{Time.now}")
303
+ end
304
+ end
305
+ end
306
+
307
+ def substitute(obj, text)
308
+ vars = obj.is_a?(Hash) ? obj : obj.vars
309
+ text % vars
310
+ end
311
+
312
+ def escape_html(str)
313
+ str.gsub(/&/, '&amp;')
314
+ .gsub(/</, '&lt;')
315
+ .gsub(/>/, '&gt;')
316
+ .gsub(/"/, '&quot;')
317
+ .gsub(/'/, '&#39;')
318
+ end
319
+
320
+ def read_commented_file(file_path)
321
+ return [] unless File.exist?(file_path)
322
+ lines = read_file(file_path, lines: true) # Read file and remove newline characters
323
+ lines.reject! do |line| # Remove empty lines and comments
324
+ line.strip.empty? || line.strip.start_with?("#")
325
+ end
326
+ lines.map! do |line| # Strip trailing comments + preceding spaces
327
+ line.sub(/# .*$/, "").strip
328
+ end
329
+ lines # Return cleaned lines
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
342
+
343
+ def cf_time(t1, t2)
344
+ t1 = t1.split(/- :/, 6)
345
+ t2 = t2.split(/- :/, 6)
346
+ t1 = Time.new(*t1)
347
+ t2 = Time.new(*t2)
348
+ t1 <=> t2
349
+ end
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
+
404
+ def get_asset_path(name)
405
+ if Scriptorium::Repo.testing
406
+ # Development/testing: Check dev_assets first, then local assets
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)
414
+ else # Production
415
+ # Production: Check user assets first, then gem assets
416
+
417
+ # Check user assets first (highest priority) - recursively
418
+ user_asset = find_asset("assets", name)
419
+ return user_asset if user_asset
420
+
421
+ # Then check gem assets (fallback) - recursively
422
+ begin
423
+ gem_spec = Gem.loaded_specs['scriptorium']
424
+ if gem_spec
425
+ gem_asset = find_asset("#{gem_spec.full_gem_path}/assets", name)
426
+ return gem_asset if gem_asset
427
+ end
428
+ rescue => e
429
+ # If gem lookup fails, continue without gem assets
430
+ end
431
+
432
+ # Asset not found
433
+ raise AssetNotFound(name)
434
+ end
435
+ end
436
+
437
+ def generate_missing_asset_svg(filename, width: 200, height: 150)
438
+ # Truncate filename if too long for display
439
+ display_name = filename.length > 20 ? filename[0..16] + "..." : filename
440
+
441
+ # Generate SVG with broken image icon and filename
442
+ svg = <<~SVG
443
+ <svg width="#{width}" height="#{height}" xmlns="http://www.w3.org/2000/svg">
444
+ <!-- Background -->
445
+ <rect fill="#f8f9fa" stroke="#ddd" stroke-width="1" width="#{width}" height="#{height}" rx="4"/>
446
+
447
+ <!-- Broken image icon -->
448
+ <g transform="translate(#{width/2}, #{height/2 - 20})">
449
+ <!-- Image frame -->
450
+ <rect x="-15" y="-10" width="30" height="20" fill="none" stroke="#999" stroke-width="1"/>
451
+ <!-- Broken corner -->
452
+ <path d="M 15 -10 L 25 -20 M 15 -10 L 25 0" stroke="#999" stroke-width="1" fill="none"/>
453
+ <!-- Image icon -->
454
+ <rect x="-12" y="-7" width="24" height="14" fill="#e9ecef"/>
455
+ <circle cx="-5" cy="-2" r="2" fill="#999"/>
456
+ <polygon points="-8,8 -2,2 2,6 8,0" fill="#999"/>
457
+ </g>
458
+
459
+ <!-- Filename -->
460
+ <text x="#{width/2}" y="#{height/2 + 15}" text-anchor="middle" fill="#666" font-family="Arial, sans-serif" font-size="11">
461
+ #{escape_html(display_name)}
462
+ </text>
463
+
464
+ <!-- "Asset not found" message -->
465
+ <text x="#{width/2}" y="#{height/2 + 30}" text-anchor="middle" fill="#999" font-family="Arial, sans-serif" font-size="9">
466
+ Asset not found
467
+ </text>
468
+ </svg>
469
+ SVG
470
+
471
+ svg.strip
472
+ end
473
+
474
+ def list_gem_assets
475
+ assets = []
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
484
+ end
485
+ end
486
+ end
487
+ assets.sort
488
+ rescue => e
489
+ # If gem lookup fails, return empty array
490
+ []
491
+ end
492
+
493
+ def copy_gem_asset_to_user(asset_name, target_dir = "assets")
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
505
+ end
506
+ end
507
+ nil
508
+ rescue => e
509
+ # If gem lookup fails, return nil
510
+ nil
511
+ end
512
+
513
+ # Clipboard helper methods
514
+ def copy_to_clipboard(text)
515
+ begin
516
+ require 'clipboard'
517
+ Clipboard.copy(text)
518
+ true
519
+ rescue LoadError => e
520
+ # Fallback to system commands if clipboard gem not available
521
+ case RbConfig::CONFIG['host_os']
522
+ when /darwin/ # macOS
523
+ system("echo '#{text}' | pbcopy")
524
+ when /linux/ # Linux
525
+ system("echo '#{text}' | xclip -selection clipboard")
526
+ when /mswin|mingw|cygwin/ # Windows
527
+ system("echo '#{text}' | clip")
528
+ else
529
+ puts "Clipboard not supported on this OS"
530
+ false
531
+ end
532
+ rescue => e
533
+ puts "Failed to copy to clipboard: #{e.message}"
534
+ false
535
+ end
536
+ end
537
+
538
+ def get_from_clipboard
539
+ begin
540
+ require 'clipboard'
541
+ Clipboard.paste
542
+ rescue LoadError => e
543
+ # Fallback to system commands if clipboard gem not available
544
+ case RbConfig::CONFIG['host_os']
545
+ when /darwin/ # macOS
546
+ `pbpaste`
547
+ when /linux/ # Linux
548
+ `xclip -selection clipboard -o`
549
+ when /mswin|mingw|cygwin/ # Windows
550
+ `powershell -command "Get-Clipboard"`
551
+ else
552
+ puts "Clipboard not supported on this OS"
553
+ nil
554
+ end
555
+ rescue => e
556
+ puts "Failed to read from clipboard: #{e.message}"
557
+ nil
558
+ end
559
+ end
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