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,675 @@
|
|
1
|
+
# Path magic
|
2
|
+
|
3
|
+
module PathSep
|
4
|
+
def /(right)
|
5
|
+
s1 = self.to_s.dup
|
6
|
+
s2 = right.to_s.dup
|
7
|
+
s1 << "/" unless s1.end_with?("/") || s2.start_with?("/")
|
8
|
+
path = s1 + s2
|
9
|
+
path.gsub!("//", "/")
|
10
|
+
path
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
String.include(PathSep)
|
15
|
+
Symbol.include(PathSep)
|
16
|
+
|
17
|
+
## Helpers
|
18
|
+
|
19
|
+
module Scriptorium::Helpers
|
20
|
+
include Scriptorium::Exceptions
|
21
|
+
|
22
|
+
# Post comparison for sorting (uses post.date which handles fallback)
|
23
|
+
def post_compare(a, b)
|
24
|
+
# Primary sort: date (newest first)
|
25
|
+
date_compare = b.date <=> a.date
|
26
|
+
return date_compare unless date_compare == 0
|
27
|
+
|
28
|
+
# Secondary sort: post number (ascending) for stable ordering when dates are identical
|
29
|
+
a.num <=> b.num
|
30
|
+
end
|
31
|
+
|
32
|
+
def getvars(file)
|
33
|
+
lines = read_file(file, lines: true)
|
34
|
+
lines.map! {|line| line.sub(/# .*$/, "").strip }
|
35
|
+
lines.reject! {|line| line.empty? }
|
36
|
+
vhash = Hash.new("")
|
37
|
+
lines.each do |line|
|
38
|
+
var, val = line.split(" ", 2)
|
39
|
+
# Fix: treat nil values as empty strings
|
40
|
+
val = "" if val.nil?
|
41
|
+
vhash[var.to_sym] = val
|
42
|
+
end
|
43
|
+
vhash
|
44
|
+
end
|
45
|
+
|
46
|
+
def d4(num)
|
47
|
+
"%04d" % num
|
48
|
+
end
|
49
|
+
|
50
|
+
def view_dir(name)
|
51
|
+
@root/:views/name
|
52
|
+
end
|
53
|
+
|
54
|
+
def write_file(file, content, empty: false)
|
55
|
+
# Input validation
|
56
|
+
raise FilePathNil if file.nil?
|
57
|
+
|
58
|
+
raise FilePathEmpty if file.to_s.strip.empty?
|
59
|
+
|
60
|
+
# Handle empty content if empty: true is specified
|
61
|
+
if empty && (content.nil? || content.to_s.strip.empty?)
|
62
|
+
# If file exists, do nothing; if it doesn't exist, touch it
|
63
|
+
unless File.exist?(file)
|
64
|
+
FileUtils.mkdir_p(File.dirname(file))
|
65
|
+
FileUtils.touch(file)
|
66
|
+
end
|
67
|
+
return
|
68
|
+
end
|
69
|
+
|
70
|
+
# Ensure parent directory exists
|
71
|
+
FileUtils.mkdir_p(File.dirname(file))
|
72
|
+
|
73
|
+
# Write the file with error handling
|
74
|
+
begin
|
75
|
+
File.open(file, "w") do |f|
|
76
|
+
f.puts content
|
77
|
+
end
|
78
|
+
rescue Errno::ENOSPC => e
|
79
|
+
raise FileDiskFull(file, e.message)
|
80
|
+
rescue Errno::EACCES => e
|
81
|
+
raise FilePermissionDenied(file, e.message)
|
82
|
+
rescue Errno::ENOENT => e
|
83
|
+
raise FileDirectoryNotFound(file, e.message)
|
84
|
+
rescue => e
|
85
|
+
raise WriteFileError(file, e.message)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def write_file!(file, *lines, empty: false)
|
90
|
+
# Convert nil values to empty strings for proper joining
|
91
|
+
processed_lines = lines.map { |line| line.nil? ? "" : line.to_s }
|
92
|
+
content = processed_lines.join("\n")
|
93
|
+
# Always add a newline at the end to ensure there's an empty line
|
94
|
+
content += "\n"
|
95
|
+
write_file(file, content, empty: empty)
|
96
|
+
end
|
97
|
+
|
98
|
+
def make_dir(dir, create_parents = false)
|
99
|
+
# Input validation
|
100
|
+
raise DirectoryPathNil if dir.nil?
|
101
|
+
|
102
|
+
raise DirectoryPathEmpty if dir.to_s.strip.empty?
|
103
|
+
|
104
|
+
# Create parent directories if requested
|
105
|
+
if create_parents
|
106
|
+
FileUtils.mkdir_p(dir)
|
107
|
+
else
|
108
|
+
# Create single directory with error handling
|
109
|
+
begin
|
110
|
+
Dir.mkdir(dir)
|
111
|
+
rescue Errno::ENOSPC => e
|
112
|
+
raise DirectoryDiskFull(dir, e.message)
|
113
|
+
rescue Errno::EACCES => e
|
114
|
+
raise DirectoryPermissionDenied(dir, e.message)
|
115
|
+
rescue Errno::ENOENT => e
|
116
|
+
raise DirectoryParentNotFound(dir, e.message)
|
117
|
+
rescue Errno::EEXIST => e
|
118
|
+
# Directory already exists - this is usually not an error
|
119
|
+
# But we could make this configurable if needed
|
120
|
+
rescue => e
|
121
|
+
raise CreateDirectoryError(dir, e.message)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def system!(command, description = nil)
|
127
|
+
# Input validation
|
128
|
+
raise CommandNil if command.nil?
|
129
|
+
|
130
|
+
raise CommandEmpty if command.to_s.strip.empty?
|
131
|
+
|
132
|
+
# Execute command with error handling
|
133
|
+
success = system(command)
|
134
|
+
|
135
|
+
unless success
|
136
|
+
desc = description ? " (#{description})" : ""
|
137
|
+
raise CommandFailedWithDesc(desc, command)
|
138
|
+
end
|
139
|
+
|
140
|
+
success
|
141
|
+
end
|
142
|
+
|
143
|
+
def need(type, path, exception_class = RuntimeError)
|
144
|
+
# Input validation
|
145
|
+
raise RequirePathNil(type) if path.nil?
|
146
|
+
|
147
|
+
raise RequirePathEmpty(type) if path.to_s.strip.empty?
|
148
|
+
|
149
|
+
# Check if file/directory exists
|
150
|
+
exists = case type
|
151
|
+
when :file
|
152
|
+
File.exist?(path)
|
153
|
+
when :dir
|
154
|
+
Dir.exist?(path)
|
155
|
+
else
|
156
|
+
raise InvalidType(type)
|
157
|
+
end
|
158
|
+
|
159
|
+
unless exists
|
160
|
+
raise RequiredFileNotFound(type, path) if exception_class == RuntimeError
|
161
|
+
|
162
|
+
# Exception class - try to call it as a method first, then as constructor
|
163
|
+
raise exception_class.call(path) if exception_class.respond_to?(:call)
|
164
|
+
raise exception_class.new(path)
|
165
|
+
end
|
166
|
+
|
167
|
+
path
|
168
|
+
end
|
169
|
+
|
170
|
+
def read_file(file, options = {})
|
171
|
+
# Input validation
|
172
|
+
raise ReadFilePathNil if file.nil?
|
173
|
+
|
174
|
+
raise ReadFilePathEmpty if file.to_s.strip.empty?
|
175
|
+
|
176
|
+
# Handle missing file with fallback
|
177
|
+
if options[:missing_fallback]
|
178
|
+
return options[:missing_fallback] unless File.exist?(file)
|
179
|
+
end
|
180
|
+
|
181
|
+
# Read the file with error handling
|
182
|
+
begin
|
183
|
+
if options[:lines]
|
184
|
+
# Read as lines
|
185
|
+
if options[:chomp]
|
186
|
+
File.readlines(file, chomp: true)
|
187
|
+
else
|
188
|
+
File.readlines(file)
|
189
|
+
end
|
190
|
+
else
|
191
|
+
# Read as content
|
192
|
+
File.read(file)
|
193
|
+
end
|
194
|
+
rescue Errno::ENOENT => e
|
195
|
+
if options[:missing_fallback]
|
196
|
+
return options[:missing_fallback]
|
197
|
+
else
|
198
|
+
raise ReadFileNotFound(file, e.message)
|
199
|
+
end
|
200
|
+
rescue Errno::EACCES => e
|
201
|
+
raise ReadFilePermissionDenied(file, e.message)
|
202
|
+
rescue => e
|
203
|
+
raise ReadFileError(file, e.message)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
|
208
|
+
|
209
|
+
def change_config(file_path, target_key, new_value)
|
210
|
+
pattern = /
|
211
|
+
^(?<leading>\s*#{Regexp.escape(target_key)}\s+) # key and spacing
|
212
|
+
(?<old_value>[^\#]*?) # value (non-greedy up to comment)
|
213
|
+
(?<trailing>\s*) # trailing space
|
214
|
+
(?<comment>\#.*)?$ # optional comment
|
215
|
+
/x
|
216
|
+
|
217
|
+
lines = read_file(file_path, lines: true)
|
218
|
+
updated_lines = lines.map do |line|
|
219
|
+
if match = pattern.match(line)
|
220
|
+
leading = match[:leading]
|
221
|
+
trailing = match[:trailing]
|
222
|
+
comment = match[:comment] || ''
|
223
|
+
"#{leading}#{new_value}#{trailing}#{comment}\n"
|
224
|
+
else
|
225
|
+
line
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
write_file(file_path, updated_lines.join)
|
230
|
+
end
|
231
|
+
|
232
|
+
def slugify(id, title)
|
233
|
+
return "#{d4(id)}-untitled" if title.nil? || title.strip.empty?
|
234
|
+
|
235
|
+
slug = title.downcase.strip
|
236
|
+
.gsub(/[^a-z0-9\s_-]/, '') # remove punctuation
|
237
|
+
.gsub(/[\s_-]+/, '-') # replace spaces and underscores with hyphen
|
238
|
+
.gsub(/^-+|-+$/, '') # trim leading/trailing hyphens
|
239
|
+
"#{d4(id)}-#{slug}"
|
240
|
+
end
|
241
|
+
|
242
|
+
def clean_slugify(title)
|
243
|
+
return "title-is-missing" if title.nil?
|
244
|
+
|
245
|
+
slug = title.downcase.strip
|
246
|
+
.gsub(/[^a-z0-9\s_-]/, '') # remove punctuation
|
247
|
+
.gsub(/[\s_-]+/, '-') # replace spaces and underscores with hyphen
|
248
|
+
.gsub(/^-+|-+$/, '') # trim leading/trailing hyphens
|
249
|
+
slug
|
250
|
+
end
|
251
|
+
|
252
|
+
def ymdhms
|
253
|
+
Time.now.strftime("%Y-%m-%d %H:%M:%S")
|
254
|
+
end
|
255
|
+
|
256
|
+
def ymdhms_filename
|
257
|
+
Time.now.strftime("%Y%m%d-%H%M%S")
|
258
|
+
end
|
259
|
+
|
260
|
+
def see_file(file) # Really from TestHelpers
|
261
|
+
puts "----- File: #{file}"
|
262
|
+
system!("cat #{file}", "displaying file contents")
|
263
|
+
puts "-----"
|
264
|
+
end
|
265
|
+
|
266
|
+
def see(label, var)
|
267
|
+
puts "#{label} = \n<<<\n#{var}\n>>>"
|
268
|
+
end
|
269
|
+
|
270
|
+
def make_tree(base, text)
|
271
|
+
lines = text.split("\n").map(&:chomp)
|
272
|
+
lines.each {|line| line.gsub!(/ *#.*$/, "") }
|
273
|
+
entries = []
|
274
|
+
|
275
|
+
# Always throw away the first line (for backward compatibility)
|
276
|
+
lines.shift
|
277
|
+
|
278
|
+
# Create the base directory and start stack there
|
279
|
+
make_dir(base) unless File.exist?(base)
|
280
|
+
stack = [base]
|
281
|
+
|
282
|
+
# Parse all lines as structure
|
283
|
+
lines.each do |line|
|
284
|
+
if (i = line.index(/ [a-zA-Z0-9_.]/))
|
285
|
+
name = line[(i + 1)..-1]
|
286
|
+
level = i / 4
|
287
|
+
else
|
288
|
+
name = line.strip
|
289
|
+
level = 0
|
290
|
+
end
|
291
|
+
entries << [level, name]
|
292
|
+
end
|
293
|
+
|
294
|
+
entries.each do |level, name|
|
295
|
+
stack = stack[0..level]
|
296
|
+
full_path = File.join(stack.last, name)
|
297
|
+
|
298
|
+
if name.end_with?("/")
|
299
|
+
make_dir(full_path) unless File.exist?(full_path)
|
300
|
+
stack << full_path
|
301
|
+
else
|
302
|
+
write_file(full_path, "Empty file generated at #{Time.now}")
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
def substitute(obj, text)
|
308
|
+
vars = obj.is_a?(Hash) ? obj : obj.vars
|
309
|
+
text % vars
|
310
|
+
end
|
311
|
+
|
312
|
+
def escape_html(str)
|
313
|
+
str.gsub(/&/, '&')
|
314
|
+
.gsub(/</, '<')
|
315
|
+
.gsub(/>/, '>')
|
316
|
+
.gsub(/"/, '"')
|
317
|
+
.gsub(/'/, ''')
|
318
|
+
end
|
319
|
+
|
320
|
+
def read_commented_file(file_path)
|
321
|
+
return [] unless File.exist?(file_path)
|
322
|
+
lines = read_file(file_path, lines: true) # Read file and remove newline characters
|
323
|
+
lines.reject! do |line| # Remove empty lines and comments
|
324
|
+
line.strip.empty? || line.strip.start_with?("#")
|
325
|
+
end
|
326
|
+
lines.map! do |line| # Strip trailing comments + preceding spaces
|
327
|
+
line.sub(/# .*$/, "").strip
|
328
|
+
end
|
329
|
+
lines # Return cleaned lines
|
330
|
+
end
|
331
|
+
|
332
|
+
def parse_commented_file(file_path)
|
333
|
+
config = {}
|
334
|
+
read_commented_file(file_path).each do |line|
|
335
|
+
if line.include?(' ')
|
336
|
+
key, value = line.split(/\s+/, 2)
|
337
|
+
config[key] = config[key.to_sym] = value
|
338
|
+
end
|
339
|
+
end
|
340
|
+
config
|
341
|
+
end
|
342
|
+
|
343
|
+
def cf_time(t1, t2)
|
344
|
+
t1 = t1.split(/- :/, 6)
|
345
|
+
t2 = t2.split(/- :/, 6)
|
346
|
+
t1 = Time.new(*t1)
|
347
|
+
t2 = Time.new(*t2)
|
348
|
+
t1 <=> t2
|
349
|
+
end
|
350
|
+
|
351
|
+
# Post state management helpers
|
352
|
+
|
353
|
+
def read_post_state_file(file_path)
|
354
|
+
return [] unless File.exist?(file_path)
|
355
|
+
content = read_file(file_path)
|
356
|
+
return [] if content.strip.empty?
|
357
|
+
content.lines.map { |line| line.strip.to_i }.reject { |id| id == 0 }
|
358
|
+
end
|
359
|
+
|
360
|
+
def write_post_state_file(file_path, post_ids)
|
361
|
+
if post_ids.empty?
|
362
|
+
# Write empty file (empty = all posts are in that state)
|
363
|
+
# For state files, empty means "all posts are in this state", so we need to clear the file
|
364
|
+
write_file(file_path, "")
|
365
|
+
else
|
366
|
+
content = post_ids.sort.uniq.join("\n") + "\n"
|
367
|
+
write_file(file_path, content)
|
368
|
+
end
|
369
|
+
end
|
370
|
+
|
371
|
+
def add_post_to_state_file(file_path, post_id)
|
372
|
+
post_ids = read_post_state_file(file_path)
|
373
|
+
post_ids << post_id unless post_ids.include?(post_id)
|
374
|
+
write_post_state_file(file_path, post_ids)
|
375
|
+
end
|
376
|
+
|
377
|
+
def remove_post_from_state_file(file_path, post_id)
|
378
|
+
post_ids = read_post_state_file(file_path)
|
379
|
+
post_ids.delete(post_id)
|
380
|
+
write_post_state_file(file_path, post_ids)
|
381
|
+
end
|
382
|
+
|
383
|
+
def post_in_state_file?(file_path, post_id)
|
384
|
+
post_ids = read_post_state_file(file_path)
|
385
|
+
post_ids.include?(post_id)
|
386
|
+
end
|
387
|
+
|
388
|
+
# Helper method to find asset recursively in a directory
|
389
|
+
def find_asset(base_dir, asset_name)
|
390
|
+
# First try exact path
|
391
|
+
exact_path = "#{base_dir}/#{asset_name}"
|
392
|
+
return exact_path if File.exist?(exact_path)
|
393
|
+
|
394
|
+
# Then search recursively
|
395
|
+
Dir.glob("#{base_dir}/**/*").each do |file|
|
396
|
+
next unless File.file?(file)
|
397
|
+
next unless File.basename(file) == File.basename(asset_name)
|
398
|
+
return file
|
399
|
+
end
|
400
|
+
|
401
|
+
nil
|
402
|
+
end
|
403
|
+
|
404
|
+
def get_asset_path(name)
|
405
|
+
if Scriptorium::Repo.testing
|
406
|
+
# Development/testing: Check dev_assets first, then local assets
|
407
|
+
dev_asset = find_asset("dev_assets", name)
|
408
|
+
return dev_asset if dev_asset
|
409
|
+
|
410
|
+
local_asset = find_asset("assets", name)
|
411
|
+
return local_asset if local_asset
|
412
|
+
|
413
|
+
raise AssetNotFound(name)
|
414
|
+
else # Production
|
415
|
+
# Production: Check user assets first, then gem assets
|
416
|
+
|
417
|
+
# Check user assets first (highest priority) - recursively
|
418
|
+
user_asset = find_asset("assets", name)
|
419
|
+
return user_asset if user_asset
|
420
|
+
|
421
|
+
# Then check gem assets (fallback) - recursively
|
422
|
+
begin
|
423
|
+
gem_spec = Gem.loaded_specs['scriptorium']
|
424
|
+
if gem_spec
|
425
|
+
gem_asset = find_asset("#{gem_spec.full_gem_path}/assets", name)
|
426
|
+
return gem_asset if gem_asset
|
427
|
+
end
|
428
|
+
rescue => e
|
429
|
+
# If gem lookup fails, continue without gem assets
|
430
|
+
end
|
431
|
+
|
432
|
+
# Asset not found
|
433
|
+
raise AssetNotFound(name)
|
434
|
+
end
|
435
|
+
end
|
436
|
+
|
437
|
+
def generate_missing_asset_svg(filename, width: 200, height: 150)
|
438
|
+
# Truncate filename if too long for display
|
439
|
+
display_name = filename.length > 20 ? filename[0..16] + "..." : filename
|
440
|
+
|
441
|
+
# Generate SVG with broken image icon and filename
|
442
|
+
svg = <<~SVG
|
443
|
+
<svg width="#{width}" height="#{height}" xmlns="http://www.w3.org/2000/svg">
|
444
|
+
<!-- Background -->
|
445
|
+
<rect fill="#f8f9fa" stroke="#ddd" stroke-width="1" width="#{width}" height="#{height}" rx="4"/>
|
446
|
+
|
447
|
+
<!-- Broken image icon -->
|
448
|
+
<g transform="translate(#{width/2}, #{height/2 - 20})">
|
449
|
+
<!-- Image frame -->
|
450
|
+
<rect x="-15" y="-10" width="30" height="20" fill="none" stroke="#999" stroke-width="1"/>
|
451
|
+
<!-- Broken corner -->
|
452
|
+
<path d="M 15 -10 L 25 -20 M 15 -10 L 25 0" stroke="#999" stroke-width="1" fill="none"/>
|
453
|
+
<!-- Image icon -->
|
454
|
+
<rect x="-12" y="-7" width="24" height="14" fill="#e9ecef"/>
|
455
|
+
<circle cx="-5" cy="-2" r="2" fill="#999"/>
|
456
|
+
<polygon points="-8,8 -2,2 2,6 8,0" fill="#999"/>
|
457
|
+
</g>
|
458
|
+
|
459
|
+
<!-- Filename -->
|
460
|
+
<text x="#{width/2}" y="#{height/2 + 15}" text-anchor="middle" fill="#666" font-family="Arial, sans-serif" font-size="11">
|
461
|
+
#{escape_html(display_name)}
|
462
|
+
</text>
|
463
|
+
|
464
|
+
<!-- "Asset not found" message -->
|
465
|
+
<text x="#{width/2}" y="#{height/2 + 30}" text-anchor="middle" fill="#999" font-family="Arial, sans-serif" font-size="9">
|
466
|
+
Asset not found
|
467
|
+
</text>
|
468
|
+
</svg>
|
469
|
+
SVG
|
470
|
+
|
471
|
+
svg.strip
|
472
|
+
end
|
473
|
+
|
474
|
+
def list_gem_assets
|
475
|
+
assets = []
|
476
|
+
gem_spec = Gem.loaded_specs['scriptorium']
|
477
|
+
if gem_spec
|
478
|
+
gem_assets_dir = "#{gem_spec.full_gem_path}/assets"
|
479
|
+
if Dir.exist?(gem_assets_dir)
|
480
|
+
Dir.glob("#{gem_assets_dir}/**/*").each do |file|
|
481
|
+
next unless File.file?(file)
|
482
|
+
relative_path = file.sub("#{gem_assets_dir}/", "")
|
483
|
+
assets << relative_path
|
484
|
+
end
|
485
|
+
end
|
486
|
+
end
|
487
|
+
assets.sort
|
488
|
+
rescue => e
|
489
|
+
# If gem lookup fails, return empty array
|
490
|
+
[]
|
491
|
+
end
|
492
|
+
|
493
|
+
def copy_gem_asset_to_user(asset_name, target_dir = "assets")
|
494
|
+
gem_spec = Gem.loaded_specs['scriptorium']
|
495
|
+
if gem_spec
|
496
|
+
gem_asset_path = "#{gem_spec.full_gem_path}/assets/#{asset_name}"
|
497
|
+
if File.exist?(gem_asset_path)
|
498
|
+
# Create target directory if it doesn't exist
|
499
|
+
FileUtils.mkdir_p(target_dir) unless Dir.exist?(target_dir)
|
500
|
+
|
501
|
+
# Copy the asset
|
502
|
+
target_path = "#{target_dir}/#{File.basename(asset_name)}"
|
503
|
+
FileUtils.cp(gem_asset_path, target_path)
|
504
|
+
return target_path
|
505
|
+
end
|
506
|
+
end
|
507
|
+
nil
|
508
|
+
rescue => e
|
509
|
+
# If gem lookup fails, return nil
|
510
|
+
nil
|
511
|
+
end
|
512
|
+
|
513
|
+
# Clipboard helper methods
|
514
|
+
def copy_to_clipboard(text)
|
515
|
+
begin
|
516
|
+
require 'clipboard'
|
517
|
+
Clipboard.copy(text)
|
518
|
+
true
|
519
|
+
rescue LoadError => e
|
520
|
+
# Fallback to system commands if clipboard gem not available
|
521
|
+
case RbConfig::CONFIG['host_os']
|
522
|
+
when /darwin/ # macOS
|
523
|
+
system("echo '#{text}' | pbcopy")
|
524
|
+
when /linux/ # Linux
|
525
|
+
system("echo '#{text}' | xclip -selection clipboard")
|
526
|
+
when /mswin|mingw|cygwin/ # Windows
|
527
|
+
system("echo '#{text}' | clip")
|
528
|
+
else
|
529
|
+
puts "Clipboard not supported on this OS"
|
530
|
+
false
|
531
|
+
end
|
532
|
+
rescue => e
|
533
|
+
puts "Failed to copy to clipboard: #{e.message}"
|
534
|
+
false
|
535
|
+
end
|
536
|
+
end
|
537
|
+
|
538
|
+
def get_from_clipboard
|
539
|
+
begin
|
540
|
+
require 'clipboard'
|
541
|
+
Clipboard.paste
|
542
|
+
rescue LoadError => e
|
543
|
+
# Fallback to system commands if clipboard gem not available
|
544
|
+
case RbConfig::CONFIG['host_os']
|
545
|
+
when /darwin/ # macOS
|
546
|
+
`pbpaste`
|
547
|
+
when /linux/ # Linux
|
548
|
+
`xclip -selection clipboard -o`
|
549
|
+
when /mswin|mingw|cygwin/ # Windows
|
550
|
+
`powershell -command "Get-Clipboard"`
|
551
|
+
else
|
552
|
+
puts "Clipboard not supported on this OS"
|
553
|
+
nil
|
554
|
+
end
|
555
|
+
rescue => e
|
556
|
+
puts "Failed to read from clipboard: #{e.message}"
|
557
|
+
nil
|
558
|
+
end
|
559
|
+
end
|
560
|
+
|
561
|
+
def tty(str)
|
562
|
+
File.open('/dev/tty', 'w') { |f| f.puts str }
|
563
|
+
end
|
564
|
+
|
565
|
+
def format_date(format, day = Time.now.to_date)
|
566
|
+
# Handle nil dates
|
567
|
+
return format if day.nil?
|
568
|
+
|
569
|
+
# Handle string dates by parsing them
|
570
|
+
if day.is_a?(String)
|
571
|
+
begin
|
572
|
+
day = Date.parse(day)
|
573
|
+
rescue Date::Error
|
574
|
+
return format # Return original format if date is invalid
|
575
|
+
end
|
576
|
+
end
|
577
|
+
|
578
|
+
# Parse the format string and replace tokens
|
579
|
+
result = format.dup
|
580
|
+
|
581
|
+
# Month name vs number
|
582
|
+
result.gsub!(/\bmonth\b/, day.strftime("%B"))
|
583
|
+
result.gsub!(/\bmm\b/, day.strftime("%m"))
|
584
|
+
|
585
|
+
# Day ordinal vs number - use smart padding
|
586
|
+
result.gsub!(/\bday\b/, ordinalize(day.day))
|
587
|
+
result.gsub!(/\bdd\b/) { smart_pad_day(day.day, result, $~.offset(0)[0]) }
|
588
|
+
|
589
|
+
# Year formats
|
590
|
+
result.gsub!(/\byyyy\b/, day.strftime("%Y"))
|
591
|
+
result.gsub!(/\byy\b/, day.strftime("%y"))
|
592
|
+
|
593
|
+
# Line breaks - handle spaces around break
|
594
|
+
result.gsub!(/\s+break\s+/, "<br>")
|
595
|
+
result.gsub!(/\s+break\b/, "<br>")
|
596
|
+
result.gsub!(/\bbreak\s+/, "<br>")
|
597
|
+
result.gsub!(/\bbreak\b/, "<br>")
|
598
|
+
|
599
|
+
result
|
600
|
+
end
|
601
|
+
|
602
|
+
private
|
603
|
+
|
604
|
+
def smart_pad_day(day, format_string, position)
|
605
|
+
# Look at the context around the dd token to determine padding
|
606
|
+
before = format_string[0...position]
|
607
|
+
after = format_string[position + 2..-1] || ""
|
608
|
+
|
609
|
+
# Check if we should pad based on context
|
610
|
+
should_pad = false
|
611
|
+
|
612
|
+
# Rule 1: Initial number followed immediately by punctuation
|
613
|
+
if position == 0 && after.match?(/^[^a-zA-Z\s]/)
|
614
|
+
should_pad = true
|
615
|
+
end
|
616
|
+
|
617
|
+
# Rule 2: Number enclosed in punctuation (before and after)
|
618
|
+
if before.match?(/[^a-zA-Z\s]$/) && after.match?(/^[^a-zA-Z\s]/)
|
619
|
+
should_pad = true
|
620
|
+
end
|
621
|
+
|
622
|
+
# Rule 3: Terminal number preceded by punctuation
|
623
|
+
if after.empty? && before.match?(/[^a-zA-Z\s]$/)
|
624
|
+
should_pad = true
|
625
|
+
end
|
626
|
+
|
627
|
+
# Rule 4: Middle number with spaces - don't pad
|
628
|
+
if before.match?(/\s$/) || after.match?(/^\s/)
|
629
|
+
should_pad = false
|
630
|
+
end
|
631
|
+
|
632
|
+
should_pad ? day.to_s.rjust(2, '0') : day.to_s
|
633
|
+
end
|
634
|
+
|
635
|
+
def ordinalize(number)
|
636
|
+
case number % 100
|
637
|
+
when 11, 12, 13
|
638
|
+
"#{number}th"
|
639
|
+
else
|
640
|
+
case number % 10
|
641
|
+
when 1 then "#{number}st"
|
642
|
+
when 2 then "#{number}nd"
|
643
|
+
when 3 then "#{number}rd"
|
644
|
+
else "#{number}th"
|
645
|
+
end
|
646
|
+
end
|
647
|
+
end
|
648
|
+
|
649
|
+
def support_file(relative_path)
|
650
|
+
# Get the path to support files, handling both dev and gem environments
|
651
|
+
begin
|
652
|
+
gem_spec = Gem.loaded_specs['scriptorium']
|
653
|
+
if gem_spec
|
654
|
+
# Production: use gem path
|
655
|
+
"#{gem_spec.full_gem_path}/lib/scriptorium/support/#{relative_path}"
|
656
|
+
else
|
657
|
+
# Development: use project root (not current working directory)
|
658
|
+
project_root = File.expand_path(File.join(File.dirname(__FILE__), '..', '..'))
|
659
|
+
File.expand_path("lib/scriptorium/support/#{relative_path}", project_root)
|
660
|
+
end
|
661
|
+
rescue => e
|
662
|
+
# Fallback to development path if gem lookup fails
|
663
|
+
project_root = File.expand_path(File.join(File.dirname(__FILE__), '..', '..'))
|
664
|
+
File.expand_path("lib/scriptorium/support/#{relative_path}", project_root)
|
665
|
+
end
|
666
|
+
end
|
667
|
+
|
668
|
+
def support_data(relative_path)
|
669
|
+
read_file(support_file(relative_path))
|
670
|
+
end
|
671
|
+
|
672
|
+
def copy_support_file(relative_path, target_path)
|
673
|
+
FileUtils.cp(support_file(relative_path), target_path)
|
674
|
+
end
|
675
|
+
end
|