scriptorium 0.0.3 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.lt3 +324 -0
- data/README.md +3155 -1
- data/assets/.DS_Store +0 -0
- data/assets/README.md +44 -0
- data/assets/back-icon.png +0 -0
- data/assets/icons/facebook.svg +1 -0
- data/assets/icons/github.svg +1 -0
- data/assets/icons/instagram.svg +1 -0
- data/assets/icons/reddit.svg +1 -0
- data/assets/icons/ui/.DS_Store +0 -0
- data/assets/icons/ui/back.png +0 -0
- data/assets/icons/ui/copy.png +0 -0
- data/assets/icons/ui/down.png +0 -0
- data/assets/icons/ui/end.png +0 -0
- data/assets/icons/ui/exit.png +0 -0
- data/assets/icons/ui/foo +10 -0
- data/assets/icons/ui/home.png +0 -0
- data/assets/icons/ui/left.png +0 -0
- data/assets/icons/ui/next.png +0 -0
- data/assets/icons/ui/right.png +0 -0
- data/assets/icons/ui/start.png +0 -0
- data/assets/icons/ui/up.png +0 -0
- data/assets/icons/x.svg +1 -0
- data/assets/icons/youtube.svg +1 -0
- data/assets/samples/placeholder.svg +9 -0
- data/assets/themes/standard/favicon.svg +6 -0
- data/bin/scriptorium +1511 -0
- data/doc/README.txt +6 -0
- data/doc/anti-amnesia/20250727-054000-scriptorium-overview.md +95 -0
- data/doc/anti-amnesia/20250727-060000-api-design-tui-planning.md +34 -0
- data/doc/anti-amnesia/20250727-061000-runeblog-tui-analysis.md +50 -0
- data/doc/anti-amnesia/20250727-123000-anti-amnesia-conventions.md +31 -0
- data/doc/anti-amnesia/20250727-154000-livetext-plugin-file-stats.md +73 -0
- data/doc/anti-amnesia/20250727-172600-cursor-rbenv-ruby-version-mystery.md +64 -0
- data/doc/anti-amnesia/20250727-172600-unified-minitest-framework.md +70 -0
- data/doc/anti-amnesia/20250727-172900-ai-cognitive-assessment-capabilities.md +40 -0
- data/doc/anti-amnesia/20250727-173000-widget-testing-achievement.md +110 -0
- data/doc/anti-amnesia/20250727-180000-post-id-num-refactoring.md +73 -0
- data/doc/anti-amnesia/20250728-124243-aaa-syntax-clarification.md +46 -0
- data/doc/anti-amnesia/20250728-124421-conversation-summary-concise.md +124 -0
- data/doc/anti-amnesia/20250729-190000-scriptorium-tui-testing-complete.md +46 -0
- data/doc/anti-amnesia/20250729-200000-scriptorium-tui-testing-edit-file-workflow.md +97 -0
- data/doc/anti-amnesia/20250729-210000-reddit-autopost-integration-complete.md +158 -0
- data/doc/anti-amnesia/20250729-211500-dependency-management-system.md +211 -0
- data/doc/anti-amnesia/20250729-213000-python-virtual-environment-setup.md +141 -0
- data/doc/anti-amnesia/20250729-214500-theme-management-commands.md +211 -0
- data/doc/anti-amnesia/20250729-215000-version-update-to-0.6.0.md +134 -0
- data/doc/anti-amnesia/20250729-220000-user-guide-complete.md +41 -0
- data/doc/anti-amnesia/20250804-190500-cognitive-loop-bug.md +45 -0
- data/doc/anti-amnesia/20250804-190700-anti-amnesia-timestamping-fix.md +30 -0
- data/doc/anti-amnesia/20250804-213700-publishing-test-fix.md +49 -0
- data/doc/anti-amnesia/20250804-214400-additional-test-fixes.md +46 -0
- data/doc/anti-amnesia/20250804-220000-asset-function-logic-clarification.md +41 -0
- data/doc/anti-amnesia/20250806-202032-asset-function-logic-clarification.md +41 -0
- data/doc/anti-amnesia/20250807-213025.md +116 -0
- data/doc/anti-amnesia/20250813-082428-syntax-highlighting-and-navigation-improvements.md +256 -0
- data/doc/banner_svg_config.md +114 -0
- data/doc/contrib.lt3 +8 -0
- data/doc/dependencies.md +281 -0
- data/doc/hacker.lt3 +5 -0
- data/doc/reddit_credentials_template.json +8 -0
- data/doc/reddit_integration.md +207 -0
- data/doc/user.lt3 +38 -0
- data/doc/user_guide_section_1.md +137 -0
- data/doc/user_guide_section_10.md +515 -0
- data/doc/user_guide_section_11.md +708 -0
- data/doc/user_guide_section_2.md +233 -0
- data/doc/user_guide_section_3.md +5 -0
- data/doc/user_guide_section_4.md +221 -0
- data/doc/user_guide_section_5.md +243 -0
- data/doc/user_guide_section_6.md +147 -0
- data/doc/user_guide_section_7.md +311 -0
- data/doc/user_guide_section_8.md +224 -0
- data/doc/user_guide_section_9.md +375 -0
- data/doc/userdoc-toc.txt +88 -0
- data/lib/rouge/lexers/livetext.rb +74 -0
- data/lib/scriptorium/api.rb +640 -0
- data/lib/scriptorium/banner_svg.rb +742 -0
- data/lib/scriptorium/contract.rb +33 -0
- data/lib/scriptorium/exceptions.rb +170 -1
- data/lib/scriptorium/helpers.rb +475 -0
- data/lib/scriptorium/post.rb +195 -0
- data/lib/scriptorium/reddit.rb +83 -0
- data/lib/scriptorium/repo.rb +624 -0
- data/lib/scriptorium/standard_files.rb +515 -0
- data/lib/scriptorium/syntax_highlighter.rb +234 -0
- data/lib/scriptorium/theme.rb +179 -0
- data/lib/scriptorium/version.rb +2 -2
- data/lib/scriptorium/view.rb +976 -0
- data/lib/scriptorium/widgets/featured_posts.rb +149 -0
- data/lib/scriptorium/widgets/links.rb +112 -0
- data/lib/scriptorium/widgets/pages.rb +133 -0
- data/lib/scriptorium/widgets/widget.rb +133 -0
- data/lib/scriptorium.rb +21 -40
- data/lib/skeleton.rb +8 -2
- data/scriptorium.gemspec +15 -4
- data/test/README.md +69 -0
- data/test/all +43 -0
- data/test/api_demo.rb +99 -0
- data/test/assets/imagenotfound.jpg +0 -0
- data/test/assets/images/.DS_Store +0 -0
- data/test/assets/images/README.md +27 -0
- data/test/assets/images/odd_aspect.png +0 -0
- data/test/assets/images/perfect.png +0 -0
- data/test/assets/images/small.png +0 -0
- data/test/assets/images/tall.png +0 -0
- data/test/assets/images/very_tall.png +0 -0
- data/test/assets/images/very_wide.png +0 -0
- data/test/assets/images/wide.png +0 -0
- data/test/assets/testbanner.jpg +0 -0
- data/test/banner_svg/simple_helpers.rb +13 -0
- data/test/banner_svg/unit.rb +768 -0
- data/test/ed_test.rb +204 -0
- data/test/integration/cursor_banner_combinations.rb +193 -0
- data/test/integration/cursor_banner_features.rb +374 -0
- data/test/integration/integration_test.rb +326 -0
- data/test/livetext_plugin_test.rb +229 -0
- data/test/manual/asset_mgmt.rb +67 -0
- data/test/manual/banner-tests/config.txt +3 -0
- data/test/manual/banner-tests/index.html +45 -0
- data/test/manual/banner-tests/test01.html +58 -0
- data/test/manual/banner-tests/test02.html +58 -0
- data/test/manual/banner-tests/test03.html +58 -0
- data/test/manual/banner-tests/test04.html +65 -0
- data/test/manual/banner-tests/test05.html +65 -0
- data/test/manual/banner-tests/test06.html +65 -0
- data/test/manual/banner-tests/test07.html +65 -0
- data/test/manual/banner-tests/test08.html +59 -0
- data/test/manual/banner-tests/test09.html +59 -0
- data/test/manual/banner-tests/test10.html +59 -0
- data/test/manual/banner-tests/test11.html +59 -0
- data/test/manual/banner-tests/test12.html +59 -0
- data/test/manual/banner-tests/test13.html +59 -0
- data/test/manual/banner-tests/test14.html +59 -0
- data/test/manual/banner-tests/test15.html +58 -0
- data/test/manual/banner-tests/test16.html +58 -0
- data/test/manual/banner-tests/test17.html +58 -0
- data/test/manual/banner-tests/test18.html +68 -0
- data/test/manual/banner-tests/test19.html +68 -0
- data/test/manual/banner-tests/test20.html +68 -0
- data/test/manual/banner-tests/test21.html +68 -0
- data/test/manual/banner-tests/test22.html +68 -0
- data/test/manual/banner-tests/test23.html +68 -0
- data/test/manual/banner-tests/test24.html +68 -0
- data/test/manual/banner-tests/test25.html +67 -0
- data/test/manual/banner_environment.rb +192 -0
- data/test/manual/deploy_symlink_demo.rb +142 -0
- data/test/manual/environment.rb +67 -0
- data/test/manual/make_banner.rb +153 -0
- data/test/manual/sample_banner_config.txt +12 -0
- data/test/manual/symlink_demo.rb +117 -0
- data/test/manual/test1.rb +47 -0
- data/test/manual/test2.rb +12 -0
- data/test/manual/test3.rb +38 -0
- data/test/manual/test4.rb +40 -0
- data/test/manual/test5.rb +24 -0
- data/test/manual/test6.rb +73 -0
- data/test/manual/test_banner_combinations.rb +120 -0
- data/test/manual/test_banner_features.rb +306 -0
- data/test/manual/test_banner_from_file.rb +150 -0
- data/test/manual/test_banner_in_header.rb +35 -0
- data/test/manual/test_code_highlighting.rb +68 -0
- data/test/manual/test_complex_header.rb +74 -0
- data/test/manual/test_empty_header.rb +32 -0
- data/test/manual/test_radial_custom.rb +58 -0
- data/test/manual/test_radial_large_radius.rb +52 -0
- data/test/manual/test_svg_debug.rb +47 -0
- data/test/manual/test_syntax_highlighting.rb +147 -0
- data/test/pages-demo/config/currentview.txt +1 -0
- data/test/pages-demo/views/demo/config/bootstrap_css.txt +5 -0
- data/test/pages-demo/views/demo/config/bootstrap_js.txt +4 -0
- data/test/pages-demo/views/demo/config/common.js +57 -0
- data/test/pages-demo/views/demo/config/footer.txt +1 -0
- data/test/pages-demo/views/demo/config/global-head.txt +8 -0
- data/test/pages-demo/views/demo/config/header.txt +1 -0
- data/test/pages-demo/views/demo/config/layout.txt +1 -0
- data/test/pages-demo/views/demo/config/left.txt +1 -0
- data/test/pages-demo/views/demo/config/main.txt +1 -0
- data/test/pages-demo/views/demo/config/right.txt +1 -0
- data/test/pages-demo/views/demo/config.txt +3 -0
- data/test/pages-demo/views/demo/output/panes/footer.html +1 -0
- data/test/pages-demo/views/demo/output/panes/header.html +1 -0
- data/test/pages-demo/views/demo/output/panes/left.html +1 -0
- data/test/pages-demo/views/demo/output/panes/main.html +1 -0
- data/test/pages-demo/views/demo/output/panes/right.html +1 -0
- data/test/rubytext/rubytext_comprehensive_test.rb +307 -0
- data/test/rubytext/rubytext_demo_test.rb +42 -0
- data/test/rubytext/rubytext_testing_guide.md +277 -0
- data/test/run_automated_tests.rb +45 -0
- data/test/scriptorium-TEST-1754622690-146/config/bootstrap_css.txt +5 -0
- data/test/scriptorium-TEST-1754622690-146/config/bootstrap_js.txt +4 -0
- data/test/scriptorium-TEST-1754622690-146/config/common.js +57 -0
- data/test/scriptorium-TEST-1754622690-146/config/currentview.txt +1 -0
- data/test/scriptorium-TEST-1754622690-146/config/global-head.txt +9 -0
- data/test/scriptorium-TEST-1754622690-146/config/last_post_num.txt +1 -0
- data/test/scriptorium-TEST-1754622690-146/config/os_helpers.rb +4 -0
- data/test/scriptorium-TEST-1754622690-146/config/widgets.txt +3 -0
- data/test/scriptorium-TEST-1754622690-146/posts/0001/meta.txt +8 -0
- data/test/scriptorium-TEST-1754622690-146/posts/0001/source.lt3 +6 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/README.txt +1 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/config.txt +1 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/initial/post.lt3 +12 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/footer.txt +2 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/header.txt +4 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/left.txt +3 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/main.txt +5 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/right.txt +3 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/gen/text.css +1 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/layout.txt +5 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/index.lt3 +1 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/index_entry.lt3 +14 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/post.lt3 +13 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/widget.lt3 +1 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/bootstrap_css.txt +5 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/bootstrap_js.txt +4 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/common.js +57 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/deploy.txt +5 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/footer.txt +2 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/global-head.txt +9 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/header.txt +4 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/layout.txt +5 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/left.txt +3 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/main.txt +5 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/reddit.txt +10 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/right.txt +3 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/social.txt +7 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/status.txt +7 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config.txt +3 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/layout/footer.html +3 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/layout/header.html +3 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/layout/left.html +3 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/layout/main.html +3 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/layout/right.html +3 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/footer.html +1 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/header.html +1 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/left.html +1 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/main.html +1 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/right.html +1 -0
- data/test/staging/.DS_Store +0 -0
- data/test/syntax_highlighting_test.lt3 +124 -0
- data/test/test_helpers.rb +230 -0
- data/test/tui_editor_integration_test.rb +296 -0
- data/test/tui_integration_test.rb +637 -0
- data/test/unit/api.rb +1056 -0
- data/test/unit/asset_management.rb +245 -0
- data/test/unit/clipboard_test.rb +60 -0
- data/test/unit/contract_test.rb +91 -0
- data/test/unit/core.rb +857 -0
- data/test/unit/deploy_test.rb +187 -0
- data/test/unit/gem_asset_management.rb +189 -0
- data/test/unit/livetext_basic.rb +69 -0
- data/test/unit/livetext_compatibility.rb +89 -0
- data/test/unit/post.rb +244 -0
- data/test/unit/read_commented_file_test.rb +276 -0
- data/test/unit/reddit_test.rb +235 -0
- data/test/unit/repo.rb +548 -0
- data/test/unit/social_test.rb +369 -0
- data/test/unit/symlink_test.rb +213 -0
- data/test/unit/view.rb +431 -0
- data/test/unit/widgets.rb +669 -0
- data/test/wizard_test.rb +123 -0
- data/ui/README.md +67 -0
- data/ui/common/lib/ui_common.rb +8 -0
- data/ui/rubytext/README.md +191 -0
- data/ui/rubytext/bin/scriptorium-rubytext +402 -0
- data/ui/rubytext/lib/rubytext_ui.rb +300 -0
- data/ui/tui/bin/scriptorium +1420 -0
- data/ui/tui/test/tui_test.rb +23 -0
- data/ui/web/app/app.rb +1378 -0
- data/ui/web/app/error_helpers.rb +150 -0
- data/ui/web/app/views/advanced_config.erb +190 -0
- data/ui/web/app/views/asset_management.erb +589 -0
- data/ui/web/app/views/banner_config.erb +200 -0
- data/ui/web/app/views/configure_view.erb +401 -0
- data/ui/web/app/views/dashboard.erb +162 -0
- data/ui/web/app/views/deploy_config.erb +146 -0
- data/ui/web/app/views/edit_pages.erb +195 -0
- data/ui/web/app/views/edit_post.erb +54 -0
- data/ui/web/app/views/error_page.erb +29 -0
- data/ui/web/app/views/header_config.erb +155 -0
- data/ui/web/app/views/layout_config.erb +147 -0
- data/ui/web/app/views/navbar_config.erb +411 -0
- data/ui/web/app/views/view_dashboard.erb +138 -0
- data/ui/web/bin/scriptorium-web +153 -0
- data/ui/web/test/web_basic_test.rb +38 -0
- data/ui/web/test_navbar.txt +7 -0
- data/ui/web/tmp/web_server.log +5 -0
- data/ui/web/tmp/web_server.pid +1 -0
- metadata +359 -7
- data/lib/scriptorium/engine.rb +0 -22
- data/test/engine/unit.rb +0 -44
@@ -0,0 +1,33 @@
|
|
1
|
+
module Scriptorium::Contract
|
2
|
+
def self.enabled?
|
3
|
+
!ENV['DBC_DISABLED']
|
4
|
+
end
|
5
|
+
|
6
|
+
def assume(condition = nil, message = nil, &block)
|
7
|
+
return unless Scriptorium::Contract.enabled?
|
8
|
+
if block_given?
|
9
|
+
raise "Precondition violated: #{message}" unless instance_eval(&block)
|
10
|
+
else
|
11
|
+
raise "Precondition violated: #{message}" unless condition
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def verify(condition = nil, message = nil, &block)
|
16
|
+
return unless Scriptorium::Contract.enabled?
|
17
|
+
if block_given?
|
18
|
+
raise "Postcondition violated: #{message}" unless instance_eval(&block)
|
19
|
+
else
|
20
|
+
raise "Postcondition violated: #{message}" unless condition
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def invariant(&block)
|
25
|
+
@invariants ||= []
|
26
|
+
@invariants << block
|
27
|
+
end
|
28
|
+
|
29
|
+
def check_invariants
|
30
|
+
return unless Scriptorium::Contract.enabled?
|
31
|
+
@invariants&.each { |invariant| raise "Invariant violated" unless instance_eval(&invariant) }
|
32
|
+
end
|
33
|
+
end
|
@@ -1,5 +1,174 @@
|
|
1
1
|
module Scriptorium::Exceptions
|
2
2
|
|
3
|
-
|
3
|
+
def make_exception(sym, str, target_class = Object)
|
4
|
+
return if target_class.constants.include?(sym)
|
5
|
+
klass = sym # :"#{sym}_Class"
|
6
|
+
target_class.const_set(klass, StandardError)
|
7
|
+
define_method(sym) do |*args|
|
8
|
+
args = [] unless args.first
|
9
|
+
msg = str.dup
|
10
|
+
args.each.with_index {|arg, i| msg.sub!("%#{i+1}", arg.to_s) }
|
11
|
+
target_class.class_eval(klass.to_s).new(msg)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
make_exception :TestModeOnly, "Test mode only"
|
16
|
+
make_exception :ViewDirAlreadyExists, "View directory already exists: %1"
|
17
|
+
make_exception :RepoDirAlreadyExists, "Repository directory already exists: %1"
|
18
|
+
make_exception :ViewDirDoesntExist, "View directory doesn't exist: %1"
|
19
|
+
make_exception :MoreThanOneResult, "More than one result found for: %1"
|
20
|
+
make_exception :CannotLookupView, "Cannot lookup view: %1"
|
21
|
+
make_exception :ThemeDoesntExist, "Theme doesn't exist: %1"
|
22
|
+
make_exception :ThemeFileNotFound, "Theme file not found: %1"
|
23
|
+
make_exception :LayoutHasUnknownTag, "Layout has unknown tag: %1"
|
24
|
+
make_exception :LayoutHasDuplicateTags, "Layout has duplicate tags: %1"
|
25
|
+
make_exception :LayoutFileMissing, "Layout file missing: %1"
|
26
|
+
make_exception :AssetNotFound, "Asset not found: %1"
|
27
|
+
make_exception :NoGemPath, "No gem path"
|
28
|
+
|
29
|
+
# Validation errors
|
30
|
+
make_exception :NilValueError, "Value is nil: %1"
|
31
|
+
make_exception :EmptyValueError, "Value is empty or whitespace-only: %1"
|
32
|
+
make_exception :InvalidFormatError, "Invalid format '%1': %2"
|
33
|
+
|
34
|
+
# Specific validation errors
|
35
|
+
make_exception :CannotCreateViewNameNil, "Cannot create view: name is nil"
|
36
|
+
make_exception :CannotCreateViewNameEmpty, "Cannot create view: name is empty or whitespace-only"
|
37
|
+
make_exception :CannotCreateViewNameInvalid, "Cannot create view: invalid name '%1' (only alphanumeric, hyphen, and underscore allowed)"
|
38
|
+
make_exception :CannotCreateViewTitleNil, "Cannot create view: title is nil"
|
39
|
+
make_exception :CannotCreateViewTitleEmpty, "Cannot create view: title is empty or whitespace-only"
|
40
|
+
|
41
|
+
make_exception :CannotLookupViewTargetNil, "Cannot lookup view: target is nil"
|
42
|
+
make_exception :CannotLookupViewTargetEmpty, "Cannot lookup view: target is empty or whitespace-only"
|
43
|
+
|
44
|
+
make_exception :CannotGetPostIdNil, "Cannot get post: id is nil"
|
45
|
+
make_exception :CannotGetPostIdEmpty, "Cannot get post: id is empty or whitespace-only"
|
46
|
+
make_exception :CannotGetPostIdInvalid, "Cannot get post: invalid id '%1' (must be numeric)"
|
47
|
+
|
48
|
+
make_exception :CannotCreatePostRepoNil, "Cannot create post: repo is nil"
|
49
|
+
make_exception :CannotCreatePostNumNil, "Cannot create post: num is nil"
|
50
|
+
make_exception :CannotCreatePostNumEmpty, "Cannot create post: num is empty or whitespace-only"
|
51
|
+
make_exception :CannotCreatePostNumInvalid, "Cannot create post: invalid num '%1' (must be numeric)"
|
52
|
+
|
53
|
+
make_exception :CannotSetPubdateYmdNil, "Cannot set pubdate: ymd is nil"
|
54
|
+
make_exception :CannotSetPubdateYmdEmpty, "Cannot set pubdate: ymd is empty or whitespace-only"
|
55
|
+
make_exception :CannotSetPubdateInvalidFormat, "Cannot set pubdate: invalid date format '%1' (expected YYYY-MM-DD)"
|
56
|
+
|
57
|
+
make_exception :CannotBuildWidgetsArgNil, "Cannot build widgets: argument is nil"
|
58
|
+
make_exception :CannotBuildWidgetsArgEmpty, "Cannot build widgets: argument is empty or whitespace-only"
|
59
|
+
make_exception :CannotBuildWidgetNameNil, "Cannot build widget: widget name is nil or empty"
|
60
|
+
make_exception :CannotBuildWidgetNameInvalid, "Cannot build widget: invalid widget name '%1' (only alphanumeric and underscore allowed)"
|
61
|
+
|
62
|
+
# File/IO errors
|
63
|
+
make_exception :FileNotFoundError, "File not found: %1"
|
64
|
+
make_exception :PermissionDeniedError, "Permission denied: %1"
|
65
|
+
make_exception :DiskFullError, "Disk full: %1"
|
66
|
+
make_exception :DirectoryNotFoundError, "Directory not found: %1"
|
67
|
+
|
68
|
+
# Specific file/IO errors
|
69
|
+
make_exception :CannotWriteFilePathNil, "Cannot write file: file path is nil"
|
70
|
+
make_exception :CannotWriteFilePathEmpty, "Cannot write file: file path is empty or whitespace-only"
|
71
|
+
make_exception :CannotWriteFilePermissionDenied, "Cannot write file %1: permission denied (%2)"
|
72
|
+
make_exception :CannotWriteFileDiskFull, "Cannot write file %1: disk full (%2)"
|
73
|
+
make_exception :CannotWriteFileDirectoryNotFound, "Cannot write file %1: directory not found (%2)"
|
74
|
+
make_exception :CannotWriteFileError, "Cannot write file %1: %2"
|
75
|
+
|
76
|
+
make_exception :CannotCreateDirectoryPathNil, "Cannot create directory: directory path is nil"
|
77
|
+
make_exception :CannotCreateDirectoryPathEmpty, "Cannot create directory: directory path is empty or whitespace-only"
|
78
|
+
make_exception :CannotCreateDirectoryPermissionDenied, "Cannot create directory %1: permission denied (%2)"
|
79
|
+
make_exception :CannotCreateDirectoryParentNotFound, "Cannot create directory %1: parent directory not found (%2)"
|
80
|
+
make_exception :CannotCreateDirectoryDiskFull, "Cannot create directory %1: disk full (%2)"
|
81
|
+
make_exception :CannotCreateDirectoryError, "Cannot create directory %1: %2"
|
82
|
+
|
83
|
+
make_exception :CannotReadFilePathNil, "Cannot read file: file path is nil"
|
84
|
+
make_exception :CannotReadFilePathEmpty, "Cannot read file: file path is empty or whitespace-only"
|
85
|
+
make_exception :CannotReadFileNotFound, "Cannot read file %1: file not found (%2)"
|
86
|
+
make_exception :CannotReadFilePermissionDenied, "Cannot read file %1: permission denied (%2)"
|
87
|
+
make_exception :CannotReadFileError, "Cannot read file %1: %2"
|
88
|
+
|
89
|
+
make_exception :CannotEditFilePathNil, "Cannot edit file: file path is nil"
|
90
|
+
make_exception :CannotEditFilePathEmpty, "Cannot edit file: file path is empty or whitespace-only"
|
91
|
+
|
92
|
+
make_exception :CannotRequirePathNil, "Cannot require %1: path is nil"
|
93
|
+
make_exception :CannotRequirePathEmpty, "Cannot require %1: path is empty or whitespace-only"
|
94
|
+
make_exception :RequiredFileNotFound, "Required %1 not found: %2"
|
95
|
+
make_exception :InvalidType, "Invalid type: %1 (must be :file or :dir)"
|
96
|
+
|
97
|
+
# View errors
|
98
|
+
make_exception :CannotCreateView, "Cannot create view: %1"
|
99
|
+
make_exception :CannotBuildWidget, "Cannot build widget: %1"
|
100
|
+
|
101
|
+
# Post errors
|
102
|
+
make_exception :CannotCreatePost, "Cannot create post: %1"
|
103
|
+
make_exception :CannotGetPost, "Cannot get post: %1"
|
104
|
+
make_exception :CannotSetPubdate, "Cannot set pubdate: %1"
|
105
|
+
|
106
|
+
# Banner SVG errors
|
107
|
+
make_exception :CannotHandleBackground, "Cannot handle background: %1"
|
108
|
+
make_exception :CannotHandleGradient, "Cannot handle gradient: %1"
|
109
|
+
make_exception :CannotHandleImage, "Cannot handle image: %1"
|
110
|
+
make_exception :CannotHandleAspect, "Cannot handle aspect: %1"
|
111
|
+
make_exception :CannotHandleFont, "Cannot handle font: %1"
|
112
|
+
make_exception :CannotHandleColor, "Cannot handle color: %1"
|
113
|
+
make_exception :CannotHandleAlign, "Cannot handle align: %1"
|
114
|
+
make_exception :CannotHandleXY, "Cannot handle xy: %1"
|
115
|
+
|
116
|
+
# Specific Banner SVG errors
|
117
|
+
make_exception :CannotHandleBackgroundNoArgs, "Cannot handle background: no arguments provided"
|
118
|
+
make_exception :CannotHandleBackgroundFirstArgNil, "Cannot handle background: first argument is nil"
|
119
|
+
make_exception :CannotHandleBackgroundFirstArgEmpty, "Cannot handle background: first argument is empty or whitespace-only"
|
120
|
+
|
121
|
+
make_exception :CannotHandleLinearGradientNoArgs, "Cannot handle linear gradient: no arguments provided"
|
122
|
+
make_exception :CannotHandleLinearGradientStartColorNil, "Cannot handle linear gradient: start color is nil or empty"
|
123
|
+
make_exception :CannotHandleLinearGradientArgEmpty, "Cannot handle linear gradient: argument %1 is empty or whitespace-only"
|
124
|
+
|
125
|
+
make_exception :CannotHandleRadialGradientNoArgs, "Cannot handle radial gradient: no arguments provided"
|
126
|
+
make_exception :CannotHandleRadialGradientStartColorNil, "Cannot handle radial gradient: start color is nil or empty"
|
127
|
+
make_exception :CannotHandleRadialGradientArgEmpty, "Cannot handle radial gradient: argument %1 is empty or whitespace-only"
|
128
|
+
|
129
|
+
make_exception :CannotHandleImageBackgroundNoArgs, "Cannot handle image background: no arguments provided"
|
130
|
+
make_exception :CannotHandleImageBackgroundFirstArgNil, "Cannot handle image background: first argument is nil"
|
131
|
+
make_exception :CannotHandleImageBackgroundFirstArgEmpty, "Cannot handle image background: first argument is empty or whitespace-only"
|
132
|
+
|
133
|
+
make_exception :CannotHandleAspectNoArgs, "Cannot handle aspect: no arguments provided"
|
134
|
+
make_exception :CannotHandleAspectFirstArgNil, "Cannot handle aspect: first argument is nil"
|
135
|
+
make_exception :CannotHandleAspectFirstArgEmpty, "Cannot handle aspect: first argument is empty or whitespace-only"
|
136
|
+
make_exception :CannotHandleAspectInvalidValue, "Cannot handle aspect: invalid aspect value '%1' (must be numeric)"
|
137
|
+
|
138
|
+
make_exception :CannotHandleFontArgsNil, "Cannot handle font: arguments are nil"
|
139
|
+
make_exception :CannotHandleFontArgNil, "Cannot handle font: argument %1 is nil"
|
140
|
+
make_exception :CannotHandleFontArgEmpty, "Cannot handle font: argument %1 is empty or whitespace-only"
|
141
|
+
|
142
|
+
make_exception :CannotHandleTextColorNoArgs, "Cannot handle text color: no arguments provided"
|
143
|
+
make_exception :CannotHandleTextColorFirstArgNil, "Cannot handle text color: first argument is nil"
|
144
|
+
make_exception :CannotHandleTextColorFirstArgEmpty, "Cannot handle text color: first argument is empty or whitespace-only"
|
145
|
+
|
146
|
+
make_exception :CannotHandleXYWhichNil, "Cannot handle xy: which is nil"
|
147
|
+
make_exception :CannotHandleXYWhichEmpty, "Cannot handle xy: which is empty or whitespace-only"
|
148
|
+
make_exception :CannotHandleXYInvalidWhich, "Cannot handle xy: invalid which '%1' (must be 'title' or 'subtitle')"
|
149
|
+
|
150
|
+
make_exception :CannotHandleAlignNoArgs, "Cannot handle align: no arguments provided"
|
151
|
+
make_exception :CannotHandleAlignDirectionNil, "Cannot handle align: direction is nil or empty"
|
152
|
+
make_exception :CannotHandleAlignInvalidDirection, "Cannot handle align: invalid direction '%1' (must be 'left', 'center', or 'right')"
|
153
|
+
make_exception :CannotHandleAlignArgEmpty, "Cannot handle align: argument %1 is empty or whitespace-only"
|
154
|
+
|
155
|
+
make_exception :CannotHandleColorNoArgs, "Cannot handle color: no arguments provided"
|
156
|
+
make_exception :CannotHandleColorFirstArgNil, "Cannot handle color: first argument is nil"
|
157
|
+
make_exception :CannotHandleColorFirstArgEmpty, "Cannot handle color: first argument is empty or whitespace-only"
|
158
|
+
|
159
|
+
|
160
|
+
|
161
|
+
# Command errors
|
162
|
+
make_exception :CommandFailed, "Command failed: %1"
|
163
|
+
make_exception :CannotExecuteCommand, "Cannot execute command: %1"
|
164
|
+
|
165
|
+
# Specific command errors
|
166
|
+
make_exception :CannotExecuteCommandNil, "Cannot execute command: command is nil"
|
167
|
+
make_exception :CannotExecuteCommandEmpty, "Cannot execute command: command is empty or whitespace-only"
|
168
|
+
make_exception :CommandFailedWithDesc, "Command failed%1: %2"
|
169
|
+
|
170
|
+
# Section/Output errors
|
171
|
+
make_exception :SectionOutputError, "Section output error: %1 (section: %2) - %3"
|
172
|
+
make_exception :FailedToWriteFrontPage, "Failed to write front page: %1"
|
4
173
|
|
5
174
|
end
|
@@ -0,0 +1,475 @@
|
|
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
|
+
def getvars(file)
|
22
|
+
lines = read_file(file, lines: true)
|
23
|
+
lines.map! {|line| line.sub(/# .*$/, "").strip }
|
24
|
+
lines.reject! {|line| line.empty? }
|
25
|
+
vhash = Hash.new("")
|
26
|
+
lines.each do |line|
|
27
|
+
var, val = line.split(" ", 2)
|
28
|
+
vhash[var.to_sym] = val
|
29
|
+
end
|
30
|
+
vhash
|
31
|
+
end
|
32
|
+
|
33
|
+
def d4(num)
|
34
|
+
"%04d" % num
|
35
|
+
end
|
36
|
+
|
37
|
+
def view_dir(name)
|
38
|
+
@root/:views/name
|
39
|
+
end
|
40
|
+
|
41
|
+
def write_file(file, content)
|
42
|
+
# Input validation
|
43
|
+
raise CannotWriteFilePathNil if file.nil?
|
44
|
+
|
45
|
+
raise CannotWriteFilePathEmpty if file.to_s.strip.empty?
|
46
|
+
|
47
|
+
# Ensure parent directory exists
|
48
|
+
FileUtils.mkdir_p(File.dirname(file))
|
49
|
+
|
50
|
+
# Write the file with error handling
|
51
|
+
begin
|
52
|
+
File.open(file, "w") do |f|
|
53
|
+
f.puts content
|
54
|
+
end
|
55
|
+
rescue Errno::ENOSPC => e
|
56
|
+
raise CannotWriteFileDiskFull(file, e.message)
|
57
|
+
rescue Errno::EACCES => e
|
58
|
+
raise CannotWriteFilePermissionDenied(file, e.message)
|
59
|
+
rescue Errno::ENOENT => e
|
60
|
+
raise CannotWriteFileDirectoryNotFound(file, e.message)
|
61
|
+
rescue => e
|
62
|
+
raise CannotWriteFileError(file, e.message)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def write_file!(file, *lines)
|
67
|
+
# Convert nil values to empty strings for proper joining
|
68
|
+
processed_lines = lines.map { |line| line.nil? ? "" : line.to_s }
|
69
|
+
content = processed_lines.join("\n")
|
70
|
+
# Always add a newline at the end to ensure there's an empty line
|
71
|
+
content += "\n"
|
72
|
+
write_file(file, content)
|
73
|
+
end
|
74
|
+
|
75
|
+
def make_dir(dir, create_parents = false)
|
76
|
+
# Input validation
|
77
|
+
raise CannotCreateDirectoryPathNil if dir.nil?
|
78
|
+
|
79
|
+
raise CannotCreateDirectoryPathEmpty if dir.to_s.strip.empty?
|
80
|
+
|
81
|
+
# Create parent directories if requested
|
82
|
+
if create_parents
|
83
|
+
FileUtils.mkdir_p(dir)
|
84
|
+
else
|
85
|
+
# Create single directory with error handling
|
86
|
+
begin
|
87
|
+
Dir.mkdir(dir)
|
88
|
+
rescue Errno::ENOSPC => e
|
89
|
+
raise CannotCreateDirectoryDiskFull(dir, e.message)
|
90
|
+
rescue Errno::EACCES => e
|
91
|
+
raise CannotCreateDirectoryPermissionDenied(dir, e.message)
|
92
|
+
rescue Errno::ENOENT => e
|
93
|
+
raise CannotCreateDirectoryParentNotFound(dir, e.message)
|
94
|
+
rescue Errno::EEXIST => e
|
95
|
+
# Directory already exists - this is usually not an error
|
96
|
+
# But we could make this configurable if needed
|
97
|
+
rescue => e
|
98
|
+
raise CannotCreateDirectoryError(dir, e.message)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def system!(command, description = nil)
|
104
|
+
# Input validation
|
105
|
+
raise CannotExecuteCommandNil if command.nil?
|
106
|
+
|
107
|
+
raise CannotExecuteCommandEmpty if command.to_s.strip.empty?
|
108
|
+
|
109
|
+
# Execute command with error handling
|
110
|
+
success = system(command)
|
111
|
+
|
112
|
+
unless success
|
113
|
+
desc = description ? " (#{description})" : ""
|
114
|
+
raise CommandFailedWithDesc(desc, command)
|
115
|
+
end
|
116
|
+
|
117
|
+
success
|
118
|
+
end
|
119
|
+
|
120
|
+
def need(type, path, exception_class = RuntimeError)
|
121
|
+
# Input validation
|
122
|
+
raise CannotRequirePathNil(type) if path.nil?
|
123
|
+
|
124
|
+
raise CannotRequirePathEmpty(type) if path.to_s.strip.empty?
|
125
|
+
|
126
|
+
# Check if file/directory exists
|
127
|
+
exists = case type
|
128
|
+
when :file
|
129
|
+
File.exist?(path)
|
130
|
+
when :dir
|
131
|
+
Dir.exist?(path)
|
132
|
+
else
|
133
|
+
raise InvalidType(type)
|
134
|
+
end
|
135
|
+
|
136
|
+
unless exists
|
137
|
+
raise RequiredFileNotFound(type, path) if exception_class == RuntimeError
|
138
|
+
|
139
|
+
# Exception class - try to call it as a method first, then as constructor
|
140
|
+
raise exception_class.call(path) if exception_class.respond_to?(:call)
|
141
|
+
raise exception_class.new(path)
|
142
|
+
end
|
143
|
+
|
144
|
+
path
|
145
|
+
end
|
146
|
+
|
147
|
+
def read_file(file, options = {})
|
148
|
+
# Input validation
|
149
|
+
raise CannotReadFilePathNil if file.nil?
|
150
|
+
|
151
|
+
raise CannotReadFilePathEmpty if file.to_s.strip.empty?
|
152
|
+
|
153
|
+
# Handle missing file with fallback
|
154
|
+
if options[:missing_fallback]
|
155
|
+
return options[:missing_fallback] unless File.exist?(file)
|
156
|
+
end
|
157
|
+
|
158
|
+
# Read the file with error handling
|
159
|
+
begin
|
160
|
+
if options[:lines]
|
161
|
+
# Read as lines
|
162
|
+
if options[:chomp]
|
163
|
+
File.readlines(file, chomp: true)
|
164
|
+
else
|
165
|
+
File.readlines(file)
|
166
|
+
end
|
167
|
+
else
|
168
|
+
# Read as content
|
169
|
+
File.read(file)
|
170
|
+
end
|
171
|
+
rescue Errno::ENOENT => e
|
172
|
+
if options[:missing_fallback]
|
173
|
+
return options[:missing_fallback]
|
174
|
+
else
|
175
|
+
raise CannotReadFileNotFound(file, e.message)
|
176
|
+
end
|
177
|
+
rescue Errno::EACCES => e
|
178
|
+
raise CannotReadFilePermissionDenied(file, e.message)
|
179
|
+
rescue => e
|
180
|
+
raise CannotReadFileError(file, e.message)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
|
185
|
+
|
186
|
+
def change_config(file_path, target_key, new_value)
|
187
|
+
pattern = /
|
188
|
+
^(?<leading>\s*#{Regexp.escape(target_key)}\s+) # key and spacing
|
189
|
+
(?<old_value>[^\#]*?) # value (non-greedy up to comment)
|
190
|
+
(?<trailing>\s*) # trailing space
|
191
|
+
(?<comment>\#.*)?$ # optional comment
|
192
|
+
/x
|
193
|
+
|
194
|
+
lines = read_file(file_path, lines: true)
|
195
|
+
updated_lines = lines.map do |line|
|
196
|
+
if match = pattern.match(line)
|
197
|
+
leading = match[:leading]
|
198
|
+
trailing = match[:trailing]
|
199
|
+
comment = match[:comment] || ''
|
200
|
+
"#{leading}#{new_value}#{trailing}#{comment}\n"
|
201
|
+
else
|
202
|
+
line
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
write_file(file_path, updated_lines.join)
|
207
|
+
end
|
208
|
+
|
209
|
+
def slugify(id, title)
|
210
|
+
slug = title.downcase.strip
|
211
|
+
.gsub(/[^a-z0-9\s_-]/, '') # remove punctuation
|
212
|
+
.gsub(/[\s_-]+/, '-') # replace spaces and underscores with hyphen
|
213
|
+
.gsub(/^-+|-+$/, '') # trim leading/trailing hyphens
|
214
|
+
"#{d4(id)}-#{slug}"
|
215
|
+
end
|
216
|
+
|
217
|
+
def clean_slugify(title)
|
218
|
+
return "title-is-missing" if title.nil?
|
219
|
+
|
220
|
+
slug = title.downcase.strip
|
221
|
+
.gsub(/[^a-z0-9\s_-]/, '') # remove punctuation
|
222
|
+
.gsub(/[\s_-]+/, '-') # replace spaces and underscores with hyphen
|
223
|
+
.gsub(/^-+|-+$/, '') # trim leading/trailing hyphens
|
224
|
+
slug
|
225
|
+
end
|
226
|
+
|
227
|
+
def ymdhms
|
228
|
+
Time.now.strftime("%Y-%m-%d-%H-%M-%S")
|
229
|
+
end
|
230
|
+
|
231
|
+
def see_file(file) # Really from TestHelpers
|
232
|
+
puts "----- File: #{file}"
|
233
|
+
system!("cat #{file}", "displaying file contents")
|
234
|
+
puts "-----"
|
235
|
+
end
|
236
|
+
|
237
|
+
def see(label, var)
|
238
|
+
puts "#{label} = \n<<<\n#{var}\n>>>"
|
239
|
+
end
|
240
|
+
|
241
|
+
def make_tree(base, text)
|
242
|
+
lines = text.split("\n").map(&:chomp)
|
243
|
+
lines.each {|line| line.gsub!(/ *#.*$/, "") }
|
244
|
+
entries = []
|
245
|
+
|
246
|
+
# Determine the root name
|
247
|
+
first_line = lines.shift
|
248
|
+
root = first_line.strip.sub(/\/$/, "") # remove trailing slash
|
249
|
+
root_path = File.join(base, root)
|
250
|
+
make_dir(root_path) unless File.exist?(root_path)
|
251
|
+
|
252
|
+
# Prepare stack starting from root
|
253
|
+
stack = [root_path]
|
254
|
+
|
255
|
+
# Parse the remaining lines
|
256
|
+
lines.each do |line|
|
257
|
+
if (i = line.index(/ [a-zA-Z0-9_.]/))
|
258
|
+
name = line[(i + 1)..-1]
|
259
|
+
level = i / 4
|
260
|
+
else
|
261
|
+
name = line.strip
|
262
|
+
level = 0
|
263
|
+
end
|
264
|
+
entries << [level, name]
|
265
|
+
end
|
266
|
+
|
267
|
+
entries.each do |level, name|
|
268
|
+
stack = stack[0..level]
|
269
|
+
full_path = File.join(stack.last, name)
|
270
|
+
|
271
|
+
if name.end_with?("/")
|
272
|
+
make_dir(full_path) unless File.exist?(full_path)
|
273
|
+
stack << full_path
|
274
|
+
else
|
275
|
+
write_file(full_path, "Empty file generated at #{Time.now}")
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
def substitute(obj, text)
|
281
|
+
vars = obj.is_a?(Hash) ? obj : obj.vars
|
282
|
+
text % vars
|
283
|
+
end
|
284
|
+
|
285
|
+
def escape_html(str)
|
286
|
+
str.gsub(/&/, '&')
|
287
|
+
.gsub(/</, '<')
|
288
|
+
.gsub(/>/, '>')
|
289
|
+
.gsub(/"/, '"')
|
290
|
+
.gsub(/'/, ''')
|
291
|
+
end
|
292
|
+
|
293
|
+
def read_commented_file(file_path)
|
294
|
+
return [] unless File.exist?(file_path)
|
295
|
+
lines = read_file(file_path, lines: true) # Read file and remove newline characters
|
296
|
+
lines.reject! do |line| # Remove empty lines and comments
|
297
|
+
line.strip.empty? || line.strip.start_with?("#")
|
298
|
+
end
|
299
|
+
lines.map! do |line| # Strip trailing comments + preceding spaces
|
300
|
+
line.sub(/# .*$/, "").strip
|
301
|
+
end
|
302
|
+
lines # Return cleaned lines
|
303
|
+
end
|
304
|
+
|
305
|
+
def cf_time(t1, t2)
|
306
|
+
t1 = t1.split(/- :/, 6)
|
307
|
+
t2 = t2.split(/- :/, 6)
|
308
|
+
t1 = Time.new(*t1)
|
309
|
+
t2 = Time.new(*t2)
|
310
|
+
t1 <=> t2
|
311
|
+
end
|
312
|
+
|
313
|
+
def get_asset_path(name)
|
314
|
+
if Scriptorium::Repo.testing
|
315
|
+
# Development/testing: Check dev_assets first, then local assets
|
316
|
+
if File.exist?("dev_assets/#{name}")
|
317
|
+
return "dev_assets/#{name}"
|
318
|
+
elsif File.exist?("assets/#{name}")
|
319
|
+
return "assets/#{name}"
|
320
|
+
else
|
321
|
+
raise AssetNotFound(name)
|
322
|
+
end
|
323
|
+
else # Production
|
324
|
+
# Production: Check user assets first, then gem assets
|
325
|
+
|
326
|
+
# Check user assets first (highest priority)
|
327
|
+
if File.exist?("assets/#{name}")
|
328
|
+
return "assets/#{name}"
|
329
|
+
end
|
330
|
+
|
331
|
+
# Then check gem assets (fallback)
|
332
|
+
begin
|
333
|
+
gem_spec = Gem.loaded_specs['scriptorium']
|
334
|
+
if gem_spec
|
335
|
+
gem_asset_path = "#{gem_spec.full_gem_path}/assets/#{name}"
|
336
|
+
if File.exist?(gem_asset_path)
|
337
|
+
return gem_asset_path
|
338
|
+
end
|
339
|
+
end
|
340
|
+
rescue => e
|
341
|
+
# If gem lookup fails, continue without gem assets
|
342
|
+
end
|
343
|
+
|
344
|
+
# Asset not found
|
345
|
+
raise AssetNotFound(name)
|
346
|
+
end
|
347
|
+
end
|
348
|
+
|
349
|
+
def generate_missing_asset_svg(filename, width: 200, height: 150)
|
350
|
+
# Truncate filename if too long for display
|
351
|
+
display_name = filename.length > 20 ? filename[0..16] + "..." : filename
|
352
|
+
|
353
|
+
# Generate SVG with broken image icon and filename
|
354
|
+
svg = <<~SVG
|
355
|
+
<svg width="#{width}" height="#{height}" xmlns="http://www.w3.org/2000/svg">
|
356
|
+
<!-- Background -->
|
357
|
+
<rect fill="#f8f9fa" stroke="#ddd" stroke-width="1" width="#{width}" height="#{height}" rx="4"/>
|
358
|
+
|
359
|
+
<!-- Broken image icon -->
|
360
|
+
<g transform="translate(#{width/2}, #{height/2 - 20})">
|
361
|
+
<!-- Image frame -->
|
362
|
+
<rect x="-15" y="-10" width="30" height="20" fill="none" stroke="#999" stroke-width="1"/>
|
363
|
+
<!-- Broken corner -->
|
364
|
+
<path d="M 15 -10 L 25 -20 M 15 -10 L 25 0" stroke="#999" stroke-width="1" fill="none"/>
|
365
|
+
<!-- Image icon -->
|
366
|
+
<rect x="-12" y="-7" width="24" height="14" fill="#e9ecef"/>
|
367
|
+
<circle cx="-5" cy="-2" r="2" fill="#999"/>
|
368
|
+
<polygon points="-8,8 -2,2 2,6 8,0" fill="#999"/>
|
369
|
+
</g>
|
370
|
+
|
371
|
+
<!-- Filename -->
|
372
|
+
<text x="#{width/2}" y="#{height/2 + 15}" text-anchor="middle" fill="#666" font-family="Arial, sans-serif" font-size="11">
|
373
|
+
#{escape_html(display_name)}
|
374
|
+
</text>
|
375
|
+
|
376
|
+
<!-- "Asset not found" message -->
|
377
|
+
<text x="#{width/2}" y="#{height/2 + 30}" text-anchor="middle" fill="#999" font-family="Arial, sans-serif" font-size="9">
|
378
|
+
Asset not found
|
379
|
+
</text>
|
380
|
+
</svg>
|
381
|
+
SVG
|
382
|
+
|
383
|
+
svg.strip
|
384
|
+
end
|
385
|
+
|
386
|
+
def list_gem_assets
|
387
|
+
assets = []
|
388
|
+
begin
|
389
|
+
gem_spec = Gem.loaded_specs['scriptorium']
|
390
|
+
if gem_spec
|
391
|
+
gem_assets_dir = "#{gem_spec.full_gem_path}/assets"
|
392
|
+
if Dir.exist?(gem_assets_dir)
|
393
|
+
Dir.glob("#{gem_assets_dir}/**/*").each do |file|
|
394
|
+
next unless File.file?(file)
|
395
|
+
relative_path = file.sub("#{gem_assets_dir}/", "")
|
396
|
+
assets << relative_path
|
397
|
+
end
|
398
|
+
end
|
399
|
+
end
|
400
|
+
rescue => e
|
401
|
+
# If gem lookup fails, return empty array
|
402
|
+
end
|
403
|
+
assets.sort
|
404
|
+
end
|
405
|
+
|
406
|
+
def copy_gem_asset_to_user(asset_name, target_dir = "assets")
|
407
|
+
begin
|
408
|
+
gem_spec = Gem.loaded_specs['scriptorium']
|
409
|
+
if gem_spec
|
410
|
+
gem_asset_path = "#{gem_spec.full_gem_path}/assets/#{asset_name}"
|
411
|
+
if File.exist?(gem_asset_path)
|
412
|
+
# Create target directory if it doesn't exist
|
413
|
+
FileUtils.mkdir_p(target_dir) unless Dir.exist?(target_dir)
|
414
|
+
|
415
|
+
# Copy the asset
|
416
|
+
target_path = "#{target_dir}/#{File.basename(asset_name)}"
|
417
|
+
FileUtils.cp(gem_asset_path, target_path)
|
418
|
+
return target_path
|
419
|
+
end
|
420
|
+
end
|
421
|
+
rescue => e
|
422
|
+
# If gem lookup fails, return nil
|
423
|
+
end
|
424
|
+
nil
|
425
|
+
end
|
426
|
+
|
427
|
+
# Clipboard helper methods
|
428
|
+
def copy_to_clipboard(text)
|
429
|
+
begin
|
430
|
+
require 'clipboard'
|
431
|
+
Clipboard.copy(text)
|
432
|
+
true
|
433
|
+
rescue LoadError => e
|
434
|
+
# Fallback to system commands if clipboard gem not available
|
435
|
+
case RbConfig::CONFIG['host_os']
|
436
|
+
when /darwin/ # macOS
|
437
|
+
system("echo '#{text}' | pbcopy")
|
438
|
+
when /linux/ # Linux
|
439
|
+
system("echo '#{text}' | xclip -selection clipboard")
|
440
|
+
when /mswin|mingw|cygwin/ # Windows
|
441
|
+
system("echo '#{text}' | clip")
|
442
|
+
else
|
443
|
+
puts "Clipboard not supported on this OS"
|
444
|
+
false
|
445
|
+
end
|
446
|
+
rescue => e
|
447
|
+
puts "Failed to copy to clipboard: #{e.message}"
|
448
|
+
false
|
449
|
+
end
|
450
|
+
end
|
451
|
+
|
452
|
+
def get_from_clipboard
|
453
|
+
begin
|
454
|
+
require 'clipboard'
|
455
|
+
Clipboard.paste
|
456
|
+
rescue LoadError => e
|
457
|
+
# Fallback to system commands if clipboard gem not available
|
458
|
+
case RbConfig::CONFIG['host_os']
|
459
|
+
when /darwin/ # macOS
|
460
|
+
`pbpaste`
|
461
|
+
when /linux/ # Linux
|
462
|
+
`xclip -selection clipboard -o`
|
463
|
+
when /mswin|mingw|cygwin/ # Windows
|
464
|
+
`powershell -command "Get-Clipboard"`
|
465
|
+
else
|
466
|
+
puts "Clipboard not supported on this OS"
|
467
|
+
nil
|
468
|
+
end
|
469
|
+
rescue => e
|
470
|
+
puts "Failed to read from clipboard: #{e.message}"
|
471
|
+
nil
|
472
|
+
end
|
473
|
+
end
|
474
|
+
end
|
475
|
+
|