scriptorium 0.0.3 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/icons/social/reddit.png +0 -0
- data/assets/icons/social/x-logo.png +0 -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/imagenotfound.jpg +0 -0
- data/assets/samples/placeholder.svg +9 -0
- data/assets/themes/standard/favicon.svg +6 -0
- data/bin/sblog +84 -5
- data/bin/scriptorium +1 -0
- data/doc/README.txt +6 -0
- data/doc/anti-amnesia/20250727-054000-scriptorium-overview.md +94 -0
- data/doc/anti-amnesia/20250727-123000-anti-amnesia-conventions.md +2 -0
- data/doc/anti-amnesia/20250727-172600-cursor-rbenv-ruby-version-mystery.md +45 -0
- data/doc/anti-amnesia/20250727-172900-ai-cognitive-assessment-capabilities.md +40 -0
- data/doc/anti-amnesia/20250728-124243-aaa-syntax-clarification.md +46 -0
- data/doc/anti-amnesia/20250729-210000-reddit-autopost-integration-complete.md +158 -0
- data/doc/anti-amnesia/20250804-190500-cognitive-loop-bug.md +35 -0
- data/doc/anti-amnesia/20250804-190700-anti-amnesia-timestamping-fix.md +27 -0
- data/doc/anti-amnesia/20250807-213025.md +116 -0
- data/doc/anti-amnesia/20250901-211714-codemirror-integration-and-web-tests.md +172 -0
- data/doc/anti-amnesia/20250902-002402-backup-restore-system.md +126 -0
- data/doc/anti-amnesia/20250907-203339-backup-metadata-implementation.md +66 -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/imported/0001-elixir-conf-2014/metadata.txt +7 -0
- data/doc/imported/0001-elixir-conf-2014/post.html +37 -0
- data/doc/imported/0001-elixir-conf-2014/source.lt3 +22 -0
- data/doc/imported/0002-programmers-and-word-processing/metadata.txt +7 -0
- data/doc/imported/0002-programmers-and-word-processing/post.html +192 -0
- data/doc/imported/0002-programmers-and-word-processing/source.lt3 +146 -0
- data/doc/imported/0003-how-to-turn-your-brain-sideways/metadata.txt +7 -0
- data/doc/imported/0003-how-to-turn-your-brain-sideways/post.html +60 -0
- data/doc/imported/0003-how-to-turn-your-brain-sideways/source.lt3 +40 -0
- data/doc/imported/0004-upcoming-lone-star-ruby-conference/metadata.txt +7 -0
- data/doc/imported/0004-upcoming-lone-star-ruby-conference/post.html +42 -0
- data/doc/imported/0004-upcoming-lone-star-ruby-conference/source.lt3 +24 -0
- data/doc/imported/0005-elixir-conf-2015-announced/metadata.txt +7 -0
- data/doc/imported/0005-elixir-conf-2015-announced/post.html +30 -0
- data/doc/imported/0005-elixir-conf-2015-announced/source.lt3 +16 -0
- data/doc/imported/0006-ruby-for-dinosaurs/metadata.txt +7 -0
- data/doc/imported/0006-ruby-for-dinosaurs/post.html +43 -0
- data/doc/imported/0006-ruby-for-dinosaurs/source.lt3 +27 -0
- data/doc/imported/0007-phoenix-isnt-rails/metadata.txt +7 -0
- data/doc/imported/0007-phoenix-isnt-rails/post.html +116 -0
- data/doc/imported/0007-phoenix-isnt-rails/source.lt3 +87 -0
- data/doc/imported/0008-concerning-the-term-monkeypatching/metadata.txt +7 -0
- data/doc/imported/0008-concerning-the-term-monkeypatching/post.html +129 -0
- data/doc/imported/0008-concerning-the-term-monkeypatching/source.lt3 +92 -0
- data/doc/imported/0009-announcement-coming-soon/metadata.txt +7 -0
- data/doc/imported/0009-announcement-coming-soon/post.html +33 -0
- data/doc/imported/0009-announcement-coming-soon/source.lt3 +19 -0
- data/doc/imported/0010-immutable-data-ditching-the-wax-tablet/metadata.txt +7 -0
- data/doc/imported/0010-immutable-data-ditching-the-wax-tablet/post.html +175 -0
- data/doc/imported/0010-immutable-data-ditching-the-wax-tablet/source.lt3 +139 -0
- data/doc/imported/0011-computer-science-as-a-lost-art/metadata.txt +7 -0
- data/doc/imported/0011-computer-science-as-a-lost-art/post.html +139 -0
- data/doc/imported/0011-computer-science-as-a-lost-art/source.lt3 +104 -0
- data/doc/imported/0012-ruby-day-in-turin-italy/metadata.txt +7 -0
- data/doc/imported/0012-ruby-day-in-turin-italy/post.html +42 -0
- data/doc/imported/0012-ruby-day-in-turin-italy/source.lt3 +24 -0
- data/doc/imported/0013-rubyday-was-a-success/metadata.txt +7 -0
- data/doc/imported/0013-rubyday-was-a-success/post.html +44 -0
- data/doc/imported/0013-rubyday-was-a-success/source.lt3 +27 -0
- data/doc/imported/0014-working-on-the-blogging-software/metadata.txt +7 -0
- data/doc/imported/0014-working-on-the-blogging-software/post.html +63 -0
- data/doc/imported/0014-working-on-the-blogging-software/source.lt3 +41 -0
- data/doc/imported/0015-ok-its-not-really-a-lost-art/metadata.txt +7 -0
- data/doc/imported/0015-ok-its-not-really-a-lost-art/post.html +172 -0
- data/doc/imported/0015-ok-its-not-really-a-lost-art/source.lt3 +134 -0
- data/doc/imported/0016-an-in-operator-for-ruby/metadata.txt +7 -0
- data/doc/imported/0016-an-in-operator-for-ruby/post.html +155 -0
- data/doc/imported/0016-an-in-operator-for-ruby/source.lt3 +106 -0
- data/doc/imported/0017-the-forgotten-mathematician/metadata.txt +7 -0
- data/doc/imported/0017-the-forgotten-mathematician/post.html +161 -0
- data/doc/imported/0017-the-forgotten-mathematician/source.lt3 +119 -0
- data/doc/imported/0018-ruby-puns/metadata.txt +7 -0
- data/doc/imported/0018-ruby-puns/post.html +46 -0
- data/doc/imported/0018-ruby-puns/source.lt3 +28 -0
- data/doc/imported/0019-custom-exceptions-via-metaprogramming/metadata.txt +7 -0
- data/doc/imported/0019-custom-exceptions-via-metaprogramming/post.html +138 -0
- data/doc/imported/0019-custom-exceptions-via-metaprogramming/source.lt3 +101 -0
- data/doc/imported/0020-fffff/metadata.txt +7 -0
- data/doc/imported/0020-fffff/post.html +24 -0
- data/doc/imported/0020-fffff/source.lt3 +12 -0
- data/doc/imported/0021-trying-ror-yet-again/metadata.txt +7 -0
- data/doc/imported/0021-trying-ror-yet-again/post.html +26 -0
- data/doc/imported/0021-trying-ror-yet-again/source.lt3 +12 -0
- data/doc/imported/0023-doctor-sleep/metadata.txt +7 -0
- data/doc/imported/0023-doctor-sleep/post.html +63 -0
- data/doc/imported/0023-doctor-sleep/source.lt3 +44 -0
- data/doc/imported/0024-just-a-test/metadata.txt +7 -0
- data/doc/imported/0024-just-a-test/post.html +24 -0
- data/doc/imported/0024-just-a-test/source.lt3 +12 -0
- data/doc/imported/import_summary.txt +98 -0
- data/doc/livetext-informal-spec.txt +65 -0
- data/doc/myuserdoc/ch-0.lt3 +31 -0
- data/doc/myuserdoc/ch-1.lt3 +37 -0
- data/doc/myuserdoc/ch-10.lt3 +22 -0
- data/doc/myuserdoc/ch-2.lt3 +37 -0
- data/doc/myuserdoc/ch-3.lt3 +19 -0
- data/doc/myuserdoc/ch-4.lt3 +43 -0
- data/doc/myuserdoc/ch-5.lt3 +22 -0
- data/doc/myuserdoc/ch-6.lt3 +19 -0
- data/doc/myuserdoc/ch-7.lt3 +16 -0
- data/doc/myuserdoc/ch-8.lt3 +13 -0
- data/doc/myuserdoc/ch-9.lt3 +19 -0
- data/doc/myuserdoc/tweak.rb +18 -0
- data/doc/myuserdoc/userdoc-toc.txt +88 -0
- data/doc/old-posts/0001-elixir-conf-2014.lt3 +24 -0
- data/doc/old-posts/0002-programmers-and-word-processing.lt3 +150 -0
- data/doc/old-posts/0003-how-to-turn-your-brain-sideways.lt3 +43 -0
- data/doc/old-posts/0004-upcoming-lone-star-ruby-conference.lt3 +26 -0
- data/doc/old-posts/0005-elixir-conf-2015-announced.lt3 +17 -0
- data/doc/old-posts/0006-ruby-for-dinosaurs.lt3 +30 -0
- data/doc/old-posts/0007-phoenix-isnt-rails.lt3 +90 -0
- data/doc/old-posts/0008-concerning-the-term-monkeypatching.lt3 +105 -0
- data/doc/old-posts/0009-announcement-coming-soon.lt3 +20 -0
- data/doc/old-posts/0010-immutable-data-ditching-the-wax-tablet.lt3 +142 -0
- data/doc/old-posts/0011-computer-science-as-a-lost-art.lt3 +117 -0
- data/doc/old-posts/0012-ruby-day-in-turin-italy.lt3 +26 -0
- data/doc/old-posts/0013-rubyday-was-a-success.lt3 +28 -0
- data/doc/old-posts/0014-working-on-the-blogging-software.lt3 +42 -0
- data/doc/old-posts/0015-ok-its-not-really-a-lost-art.lt3 +137 -0
- data/doc/old-posts/0016-an-in-operator-for-ruby.lt3 +142 -0
- data/doc/old-posts/0017-the-forgotten-mathematician.lt3 +129 -0
- data/doc/old-posts/0018-ruby-puns.lt3 +31 -0
- data/doc/old-posts/0019-custom-exceptions-via-metaprogramming.lt3 +116 -0
- data/doc/old-posts/0021-trying-ror-yet-again.lt3 +35 -0
- data/doc/old-posts/0023-doctor-sleep.lt3 +43 -0
- data/doc/old-posts/0024-just-a-test.lt3 +12 -0
- data/doc/old-posts/0025-trying-another-post.lt3 +12 -0
- data/doc/old-repo +1 -0
- data/doc/reddit_credentials_template.json +8 -0
- data/doc/reddit_integration.md +207 -0
- data/doc/user.lt3 +35 -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/lib/rouge/lexers/livetext.rb +74 -0
- data/lib/scriptorium/api.rb +2373 -0
- data/lib/scriptorium/banner_svg.rb +729 -0
- data/lib/scriptorium/contract.rb +34 -0
- data/lib/scriptorium/exceptions.rb +201 -1
- data/lib/scriptorium/helpers.rb +675 -0
- data/lib/scriptorium/post.rb +259 -0
- data/lib/scriptorium/reddit.rb +83 -0
- data/lib/scriptorium/repo.rb +938 -0
- data/lib/scriptorium/standard_files.rb +149 -0
- data/lib/scriptorium/support/bootstrap/css.txt +5 -0
- data/lib/scriptorium/support/bootstrap/js.txt +4 -0
- data/lib/scriptorium/support/common_js/clipboard.js +35 -0
- data/lib/scriptorium/support/common_js/content-loader.js +187 -0
- data/lib/scriptorium/support/common_js/navigation.js +52 -0
- data/lib/scriptorium/support/common_js/syntax-highlighting.js +27 -0
- data/lib/scriptorium/support/config/reddit.txt +10 -0
- data/lib/scriptorium/support/config/reddit_template.txt +17 -0
- data/lib/scriptorium/support/config/social.txt +8 -0
- data/lib/scriptorium/support/highlight/css.txt +2 -0
- data/lib/scriptorium/support/highlight/custom.css +119 -0
- data/lib/scriptorium/support/highlight/js.txt +1 -0
- data/lib/scriptorium/support/post_index/config.txt +15 -0
- data/lib/scriptorium/support/post_index/style.css +55 -0
- data/lib/scriptorium/support/templates/index_entry.lt3 +16 -0
- data/lib/scriptorium/support/templates/initial_post.lt3 +12 -0
- data/lib/scriptorium/support/templates/layout.txt +5 -0
- data/lib/scriptorium/support/templates/post.lt3 +104 -0
- data/lib/scriptorium/support/theme/footer.lt3 +2 -0
- data/lib/scriptorium/support/theme/header.lt3 +4 -0
- data/lib/scriptorium/support/theme/left.lt3 +3 -0
- data/lib/scriptorium/support/theme/main.lt3 +5 -0
- data/lib/scriptorium/support/theme/right.lt3 +3 -0
- data/lib/scriptorium/theme.rb +192 -0
- data/lib/scriptorium/version.rb +1 -1
- data/lib/scriptorium/view.rb +1021 -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 +38 -34
- data/lib/skeleton.rb +10 -1
- data/scriptorium.gemspec +17 -5
- data/test/README.md +69 -0
- data/test/WEB_INTEGRATION_README.md +196 -0
- data/test/all +83 -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 +1000 -0
- data/test/config/deployment.txt +5 -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/integration/preview_flow_test.rb +94 -0
- data/test/livetext_plugin_test.rb +500 -0
- data/test/manual/asset_mgmt.rb +67 -0
- data/test/manual/banner-tests/index.html +45 -0
- data/test/manual/banner-tests/svg.txt +3 -0
- data/test/manual/banner-tests/test01.html +122 -0
- data/test/manual/banner-tests/test02.html +122 -0
- data/test/manual/banner-tests/test03.html +122 -0
- data/test/manual/banner-tests/test04.html +129 -0
- data/test/manual/banner-tests/test05.html +129 -0
- data/test/manual/banner-tests/test06.html +129 -0
- data/test/manual/banner-tests/test07.html +129 -0
- data/test/manual/banner-tests/test08.html +123 -0
- data/test/manual/banner-tests/test09.html +123 -0
- data/test/manual/banner-tests/test10.html +123 -0
- data/test/manual/banner-tests/test11.html +123 -0
- data/test/manual/banner-tests/test12.html +123 -0
- data/test/manual/banner-tests/test13.html +123 -0
- data/test/manual/banner-tests/test14.html +123 -0
- data/test/manual/banner-tests/test15.html +122 -0
- data/test/manual/banner-tests/test16.html +122 -0
- data/test/manual/banner-tests/test17.html +122 -0
- data/test/manual/banner-tests/test18.html +132 -0
- data/test/manual/banner-tests/test19.html +132 -0
- data/test/manual/banner-tests/test20.html +132 -0
- data/test/manual/banner-tests/test21.html +132 -0
- data/test/manual/banner-tests/test22.html +132 -0
- data/test/manual/banner-tests/test23.html +132 -0
- data/test/manual/banner-tests/test24.html +132 -0
- data/test/manual/banner-tests/test25.html +131 -0
- data/test/manual/banner_environment.rb +205 -0
- data/test/manual/codemirror_demo.html +773 -0
- data/test/manual/create_posts_for_web.rb +114 -0
- data/test/manual/environment.rb +67 -0
- data/test/manual/make_banner.rb +153 -0
- data/test/manual/preview_manual_test.rb +129 -0
- data/test/manual/sample_banner_config.txt +12 -0
- data/test/manual/test_advanced_widgets.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_integration.rb +115 -0
- data/test/manual/test_banner_radial.rb +87 -0
- data/test/manual/test_basic_posts.rb +47 -0
- data/test/manual/test_layout_widgets.rb +40 -0
- data/test/manual/test_pagination.rb +24 -0
- data/test/manual/test_random_posts.rb +38 -0
- data/test/manual/test_syntax_highlighting.rb +167 -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/staging/.DS_Store +0 -0
- data/test/support/preview_utils.rb +88 -0
- data/test/syntax_highlighting_test.lt3 +124 -0
- data/test/test_gem_assets.rb +48 -0
- data/test/test_helpers.rb +240 -0
- data/test/tui_editor_integration_test.rb +296 -0
- data/test/tui_integration_test.rb +883 -0
- data/test/unit/api.rb +1776 -0
- data/test/unit/asset_management.rb +219 -0
- data/test/unit/backup_test.rb +451 -0
- data/test/unit/clipboard_test.rb +60 -0
- data/test/unit/contract_test.rb +69 -0
- data/test/unit/core.rb +1211 -0
- data/test/unit/deploy_config_test.rb +248 -0
- data/test/unit/deploy_test.rb +478 -0
- data/test/unit/edit_post_test.rb +168 -0
- data/test/unit/gem_asset_management.rb +183 -0
- data/test/unit/livetext_basic.rb +57 -0
- data/test/unit/livetext_compatibility.rb +82 -0
- data/test/unit/parse_cmd_test.rb +260 -0
- data/test/unit/permalink_copy_test.rb +211 -0
- data/test/unit/post.rb +309 -0
- data/test/unit/post_index_config_test.rb +258 -0
- data/test/unit/post_state_helpers_test.rb +137 -0
- data/test/unit/read_commented_file_test.rb +278 -0
- data/test/unit/reddit_test.rb +235 -0
- data/test/unit/repo.rb +569 -0
- data/test/unit/social_test.rb +366 -0
- data/test/unit/syntax_highlighting.rb +70 -0
- data/test/unit/theme_management_test.rb +91 -0
- data/test/unit/view.rb +498 -0
- data/test/unit/widgets.rb +669 -0
- data/test/web_integration_test.rb +231 -0
- data/test/web_test_helper.rb +218 -0
- data/test/web_workflow_test.rb +527 -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 +1890 -0
- data/ui/tui/test/tui_test.rb +23 -0
- data/ui/web/app/app.rb +2600 -0
- data/ui/web/app/assets/livetext_mode.js +244 -0
- data/ui/web/app/error_helpers.rb +150 -0
- data/ui/web/app/views/advanced_config.erb +196 -0
- data/ui/web/app/views/asset_management.erb +645 -0
- data/ui/web/app/views/backup_management.erb +238 -0
- data/ui/web/app/views/banner_config.erb +200 -0
- data/ui/web/app/views/config_widget.erb +232 -0
- data/ui/web/app/views/configure_view.erb +401 -0
- data/ui/web/app/views/dashboard.erb +154 -0
- data/ui/web/app/views/deploy_config.erb +149 -0
- data/ui/web/app/views/edit_pages.erb +363 -0
- data/ui/web/app/views/edit_post.erb +175 -0
- data/ui/web/app/views/edit_theme.erb +73 -0
- data/ui/web/app/views/edit_theme_file.erb +74 -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/theme_management.erb +130 -0
- data/ui/web/app/views/view_dashboard.erb +779 -0
- data/ui/web/app/views/widgets.erb +249 -0
- data/ui/web/bin/scriptorium-web +164 -0
- data/ui/web/test/web_basic_test.rb +38 -0
- data/ui/web/test_navbar.txt +7 -0
- data/ui/web/tmp/timing.log +17 -0
- data/ui/web/tmp/web_server.log +0 -0
- metadata +434 -8
- data/lib/scriptorium/engine.rb +0 -22
- data/test/engine/unit.rb +0 -44
@@ -0,0 +1,729 @@
|
|
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
|
+
@base_font_size = 60
|
34
|
+
@title_font_size = @base_font_size * @title_scale
|
35
|
+
@subtitle_font_size = @base_font_size * @subtitle_scale
|
36
|
+
# Remove default @title_xy and @subtitle_xy
|
37
|
+
@background = "#fff"
|
38
|
+
@gradient_start_color = nil
|
39
|
+
@gradient_end_color = nil
|
40
|
+
@gradient_direction = nil
|
41
|
+
@radial_start_color = nil
|
42
|
+
@radial_end_color = nil
|
43
|
+
@image_background = nil
|
44
|
+
@title_xy_set = false
|
45
|
+
@subtitle_xy_set = false
|
46
|
+
|
47
|
+
define_invariants
|
48
|
+
verify { @title == title }
|
49
|
+
verify { @subtitle == subtitle }
|
50
|
+
check_invariants
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
|
55
|
+
def handle_background(*args)
|
56
|
+
check_invariants
|
57
|
+
assume { args.is_a?(Array) }
|
58
|
+
|
59
|
+
validate_background_args(args)
|
60
|
+
@background = args.first
|
61
|
+
|
62
|
+
verify { @background.is_a?(String) && !@background.empty? }
|
63
|
+
check_invariants
|
64
|
+
end
|
65
|
+
|
66
|
+
private def validate_background_args(args)
|
67
|
+
raise BackgroundNoArgs if args.nil? || args.empty?
|
68
|
+
|
69
|
+
raise BackgroundFirstArgNil if args.first.nil?
|
70
|
+
|
71
|
+
raise BackgroundFirstArgEmpty if args.first.to_s.strip.empty?
|
72
|
+
end
|
73
|
+
|
74
|
+
def handle_linear_gradient(*args)
|
75
|
+
validate_linear_gradient_args(args)
|
76
|
+
@gradient_start_color = args[0]
|
77
|
+
@gradient_end_color = args[1]
|
78
|
+
@gradient_direction = args[2] || "lr"
|
79
|
+
end
|
80
|
+
|
81
|
+
private def validate_linear_gradient_args(args)
|
82
|
+
raise LinearGradientNoArgs if args.nil? || args.empty?
|
83
|
+
|
84
|
+
raise LinearGradientStartColorNil if args[0].nil? || args[0].to_s.strip.empty?
|
85
|
+
|
86
|
+
# Validate all provided arguments (up to 3: start_color, end_color, direction)
|
87
|
+
args.each_with_index do |arg, index|
|
88
|
+
next if arg.nil? # Allow nil for optional arguments
|
89
|
+
raise LinearGradientArgEmpty(index + 1) if arg.to_s.strip.empty?
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def handle_radial_gradient(*args)
|
94
|
+
validate_radial_gradient_args(args)
|
95
|
+
@radial_start_color = args[0]
|
96
|
+
@radial_end_color = args[1]
|
97
|
+
# Optional: cx, cy, r
|
98
|
+
@radial_cx = args[2] || '50%'
|
99
|
+
@radial_cy = args[3] || '50%'
|
100
|
+
@radial_r = args[4] || '50%'
|
101
|
+
# 6th param: aspect ratio compensation for gradientTransform
|
102
|
+
@radial_ar = args[5] ? args[5].to_f : nil
|
103
|
+
end
|
104
|
+
|
105
|
+
private def validate_radial_gradient_args(args)
|
106
|
+
raise RadialGradientNoArgs if args.nil? || args.empty?
|
107
|
+
|
108
|
+
raise RadialGradientStartColorNil if args[0].nil? || args[0].to_s.strip.empty?
|
109
|
+
|
110
|
+
# Validate all provided arguments (up to 6: start_color, end_color, cx, cy, r, aspect_ratio)
|
111
|
+
args.each_with_index do |arg, index|
|
112
|
+
next if arg.nil? # Allow nil for optional arguments
|
113
|
+
raise RadialGradientArgEmpty(index + 1) if arg.to_s.strip.empty?
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Image backgrounds: Users should provide images matching the banner's aspect ratio.
|
118
|
+
# SVG will crop/stretch if aspect ratios don't match (use preserveAspectRatio="xMidYMid slice" for cropping).
|
119
|
+
def handle_image_background(*args)
|
120
|
+
validate_image_background_args(args)
|
121
|
+
@image_background = args[0]
|
122
|
+
end
|
123
|
+
|
124
|
+
private def validate_image_background_args(args)
|
125
|
+
raise ImageBackgroundNoArgs if args.nil? || args.empty?
|
126
|
+
|
127
|
+
raise ImageBackgroundFirstArgNil if args[0].nil?
|
128
|
+
|
129
|
+
raise ImageBackgroundFirstArgEmpty if args[0].to_s.strip.empty?
|
130
|
+
end
|
131
|
+
|
132
|
+
def handle_aspect(*args)
|
133
|
+
check_invariants
|
134
|
+
assume { args.is_a?(Array) }
|
135
|
+
|
136
|
+
validate_aspect_args(args)
|
137
|
+
@aspect = args.first.to_f
|
138
|
+
|
139
|
+
verify { @aspect.is_a?(Numeric) && @aspect > 0 }
|
140
|
+
check_invariants
|
141
|
+
end
|
142
|
+
|
143
|
+
private def validate_aspect_args(args)
|
144
|
+
raise AspectNoArgs if args.nil? || args.empty?
|
145
|
+
|
146
|
+
raise AspectFirstArgNil if args.first.nil?
|
147
|
+
|
148
|
+
raise AspectFirstArgEmpty if args.first.to_s.strip.empty?
|
149
|
+
|
150
|
+
unless args.first.to_s.match?(/^\d+(\.\d+)?$/)
|
151
|
+
raise AspectInvalidValue(args.first)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def handle_preserve_aspect(*args)
|
156
|
+
@preserve_aspect = args.first
|
157
|
+
end
|
158
|
+
|
159
|
+
def handle_font(*args)
|
160
|
+
validate_font_args(args)
|
161
|
+
@font = args.join(" ")
|
162
|
+
end
|
163
|
+
|
164
|
+
private def validate_font_args(args)
|
165
|
+
raise FontArgsNil if args.nil?
|
166
|
+
|
167
|
+
# Font arguments are optional - empty args array is allowed
|
168
|
+
# But if any arguments are provided, they must be valid
|
169
|
+
args.each_with_index do |arg, index|
|
170
|
+
raise FontArgNil(index + 1) if arg.nil?
|
171
|
+
|
172
|
+
raise FontArgEmpty(index + 1) if arg.to_s.strip.empty?
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def handle_text_color(*args)
|
177
|
+
validate_text_color_args(args)
|
178
|
+
@text_color = args.first
|
179
|
+
end
|
180
|
+
|
181
|
+
private def validate_text_color_args(args)
|
182
|
+
raise TextColorNoArgs if args.nil? || args.empty?
|
183
|
+
|
184
|
+
raise TextColorFirstArgNil if args.first.nil?
|
185
|
+
|
186
|
+
raise TextColorFirstArgEmpty if args.first.to_s.strip.empty?
|
187
|
+
end
|
188
|
+
|
189
|
+
def handle_text_align(*args)
|
190
|
+
direction = args[0]
|
191
|
+
# Apply to both title and subtitle
|
192
|
+
handle_title_align(*args)
|
193
|
+
handle_subtitle_align(*args)
|
194
|
+
end
|
195
|
+
|
196
|
+
def handle_scale(which, *args)
|
197
|
+
check_invariants
|
198
|
+
assume { which.is_a?(String) && !which.empty? }
|
199
|
+
assume { args.is_a?(Array) }
|
200
|
+
|
201
|
+
if which == "title"
|
202
|
+
@title_scale = args.first.to_f
|
203
|
+
verify { @title_scale.is_a?(Numeric) && @title_scale > 0 }
|
204
|
+
elsif which == "subtitle"
|
205
|
+
@subtitle_scale = args.first.to_f
|
206
|
+
verify { @subtitle_scale.is_a?(Numeric) && @subtitle_scale > 0 }
|
207
|
+
end
|
208
|
+
|
209
|
+
check_invariants
|
210
|
+
end
|
211
|
+
|
212
|
+
def handle_style(which, *args)
|
213
|
+
args.each do |arg|
|
214
|
+
case
|
215
|
+
when which == "title" && arg =~ /bold/i
|
216
|
+
@title_weight = "bold"
|
217
|
+
when which == "title" && arg =~ /italic/i
|
218
|
+
@title_style = "italic"
|
219
|
+
when which == "subtitle" && arg =~ /bold/i
|
220
|
+
@subtitle_weight = "bold"
|
221
|
+
when which == "subtitle" && arg =~ /italic/i
|
222
|
+
@subtitle_style = "italic"
|
223
|
+
else
|
224
|
+
@title_style = arg
|
225
|
+
@subtitle_style = arg
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
def handle_xy(which, *args)
|
231
|
+
validate_xy_which(which)
|
232
|
+
|
233
|
+
if which == "title"
|
234
|
+
@title_xy = args
|
235
|
+
@title_xy_set = true
|
236
|
+
elsif which == "subtitle"
|
237
|
+
@subtitle_xy = args
|
238
|
+
@subtitle_xy_set = true
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
private def validate_xy_which(which)
|
243
|
+
raise XYWhichNil if which.nil?
|
244
|
+
|
245
|
+
raise XYWhichEmpty if which.to_s.strip.empty?
|
246
|
+
|
247
|
+
unless ["title", "subtitle"].include?(which)
|
248
|
+
raise XYInvalidWhich(which)
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
private def validate_align_args(args)
|
253
|
+
raise AlignNoArgs if args.nil? || args.empty?
|
254
|
+
|
255
|
+
raise AlignDirectionNil if args[0].nil? || args[0].to_s.strip.empty?
|
256
|
+
|
257
|
+
unless ["left", "center", "right"].include?(args[0])
|
258
|
+
raise AlignInvalidDirection(args[0])
|
259
|
+
end
|
260
|
+
|
261
|
+
# Validate optional x and y arguments if provided
|
262
|
+
args.each_with_index do |arg, index|
|
263
|
+
next if index == 0 # Skip direction (already validated)
|
264
|
+
next if arg.nil? # Allow nil for optional arguments
|
265
|
+
raise AlignArgEmpty(index + 1) if arg.to_s.strip.empty?
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
def handle_title_align(*args)
|
270
|
+
validate_align_args(args)
|
271
|
+
direction = args[0]
|
272
|
+
x = args[1]
|
273
|
+
y = args[2]
|
274
|
+
@title_align = direction
|
275
|
+
@title_align_x = x
|
276
|
+
@title_align_y = y
|
277
|
+
# Smart default for x if 'auto'
|
278
|
+
if x == 'auto' || x.nil?
|
279
|
+
@title_align_x = case direction
|
280
|
+
when 'left' then '5%'
|
281
|
+
when 'center' then '50%'
|
282
|
+
when 'right' then '95%'
|
283
|
+
else '5%'
|
284
|
+
end
|
285
|
+
end
|
286
|
+
# Warn if direction and x seem incompatible
|
287
|
+
if direction == 'center' && @title_align_x !~ /^50%$/
|
288
|
+
warn "[BannerSVG] Warning: title.align center with x=#{@title_align_x} may not be visually centered."
|
289
|
+
elsif direction == 'left' && @title_align_x !~ /^5%$/
|
290
|
+
warn "[BannerSVG] Warning: title.align left with x=#{@title_align_x} may not be visually left-aligned."
|
291
|
+
elsif direction == 'right' && @title_align_x !~ /^95%$/
|
292
|
+
warn "[BannerSVG] Warning: title.align right with x=#{@title_align_x} may not be visually right-aligned."
|
293
|
+
end
|
294
|
+
# Set anchor
|
295
|
+
@title_text_anchor = case direction
|
296
|
+
when 'left' then 'start'
|
297
|
+
when 'center' then 'middle'
|
298
|
+
when 'right' then 'end'
|
299
|
+
else 'start'
|
300
|
+
end
|
301
|
+
# Set y if provided
|
302
|
+
@title_align_y = y if y
|
303
|
+
end
|
304
|
+
|
305
|
+
def handle_subtitle_align(*args)
|
306
|
+
validate_align_args(args)
|
307
|
+
direction = args[0]
|
308
|
+
x = args[1]
|
309
|
+
y = args[2]
|
310
|
+
@subtitle_align = direction
|
311
|
+
@subtitle_align_x = x
|
312
|
+
@subtitle_align_y = y
|
313
|
+
if x == 'auto' || x.nil?
|
314
|
+
@subtitle_align_x = case direction
|
315
|
+
when 'left' then '5%'
|
316
|
+
when 'center' then '50%'
|
317
|
+
when 'right' then '95%'
|
318
|
+
else '5%'
|
319
|
+
end
|
320
|
+
end
|
321
|
+
if direction == 'center' && @subtitle_align_x !~ /^50%$/
|
322
|
+
warn "[BannerSVG] Warning: subtitle.align center with x=#{@subtitle_align_x} may not be visually centered."
|
323
|
+
elsif direction == 'left' && @subtitle_align_x !~ /^5%$/
|
324
|
+
warn "[BannerSVG] Warning: subtitle.align left with x=#{@subtitle_align_x} may not be visually left-aligned."
|
325
|
+
elsif direction == 'right' && @subtitle_align_x !~ /^95%$/
|
326
|
+
warn "[BannerSVG] Warning: subtitle.align right with x=#{@subtitle_align_x} may not be visually right-aligned."
|
327
|
+
end
|
328
|
+
@subtitle_text_anchor = case direction
|
329
|
+
when 'left' then 'start'
|
330
|
+
when 'center' then 'middle'
|
331
|
+
when 'right' then 'end'
|
332
|
+
else 'start'
|
333
|
+
end
|
334
|
+
@subtitle_align_y = y if y
|
335
|
+
end
|
336
|
+
|
337
|
+
def handle_title_color(*args)
|
338
|
+
validate_color_args(args)
|
339
|
+
@title_color = args.first
|
340
|
+
end
|
341
|
+
|
342
|
+
def handle_subtitle_color(*args)
|
343
|
+
validate_color_args(args)
|
344
|
+
@subtitle_color = args.first
|
345
|
+
end
|
346
|
+
|
347
|
+
private def validate_color_args(args)
|
348
|
+
raise ColorNoArgs if args.nil? || args.empty?
|
349
|
+
raise ColorFirstArgNil if args.first.nil?
|
350
|
+
raise ColorFirstArgEmpty if args.first.to_s.strip.empty?
|
351
|
+
end
|
352
|
+
|
353
|
+
def parse_header_svg(config_file = "svg.txt")
|
354
|
+
check_invariants
|
355
|
+
assume { config_file.is_a?(String) && !config_file.empty? }
|
356
|
+
|
357
|
+
lines = read_commented_file(config_file)
|
358
|
+
|
359
|
+
# Parse config into a hash
|
360
|
+
cfg = {}
|
361
|
+
lines.each do |line|
|
362
|
+
key, *values = line.split(/\s+/)
|
363
|
+
cfg[key.strip] = Array(values) if key && values
|
364
|
+
end
|
365
|
+
|
366
|
+
# Use instance variables instead of local variables
|
367
|
+
handlers = {
|
368
|
+
"back.color" => ->(args) { handle_background(*args) },
|
369
|
+
"back.linear" => ->(args) { handle_linear_gradient(*args) },
|
370
|
+
"back.radial" => ->(args) { handle_radial_gradient(*args) },
|
371
|
+
"back.image" => ->(args) { handle_image_background(*args) },
|
372
|
+
"aspect" => ->(args) { handle_aspect(*args) },
|
373
|
+
"preserve_aspect" => ->(args) { handle_preserve_aspect(*args) },
|
374
|
+
"text.font" => ->(args) { handle_font(*args) },
|
375
|
+
"text.color" => ->(args) { handle_text_color(*args) },
|
376
|
+
"title.color" => ->(args) { handle_title_color(*args) },
|
377
|
+
"subtitle.color" => ->(args) { handle_subtitle_color(*args) },
|
378
|
+
|
379
|
+
"title.align" => ->(args) { handle_title_align(*args) },
|
380
|
+
"subtitle.align" => ->(args) { handle_subtitle_align(*args) },
|
381
|
+
"title.scale" => ->(args) { handle_scale("title", *args) },
|
382
|
+
"subtitle.scale" => ->(args) { handle_scale("subtitle", *args) },
|
383
|
+
"title.style" => ->(args) { handle_style("title", *args) },
|
384
|
+
"subtitle.style" => ->(args) { handle_style("subtitle", *args) },
|
385
|
+
"title.xy" => ->(args) { handle_xy("title", *args) },
|
386
|
+
"subtitle.xy" => ->(args) { handle_xy("subtitle", *args) },
|
387
|
+
"text.align" => ->(args) { handle_text_align(*args) }
|
388
|
+
}
|
389
|
+
|
390
|
+
cfg.each_pair do |key, args|
|
391
|
+
handler = handlers[key]
|
392
|
+
if handler
|
393
|
+
# Skip malformed lines (empty args) to avoid validation errors
|
394
|
+
next if args.nil? || args.empty?
|
395
|
+
handler.call(args)
|
396
|
+
end
|
397
|
+
end
|
398
|
+
|
399
|
+
# Check for align/xy conflicts and warn
|
400
|
+
# Note: xy coordinates take precedence over align coordinates when both are set
|
401
|
+
if @title_align && @title_xy && @title_align_x && @title_xy[0] && @title_align_x != @title_xy[0]
|
402
|
+
warn "[BannerSVG] Warning: title.align x=#{@title_align_x} conflicts with title.xy x=#{@title_xy[0]} (xy will override)"
|
403
|
+
end
|
404
|
+
if @subtitle_align && @subtitle_xy && @subtitle_align_x && @subtitle_xy[0] && @subtitle_align_x != @subtitle_xy[0]
|
405
|
+
warn "[BannerSVG] Warning: subtitle.align x=#{@subtitle_align_x} conflicts with subtitle.xy x=#{@subtitle_xy[0]} (xy will override)"
|
406
|
+
end
|
407
|
+
|
408
|
+
# Recalculate font sizes after config parsing
|
409
|
+
@title_font_size = @base_font_size * @title_scale
|
410
|
+
@subtitle_font_size = @base_font_size * @subtitle_scale
|
411
|
+
|
412
|
+
width = 800 # Arbitrary starting point for calculations
|
413
|
+
height = (width / @aspect).to_i # height calculated based on aspect ratio
|
414
|
+
|
415
|
+
# Handle background (image, radial gradient, linear gradient, or solid color)
|
416
|
+
background_svg = ""
|
417
|
+
if @image_background
|
418
|
+
# Generate image background
|
419
|
+
background_svg = <<~IMAGE
|
420
|
+
<defs>
|
421
|
+
<pattern id="bg-pattern" x="0" y="0" width="100%" height="100%" patternUnits="objectBoundingBox">
|
422
|
+
<image href="#{@image_background}" x="0" y="0" width="100%" height="100%"
|
423
|
+
preserveAspectRatio="xMidYMid slice" />
|
424
|
+
</pattern>
|
425
|
+
</defs>
|
426
|
+
<rect x='0' y='0' width='100%' height='100%' fill='url(#bg-pattern)' />
|
427
|
+
IMAGE
|
428
|
+
elsif @radial_start_color && @radial_end_color
|
429
|
+
# Generate radial gradient
|
430
|
+
background_svg = <<~RADIAL
|
431
|
+
<defs>
|
432
|
+
<radialGradient id="radial1" cx="50%" cy="50%" r="50%">
|
433
|
+
<stop offset="0%" style="stop-color:#{@radial_start_color};stop-opacity:1" />
|
434
|
+
<stop offset="100%" style="stop-color:#{@radial_end_color};stop-opacity:1" />
|
435
|
+
</radialGradient>
|
436
|
+
</defs>
|
437
|
+
<rect x='0' y='0' width='100%' height='100%' fill='url(#radial1)' />
|
438
|
+
RADIAL
|
439
|
+
elsif @gradient_start_color && @gradient_end_color
|
440
|
+
# Generate linear gradient
|
441
|
+
directions = {
|
442
|
+
"lr" => ["0%", "0%", "100%", "0%"],
|
443
|
+
"tb" => ["0%", "0%", "0%", "100%"],
|
444
|
+
"ul-lr" => ["0%", "0%", "100%", "100%"],
|
445
|
+
"ll-ur" => ["0%", "100%", "100%", "0%"]
|
446
|
+
}
|
447
|
+
|
448
|
+
direction_coords = directions[@gradient_direction] || directions["lr"]
|
449
|
+
x1, y1, x2, y2 = direction_coords
|
450
|
+
|
451
|
+
background_svg = <<~GRADIENT
|
452
|
+
<defs>
|
453
|
+
<linearGradient id="grad1" x1="#{x1}" y1="#{y1}" x2="#{x2}" y2="#{y2}">
|
454
|
+
<stop offset="0%" style="stop-color:#{@gradient_start_color};stop-opacity:1" />
|
455
|
+
<stop offset="100%" style="stop-color:#{@gradient_end_color};stop-opacity:1" />
|
456
|
+
</linearGradient>
|
457
|
+
</defs>
|
458
|
+
<rect x='0' y='0' width='100%' height='100%' fill='url(#grad1)' />
|
459
|
+
GRADIENT
|
460
|
+
else
|
461
|
+
# Solid color background
|
462
|
+
background_svg = "<rect x='0' y='0' width='100%' height='100%' fill='#{@background}' />"
|
463
|
+
end
|
464
|
+
|
465
|
+
# Build style strings
|
466
|
+
title_style = "font-family: #{@font}; "
|
467
|
+
title_style << "font-size: #{@title_font_size}px; "
|
468
|
+
title_style << "font-weight: #{@title_weight}; "
|
469
|
+
title_style << "font-style: #{@title_style}"
|
470
|
+
|
471
|
+
subtitle_style = "font-family: #{@font}; "
|
472
|
+
subtitle_style << "font-size: #{@subtitle_font_size}px; "
|
473
|
+
subtitle_style << "font-weight: #{@subtitle_weight}; "
|
474
|
+
subtitle_style << "font-style: #{@subtitle_style}"
|
475
|
+
|
476
|
+
# Get xy coordinates if set, otherwise use alignment fallbacks
|
477
|
+
if @title_xy_set && @title_xy
|
478
|
+
title_x = @title_xy[0]
|
479
|
+
title_y = @title_xy[1]
|
480
|
+
else
|
481
|
+
title_x = @title_align_x || '5%'
|
482
|
+
title_y = @title_align_y || '52%'
|
483
|
+
end
|
484
|
+
if @subtitle_xy_set && @subtitle_xy
|
485
|
+
subtitle_x = @subtitle_xy[0]
|
486
|
+
subtitle_y = @subtitle_xy[1]
|
487
|
+
else
|
488
|
+
subtitle_x = @subtitle_align_x || '5%'
|
489
|
+
subtitle_y = @subtitle_align_y || '82%'
|
490
|
+
end
|
491
|
+
|
492
|
+
title_svg = <<~EOS
|
493
|
+
<text x='#{title_x}'
|
494
|
+
y='#{title_y}'
|
495
|
+
text-anchor='#{@text_anchor}'
|
496
|
+
style='#{title_style}'
|
497
|
+
fill='#{@text_color}'>#@title</text>
|
498
|
+
EOS
|
499
|
+
|
500
|
+
# Call generate_svg to return the complete SVG
|
501
|
+
generate_svg
|
502
|
+
end
|
503
|
+
|
504
|
+
def generate_svg
|
505
|
+
check_invariants
|
506
|
+
|
507
|
+
width = 800 # Arbitrary starting point for calculations
|
508
|
+
height = (width / @aspect).to_i # height calculated based on aspect ratio
|
509
|
+
|
510
|
+
# Handle background (image, radial gradient, linear gradient, or solid color)
|
511
|
+
background_svg = ""
|
512
|
+
if @image_background
|
513
|
+
# Generate image background
|
514
|
+
background_svg = <<~IMAGE
|
515
|
+
<defs>
|
516
|
+
<pattern id="bg-pattern" x="0" y="0" width="100%" height="100%" patternUnits="objectBoundingBox">
|
517
|
+
<image href="#{@image_background}" x="0" y="0" width="100%" height="100%"
|
518
|
+
preserveAspectRatio="xMidYMid slice" />
|
519
|
+
</pattern>
|
520
|
+
</defs>
|
521
|
+
<rect x='0' y='0' width='100%' height='100%' fill='url(#bg-pattern)' />
|
522
|
+
IMAGE
|
523
|
+
elsif @radial_start_color && @radial_end_color
|
524
|
+
# Calculate aspect ratio compensation for gradientTransform
|
525
|
+
ar = @radial_ar || (1.0 / @aspect)
|
526
|
+
# Compensate cx for X scaling so that cx visually matches the intended center
|
527
|
+
cx_val = @radial_cx
|
528
|
+
if cx_val.is_a?(String) && cx_val.strip.end_with?('%')
|
529
|
+
cx_num = cx_val.strip.chomp('%').to_f
|
530
|
+
cx_val = (cx_num / ar).to_s + '%'
|
531
|
+
end
|
532
|
+
gradient_transform = "gradientTransform=\"scale(#{ar},1)\"" if ar
|
533
|
+
background_svg = <<~RADIAL
|
534
|
+
<defs>
|
535
|
+
<radialGradient id="radial1" cx="#{cx_val}" cy="#{@radial_cy}" r="#{@radial_r}" #{gradient_transform}>
|
536
|
+
<stop offset="0%" style="stop-color:#{@radial_start_color};stop-opacity:1" />
|
537
|
+
<stop offset="100%" style="stop-color:#{@radial_end_color};stop-opacity:1" />
|
538
|
+
</radialGradient>
|
539
|
+
</defs>
|
540
|
+
<rect x='0' y='0' width='100%' height='100%' fill='url(#radial1)' />
|
541
|
+
RADIAL
|
542
|
+
elsif @gradient_start_color && @gradient_end_color
|
543
|
+
# Generate linear gradient
|
544
|
+
directions = {
|
545
|
+
"lr" => ["0%", "0%", "100%", "0%"],
|
546
|
+
"tb" => ["0%", "0%", "0%", "100%"],
|
547
|
+
"ul-lr" => ["0%", "0%", "100%", "100%"],
|
548
|
+
"ll-ur" => ["0%", "100%", "100%", "0%"]
|
549
|
+
}
|
550
|
+
|
551
|
+
direction_coords = directions[@gradient_direction] || directions["lr"]
|
552
|
+
x1, y1, x2, y2 = direction_coords
|
553
|
+
|
554
|
+
background_svg = <<~GRADIENT
|
555
|
+
<defs>
|
556
|
+
<linearGradient id="grad1" x1="#{x1}" y1="#{y1}" x2="#{x2}" y2="#{y2}">
|
557
|
+
<stop offset="0%" style="stop-color:#{@gradient_start_color};stop-opacity:1" />
|
558
|
+
<stop offset="100%" style="stop-color:#{@gradient_end_color};stop-opacity:1" />
|
559
|
+
</linearGradient>
|
560
|
+
</defs>
|
561
|
+
<rect x='0' y='0' width='100%' height='100%' fill='url(#grad1)' />
|
562
|
+
GRADIENT
|
563
|
+
else
|
564
|
+
# Solid color background
|
565
|
+
background_svg = "<rect x='0' y='0' width='100%' height='100%' fill='#{@background}' />"
|
566
|
+
end
|
567
|
+
|
568
|
+
# Build style strings
|
569
|
+
title_style = "font-family: #{@font}; "
|
570
|
+
title_style << "font-size: #{@title_font_size}px; "
|
571
|
+
title_style << "font-weight: #{@title_weight}; "
|
572
|
+
title_style << "font-style: #{@title_style}"
|
573
|
+
|
574
|
+
subtitle_style = "font-family: #{@font}; "
|
575
|
+
subtitle_style << "font-size: #{@subtitle_font_size}px; "
|
576
|
+
subtitle_style << "font-weight: #{@subtitle_weight}; "
|
577
|
+
subtitle_style << "font-style: #{@subtitle_style}"
|
578
|
+
|
579
|
+
title_color = @title_color || @text_color
|
580
|
+
subtitle_color = @subtitle_color || @text_color
|
581
|
+
|
582
|
+
# Get xy coordinates if set, otherwise use alignment fallbacks
|
583
|
+
if @title_xy_set && @title_xy
|
584
|
+
title_x = @title_xy[0]
|
585
|
+
title_y = @title_xy[1]
|
586
|
+
else
|
587
|
+
title_x = @title_align_x || '5%'
|
588
|
+
title_y = @title_align_y || '52%'
|
589
|
+
end
|
590
|
+
|
591
|
+
if @subtitle_xy_set && @subtitle_xy
|
592
|
+
subtitle_x = @subtitle_xy[0]
|
593
|
+
subtitle_y = @subtitle_xy[1]
|
594
|
+
else
|
595
|
+
subtitle_x = @subtitle_align_x || '5%'
|
596
|
+
subtitle_y = @subtitle_align_y || '82%'
|
597
|
+
end
|
598
|
+
|
599
|
+
title_anchor = @title_text_anchor || @text_anchor
|
600
|
+
subtitle_anchor = @subtitle_text_anchor || @text_anchor
|
601
|
+
|
602
|
+
title_svg = <<~EOS
|
603
|
+
<text x='#{title_x}'
|
604
|
+
y='#{title_y}'
|
605
|
+
text-anchor='#{title_anchor}'
|
606
|
+
style='#{title_style}'
|
607
|
+
fill='#{title_color}'>#@title</text>
|
608
|
+
EOS
|
609
|
+
subtitle_svg = <<~EOS
|
610
|
+
<text x='#{subtitle_x}'
|
611
|
+
y='#{subtitle_y}'
|
612
|
+
text-anchor='#{subtitle_anchor}'
|
613
|
+
style='#{subtitle_style}'
|
614
|
+
fill='#{subtitle_color}'>#@subtitle</text>
|
615
|
+
EOS
|
616
|
+
|
617
|
+
# Define the SVG output
|
618
|
+
# Use different preserveAspectRatio for radial gradients to maintain circular shape
|
619
|
+
preserve_aspect = if @radial_start_color && @radial_end_color && @preserve_aspect
|
620
|
+
@preserve_aspect
|
621
|
+
elsif @radial_start_color && @radial_end_color
|
622
|
+
'xMidYMid slice' # Default for radial gradients: crop to maintain aspect ratio
|
623
|
+
else
|
624
|
+
'xMidYMid meet' # Default for other backgrounds: fit within bounds
|
625
|
+
end
|
626
|
+
|
627
|
+
svg = <<~SVG
|
628
|
+
<svg xmlns='http://www.w3.org/2000/svg'
|
629
|
+
width='100%' height='#{height}'
|
630
|
+
viewBox='0 0 #{width} #{height}'
|
631
|
+
preserveAspectRatio='#{preserve_aspect}'>
|
632
|
+
#{background_svg}
|
633
|
+
#{title_svg}
|
634
|
+
#{subtitle_svg}
|
635
|
+
</svg>
|
636
|
+
SVG
|
637
|
+
|
638
|
+
svg
|
639
|
+
end
|
640
|
+
|
641
|
+
def get_svg
|
642
|
+
check_invariants
|
643
|
+
|
644
|
+
# Generate SVG without re-parsing config (use current instance variables)
|
645
|
+
svg_code = generate_svg
|
646
|
+
svg_lines = svg_code.split("\n").map {|line| " "*6 + line }
|
647
|
+
svg_code = svg_lines.join("\n")
|
648
|
+
|
649
|
+
# Calculate coordinates safely
|
650
|
+
title_x = @title_xy_set && @title_xy ? @title_xy[0] : (@title_align_x || '5%')
|
651
|
+
title_y = @title_xy_set && @title_xy ? @title_xy[1] : (@title_align_y || '52%')
|
652
|
+
subtitle_x = @subtitle_xy_set && @subtitle_xy ? @subtitle_xy[0] : (@subtitle_align_x || '5%')
|
653
|
+
subtitle_y = @subtitle_xy_set && @subtitle_xy ? @subtitle_xy[1] : (@subtitle_align_y || '82%')
|
654
|
+
|
655
|
+
code = <<~EOS
|
656
|
+
<script>
|
657
|
+
function insert_svg_header(container) {
|
658
|
+
const svg_text = `#{svg_code}`;
|
659
|
+
const svgElement = document.createElement('div');
|
660
|
+
svgElement.innerHTML = svg_text;
|
661
|
+
const svg = svgElement.firstElementChild;
|
662
|
+
|
663
|
+
const svgWidth = window.innerWidth;
|
664
|
+
const aspectRatio = #{@aspect};
|
665
|
+
const svgHeight = svgWidth / aspectRatio;
|
666
|
+
|
667
|
+
svg.setAttribute('viewBox', `0 0 ${svgWidth} ${svgHeight}`);
|
668
|
+
svg.setAttribute('width', svgWidth);
|
669
|
+
svg.setAttribute('height', svgHeight);
|
670
|
+
|
671
|
+
const titleFontSize = #{@title_scale} * #{@base_font_size};
|
672
|
+
const subtitleFontSize = #{@subtitle_scale} * #{@base_font_size};
|
673
|
+
|
674
|
+
const te1 = svg.querySelector('text:nth-of-type(1)')
|
675
|
+
const te2 = svg.querySelector('text:nth-of-type(2)')
|
676
|
+
|
677
|
+
// Don't override the styles - they're already set correctly in the SVG
|
678
|
+
// Just update the positioning and text-anchor
|
679
|
+
|
680
|
+
const titleXpct = "#{title_x}";
|
681
|
+
const titleYpct = "#{title_y}";
|
682
|
+
const subtitleXpct = "#{subtitle_x}";
|
683
|
+
const subtitleYpct = "#{subtitle_y}";
|
684
|
+
|
685
|
+
const tX = svgWidth * (parseFloat(titleXpct) / 100);
|
686
|
+
const tY = svgHeight * (parseFloat(titleYpct) / 100);
|
687
|
+
const sX = svgWidth * (parseFloat(subtitleXpct) / 100);
|
688
|
+
const sY = svgHeight * (parseFloat(subtitleYpct) / 100);
|
689
|
+
|
690
|
+
te1.setAttribute('x', tX);
|
691
|
+
te1.setAttribute('y', tY);
|
692
|
+
te2.setAttribute('x', sX);
|
693
|
+
te2.setAttribute('y', sY);
|
694
|
+
|
695
|
+
// Set text-anchor for proper positioning (use individual anchors if set)
|
696
|
+
te1.setAttribute('text-anchor', '#{@title_text_anchor || @text_anchor}');
|
697
|
+
te2.setAttribute('text-anchor', '#{@subtitle_text_anchor || @text_anchor}');
|
698
|
+
|
699
|
+
// Apply calculated font sizes
|
700
|
+
te1.setAttribute('font-size', titleFontSize + 'px');
|
701
|
+
te2.setAttribute('font-size', subtitleFontSize + 'px');
|
702
|
+
|
703
|
+
const containerElement = document.getElementById(container);
|
704
|
+
if (containerElement) {
|
705
|
+
console.log('Container found, inserting SVG...');
|
706
|
+
containerElement.innerHTML = svg.outerHTML;
|
707
|
+
console.log('SVG inserted successfully');
|
708
|
+
} else {
|
709
|
+
console.error('Container not found:', container);
|
710
|
+
}
|
711
|
+
}
|
712
|
+
|
713
|
+
console.log('SVG script loaded');
|
714
|
+
|
715
|
+
// Use DOMContentLoaded to avoid conflicts with main window.onload
|
716
|
+
document.addEventListener('DOMContentLoaded', function() {
|
717
|
+
console.log('DOM ready, trying SVG insertion...');
|
718
|
+
insert_svg_header('header');
|
719
|
+
});
|
720
|
+
</script>
|
721
|
+
EOS
|
722
|
+
|
723
|
+
code
|
724
|
+
end
|
725
|
+
|
726
|
+
# How to call?? bsvg = BannerSVG.new(...); bsvg.parse_svg_header; code = bsvg.get_svg # Simplify?
|
727
|
+
# Doesn't output, just returns string...
|
728
|
+
|
729
|
+
end
|