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,938 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
class Scriptorium::Repo
|
4
|
+
include Scriptorium::Exceptions
|
5
|
+
extend Scriptorium::Exceptions
|
6
|
+
include Scriptorium::Helpers
|
7
|
+
extend Scriptorium::Helpers
|
8
|
+
include Scriptorium::Contract
|
9
|
+
extend Scriptorium::Contract
|
10
|
+
|
11
|
+
class << self
|
12
|
+
attr_accessor :testing
|
13
|
+
attr_reader :root, :repo # class level
|
14
|
+
end
|
15
|
+
|
16
|
+
# instance attrs
|
17
|
+
|
18
|
+
attr_reader :root, :views, :current_view
|
19
|
+
|
20
|
+
def self.exist?
|
21
|
+
dir = Scriptorium::Repo.root
|
22
|
+
return false if dir.nil?
|
23
|
+
Dir.exist?(dir)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.create(path = nil, testmode: false)
|
27
|
+
msg = "path must be nil or String, got #{path.class}"
|
28
|
+
assume(msg) { path.nil? || path.is_a?(String) }
|
29
|
+
# Handle backward compatibility: boolean true means testing mode
|
30
|
+
if testmode == true
|
31
|
+
Scriptorium::Repo.testing = path
|
32
|
+
else
|
33
|
+
Scriptorium::Repo.testing = nil
|
34
|
+
end
|
35
|
+
home = ENV['HOME']
|
36
|
+
@predef = Scriptorium::StandardFiles.new
|
37
|
+
@root = path || "#{home}/.scriptorium"
|
38
|
+
raise self.RepoDirAlreadyExists(@root) if Dir.exist?(@root)
|
39
|
+
make_tree(@root, <<~EOS)
|
40
|
+
.
|
41
|
+
├── config/ # Global config files
|
42
|
+
├── views/ # Views
|
43
|
+
├── drafts/ # Draft posts (global)
|
44
|
+
├── posts/ # Global generated posts (slug.html)
|
45
|
+
├── assets/ # Images, etc.
|
46
|
+
│ └── library/ # Common images, icons, etc.
|
47
|
+
└── themes/ # Themes
|
48
|
+
EOS
|
49
|
+
|
50
|
+
postnum_file = "#@root/config/last_post_num.txt"
|
51
|
+
write_file(postnum_file, "0")
|
52
|
+
write_file(@root/:config/"global-head.txt", @predef.html_head_content)
|
53
|
+
copy_support_file('bootstrap/js.txt', @root/:config/"bootstrap_js.txt")
|
54
|
+
copy_support_file('bootstrap/css.txt', @root/:config/"bootstrap_css.txt")
|
55
|
+
write_file(@root/:config/"common.js", @predef.common_js)
|
56
|
+
write_file(@root/:config/"widgets.txt", @predef.available_widgets)
|
57
|
+
copy_support_file('post_index/config.txt', @root/:config/"post_index_defaults.txt")
|
58
|
+
|
59
|
+
# Create global credentials directory and template
|
60
|
+
FileUtils.mkdir_p(@root/:credentials)
|
61
|
+
copy_support_file('config/reddit_template.txt', @root/:credentials/"reddit.txt")
|
62
|
+
|
63
|
+
|
64
|
+
|
65
|
+
Scriptorium::Theme.create_standard(@root) # Theme: templates, etc.
|
66
|
+
|
67
|
+
# Copy application-wide gem assets to library
|
68
|
+
Scriptorium::Theme.copy_gem_assets_to_library(@root)
|
69
|
+
|
70
|
+
# Generate OS-specific helper code
|
71
|
+
generate_os_helpers(@root)
|
72
|
+
|
73
|
+
@repo = self.open(@root)
|
74
|
+
Scriptorium::View.create_sample_view(@repo)
|
75
|
+
verify { @repo.is_a?(Scriptorium::Repo) }
|
76
|
+
return @repo
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.open(root)
|
80
|
+
msg = "root must be a non-empty String, got #{root.class} (#{root.inspect})"
|
81
|
+
assume(msg) { root.is_a?(String) && !root.empty? }
|
82
|
+
repo = Scriptorium::Repo.new(root)
|
83
|
+
verify { repo.is_a?(Scriptorium::Repo) }
|
84
|
+
repo
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.destroy
|
88
|
+
msg = "Repo.testing must be true in test mode"
|
89
|
+
assume(msg) { Scriptorium::Repo.testing }
|
90
|
+
raise self.TestModeOnly unless Scriptorium::Repo.testing
|
91
|
+
system!("rm -rf #@root", "destroying repository")
|
92
|
+
verify { !Dir.exist?(@root) }
|
93
|
+
end
|
94
|
+
|
95
|
+
def postnum_file
|
96
|
+
"#@root/config/last_post_num.txt"
|
97
|
+
end
|
98
|
+
|
99
|
+
# Invariants
|
100
|
+
def define_invariants
|
101
|
+
invariant { @root.is_a?(String) && !@root.empty? }
|
102
|
+
invariant { @views.is_a?(Array) }
|
103
|
+
invariant { @current_view.nil? || @current_view.is_a?(Scriptorium::View) }
|
104
|
+
end
|
105
|
+
|
106
|
+
def initialize(root) # repo
|
107
|
+
msg = "root must be a non-empty String, got #{root.class} (#{root.inspect})"
|
108
|
+
assume(msg) { root.is_a?(String) && !root.empty? }
|
109
|
+
@root = root
|
110
|
+
@predef = Scriptorium::StandardFiles.new
|
111
|
+
# Scriptorium::Repo.class_eval { @root, @repo = root, self }
|
112
|
+
self.class.instance_variable_set(:@root, root)
|
113
|
+
self.class.instance_variable_set(:@repo, self)
|
114
|
+
load_views
|
115
|
+
@reddit = nil # Lazy load Reddit integration
|
116
|
+
define_invariants
|
117
|
+
verify { @root == root }
|
118
|
+
check_invariants
|
119
|
+
end
|
120
|
+
|
121
|
+
private def load_views
|
122
|
+
@views = []
|
123
|
+
list = Dir.entries(@root/:views) - %w[. .. config.txt]
|
124
|
+
list.each {|dir| open_view(dir) }
|
125
|
+
cview_file = @root/:config/"currentview.txt"
|
126
|
+
@current_view = nil
|
127
|
+
if File.exist?(cview_file)
|
128
|
+
view_name = read_file(cview_file).chomp
|
129
|
+
begin
|
130
|
+
@current_view = lookup_view(view_name)
|
131
|
+
rescue
|
132
|
+
# If the saved view doesn't exist, just leave current_view as nil
|
133
|
+
# It will be set when a view is created or selected
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
### View methods...
|
139
|
+
|
140
|
+
def lookup_view(target)
|
141
|
+
return target if target.is_a?(Scriptorium::View)
|
142
|
+
|
143
|
+
validate_view_target(target)
|
144
|
+
|
145
|
+
list = @views.select {|v| v.name == target }
|
146
|
+
raise CannotLookupView(target) if list.empty?
|
147
|
+
raise MoreThanOneResult(target) if list.size > 1
|
148
|
+
return list[0]
|
149
|
+
end
|
150
|
+
|
151
|
+
private def validate_view_target(target)
|
152
|
+
raise ViewTargetNil if target.nil?
|
153
|
+
|
154
|
+
raise ViewTargetEmpty if target.to_s.strip.empty?
|
155
|
+
|
156
|
+
# Validate that target is a valid view name (alphanumeric, hyphen, underscore)
|
157
|
+
unless target.match?(/^[a-zA-Z0-9_-]+$/)
|
158
|
+
raise ViewTargetInvalid(target)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def view(change = nil) # get/set current view
|
163
|
+
return @current_view if change.nil?
|
164
|
+
vnew = change.is_a?(Scriptorium::View) ? change : lookup_view(change)
|
165
|
+
write_file(@root/:config/"currentview.txt", vnew.name)
|
166
|
+
@current_view = vnew
|
167
|
+
@current_view
|
168
|
+
end
|
169
|
+
|
170
|
+
def current_view
|
171
|
+
@current_view
|
172
|
+
end
|
173
|
+
|
174
|
+
def view_exist?(name)
|
175
|
+
Dir.exist?("#@root/views/#{name}")
|
176
|
+
end
|
177
|
+
|
178
|
+
def create_view(name, title, subtitle = "", theme: "standard")
|
179
|
+
msg = "name must be a String, got #{name.class}"
|
180
|
+
assume(msg) { name.is_a?(String) }
|
181
|
+
msg = "title must be a String, got #{title.class}"
|
182
|
+
assume(msg) { title.is_a?(String) }
|
183
|
+
validate_view_name(name)
|
184
|
+
validate_view_title(title)
|
185
|
+
|
186
|
+
# Validate name format (only allow alphanumeric, hyphen, underscore)
|
187
|
+
unless name.match?(/^[a-zA-Z0-9_-]+$/)
|
188
|
+
raise ViewNameInvalid(name)
|
189
|
+
end
|
190
|
+
|
191
|
+
raise ViewDirAlreadyExists(name) if view_exist?(name)
|
192
|
+
make_tree(@root/:views/name, <<~EOS)
|
193
|
+
.
|
194
|
+
├── config/ # View-specific config files
|
195
|
+
│ ├── layout.txt # Overall layout for front page
|
196
|
+
│ ├── footer.txt # Content for footer.html
|
197
|
+
│ ├── header.txt # Content for header.html
|
198
|
+
│ ├── left.txt # Content for left.html
|
199
|
+
│ ├── main.txt # Content for main.html
|
200
|
+
│ └── right.txt # Content for right.html
|
201
|
+
├── config.txt # View-specific config file # maybe call settings.txt?
|
202
|
+
├── layout/ # Unused?
|
203
|
+
├── pages/ # Static pages for view
|
204
|
+
├── assets/ # Images, etc. (view-specific)
|
205
|
+
│ └── missing/ # Missing assets (SVG placeholder files)
|
206
|
+
├── output/ # Output files (generated HTML)
|
207
|
+
│ ├── panes/ # Containers from layout.txt
|
208
|
+
│ │ ├── footer.html # Generated from footer.txt
|
209
|
+
│ │ ├── header.html # Generated from header.txt
|
210
|
+
│ │ ├── left.html # Generated from left.txt
|
211
|
+
│ │ ├── main.html # Generated from main.txt
|
212
|
+
│ │ └── right.html # Generated from right.txt
|
213
|
+
│ └── posts/ # Generated posts for view (slug.html)
|
214
|
+
├── posts/ # Post state tracking
|
215
|
+
│ ├── unpublished.txt # Posts NOT published in this view (empty = all published)
|
216
|
+
│ └── undeployed.txt # Posts NOT published in this view (empty = all deployed)
|
217
|
+
├── widgets/ # Widgets for view
|
218
|
+
└── staging/ # Staging area prior to deployment
|
219
|
+
EOS
|
220
|
+
|
221
|
+
###
|
222
|
+
|
223
|
+
dir = "#@root/views/#{name}"
|
224
|
+
|
225
|
+
begin
|
226
|
+
write_file!(dir/"config.txt",
|
227
|
+
"title #{title}",
|
228
|
+
"subtitle #{subtitle}",
|
229
|
+
"theme #{theme}")
|
230
|
+
|
231
|
+
write_file(dir/:config/"global-head.txt", @predef.html_head_content(true)) # true = view-specific
|
232
|
+
copy_support_file('bootstrap/js.txt', dir/:config/"bootstrap_js.txt")
|
233
|
+
copy_support_file('bootstrap/css.txt', dir/:config/"bootstrap_css.txt")
|
234
|
+
# Highlight.js config files (renamed from prism_* for clarity)
|
235
|
+
copy_support_file('highlight/js.txt', dir/:config/"highlight_js.txt")
|
236
|
+
write_file(dir/:config/"highlight_ruby_js.txt", @predef.highlight_ruby_js)
|
237
|
+
copy_support_file('highlight/css.txt', dir/:config/"highlight_css.txt")
|
238
|
+
write_file(dir/:config/"common.js", @predef.common_js)
|
239
|
+
copy_support_file('config/social.txt', dir/:config/"social.txt")
|
240
|
+
copy_support_file('config/reddit.txt', dir/:config/"reddit.txt")
|
241
|
+
write_file(dir/:config/"deploy.txt", @predef.deploy_text % {view: name, domain: "example.com"})
|
242
|
+
write_file(dir/:config/"status.txt", @predef.status_txt)
|
243
|
+
copy_support_file('post_index/config.txt', dir/:config/"post_index.txt")
|
244
|
+
|
245
|
+
# Create view credentials directory and template
|
246
|
+
FileUtils.mkdir_p(dir/:credentials)
|
247
|
+
copy_support_file('config/reddit_template.txt', dir/:credentials/"reddit.txt")
|
248
|
+
|
249
|
+
# Copy essential icons to view assets directory
|
250
|
+
view_assets_dir = dir/:assets
|
251
|
+
FileUtils.mkdir_p(view_assets_dir)
|
252
|
+
FileUtils.mkdir_p(view_assets_dir/"icons"/"ui")
|
253
|
+
FileUtils.mkdir_p(view_assets_dir/"icons"/"social")
|
254
|
+
|
255
|
+
# Copy UI icons
|
256
|
+
if File.exist?(@root/:assets/"icons"/"ui"/"back.png")
|
257
|
+
FileUtils.cp(@root/:assets/"icons"/"ui"/"back.png", view_assets_dir/"icons"/"ui"/"back.png")
|
258
|
+
end
|
259
|
+
|
260
|
+
# Copy social icons
|
261
|
+
if File.exist?(@root/:assets/"icons"/"social"/"reddit.png")
|
262
|
+
FileUtils.cp(@root/:assets/"icons"/"social"/"reddit.png", view_assets_dir/"icons"/"social"/"reddit.png")
|
263
|
+
end
|
264
|
+
|
265
|
+
# Copy missing image placeholder
|
266
|
+
if File.exist?(@root/:assets/"imagenotfound.jpg")
|
267
|
+
FileUtils.cp(@root/:assets/"imagenotfound.jpg", view_assets_dir/"imagenotfound.jpg")
|
268
|
+
end
|
269
|
+
|
270
|
+
# Create post state tracking files
|
271
|
+
write_file(dir/:posts/"unpublished.txt", "") # Empty = all posts published
|
272
|
+
write_file(dir/:posts/"undeployed.txt", "") # Empty = all posts deployed
|
273
|
+
|
274
|
+
view = open_view(name)
|
275
|
+
rescue => e
|
276
|
+
# Clean up partial view directory if creation fails
|
277
|
+
FileUtils.rm_rf(dir) if Dir.exist?(dir)
|
278
|
+
raise CannotCreateView("Failed to create view '#{name}': #{e.message}")
|
279
|
+
end
|
280
|
+
@views -= [view]
|
281
|
+
@views << view
|
282
|
+
@current_view = view
|
283
|
+
write_file(@root/:config/"currentview.txt", view.name)
|
284
|
+
cfg = dir/:config # Should these be copied from theme??
|
285
|
+
theme_config = @root/:themes/theme/:layout/:config
|
286
|
+
containers = %w[header.txt footer.txt left.txt right.txt main.txt]
|
287
|
+
containers.each { |container| FileUtils.cp(theme_config/container, cfg/container) } # from theme to view
|
288
|
+
|
289
|
+
# Create default SVG configuration using standard files
|
290
|
+
write_file(cfg/"svg.txt", @predef.svg_txt)
|
291
|
+
|
292
|
+
view.apply_theme(theme)
|
293
|
+
verify { view.is_a?(Scriptorium::View) }
|
294
|
+
return view
|
295
|
+
end
|
296
|
+
|
297
|
+
def open_view(name)
|
298
|
+
config_file = view_dir(name)/"config.txt"
|
299
|
+
vhash = getvars(config_file)
|
300
|
+
title, subtitle, theme = vhash.values_at(:title, :subtitle, :theme)
|
301
|
+
view = Scriptorium::View.new(name, title, subtitle, theme)
|
302
|
+
@views -= [view]
|
303
|
+
@views << view
|
304
|
+
# Remove this line - current view should only be set from currentview.txt
|
305
|
+
# @current_view = view
|
306
|
+
# write_file(@root/:config/"currentview.txt", view.name)
|
307
|
+
view
|
308
|
+
end
|
309
|
+
|
310
|
+
def create_draft(title: nil, blurb: nil, views: nil, tags: nil, body: nil)
|
311
|
+
ts = Time.now.strftime("%Y%m%d-%H%M%S")
|
312
|
+
content_name = "#@root/drafts/#{ts}-draft.lt3"
|
313
|
+
metadata_name = "#@root/drafts/#{ts}-draft.meta"
|
314
|
+
|
315
|
+
# Whoa - what if different views have different themes??? FIXME
|
316
|
+
# Maybe solution is as simple as: Initial post is not theme-dependent
|
317
|
+
views ||= @current_view.name # initial_post wants a String!
|
318
|
+
views, tags = Array(views), Array(tags)
|
319
|
+
|
320
|
+
# Create content file (no ID, no created date)
|
321
|
+
content = @predef.initial_post(:filled, title: title, blurb: blurb,
|
322
|
+
views: views, tags: tags, body: body)
|
323
|
+
write_file(content_name, content)
|
324
|
+
|
325
|
+
# Create metadata file (no ID for drafts)
|
326
|
+
metadata = @predef.initial_post_metadata(title: title, blurb: blurb,
|
327
|
+
views: views, tags: tags)
|
328
|
+
write_file(metadata_name, metadata)
|
329
|
+
|
330
|
+
# Return the content file name (for backward compatibility)
|
331
|
+
content_name
|
332
|
+
end
|
333
|
+
|
334
|
+
def last_post_num
|
335
|
+
read_file(postnum_file).to_i
|
336
|
+
end
|
337
|
+
|
338
|
+
def incr_post_num
|
339
|
+
num = last_post_num + 1
|
340
|
+
write_file(postnum_file, num.to_s)
|
341
|
+
num
|
342
|
+
end
|
343
|
+
|
344
|
+
def finish_draft(name)
|
345
|
+
id = incr_post_num
|
346
|
+
id4 = d4(id)
|
347
|
+
posts = @root/:posts
|
348
|
+
make_dir(posts/id4)
|
349
|
+
make_dir(posts/id4/:assets)
|
350
|
+
|
351
|
+
# Move content file
|
352
|
+
FileUtils.mv(name, posts/id4/"source.lt3")
|
353
|
+
|
354
|
+
# Move metadata file (same timestamp, different extension)
|
355
|
+
metadata_name = name.sub('.lt3', '.meta')
|
356
|
+
FileUtils.mv(metadata_name, posts/id4/"meta.txt") if File.exist?(metadata_name)
|
357
|
+
id
|
358
|
+
end
|
359
|
+
|
360
|
+
def tree(file = nil)
|
361
|
+
cmd = "tree #@root"
|
362
|
+
cmd << " >#{file}" if file
|
363
|
+
system!(cmd, "generating tree structure")
|
364
|
+
end
|
365
|
+
|
366
|
+
|
367
|
+
private def write_post_metadata(data, view)
|
368
|
+
num, title = data.values_at(:"post.id", :"post.title")
|
369
|
+
metadata_file = @root/:posts/d4(num)/"meta.txt"
|
370
|
+
|
371
|
+
# Read existing metadata to preserve fields like post.published
|
372
|
+
existing_metadata = {}
|
373
|
+
existing_metadata = getvars(metadata_file) if File.exist?(metadata_file)
|
374
|
+
|
375
|
+
# Prepare new metadata from data
|
376
|
+
new_metadata = data.select {|k,v| k.to_s.start_with?("post.") }
|
377
|
+
new_metadata.delete(:"post.body")
|
378
|
+
new_metadata[:"post.slug"] = slugify(num, title) + ".html"
|
379
|
+
|
380
|
+
# Merge existing metadata over new metadata to preserve important fields
|
381
|
+
# Only preserve fields that should not be overwritten by source file changes
|
382
|
+
fields_to_preserve = [:"post.published", :"post.deployed", :"post.created"]
|
383
|
+
existing_metadata.each { |key, value| new_metadata[key] = value if fields_to_preserve.include?(key) }
|
384
|
+
|
385
|
+
lines = new_metadata.map { |k, v| sprintf("%-18s %s", k, v) }
|
386
|
+
write_file(metadata_file, lines.join("\n"))
|
387
|
+
end
|
388
|
+
|
389
|
+
private def write_generated_post(data, view, final)
|
390
|
+
num, title = data.values_at(:"post.id", :"post.title")
|
391
|
+
id4 = d4(num)
|
392
|
+
slug = slugify(num, title) + ".html"
|
393
|
+
# Write to:
|
394
|
+
# root/posts/0123/body.html meta.txt (assets/ draft.lt3)
|
395
|
+
top = @root/:posts/id4/"body.html"
|
396
|
+
write_file(top, final)
|
397
|
+
write_post_metadata(data, view)
|
398
|
+
# view/.../output/posts/0123-this-is-me.html
|
399
|
+
path = view.dir/:output/:posts/slug
|
400
|
+
write_file(path, final)
|
401
|
+
# view/.../output/permalink/0123-this-is-me.html (for direct access)
|
402
|
+
permalink_path = view.dir/:output/:permalink/slug
|
403
|
+
make_dir(File.dirname(permalink_path))
|
404
|
+
# Remove the Back-to-index block from permalink content
|
405
|
+
cleaned_final = final.gsub(/<div align='right'>.*?Back to index<\/a>\s*<\/div>\s*/m, "")
|
406
|
+
# Write the permalink version with "Visit Blog" link and "Copy link" button
|
407
|
+
permalink_content = cleaned_final + "\n<div style=\"text-align: center; margin-top: 20px;\">\n<a href=\"../index.html\">Visit Blog</a>\n</div>\n<div style=\"text-align: center; margin-top: 10px;\">\n<button onclick=\"copyPermalinkToClipboard()\" style=\"padding: 8px 16px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer;\">Copy link</button>\n</div>\n<script>\nfunction copyPermalinkToClipboard() {\n navigator.clipboard.writeText(window.location.href).then(function() {\n const button = event.target;\n const originalText = button.textContent;\n button.textContent = 'Copied!';\n button.style.background = '#28a745';\n setTimeout(function() {\n button.textContent = originalText;\n button.style.background = '#007bff';\n }, 2000);\n }).catch(function(err) {\n console.error('Failed to copy: ', err);\n alert('Failed to copy link to clipboard');\n });\n}\n</script>"
|
408
|
+
write_file(permalink_path, permalink_content)
|
409
|
+
|
410
|
+
# Create copy for clean URL (without numeric prefix)
|
411
|
+
clean_slug = clean_slugify(title) + ".html"
|
412
|
+
clean_copy_path = view.dir/:output/:permalink/clean_slug
|
413
|
+
|
414
|
+
# Remove existing file if it exists
|
415
|
+
File.delete(clean_copy_path) if File.exist?(clean_copy_path)
|
416
|
+
|
417
|
+
# Copy the permalink file to create clean URL
|
418
|
+
FileUtils.cp(permalink_path, clean_copy_path)
|
419
|
+
end
|
420
|
+
|
421
|
+
def create_post(title: nil, views: nil, tags: nil, body: nil, blurb: nil)
|
422
|
+
msg = "title must be nil or String, got #{title.class}"
|
423
|
+
assume(msg) { title.nil? || title.is_a?(String) }
|
424
|
+
msg = "views must be nil, Array, or String, got #{views.class}"
|
425
|
+
assume(msg) { views.nil? || views.is_a?(Array) || views.is_a?(String) }
|
426
|
+
msg = "tags must be nil, Array, or String, got #{tags.class}"
|
427
|
+
assume(msg) { tags.nil? || tags.is_a?(Array) || tags.is_a?(String) }
|
428
|
+
msg = "body must be nil or String, got #{body.class}"
|
429
|
+
assume(msg) { body.nil? || body.is_a?(String) }
|
430
|
+
msg = "blurb must be nil or String, got #{blurb.class}"
|
431
|
+
assume(msg) { blurb.nil? || blurb.is_a?(String) }
|
432
|
+
name = create_draft(title: title, views: views, tags: tags, body: body, blurb: blurb)
|
433
|
+
num = finish_draft(name)
|
434
|
+
|
435
|
+
# Add post to unpublished and undeployed lists for all views it belongs to
|
436
|
+
metadata_file = @root/:posts/d4(num)/"meta.txt"
|
437
|
+
if File.exist?(metadata_file)
|
438
|
+
metadata = getvars(metadata_file)
|
439
|
+
views = metadata[:"post.views"]&.strip&.split(/\s+/) || ["sample"]
|
440
|
+
views.each do |view_name|
|
441
|
+
view_obj = lookup_view(view_name)
|
442
|
+
unpublished_file = view_obj.dir/:posts/"unpublished.txt"
|
443
|
+
undeployed_file = view_obj.dir/:posts/"undeployed.txt"
|
444
|
+
add_post_to_state_file(unpublished_file, num)
|
445
|
+
add_post_to_state_file(undeployed_file, num)
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
generate_post(num)
|
450
|
+
post = self.post(num) # Return the Post object
|
451
|
+
verify { post.is_a?(Scriptorium::Post) }
|
452
|
+
post
|
453
|
+
end
|
454
|
+
|
455
|
+
def publish_post(num, view = nil)
|
456
|
+
validate_post_id(num)
|
457
|
+
|
458
|
+
# Check if post exists in normal location first
|
459
|
+
metadata_file = @root/:posts/d4(num)/"meta.txt"
|
460
|
+
unless File.exist?(metadata_file)
|
461
|
+
# Check if post is deleted
|
462
|
+
if post_deleted?(num)
|
463
|
+
raise PostDeleted(num)
|
464
|
+
else
|
465
|
+
raise CannotGetPost("Post #{num} not found")
|
466
|
+
end
|
467
|
+
end
|
468
|
+
|
469
|
+
# Read current metadata
|
470
|
+
metadata = getvars(metadata_file)
|
471
|
+
|
472
|
+
if view.nil?
|
473
|
+
# Use current view if no view specified
|
474
|
+
view = @current_view&.name || "sample"
|
475
|
+
end
|
476
|
+
|
477
|
+
# Check if already published in this view
|
478
|
+
view_obj = lookup_view(view)
|
479
|
+
unpublished_file = view_obj.dir/:posts/"unpublished.txt"
|
480
|
+
if !post_in_state_file?(unpublished_file, num)
|
481
|
+
raise PostAlreadyPublished(num)
|
482
|
+
end
|
483
|
+
|
484
|
+
# View-specific publish - only update the specified view's state
|
485
|
+
remove_post_from_state_file(unpublished_file, num)
|
486
|
+
|
487
|
+
# If this is the first time publishing this post, update global metadata
|
488
|
+
if metadata[:"post.published"] == "no" || metadata[:"post.published"].nil?
|
489
|
+
metadata[:"post.published"] = ymdhms
|
490
|
+
|
491
|
+
# Write updated metadata
|
492
|
+
lines = metadata.map { |k, v| sprintf("%-18s %s", k, v) }
|
493
|
+
write_file(metadata_file, lines.join("\n"))
|
494
|
+
end
|
495
|
+
|
496
|
+
self.post(num)
|
497
|
+
end
|
498
|
+
|
499
|
+
def mark_post_deployed(num, view = nil)
|
500
|
+
validate_post_id(num)
|
501
|
+
|
502
|
+
# Check if post exists in normal location first
|
503
|
+
metadata_file = @root/:posts/d4(num)/"meta.txt"
|
504
|
+
unless File.exist?(metadata_file)
|
505
|
+
# Check if post is deleted
|
506
|
+
if post_deleted?(num)
|
507
|
+
raise PostDeleted(num)
|
508
|
+
else
|
509
|
+
raise CannotGetPost("Post #{num} not found")
|
510
|
+
end
|
511
|
+
end
|
512
|
+
|
513
|
+
if view.nil?
|
514
|
+
# Use current view if no view specified
|
515
|
+
view = @current_view&.name || "sample"
|
516
|
+
end
|
517
|
+
|
518
|
+
# Check if already deployed in this view
|
519
|
+
view_obj = lookup_view(view)
|
520
|
+
undeployed_file = view_obj.dir/:posts/"undeployed.txt"
|
521
|
+
if !post_in_state_file?(undeployed_file, num)
|
522
|
+
raise PostAlreadyDeployed(num)
|
523
|
+
end
|
524
|
+
|
525
|
+
# Validate that only published posts can be deployed
|
526
|
+
unless post_published?(num, view)
|
527
|
+
raise PostNotPublished(num)
|
528
|
+
end
|
529
|
+
|
530
|
+
# Remove from undeployed list for this view
|
531
|
+
remove_post_from_state_file(undeployed_file, num)
|
532
|
+
|
533
|
+
# Update global metadata if this is the first deployment
|
534
|
+
metadata_file = @root/:posts/d4(num)/"meta.txt"
|
535
|
+
if File.exist?(metadata_file)
|
536
|
+
metadata = getvars(metadata_file)
|
537
|
+
if metadata[:"post.deployed"] == "no" || metadata[:"post.deployed"].nil?
|
538
|
+
metadata[:"post.deployed"] = ymdhms
|
539
|
+
lines = metadata.map { |k, v| sprintf("%-18s %s", k, v) }
|
540
|
+
write_file(metadata_file, lines.join("\n"))
|
541
|
+
end
|
542
|
+
else
|
543
|
+
raise CannotGetPost("Post #{num} metadata not found")
|
544
|
+
end
|
545
|
+
end
|
546
|
+
|
547
|
+
def mark_post_undeployed(num, view = nil)
|
548
|
+
validate_post_id(num)
|
549
|
+
|
550
|
+
if view.nil?
|
551
|
+
# Use current view if no view specified
|
552
|
+
view = @current_view&.name || "sample"
|
553
|
+
end
|
554
|
+
|
555
|
+
# Add to undeployed list for this view
|
556
|
+
view_obj = lookup_view(view)
|
557
|
+
undeployed_file = view_obj.dir/:posts/"undeployed.txt"
|
558
|
+
add_post_to_state_file(undeployed_file, num)
|
559
|
+
|
560
|
+
# Update global metadata
|
561
|
+
metadata_file = @root/:posts/d4(num)/"meta.txt"
|
562
|
+
if File.exist?(metadata_file)
|
563
|
+
metadata = getvars(metadata_file)
|
564
|
+
metadata[:"post.deployed"] = "no"
|
565
|
+
lines = metadata.map { |k, v| sprintf("%-18s %s", k, v) }
|
566
|
+
write_file(metadata_file, lines.join("\n"))
|
567
|
+
end
|
568
|
+
end
|
569
|
+
|
570
|
+
def post_deployed?(num, view = nil)
|
571
|
+
validate_post_id(num)
|
572
|
+
|
573
|
+
# If no specific view, check global metadata (for backward compatibility)
|
574
|
+
if view.nil?
|
575
|
+
metadata_file = @root/:posts/d4(num)/"meta.txt"
|
576
|
+
return false unless File.exist?(metadata_file)
|
577
|
+
|
578
|
+
metadata = getvars(metadata_file)
|
579
|
+
return metadata[:"post.deployed"] != "no"
|
580
|
+
end
|
581
|
+
|
582
|
+
# Check view-specific deployed status
|
583
|
+
view = lookup_view(view)
|
584
|
+
undeployed_file = view.dir/:posts/"undeployed.txt"
|
585
|
+
!post_in_state_file?(undeployed_file, num)
|
586
|
+
end
|
587
|
+
|
588
|
+
def get_deployed_posts(view = nil)
|
589
|
+
all_posts = all_posts(view)
|
590
|
+
|
591
|
+
if view.nil?
|
592
|
+
# If no specific view, use global metadata (for backward compatibility)
|
593
|
+
all_posts.select { |post| post_deployed?(post.id) }
|
594
|
+
else
|
595
|
+
# Use view-specific deployed status
|
596
|
+
view = lookup_view(view)
|
597
|
+
undeployed_file = view.dir/:posts/"undeployed.txt"
|
598
|
+
all_posts.reject { |post| post_in_state_file?(undeployed_file, post.id) }
|
599
|
+
end
|
600
|
+
end
|
601
|
+
|
602
|
+
def unpublish_post(num, view = nil)
|
603
|
+
validate_post_id(num)
|
604
|
+
|
605
|
+
# Check if post is deployed in any view (including current view if none specified)
|
606
|
+
if view.nil?
|
607
|
+
view = @current_view&.name || "sample"
|
608
|
+
end
|
609
|
+
|
610
|
+
# Check if post is deployed in this view
|
611
|
+
view_obj = lookup_view(view)
|
612
|
+
if post_deployed?(num, view)
|
613
|
+
raise PostAlreadyDeployed(num)
|
614
|
+
end
|
615
|
+
|
616
|
+
# Always use view-specific logic when a specific view is provided
|
617
|
+
# Add to view-specific unpublished list
|
618
|
+
unpublished_file = view_obj.dir/:posts/"unpublished.txt"
|
619
|
+
add_post_to_state_file(unpublished_file, num)
|
620
|
+
|
621
|
+
# Also update global metadata to "no" if this was the first published view
|
622
|
+
metadata_file = @root/:posts/d4(num)/"meta.txt"
|
623
|
+
if File.exist?(metadata_file)
|
624
|
+
metadata = getvars(metadata_file)
|
625
|
+
metadata[:"post.published"] = "no"
|
626
|
+
lines = metadata.map { |k, v| sprintf("%-18s %s", k, v) }
|
627
|
+
write_file(metadata_file, lines.join("\n"))
|
628
|
+
end
|
629
|
+
|
630
|
+
# Regenerate the post
|
631
|
+
generate_post(num)
|
632
|
+
end
|
633
|
+
|
634
|
+
def post_published?(num, view = nil)
|
635
|
+
validate_post_id(num)
|
636
|
+
|
637
|
+
# If no specific view, check global metadata (for backward compatibility)
|
638
|
+
if view.nil?
|
639
|
+
metadata_file = @root/:posts/d4(num)/"meta.txt"
|
640
|
+
return false unless File.exist?(metadata_file)
|
641
|
+
|
642
|
+
metadata = getvars(metadata_file)
|
643
|
+
return metadata[:"post.published"] != "no"
|
644
|
+
end
|
645
|
+
|
646
|
+
# Check view-specific published status
|
647
|
+
view = lookup_view(view)
|
648
|
+
unpublished_file = view.dir/:posts/"unpublished.txt"
|
649
|
+
!post_in_state_file?(unpublished_file, num)
|
650
|
+
end
|
651
|
+
|
652
|
+
def post_deleted?(num)
|
653
|
+
validate_post_id(num)
|
654
|
+
|
655
|
+
# Check deleted location first (with underscore prefix)
|
656
|
+
deleted_metadata_file = @root/:posts/"_#{d4(num)}"/"meta.txt"
|
657
|
+
if File.exist?(deleted_metadata_file)
|
658
|
+
return true
|
659
|
+
end
|
660
|
+
|
661
|
+
# Check normal location
|
662
|
+
metadata_file = @root/:posts/d4(num)/"meta.txt"
|
663
|
+
return false unless File.exist?(metadata_file)
|
664
|
+
|
665
|
+
metadata = getvars(metadata_file)
|
666
|
+
metadata[:"post.deleted"] == "yes"
|
667
|
+
end
|
668
|
+
|
669
|
+
def generate_post(num)
|
670
|
+
content_file = @root/:posts/d4(num)/"source.lt3"
|
671
|
+
metadata_file = @root/:posts/d4(num)/"meta.txt"
|
672
|
+
|
673
|
+
need(:file, content_file)
|
674
|
+
|
675
|
+
# Read content file
|
676
|
+
vars = { View: @current_view.name, :"post.id" => num }
|
677
|
+
# Mark transform as post-context
|
678
|
+
vars[:"post.context"] = "post"
|
679
|
+
|
680
|
+
# Merge metadata into vars if metadata file exists
|
681
|
+
if File.exist?(metadata_file)
|
682
|
+
metadata = getvars(metadata_file)
|
683
|
+
vars.merge!(metadata)
|
684
|
+
end
|
685
|
+
|
686
|
+
live = Livetext.customize(mix: "lt3scriptor", call: ".nopara", vars: vars)
|
687
|
+
body, vars = live.process(file: content_file)
|
688
|
+
|
689
|
+
# Debug: Write vars to file
|
690
|
+
File.open("/tmp/debug_vars_#{num}.txt", "w") do |f|
|
691
|
+
f.puts "Vars after LiveText processing:"
|
692
|
+
vars.each { |k, v| f.puts "#{k} = #{v.inspect}" }
|
693
|
+
end
|
694
|
+
|
695
|
+
# Use metadata from LiveText processing
|
696
|
+
# Filter vars to only include post.* fields for metadata
|
697
|
+
metadata_vars = vars.select {|k,v| k.to_s.start_with?("post.") }
|
698
|
+
metadata_vars.delete(:"post.body")
|
699
|
+
metadata_vars[:"post.slug"] = slugify(num, vars[:"post.title"]) + ".html"
|
700
|
+
metadata_vars[:"post.published"] = "no"
|
701
|
+
metadata_vars[:"post.deployed"] = "no"
|
702
|
+
|
703
|
+
if vars[:"post.created"]
|
704
|
+
time = Time.parse(vars[:"post.created"])
|
705
|
+
metadata_vars["post.pubdate"] = time.strftime("%Y-%m-%d %H:%M:%S")
|
706
|
+
metadata_vars["post.pubdate.month"] = time.strftime("%B")
|
707
|
+
metadata_vars["post.pubdate.day"] = time.strftime("%e")
|
708
|
+
metadata_vars["post.pubdate.year"] = time.strftime("%Y")
|
709
|
+
end
|
710
|
+
|
711
|
+
# Write metadata file
|
712
|
+
lines = metadata_vars.map { |k, v| sprintf("%-18s %s", k, v) }
|
713
|
+
write_file(metadata_file, lines.join("\n"))
|
714
|
+
|
715
|
+
views = (vars[:"post.views"] || "").strip.split(/\s+/)
|
716
|
+
vars[:"post.views"] = views.join(" ") # Ensure post.views is set in vars
|
717
|
+
views.each do |view|
|
718
|
+
view = lookup_view(view)
|
719
|
+
vars[:"post.id"] = num.to_s # Always use the post number as ID
|
720
|
+
vars[:"post.body"] = body
|
721
|
+
vars[:"post.date"] = self.post(num).date # Set post.date for templates
|
722
|
+
|
723
|
+
|
724
|
+
template = support_data('templates/post.lt3')
|
725
|
+
# Add Reddit button if enabled
|
726
|
+
vars[:"reddit_button"] = view.generate_reddit_button(vars)
|
727
|
+
final = substitute(vars, template)
|
728
|
+
write_generated_post(vars, view, final)
|
729
|
+
end
|
730
|
+
end
|
731
|
+
|
732
|
+
|
733
|
+
|
734
|
+
def all_posts(view = nil)
|
735
|
+
posts = []
|
736
|
+
dirs = Dir.children(@root/:posts)
|
737
|
+
dirs.each do |id4|
|
738
|
+
# Skip deleted posts (directories starting with underscore)
|
739
|
+
next if id4.start_with?('_')
|
740
|
+
posts << Scriptorium::Post.read(self, id4)
|
741
|
+
end
|
742
|
+
return posts if view.nil?
|
743
|
+
view = lookup_view(view)
|
744
|
+
posts.select {|x| x.views.include?(view.name) }
|
745
|
+
end
|
746
|
+
|
747
|
+
def all_posts_including_deleted(view = nil)
|
748
|
+
posts = []
|
749
|
+
dirs = Dir.children(@root/:posts)
|
750
|
+
dirs.each do |id4|
|
751
|
+
# Include both normal and deleted posts
|
752
|
+
if id4.start_with?('_')
|
753
|
+
# Deleted post - remove underscore prefix and pass deleted: true
|
754
|
+
original_id = id4[1..-1]
|
755
|
+
posts << Scriptorium::Post.read(self, original_id, deleted: true)
|
756
|
+
else
|
757
|
+
posts << Scriptorium::Post.read(self, id4)
|
758
|
+
end
|
759
|
+
end
|
760
|
+
return posts if view.nil?
|
761
|
+
view = lookup_view(view)
|
762
|
+
posts.select {|x|
|
763
|
+
views_str = x.views
|
764
|
+
if views_str.nil? || views_str.strip.empty?
|
765
|
+
false
|
766
|
+
else
|
767
|
+
views_str.strip.split(/\s+/).include?(view.name)
|
768
|
+
end
|
769
|
+
}
|
770
|
+
end
|
771
|
+
|
772
|
+
def generate_post_index(view)
|
773
|
+
view = lookup_view(view)
|
774
|
+
view.generate_post_index
|
775
|
+
end
|
776
|
+
|
777
|
+
def post(id)
|
778
|
+
validate_post_id(id)
|
779
|
+
|
780
|
+
# Check normal directory first
|
781
|
+
meta = @root/:posts/d4(id)/"meta.txt"
|
782
|
+
return Scriptorium::Post.new(self, id) if File.exist?(meta)
|
783
|
+
|
784
|
+
# Check deleted directory (with underscore prefix)
|
785
|
+
deleted_meta = @root/:posts/"_#{d4(id)}"/"meta.txt"
|
786
|
+
return Scriptorium::Post.new(self, id) if File.exist?(deleted_meta)
|
787
|
+
|
788
|
+
# Post not found in either location
|
789
|
+
raise CannotGetPost("Post with ID #{id} not found")
|
790
|
+
end
|
791
|
+
|
792
|
+
def delete_post(num)
|
793
|
+
validate_post_id(num)
|
794
|
+
|
795
|
+
# Check if post exists
|
796
|
+
metadata_file = @root/:posts/d4(num)/"meta.txt"
|
797
|
+
unless File.exist?(metadata_file)
|
798
|
+
raise CannotGetPost("Post #{num} not found")
|
799
|
+
end
|
800
|
+
|
801
|
+
# Check if already deleted
|
802
|
+
if post_deleted?(num)
|
803
|
+
raise PostAlreadyDeleted(num)
|
804
|
+
end
|
805
|
+
|
806
|
+
# Mark as deleted in metadata
|
807
|
+
metadata = getvars(metadata_file)
|
808
|
+
metadata[:"post.deleted"] = "yes"
|
809
|
+
metadata[:"post.deleted_at"] = ymdhms
|
810
|
+
|
811
|
+
# Write updated metadata
|
812
|
+
lines = metadata.map { |k, v| sprintf("%-18s %s", k, v) }
|
813
|
+
write_file(metadata_file, lines.join("\n"))
|
814
|
+
|
815
|
+
# Move post directory to deleted location (with underscore prefix)
|
816
|
+
post_dir = @root/:posts/d4(num)
|
817
|
+
deleted_dir = @root/:posts/"_#{d4(num)}"
|
818
|
+
|
819
|
+
if Dir.exist?(post_dir)
|
820
|
+
FileUtils.mv(post_dir, deleted_dir)
|
821
|
+
end
|
822
|
+
end
|
823
|
+
|
824
|
+
def undelete_post(num)
|
825
|
+
validate_post_id(num)
|
826
|
+
|
827
|
+
# Check if post exists in deleted location
|
828
|
+
deleted_dir = @root/:posts/"_#{d4(num)}"
|
829
|
+
unless Dir.exist?(deleted_dir)
|
830
|
+
raise CannotGetPost("Deleted post #{num} not found")
|
831
|
+
end
|
832
|
+
|
833
|
+
# Check if already undeleted
|
834
|
+
unless post_deleted?(num)
|
835
|
+
raise PostNotDeleted(num)
|
836
|
+
end
|
837
|
+
|
838
|
+
# Move post directory back to normal location
|
839
|
+
post_dir = @root/:posts/d4(num)
|
840
|
+
FileUtils.mv(deleted_dir, post_dir)
|
841
|
+
|
842
|
+
# Remove deleted flag from metadata
|
843
|
+
metadata_file = @root/:posts/d4(num)/"meta.txt"
|
844
|
+
if File.exist?(metadata_file)
|
845
|
+
metadata = getvars(metadata_file)
|
846
|
+
metadata.delete(:"post.deleted")
|
847
|
+
metadata.delete(:"post.deleted_at")
|
848
|
+
|
849
|
+
# Write updated metadata
|
850
|
+
lines = metadata.map { |k, v| sprintf("%-18s %s", k, v) }
|
851
|
+
write_file(metadata_file, lines.join("\n"))
|
852
|
+
end
|
853
|
+
end
|
854
|
+
|
855
|
+
|
856
|
+
|
857
|
+
private def validate_post_id(id)
|
858
|
+
raise PostIdNil if id.nil?
|
859
|
+
|
860
|
+
raise PostIdEmpty if id.to_s.strip.empty?
|
861
|
+
|
862
|
+
unless id.to_s.match?(/^\d+$/)
|
863
|
+
raise PostIdInvalid(id)
|
864
|
+
end
|
865
|
+
end
|
866
|
+
|
867
|
+
def generate_front_page(view)
|
868
|
+
view = lookup_view(view)
|
869
|
+
view.generate_front_page
|
870
|
+
end
|
871
|
+
|
872
|
+
# Reddit integration
|
873
|
+
def reddit
|
874
|
+
@reddit ||= Scriptorium::Reddit.new(self)
|
875
|
+
end
|
876
|
+
|
877
|
+
def autopost_to_reddit(post_data, subreddit = nil)
|
878
|
+
reddit.autopost(post_data, subreddit)
|
879
|
+
end
|
880
|
+
|
881
|
+
def reddit_configured?
|
882
|
+
reddit.configured?
|
883
|
+
end
|
884
|
+
|
885
|
+
|
886
|
+
|
887
|
+
private def validate_view_name(name)
|
888
|
+
raise ViewNameNil if name.nil?
|
889
|
+
|
890
|
+
raise ViewNameEmpty if name.to_s.strip.empty?
|
891
|
+
end
|
892
|
+
|
893
|
+
|
894
|
+
|
895
|
+
private def validate_view_title(title)
|
896
|
+
raise ViewTitleNil if title.nil?
|
897
|
+
|
898
|
+
raise ViewTitleEmpty if title.to_s.strip.empty?
|
899
|
+
end
|
900
|
+
|
901
|
+
|
902
|
+
|
903
|
+
def self.generate_os_helpers(root)
|
904
|
+
os_code = case RbConfig::CONFIG['host_os']
|
905
|
+
when /darwin/ # macOS
|
906
|
+
<<~RUBY
|
907
|
+
# Generated at repo creation for macOS
|
908
|
+
def open_file(file_path)
|
909
|
+
system("open", file_path)
|
910
|
+
end
|
911
|
+
RUBY
|
912
|
+
when /linux/ # Linux
|
913
|
+
<<~RUBY
|
914
|
+
# Generated at repo creation for Linux
|
915
|
+
def open_file(file_path)
|
916
|
+
system("xdg-open", file_path)
|
917
|
+
end
|
918
|
+
RUBY
|
919
|
+
when /mswin|mingw|cygwin/ # Windows
|
920
|
+
<<~RUBY
|
921
|
+
# Generated at repo creation for Windows
|
922
|
+
def open_file(file_path)
|
923
|
+
system("start", file_path)
|
924
|
+
end
|
925
|
+
RUBY
|
926
|
+
else
|
927
|
+
<<~RUBY
|
928
|
+
# Generated at repo creation for unknown OS
|
929
|
+
def open_file(file_path)
|
930
|
+
puts " Unable to open file on this OS"
|
931
|
+
end
|
932
|
+
RUBY
|
933
|
+
end
|
934
|
+
|
935
|
+
write_file(root/:config/"os_helpers.rb", os_code)
|
936
|
+
end
|
937
|
+
|
938
|
+
end
|