scriptorium 0.6.1 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. checksums.yaml +4 -4
  2. data/assets/icons/social/reddit.png +0 -0
  3. data/assets/icons/social/x-logo.png +0 -0
  4. data/assets/imagenotfound.jpg +0 -0
  5. data/bin/sblog +84 -5
  6. data/bin/scriptorium +1 -0
  7. data/doc/anti-amnesia/20250727-054000-scriptorium-overview.md +0 -1
  8. data/doc/anti-amnesia/20250727-123000-anti-amnesia-conventions.md +0 -29
  9. data/doc/anti-amnesia/20250727-172600-cursor-rbenv-ruby-version-mystery.md +0 -19
  10. data/doc/anti-amnesia/20250727-172900-ai-cognitive-assessment-capabilities.md +1 -1
  11. data/doc/anti-amnesia/20250728-124243-aaa-syntax-clarification.md +1 -1
  12. data/doc/anti-amnesia/20250729-210000-reddit-autopost-integration-complete.md +1 -1
  13. data/doc/anti-amnesia/20250804-190500-cognitive-loop-bug.md +0 -10
  14. data/doc/anti-amnesia/20250804-190700-anti-amnesia-timestamping-fix.md +1 -4
  15. data/doc/anti-amnesia/20250901-211714-codemirror-integration-and-web-tests.md +172 -0
  16. data/doc/anti-amnesia/20250902-002402-backup-restore-system.md +126 -0
  17. data/doc/anti-amnesia/20250907-203339-backup-metadata-implementation.md +66 -0
  18. data/doc/imported/0001-elixir-conf-2014/metadata.txt +7 -0
  19. data/doc/imported/0001-elixir-conf-2014/post.html +37 -0
  20. data/doc/imported/0001-elixir-conf-2014/source.lt3 +22 -0
  21. data/doc/imported/0002-programmers-and-word-processing/metadata.txt +7 -0
  22. data/doc/imported/0002-programmers-and-word-processing/post.html +192 -0
  23. data/doc/imported/0002-programmers-and-word-processing/source.lt3 +146 -0
  24. data/doc/imported/0003-how-to-turn-your-brain-sideways/metadata.txt +7 -0
  25. data/doc/imported/0003-how-to-turn-your-brain-sideways/post.html +60 -0
  26. data/doc/imported/0003-how-to-turn-your-brain-sideways/source.lt3 +40 -0
  27. data/doc/imported/0004-upcoming-lone-star-ruby-conference/metadata.txt +7 -0
  28. data/doc/imported/0004-upcoming-lone-star-ruby-conference/post.html +42 -0
  29. data/doc/imported/0004-upcoming-lone-star-ruby-conference/source.lt3 +24 -0
  30. data/doc/imported/0005-elixir-conf-2015-announced/metadata.txt +7 -0
  31. data/doc/imported/0005-elixir-conf-2015-announced/post.html +30 -0
  32. data/doc/imported/0005-elixir-conf-2015-announced/source.lt3 +16 -0
  33. data/doc/imported/0006-ruby-for-dinosaurs/metadata.txt +7 -0
  34. data/doc/imported/0006-ruby-for-dinosaurs/post.html +43 -0
  35. data/doc/imported/0006-ruby-for-dinosaurs/source.lt3 +27 -0
  36. data/doc/imported/0007-phoenix-isnt-rails/metadata.txt +7 -0
  37. data/doc/imported/0007-phoenix-isnt-rails/post.html +116 -0
  38. data/doc/imported/0007-phoenix-isnt-rails/source.lt3 +87 -0
  39. data/doc/imported/0008-concerning-the-term-monkeypatching/metadata.txt +7 -0
  40. data/doc/imported/0008-concerning-the-term-monkeypatching/post.html +129 -0
  41. data/doc/imported/0008-concerning-the-term-monkeypatching/source.lt3 +92 -0
  42. data/doc/imported/0009-announcement-coming-soon/metadata.txt +7 -0
  43. data/doc/imported/0009-announcement-coming-soon/post.html +33 -0
  44. data/doc/imported/0009-announcement-coming-soon/source.lt3 +19 -0
  45. data/doc/imported/0010-immutable-data-ditching-the-wax-tablet/metadata.txt +7 -0
  46. data/doc/imported/0010-immutable-data-ditching-the-wax-tablet/post.html +175 -0
  47. data/doc/imported/0010-immutable-data-ditching-the-wax-tablet/source.lt3 +139 -0
  48. data/doc/imported/0011-computer-science-as-a-lost-art/metadata.txt +7 -0
  49. data/doc/imported/0011-computer-science-as-a-lost-art/post.html +139 -0
  50. data/doc/imported/0011-computer-science-as-a-lost-art/source.lt3 +104 -0
  51. data/doc/imported/0012-ruby-day-in-turin-italy/metadata.txt +7 -0
  52. data/doc/imported/0012-ruby-day-in-turin-italy/post.html +42 -0
  53. data/doc/imported/0012-ruby-day-in-turin-italy/source.lt3 +24 -0
  54. data/doc/imported/0013-rubyday-was-a-success/metadata.txt +7 -0
  55. data/doc/imported/0013-rubyday-was-a-success/post.html +44 -0
  56. data/doc/imported/0013-rubyday-was-a-success/source.lt3 +27 -0
  57. data/doc/imported/0014-working-on-the-blogging-software/metadata.txt +7 -0
  58. data/doc/imported/0014-working-on-the-blogging-software/post.html +63 -0
  59. data/doc/imported/0014-working-on-the-blogging-software/source.lt3 +41 -0
  60. data/doc/imported/0015-ok-its-not-really-a-lost-art/metadata.txt +7 -0
  61. data/doc/imported/0015-ok-its-not-really-a-lost-art/post.html +172 -0
  62. data/doc/imported/0015-ok-its-not-really-a-lost-art/source.lt3 +134 -0
  63. data/doc/imported/0016-an-in-operator-for-ruby/metadata.txt +7 -0
  64. data/doc/imported/0016-an-in-operator-for-ruby/post.html +155 -0
  65. data/doc/imported/0016-an-in-operator-for-ruby/source.lt3 +106 -0
  66. data/doc/imported/0017-the-forgotten-mathematician/metadata.txt +7 -0
  67. data/doc/imported/0017-the-forgotten-mathematician/post.html +161 -0
  68. data/doc/imported/0017-the-forgotten-mathematician/source.lt3 +119 -0
  69. data/doc/imported/0018-ruby-puns/metadata.txt +7 -0
  70. data/doc/imported/0018-ruby-puns/post.html +46 -0
  71. data/doc/imported/0018-ruby-puns/source.lt3 +28 -0
  72. data/doc/imported/0019-custom-exceptions-via-metaprogramming/metadata.txt +7 -0
  73. data/doc/imported/0019-custom-exceptions-via-metaprogramming/post.html +138 -0
  74. data/doc/imported/0019-custom-exceptions-via-metaprogramming/source.lt3 +101 -0
  75. data/doc/imported/0020-fffff/metadata.txt +7 -0
  76. data/doc/imported/0020-fffff/post.html +24 -0
  77. data/doc/imported/0020-fffff/source.lt3 +12 -0
  78. data/doc/imported/0021-trying-ror-yet-again/metadata.txt +7 -0
  79. data/doc/imported/0021-trying-ror-yet-again/post.html +26 -0
  80. data/doc/imported/0021-trying-ror-yet-again/source.lt3 +12 -0
  81. data/doc/imported/0023-doctor-sleep/metadata.txt +7 -0
  82. data/doc/imported/0023-doctor-sleep/post.html +63 -0
  83. data/doc/imported/0023-doctor-sleep/source.lt3 +44 -0
  84. data/doc/imported/0024-just-a-test/metadata.txt +7 -0
  85. data/doc/imported/0024-just-a-test/post.html +24 -0
  86. data/doc/imported/0024-just-a-test/source.lt3 +12 -0
  87. data/doc/imported/import_summary.txt +98 -0
  88. data/doc/livetext-informal-spec.txt +65 -0
  89. data/doc/myuserdoc/ch-0.lt3 +31 -0
  90. data/doc/myuserdoc/ch-1.lt3 +37 -0
  91. data/doc/myuserdoc/ch-10.lt3 +22 -0
  92. data/doc/myuserdoc/ch-2.lt3 +37 -0
  93. data/doc/myuserdoc/ch-3.lt3 +19 -0
  94. data/doc/myuserdoc/ch-4.lt3 +43 -0
  95. data/doc/myuserdoc/ch-5.lt3 +22 -0
  96. data/doc/myuserdoc/ch-6.lt3 +19 -0
  97. data/doc/myuserdoc/ch-7.lt3 +16 -0
  98. data/doc/myuserdoc/ch-8.lt3 +13 -0
  99. data/doc/myuserdoc/ch-9.lt3 +19 -0
  100. data/doc/myuserdoc/tweak.rb +18 -0
  101. data/doc/{userdoc-toc.txt → myuserdoc/userdoc-toc.txt} +27 -27
  102. data/doc/old-posts/0001-elixir-conf-2014.lt3 +24 -0
  103. data/doc/old-posts/0002-programmers-and-word-processing.lt3 +150 -0
  104. data/doc/old-posts/0003-how-to-turn-your-brain-sideways.lt3 +43 -0
  105. data/doc/old-posts/0004-upcoming-lone-star-ruby-conference.lt3 +26 -0
  106. data/doc/old-posts/0005-elixir-conf-2015-announced.lt3 +17 -0
  107. data/doc/old-posts/0006-ruby-for-dinosaurs.lt3 +30 -0
  108. data/doc/old-posts/0007-phoenix-isnt-rails.lt3 +90 -0
  109. data/doc/old-posts/0008-concerning-the-term-monkeypatching.lt3 +105 -0
  110. data/doc/old-posts/0009-announcement-coming-soon.lt3 +20 -0
  111. data/doc/old-posts/0010-immutable-data-ditching-the-wax-tablet.lt3 +142 -0
  112. data/doc/old-posts/0011-computer-science-as-a-lost-art.lt3 +117 -0
  113. data/doc/old-posts/0012-ruby-day-in-turin-italy.lt3 +26 -0
  114. data/doc/old-posts/0013-rubyday-was-a-success.lt3 +28 -0
  115. data/doc/old-posts/0014-working-on-the-blogging-software.lt3 +42 -0
  116. data/doc/old-posts/0015-ok-its-not-really-a-lost-art.lt3 +137 -0
  117. data/doc/old-posts/0016-an-in-operator-for-ruby.lt3 +142 -0
  118. data/doc/old-posts/0017-the-forgotten-mathematician.lt3 +129 -0
  119. data/doc/old-posts/0018-ruby-puns.lt3 +31 -0
  120. data/doc/old-posts/0019-custom-exceptions-via-metaprogramming.lt3 +116 -0
  121. data/doc/old-posts/0021-trying-ror-yet-again.lt3 +35 -0
  122. data/doc/old-posts/0023-doctor-sleep.lt3 +43 -0
  123. data/doc/old-posts/0024-just-a-test.lt3 +12 -0
  124. data/doc/old-posts/0025-trying-another-post.lt3 +12 -0
  125. data/doc/old-repo +1 -0
  126. data/doc/reddit_integration.md +2 -2
  127. data/doc/user.lt3 +0 -3
  128. data/lib/scriptorium/api.rb +1811 -78
  129. data/lib/scriptorium/banner_svg.rb +55 -68
  130. data/lib/scriptorium/contract.rb +3 -2
  131. data/lib/scriptorium/exceptions.rb +133 -102
  132. data/lib/scriptorium/helpers.rb +282 -82
  133. data/lib/scriptorium/post.rb +81 -17
  134. data/lib/scriptorium/reddit.rb +1 -1
  135. data/lib/scriptorium/repo.rb +478 -164
  136. data/lib/scriptorium/standard_files.rb +30 -396
  137. data/lib/scriptorium/support/common_js/clipboard.js +35 -0
  138. data/lib/scriptorium/support/common_js/content-loader.js +187 -0
  139. data/lib/scriptorium/support/common_js/navigation.js +52 -0
  140. data/lib/scriptorium/support/common_js/syntax-highlighting.js +27 -0
  141. data/lib/scriptorium/support/config/reddit_template.txt +17 -0
  142. data/{test/scriptorium-TEST-1754622690-146/views/sample → lib/scriptorium/support}/config/social.txt +1 -0
  143. data/lib/scriptorium/support/highlight/css.txt +2 -0
  144. data/lib/scriptorium/support/highlight/custom.css +119 -0
  145. data/lib/scriptorium/support/highlight/js.txt +1 -0
  146. data/lib/scriptorium/support/post_index/config.txt +15 -0
  147. data/lib/scriptorium/support/post_index/style.css +55 -0
  148. data/lib/scriptorium/support/templates/index_entry.lt3 +16 -0
  149. data/{test/scriptorium-TEST-1754622690-146/themes/standard/initial/post.lt3 → lib/scriptorium/support/templates/initial_post.lt3} +5 -5
  150. data/lib/scriptorium/support/templates/post.lt3 +104 -0
  151. data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/header.txt → lib/scriptorium/support/theme/header.lt3} +1 -1
  152. data/lib/scriptorium/theme.rb +83 -70
  153. data/lib/scriptorium/version.rb +2 -2
  154. data/lib/scriptorium/view.rb +194 -149
  155. data/lib/scriptorium.rb +24 -1
  156. data/lib/skeleton.rb +4 -1
  157. data/scriptorium.gemspec +2 -1
  158. data/test/WEB_INTEGRATION_README.md +196 -0
  159. data/test/all +40 -0
  160. data/test/banner_svg/unit.rb +267 -35
  161. data/test/config/deployment.txt +5 -0
  162. data/test/integration/integration_test.rb +7 -7
  163. data/test/integration/preview_flow_test.rb +94 -0
  164. data/test/livetext_plugin_test.rb +453 -182
  165. data/test/manual/banner-tests/test01.html +82 -18
  166. data/test/manual/banner-tests/test02.html +82 -18
  167. data/test/manual/banner-tests/test03.html +82 -18
  168. data/test/manual/banner-tests/test04.html +89 -25
  169. data/test/manual/banner-tests/test05.html +89 -25
  170. data/test/manual/banner-tests/test06.html +89 -25
  171. data/test/manual/banner-tests/test07.html +89 -25
  172. data/test/manual/banner-tests/test08.html +82 -18
  173. data/test/manual/banner-tests/test09.html +82 -18
  174. data/test/manual/banner-tests/test10.html +82 -18
  175. data/test/manual/banner-tests/test11.html +82 -18
  176. data/test/manual/banner-tests/test12.html +82 -18
  177. data/test/manual/banner-tests/test13.html +82 -18
  178. data/test/manual/banner-tests/test14.html +82 -18
  179. data/test/manual/banner-tests/test15.html +82 -18
  180. data/test/manual/banner-tests/test16.html +82 -18
  181. data/test/manual/banner-tests/test17.html +82 -18
  182. data/test/manual/banner-tests/test18.html +90 -26
  183. data/test/manual/banner-tests/test19.html +90 -26
  184. data/test/manual/banner-tests/test20.html +90 -26
  185. data/test/manual/banner-tests/test21.html +90 -26
  186. data/test/manual/banner-tests/test22.html +90 -26
  187. data/test/manual/banner-tests/test23.html +90 -26
  188. data/test/manual/banner-tests/test24.html +90 -26
  189. data/test/manual/banner-tests/test25.html +89 -25
  190. data/test/manual/banner_environment.rb +15 -2
  191. data/test/manual/codemirror_demo.html +773 -0
  192. data/test/manual/create_posts_for_web.rb +114 -0
  193. data/test/manual/preview_manual_test.rb +129 -0
  194. data/test/manual/test_banner_features.rb +14 -14
  195. data/test/manual/test_banner_integration.rb +115 -0
  196. data/test/manual/test_banner_radial.rb +87 -0
  197. data/test/manual/test_syntax_highlighting.rb +60 -40
  198. data/test/support/preview_utils.rb +88 -0
  199. data/test/test_gem_assets.rb +48 -0
  200. data/test/test_helpers.rb +10 -0
  201. data/test/tui_editor_integration_test.rb +15 -15
  202. data/test/tui_integration_test.rb +687 -441
  203. data/test/unit/api.rb +757 -37
  204. data/test/unit/asset_management.rb +195 -221
  205. data/test/unit/backup_test.rb +451 -0
  206. data/test/unit/contract_test.rb +1 -23
  207. data/test/unit/core.rb +415 -61
  208. data/test/unit/deploy_config_test.rb +248 -0
  209. data/test/unit/deploy_test.rb +312 -21
  210. data/test/unit/edit_post_test.rb +168 -0
  211. data/test/unit/gem_asset_management.rb +36 -42
  212. data/test/unit/livetext_basic.rb +23 -35
  213. data/test/unit/livetext_compatibility.rb +7 -14
  214. data/test/unit/parse_cmd_test.rb +260 -0
  215. data/test/unit/{symlink_test.rb → permalink_copy_test.rb} +47 -49
  216. data/test/unit/post.rb +91 -26
  217. data/test/unit/post_index_config_test.rb +258 -0
  218. data/test/unit/post_state_helpers_test.rb +137 -0
  219. data/test/unit/read_commented_file_test.rb +8 -6
  220. data/test/unit/repo.rb +75 -54
  221. data/test/unit/social_test.rb +41 -44
  222. data/test/unit/syntax_highlighting.rb +70 -0
  223. data/test/unit/theme_management_test.rb +91 -0
  224. data/test/unit/view.rb +79 -12
  225. data/test/unit/widgets.rb +8 -8
  226. data/test/web_integration_test.rb +231 -0
  227. data/test/web_test_helper.rb +218 -0
  228. data/test/web_workflow_test.rb +527 -0
  229. data/ui/tui/bin/scriptorium +885 -415
  230. data/ui/web/app/app.rb +1398 -176
  231. data/ui/web/app/assets/livetext_mode.js +244 -0
  232. data/ui/web/app/error_helpers.rb +16 -16
  233. data/ui/web/app/views/advanced_config.erb +8 -2
  234. data/ui/web/app/views/asset_management.erb +56 -0
  235. data/ui/web/app/views/backup_management.erb +238 -0
  236. data/ui/web/app/views/config_widget.erb +232 -0
  237. data/ui/web/app/views/dashboard.erb +64 -72
  238. data/ui/web/app/views/deploy_config.erb +3 -0
  239. data/ui/web/app/views/edit_pages.erb +170 -2
  240. data/ui/web/app/views/edit_post.erb +130 -9
  241. data/ui/web/app/views/edit_theme.erb +73 -0
  242. data/ui/web/app/views/edit_theme_file.erb +74 -0
  243. data/ui/web/app/views/theme_management.erb +130 -0
  244. data/ui/web/app/views/view_dashboard.erb +666 -25
  245. data/ui/web/app/views/widgets.erb +249 -0
  246. data/ui/web/bin/scriptorium-web +35 -24
  247. data/ui/web/tmp/timing.log +17 -0
  248. data/ui/web/tmp/web_server.log +0 -5
  249. metadata +190 -116
  250. data/assets/back-icon.png +0 -0
  251. data/assets/icons/facebook.svg +0 -1
  252. data/assets/icons/github.svg +0 -1
  253. data/assets/icons/instagram.svg +0 -1
  254. data/assets/icons/reddit.svg +0 -1
  255. data/assets/icons/x.svg +0 -1
  256. data/assets/icons/youtube.svg +0 -1
  257. data/bin/scriptorium +0 -1511
  258. data/doc/anti-amnesia/20250727-060000-api-design-tui-planning.md +0 -34
  259. data/doc/anti-amnesia/20250727-061000-runeblog-tui-analysis.md +0 -50
  260. data/doc/anti-amnesia/20250727-154000-livetext-plugin-file-stats.md +0 -73
  261. data/doc/anti-amnesia/20250727-172600-unified-minitest-framework.md +0 -70
  262. data/doc/anti-amnesia/20250727-173000-widget-testing-achievement.md +0 -110
  263. data/doc/anti-amnesia/20250727-180000-post-id-num-refactoring.md +0 -73
  264. data/doc/anti-amnesia/20250728-124421-conversation-summary-concise.md +0 -124
  265. data/doc/anti-amnesia/20250729-190000-scriptorium-tui-testing-complete.md +0 -46
  266. data/doc/anti-amnesia/20250729-200000-scriptorium-tui-testing-edit-file-workflow.md +0 -97
  267. data/doc/anti-amnesia/20250729-211500-dependency-management-system.md +0 -211
  268. data/doc/anti-amnesia/20250729-213000-python-virtual-environment-setup.md +0 -141
  269. data/doc/anti-amnesia/20250729-214500-theme-management-commands.md +0 -211
  270. data/doc/anti-amnesia/20250729-215000-version-update-to-0.6.0.md +0 -134
  271. data/doc/anti-amnesia/20250729-220000-user-guide-complete.md +0 -41
  272. data/doc/anti-amnesia/20250804-213700-publishing-test-fix.md +0 -49
  273. data/doc/anti-amnesia/20250804-214400-additional-test-fixes.md +0 -46
  274. data/doc/anti-amnesia/20250804-220000-asset-function-logic-clarification.md +0 -41
  275. data/doc/anti-amnesia/20250806-202032-asset-function-logic-clarification.md +0 -41
  276. data/doc/anti-amnesia/20250813-082428-syntax-highlighting-and-navigation-improvements.md +0 -256
  277. data/lib/scriptorium/syntax_highlighter.rb +0 -234
  278. data/test/manual/deploy_symlink_demo.rb +0 -142
  279. data/test/manual/symlink_demo.rb +0 -117
  280. data/test/manual/test2.rb +0 -12
  281. data/test/manual/test_banner_from_file.rb +0 -150
  282. data/test/manual/test_banner_in_header.rb +0 -35
  283. data/test/manual/test_code_highlighting.rb +0 -68
  284. data/test/manual/test_complex_header.rb +0 -74
  285. data/test/manual/test_empty_header.rb +0 -32
  286. data/test/manual/test_radial_custom.rb +0 -58
  287. data/test/manual/test_radial_large_radius.rb +0 -52
  288. data/test/manual/test_svg_debug.rb +0 -47
  289. data/test/pages-demo/config/currentview.txt +0 -1
  290. data/test/pages-demo/views/demo/config/common.js +0 -57
  291. data/test/pages-demo/views/demo/config/footer.txt +0 -1
  292. data/test/pages-demo/views/demo/config/global-head.txt +0 -8
  293. data/test/pages-demo/views/demo/config/header.txt +0 -1
  294. data/test/pages-demo/views/demo/config/layout.txt +0 -1
  295. data/test/pages-demo/views/demo/config/left.txt +0 -1
  296. data/test/pages-demo/views/demo/config/main.txt +0 -1
  297. data/test/pages-demo/views/demo/config/right.txt +0 -1
  298. data/test/pages-demo/views/demo/config.txt +0 -3
  299. data/test/pages-demo/views/demo/output/panes/footer.html +0 -1
  300. data/test/pages-demo/views/demo/output/panes/header.html +0 -1
  301. data/test/pages-demo/views/demo/output/panes/left.html +0 -1
  302. data/test/pages-demo/views/demo/output/panes/main.html +0 -1
  303. data/test/pages-demo/views/demo/output/panes/right.html +0 -1
  304. data/test/scriptorium-TEST-1754622690-146/config/bootstrap_css.txt +0 -5
  305. data/test/scriptorium-TEST-1754622690-146/config/bootstrap_js.txt +0 -4
  306. data/test/scriptorium-TEST-1754622690-146/config/common.js +0 -57
  307. data/test/scriptorium-TEST-1754622690-146/config/currentview.txt +0 -1
  308. data/test/scriptorium-TEST-1754622690-146/config/global-head.txt +0 -9
  309. data/test/scriptorium-TEST-1754622690-146/config/last_post_num.txt +0 -1
  310. data/test/scriptorium-TEST-1754622690-146/config/os_helpers.rb +0 -4
  311. data/test/scriptorium-TEST-1754622690-146/config/widgets.txt +0 -3
  312. data/test/scriptorium-TEST-1754622690-146/posts/0001/meta.txt +0 -8
  313. data/test/scriptorium-TEST-1754622690-146/posts/0001/source.lt3 +0 -6
  314. data/test/scriptorium-TEST-1754622690-146/themes/standard/README.txt +0 -1
  315. data/test/scriptorium-TEST-1754622690-146/themes/standard/config.txt +0 -1
  316. data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/gen/text.css +0 -1
  317. data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/index.lt3 +0 -1
  318. data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/index_entry.lt3 +0 -14
  319. data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/post.lt3 +0 -13
  320. data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/widget.lt3 +0 -1
  321. data/test/scriptorium-TEST-1754622690-146/views/sample/config/bootstrap_css.txt +0 -5
  322. data/test/scriptorium-TEST-1754622690-146/views/sample/config/bootstrap_js.txt +0 -4
  323. data/test/scriptorium-TEST-1754622690-146/views/sample/config/common.js +0 -57
  324. data/test/scriptorium-TEST-1754622690-146/views/sample/config/deploy.txt +0 -5
  325. data/test/scriptorium-TEST-1754622690-146/views/sample/config/footer.txt +0 -2
  326. data/test/scriptorium-TEST-1754622690-146/views/sample/config/global-head.txt +0 -9
  327. data/test/scriptorium-TEST-1754622690-146/views/sample/config/header.txt +0 -4
  328. data/test/scriptorium-TEST-1754622690-146/views/sample/config/layout.txt +0 -5
  329. data/test/scriptorium-TEST-1754622690-146/views/sample/config/left.txt +0 -3
  330. data/test/scriptorium-TEST-1754622690-146/views/sample/config/main.txt +0 -5
  331. data/test/scriptorium-TEST-1754622690-146/views/sample/config/right.txt +0 -3
  332. data/test/scriptorium-TEST-1754622690-146/views/sample/config/status.txt +0 -7
  333. data/test/scriptorium-TEST-1754622690-146/views/sample/config.txt +0 -3
  334. data/test/scriptorium-TEST-1754622690-146/views/sample/layout/footer.html +0 -3
  335. data/test/scriptorium-TEST-1754622690-146/views/sample/layout/header.html +0 -3
  336. data/test/scriptorium-TEST-1754622690-146/views/sample/layout/left.html +0 -3
  337. data/test/scriptorium-TEST-1754622690-146/views/sample/layout/main.html +0 -3
  338. data/test/scriptorium-TEST-1754622690-146/views/sample/layout/right.html +0 -3
  339. data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/footer.html +0 -1
  340. data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/header.html +0 -1
  341. data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/left.html +0 -1
  342. data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/main.html +0 -1
  343. data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/right.html +0 -1
  344. data/ui/web/tmp/web_server.pid +0 -1
  345. /data/{test/pages-demo/views/demo/config/bootstrap_css.txt → lib/scriptorium/support/bootstrap/css.txt} +0 -0
  346. /data/{test/pages-demo/views/demo/config/bootstrap_js.txt → lib/scriptorium/support/bootstrap/js.txt} +0 -0
  347. /data/{test/scriptorium-TEST-1754622690-146/views/sample → lib/scriptorium/support}/config/reddit.txt +0 -0
  348. /data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout → lib/scriptorium/support/templates}/layout.txt +0 -0
  349. /data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/footer.txt → lib/scriptorium/support/theme/footer.lt3} +0 -0
  350. /data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/left.txt → lib/scriptorium/support/theme/left.lt3} +0 -0
  351. /data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/main.txt → lib/scriptorium/support/theme/main.lt3} +0 -0
  352. /data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/right.txt → lib/scriptorium/support/theme/right.lt3} +0 -0
  353. /data/test/manual/banner-tests/{config.txt → svg.txt} +0 -0
  354. /data/test/manual/{test6.rb → test_advanced_widgets.rb} +0 -0
  355. /data/test/manual/{test1.rb → test_basic_posts.rb} +0 -0
  356. /data/test/manual/{test4.rb → test_layout_widgets.rb} +0 -0
  357. /data/test/manual/{test5.rb → test_pagination.rb} +0 -0
  358. /data/test/manual/{test3.rb → test_random_posts.rb} +0 -0
@@ -1,5 +1,4 @@
1
1
  require 'fileutils'
2
- require_relative 'syntax_highlighter'
3
2
 
4
3
  class Scriptorium::View
5
4
  include Scriptorium::Exceptions
@@ -25,10 +24,14 @@ class Scriptorium::View
25
24
  end
26
25
 
27
26
  def initialize(name, title, subtitle = "", theme = "standard")
28
- assume { name.is_a?(String) }
29
- assume { title.is_a?(String) }
30
- assume { subtitle.is_a?(String) }
31
- assume { theme.is_a?(String) }
27
+ msg = "name must be a String, got #{name.class}"
28
+ assume(msg) { name.is_a?(String) }
29
+ msg = "title must be a String, got #{title.class}"
30
+ assume(msg) { title.is_a?(String) }
31
+ msg = "subtitle must be a String, got #{subtitle.class}"
32
+ assume(msg) { subtitle.is_a?(String) }
33
+ msg = "theme must be a String, got #{theme.class}"
34
+ assume(msg) { theme.is_a?(String) }
32
35
 
33
36
  validate_name(name)
34
37
  validate_title(title)
@@ -50,46 +53,21 @@ class Scriptorium::View
50
53
  end
51
54
 
52
55
  private def validate_name(name)
53
- raise CannotCreateViewNameNil if name.nil?
56
+ raise ViewNameNil if name.nil?
54
57
 
55
- raise CannotCreateViewNameEmpty if name.to_s.strip.empty?
58
+ raise ViewNameEmpty if name.to_s.strip.empty?
56
59
 
57
60
  unless name.match?(/^[a-zA-Z0-9_-]+$/)
58
- raise CannotCreateViewNameInvalid(name)
61
+ raise ViewNameInvalid(name)
59
62
  end
60
63
  end
61
64
 
62
65
  private def validate_title(title)
63
- raise CannotCreateViewTitleNil if title.nil?
66
+ raise ViewTitleNil if title.nil?
64
67
 
65
- raise CannotCreateViewTitleEmpty if title.to_s.strip.empty?
68
+ raise ViewTitleEmpty if title.to_s.strip.empty?
66
69
  end
67
70
 
68
- =begin
69
- 1. The theme provides layout/config/header.txt with default content instructions.
70
- 2. When the theme is applied, header.txt is copied to views/VIEW/config/.
71
- 3. A placeholder layout/header.html is created in views/VIEW/layout/ with <!-- HEADER CONTENT -->.
72
- 4. The file views/VIEW/config/header.txt is parsed to generate actual HTML.
73
- 5. That HTML replaces the placeholder and is written to views/VIEW/output/panes/header.html.
74
- 6. Later, output/panes/header.html is included when assembling views/VIEW/output/index.html.
75
-
76
- That process is clean and logical. I see only minor points worth considering:
77
-
78
- Copying header.txt from theme to view config/ is irreversible by design—once copied,
79
- any theme updates won’t affect the view’s header.txt. That’s good for isolation, but
80
- it might be worth exposing a way to “reapply” or “sync” a theme’s layout/config/
81
- if desired.
82
- Placeholder files like layout/header.html in layout/ may be unnecessary once
83
- output/panes/header.html is reliably generated. If they exist solely for the
84
- <!-- CONTENT --> tags, consider templating that in-memory instead.
85
- You may want to enforce (or warn) if config/header.txt is missing or invalid
86
- at generation time, to catch misconfigured views.
87
- If you add more optional components (like navbars, banners, etc.), consider
88
- adding light validation or doc comments to header.txt to aid future users/editors.
89
-
90
- But overall, the process is robust and well thought-out. No major changes needed.
91
- =end
92
-
93
71
  def read_layout
94
72
  layout_file = @dir/:config/"layout.txt"
95
73
 
@@ -114,7 +92,7 @@ But overall, the process is robust and well thought-out. No major changes needed
114
92
  return unless File.exist?(layout_file)
115
93
 
116
94
  flexing = {
117
- header: %[id="header" class="header" style="padding: 10px;"],
95
+ header: %[id="header" class="header" style="padding: 10px; width: 100%; box-sizing: border-box;"],
118
96
  footer: %[class="footer" style="background: lightgray; padding: 10px;"],
119
97
  left: %[class="left" style="width: %{width}; background: #f0f0f0; padding: 10px; flex-grow: 0; flex-shrink: 0;"],
120
98
  right: %[class="right" style="width: %{width}; background: #f0f0f0; padding: 10px; flex-grow: 0; flex-shrink: 0;"],
@@ -155,7 +133,8 @@ But overall, the process is robust and well thought-out. No major changes needed
155
133
 
156
134
  def apply_theme(theme)
157
135
  check_invariants
158
- assume { theme.is_a?(String) && !theme.empty? }
136
+ msg = "theme must be a non-empty String, got #{theme.class} (#{theme.inspect})"
137
+ assume(msg) { theme.is_a?(String) && !theme.empty? }
159
138
 
160
139
  # check to see if ever done before?
161
140
  # copy layout.txt to view
@@ -204,7 +183,6 @@ But overall, the process is robust and well thought-out. No major changes needed
204
183
 
205
184
  def section_core(section, hash)
206
185
  cfg = @dir/:config
207
- template = @dir/:layout/"#{section}.html"
208
186
  sectxt = cfg/"#{section}.txt"
209
187
 
210
188
  # Only add placeholder if section has no real content
@@ -246,7 +224,6 @@ write output: write the result to output/panes/header.html
246
224
  =end
247
225
 
248
226
  def build_section(section, hash2 = {}, args = "")
249
- config = @dir/:config/"#{section}.txt"
250
227
  template = @dir/:layout/"#{section}.html"
251
228
  output = @dir/:output/:panes/"#{section}.html"
252
229
 
@@ -265,11 +242,7 @@ write output: write the result to output/panes/header.html
265
242
  target = content_tag(section)
266
243
  temp_txt.sub!(target, core)
267
244
 
268
- begin
269
- write_file(output, temp_txt)
270
- rescue Errno::EACCES, Errno::ENOSPC => e
271
- raise SectionOutputError(output, section, e.message)
272
- end
245
+ write_file(output, temp_txt)
273
246
 
274
247
  html = read_file(output)
275
248
  html
@@ -292,26 +265,13 @@ write output: write the result to output/panes/header.html
292
265
 
293
266
  def build_banner(arg)
294
267
  # Check if this is an SVG banner request
295
- return build_banner_svg_from_file if arg == "svg"
268
+ return build_banner_svg("svg") if arg == "svg"
296
269
 
297
270
  # Otherwise, treat as image filename
298
271
  return build_banner_image(arg)
299
272
  end
300
273
 
301
- def build_banner_svg_from_file
302
- bsvg = Scriptorium::BannerSVG.new(@title, @subtitle)
303
-
304
- # Look for svg.txt file in the view's config directory
305
- svg_config_file = @dir/:config/"svg.txt"
306
- if File.exist?(svg_config_file)
307
- bsvg.parse_header_svg(svg_config_file)
308
- else
309
- # No svg.txt file, use defaults
310
- bsvg.parse_header_svg
311
- end
312
-
313
- bsvg.get_svg
314
- end
274
+
315
275
 
316
276
  def build_banner_image(image_filename)
317
277
  # Search for image in multiple locations
@@ -360,16 +320,16 @@ write output: write the result to output/panes/header.html
360
320
  def build_banner_svg(arg)
361
321
  bsvg = Scriptorium::BannerSVG.new(@title, @subtitle)
362
322
 
363
- # Look for config file in the view's config directory
364
- config_file = @dir/:config/"config.txt"
365
- if File.exist?(config_file)
366
- bsvg.parse_header_svg(config_file)
323
+ # Look for svg.txt file in the view's config directory
324
+ svg_config_file = @dir/:config/"svg.txt"
325
+ if File.exist?(svg_config_file)
326
+ bsvg.parse_header_svg(svg_config_file)
367
327
  else
368
- # No config file, just use defaults
328
+ # No svg.txt file, just use defaults
369
329
  bsvg.parse_header_svg
370
330
  end
371
331
 
372
- code = bsvg.get_svg
332
+ bsvg.get_svg
373
333
  end
374
334
 
375
335
  def build_nav(arg)
@@ -390,20 +350,22 @@ write output: write the result to output/panes/header.html
390
350
  def generate_bootstrap_navbar(nav_content)
391
351
  menu_items = parse_navbar_content(nav_content)
392
352
 
393
- # Generate Bootstrap navbar HTML
353
+ # Generate Bootstrap navbar HTML wrapped in bootstrap-scope
394
354
  html = <<~HTML
395
- <nav class="navbar navbar-expand-lg navbar-light bg-light">
396
- <div class="container-fluid">
397
- <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
398
- <span class="navbar-toggler-icon"></span>
399
- </button>
400
- <div class="collapse navbar-collapse" id="navbarNav">
401
- <ul class="navbar-nav">
402
- #{generate_navbar_items(menu_items)}
403
- </ul>
355
+ <div class="bootstrap-scope">
356
+ <nav class="navbar navbar-expand-lg navbar-light bg-light">
357
+ <div class="container-fluid">
358
+ <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
359
+ <span class="navbar-toggler-icon"></span>
360
+ </button>
361
+ <div class="collapse navbar-collapse" id="navbarNav">
362
+ <ul class="navbar-nav">
363
+ #{generate_navbar_items(menu_items)}
364
+ </ul>
365
+ </div>
404
366
  </div>
405
- </div>
406
- </nav>
367
+ </nav>
368
+ </div>
407
369
  HTML
408
370
 
409
371
  html
@@ -467,8 +429,6 @@ write output: write the result to output/panes/header.html
467
429
  end
468
430
 
469
431
  def generate_dropdown_item(item)
470
- dropdown_id = "dropdown-#{item[:label].downcase.gsub(/\s+/, '-')}"
471
-
472
432
  html = <<~HTML
473
433
  <li class="nav-item dropdown">
474
434
  <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
@@ -477,9 +437,7 @@ write output: write the result to output/panes/header.html
477
437
  <ul class="dropdown-menu">
478
438
  HTML
479
439
 
480
- item[:children].each do |child|
481
- html << generate_dropdown_child(child)
482
- end
440
+ item[:children].each {|child| html << generate_dropdown_child(child) }
483
441
 
484
442
  html << <<~HTML
485
443
  </ul>
@@ -534,7 +492,8 @@ write output: write the result to output/panes/header.html
534
492
 
535
493
  def build_widgets(arg)
536
494
  check_invariants
537
- assume { arg.is_a?(String) }
495
+ msg = "arg must be a String, got #{arg.class}"
496
+ assume(msg) { arg.is_a?(String) }
538
497
  validate_widget_arg(arg)
539
498
 
540
499
  widgets = arg.split
@@ -542,27 +501,43 @@ write output: write the result to output/panes/header.html
542
501
  widgets.each do |widget|
543
502
  validate_widget_name(widget)
544
503
 
545
- widget_class = eval("Scriptorium::Widget::#{widget.capitalize}")
546
- obj = widget_class.new(@repo, self)
547
- obj.generate
548
- content << obj.card
504
+ begin
505
+ widget_class = eval("Scriptorium::Widget::#{widget.capitalize}")
506
+ obj = widget_class.new(@repo, self)
507
+ obj.generate
508
+ content << obj.card
509
+ rescue => e
510
+ raise CannotBuildWidget("Failed to build widget '#{widget}': #{e.message}")
511
+ end
549
512
  end
550
- verify { content.is_a?(String) }
513
+
514
+ # Wrap widgets in bootstrap-scope container
515
+ if content.strip.empty?
516
+ result = content
517
+ else
518
+ result = <<~HTML
519
+ <div class="bootstrap-scope">
520
+ #{content}
521
+ </div>
522
+ HTML
523
+ end
524
+
525
+ verify { result.is_a?(String) }
551
526
  check_invariants
552
- content
527
+ result
553
528
  end
554
529
 
555
530
  private def validate_widget_arg(arg)
556
- raise CannotBuildWidgetsArgNil if arg.nil?
531
+ raise WidgetsArgNil if arg.nil?
557
532
 
558
- raise CannotBuildWidgetsArgEmpty if arg.to_s.strip.empty?
533
+ raise WidgetsArgEmpty if arg.to_s.strip.empty?
559
534
  end
560
535
 
561
536
  private def validate_widget_name(name)
562
- raise CannotBuildWidgetNameNil if name.nil? || name.strip.empty?
537
+ raise WidgetNameNil if name.nil? || name.strip.empty?
563
538
 
564
539
  unless name.match?(/^[a-zA-Z0-9_]+$/)
565
- raise CannotBuildWidgetNameInvalid(name)
540
+ raise WidgetNameInvalid(name)
566
541
  end
567
542
  end
568
543
 
@@ -592,9 +567,9 @@ write output: write the result to output/panes/header.html
592
567
  args = sections["main"]
593
568
  return "" unless args
594
569
  html = " <!-- Section: main (output) -->\n"
595
- html << %[ <div id="main" class="main" style="flex-grow: 1; padding: 10px; overflow-y: auto; position: relative; display: flex; flex-direction: column;">]
570
+ html << %[ <div id="main" class="main" style="flex-grow: 1; padding: 5px; overflow-y: auto; position: relative; display: block;">]
596
571
  # html << %[<div id="main" class="main" style="position: relative; display: flex; flex-direction: column;">\n]
597
- html << @predef.post_index_style
572
+ html << support_data('post_index/style.css')
598
573
  if view_posts.empty?
599
574
  html << " <h1>No posts yet!</h1>"
600
575
  else
@@ -607,32 +582,54 @@ write output: write the result to output/panes/header.html
607
582
 
608
583
  def generate_post_index
609
584
  posts = @repo.all_posts(self)
610
- str = ""
585
+ config = read_post_index_config
586
+
587
+ # Build table with configurable margin-top
588
+ margin_top = config[:"index.margin.top"] || "0px"
589
+ str = "<table width=100% cellpadding=#{config[:'entry.cellpadding']} style=\"margin-top: #{margin_top};\">"
590
+
611
591
  # FIXME - many decisions to make here...
612
- posts.each do |post|
613
- str << post_index_entry(post)
614
- end
592
+ posts.each {|post| str << post_index_entry(post) }
593
+ str << "</table>"
594
+
615
595
  write_file(@dir/:output/"post_index.html", str)
616
596
  end
617
597
 
618
598
  def post_index_entry(post)
619
- # grab index-entry template
620
- # generate index-entry for each post
621
- # append to str
622
- num, title, pubdate, blurb = post.attrs(:id, :title, :pubdate, :blurb)
623
- template = @predef.index_entry
624
- entry = substitute(post, template)
599
+ template = support_data('templates/index_entry.lt3')
600
+ config = read_post_index_config
601
+ vars = post.vars.merge(config)
602
+
603
+ # Add formatted_date to vars (use post.date to get pubdate/created fallback)
604
+ date_format = config[:"entry.date.format"] || "month dd break yyyy"
605
+ vars[:formatted_date] = format_date(date_format, post.date)
606
+
607
+ entry = substitute(vars, template)
625
608
  entry
626
609
  end
627
610
 
611
+ def read_post_index_config
612
+ # Read global defaults first
613
+ global_config_file = @repo.root/:config/"post_index_defaults.txt"
614
+ global_config = File.exist?(global_config_file) ? parse_commented_file(global_config_file) : {}
615
+
616
+ # Read view-specific overrides
617
+ view_config_file = @dir/:config/"post_index.txt"
618
+ view_config = File.exist?(view_config_file) ? parse_commented_file(view_config_file) : {}
619
+
620
+ # Merge: view config overrides global defaults
621
+ global_config.merge(view_config)
622
+ rescue => e
623
+ {}
624
+ end
625
+
628
626
  def post_index_array
629
- posts = view_posts.sort {|a,b| cf_time(b.pubdate, a.pubdate) }
627
+ posts = view_posts.sort {|a,b| post_compare(a, b) }
630
628
  posts.map {|post| post_index_entry(post)}
631
629
  end
632
630
 
633
631
  def view_posts
634
- posts = []
635
- @repo.all_posts(self).sort_by {|post| post.pubdate}
632
+ @repo.all_posts(self).sort {|a,b| post_compare(a, b)}
636
633
  end
637
634
 
638
635
  def generate_html_head(view = nil)
@@ -667,16 +664,16 @@ write output: write the result to output/panes/header.html
667
664
  content << generate_bootstrap_css(view)
668
665
  when "social"
669
666
  content << generate_social_meta_tags(args)
670
- when "syntax"
671
- content << generate_syntax_css
667
+ when "highlight"
668
+ content << generate_highlight_css(view) # loads highlight_css.txt
669
+ when "highlight_custom"
670
+ content << support_data('highlight/custom.css')
672
671
  end
673
672
  end
674
673
  content << "</head>\n"
675
674
  content
676
675
  end
677
676
 
678
-
679
-
680
677
  def get_common_js(view = nil)
681
678
  global_js = @root/:config/"common.js"
682
679
  view_js = @dir/:config/"common.js"
@@ -723,7 +720,7 @@ write output: write the result to output/panes/header.html
723
720
  when "src"
724
721
  src = args
725
722
  when "rel"
726
- rel = args
723
+ # rel = args
727
724
  when "integrity"
728
725
  integrity = args
729
726
  when "crossorigin"
@@ -758,8 +755,9 @@ write output: write the result to output/panes/header.html
758
755
  # Get the appropriate title, description, and URL
759
756
  if is_post
760
757
  title = post_data[:"post.title"] || @title
761
- description = post_data[:"post.blurb"] || post_data[:"post.body"]&.truncate(200) || @desc || @subtitle || @title
762
- url = "posts/#{post_data[:"post.slug"] || slugify(post_data[:"post.id"], title)}.html"
758
+ description = post_data[:"post.blurb"] || (post_data[:"post.body"] ? post_data[:"post.body"][0..200] : nil) || @desc || @subtitle || @title
759
+ slug = post_data[:"post.slug"] || (post_data[:"post.id"] ? slugify(post_data[:"post.id"], title) : 'post')
760
+ url = "posts/#{slug}#{slug.end_with?('.html') ? '' : '.html'}"
763
761
  type = "article"
764
762
  else
765
763
  title = @title
@@ -829,17 +827,20 @@ write output: write the result to output/panes/header.html
829
827
  # Determine post URL and title
830
828
  if post_data
831
829
  title = post_data[:"post.title"] || @title
832
- url = "posts/#{post_data[:"post.slug"] || slugify(post_data[:"post.id"], title)}.html"
830
+ slug = post_data[:"post.slug"] || slugify(post_data[:"post.id"], title)
831
+ url = "posts/#{slug}#{slug.end_with?('.html') ? '' : '.html'}"
833
832
  else
834
833
  title = @title
835
834
  url = "index.html"
836
835
  end
837
836
 
838
837
  # Build Reddit share URL
838
+ require 'uri'
839
+ encoded_title = URI.encode_www_form_component(title)
839
840
  if subreddit.empty?
840
- reddit_url = "https://reddit.com/submit?url=#{escape_html(url)}&title=#{escape_html(title)}"
841
+ reddit_url = "https://reddit.com/submit?url=#{escape_html(url)}&title=#{encoded_title}"
841
842
  else
842
- reddit_url = "https://reddit.com/r/#{subreddit}/submit?url=#{escape_html(url)}&title=#{escape_html(title)}"
843
+ reddit_url = "https://reddit.com/r/#{subreddit}/submit?url=#{escape_html(url)}&title=#{encoded_title}"
843
844
  end
844
845
 
845
846
  # Determine hover text
@@ -847,9 +848,9 @@ write output: write the result to output/panes/header.html
847
848
  hover_text = subreddit.empty? ? "Share on Reddit" : "Share on r/#{subreddit}"
848
849
  end
849
850
 
850
- # Generate button HTML
851
+ # Generate button HTML (use production-relative ../../assets for posts context)
851
852
  button_html = %[<a href="#{reddit_url}" target="_blank" title="#{hover_text}" style="text-decoration: none; margin-right: 8px;">
852
- <img src="assets/reddit-logo.png" width="16" height="16" alt="Share on Reddit" style="vertical-align: middle;">
853
+ <img src="../../assets/icons/social/reddit.png" width="32" height="32" alt="Share on Reddit" style="vertical-align: middle;">
853
854
  </a>]
854
855
 
855
856
  button_html
@@ -870,7 +871,7 @@ write output: write the result to output/panes/header.html
870
871
  end
871
872
 
872
873
  def pagination_bar(group, count, nth) # nth group of total 'count'
873
- str = %[<div style="align-self: flex-end;">Pages: ]
874
+ str = %[<div style="text-align: center;">Pages: ]
874
875
  1.upto(count) do |i|
875
876
  if i == nth # 0-based
876
877
  str << "<b>[#{i}]</b>&nbsp;&nbsp;"
@@ -883,18 +884,28 @@ write output: write the result to output/panes/header.html
883
884
  end
884
885
 
885
886
  def paginate_posts
887
+ config = read_post_index_config
886
888
  posts = @repo.all_posts(self)
887
- posts.sort! {|a,b| cf_time(b.pubdate, a.pubdate) }
888
- ppp = 10 # FIXME posts per page
889
+ posts.sort! {|a,b| post_compare(a, b) }
890
+ @ppp ||= config[:"posts.per.page"].to_i
889
891
  pages = []
890
- posts.each_slice(ppp).with_index do |group, i|
892
+ posts.each_slice(@ppp).with_index do |group, i|
891
893
  pages << group.map {|post| post_index_entry(post) }
892
894
  end
893
895
  out = self.dir/:output
894
896
  pages.each.with_index do |page, i|
895
897
  bar = pagination_bar(page, pages.size, i+1)
896
- page << %[<div style="position: absolute; bottom: 0; width: 100%;">#{bar}</div>]
897
- write_file(out/"page#{i+1}.html", page.join)
898
+ # Wrap rows in a table to preserve layout (match non-paginated index)
899
+ margin_top = config[:"index.margin.top"] || "0px"
900
+ cellpadding = config[:'entry.cellpadding']
901
+ table_open = %(<table width=100% cellpadding=#{cellpadding} style="margin-top: #{margin_top};">)
902
+ table_close = %(</table>)
903
+ html = String.new
904
+ html << table_open
905
+ html << page.join
906
+ html << table_close
907
+ html << %[<div style="position: absolute; bottom: 0; width: 100%;">#{bar}</div>]
908
+ write_file(out/"page#{i+1}.html", html)
898
909
  end
899
910
  # Remove existing link if it exists, then create new one
900
911
  post_index_link = out/"post_index.html"
@@ -903,17 +914,15 @@ write output: write the result to output/panes/header.html
903
914
  end
904
915
 
905
916
  def generate_front_page
906
- layout_file = @dir/:config/"layout.txt"
907
917
  index_file = @dir/:output/"index.html"
908
- panes = @dir/:output/:panes
909
-
910
- # Ensure output directory exists
911
918
  FileUtils.mkdir_p(File.dirname(index_file))
912
919
 
913
920
  html_head = generate_html_head(true)
914
921
  content = build_containers
915
922
  common = get_common_js
916
923
  boot = generate_bootstrap_js
924
+ highlight_js = generate_highlight_js(true)
925
+ highlight_ruby_js = generate_highlight_ruby_js(true)
917
926
  full_html = <<~HTML
918
927
  <!DOCTYPE html>
919
928
  #{html_head}
@@ -921,26 +930,24 @@ write output: write the result to output/panes/header.html
921
930
  <body style="height: 100%; margin: 0; display: flex; flex-direction: column;">
922
931
  #{content.strip}
923
932
  #{boot.strip}
933
+ #{highlight_js.strip}
934
+ #{highlight_ruby_js.strip}
924
935
  #{common.strip}
925
936
  </body>
926
937
  </html>
927
938
  HTML
928
939
 
929
940
  # Beautify HTML if HtmlBeautifier is available
930
- # begin
931
- # full_html = ::HtmlBeautifier.beautify(full_html)
932
- # rescue NameError, LoadError => e
933
- # # HtmlBeautifier not available, continue without beautification
934
- # # This is not critical for functionality
935
- # end
936
-
937
- # Write the main index file
938
941
  begin
939
- write_file(index_file, full_html)
940
- rescue Errno::ENOSPC, Errno::EACCES => e
941
- raise FailedToWriteFrontPage(e.message)
942
+ full_html = ::HtmlBeautifier.beautify(full_html)
943
+ rescue NameError, LoadError => e
944
+ # HtmlBeautifier not available, continue without beautification
945
+ # This is not critical for functionality
942
946
  end
943
947
 
948
+ # Write the main index file
949
+ write_file(index_file, full_html)
950
+
944
951
  # Write debug file (optional, don't fail if it doesn't work)
945
952
  begin
946
953
  write_file("/tmp/full.html", full_html)
@@ -960,17 +967,55 @@ write output: write the result to output/panes/header.html
960
967
  end
961
968
  end
962
969
 
970
+ def generate_highlight_css(view = nil)
971
+ global_highlight = @root/:config/"highlight_css.txt"
972
+ view_highlight = @dir/:config/"highlight_css.txt"
973
+ highlight_file = view ? view_highlight : global_highlight
974
+ lines = read_commented_file(highlight_file)
975
+ href = rel = nil
976
+ lines.each do |line|
977
+ component, args = line.split(/\s+/, 2)
978
+ case component.downcase
979
+ when "href"
980
+ href = args
981
+ when "rel"
982
+ rel = args
983
+ end
984
+ end
985
+ %[<link rel="#{rel}" href="#{href}">\n]
986
+ end
963
987
 
964
-
965
-
966
- def generate_syntax_css
967
- highlighter = Scriptorium::SyntaxHighlighter.new
968
- "<style>\n#{highlighter.generate_css}\n</style>\n"
988
+ def generate_highlight_js(view = nil)
989
+ global_highlight = @root/:config/"highlight_js.txt"
990
+ view_highlight = @dir/:config/"highlight_js.txt"
991
+ highlight_file = view ? view_highlight : global_highlight
992
+ lines = read_commented_file(highlight_file)
993
+ src = nil
994
+ lines.each do |line|
995
+ component, args = line.split(/\s+/, 2)
996
+ case component.downcase
997
+ when "src"
998
+ src = args
999
+ end
1000
+ end
1001
+ %[<script src="#{src}"></script>\n]
969
1002
  end
970
1003
 
971
- def highlight_code(code, language = nil)
972
- highlighter = Scriptorium::SyntaxHighlighter.new
973
- highlighter.highlight(code, language)
1004
+ def generate_highlight_ruby_js(view = nil)
1005
+ global_highlight = @root/:config/"highlight_ruby_js.txt"
1006
+ view_highlight = @dir/:config/"highlight_ruby_js.txt"
1007
+ highlight_file = view ? view_highlight : global_highlight
1008
+ lines = read_commented_file(highlight_file)
1009
+ src = nil
1010
+ lines.each do |line|
1011
+ component, args = line.split(/\s+/, 2)
1012
+ case component.downcase
1013
+ when "src"
1014
+ src = args
1015
+ end
1016
+ end
1017
+ %[<script src="#{src}"></script>\n]
974
1018
  end
975
1019
 
1020
+
976
1021
  end
data/lib/scriptorium.rb CHANGED
@@ -2,6 +2,7 @@ require 'livetext'
2
2
  require 'find'
3
3
  require 'htmlbeautifier'
4
4
  require 'pp'
5
+ require 'digest'
5
6
 
6
7
  require_relative "skeleton"
7
8
  require_relative "scriptorium/version"
@@ -20,4 +21,26 @@ require_relative "scriptorium/widgets/links"
20
21
  require_relative "scriptorium/widgets/pages"
21
22
  require_relative "scriptorium/widgets/featured_posts"
22
23
  require_relative "scriptorium/api"
23
- require_relative "scriptorium/syntax_highlighter"
24
+
25
+ # Main Scriptorium class that provides backward compatibility
26
+ class Scriptorium
27
+ def initialize(testmode: false)
28
+ @api = Scriptorium::API.new(testmode: testmode)
29
+ end
30
+
31
+ # Delegate all the main operations to the API
32
+ def method_missing(method, *args, **kwargs, &block)
33
+ if @api.respond_to?(method)
34
+ @api.send(method, *args, **kwargs, &block)
35
+ else
36
+ super
37
+ end
38
+ end
39
+
40
+ def respond_to_missing?(method, include_private = false)
41
+ @api.respond_to?(method) || super
42
+ end
43
+
44
+ # Keep the API accessible for advanced users
45
+ attr_reader :api
46
+ end
data/lib/skeleton.rb CHANGED
@@ -1,4 +1,4 @@
1
- module Scriptorium
1
+ class Scriptorium
2
2
 
3
3
  module Exceptions
4
4
  end
@@ -15,4 +15,7 @@ module Scriptorium
15
15
  class UI
16
16
  end
17
17
 
18
+ class API
19
+ end
20
+
18
21
  end
data/scriptorium.gemspec CHANGED
@@ -28,6 +28,7 @@ spec = Gem::Specification.new do |s|
28
28
  s.add_runtime_dependency 'rouge', '~> 3.25', '>= 3.25.0'
29
29
  s.add_runtime_dependency 'sinatra', '~> 3.0', '>= 3.0.0'
30
30
  s.add_runtime_dependency 'redd', '~> 0.8.8'
31
+ s.add_runtime_dependency 'x', '~> 1.0'
31
32
  s.add_runtime_dependency 'htmlbeautifier', '~> 1.4', '>= 1.4.0'
32
33
  s.add_runtime_dependency 'clipboard', '~> 1.3', '>= 1.3.6'
33
34
 
@@ -52,7 +53,7 @@ spec = Gem::Specification.new do |s|
52
53
  s.homepage = 'https://github.com/Hal9000/scriptorium'
53
54
  s.license = "Ruby"
54
55
  s.post_install_message =
55
- "\n Success! Run 'sblog' command and type h for help.\n "
56
+ "\n Success! Run command 'sblog help' to get started.\n "
56
57
  end
57
58
 
58
59
  spec