scriptorium 0.0.2 → 0.6.1

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 (290) 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/back-icon.png +0 -0
  7. data/assets/icons/facebook.svg +1 -0
  8. data/assets/icons/github.svg +1 -0
  9. data/assets/icons/instagram.svg +1 -0
  10. data/assets/icons/reddit.svg +1 -0
  11. data/assets/icons/ui/.DS_Store +0 -0
  12. data/assets/icons/ui/back.png +0 -0
  13. data/assets/icons/ui/copy.png +0 -0
  14. data/assets/icons/ui/down.png +0 -0
  15. data/assets/icons/ui/end.png +0 -0
  16. data/assets/icons/ui/exit.png +0 -0
  17. data/assets/icons/ui/foo +10 -0
  18. data/assets/icons/ui/home.png +0 -0
  19. data/assets/icons/ui/left.png +0 -0
  20. data/assets/icons/ui/next.png +0 -0
  21. data/assets/icons/ui/right.png +0 -0
  22. data/assets/icons/ui/start.png +0 -0
  23. data/assets/icons/ui/up.png +0 -0
  24. data/assets/icons/x.svg +1 -0
  25. data/assets/icons/youtube.svg +1 -0
  26. data/assets/samples/placeholder.svg +9 -0
  27. data/assets/themes/standard/favicon.svg +6 -0
  28. data/bin/scriptorium +1511 -0
  29. data/doc/README.txt +6 -0
  30. data/doc/anti-amnesia/20250727-054000-scriptorium-overview.md +95 -0
  31. data/doc/anti-amnesia/20250727-060000-api-design-tui-planning.md +34 -0
  32. data/doc/anti-amnesia/20250727-061000-runeblog-tui-analysis.md +50 -0
  33. data/doc/anti-amnesia/20250727-123000-anti-amnesia-conventions.md +31 -0
  34. data/doc/anti-amnesia/20250727-154000-livetext-plugin-file-stats.md +73 -0
  35. data/doc/anti-amnesia/20250727-172600-cursor-rbenv-ruby-version-mystery.md +64 -0
  36. data/doc/anti-amnesia/20250727-172600-unified-minitest-framework.md +70 -0
  37. data/doc/anti-amnesia/20250727-172900-ai-cognitive-assessment-capabilities.md +40 -0
  38. data/doc/anti-amnesia/20250727-173000-widget-testing-achievement.md +110 -0
  39. data/doc/anti-amnesia/20250727-180000-post-id-num-refactoring.md +73 -0
  40. data/doc/anti-amnesia/20250728-124243-aaa-syntax-clarification.md +46 -0
  41. data/doc/anti-amnesia/20250728-124421-conversation-summary-concise.md +124 -0
  42. data/doc/anti-amnesia/20250729-190000-scriptorium-tui-testing-complete.md +46 -0
  43. data/doc/anti-amnesia/20250729-200000-scriptorium-tui-testing-edit-file-workflow.md +97 -0
  44. data/doc/anti-amnesia/20250729-210000-reddit-autopost-integration-complete.md +158 -0
  45. data/doc/anti-amnesia/20250729-211500-dependency-management-system.md +211 -0
  46. data/doc/anti-amnesia/20250729-213000-python-virtual-environment-setup.md +141 -0
  47. data/doc/anti-amnesia/20250729-214500-theme-management-commands.md +211 -0
  48. data/doc/anti-amnesia/20250729-215000-version-update-to-0.6.0.md +134 -0
  49. data/doc/anti-amnesia/20250729-220000-user-guide-complete.md +41 -0
  50. data/doc/anti-amnesia/20250804-190500-cognitive-loop-bug.md +45 -0
  51. data/doc/anti-amnesia/20250804-190700-anti-amnesia-timestamping-fix.md +30 -0
  52. data/doc/anti-amnesia/20250804-213700-publishing-test-fix.md +49 -0
  53. data/doc/anti-amnesia/20250804-214400-additional-test-fixes.md +46 -0
  54. data/doc/anti-amnesia/20250804-220000-asset-function-logic-clarification.md +41 -0
  55. data/doc/anti-amnesia/20250806-202032-asset-function-logic-clarification.md +41 -0
  56. data/doc/anti-amnesia/20250807-213025.md +116 -0
  57. data/doc/anti-amnesia/20250813-082428-syntax-highlighting-and-navigation-improvements.md +256 -0
  58. data/doc/banner_svg_config.md +114 -0
  59. data/doc/contrib.lt3 +8 -0
  60. data/doc/dependencies.md +281 -0
  61. data/doc/hacker.lt3 +5 -0
  62. data/doc/reddit_credentials_template.json +8 -0
  63. data/doc/reddit_integration.md +207 -0
  64. data/doc/user.lt3 +38 -0
  65. data/doc/user_guide_section_1.md +137 -0
  66. data/doc/user_guide_section_10.md +515 -0
  67. data/doc/user_guide_section_11.md +708 -0
  68. data/doc/user_guide_section_2.md +233 -0
  69. data/doc/user_guide_section_3.md +5 -0
  70. data/doc/user_guide_section_4.md +221 -0
  71. data/doc/user_guide_section_5.md +243 -0
  72. data/doc/user_guide_section_6.md +147 -0
  73. data/doc/user_guide_section_7.md +311 -0
  74. data/doc/user_guide_section_8.md +224 -0
  75. data/doc/user_guide_section_9.md +375 -0
  76. data/doc/userdoc-toc.txt +88 -0
  77. data/lib/rouge/lexers/livetext.rb +74 -0
  78. data/lib/scriptorium/api.rb +640 -0
  79. data/lib/scriptorium/banner_svg.rb +742 -0
  80. data/lib/scriptorium/contract.rb +33 -0
  81. data/lib/scriptorium/exceptions.rb +174 -0
  82. data/lib/scriptorium/helpers.rb +475 -0
  83. data/lib/scriptorium/post.rb +195 -0
  84. data/lib/scriptorium/reddit.rb +83 -0
  85. data/lib/scriptorium/repo.rb +624 -0
  86. data/lib/scriptorium/standard_files.rb +515 -0
  87. data/lib/scriptorium/syntax_highlighter.rb +234 -0
  88. data/lib/scriptorium/theme.rb +179 -0
  89. data/lib/scriptorium/version.rb +2 -2
  90. data/lib/scriptorium/view.rb +976 -0
  91. data/lib/scriptorium/widgets/featured_posts.rb +149 -0
  92. data/lib/scriptorium/widgets/links.rb +112 -0
  93. data/lib/scriptorium/widgets/pages.rb +133 -0
  94. data/lib/scriptorium/widgets/widget.rb +133 -0
  95. data/lib/scriptorium.rb +22 -9
  96. data/lib/skeleton.rb +11 -2
  97. data/scriptorium.gemspec +15 -4
  98. data/test/README.md +69 -0
  99. data/test/all +43 -0
  100. data/test/api_demo.rb +99 -0
  101. data/test/assets/imagenotfound.jpg +0 -0
  102. data/test/assets/images/.DS_Store +0 -0
  103. data/test/assets/images/README.md +27 -0
  104. data/test/assets/images/odd_aspect.png +0 -0
  105. data/test/assets/images/perfect.png +0 -0
  106. data/test/assets/images/small.png +0 -0
  107. data/test/assets/images/tall.png +0 -0
  108. data/test/assets/images/very_tall.png +0 -0
  109. data/test/assets/images/very_wide.png +0 -0
  110. data/test/assets/images/wide.png +0 -0
  111. data/test/assets/testbanner.jpg +0 -0
  112. data/test/banner_svg/simple_helpers.rb +13 -0
  113. data/test/banner_svg/unit.rb +768 -0
  114. data/test/ed_test.rb +204 -0
  115. data/test/integration/cursor_banner_combinations.rb +193 -0
  116. data/test/integration/cursor_banner_features.rb +374 -0
  117. data/test/integration/integration_test.rb +326 -0
  118. data/test/livetext_plugin_test.rb +229 -0
  119. data/test/manual/asset_mgmt.rb +67 -0
  120. data/test/manual/banner-tests/config.txt +3 -0
  121. data/test/manual/banner-tests/index.html +45 -0
  122. data/test/manual/banner-tests/test01.html +58 -0
  123. data/test/manual/banner-tests/test02.html +58 -0
  124. data/test/manual/banner-tests/test03.html +58 -0
  125. data/test/manual/banner-tests/test04.html +65 -0
  126. data/test/manual/banner-tests/test05.html +65 -0
  127. data/test/manual/banner-tests/test06.html +65 -0
  128. data/test/manual/banner-tests/test07.html +65 -0
  129. data/test/manual/banner-tests/test08.html +59 -0
  130. data/test/manual/banner-tests/test09.html +59 -0
  131. data/test/manual/banner-tests/test10.html +59 -0
  132. data/test/manual/banner-tests/test11.html +59 -0
  133. data/test/manual/banner-tests/test12.html +59 -0
  134. data/test/manual/banner-tests/test13.html +59 -0
  135. data/test/manual/banner-tests/test14.html +59 -0
  136. data/test/manual/banner-tests/test15.html +58 -0
  137. data/test/manual/banner-tests/test16.html +58 -0
  138. data/test/manual/banner-tests/test17.html +58 -0
  139. data/test/manual/banner-tests/test18.html +68 -0
  140. data/test/manual/banner-tests/test19.html +68 -0
  141. data/test/manual/banner-tests/test20.html +68 -0
  142. data/test/manual/banner-tests/test21.html +68 -0
  143. data/test/manual/banner-tests/test22.html +68 -0
  144. data/test/manual/banner-tests/test23.html +68 -0
  145. data/test/manual/banner-tests/test24.html +68 -0
  146. data/test/manual/banner-tests/test25.html +67 -0
  147. data/test/manual/banner_environment.rb +192 -0
  148. data/test/manual/deploy_symlink_demo.rb +142 -0
  149. data/test/manual/environment.rb +67 -0
  150. data/test/manual/make_banner.rb +153 -0
  151. data/test/manual/sample_banner_config.txt +12 -0
  152. data/test/manual/symlink_demo.rb +117 -0
  153. data/test/manual/test1.rb +47 -0
  154. data/test/manual/test2.rb +12 -0
  155. data/test/manual/test3.rb +38 -0
  156. data/test/manual/test4.rb +40 -0
  157. data/test/manual/test5.rb +24 -0
  158. data/test/manual/test6.rb +73 -0
  159. data/test/manual/test_banner_combinations.rb +120 -0
  160. data/test/manual/test_banner_features.rb +306 -0
  161. data/test/manual/test_banner_from_file.rb +150 -0
  162. data/test/manual/test_banner_in_header.rb +35 -0
  163. data/test/manual/test_code_highlighting.rb +68 -0
  164. data/test/manual/test_complex_header.rb +74 -0
  165. data/test/manual/test_empty_header.rb +32 -0
  166. data/test/manual/test_radial_custom.rb +58 -0
  167. data/test/manual/test_radial_large_radius.rb +52 -0
  168. data/test/manual/test_svg_debug.rb +47 -0
  169. data/test/manual/test_syntax_highlighting.rb +147 -0
  170. data/test/pages-demo/config/currentview.txt +1 -0
  171. data/test/pages-demo/views/demo/config/bootstrap_css.txt +5 -0
  172. data/test/pages-demo/views/demo/config/bootstrap_js.txt +4 -0
  173. data/test/pages-demo/views/demo/config/common.js +57 -0
  174. data/test/pages-demo/views/demo/config/footer.txt +1 -0
  175. data/test/pages-demo/views/demo/config/global-head.txt +8 -0
  176. data/test/pages-demo/views/demo/config/header.txt +1 -0
  177. data/test/pages-demo/views/demo/config/layout.txt +1 -0
  178. data/test/pages-demo/views/demo/config/left.txt +1 -0
  179. data/test/pages-demo/views/demo/config/main.txt +1 -0
  180. data/test/pages-demo/views/demo/config/right.txt +1 -0
  181. data/test/pages-demo/views/demo/config.txt +3 -0
  182. data/test/pages-demo/views/demo/output/panes/footer.html +1 -0
  183. data/test/pages-demo/views/demo/output/panes/header.html +1 -0
  184. data/test/pages-demo/views/demo/output/panes/left.html +1 -0
  185. data/test/pages-demo/views/demo/output/panes/main.html +1 -0
  186. data/test/pages-demo/views/demo/output/panes/right.html +1 -0
  187. data/test/rubytext/rubytext_comprehensive_test.rb +307 -0
  188. data/test/rubytext/rubytext_demo_test.rb +42 -0
  189. data/test/rubytext/rubytext_testing_guide.md +277 -0
  190. data/test/run_automated_tests.rb +45 -0
  191. data/test/scriptorium-TEST-1754622690-146/config/bootstrap_css.txt +5 -0
  192. data/test/scriptorium-TEST-1754622690-146/config/bootstrap_js.txt +4 -0
  193. data/test/scriptorium-TEST-1754622690-146/config/common.js +57 -0
  194. data/test/scriptorium-TEST-1754622690-146/config/currentview.txt +1 -0
  195. data/test/scriptorium-TEST-1754622690-146/config/global-head.txt +9 -0
  196. data/test/scriptorium-TEST-1754622690-146/config/last_post_num.txt +1 -0
  197. data/test/scriptorium-TEST-1754622690-146/config/os_helpers.rb +4 -0
  198. data/test/scriptorium-TEST-1754622690-146/config/widgets.txt +3 -0
  199. data/test/scriptorium-TEST-1754622690-146/posts/0001/meta.txt +8 -0
  200. data/test/scriptorium-TEST-1754622690-146/posts/0001/source.lt3 +6 -0
  201. data/test/scriptorium-TEST-1754622690-146/themes/standard/README.txt +1 -0
  202. data/test/scriptorium-TEST-1754622690-146/themes/standard/config.txt +1 -0
  203. data/test/scriptorium-TEST-1754622690-146/themes/standard/initial/post.lt3 +12 -0
  204. data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/footer.txt +2 -0
  205. data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/header.txt +4 -0
  206. data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/left.txt +3 -0
  207. data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/main.txt +5 -0
  208. data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/right.txt +3 -0
  209. data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/gen/text.css +1 -0
  210. data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/layout.txt +5 -0
  211. data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/index.lt3 +1 -0
  212. data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/index_entry.lt3 +14 -0
  213. data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/post.lt3 +13 -0
  214. data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/widget.lt3 +1 -0
  215. data/test/scriptorium-TEST-1754622690-146/views/sample/config/bootstrap_css.txt +5 -0
  216. data/test/scriptorium-TEST-1754622690-146/views/sample/config/bootstrap_js.txt +4 -0
  217. data/test/scriptorium-TEST-1754622690-146/views/sample/config/common.js +57 -0
  218. data/test/scriptorium-TEST-1754622690-146/views/sample/config/deploy.txt +5 -0
  219. data/test/scriptorium-TEST-1754622690-146/views/sample/config/footer.txt +2 -0
  220. data/test/scriptorium-TEST-1754622690-146/views/sample/config/global-head.txt +9 -0
  221. data/test/scriptorium-TEST-1754622690-146/views/sample/config/header.txt +4 -0
  222. data/test/scriptorium-TEST-1754622690-146/views/sample/config/layout.txt +5 -0
  223. data/test/scriptorium-TEST-1754622690-146/views/sample/config/left.txt +3 -0
  224. data/test/scriptorium-TEST-1754622690-146/views/sample/config/main.txt +5 -0
  225. data/test/scriptorium-TEST-1754622690-146/views/sample/config/reddit.txt +10 -0
  226. data/test/scriptorium-TEST-1754622690-146/views/sample/config/right.txt +3 -0
  227. data/test/scriptorium-TEST-1754622690-146/views/sample/config/social.txt +7 -0
  228. data/test/scriptorium-TEST-1754622690-146/views/sample/config/status.txt +7 -0
  229. data/test/scriptorium-TEST-1754622690-146/views/sample/config.txt +3 -0
  230. data/test/scriptorium-TEST-1754622690-146/views/sample/layout/footer.html +3 -0
  231. data/test/scriptorium-TEST-1754622690-146/views/sample/layout/header.html +3 -0
  232. data/test/scriptorium-TEST-1754622690-146/views/sample/layout/left.html +3 -0
  233. data/test/scriptorium-TEST-1754622690-146/views/sample/layout/main.html +3 -0
  234. data/test/scriptorium-TEST-1754622690-146/views/sample/layout/right.html +3 -0
  235. data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/footer.html +1 -0
  236. data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/header.html +1 -0
  237. data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/left.html +1 -0
  238. data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/main.html +1 -0
  239. data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/right.html +1 -0
  240. data/test/staging/.DS_Store +0 -0
  241. data/test/syntax_highlighting_test.lt3 +124 -0
  242. data/test/test_helpers.rb +230 -0
  243. data/test/tui_editor_integration_test.rb +296 -0
  244. data/test/tui_integration_test.rb +637 -0
  245. data/test/unit/api.rb +1056 -0
  246. data/test/unit/asset_management.rb +245 -0
  247. data/test/unit/clipboard_test.rb +60 -0
  248. data/test/unit/contract_test.rb +91 -0
  249. data/test/unit/core.rb +857 -0
  250. data/test/unit/deploy_test.rb +187 -0
  251. data/test/unit/gem_asset_management.rb +189 -0
  252. data/test/unit/livetext_basic.rb +69 -0
  253. data/test/unit/livetext_compatibility.rb +89 -0
  254. data/test/unit/post.rb +244 -0
  255. data/test/unit/read_commented_file_test.rb +276 -0
  256. data/test/unit/reddit_test.rb +235 -0
  257. data/test/unit/repo.rb +548 -0
  258. data/test/unit/social_test.rb +369 -0
  259. data/test/unit/symlink_test.rb +213 -0
  260. data/test/unit/view.rb +431 -0
  261. data/test/unit/widgets.rb +669 -0
  262. data/test/wizard_test.rb +123 -0
  263. data/ui/README.md +67 -0
  264. data/ui/common/lib/ui_common.rb +8 -0
  265. data/ui/rubytext/README.md +191 -0
  266. data/ui/rubytext/bin/scriptorium-rubytext +402 -0
  267. data/ui/rubytext/lib/rubytext_ui.rb +300 -0
  268. data/ui/tui/bin/scriptorium +1420 -0
  269. data/ui/tui/test/tui_test.rb +23 -0
  270. data/ui/web/app/app.rb +1378 -0
  271. data/ui/web/app/error_helpers.rb +150 -0
  272. data/ui/web/app/views/advanced_config.erb +190 -0
  273. data/ui/web/app/views/asset_management.erb +589 -0
  274. data/ui/web/app/views/banner_config.erb +200 -0
  275. data/ui/web/app/views/configure_view.erb +401 -0
  276. data/ui/web/app/views/dashboard.erb +162 -0
  277. data/ui/web/app/views/deploy_config.erb +146 -0
  278. data/ui/web/app/views/edit_pages.erb +195 -0
  279. data/ui/web/app/views/edit_post.erb +54 -0
  280. data/ui/web/app/views/error_page.erb +29 -0
  281. data/ui/web/app/views/header_config.erb +155 -0
  282. data/ui/web/app/views/layout_config.erb +147 -0
  283. data/ui/web/app/views/navbar_config.erb +411 -0
  284. data/ui/web/app/views/view_dashboard.erb +138 -0
  285. data/ui/web/bin/scriptorium-web +153 -0
  286. data/ui/web/test/web_basic_test.rb +38 -0
  287. data/ui/web/test_navbar.txt +7 -0
  288. data/ui/web/tmp/web_server.log +5 -0
  289. data/ui/web/tmp/web_server.pid +1 -0
  290. metadata +360 -5
@@ -0,0 +1,149 @@
1
+ class Scriptorium::Widget::FeaturedPosts < Scriptorium::Widget::ListWidget
2
+ include Scriptorium::Contract
3
+
4
+ Title = "Featured Posts"
5
+
6
+ # Invariants
7
+ def define_invariants
8
+ invariant { @lines.is_a?(Array) }
9
+ invariant { Title.is_a?(String) && !Title.empty? }
10
+ end
11
+
12
+ def initialize(repo, view)
13
+ assume { repo.is_a?(Scriptorium::Repo) }
14
+ assume { view.is_a?(Scriptorium::View) }
15
+
16
+ super(repo, view)
17
+ @lines = @data
18
+
19
+ define_invariants
20
+ verify { @lines == @data }
21
+ check_invariants
22
+ end
23
+
24
+ def load_config
25
+ check_invariants
26
+ # No configuration needed for this widget
27
+ check_invariants
28
+ end
29
+
30
+ def generate
31
+ check_invariants
32
+ assume { @view.is_a?(Scriptorium::View) }
33
+
34
+ write_main
35
+ write_card
36
+ result = true
37
+
38
+ verify { result == true }
39
+ check_invariants
40
+ result
41
+ end
42
+
43
+ def widget_title
44
+ check_invariants
45
+ result = Title
46
+ verify { result.is_a?(String) && !result.empty? }
47
+ check_invariants
48
+ result
49
+ end
50
+
51
+ def card
52
+ check_invariants
53
+ assume { @view.is_a?(Scriptorium::View) }
54
+ assume { @name.is_a?(String) && !@name.empty? }
55
+
56
+ file = "#{@view.dir}/widgets/#@name/#@name-card.html"
57
+ result = read_file(file)
58
+
59
+ verify { result.is_a?(String) }
60
+ check_invariants
61
+ result
62
+ end
63
+
64
+ def write_main
65
+ check_invariants
66
+ # Nothing in this case
67
+ check_invariants
68
+ end
69
+
70
+ def parse_featured_line(line)
71
+ check_invariants
72
+ assume { line.is_a?(String) }
73
+
74
+ # Parse line in format: <id> <title>
75
+ # Title is optional, so we need to handle both cases
76
+ parts = line.strip.split(/\s+/, 2)
77
+ if parts.length >= 2
78
+ result = [parts[0], parts[1]]
79
+ else
80
+ result = [parts[0], nil]
81
+ end
82
+
83
+ verify { result.is_a?(Array) && result.length == 2 }
84
+ check_invariants
85
+ result
86
+ end
87
+
88
+ def get_post_title(post_id)
89
+ check_invariants
90
+ assume { post_id.is_a?(String) && !post_id.empty? }
91
+ assume { @repo.is_a?(Scriptorium::Repo) }
92
+
93
+ # Get the actual post title from metadata
94
+ post = Scriptorium::Post.new(@repo, post_id)
95
+
96
+ # Check if the post actually exists by looking for the meta file
97
+ if File.exist?(post.meta_file)
98
+ result = post.title || "Untitled Post"
99
+ else
100
+ result = "Error: Post #{post_id} not found"
101
+ end
102
+ rescue => e
103
+ # If post can't be created (invalid ID, etc.), return error message
104
+ result = "Error: Post #{post_id} not found"
105
+ ensure
106
+ verify { result.is_a?(String) && !result.empty? }
107
+ check_invariants
108
+ result
109
+ end
110
+
111
+ def featured_post_item(post_id, list_title = nil)
112
+ check_invariants
113
+ assume { post_id.is_a?(String) && !post_id.empty? }
114
+ assume { list_title.nil? || list_title.is_a?(String) }
115
+
116
+ # Use actual post title from metadata, show error if post doesn't exist
117
+ display_title = get_post_title(post_id)
118
+
119
+ # Create link to the post (or error message)
120
+ anchor = %[<a href="javascript:void(0)" onclick="load_main('posts/#{post_id}.html')" style="text-decoration: none;">#{display_title}</a>]
121
+ result = %[<li class="list-group-item">#{anchor}</li>]
122
+
123
+ verify { result.is_a?(String) && result.include?("list-group-item") }
124
+ check_invariants
125
+ result
126
+ end
127
+
128
+ def write_card
129
+ check_invariants
130
+ assume { @view.is_a?(Scriptorium::View) }
131
+ assume { @name.is_a?(String) && !@name.empty? }
132
+ assume { @data.is_a?(Array) }
133
+
134
+ tag = @name
135
+ cardout = "#{@view.dir}/widgets/#@name/#@name-card.html"
136
+ card_title = Title
137
+ content = ""
138
+
139
+ @data.each do |line|
140
+ next if line.strip.empty?
141
+
142
+ post_id, list_title = parse_featured_line(line)
143
+ content << featured_post_item(post_id, list_title)
144
+ end
145
+
146
+ write_file(cardout, html_card(card_title, tag, content))
147
+ check_invariants
148
+ end
149
+ end
@@ -0,0 +1,112 @@
1
+ class Scriptorium::Widget::Links < Scriptorium::Widget::ListWidget
2
+ include Scriptorium::Contract
3
+
4
+ Title = "External links"
5
+
6
+ # Invariants
7
+ def define_invariants
8
+ invariant { @lines.is_a?(Array) }
9
+ invariant { Title.is_a?(String) && !Title.empty? }
10
+ end
11
+
12
+ def initialize(repo, view)
13
+ assume { repo.is_a?(Scriptorium::Repo) }
14
+ assume { view.is_a?(Scriptorium::View) }
15
+
16
+ super(repo, view)
17
+ @lines = @data
18
+
19
+ define_invariants
20
+ verify { @lines == @data }
21
+ check_invariants
22
+ end
23
+
24
+ def get_list
25
+ check_invariants
26
+ assume { @lines.is_a?(Array) }
27
+
28
+ result = @lines.map { |line| line.chomp.split(/, */, 2) }
29
+
30
+ verify { result.is_a?(Array) }
31
+ check_invariants
32
+ result
33
+ end
34
+
35
+ def load_config
36
+ check_invariants
37
+ # No configuration needed for this widget
38
+ check_invariants
39
+ end
40
+
41
+ def generate
42
+ check_invariants
43
+ assume { @view.is_a?(Scriptorium::View) }
44
+
45
+ write_main
46
+ write_card
47
+ result = true
48
+
49
+ verify { result == true }
50
+ check_invariants
51
+ result
52
+ end
53
+
54
+ def widget_title
55
+ check_invariants
56
+ result = Title
57
+ verify { result.is_a?(String) && !result.empty? }
58
+ check_invariants
59
+ result
60
+ end
61
+
62
+ def card
63
+ check_invariants
64
+ assume { @view.is_a?(Scriptorium::View) }
65
+ assume { @name.is_a?(String) && !@name.empty? }
66
+
67
+ file = "#{@view.dir}/widgets/#@name/#@name-card.html"
68
+ result = read_file(file)
69
+
70
+ verify { result.is_a?(String) }
71
+ check_invariants
72
+ result
73
+ end
74
+
75
+ def write_main
76
+ check_invariants
77
+ # Nothing in this case
78
+ check_invariants
79
+ end
80
+
81
+ def link_item(url, title)
82
+ check_invariants
83
+ assume { url.is_a?(String) && !url.empty? }
84
+ assume { title.is_a?(String) && !title.empty? }
85
+
86
+ anchor = %[<a href="#{url}" target="_blank" style="text-decoration: none;">#{title}</a>]
87
+ result = %[<li class="list-group-item">#{anchor}</li>]
88
+
89
+ verify { result.is_a?(String) && result.include?("list-group-item") }
90
+ check_invariants
91
+ result
92
+ end
93
+
94
+ def write_card
95
+ check_invariants
96
+ assume { @view.is_a?(Scriptorium::View) }
97
+ assume { @name.is_a?(String) && !@name.empty? }
98
+ assume { @data.is_a?(Array) }
99
+
100
+ tag = @name
101
+ cardout = "#{@view.dir}/widgets/#@name/#@name-card.html"
102
+ # url = "../widgets/#@name/#@name-card.html"
103
+ card_title = Title
104
+ content = ""
105
+ @data.each do |line|
106
+ url2, title = line.chomp.split(",")
107
+ content << link_item(url2, title)
108
+ end
109
+ write_file(cardout, html_card(card_title, tag, content))
110
+ check_invariants
111
+ end
112
+ end
@@ -0,0 +1,133 @@
1
+ class Scriptorium::Widget::Pages < Scriptorium::Widget::ListWidget
2
+ include Scriptorium::Contract
3
+
4
+ Title = "Pages"
5
+ attr_reader :data
6
+
7
+ # Invariants
8
+ def define_invariants
9
+ invariant { @pages_dir.is_a?(String) && !@pages_dir.empty? }
10
+ invariant { @data.is_a?(Array) }
11
+ invariant { Title.is_a?(String) && !Title.empty? }
12
+ end
13
+
14
+ def initialize(repo, view)
15
+ assume { repo.is_a?(Scriptorium::Repo) }
16
+ assume { view.is_a?(Scriptorium::View) }
17
+
18
+ super(repo, view)
19
+ @pages_dir = "#{@view.dir}/pages"
20
+
21
+ define_invariants
22
+ verify { @pages_dir.include?("pages") }
23
+ check_invariants
24
+ end
25
+
26
+ def load_config
27
+ check_invariants
28
+ # No configuration needed for this widget
29
+ check_invariants
30
+ end
31
+
32
+ def generate
33
+ check_invariants
34
+ assume { @view.is_a?(Scriptorium::View) }
35
+
36
+ write_main
37
+ write_card
38
+ result = true
39
+
40
+ verify { result == true }
41
+ check_invariants
42
+ result
43
+ end
44
+
45
+ def widget_title
46
+ check_invariants
47
+ result = Title
48
+ verify { result.is_a?(String) && !result.empty? }
49
+ check_invariants
50
+ result
51
+ end
52
+
53
+ def card
54
+ check_invariants
55
+ assume { @view.is_a?(Scriptorium::View) }
56
+ assume { @name.is_a?(String) && !@name.empty? }
57
+
58
+ file = "#{@view.dir}/widgets/#@name/#@name-card.html"
59
+ result = read_file(file)
60
+
61
+ verify { result.is_a?(String) }
62
+ check_invariants
63
+ result
64
+ end
65
+
66
+ def write_main
67
+ check_invariants
68
+ # Nothing in this case
69
+ check_invariants
70
+ end
71
+
72
+ def extract_title_from_html(html_content)
73
+ check_invariants
74
+ assume { html_content.is_a?(String) }
75
+
76
+ # Try to extract title from <title> tag first
77
+ if match = html_content.match(/<title[^>]*>(.*?)<\/title>/i)
78
+ result = match[1].strip
79
+ elsif match = html_content.match(/<h1[^>]*>(.*?)<\/h1>/i)
80
+ # Fall back to first <h1> tag
81
+ result = match[1].strip
82
+ else
83
+ # Last resort: use filename
84
+ result = "Untitled"
85
+ end
86
+
87
+ verify { result.is_a?(String) && !result.empty? }
88
+ check_invariants
89
+ result
90
+ end
91
+
92
+ def page_item(filename, title)
93
+ check_invariants
94
+ assume { filename.is_a?(String) && !filename.empty? }
95
+ assume { title.is_a?(String) && !title.empty? }
96
+
97
+ anchor = %[<a href="javascript:void(0)" onclick="load_main('pages/#{filename}.html')" style="text-decoration: none;">#{title}</a>]
98
+ result = %[<li class="list-group-item">#{anchor}</li>]
99
+
100
+ verify { result.is_a?(String) && result.include?("list-group-item") }
101
+ check_invariants
102
+ result
103
+ end
104
+
105
+ def write_card
106
+ check_invariants
107
+ assume { @view.is_a?(Scriptorium::View) }
108
+ assume { @name.is_a?(String) && !@name.empty? }
109
+ assume { @data.is_a?(Array) }
110
+ assume { @pages_dir.is_a?(String) && !@pages_dir.empty? }
111
+
112
+ tag = @name
113
+ cardout = "#{@view.dir}/widgets/#@name/#@name-card.html"
114
+ card_title = Title
115
+ content = ""
116
+
117
+ @data.each do |filename|
118
+ html_file = "#{@pages_dir}/#{filename}.html"
119
+
120
+ if File.exist?(html_file)
121
+ html_content = read_file(html_file)
122
+ title = extract_title_from_html(html_content)
123
+ content << page_item(filename, title)
124
+ else
125
+ # Page doesn't exist, skip it
126
+ next
127
+ end
128
+ end
129
+
130
+ write_file(cardout, html_card(card_title, tag, content))
131
+ check_invariants
132
+ end
133
+ end
@@ -0,0 +1,133 @@
1
+ class Scriptorium::Widget
2
+ include Scriptorium::Helpers
3
+ include Scriptorium::Contract
4
+ attr_reader :repo, :view, :config, :name, :path
5
+
6
+ # Invariants
7
+ def define_invariants
8
+ invariant { @repo.is_a?(Scriptorium::Repo) }
9
+ invariant { @view.is_a?(Scriptorium::View) }
10
+ invariant { @name.is_a?(String) && !@name.empty? }
11
+ invariant { @path.is_a?(String) && !@path.empty? }
12
+ end
13
+
14
+ def initialize(repo, view)
15
+ assume { repo.is_a?(Scriptorium::Repo) }
16
+ assume { view.is_a?(Scriptorium::View) }
17
+
18
+ @repo = repo
19
+ @view = view
20
+ @config = load_config
21
+ @name = self.class.to_s.split("::").last.downcase
22
+ @path = "#{@view.dir}/widgets/#@name"
23
+
24
+ define_invariants
25
+ verify { @repo == repo }
26
+ verify { @view == view }
27
+ check_invariants
28
+ end
29
+
30
+ def generate
31
+ raise NotImplementedError, "Subclasses must implement the 'generate' method"
32
+ end
33
+
34
+ def load_config
35
+ raise NotImplementedError, "Subclasses must implement 'load_config'"
36
+ end
37
+
38
+ # Common HTML body wrapper for widgets
39
+ def html_body(css = nil)
40
+ check_invariants
41
+ assume { css.nil? || css.is_a?(String) }
42
+
43
+ result = "<html>" +
44
+ (css ? "<head><style>#{css}</style></head>" : "") +
45
+ "<body>" + yield + "</body></html>"
46
+
47
+ verify { result.is_a?(String) && result.include?("<html>") }
48
+ check_invariants
49
+ result
50
+ end
51
+
52
+ # Common HTML card structure for widgets like Links
53
+ def html_card(card_title = "Widget Card", tag = "widget", content)
54
+ check_invariants
55
+ assume { card_title.is_a?(String) }
56
+ assume { tag.is_a?(String) }
57
+ assume { content.is_a?(String) }
58
+
59
+ result = <<~EOS
60
+ <div class="card mb-3">
61
+ <div class="card-body">
62
+ <h5 class="card-title">
63
+ <button type="button" class="btn btn-primary" data-bs-toggle="collapse" data-bs-target="##{tag}">+</button>
64
+ <a href="javascript:void(0)" onclick="javascript:load_main('#{tag}-main.html')" style="text-decoration: none; color: black">#{card_title}</a>
65
+ </h5>
66
+ <div class="collapse" id="#{tag}">
67
+ #{content}
68
+ </div>
69
+ </div>
70
+ </div>
71
+ EOS
72
+
73
+ verify { result.is_a?(String) && result.include?("card") }
74
+ check_invariants
75
+ result
76
+ end
77
+
78
+ # A generic container for widget content (for those that don't use cards)
79
+ def html_container(content)
80
+ check_invariants
81
+ assume { content.is_a?(String) }
82
+
83
+ result = <<~HTML
84
+ <div class="widget-container">
85
+ #{content}
86
+ </div>
87
+ HTML
88
+
89
+ verify { result.is_a?(String) && result.include?("widget-container") }
90
+ check_invariants
91
+ result
92
+ end
93
+ end
94
+
95
+
96
+ ####
97
+
98
+
99
+ class Scriptorium::Widget::ListWidget < Scriptorium::Widget
100
+ include Scriptorium::Contract
101
+
102
+ # Invariants
103
+ def define_invariants
104
+ invariant { @list.is_a?(String) && !@list.empty? }
105
+ invariant { @data.is_a?(Array) }
106
+ end
107
+
108
+ def initialize(repo, view)
109
+ assume { repo.is_a?(Scriptorium::Repo) }
110
+ assume { view.is_a?(Scriptorium::View) }
111
+
112
+ super(repo, view)
113
+ @list = "#{@path}/list.txt"
114
+ @data = load_data
115
+
116
+ define_invariants
117
+ verify { @list.include?("list.txt") }
118
+ verify { @data.is_a?(Array) }
119
+ check_invariants
120
+ end
121
+
122
+ # Method to load the list data from the list.txt file
123
+ def load_data
124
+ check_invariants
125
+ assume { @list.is_a?(String) && !@list.empty? }
126
+
127
+ result = read_file(@list, lines: true, chomp: true).map(&:strip)
128
+
129
+ verify { result.is_a?(Array) }
130
+ check_invariants
131
+ result
132
+ end
133
+ end
data/lib/scriptorium.rb CHANGED
@@ -1,10 +1,23 @@
1
- require_relative "skeleton"
2
-
3
- class Scriptorium
1
+ require 'livetext'
2
+ require 'find'
3
+ require 'htmlbeautifier'
4
+ require 'pp'
4
5
 
5
- def initialize
6
- puts "Initializing... no code yet."
7
- puts
8
- end
9
-
10
- end
6
+ require_relative "skeleton"
7
+ require_relative "scriptorium/version"
8
+ require_relative "scriptorium/contract"
9
+ require_relative "scriptorium/repo"
10
+ require_relative "scriptorium/post"
11
+ require_relative "scriptorium/theme"
12
+ require_relative "scriptorium/view"
13
+ require_relative "scriptorium/exceptions"
14
+ require_relative "scriptorium/helpers"
15
+ require_relative "scriptorium/standard_files"
16
+ require_relative "scriptorium/banner_svg"
17
+ require_relative "scriptorium/reddit"
18
+ require_relative "scriptorium/widgets/widget"
19
+ require_relative "scriptorium/widgets/links"
20
+ require_relative "scriptorium/widgets/pages"
21
+ require_relative "scriptorium/widgets/featured_posts"
22
+ require_relative "scriptorium/api"
23
+ require_relative "scriptorium/syntax_highlighter"
data/lib/skeleton.rb CHANGED
@@ -1,6 +1,15 @@
1
- class Scriptorium
1
+ module Scriptorium
2
2
 
3
- class Engine
3
+ module Exceptions
4
+ end
5
+
6
+ class Repo
7
+ end
8
+
9
+ class View
10
+ end
11
+
12
+ module Helpers
4
13
  end
5
14
 
6
15
  class UI
data/scriptorium.gemspec CHANGED
@@ -21,23 +21,34 @@ spec = Gem::Specification.new do |s|
21
21
  s.authors = ["Hal Fulton"]
22
22
  s.email = 'rubyhacker@gmail.com'
23
23
  s.executables << "sblog"
24
+ s.executables << "scriptorium"
24
25
 
25
26
  s.add_runtime_dependency 'livetext', '~> 0.9', '>= 0.9.41'
26
27
  s.add_runtime_dependency 'rubytext', '~> 0.1', '>= 0.1.26'
27
28
  s.add_runtime_dependency 'rouge', '~> 3.25', '>= 3.25.0'
29
+ s.add_runtime_dependency 'sinatra', '~> 3.0', '>= 3.0.0'
30
+ s.add_runtime_dependency 'redd', '~> 0.8.8'
31
+ s.add_runtime_dependency 'htmlbeautifier', '~> 1.4', '>= 1.4.0'
32
+ s.add_runtime_dependency 'clipboard', '~> 1.3', '>= 1.3.6'
28
33
 
29
34
  s.add_development_dependency 'minitest', '~> 5.10', '>= 5.10.0'
30
35
 
31
36
  # Files...
32
37
 
33
- main = Find.find("bin").to_a +
34
- Find.find("lib").to_a +
35
- Find.find("doc").to_a
38
+ # Core library files
39
+ main = Find.find("lib").to_a +
40
+ Find.find("doc").to_a +
41
+ Find.find("assets").to_a
42
+
43
+ # UI-specific files
44
+ ui_files = Find.find("ui").to_a
45
+
46
+ # Test files
36
47
  test = Find.find("test").to_a
37
48
 
38
49
  misc = %w[./README.lt3 ./README.md ./scriptorium.gemspec]
39
50
 
40
- s.files = main + misc + test
51
+ s.files = main + ui_files + misc + test
41
52
  s.homepage = 'https://github.com/Hal9000/scriptorium'
42
53
  s.license = "Ruby"
43
54
  s.post_install_message =
data/test/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # Scriptorium Test Suite
2
+
3
+ ## Quick Start
4
+
5
+ ### Run All Tests (Unit + Automated Manual Tests)
6
+ ```bash
7
+ ruby test/run_automated_tests.rb
8
+ ```
9
+
10
+ ### Run Unit Tests Only
11
+ ```bash
12
+ ruby test/all
13
+ ```
14
+
15
+ ### Run Individual Manual Tests
16
+ ```bash
17
+ # Interactive mode (opens browser)
18
+ ruby test/manual/test1.rb
19
+
20
+ # Automated mode (no browser, just validation)
21
+ ruby test/manual/test1.rb --automated
22
+ ```
23
+
24
+ ## Test Types
25
+
26
+ ### Unit Tests (`test/unit/`)
27
+ - **Core functionality tests** - Basic helper methods, file operations
28
+ - **Repo tests** - Repository creation, management, post handling
29
+ - **Post tests** - Individual post functionality
30
+ - **View tests** - View generation and management
31
+ - **Banner SVG tests** - SVG banner generation and validation
32
+
33
+ ### Manual Tests (`test/manual/`)
34
+ - **Integration tests** - End-to-end workflows
35
+ - **Banner feature tests** - Visual verification of SVG banners
36
+ - **Widget tests** - Sidebar widgets and functionality
37
+ - **Layout tests** - Page layout and styling
38
+
39
+ ## Automated Testing
40
+
41
+ All manual tests support `--automated` mode for CI/CD:
42
+ - No browser interaction required
43
+ - Validates file generation
44
+ - Returns exit codes for automation
45
+ - Use `--automated` as the **last** parameter
46
+
47
+ ## Environment Setup
48
+
49
+ The test environment is automatically configured:
50
+ - PATH is set to include rbenv shims
51
+ - Test repositories are created in `test/scriptorium-TEST/`
52
+ - Webrick server starts on port 8000 for manual tests
53
+ - Cleanup happens automatically
54
+
55
+ ## Examples
56
+
57
+ ```bash
58
+ # Run all tests
59
+ ruby test/run_automated_tests.rb
60
+
61
+ # Run specific manual test interactively
62
+ ruby test/manual/test_banner_combinations.rb
63
+
64
+ # Run specific manual test in automated mode
65
+ ruby test/manual/test_banner_combinations.rb --automated
66
+
67
+ # Run test3 with specific view
68
+ ruby test/manual/test3.rb blog1 --automated
69
+ ```