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,742 @@
1
+ class Scriptorium::BannerSVG
2
+ include Scriptorium::Helpers
3
+ include Scriptorium::Exceptions
4
+ include Scriptorium::Contract
5
+
6
+ # Invariants
7
+ def define_invariants
8
+ invariant { @title.is_a?(String) }
9
+ invariant { @subtitle.is_a?(String) }
10
+ invariant { @title_scale.is_a?(Numeric) && @title_scale > 0 }
11
+ invariant { @subtitle_scale.is_a?(Numeric) && @subtitle_scale > 0 }
12
+ invariant { @aspect.is_a?(Numeric) && @aspect > 0 }
13
+ invariant { @font.is_a?(String) && !@font.empty? }
14
+ invariant { @text_color.is_a?(String) && !@text_color.empty? }
15
+ invariant { @background.is_a?(String) && !@background.empty? }
16
+ end
17
+
18
+ def initialize(title, subtitle)
19
+ assume { title.is_a?(String) }
20
+ assume { subtitle.is_a?(String) }
21
+
22
+ @title, @subtitle = title, subtitle
23
+ @title_scale = 0.8
24
+ @subtitle_scale = 0.4
25
+ @title_style = "normal"
26
+ @subtitle_style = "normal"
27
+ @title_weight = "normal"
28
+ @subtitle_weight = "normal"
29
+ @text_color = "#374151"
30
+ @text_anchor = "start"
31
+ @aspect = 8.0
32
+ @font = "Verdana"
33
+ # Remove default @title_xy and @subtitle_xy
34
+ @background = "#fff"
35
+ @gradient_start_color = nil
36
+ @gradient_end_color = nil
37
+ @gradient_direction = nil
38
+ @radial_start_color = nil
39
+ @radial_end_color = nil
40
+ @image_background = nil
41
+ @title_xy_set = false
42
+ @subtitle_xy_set = false
43
+
44
+ define_invariants
45
+ verify { @title == title }
46
+ verify { @subtitle == subtitle }
47
+ check_invariants
48
+ end
49
+
50
+
51
+
52
+ def handle_background(*args)
53
+ check_invariants
54
+ assume { args.is_a?(Array) }
55
+
56
+ validate_background_args(args)
57
+ @background = args.first
58
+
59
+ verify { @background.is_a?(String) && !@background.empty? }
60
+ check_invariants
61
+ end
62
+
63
+ private def validate_background_args(args)
64
+ raise CannotHandleBackgroundNoArgs if args.nil? || args.empty?
65
+
66
+ raise CannotHandleBackgroundFirstArgNil if args.first.nil?
67
+
68
+ raise CannotHandleBackgroundFirstArgEmpty if args.first.to_s.strip.empty?
69
+ end
70
+
71
+ def handle_linear_gradient(*args)
72
+ validate_linear_gradient_args(args)
73
+ @gradient_start_color = args[0]
74
+ @gradient_end_color = args[1]
75
+ @gradient_direction = args[2] || "lr"
76
+ end
77
+
78
+ private def validate_linear_gradient_args(args)
79
+ raise CannotHandleLinearGradientNoArgs if args.nil? || args.empty?
80
+
81
+ raise CannotHandleLinearGradientStartColorNil if args[0].nil? || args[0].to_s.strip.empty?
82
+
83
+ # Validate all provided arguments (up to 3: start_color, end_color, direction)
84
+ args.each_with_index do |arg, index|
85
+ next if arg.nil? # Allow nil for optional arguments
86
+ raise CannotHandleLinearGradientArgEmpty(index + 1) if arg.to_s.strip.empty?
87
+ end
88
+ end
89
+
90
+ def handle_radial_gradient(*args)
91
+ validate_radial_gradient_args(args)
92
+ @radial_start_color = args[0]
93
+ @radial_end_color = args[1]
94
+ # Optional: cx, cy, r
95
+ @radial_cx = args[2] || '50%'
96
+ @radial_cy = args[3] || '50%'
97
+ @radial_r = args[4] || '50%'
98
+ # 6th param: aspect ratio compensation for gradientTransform
99
+ @radial_ar = args[5] ? args[5].to_f : nil
100
+ end
101
+
102
+ private def validate_radial_gradient_args(args)
103
+ raise CannotHandleRadialGradientNoArgs if args.nil? || args.empty?
104
+
105
+ raise CannotHandleRadialGradientStartColorNil if args[0].nil? || args[0].to_s.strip.empty?
106
+
107
+ # Validate all provided arguments (up to 6: start_color, end_color, cx, cy, r, aspect_ratio)
108
+ args.each_with_index do |arg, index|
109
+ next if arg.nil? # Allow nil for optional arguments
110
+ raise CannotHandleRadialGradientArgEmpty(index + 1) if arg.to_s.strip.empty?
111
+ end
112
+ end
113
+
114
+ # Image backgrounds: Users should provide images matching the banner's aspect ratio.
115
+ # SVG will crop/stretch if aspect ratios don't match (use preserveAspectRatio="xMidYMid slice" for cropping).
116
+ def handle_image_background(*args)
117
+ validate_image_background_args(args)
118
+ @image_background = args[0]
119
+ end
120
+
121
+ private def validate_image_background_args(args)
122
+ raise CannotHandleImageBackgroundNoArgs if args.nil? || args.empty?
123
+
124
+ raise CannotHandleImageBackgroundFirstArgNil if args[0].nil?
125
+
126
+ raise CannotHandleImageBackgroundFirstArgEmpty if args[0].to_s.strip.empty?
127
+ end
128
+
129
+ def handle_aspect(*args)
130
+ check_invariants
131
+ assume { args.is_a?(Array) }
132
+
133
+ validate_aspect_args(args)
134
+ @aspect = args.first.to_f
135
+
136
+ verify { @aspect.is_a?(Numeric) && @aspect > 0 }
137
+ check_invariants
138
+ end
139
+
140
+ private def validate_aspect_args(args)
141
+ raise CannotHandleAspectNoArgs if args.nil? || args.empty?
142
+
143
+ raise CannotHandleAspectFirstArgNil if args.first.nil?
144
+
145
+ raise CannotHandleAspectFirstArgEmpty if args.first.to_s.strip.empty?
146
+
147
+ unless args.first.to_s.match?(/^\d+(\.\d+)?$/)
148
+ raise CannotHandleAspectInvalidValue(args.first)
149
+ end
150
+ end
151
+
152
+ def handle_preserve_aspect(*args)
153
+ @preserve_aspect = args.first
154
+ end
155
+
156
+ def handle_font(*args)
157
+ validate_font_args(args)
158
+ @font = args.join(" ")
159
+ end
160
+
161
+ private def validate_font_args(args)
162
+ raise CannotHandleFontArgsNil if args.nil?
163
+
164
+ # Font arguments are optional - empty args array is allowed
165
+ # But if any arguments are provided, they must be valid
166
+ args.each_with_index do |arg, index|
167
+ raise CannotHandleFontArgNil(index + 1) if arg.nil?
168
+
169
+ raise CannotHandleFontArgEmpty(index + 1) if arg.to_s.strip.empty?
170
+ end
171
+ end
172
+
173
+ def handle_text_color(*args)
174
+ validate_text_color_args(args)
175
+ @text_color = args.first
176
+ end
177
+
178
+ private def validate_text_color_args(args)
179
+ raise CannotHandleTextColorNoArgs if args.nil? || args.empty?
180
+
181
+ raise CannotHandleTextColorFirstArgNil if args.first.nil?
182
+
183
+ raise CannotHandleTextColorFirstArgEmpty if args.first.to_s.strip.empty?
184
+ end
185
+
186
+ def handle_text_align(*args)
187
+ direction = args[0]
188
+ # Apply to both title and subtitle
189
+ handle_title_align(*args)
190
+ handle_subtitle_align(*args)
191
+ end
192
+
193
+ def handle_scale(which, *args)
194
+ check_invariants
195
+ assume { which.is_a?(String) && !which.empty? }
196
+ assume { args.is_a?(Array) }
197
+
198
+ if which == "title"
199
+ @title_scale = args.first.to_f
200
+ verify { @title_scale.is_a?(Numeric) && @title_scale > 0 }
201
+ elsif which == "subtitle"
202
+ @subtitle_scale = args.first.to_f
203
+ verify { @subtitle_scale.is_a?(Numeric) && @subtitle_scale > 0 }
204
+ end
205
+
206
+ check_invariants
207
+ end
208
+
209
+ def handle_style(which, *args)
210
+ args.each do |arg|
211
+ case
212
+ when which == "title" && arg =~ /bold/i
213
+ @title_weight = "bold"
214
+ when which == "title" && arg =~ /italic/i
215
+ @title_style = "italic"
216
+ when which == "subtitle" && arg =~ /bold/i
217
+ @subtitle_weight = "bold"
218
+ when which == "subtitle" && arg =~ /italic/i
219
+ @subtitle_style = "italic"
220
+ else
221
+ @title_style = arg
222
+ @subtitle_style = arg
223
+ end
224
+ end
225
+ end
226
+
227
+ def handle_xy(which, *args)
228
+ validate_xy_which(which)
229
+
230
+ if which == "title"
231
+ @title_xy = args
232
+ @title_xy_set = true
233
+ elsif which == "subtitle"
234
+ @subtitle_xy = args
235
+ @subtitle_xy_set = true
236
+ end
237
+ end
238
+
239
+ private def validate_xy_which(which)
240
+ raise CannotHandleXYWhichNil if which.nil?
241
+
242
+ raise CannotHandleXYWhichEmpty if which.to_s.strip.empty?
243
+
244
+ unless ["title", "subtitle"].include?(which)
245
+ raise CannotHandleXYInvalidWhich(which)
246
+ end
247
+ end
248
+
249
+ private def validate_align_args(args)
250
+ raise CannotHandleAlignNoArgs if args.nil? || args.empty?
251
+
252
+ raise CannotHandleAlignDirectionNil if args[0].nil? || args[0].to_s.strip.empty?
253
+
254
+ unless ["left", "center", "right"].include?(args[0])
255
+ raise CannotHandleAlignInvalidDirection(args[0])
256
+ end
257
+
258
+ # Validate optional x and y arguments if provided
259
+ args.each_with_index do |arg, index|
260
+ next if index == 0 # Skip direction (already validated)
261
+ next if arg.nil? # Allow nil for optional arguments
262
+ raise CannotHandleAlignArgEmpty(index + 1) if arg.to_s.strip.empty?
263
+ end
264
+ end
265
+
266
+ def handle_title_align(*args)
267
+ validate_align_args(args)
268
+ direction = args[0]
269
+ x = args[1]
270
+ y = args[2]
271
+ @title_align = direction
272
+ @title_align_x = x
273
+ @title_align_y = y
274
+ # Smart default for x if 'auto'
275
+ if x == 'auto' || x.nil?
276
+ @title_align_x = case direction
277
+ when 'left' then '5%'
278
+ when 'center' then '50%'
279
+ when 'right' then '95%'
280
+ else '5%'
281
+ end
282
+ end
283
+ # Warn if direction and x seem incompatible
284
+ if direction == 'center' && @title_align_x !~ /^50%$/
285
+ warn "[BannerSVG] Warning: title.align center with x=#{@title_align_x} may not be visually centered."
286
+ elsif direction == 'left' && @title_align_x !~ /^5%$/
287
+ warn "[BannerSVG] Warning: title.align left with x=#{@title_align_x} may not be visually left-aligned."
288
+ elsif direction == 'right' && @title_align_x !~ /^95%$/
289
+ warn "[BannerSVG] Warning: title.align right with x=#{@title_align_x} may not be visually right-aligned."
290
+ end
291
+ # Set anchor
292
+ @title_text_anchor = case direction
293
+ when 'left' then 'start'
294
+ when 'center' then 'middle'
295
+ when 'right' then 'end'
296
+ else 'start'
297
+ end
298
+ # Set y if provided
299
+ @title_align_y = y if y
300
+ end
301
+
302
+ def handle_subtitle_align(*args)
303
+ validate_align_args(args)
304
+ direction = args[0]
305
+ x = args[1]
306
+ y = args[2]
307
+ @subtitle_align = direction
308
+ @subtitle_align_x = x
309
+ @subtitle_align_y = y
310
+ if x == 'auto' || x.nil?
311
+ @subtitle_align_x = case direction
312
+ when 'left' then '5%'
313
+ when 'center' then '50%'
314
+ when 'right' then '95%'
315
+ else '5%'
316
+ end
317
+ end
318
+ if direction == 'center' && @subtitle_align_x !~ /^50%$/
319
+ warn "[BannerSVG] Warning: subtitle.align center with x=#{@subtitle_align_x} may not be visually centered."
320
+ elsif direction == 'left' && @subtitle_align_x !~ /^5%$/
321
+ warn "[BannerSVG] Warning: subtitle.align left with x=#{@subtitle_align_x} may not be visually left-aligned."
322
+ elsif direction == 'right' && @subtitle_align_x !~ /^95%$/
323
+ warn "[BannerSVG] Warning: subtitle.align right with x=#{@subtitle_align_x} may not be visually right-aligned."
324
+ end
325
+ @subtitle_text_anchor = case direction
326
+ when 'left' then 'start'
327
+ when 'center' then 'middle'
328
+ when 'right' then 'end'
329
+ else 'start'
330
+ end
331
+ @subtitle_align_y = y if y
332
+ end
333
+
334
+ def handle_title_color(*args)
335
+ validate_color_args(args)
336
+ @title_color = args.first
337
+ end
338
+
339
+ def handle_subtitle_color(*args)
340
+ validate_color_args(args)
341
+ @subtitle_color = args.first
342
+ end
343
+
344
+
345
+
346
+ private def validate_color_args(args)
347
+ raise CannotHandleColorNoArgs if args.nil? || args.empty?
348
+
349
+ raise CannotHandleColorFirstArgNil if args.first.nil?
350
+
351
+ raise CannotHandleColorFirstArgEmpty if args.first.to_s.strip.empty?
352
+ end
353
+
354
+ def parse_header_svg(config_file = "config.txt")
355
+ check_invariants
356
+ assume { config_file.is_a?(String) && !config_file.empty? }
357
+
358
+ lines = read_commented_file(config_file)
359
+
360
+ # Parse config into a hash
361
+ cfg = {}
362
+ lines.each do |line|
363
+ key, *values = line.split(/\s+/)
364
+ cfg[key.strip] = Array(values) if key && values
365
+ end
366
+
367
+ # Use instance variables instead of local variables
368
+ handlers = {
369
+ "back.color" => ->(args) { handle_background(*args) },
370
+ "back.linear" => ->(args) { handle_linear_gradient(*args) },
371
+ "back.radial" => ->(args) { handle_radial_gradient(*args) },
372
+ "back.image" => ->(args) { handle_image_background(*args) },
373
+ "aspect" => ->(args) { handle_aspect(*args) },
374
+ "preserve_aspect" => ->(args) { handle_preserve_aspect(*args) },
375
+ "text.font" => ->(args) { handle_font(*args) },
376
+ "text.color" => ->(args) { handle_text_color(*args) },
377
+ "title.color" => ->(args) { handle_title_color(*args) },
378
+ "subtitle.color" => ->(args) { handle_subtitle_color(*args) },
379
+
380
+ "title.align" => ->(args) { handle_title_align(*args) },
381
+ "subtitle.align" => ->(args) { handle_subtitle_align(*args) },
382
+ "title.scale" => ->(args) { handle_scale("title", *args) },
383
+ "subtitle.scale" => ->(args) { handle_scale("subtitle", *args) },
384
+ "title.style" => ->(args) { handle_style("title", *args) },
385
+ "subtitle.style" => ->(args) { handle_style("subtitle", *args) },
386
+ "title.xy" => ->(args) { handle_xy("title", *args) },
387
+ "subtitle.xy" => ->(args) { handle_xy("subtitle", *args) },
388
+ "text.align" => ->(args) { handle_text_align(*args) }
389
+ }
390
+
391
+ cfg.each_pair do |key, args|
392
+ handler = handlers[key]
393
+ if handler
394
+ # Skip malformed lines (empty args) to avoid validation errors
395
+ next if args.nil? || args.empty?
396
+ handler.call(args)
397
+ end
398
+ end
399
+
400
+ # Check for align/xy conflicts and warn
401
+ # Note: xy coordinates take precedence over align coordinates when both are set
402
+ if @title_align && @title_xy && @title_align_x && @title_xy[0] && @title_align_x != @title_xy[0]
403
+ warn "[BannerSVG] Warning: title.align x=#{@title_align_x} conflicts with title.xy x=#{@title_xy[0]} (xy will override)"
404
+ end
405
+ if @subtitle_align && @subtitle_xy && @subtitle_align_x && @subtitle_xy[0] && @subtitle_align_x != @subtitle_xy[0]
406
+ warn "[BannerSVG] Warning: subtitle.align x=#{@subtitle_align_x} conflicts with subtitle.xy x=#{@subtitle_xy[0]} (xy will override)"
407
+ end
408
+
409
+ # Set base font size
410
+ base_font_size = 60
411
+ title_font_size = (base_font_size * @title_scale).to_i
412
+ subtitle_font_size = (base_font_size * @subtitle_scale).to_i
413
+
414
+ width = 800 # Arbitrary starting point for calculations
415
+ height = (width / @aspect).to_i # height calculated based on aspect ratio
416
+
417
+ # Handle background (image, radial gradient, linear gradient, or solid color)
418
+ background_svg = ""
419
+ if @image_background
420
+ # Generate image background
421
+ background_svg = <<~IMAGE
422
+ <defs>
423
+ <pattern id="bg-pattern" x="0" y="0" width="100%" height="100%" patternUnits="objectBoundingBox">
424
+ <image href="#{@image_background}" x="0" y="0" width="100%" height="100%"
425
+ preserveAspectRatio="xMidYMid slice" />
426
+ </pattern>
427
+ </defs>
428
+ <rect x='0' y='0' width='100%' height='100%' fill='url(#bg-pattern)' />
429
+ IMAGE
430
+ elsif @radial_start_color && @radial_end_color
431
+ # Generate radial gradient
432
+ background_svg = <<~RADIAL
433
+ <defs>
434
+ <radialGradient id="radial1" cx="50%" cy="50%" r="50%">
435
+ <stop offset="0%" style="stop-color:#{@radial_start_color};stop-opacity:1" />
436
+ <stop offset="100%" style="stop-color:#{@radial_end_color};stop-opacity:1" />
437
+ </radialGradient>
438
+ </defs>
439
+ <rect x='0' y='0' width='100%' height='100%' fill='url(#radial1)' />
440
+ RADIAL
441
+ elsif @gradient_start_color && @gradient_end_color
442
+ # Generate linear gradient
443
+ directions = {
444
+ "lr" => ["0%", "0%", "100%", "0%"],
445
+ "tb" => ["0%", "0%", "0%", "100%"],
446
+ "ul-lr" => ["0%", "0%", "100%", "100%"],
447
+ "ll-ur" => ["0%", "100%", "100%", "0%"]
448
+ }
449
+
450
+ direction_coords = directions[@gradient_direction] || directions["lr"]
451
+ x1, y1, x2, y2 = direction_coords
452
+
453
+ background_svg = <<~GRADIENT
454
+ <defs>
455
+ <linearGradient id="grad1" x1="#{x1}" y1="#{y1}" x2="#{x2}" y2="#{y2}">
456
+ <stop offset="0%" style="stop-color:#{@gradient_start_color};stop-opacity:1" />
457
+ <stop offset="100%" style="stop-color:#{@gradient_end_color};stop-opacity:1" />
458
+ </linearGradient>
459
+ </defs>
460
+ <rect x='0' y='0' width='100%' height='100%' fill='url(#grad1)' />
461
+ GRADIENT
462
+ else
463
+ # Solid color background
464
+ background_svg = "<rect x='0' y='0' width='100%' height='100%' fill='#{@background}' />"
465
+ end
466
+
467
+ # Build style strings
468
+ title_style = "font-family: #{@font}; "
469
+ title_style << "font-size: #{title_font_size}px; "
470
+ title_style << "font-weight: #{@title_weight}; "
471
+ title_style << "font-style: #{@title_style}"
472
+
473
+ subtitle_style = "font-family: #{@font}; "
474
+ subtitle_style << "font-size: #{subtitle_font_size}px; "
475
+ subtitle_style << "font-weight: #{@subtitle_weight}; "
476
+ subtitle_style << "font-style: #{@subtitle_style}"
477
+
478
+ # Get xy coordinates if set, otherwise use alignment fallbacks
479
+ if @title_xy_set && @title_xy
480
+ title_x = @title_xy[0]
481
+ title_y = @title_xy[1]
482
+ else
483
+ title_x = @title_align_x || '5%'
484
+ title_y = @title_align_y || '52%'
485
+ end
486
+ if @subtitle_xy_set && @subtitle_xy
487
+ subtitle_x = @subtitle_xy[0]
488
+ subtitle_y = @subtitle_xy[1]
489
+ else
490
+ subtitle_x = @subtitle_align_x || '5%'
491
+ subtitle_y = @subtitle_align_y || '82%'
492
+ end
493
+
494
+ title_svg = <<~EOS
495
+ <text x='#{title_x}'
496
+ y='#{title_y}'
497
+ text-anchor='#{@text_anchor}'
498
+ style='#{title_style}'
499
+ fill='#{@text_color}'>#@title</text>
500
+ EOS
501
+
502
+ # Call generate_svg to return the complete SVG
503
+ generate_svg
504
+ end
505
+
506
+ def generate_svg
507
+ check_invariants
508
+
509
+ # Set base font size
510
+ base_font_size = 60
511
+ title_font_size = (base_font_size * @title_scale).to_i
512
+ subtitle_font_size = (base_font_size * @subtitle_scale).to_i
513
+
514
+ width = 800 # Arbitrary starting point for calculations
515
+ height = (width / @aspect).to_i # height calculated based on aspect ratio
516
+
517
+ # Handle background (image, radial gradient, linear gradient, or solid color)
518
+ background_svg = ""
519
+ if @image_background
520
+ # Generate image background
521
+ background_svg = <<~IMAGE
522
+ <defs>
523
+ <pattern id="bg-pattern" x="0" y="0" width="100%" height="100%" patternUnits="objectBoundingBox">
524
+ <image href="#{@image_background}" x="0" y="0" width="100%" height="100%"
525
+ preserveAspectRatio="xMidYMid slice" />
526
+ </pattern>
527
+ </defs>
528
+ <rect x='0' y='0' width='100%' height='100%' fill='url(#bg-pattern)' />
529
+ IMAGE
530
+ elsif @radial_start_color && @radial_end_color
531
+ # Calculate aspect ratio compensation for gradientTransform
532
+ ar = @radial_ar || (1.0 / @aspect)
533
+ # Compensate cx for X scaling so that cx visually matches the intended center
534
+ cx_val = @radial_cx
535
+ if cx_val.is_a?(String) && cx_val.strip.end_with?('%')
536
+ cx_num = cx_val.strip.chomp('%').to_f
537
+ cx_val = (cx_num / ar).to_s + '%'
538
+ end
539
+ gradient_transform = "gradientTransform=\"scale(#{ar},1)\"" if ar
540
+ background_svg = <<~RADIAL
541
+ <defs>
542
+ <radialGradient id="radial1" cx="#{cx_val}" cy="#{@radial_cy}" r="#{@radial_r}" #{gradient_transform}>
543
+ <stop offset="0%" style="stop-color:#{@radial_start_color};stop-opacity:1" />
544
+ <stop offset="100%" style="stop-color:#{@radial_end_color};stop-opacity:1" />
545
+ </radialGradient>
546
+ </defs>
547
+ <rect x='0' y='0' width='100%' height='100%' fill='url(#radial1)' />
548
+ RADIAL
549
+ elsif @gradient_start_color && @gradient_end_color
550
+ # Generate linear gradient
551
+ directions = {
552
+ "lr" => ["0%", "0%", "100%", "0%"],
553
+ "tb" => ["0%", "0%", "0%", "100%"],
554
+ "ul-lr" => ["0%", "0%", "100%", "100%"],
555
+ "ll-ur" => ["0%", "100%", "100%", "0%"]
556
+ }
557
+
558
+ direction_coords = directions[@gradient_direction] || directions["lr"]
559
+ x1, y1, x2, y2 = direction_coords
560
+
561
+ background_svg = <<~GRADIENT
562
+ <defs>
563
+ <linearGradient id="grad1" x1="#{x1}" y1="#{y1}" x2="#{x2}" y2="#{y2}">
564
+ <stop offset="0%" style="stop-color:#{@gradient_start_color};stop-opacity:1" />
565
+ <stop offset="100%" style="stop-color:#{@gradient_end_color};stop-opacity:1" />
566
+ </linearGradient>
567
+ </defs>
568
+ <rect x='0' y='0' width='100%' height='100%' fill='url(#grad1)' />
569
+ GRADIENT
570
+ else
571
+ # Solid color background
572
+ background_svg = "<rect x='0' y='0' width='100%' height='100%' fill='#{@background}' />"
573
+ end
574
+
575
+ # Build style strings
576
+ title_style = "font-family: #{@font}; "
577
+ title_style << "font-size: #{title_font_size}px; "
578
+ title_style << "font-weight: #{@title_weight}; "
579
+ title_style << "font-style: #{@title_style}"
580
+
581
+ subtitle_style = "font-family: #{@font}; "
582
+ subtitle_style << "font-size: #{subtitle_font_size}px; "
583
+ subtitle_style << "font-weight: #{@subtitle_weight}; "
584
+ subtitle_style << "font-style: #{@subtitle_style}"
585
+
586
+ title_color = @title_color || @text_color
587
+ subtitle_color = @subtitle_color || @text_color
588
+
589
+ # Get xy coordinates if set, otherwise use alignment fallbacks
590
+ if @title_xy_set && @title_xy
591
+ title_x = @title_xy[0]
592
+ title_y = @title_xy[1]
593
+ else
594
+ title_x = @title_align_x || '5%'
595
+ title_y = @title_align_y || '52%'
596
+ end
597
+
598
+ if @subtitle_xy_set && @subtitle_xy
599
+ subtitle_x = @subtitle_xy[0]
600
+ subtitle_y = @subtitle_xy[1]
601
+ else
602
+ subtitle_x = @subtitle_align_x || '5%'
603
+ subtitle_y = @subtitle_align_y || '82%'
604
+ end
605
+
606
+ title_anchor = @title_text_anchor || @text_anchor
607
+ subtitle_anchor = @subtitle_text_anchor || @text_anchor
608
+
609
+ title_svg = <<~EOS
610
+ <text x='#{title_x}'
611
+ y='#{title_y}'
612
+ text-anchor='#{title_anchor}'
613
+ style='#{title_style}'
614
+ fill='#{title_color}'>#@title</text>
615
+ EOS
616
+ subtitle_svg = <<~EOS
617
+ <text x='#{subtitle_x}'
618
+ y='#{subtitle_y}'
619
+ text-anchor='#{subtitle_anchor}'
620
+ style='#{subtitle_style}'
621
+ fill='#{subtitle_color}'>#@subtitle</text>
622
+ EOS
623
+
624
+ # Define the SVG output
625
+ # Use different preserveAspectRatio for radial gradients to maintain circular shape
626
+ preserve_aspect = if @radial_start_color && @radial_end_color && @preserve_aspect
627
+ @preserve_aspect
628
+ elsif @radial_start_color && @radial_end_color
629
+ 'xMidYMid slice' # Default for radial gradients: crop to maintain aspect ratio
630
+ else
631
+ 'xMidYMid meet' # Default for other backgrounds: fit within bounds
632
+ end
633
+
634
+ svg = <<~SVG
635
+ <svg xmlns='http://www.w3.org/2000/svg'
636
+ width='100%' height='#{height}'
637
+ viewBox='0 0 #{width} #{height}'
638
+ preserveAspectRatio='#{preserve_aspect}'>
639
+ #{background_svg}
640
+ #{title_svg}
641
+ #{subtitle_svg}
642
+ </svg>
643
+ SVG
644
+
645
+ svg
646
+ end
647
+
648
+ def get_svg
649
+ check_invariants
650
+
651
+ # Generate SVG without re-parsing config (use current instance variables)
652
+ svg_code = generate_svg
653
+ svg_lines = svg_code.split("\n").map {|line| " "*6 + line }
654
+ svg_code = svg_lines.join("\n")
655
+
656
+ # Calculate coordinates safely
657
+ title_x = @title_xy_set && @title_xy ? @title_xy[0] : (@title_align_x || '5%')
658
+ title_y = @title_xy_set && @title_xy ? @title_xy[1] : (@title_align_y || '52%')
659
+ subtitle_x = @subtitle_xy_set && @subtitle_xy ? @subtitle_xy[0] : (@subtitle_align_x || '5%')
660
+ subtitle_y = @subtitle_xy_set && @subtitle_xy ? @subtitle_xy[1] : (@subtitle_align_y || '82%')
661
+
662
+ code = <<~EOS
663
+ <script>
664
+ function insert_svg_header(container) {
665
+ const svg_text = `#{svg_code}`;
666
+ const svgElement = document.createElement('div');
667
+ svgElement.innerHTML = svg_text;
668
+ const svg = svgElement.firstElementChild;
669
+
670
+ const svgWidth = window.innerWidth;
671
+ const aspectRatio = #{@aspect};
672
+ const svgHeight = svgWidth / aspectRatio;
673
+
674
+ svg.setAttribute('viewBox', `0 0 ${svgWidth} ${svgHeight}`);
675
+ svg.setAttribute('width', svgWidth);
676
+ svg.setAttribute('height', svgHeight);
677
+
678
+ const titleScale = #{@title_scale};
679
+ const subtitleScale = #{@subtitle_scale};
680
+
681
+ const base_font_size = 60;
682
+ const titleFontSize = titleScale * base_font_size;
683
+ const subtitleFontSize = subtitleScale * base_font_size;
684
+
685
+ const te1 = svg.querySelector('text:nth-of-type(1)')
686
+ const te2 = svg.querySelector('text:nth-of-type(2)')
687
+
688
+ // Don't override the styles - they're already set correctly in the SVG
689
+ // Just update the positioning and text-anchor
690
+
691
+ const titleXpct = "#{title_x}";
692
+ const titleYpct = "#{title_y}";
693
+ const subtitleXpct = "#{subtitle_x}";
694
+ const subtitleYpct = "#{subtitle_y}";
695
+
696
+ const tX = svgWidth * (parseFloat(titleXpct) / 100);
697
+ const tY = svgHeight * (parseFloat(titleYpct) / 100);
698
+ const sX = svgWidth * (parseFloat(subtitleXpct) / 100);
699
+ const sY = svgHeight * (parseFloat(subtitleYpct) / 100);
700
+
701
+ te1.setAttribute('x', tX);
702
+ te1.setAttribute('y', tY);
703
+ te2.setAttribute('x', sX);
704
+ te2.setAttribute('y', sY);
705
+
706
+ // Set text-anchor for proper positioning (use individual anchors if set)
707
+ te1.setAttribute('text-anchor', '#{@title_text_anchor || @text_anchor}');
708
+ te2.setAttribute('text-anchor', '#{@subtitle_text_anchor || @text_anchor}');
709
+
710
+ const containerElement = document.getElementById(container);
711
+ if (containerElement) {
712
+ console.log('Container found, inserting SVG...');
713
+ containerElement.innerHTML = svg.outerHTML;
714
+ console.log('SVG inserted successfully');
715
+ } else {
716
+ console.error('Container not found:', container);
717
+ }
718
+ }
719
+
720
+ console.log('SVG script loaded');
721
+ console.log('Header element exists:', !!document.querySelector('header'));
722
+
723
+ window.onload = function() {
724
+ console.log('SVG insertion starting...');
725
+ insert_svg_header('header');
726
+ console.log('SVG insertion complete');
727
+ }
728
+
729
+ // Also try immediate execution
730
+ document.addEventListener('DOMContentLoaded', function() {
731
+ console.log('DOM ready, trying SVG insertion...');
732
+ insert_svg_header('header');
733
+ });
734
+ </script>
735
+ EOS
736
+ code
737
+ end
738
+
739
+ # How to call?? bsvg = BannerSVG.new(...); bsvg.parse_svg_header; code = bsvg.get_svg # Simplify?
740
+ # Doesn't output, just returns string...
741
+
742
+ end