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,1021 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
class Scriptorium::View
|
4
|
+
include Scriptorium::Exceptions
|
5
|
+
include Scriptorium::Helpers
|
6
|
+
include Scriptorium::Contract
|
7
|
+
|
8
|
+
attr_reader :name, :title, :subtitle, :theme, :dir
|
9
|
+
|
10
|
+
def self.create_sample_view(repo)
|
11
|
+
repo.create_view("sample", "My first view", "This is just a sample")
|
12
|
+
# repo.generate_front_page("sample")
|
13
|
+
end
|
14
|
+
|
15
|
+
# Invariants
|
16
|
+
def define_invariants
|
17
|
+
invariant { @name.is_a?(String) && !@name.empty? }
|
18
|
+
invariant { @title.is_a?(String) && !@title.empty? }
|
19
|
+
invariant { @subtitle.is_a?(String) }
|
20
|
+
invariant { @theme.is_a?(String) && !@theme.empty? }
|
21
|
+
invariant { @root.is_a?(String) && !@root.empty? }
|
22
|
+
invariant { @repo.is_a?(Scriptorium::Repo) }
|
23
|
+
invariant { @dir.is_a?(String) && !@dir.empty? }
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize(name, title, subtitle = "", theme = "standard")
|
27
|
+
msg = "name must be a String, got #{name.class}"
|
28
|
+
assume(msg) { name.is_a?(String) }
|
29
|
+
msg = "title must be a String, got #{title.class}"
|
30
|
+
assume(msg) { title.is_a?(String) }
|
31
|
+
msg = "subtitle must be a String, got #{subtitle.class}"
|
32
|
+
assume(msg) { subtitle.is_a?(String) }
|
33
|
+
msg = "theme must be a String, got #{theme.class}"
|
34
|
+
assume(msg) { theme.is_a?(String) }
|
35
|
+
|
36
|
+
validate_name(name)
|
37
|
+
validate_title(title)
|
38
|
+
|
39
|
+
@name, @title, @subtitle, @theme = name, title, subtitle, theme
|
40
|
+
@root = Scriptorium::Repo.root
|
41
|
+
@repo = Scriptorium::Repo.repo
|
42
|
+
@dir = "#@root/views/#{name}"
|
43
|
+
@predef = Scriptorium::StandardFiles.new
|
44
|
+
|
45
|
+
define_invariants
|
46
|
+
verify { @name == name }
|
47
|
+
verify { @title == title }
|
48
|
+
check_invariants
|
49
|
+
end
|
50
|
+
|
51
|
+
def inspect
|
52
|
+
"<View: #@name #{@title.inspect} theme: #@theme>"
|
53
|
+
end
|
54
|
+
|
55
|
+
private def validate_name(name)
|
56
|
+
raise ViewNameNil if name.nil?
|
57
|
+
|
58
|
+
raise ViewNameEmpty if name.to_s.strip.empty?
|
59
|
+
|
60
|
+
unless name.match?(/^[a-zA-Z0-9_-]+$/)
|
61
|
+
raise ViewNameInvalid(name)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
private def validate_title(title)
|
66
|
+
raise ViewTitleNil if title.nil?
|
67
|
+
|
68
|
+
raise ViewTitleEmpty if title.to_s.strip.empty?
|
69
|
+
end
|
70
|
+
|
71
|
+
def read_layout
|
72
|
+
layout_file = @dir/:config/"layout.txt"
|
73
|
+
|
74
|
+
need(:file, layout_file, LayoutFileMissing)
|
75
|
+
|
76
|
+
lines = read_commented_file(layout_file)
|
77
|
+
containers = {}
|
78
|
+
secs = []
|
79
|
+
lines.each do |line|
|
80
|
+
sec, args = line.split(/\s+/, 2)
|
81
|
+
containers[sec] = (args || "")
|
82
|
+
secs << sec
|
83
|
+
end
|
84
|
+
directives = %w[header footer left right main]
|
85
|
+
secs.each {|sec| raise LayoutHasUnknownTag(sec) unless directives.include?(sec)}
|
86
|
+
directives.each {|sec| raise LayoutHasDuplicateTags(sec) if lines.count(sec) > 1}
|
87
|
+
containers
|
88
|
+
end
|
89
|
+
|
90
|
+
def generate_empty_containers
|
91
|
+
layout_file = @dir/:config/"layout.txt"
|
92
|
+
return unless File.exist?(layout_file)
|
93
|
+
|
94
|
+
flexing = {
|
95
|
+
header: %[id="header" class="header" style="padding: 10px; width: 100%; box-sizing: border-box;"],
|
96
|
+
footer: %[class="footer" style="background: lightgray; padding: 10px;"],
|
97
|
+
left: %[class="left" style="width: %{width}; background: #f0f0f0; padding: 10px; flex-grow: 0; flex-shrink: 0;"],
|
98
|
+
right: %[class="right" style="width: %{width}; background: #f0f0f0; padding: 10px; flex-grow: 0; flex-shrink: 0;"],
|
99
|
+
main: %[class="main" style="flex-grow: 1; padding: 10px;"]
|
100
|
+
}
|
101
|
+
sections = read_layout
|
102
|
+
lines = sections.keys
|
103
|
+
# FIXME Pleeeease refactor this.
|
104
|
+
lines.each do |section|
|
105
|
+
args = sections[section] # like 20% for right, left
|
106
|
+
filename = @dir/:layout/"#{section}.html"
|
107
|
+
tag = section # header, footer, main
|
108
|
+
tag = "aside" if section == 'left' || tag == 'right'
|
109
|
+
|
110
|
+
inline = flexing[section.to_sym]
|
111
|
+
if section == "left" || section == "right"
|
112
|
+
mod = {width: args}
|
113
|
+
inline = inline % mod
|
114
|
+
end
|
115
|
+
content = <<~HTML
|
116
|
+
<#{tag} #{inline}>
|
117
|
+
<!-- Section: #{section} -->
|
118
|
+
</#{tag}>
|
119
|
+
HTML
|
120
|
+
|
121
|
+
write_file(filename, content)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def theme(change = nil)
|
126
|
+
return @theme if change.nil?
|
127
|
+
# what if it doesn't exist?
|
128
|
+
need(:dir, @root/:themes/change, ThemeDoesntExist)
|
129
|
+
@theme = change
|
130
|
+
change_config(@dir/"config.txt", "theme", change)
|
131
|
+
apply_theme(change)
|
132
|
+
end
|
133
|
+
|
134
|
+
def apply_theme(theme)
|
135
|
+
check_invariants
|
136
|
+
msg = "theme must be a non-empty String, got #{theme.class} (#{theme.inspect})"
|
137
|
+
assume(msg) { theme.is_a?(String) && !theme.empty? }
|
138
|
+
|
139
|
+
# check to see if ever done before?
|
140
|
+
# copy layout.txt to view
|
141
|
+
t = Scriptorium::Theme.new(@root, theme)
|
142
|
+
need(:file, t.file("layout.txt"), ThemeFileNotFound)
|
143
|
+
FileUtils.cp(t.file("layout.txt"), @dir/:config)
|
144
|
+
# copy other .txt to view? header, footer, ...
|
145
|
+
names = %w[header footer left right main]
|
146
|
+
lay = @root/:themes/theme/:layout
|
147
|
+
names.each do |name|
|
148
|
+
f1, f2 = lay/:config/"#{name}.txt", dir/:config
|
149
|
+
need(:file, f1, ThemeFileNotFound)
|
150
|
+
FileUtils.cp(f1, f2)
|
151
|
+
end
|
152
|
+
generate_empty_containers
|
153
|
+
|
154
|
+
verify { @theme == theme }
|
155
|
+
check_invariants
|
156
|
+
end
|
157
|
+
|
158
|
+
def content_tag(section)
|
159
|
+
"<!-- Section: #{section} -->"
|
160
|
+
end
|
161
|
+
|
162
|
+
def placeholder_text(str)
|
163
|
+
if str.start_with?("@")
|
164
|
+
file = @dir/:config/:text/"#{str[1..]}"
|
165
|
+
read_file(file, missing_fallback: "[Missing: #{file}]")
|
166
|
+
else
|
167
|
+
str
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def section_append(sec, str)
|
172
|
+
file = @dir/:config/"#{sec}.txt"
|
173
|
+
text = read_file(file)
|
174
|
+
text << str
|
175
|
+
write_file(file, text)
|
176
|
+
end
|
177
|
+
|
178
|
+
def section_hash(section)
|
179
|
+
hash = Hash.new { |hash, key| ->(arg = nil) { "<!-- Not defined for key: #{key} -->\n" } }
|
180
|
+
hash["text"] = ->(arg) { " <p>" + placeholder_text(arg) + "</p>\n" }
|
181
|
+
hash
|
182
|
+
end
|
183
|
+
|
184
|
+
def section_core(section, hash)
|
185
|
+
cfg = @dir/:config
|
186
|
+
sectxt = cfg/"#{section}.txt"
|
187
|
+
|
188
|
+
# Only add placeholder if section has no real content
|
189
|
+
lines = read_commented_file(sectxt)
|
190
|
+
if lines.empty? && section != "main"
|
191
|
+
section_append(section, "\ntext This is #{section}...")
|
192
|
+
lines = read_commented_file(sectxt)
|
193
|
+
end
|
194
|
+
|
195
|
+
result = "<!-- Section: #{section} (output) -->\n"
|
196
|
+
lines.each do |line|
|
197
|
+
component, arg = line.split(/\s+/, 2)
|
198
|
+
|
199
|
+
# Handle malformed config lines
|
200
|
+
if component.nil? || component.strip.empty?
|
201
|
+
result << "<!-- Invalid config line: #{line.inspect} -->\n"
|
202
|
+
next
|
203
|
+
end
|
204
|
+
|
205
|
+
component = component.downcase
|
206
|
+
if hash.key?(component)
|
207
|
+
result << hash[component].call(arg)
|
208
|
+
else
|
209
|
+
result << "<!-- Unknown component: #{component} -->\n"
|
210
|
+
end
|
211
|
+
end
|
212
|
+
result
|
213
|
+
end
|
214
|
+
|
215
|
+
=begin
|
216
|
+
To build a header, I start with two things:
|
217
|
+
config/header.txt (which is user-supplied and has things such as "title" in it); and
|
218
|
+
layout/header.html (which is a template with <header> tags enclosing at least a line
|
219
|
+
like "<!-- Section: header -->"
|
220
|
+
|
221
|
+
get core: I process header.txt line by line, gathering the "core" or "guts" of the header.
|
222
|
+
sub into template: I substitute this into the template contents and
|
223
|
+
write output: write the result to output/panes/header.html
|
224
|
+
=end
|
225
|
+
|
226
|
+
def build_section(section, hash2 = {}, args = "")
|
227
|
+
template = @dir/:layout/"#{section}.html"
|
228
|
+
output = @dir/:output/:panes/"#{section}.html"
|
229
|
+
|
230
|
+
# Ensure output directory exists
|
231
|
+
FileUtils.mkdir_p(File.dirname(output))
|
232
|
+
|
233
|
+
# Check if template exists
|
234
|
+
need(:file, template)
|
235
|
+
|
236
|
+
hash = section_hash(section)
|
237
|
+
hash.merge!(hash2)
|
238
|
+
core = section_core(section, hash)
|
239
|
+
|
240
|
+
temp_txt = read_file(template)
|
241
|
+
|
242
|
+
target = content_tag(section)
|
243
|
+
temp_txt.sub!(target, core)
|
244
|
+
|
245
|
+
write_file(output, temp_txt)
|
246
|
+
|
247
|
+
html = read_file(output)
|
248
|
+
html
|
249
|
+
end
|
250
|
+
|
251
|
+
def build_header(sections)
|
252
|
+
args = sections["header"]
|
253
|
+
return "" unless args
|
254
|
+
h2 = {
|
255
|
+
"title" => ->(arg = nil) { " <h1>#{escape_html(@title)}</h1>" },
|
256
|
+
"subtitle" => ->(arg = nil) { " <p>#{escape_html(@subtitle)}</p>" },
|
257
|
+
"nav" => ->(arg = nil) { build_nav(arg) },
|
258
|
+
"banner" => ->(arg = nil) { build_banner(arg) }
|
259
|
+
}
|
260
|
+
|
261
|
+
build_section("header", h2, args)
|
262
|
+
end
|
263
|
+
|
264
|
+
### Helpers for header
|
265
|
+
|
266
|
+
def build_banner(arg)
|
267
|
+
# Check if this is an SVG banner request
|
268
|
+
return build_banner_svg("svg") if arg == "svg"
|
269
|
+
|
270
|
+
# Otherwise, treat as image filename
|
271
|
+
return build_banner_image(arg)
|
272
|
+
end
|
273
|
+
|
274
|
+
|
275
|
+
|
276
|
+
def build_banner_image(image_filename)
|
277
|
+
# Search for image in multiple locations
|
278
|
+
image_paths = [
|
279
|
+
@dir/:assets/image_filename, # view/assets/
|
280
|
+
@repo.root/:assets/image_filename, # repo/assets/
|
281
|
+
]
|
282
|
+
|
283
|
+
# Find the first existing image
|
284
|
+
image_path = image_paths.find { |path| File.exist?(path) }
|
285
|
+
|
286
|
+
if image_path
|
287
|
+
# Use relative path for the img src
|
288
|
+
if image_path.to_s.start_with?(@dir.to_s)
|
289
|
+
# Image is in view directory, use relative path
|
290
|
+
relative_path = image_path.to_s.sub(@dir.to_s + "/", "")
|
291
|
+
else
|
292
|
+
# Image is in repo directory, use relative path from view
|
293
|
+
relative_path = "../assets/#{image_filename}"
|
294
|
+
end
|
295
|
+
html = %[<img src='#{relative_path}' alt='Banner Image' style='width: 100%; height: auto;' />]
|
296
|
+
return html
|
297
|
+
else
|
298
|
+
# Try to copy from global assets
|
299
|
+
global_assets_dir = @repo.root/:assets
|
300
|
+
global_image_path = global_assets_dir/image_filename
|
301
|
+
|
302
|
+
if File.exist?(global_image_path)
|
303
|
+
# Copy to view assets
|
304
|
+
view_assets_dir = @dir/:assets
|
305
|
+
make_dir(view_assets_dir) unless Dir.exist?(view_assets_dir)
|
306
|
+
FileUtils.cp(global_image_path, view_assets_dir/image_filename)
|
307
|
+
|
308
|
+
# Use relative path
|
309
|
+
relative_path = "assets/#{image_filename}"
|
310
|
+
html = %[<img src='#{relative_path}' alt='Banner Image' style='width: 100%; height: auto;' />]
|
311
|
+
return html
|
312
|
+
else
|
313
|
+
# Image not found anywhere
|
314
|
+
html = %[<p>Banner image missing: #{image_filename}</p>]
|
315
|
+
return html
|
316
|
+
end
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
def build_banner_svg(arg)
|
321
|
+
bsvg = Scriptorium::BannerSVG.new(@title, @subtitle)
|
322
|
+
|
323
|
+
# Look for svg.txt file in the view's config directory
|
324
|
+
svg_config_file = @dir/:config/"svg.txt"
|
325
|
+
if File.exist?(svg_config_file)
|
326
|
+
bsvg.parse_header_svg(svg_config_file)
|
327
|
+
else
|
328
|
+
# No svg.txt file, just use defaults
|
329
|
+
bsvg.parse_header_svg
|
330
|
+
end
|
331
|
+
|
332
|
+
bsvg.get_svg
|
333
|
+
end
|
334
|
+
|
335
|
+
def build_nav(arg)
|
336
|
+
# Determine navbar file - if no arg, use navbar.txt, otherwise use specified file
|
337
|
+
nav_file = if arg.nil? || arg.strip.empty?
|
338
|
+
@dir/:config/"navbar.txt"
|
339
|
+
else
|
340
|
+
@dir/:config/"#{arg}"
|
341
|
+
end
|
342
|
+
|
343
|
+
# Read navbar content with fallback for missing files
|
344
|
+
nav_content = read_file(nav_file, missing_fallback: "<p>Navigation not available</p>")
|
345
|
+
|
346
|
+
# Parse and generate Bootstrap navbar
|
347
|
+
generate_bootstrap_navbar(nav_content)
|
348
|
+
end
|
349
|
+
|
350
|
+
def generate_bootstrap_navbar(nav_content)
|
351
|
+
menu_items = parse_navbar_content(nav_content)
|
352
|
+
|
353
|
+
# Generate Bootstrap navbar HTML wrapped in bootstrap-scope
|
354
|
+
html = <<~HTML
|
355
|
+
<div class="bootstrap-scope">
|
356
|
+
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
357
|
+
<div class="container-fluid">
|
358
|
+
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
359
|
+
<span class="navbar-toggler-icon"></span>
|
360
|
+
</button>
|
361
|
+
<div class="collapse navbar-collapse" id="navbarNav">
|
362
|
+
<ul class="navbar-nav">
|
363
|
+
#{generate_navbar_items(menu_items)}
|
364
|
+
</ul>
|
365
|
+
</div>
|
366
|
+
</div>
|
367
|
+
</nav>
|
368
|
+
</div>
|
369
|
+
HTML
|
370
|
+
|
371
|
+
html
|
372
|
+
end
|
373
|
+
|
374
|
+
def parse_navbar_content(content)
|
375
|
+
menu_items = []
|
376
|
+
current_dropdown = nil
|
377
|
+
|
378
|
+
content.lines.each do |line|
|
379
|
+
line = line.rstrip # Keep leading spaces, remove trailing
|
380
|
+
next if line.empty? || line.start_with?('#')
|
381
|
+
|
382
|
+
if line.start_with?('=')
|
383
|
+
# Top-level dropdown item
|
384
|
+
label = line[1..-1].strip
|
385
|
+
current_dropdown = { type: :dropdown, label: label, children: [] }
|
386
|
+
menu_items << current_dropdown
|
387
|
+
elsif line.start_with?(' ')
|
388
|
+
# Child of previous dropdown
|
389
|
+
if current_dropdown
|
390
|
+
# Remove leading spaces and split on multiple spaces
|
391
|
+
clean_line = line.strip
|
392
|
+
if clean_line.include?(' ') # Look for multiple spaces
|
393
|
+
parts = clean_line.split(/\s{2,}/, 2) # Split on 2+ spaces
|
394
|
+
if parts.length >= 2
|
395
|
+
title, filename = parts[0], parts[1]
|
396
|
+
current_dropdown[:children] << { type: :child, title: title, filename: filename }
|
397
|
+
end
|
398
|
+
end
|
399
|
+
end
|
400
|
+
elsif line.start_with?('-')
|
401
|
+
# Top-level item (no children)
|
402
|
+
clean_line = line[1..-1].strip
|
403
|
+
if clean_line.include?(' ') # Look for multiple spaces
|
404
|
+
parts = clean_line.split(/\s{2,}/, 2) # Split on 2+ spaces
|
405
|
+
if parts.length >= 2
|
406
|
+
title, filename = parts[0], parts[1]
|
407
|
+
menu_items << { type: :item, title: title, filename: filename }
|
408
|
+
end
|
409
|
+
end
|
410
|
+
end
|
411
|
+
end
|
412
|
+
|
413
|
+
menu_items
|
414
|
+
end
|
415
|
+
|
416
|
+
def generate_navbar_items(menu_items)
|
417
|
+
html = ""
|
418
|
+
|
419
|
+
menu_items.each do |item|
|
420
|
+
case item[:type]
|
421
|
+
when :dropdown
|
422
|
+
html << generate_dropdown_item(item)
|
423
|
+
when :item
|
424
|
+
html << generate_nav_item(item)
|
425
|
+
end
|
426
|
+
end
|
427
|
+
|
428
|
+
html
|
429
|
+
end
|
430
|
+
|
431
|
+
def generate_dropdown_item(item)
|
432
|
+
html = <<~HTML
|
433
|
+
<li class="nav-item dropdown">
|
434
|
+
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
435
|
+
#{escape_html(item[:label])}
|
436
|
+
</a>
|
437
|
+
<ul class="dropdown-menu">
|
438
|
+
HTML
|
439
|
+
|
440
|
+
item[:children].each {|child| html << generate_dropdown_child(child) }
|
441
|
+
|
442
|
+
html << <<~HTML
|
443
|
+
</ul>
|
444
|
+
</li>
|
445
|
+
HTML
|
446
|
+
|
447
|
+
html
|
448
|
+
end
|
449
|
+
|
450
|
+
def generate_dropdown_child(child)
|
451
|
+
link_url, warning = get_page_link(child[:filename])
|
452
|
+
|
453
|
+
html = <<~HTML
|
454
|
+
<li><a class="dropdown-item" href="javascript:void(0)" onclick="load_main('#{link_url}')">#{escape_html(child[:title])}</a></li>
|
455
|
+
HTML
|
456
|
+
|
457
|
+
html << "<!-- #{warning} -->\n" if warning
|
458
|
+
|
459
|
+
html
|
460
|
+
end
|
461
|
+
|
462
|
+
def generate_nav_item(item)
|
463
|
+
link_url, warning = get_page_link(item[:filename])
|
464
|
+
|
465
|
+
html = <<~HTML
|
466
|
+
<li class="nav-item">
|
467
|
+
<a class="nav-link" href="javascript:void(0)" onclick="load_main('#{link_url}')">#{escape_html(item[:title])}</a>
|
468
|
+
</li>
|
469
|
+
HTML
|
470
|
+
|
471
|
+
html << "<!-- #{warning} -->\n" if warning
|
472
|
+
|
473
|
+
html
|
474
|
+
end
|
475
|
+
|
476
|
+
def get_page_link(filename)
|
477
|
+
# Check if the page file exists
|
478
|
+
page_file = @dir/:pages/"#{filename}.html"
|
479
|
+
|
480
|
+
if File.exist?(page_file)
|
481
|
+
# Page exists, return relative path
|
482
|
+
link_url = "pages/#{filename}.html"
|
483
|
+
warning = nil
|
484
|
+
else
|
485
|
+
# Page doesn't exist, still create link but warn
|
486
|
+
link_url = "pages/#{filename}.html"
|
487
|
+
warning = "Warning: Page file '#{filename}.html' not found in pages directory"
|
488
|
+
end
|
489
|
+
|
490
|
+
[link_url, warning]
|
491
|
+
end
|
492
|
+
|
493
|
+
def build_widgets(arg)
|
494
|
+
check_invariants
|
495
|
+
msg = "arg must be a String, got #{arg.class}"
|
496
|
+
assume(msg) { arg.is_a?(String) }
|
497
|
+
validate_widget_arg(arg)
|
498
|
+
|
499
|
+
widgets = arg.split
|
500
|
+
content = ""
|
501
|
+
widgets.each do |widget|
|
502
|
+
validate_widget_name(widget)
|
503
|
+
|
504
|
+
begin
|
505
|
+
widget_class = eval("Scriptorium::Widget::#{widget.capitalize}")
|
506
|
+
obj = widget_class.new(@repo, self)
|
507
|
+
obj.generate
|
508
|
+
content << obj.card
|
509
|
+
rescue => e
|
510
|
+
raise CannotBuildWidget("Failed to build widget '#{widget}': #{e.message}")
|
511
|
+
end
|
512
|
+
end
|
513
|
+
|
514
|
+
# Wrap widgets in bootstrap-scope container
|
515
|
+
if content.strip.empty?
|
516
|
+
result = content
|
517
|
+
else
|
518
|
+
result = <<~HTML
|
519
|
+
<div class="bootstrap-scope">
|
520
|
+
#{content}
|
521
|
+
</div>
|
522
|
+
HTML
|
523
|
+
end
|
524
|
+
|
525
|
+
verify { result.is_a?(String) }
|
526
|
+
check_invariants
|
527
|
+
result
|
528
|
+
end
|
529
|
+
|
530
|
+
private def validate_widget_arg(arg)
|
531
|
+
raise WidgetsArgNil if arg.nil?
|
532
|
+
|
533
|
+
raise WidgetsArgEmpty if arg.to_s.strip.empty?
|
534
|
+
end
|
535
|
+
|
536
|
+
private def validate_widget_name(name)
|
537
|
+
raise WidgetNameNil if name.nil? || name.strip.empty?
|
538
|
+
|
539
|
+
unless name.match?(/^[a-zA-Z0-9_]+$/)
|
540
|
+
raise WidgetNameInvalid(name)
|
541
|
+
end
|
542
|
+
end
|
543
|
+
|
544
|
+
###
|
545
|
+
|
546
|
+
def build_footer(sections)
|
547
|
+
args = sections["footer"]
|
548
|
+
return "" unless args
|
549
|
+
build_section("footer", {}, args)
|
550
|
+
end
|
551
|
+
|
552
|
+
def build_left(sections)
|
553
|
+
args = sections["left"]
|
554
|
+
return "" unless args
|
555
|
+
h2 = { "widget" => ->(arg = nil) { build_widgets(arg) } }
|
556
|
+
build_section("left", h2, args)
|
557
|
+
end
|
558
|
+
|
559
|
+
def build_right(sections)
|
560
|
+
args = sections["right"]
|
561
|
+
return "" unless args
|
562
|
+
h2 = { "widget" => ->(arg = nil) { build_widgets(arg) } }
|
563
|
+
build_section("right", h2, args)
|
564
|
+
end
|
565
|
+
|
566
|
+
def build_main(sections)
|
567
|
+
args = sections["main"]
|
568
|
+
return "" unless args
|
569
|
+
html = " <!-- Section: main (output) -->\n"
|
570
|
+
html << %[ <div id="main" class="main" style="flex-grow: 1; padding: 5px; overflow-y: auto; position: relative; display: block;">]
|
571
|
+
# html << %[<div id="main" class="main" style="position: relative; display: flex; flex-direction: column;">\n]
|
572
|
+
html << support_data('post_index/style.css')
|
573
|
+
if view_posts.empty?
|
574
|
+
html << " <h1>No posts yet!</h1>"
|
575
|
+
else
|
576
|
+
paginate_posts
|
577
|
+
need(:file, self.dir/:output/"post_index.html")
|
578
|
+
html << read_file(self.dir/:output/"post_index.html")
|
579
|
+
end
|
580
|
+
html << "</div> <!-- end main -->\n"
|
581
|
+
end
|
582
|
+
|
583
|
+
def generate_post_index
|
584
|
+
posts = @repo.all_posts(self)
|
585
|
+
config = read_post_index_config
|
586
|
+
|
587
|
+
# Build table with configurable margin-top
|
588
|
+
margin_top = config[:"index.margin.top"] || "0px"
|
589
|
+
str = "<table width=100% cellpadding=#{config[:'entry.cellpadding']} style=\"margin-top: #{margin_top};\">"
|
590
|
+
|
591
|
+
# FIXME - many decisions to make here...
|
592
|
+
posts.each {|post| str << post_index_entry(post) }
|
593
|
+
str << "</table>"
|
594
|
+
|
595
|
+
write_file(@dir/:output/"post_index.html", str)
|
596
|
+
end
|
597
|
+
|
598
|
+
def post_index_entry(post)
|
599
|
+
template = support_data('templates/index_entry.lt3')
|
600
|
+
config = read_post_index_config
|
601
|
+
vars = post.vars.merge(config)
|
602
|
+
|
603
|
+
# Add formatted_date to vars (use post.date to get pubdate/created fallback)
|
604
|
+
date_format = config[:"entry.date.format"] || "month dd break yyyy"
|
605
|
+
vars[:formatted_date] = format_date(date_format, post.date)
|
606
|
+
|
607
|
+
entry = substitute(vars, template)
|
608
|
+
entry
|
609
|
+
end
|
610
|
+
|
611
|
+
def read_post_index_config
|
612
|
+
# Read global defaults first
|
613
|
+
global_config_file = @repo.root/:config/"post_index_defaults.txt"
|
614
|
+
global_config = File.exist?(global_config_file) ? parse_commented_file(global_config_file) : {}
|
615
|
+
|
616
|
+
# Read view-specific overrides
|
617
|
+
view_config_file = @dir/:config/"post_index.txt"
|
618
|
+
view_config = File.exist?(view_config_file) ? parse_commented_file(view_config_file) : {}
|
619
|
+
|
620
|
+
# Merge: view config overrides global defaults
|
621
|
+
global_config.merge(view_config)
|
622
|
+
rescue => e
|
623
|
+
{}
|
624
|
+
end
|
625
|
+
|
626
|
+
def post_index_array
|
627
|
+
posts = view_posts.sort {|a,b| post_compare(a, b) }
|
628
|
+
posts.map {|post| post_index_entry(post)}
|
629
|
+
end
|
630
|
+
|
631
|
+
def view_posts
|
632
|
+
@repo.all_posts(self).sort {|a,b| post_compare(a, b)}
|
633
|
+
end
|
634
|
+
|
635
|
+
def generate_html_head(view = nil)
|
636
|
+
# FIXME - view does not yet override global
|
637
|
+
global_head = @root/:config/"global-head.txt"
|
638
|
+
view_head = @dir/:config/"global-head.txt"
|
639
|
+
head_file = view ? view_head : global_head
|
640
|
+
which = view ? "view" : "global"
|
641
|
+
line1 = "<!-- head info from #{which} -->"
|
642
|
+
lines = read_commented_file(head_file)
|
643
|
+
content = "<head>\n#{line1}\n<title>#{@title}</title>\n"
|
644
|
+
lines.each do |line|
|
645
|
+
component, args = line.split(/\s+/, 2)
|
646
|
+
case component.downcase
|
647
|
+
when "charset"
|
648
|
+
@charset = args
|
649
|
+
content << %[<meta charset="#{args}">\n]
|
650
|
+
when "desc"
|
651
|
+
@desc = args
|
652
|
+
content << %[<meta name="description" content="#{args}">\n]
|
653
|
+
when "viewport"
|
654
|
+
@viewport = args
|
655
|
+
str = args.split.join(" ")
|
656
|
+
content << %[<meta name="viewport" content="#{str}">\n]
|
657
|
+
when "robots"
|
658
|
+
@robots = args
|
659
|
+
str = args.split.join(", ")
|
660
|
+
content << %[<meta name="robots" content="#{str}">\n]
|
661
|
+
# when "javascript"
|
662
|
+
# content << get_common_js(view)
|
663
|
+
when "bootstrap"
|
664
|
+
content << generate_bootstrap_css(view)
|
665
|
+
when "social"
|
666
|
+
content << generate_social_meta_tags(args)
|
667
|
+
when "highlight"
|
668
|
+
content << generate_highlight_css(view) # loads highlight_css.txt
|
669
|
+
when "highlight_custom"
|
670
|
+
content << support_data('highlight/custom.css')
|
671
|
+
end
|
672
|
+
end
|
673
|
+
content << "</head>\n"
|
674
|
+
content
|
675
|
+
end
|
676
|
+
|
677
|
+
def get_common_js(view = nil)
|
678
|
+
global_js = @root/:config/"common.js"
|
679
|
+
view_js = @dir/:config/"common.js"
|
680
|
+
js_file = view ? view_js : global_js
|
681
|
+
code = read_file(js_file)
|
682
|
+
return %[<script>#{code}</script>\n]
|
683
|
+
end
|
684
|
+
|
685
|
+
def generate_bootstrap_css(view = nil)
|
686
|
+
global_boot = @root/:config/"bootstrap_css.txt"
|
687
|
+
view_boot = @dir/:config/"bootstrap_css.txt"
|
688
|
+
bs_file = view ? view_boot : global_boot
|
689
|
+
lines = read_commented_file(bs_file)
|
690
|
+
href = rel = integrity = crossorigin = nil
|
691
|
+
lines.each do |line|
|
692
|
+
component, args = line.split(/\s+/, 2)
|
693
|
+
case component.downcase
|
694
|
+
when "href"
|
695
|
+
href = args
|
696
|
+
when "rel"
|
697
|
+
rel = args
|
698
|
+
when "integrity"
|
699
|
+
integrity = args
|
700
|
+
when "crossorigin"
|
701
|
+
crossorigin = args
|
702
|
+
end
|
703
|
+
end
|
704
|
+
# content = %[<link rel="#{rel}" href="#{href}" integrity="#{integrity}" crossorigin="#{crossorigin}">\n]
|
705
|
+
content = %[<link rel="stylesheet" href="#{href}"></link>\n]
|
706
|
+
content
|
707
|
+
end
|
708
|
+
|
709
|
+
|
710
|
+
|
711
|
+
def generate_bootstrap_js(view = nil)
|
712
|
+
global_boot = @root/:config/"bootstrap_js.txt"
|
713
|
+
view_boot = @dir/:config/"bootstrap_js.txt"
|
714
|
+
bs_file = view ? view_boot : global_boot
|
715
|
+
lines = read_commented_file(bs_file)
|
716
|
+
src = integrity = crossorigin = nil
|
717
|
+
lines.each do |line|
|
718
|
+
component, args = line.split(/\s+/, 2)
|
719
|
+
case component.downcase
|
720
|
+
when "src"
|
721
|
+
src = args
|
722
|
+
when "rel"
|
723
|
+
# rel = args
|
724
|
+
when "integrity"
|
725
|
+
integrity = args
|
726
|
+
when "crossorigin"
|
727
|
+
crossorigin = args
|
728
|
+
end
|
729
|
+
end
|
730
|
+
# content = %[<script src="#{src}" integrity="#{integrity}" crossorigin="#{crossorigin}"></script>\n]
|
731
|
+
content = %[<script src="#{src}"></script>\n]
|
732
|
+
content
|
733
|
+
end
|
734
|
+
|
735
|
+
def generate_social_meta_tags(args = nil, post_data = nil)
|
736
|
+
# Check if social is enabled for this view
|
737
|
+
social_config_file = @dir/:config/"social.txt"
|
738
|
+
return "" unless File.exist?(social_config_file)
|
739
|
+
|
740
|
+
# Read social configuration
|
741
|
+
social_config = read_commented_file(social_config_file)
|
742
|
+
platforms = []
|
743
|
+
|
744
|
+
# Each non-comment line is a platform name
|
745
|
+
social_config.each do |line|
|
746
|
+
platform = line.strip.downcase
|
747
|
+
platforms << platform if platform.match?(/^(facebook|twitter|linkedin|reddit)$/)
|
748
|
+
end
|
749
|
+
|
750
|
+
return "" if platforms.empty?
|
751
|
+
|
752
|
+
# Determine if this is for a specific post or the main page
|
753
|
+
is_post = !post_data.nil?
|
754
|
+
|
755
|
+
# Get the appropriate title, description, and URL
|
756
|
+
if is_post
|
757
|
+
title = post_data[:"post.title"] || @title
|
758
|
+
description = post_data[:"post.blurb"] || (post_data[:"post.body"] ? post_data[:"post.body"][0..200] : nil) || @desc || @subtitle || @title
|
759
|
+
slug = post_data[:"post.slug"] || (post_data[:"post.id"] ? slugify(post_data[:"post.id"], title) : 'post')
|
760
|
+
url = "posts/#{slug}#{slug.end_with?('.html') ? '' : '.html'}"
|
761
|
+
type = "article"
|
762
|
+
else
|
763
|
+
title = @title
|
764
|
+
description = @desc || @subtitle || @title
|
765
|
+
url = "index.html"
|
766
|
+
type = "website"
|
767
|
+
end
|
768
|
+
|
769
|
+
# Generate meta tags
|
770
|
+
content = ""
|
771
|
+
|
772
|
+
# Open Graph meta tags (Facebook, LinkedIn, etc.)
|
773
|
+
if platforms.include?("facebook") || platforms.include?("linkedin")
|
774
|
+
content << %[<meta property="og:title" content="#{escape_html(title)}">\n]
|
775
|
+
content << %[<meta property="og:type" content="#{type}">\n]
|
776
|
+
content << %[<meta property="og:url" content="#{url}">\n]
|
777
|
+
content << %[<meta property="og:description" content="#{escape_html(description)}">\n]
|
778
|
+
content << %[<meta property="og:site_name" content="#{escape_html(@title)}">\n]
|
779
|
+
if is_post && post_data[:"post.pubdate"]
|
780
|
+
content << %[<meta property="article:published_time" content="#{post_data[:"post.pubdate"]}">\n]
|
781
|
+
end
|
782
|
+
end
|
783
|
+
|
784
|
+
# Twitter Card meta tags
|
785
|
+
if platforms.include?("twitter")
|
786
|
+
content << %[<meta name="twitter:card" content="summary">\n]
|
787
|
+
content << %[<meta name="twitter:title" content="#{escape_html(title)}">\n]
|
788
|
+
content << %[<meta name="twitter:description" content="#{escape_html(description)}">\n]
|
789
|
+
content << %[<meta name="twitter:url" content="#{url}">\n]
|
790
|
+
end
|
791
|
+
|
792
|
+
content
|
793
|
+
end
|
794
|
+
|
795
|
+
def generate_reddit_button(post_data = nil)
|
796
|
+
# Check if Reddit is enabled in social config
|
797
|
+
social_config_file = @dir/:config/"social.txt"
|
798
|
+
return "" unless File.exist?(social_config_file)
|
799
|
+
|
800
|
+
social_config = read_commented_file(social_config_file)
|
801
|
+
reddit_enabled = social_config.any? { |line| line.strip.downcase == "reddit" }
|
802
|
+
return "" unless reddit_enabled
|
803
|
+
|
804
|
+
# Check if Reddit button is enabled
|
805
|
+
reddit_config_file = @dir/:config/"reddit.txt"
|
806
|
+
return "" unless File.exist?(reddit_config_file)
|
807
|
+
|
808
|
+
reddit_config = read_commented_file(reddit_config_file)
|
809
|
+
button_enabled = false
|
810
|
+
subreddit = ""
|
811
|
+
hover_text = ""
|
812
|
+
|
813
|
+
reddit_config.each do |line|
|
814
|
+
component, args = line.split(/\s+/, 2)
|
815
|
+
case component.downcase
|
816
|
+
when "button"
|
817
|
+
button_enabled = (args&.downcase == "true")
|
818
|
+
when "subreddit"
|
819
|
+
subreddit = args&.strip || ""
|
820
|
+
when "hover_text"
|
821
|
+
hover_text = args&.strip || ""
|
822
|
+
end
|
823
|
+
end
|
824
|
+
|
825
|
+
return "" unless button_enabled
|
826
|
+
|
827
|
+
# Determine post URL and title
|
828
|
+
if post_data
|
829
|
+
title = post_data[:"post.title"] || @title
|
830
|
+
slug = post_data[:"post.slug"] || slugify(post_data[:"post.id"], title)
|
831
|
+
url = "posts/#{slug}#{slug.end_with?('.html') ? '' : '.html'}"
|
832
|
+
else
|
833
|
+
title = @title
|
834
|
+
url = "index.html"
|
835
|
+
end
|
836
|
+
|
837
|
+
# Build Reddit share URL
|
838
|
+
require 'uri'
|
839
|
+
encoded_title = URI.encode_www_form_component(title)
|
840
|
+
if subreddit.empty?
|
841
|
+
reddit_url = "https://reddit.com/submit?url=#{escape_html(url)}&title=#{encoded_title}"
|
842
|
+
else
|
843
|
+
reddit_url = "https://reddit.com/r/#{subreddit}/submit?url=#{escape_html(url)}&title=#{encoded_title}"
|
844
|
+
end
|
845
|
+
|
846
|
+
# Determine hover text
|
847
|
+
if hover_text.empty?
|
848
|
+
hover_text = subreddit.empty? ? "Share on Reddit" : "Share on r/#{subreddit}"
|
849
|
+
end
|
850
|
+
|
851
|
+
# Generate button HTML (use production-relative ../../assets for posts context)
|
852
|
+
button_html = %[<a href="#{reddit_url}" target="_blank" title="#{hover_text}" style="text-decoration: none; margin-right: 8px;">
|
853
|
+
<img src="../../assets/icons/social/reddit.png" width="32" height="32" alt="Share on Reddit" style="vertical-align: middle;">
|
854
|
+
</a>]
|
855
|
+
|
856
|
+
button_html
|
857
|
+
end
|
858
|
+
|
859
|
+
def build_containers
|
860
|
+
sections = read_layout
|
861
|
+
content = ""
|
862
|
+
content << build_header(sections)
|
863
|
+
content << "<!-- before left/main/right -->\n"
|
864
|
+
content << "<div style='display: flex; flex-grow: 1; height: 100%; flex-direction: row;'>"
|
865
|
+
content << build_left(sections)
|
866
|
+
content << build_main(sections)
|
867
|
+
content << build_right(sections)
|
868
|
+
content << "</div> <!-- after left/main/right --></div>\n"
|
869
|
+
content << build_footer(sections)
|
870
|
+
content
|
871
|
+
end
|
872
|
+
|
873
|
+
def pagination_bar(group, count, nth) # nth group of total 'count'
|
874
|
+
str = %[<div style="text-align: center;">Pages: ]
|
875
|
+
1.upto(count) do |i|
|
876
|
+
if i == nth # 0-based
|
877
|
+
str << "<b>[#{i}]</b> "
|
878
|
+
else
|
879
|
+
str << %[<a href="javascript:void(0)" style="text-decoration: none;"
|
880
|
+
onclick="load_main('page#{i}.html')">#{i} </a>]
|
881
|
+
end
|
882
|
+
end
|
883
|
+
str << "<br><br></div>"
|
884
|
+
end
|
885
|
+
|
886
|
+
def paginate_posts
|
887
|
+
config = read_post_index_config
|
888
|
+
posts = @repo.all_posts(self)
|
889
|
+
posts.sort! {|a,b| post_compare(a, b) }
|
890
|
+
@ppp ||= config[:"posts.per.page"].to_i
|
891
|
+
pages = []
|
892
|
+
posts.each_slice(@ppp).with_index do |group, i|
|
893
|
+
pages << group.map {|post| post_index_entry(post) }
|
894
|
+
end
|
895
|
+
out = self.dir/:output
|
896
|
+
pages.each.with_index do |page, i|
|
897
|
+
bar = pagination_bar(page, pages.size, i+1)
|
898
|
+
# Wrap rows in a table to preserve layout (match non-paginated index)
|
899
|
+
margin_top = config[:"index.margin.top"] || "0px"
|
900
|
+
cellpadding = config[:'entry.cellpadding']
|
901
|
+
table_open = %(<table width=100% cellpadding=#{cellpadding} style="margin-top: #{margin_top};">)
|
902
|
+
table_close = %(</table>)
|
903
|
+
html = String.new
|
904
|
+
html << table_open
|
905
|
+
html << page.join
|
906
|
+
html << table_close
|
907
|
+
html << %[<div style="position: absolute; bottom: 0; width: 100%;">#{bar}</div>]
|
908
|
+
write_file(out/"page#{i+1}.html", html)
|
909
|
+
end
|
910
|
+
# Remove existing link if it exists, then create new one
|
911
|
+
post_index_link = out/"post_index.html"
|
912
|
+
File.delete(post_index_link) if File.exist?(post_index_link)
|
913
|
+
FileUtils.ln(out/"page1.html", post_index_link)
|
914
|
+
end
|
915
|
+
|
916
|
+
def generate_front_page
|
917
|
+
index_file = @dir/:output/"index.html"
|
918
|
+
FileUtils.mkdir_p(File.dirname(index_file))
|
919
|
+
|
920
|
+
html_head = generate_html_head(true)
|
921
|
+
content = build_containers
|
922
|
+
common = get_common_js
|
923
|
+
boot = generate_bootstrap_js
|
924
|
+
highlight_js = generate_highlight_js(true)
|
925
|
+
highlight_ruby_js = generate_highlight_ruby_js(true)
|
926
|
+
full_html = <<~HTML
|
927
|
+
<!DOCTYPE html>
|
928
|
+
#{html_head}
|
929
|
+
<html style="height: 100%; margin: 0;">
|
930
|
+
<body style="height: 100%; margin: 0; display: flex; flex-direction: column;">
|
931
|
+
#{content.strip}
|
932
|
+
#{boot.strip}
|
933
|
+
#{highlight_js.strip}
|
934
|
+
#{highlight_ruby_js.strip}
|
935
|
+
#{common.strip}
|
936
|
+
</body>
|
937
|
+
</html>
|
938
|
+
HTML
|
939
|
+
|
940
|
+
# Beautify HTML if HtmlBeautifier is available
|
941
|
+
begin
|
942
|
+
full_html = ::HtmlBeautifier.beautify(full_html)
|
943
|
+
rescue NameError, LoadError => e
|
944
|
+
# HtmlBeautifier not available, continue without beautification
|
945
|
+
# This is not critical for functionality
|
946
|
+
end
|
947
|
+
|
948
|
+
# Write the main index file
|
949
|
+
write_file(index_file, full_html)
|
950
|
+
|
951
|
+
# Write debug file (optional, don't fail if it doesn't work)
|
952
|
+
begin
|
953
|
+
write_file("/tmp/full.html", full_html)
|
954
|
+
rescue => e
|
955
|
+
# Debug file write failed, but this is not critical
|
956
|
+
end
|
957
|
+
|
958
|
+
# Copy pages directory to output if it exists
|
959
|
+
pages_source = @dir/:pages
|
960
|
+
pages_output = @dir/:output/:pages
|
961
|
+
if Dir.exist?(pages_source)
|
962
|
+
FileUtils.mkdir_p(pages_output)
|
963
|
+
Dir.glob(pages_source/"*").each do |file|
|
964
|
+
next unless File.file?(file)
|
965
|
+
FileUtils.cp(file, pages_output/File.basename(file))
|
966
|
+
end
|
967
|
+
end
|
968
|
+
end
|
969
|
+
|
970
|
+
def generate_highlight_css(view = nil)
|
971
|
+
global_highlight = @root/:config/"highlight_css.txt"
|
972
|
+
view_highlight = @dir/:config/"highlight_css.txt"
|
973
|
+
highlight_file = view ? view_highlight : global_highlight
|
974
|
+
lines = read_commented_file(highlight_file)
|
975
|
+
href = rel = nil
|
976
|
+
lines.each do |line|
|
977
|
+
component, args = line.split(/\s+/, 2)
|
978
|
+
case component.downcase
|
979
|
+
when "href"
|
980
|
+
href = args
|
981
|
+
when "rel"
|
982
|
+
rel = args
|
983
|
+
end
|
984
|
+
end
|
985
|
+
%[<link rel="#{rel}" href="#{href}">\n]
|
986
|
+
end
|
987
|
+
|
988
|
+
def generate_highlight_js(view = nil)
|
989
|
+
global_highlight = @root/:config/"highlight_js.txt"
|
990
|
+
view_highlight = @dir/:config/"highlight_js.txt"
|
991
|
+
highlight_file = view ? view_highlight : global_highlight
|
992
|
+
lines = read_commented_file(highlight_file)
|
993
|
+
src = nil
|
994
|
+
lines.each do |line|
|
995
|
+
component, args = line.split(/\s+/, 2)
|
996
|
+
case component.downcase
|
997
|
+
when "src"
|
998
|
+
src = args
|
999
|
+
end
|
1000
|
+
end
|
1001
|
+
%[<script src="#{src}"></script>\n]
|
1002
|
+
end
|
1003
|
+
|
1004
|
+
def generate_highlight_ruby_js(view = nil)
|
1005
|
+
global_highlight = @root/:config/"highlight_ruby_js.txt"
|
1006
|
+
view_highlight = @dir/:config/"highlight_ruby_js.txt"
|
1007
|
+
highlight_file = view ? view_highlight : global_highlight
|
1008
|
+
lines = read_commented_file(highlight_file)
|
1009
|
+
src = nil
|
1010
|
+
lines.each do |line|
|
1011
|
+
component, args = line.split(/\s+/, 2)
|
1012
|
+
case component.downcase
|
1013
|
+
when "src"
|
1014
|
+
src = args
|
1015
|
+
end
|
1016
|
+
end
|
1017
|
+
%[<script src="#{src}"></script>\n]
|
1018
|
+
end
|
1019
|
+
|
1020
|
+
|
1021
|
+
end
|