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.
- checksums.yaml +4 -4
- data/README.lt3 +324 -0
- data/README.md +3155 -1
- data/assets/.DS_Store +0 -0
- data/assets/README.md +44 -0
- data/assets/back-icon.png +0 -0
- data/assets/icons/facebook.svg +1 -0
- data/assets/icons/github.svg +1 -0
- data/assets/icons/instagram.svg +1 -0
- data/assets/icons/reddit.svg +1 -0
- data/assets/icons/ui/.DS_Store +0 -0
- data/assets/icons/ui/back.png +0 -0
- data/assets/icons/ui/copy.png +0 -0
- data/assets/icons/ui/down.png +0 -0
- data/assets/icons/ui/end.png +0 -0
- data/assets/icons/ui/exit.png +0 -0
- data/assets/icons/ui/foo +10 -0
- data/assets/icons/ui/home.png +0 -0
- data/assets/icons/ui/left.png +0 -0
- data/assets/icons/ui/next.png +0 -0
- data/assets/icons/ui/right.png +0 -0
- data/assets/icons/ui/start.png +0 -0
- data/assets/icons/ui/up.png +0 -0
- data/assets/icons/x.svg +1 -0
- data/assets/icons/youtube.svg +1 -0
- data/assets/samples/placeholder.svg +9 -0
- data/assets/themes/standard/favicon.svg +6 -0
- data/bin/scriptorium +1511 -0
- data/doc/README.txt +6 -0
- data/doc/anti-amnesia/20250727-054000-scriptorium-overview.md +95 -0
- data/doc/anti-amnesia/20250727-060000-api-design-tui-planning.md +34 -0
- data/doc/anti-amnesia/20250727-061000-runeblog-tui-analysis.md +50 -0
- data/doc/anti-amnesia/20250727-123000-anti-amnesia-conventions.md +31 -0
- data/doc/anti-amnesia/20250727-154000-livetext-plugin-file-stats.md +73 -0
- data/doc/anti-amnesia/20250727-172600-cursor-rbenv-ruby-version-mystery.md +64 -0
- data/doc/anti-amnesia/20250727-172600-unified-minitest-framework.md +70 -0
- data/doc/anti-amnesia/20250727-172900-ai-cognitive-assessment-capabilities.md +40 -0
- data/doc/anti-amnesia/20250727-173000-widget-testing-achievement.md +110 -0
- data/doc/anti-amnesia/20250727-180000-post-id-num-refactoring.md +73 -0
- data/doc/anti-amnesia/20250728-124243-aaa-syntax-clarification.md +46 -0
- data/doc/anti-amnesia/20250728-124421-conversation-summary-concise.md +124 -0
- data/doc/anti-amnesia/20250729-190000-scriptorium-tui-testing-complete.md +46 -0
- data/doc/anti-amnesia/20250729-200000-scriptorium-tui-testing-edit-file-workflow.md +97 -0
- data/doc/anti-amnesia/20250729-210000-reddit-autopost-integration-complete.md +158 -0
- data/doc/anti-amnesia/20250729-211500-dependency-management-system.md +211 -0
- data/doc/anti-amnesia/20250729-213000-python-virtual-environment-setup.md +141 -0
- data/doc/anti-amnesia/20250729-214500-theme-management-commands.md +211 -0
- data/doc/anti-amnesia/20250729-215000-version-update-to-0.6.0.md +134 -0
- data/doc/anti-amnesia/20250729-220000-user-guide-complete.md +41 -0
- data/doc/anti-amnesia/20250804-190500-cognitive-loop-bug.md +45 -0
- data/doc/anti-amnesia/20250804-190700-anti-amnesia-timestamping-fix.md +30 -0
- data/doc/anti-amnesia/20250804-213700-publishing-test-fix.md +49 -0
- data/doc/anti-amnesia/20250804-214400-additional-test-fixes.md +46 -0
- data/doc/anti-amnesia/20250804-220000-asset-function-logic-clarification.md +41 -0
- data/doc/anti-amnesia/20250806-202032-asset-function-logic-clarification.md +41 -0
- data/doc/anti-amnesia/20250807-213025.md +116 -0
- data/doc/anti-amnesia/20250813-082428-syntax-highlighting-and-navigation-improvements.md +256 -0
- data/doc/banner_svg_config.md +114 -0
- data/doc/contrib.lt3 +8 -0
- data/doc/dependencies.md +281 -0
- data/doc/hacker.lt3 +5 -0
- data/doc/reddit_credentials_template.json +8 -0
- data/doc/reddit_integration.md +207 -0
- data/doc/user.lt3 +38 -0
- data/doc/user_guide_section_1.md +137 -0
- data/doc/user_guide_section_10.md +515 -0
- data/doc/user_guide_section_11.md +708 -0
- data/doc/user_guide_section_2.md +233 -0
- data/doc/user_guide_section_3.md +5 -0
- data/doc/user_guide_section_4.md +221 -0
- data/doc/user_guide_section_5.md +243 -0
- data/doc/user_guide_section_6.md +147 -0
- data/doc/user_guide_section_7.md +311 -0
- data/doc/user_guide_section_8.md +224 -0
- data/doc/user_guide_section_9.md +375 -0
- data/doc/userdoc-toc.txt +88 -0
- data/lib/rouge/lexers/livetext.rb +74 -0
- data/lib/scriptorium/api.rb +640 -0
- data/lib/scriptorium/banner_svg.rb +742 -0
- data/lib/scriptorium/contract.rb +33 -0
- data/lib/scriptorium/exceptions.rb +174 -0
- data/lib/scriptorium/helpers.rb +475 -0
- data/lib/scriptorium/post.rb +195 -0
- data/lib/scriptorium/reddit.rb +83 -0
- data/lib/scriptorium/repo.rb +624 -0
- data/lib/scriptorium/standard_files.rb +515 -0
- data/lib/scriptorium/syntax_highlighter.rb +234 -0
- data/lib/scriptorium/theme.rb +179 -0
- data/lib/scriptorium/version.rb +2 -2
- data/lib/scriptorium/view.rb +976 -0
- data/lib/scriptorium/widgets/featured_posts.rb +149 -0
- data/lib/scriptorium/widgets/links.rb +112 -0
- data/lib/scriptorium/widgets/pages.rb +133 -0
- data/lib/scriptorium/widgets/widget.rb +133 -0
- data/lib/scriptorium.rb +22 -9
- data/lib/skeleton.rb +11 -2
- data/scriptorium.gemspec +15 -4
- data/test/README.md +69 -0
- data/test/all +43 -0
- data/test/api_demo.rb +99 -0
- data/test/assets/imagenotfound.jpg +0 -0
- data/test/assets/images/.DS_Store +0 -0
- data/test/assets/images/README.md +27 -0
- data/test/assets/images/odd_aspect.png +0 -0
- data/test/assets/images/perfect.png +0 -0
- data/test/assets/images/small.png +0 -0
- data/test/assets/images/tall.png +0 -0
- data/test/assets/images/very_tall.png +0 -0
- data/test/assets/images/very_wide.png +0 -0
- data/test/assets/images/wide.png +0 -0
- data/test/assets/testbanner.jpg +0 -0
- data/test/banner_svg/simple_helpers.rb +13 -0
- data/test/banner_svg/unit.rb +768 -0
- data/test/ed_test.rb +204 -0
- data/test/integration/cursor_banner_combinations.rb +193 -0
- data/test/integration/cursor_banner_features.rb +374 -0
- data/test/integration/integration_test.rb +326 -0
- data/test/livetext_plugin_test.rb +229 -0
- data/test/manual/asset_mgmt.rb +67 -0
- data/test/manual/banner-tests/config.txt +3 -0
- data/test/manual/banner-tests/index.html +45 -0
- data/test/manual/banner-tests/test01.html +58 -0
- data/test/manual/banner-tests/test02.html +58 -0
- data/test/manual/banner-tests/test03.html +58 -0
- data/test/manual/banner-tests/test04.html +65 -0
- data/test/manual/banner-tests/test05.html +65 -0
- data/test/manual/banner-tests/test06.html +65 -0
- data/test/manual/banner-tests/test07.html +65 -0
- data/test/manual/banner-tests/test08.html +59 -0
- data/test/manual/banner-tests/test09.html +59 -0
- data/test/manual/banner-tests/test10.html +59 -0
- data/test/manual/banner-tests/test11.html +59 -0
- data/test/manual/banner-tests/test12.html +59 -0
- data/test/manual/banner-tests/test13.html +59 -0
- data/test/manual/banner-tests/test14.html +59 -0
- data/test/manual/banner-tests/test15.html +58 -0
- data/test/manual/banner-tests/test16.html +58 -0
- data/test/manual/banner-tests/test17.html +58 -0
- data/test/manual/banner-tests/test18.html +68 -0
- data/test/manual/banner-tests/test19.html +68 -0
- data/test/manual/banner-tests/test20.html +68 -0
- data/test/manual/banner-tests/test21.html +68 -0
- data/test/manual/banner-tests/test22.html +68 -0
- data/test/manual/banner-tests/test23.html +68 -0
- data/test/manual/banner-tests/test24.html +68 -0
- data/test/manual/banner-tests/test25.html +67 -0
- data/test/manual/banner_environment.rb +192 -0
- data/test/manual/deploy_symlink_demo.rb +142 -0
- data/test/manual/environment.rb +67 -0
- data/test/manual/make_banner.rb +153 -0
- data/test/manual/sample_banner_config.txt +12 -0
- data/test/manual/symlink_demo.rb +117 -0
- data/test/manual/test1.rb +47 -0
- data/test/manual/test2.rb +12 -0
- data/test/manual/test3.rb +38 -0
- data/test/manual/test4.rb +40 -0
- data/test/manual/test5.rb +24 -0
- data/test/manual/test6.rb +73 -0
- data/test/manual/test_banner_combinations.rb +120 -0
- data/test/manual/test_banner_features.rb +306 -0
- data/test/manual/test_banner_from_file.rb +150 -0
- data/test/manual/test_banner_in_header.rb +35 -0
- data/test/manual/test_code_highlighting.rb +68 -0
- data/test/manual/test_complex_header.rb +74 -0
- data/test/manual/test_empty_header.rb +32 -0
- data/test/manual/test_radial_custom.rb +58 -0
- data/test/manual/test_radial_large_radius.rb +52 -0
- data/test/manual/test_svg_debug.rb +47 -0
- data/test/manual/test_syntax_highlighting.rb +147 -0
- data/test/pages-demo/config/currentview.txt +1 -0
- data/test/pages-demo/views/demo/config/bootstrap_css.txt +5 -0
- data/test/pages-demo/views/demo/config/bootstrap_js.txt +4 -0
- data/test/pages-demo/views/demo/config/common.js +57 -0
- data/test/pages-demo/views/demo/config/footer.txt +1 -0
- data/test/pages-demo/views/demo/config/global-head.txt +8 -0
- data/test/pages-demo/views/demo/config/header.txt +1 -0
- data/test/pages-demo/views/demo/config/layout.txt +1 -0
- data/test/pages-demo/views/demo/config/left.txt +1 -0
- data/test/pages-demo/views/demo/config/main.txt +1 -0
- data/test/pages-demo/views/demo/config/right.txt +1 -0
- data/test/pages-demo/views/demo/config.txt +3 -0
- data/test/pages-demo/views/demo/output/panes/footer.html +1 -0
- data/test/pages-demo/views/demo/output/panes/header.html +1 -0
- data/test/pages-demo/views/demo/output/panes/left.html +1 -0
- data/test/pages-demo/views/demo/output/panes/main.html +1 -0
- data/test/pages-demo/views/demo/output/panes/right.html +1 -0
- data/test/rubytext/rubytext_comprehensive_test.rb +307 -0
- data/test/rubytext/rubytext_demo_test.rb +42 -0
- data/test/rubytext/rubytext_testing_guide.md +277 -0
- data/test/run_automated_tests.rb +45 -0
- data/test/scriptorium-TEST-1754622690-146/config/bootstrap_css.txt +5 -0
- data/test/scriptorium-TEST-1754622690-146/config/bootstrap_js.txt +4 -0
- data/test/scriptorium-TEST-1754622690-146/config/common.js +57 -0
- data/test/scriptorium-TEST-1754622690-146/config/currentview.txt +1 -0
- data/test/scriptorium-TEST-1754622690-146/config/global-head.txt +9 -0
- data/test/scriptorium-TEST-1754622690-146/config/last_post_num.txt +1 -0
- data/test/scriptorium-TEST-1754622690-146/config/os_helpers.rb +4 -0
- data/test/scriptorium-TEST-1754622690-146/config/widgets.txt +3 -0
- data/test/scriptorium-TEST-1754622690-146/posts/0001/meta.txt +8 -0
- data/test/scriptorium-TEST-1754622690-146/posts/0001/source.lt3 +6 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/README.txt +1 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/config.txt +1 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/initial/post.lt3 +12 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/footer.txt +2 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/header.txt +4 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/left.txt +3 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/main.txt +5 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/right.txt +3 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/gen/text.css +1 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/layout.txt +5 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/index.lt3 +1 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/index_entry.lt3 +14 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/post.lt3 +13 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/widget.lt3 +1 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/bootstrap_css.txt +5 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/bootstrap_js.txt +4 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/common.js +57 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/deploy.txt +5 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/footer.txt +2 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/global-head.txt +9 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/header.txt +4 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/layout.txt +5 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/left.txt +3 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/main.txt +5 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/reddit.txt +10 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/right.txt +3 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/social.txt +7 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/status.txt +7 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config.txt +3 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/layout/footer.html +3 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/layout/header.html +3 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/layout/left.html +3 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/layout/main.html +3 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/layout/right.html +3 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/footer.html +1 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/header.html +1 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/left.html +1 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/main.html +1 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/right.html +1 -0
- data/test/staging/.DS_Store +0 -0
- data/test/syntax_highlighting_test.lt3 +124 -0
- data/test/test_helpers.rb +230 -0
- data/test/tui_editor_integration_test.rb +296 -0
- data/test/tui_integration_test.rb +637 -0
- data/test/unit/api.rb +1056 -0
- data/test/unit/asset_management.rb +245 -0
- data/test/unit/clipboard_test.rb +60 -0
- data/test/unit/contract_test.rb +91 -0
- data/test/unit/core.rb +857 -0
- data/test/unit/deploy_test.rb +187 -0
- data/test/unit/gem_asset_management.rb +189 -0
- data/test/unit/livetext_basic.rb +69 -0
- data/test/unit/livetext_compatibility.rb +89 -0
- data/test/unit/post.rb +244 -0
- data/test/unit/read_commented_file_test.rb +276 -0
- data/test/unit/reddit_test.rb +235 -0
- data/test/unit/repo.rb +548 -0
- data/test/unit/social_test.rb +369 -0
- data/test/unit/symlink_test.rb +213 -0
- data/test/unit/view.rb +431 -0
- data/test/unit/widgets.rb +669 -0
- data/test/wizard_test.rb +123 -0
- data/ui/README.md +67 -0
- data/ui/common/lib/ui_common.rb +8 -0
- data/ui/rubytext/README.md +191 -0
- data/ui/rubytext/bin/scriptorium-rubytext +402 -0
- data/ui/rubytext/lib/rubytext_ui.rb +300 -0
- data/ui/tui/bin/scriptorium +1420 -0
- data/ui/tui/test/tui_test.rb +23 -0
- data/ui/web/app/app.rb +1378 -0
- data/ui/web/app/error_helpers.rb +150 -0
- data/ui/web/app/views/advanced_config.erb +190 -0
- data/ui/web/app/views/asset_management.erb +589 -0
- data/ui/web/app/views/banner_config.erb +200 -0
- data/ui/web/app/views/configure_view.erb +401 -0
- data/ui/web/app/views/dashboard.erb +162 -0
- data/ui/web/app/views/deploy_config.erb +146 -0
- data/ui/web/app/views/edit_pages.erb +195 -0
- data/ui/web/app/views/edit_post.erb +54 -0
- data/ui/web/app/views/error_page.erb +29 -0
- data/ui/web/app/views/header_config.erb +155 -0
- data/ui/web/app/views/layout_config.erb +147 -0
- data/ui/web/app/views/navbar_config.erb +411 -0
- data/ui/web/app/views/view_dashboard.erb +138 -0
- data/ui/web/bin/scriptorium-web +153 -0
- data/ui/web/test/web_basic_test.rb +38 -0
- data/ui/web/test_navbar.txt +7 -0
- data/ui/web/tmp/web_server.log +5 -0
- data/ui/web/tmp/web_server.pid +1 -0
- 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
|