scriptorium 0.6.1 → 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/assets/icons/social/reddit.png +0 -0
- data/assets/icons/social/x-logo.png +0 -0
- data/assets/imagenotfound.jpg +0 -0
- data/bin/sblog +84 -5
- data/bin/scriptorium +1 -0
- data/doc/anti-amnesia/20250727-054000-scriptorium-overview.md +0 -1
- data/doc/anti-amnesia/20250727-123000-anti-amnesia-conventions.md +0 -29
- data/doc/anti-amnesia/20250727-172600-cursor-rbenv-ruby-version-mystery.md +0 -19
- data/doc/anti-amnesia/20250727-172900-ai-cognitive-assessment-capabilities.md +1 -1
- data/doc/anti-amnesia/20250728-124243-aaa-syntax-clarification.md +1 -1
- data/doc/anti-amnesia/20250729-210000-reddit-autopost-integration-complete.md +1 -1
- data/doc/anti-amnesia/20250804-190500-cognitive-loop-bug.md +0 -10
- data/doc/anti-amnesia/20250804-190700-anti-amnesia-timestamping-fix.md +1 -4
- 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/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/{userdoc-toc.txt → myuserdoc/userdoc-toc.txt} +27 -27
- 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_integration.md +2 -2
- data/doc/user.lt3 +0 -3
- data/lib/scriptorium/api.rb +1811 -78
- data/lib/scriptorium/banner_svg.rb +55 -68
- data/lib/scriptorium/contract.rb +3 -2
- data/lib/scriptorium/exceptions.rb +133 -102
- data/lib/scriptorium/helpers.rb +282 -82
- data/lib/scriptorium/post.rb +81 -17
- data/lib/scriptorium/reddit.rb +1 -1
- data/lib/scriptorium/repo.rb +478 -164
- data/lib/scriptorium/standard_files.rb +30 -396
- 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_template.txt +17 -0
- data/{test/scriptorium-TEST-1754622690-146/views/sample → lib/scriptorium/support}/config/social.txt +1 -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/{test/scriptorium-TEST-1754622690-146/themes/standard/initial/post.lt3 → lib/scriptorium/support/templates/initial_post.lt3} +5 -5
- data/lib/scriptorium/support/templates/post.lt3 +104 -0
- data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/header.txt → lib/scriptorium/support/theme/header.lt3} +1 -1
- data/lib/scriptorium/theme.rb +83 -70
- data/lib/scriptorium/version.rb +2 -2
- data/lib/scriptorium/view.rb +194 -149
- data/lib/scriptorium.rb +24 -1
- data/lib/skeleton.rb +4 -1
- data/scriptorium.gemspec +2 -1
- data/test/WEB_INTEGRATION_README.md +196 -0
- data/test/all +40 -0
- data/test/banner_svg/unit.rb +267 -35
- data/test/config/deployment.txt +5 -0
- data/test/integration/integration_test.rb +7 -7
- data/test/integration/preview_flow_test.rb +94 -0
- data/test/livetext_plugin_test.rb +453 -182
- data/test/manual/banner-tests/test01.html +82 -18
- data/test/manual/banner-tests/test02.html +82 -18
- data/test/manual/banner-tests/test03.html +82 -18
- data/test/manual/banner-tests/test04.html +89 -25
- data/test/manual/banner-tests/test05.html +89 -25
- data/test/manual/banner-tests/test06.html +89 -25
- data/test/manual/banner-tests/test07.html +89 -25
- data/test/manual/banner-tests/test08.html +82 -18
- data/test/manual/banner-tests/test09.html +82 -18
- data/test/manual/banner-tests/test10.html +82 -18
- data/test/manual/banner-tests/test11.html +82 -18
- data/test/manual/banner-tests/test12.html +82 -18
- data/test/manual/banner-tests/test13.html +82 -18
- data/test/manual/banner-tests/test14.html +82 -18
- data/test/manual/banner-tests/test15.html +82 -18
- data/test/manual/banner-tests/test16.html +82 -18
- data/test/manual/banner-tests/test17.html +82 -18
- data/test/manual/banner-tests/test18.html +90 -26
- data/test/manual/banner-tests/test19.html +90 -26
- data/test/manual/banner-tests/test20.html +90 -26
- data/test/manual/banner-tests/test21.html +90 -26
- data/test/manual/banner-tests/test22.html +90 -26
- data/test/manual/banner-tests/test23.html +90 -26
- data/test/manual/banner-tests/test24.html +90 -26
- data/test/manual/banner-tests/test25.html +89 -25
- data/test/manual/banner_environment.rb +15 -2
- data/test/manual/codemirror_demo.html +773 -0
- data/test/manual/create_posts_for_web.rb +114 -0
- data/test/manual/preview_manual_test.rb +129 -0
- data/test/manual/test_banner_features.rb +14 -14
- data/test/manual/test_banner_integration.rb +115 -0
- data/test/manual/test_banner_radial.rb +87 -0
- data/test/manual/test_syntax_highlighting.rb +60 -40
- data/test/support/preview_utils.rb +88 -0
- data/test/test_gem_assets.rb +48 -0
- data/test/test_helpers.rb +10 -0
- data/test/tui_editor_integration_test.rb +15 -15
- data/test/tui_integration_test.rb +687 -441
- data/test/unit/api.rb +757 -37
- data/test/unit/asset_management.rb +195 -221
- data/test/unit/backup_test.rb +451 -0
- data/test/unit/contract_test.rb +1 -23
- data/test/unit/core.rb +415 -61
- data/test/unit/deploy_config_test.rb +248 -0
- data/test/unit/deploy_test.rb +312 -21
- data/test/unit/edit_post_test.rb +168 -0
- data/test/unit/gem_asset_management.rb +36 -42
- data/test/unit/livetext_basic.rb +23 -35
- data/test/unit/livetext_compatibility.rb +7 -14
- data/test/unit/parse_cmd_test.rb +260 -0
- data/test/unit/{symlink_test.rb → permalink_copy_test.rb} +47 -49
- data/test/unit/post.rb +91 -26
- 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 +8 -6
- data/test/unit/repo.rb +75 -54
- data/test/unit/social_test.rb +41 -44
- data/test/unit/syntax_highlighting.rb +70 -0
- data/test/unit/theme_management_test.rb +91 -0
- data/test/unit/view.rb +79 -12
- data/test/unit/widgets.rb +8 -8
- 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/ui/tui/bin/scriptorium +885 -415
- data/ui/web/app/app.rb +1398 -176
- data/ui/web/app/assets/livetext_mode.js +244 -0
- data/ui/web/app/error_helpers.rb +16 -16
- data/ui/web/app/views/advanced_config.erb +8 -2
- data/ui/web/app/views/asset_management.erb +56 -0
- data/ui/web/app/views/backup_management.erb +238 -0
- data/ui/web/app/views/config_widget.erb +232 -0
- data/ui/web/app/views/dashboard.erb +64 -72
- data/ui/web/app/views/deploy_config.erb +3 -0
- data/ui/web/app/views/edit_pages.erb +170 -2
- data/ui/web/app/views/edit_post.erb +130 -9
- 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/theme_management.erb +130 -0
- data/ui/web/app/views/view_dashboard.erb +666 -25
- data/ui/web/app/views/widgets.erb +249 -0
- data/ui/web/bin/scriptorium-web +35 -24
- data/ui/web/tmp/timing.log +17 -0
- data/ui/web/tmp/web_server.log +0 -5
- metadata +190 -116
- data/assets/back-icon.png +0 -0
- data/assets/icons/facebook.svg +0 -1
- data/assets/icons/github.svg +0 -1
- data/assets/icons/instagram.svg +0 -1
- data/assets/icons/reddit.svg +0 -1
- data/assets/icons/x.svg +0 -1
- data/assets/icons/youtube.svg +0 -1
- data/bin/scriptorium +0 -1511
- data/doc/anti-amnesia/20250727-060000-api-design-tui-planning.md +0 -34
- data/doc/anti-amnesia/20250727-061000-runeblog-tui-analysis.md +0 -50
- data/doc/anti-amnesia/20250727-154000-livetext-plugin-file-stats.md +0 -73
- data/doc/anti-amnesia/20250727-172600-unified-minitest-framework.md +0 -70
- data/doc/anti-amnesia/20250727-173000-widget-testing-achievement.md +0 -110
- data/doc/anti-amnesia/20250727-180000-post-id-num-refactoring.md +0 -73
- data/doc/anti-amnesia/20250728-124421-conversation-summary-concise.md +0 -124
- data/doc/anti-amnesia/20250729-190000-scriptorium-tui-testing-complete.md +0 -46
- data/doc/anti-amnesia/20250729-200000-scriptorium-tui-testing-edit-file-workflow.md +0 -97
- data/doc/anti-amnesia/20250729-211500-dependency-management-system.md +0 -211
- data/doc/anti-amnesia/20250729-213000-python-virtual-environment-setup.md +0 -141
- data/doc/anti-amnesia/20250729-214500-theme-management-commands.md +0 -211
- data/doc/anti-amnesia/20250729-215000-version-update-to-0.6.0.md +0 -134
- data/doc/anti-amnesia/20250729-220000-user-guide-complete.md +0 -41
- data/doc/anti-amnesia/20250804-213700-publishing-test-fix.md +0 -49
- data/doc/anti-amnesia/20250804-214400-additional-test-fixes.md +0 -46
- data/doc/anti-amnesia/20250804-220000-asset-function-logic-clarification.md +0 -41
- data/doc/anti-amnesia/20250806-202032-asset-function-logic-clarification.md +0 -41
- data/doc/anti-amnesia/20250813-082428-syntax-highlighting-and-navigation-improvements.md +0 -256
- data/lib/scriptorium/syntax_highlighter.rb +0 -234
- data/test/manual/deploy_symlink_demo.rb +0 -142
- data/test/manual/symlink_demo.rb +0 -117
- data/test/manual/test2.rb +0 -12
- data/test/manual/test_banner_from_file.rb +0 -150
- data/test/manual/test_banner_in_header.rb +0 -35
- data/test/manual/test_code_highlighting.rb +0 -68
- data/test/manual/test_complex_header.rb +0 -74
- data/test/manual/test_empty_header.rb +0 -32
- data/test/manual/test_radial_custom.rb +0 -58
- data/test/manual/test_radial_large_radius.rb +0 -52
- data/test/manual/test_svg_debug.rb +0 -47
- data/test/pages-demo/config/currentview.txt +0 -1
- data/test/pages-demo/views/demo/config/common.js +0 -57
- data/test/pages-demo/views/demo/config/footer.txt +0 -1
- data/test/pages-demo/views/demo/config/global-head.txt +0 -8
- data/test/pages-demo/views/demo/config/header.txt +0 -1
- data/test/pages-demo/views/demo/config/layout.txt +0 -1
- data/test/pages-demo/views/demo/config/left.txt +0 -1
- data/test/pages-demo/views/demo/config/main.txt +0 -1
- data/test/pages-demo/views/demo/config/right.txt +0 -1
- data/test/pages-demo/views/demo/config.txt +0 -3
- data/test/pages-demo/views/demo/output/panes/footer.html +0 -1
- data/test/pages-demo/views/demo/output/panes/header.html +0 -1
- data/test/pages-demo/views/demo/output/panes/left.html +0 -1
- data/test/pages-demo/views/demo/output/panes/main.html +0 -1
- data/test/pages-demo/views/demo/output/panes/right.html +0 -1
- data/test/scriptorium-TEST-1754622690-146/config/bootstrap_css.txt +0 -5
- data/test/scriptorium-TEST-1754622690-146/config/bootstrap_js.txt +0 -4
- data/test/scriptorium-TEST-1754622690-146/config/common.js +0 -57
- data/test/scriptorium-TEST-1754622690-146/config/currentview.txt +0 -1
- data/test/scriptorium-TEST-1754622690-146/config/global-head.txt +0 -9
- data/test/scriptorium-TEST-1754622690-146/config/last_post_num.txt +0 -1
- data/test/scriptorium-TEST-1754622690-146/config/os_helpers.rb +0 -4
- data/test/scriptorium-TEST-1754622690-146/config/widgets.txt +0 -3
- data/test/scriptorium-TEST-1754622690-146/posts/0001/meta.txt +0 -8
- data/test/scriptorium-TEST-1754622690-146/posts/0001/source.lt3 +0 -6
- data/test/scriptorium-TEST-1754622690-146/themes/standard/README.txt +0 -1
- data/test/scriptorium-TEST-1754622690-146/themes/standard/config.txt +0 -1
- data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/gen/text.css +0 -1
- data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/index.lt3 +0 -1
- data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/index_entry.lt3 +0 -14
- data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/post.lt3 +0 -13
- data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/widget.lt3 +0 -1
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/bootstrap_css.txt +0 -5
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/bootstrap_js.txt +0 -4
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/common.js +0 -57
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/deploy.txt +0 -5
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/footer.txt +0 -2
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/global-head.txt +0 -9
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/header.txt +0 -4
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/layout.txt +0 -5
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/left.txt +0 -3
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/main.txt +0 -5
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/right.txt +0 -3
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/status.txt +0 -7
- data/test/scriptorium-TEST-1754622690-146/views/sample/config.txt +0 -3
- data/test/scriptorium-TEST-1754622690-146/views/sample/layout/footer.html +0 -3
- data/test/scriptorium-TEST-1754622690-146/views/sample/layout/header.html +0 -3
- data/test/scriptorium-TEST-1754622690-146/views/sample/layout/left.html +0 -3
- data/test/scriptorium-TEST-1754622690-146/views/sample/layout/main.html +0 -3
- data/test/scriptorium-TEST-1754622690-146/views/sample/layout/right.html +0 -3
- data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/footer.html +0 -1
- data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/header.html +0 -1
- data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/left.html +0 -1
- data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/main.html +0 -1
- data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/right.html +0 -1
- data/ui/web/tmp/web_server.pid +0 -1
- /data/{test/pages-demo/views/demo/config/bootstrap_css.txt → lib/scriptorium/support/bootstrap/css.txt} +0 -0
- /data/{test/pages-demo/views/demo/config/bootstrap_js.txt → lib/scriptorium/support/bootstrap/js.txt} +0 -0
- /data/{test/scriptorium-TEST-1754622690-146/views/sample → lib/scriptorium/support}/config/reddit.txt +0 -0
- /data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout → lib/scriptorium/support/templates}/layout.txt +0 -0
- /data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/footer.txt → lib/scriptorium/support/theme/footer.lt3} +0 -0
- /data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/left.txt → lib/scriptorium/support/theme/left.lt3} +0 -0
- /data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/main.txt → lib/scriptorium/support/theme/main.lt3} +0 -0
- /data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/right.txt → lib/scriptorium/support/theme/right.lt3} +0 -0
- /data/test/manual/banner-tests/{config.txt → svg.txt} +0 -0
- /data/test/manual/{test6.rb → test_advanced_widgets.rb} +0 -0
- /data/test/manual/{test1.rb → test_basic_posts.rb} +0 -0
- /data/test/manual/{test4.rb → test_layout_widgets.rb} +0 -0
- /data/test/manual/{test5.rb → test_pagination.rb} +0 -0
- /data/test/manual/{test3.rb → test_random_posts.rb} +0 -0
data/lib/scriptorium/repo.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
|
2
|
+
|
1
3
|
class Scriptorium::Repo
|
2
4
|
include Scriptorium::Exceptions
|
3
5
|
extend Scriptorium::Exceptions
|
@@ -22,7 +24,8 @@ class Scriptorium::Repo
|
|
22
24
|
end
|
23
25
|
|
24
26
|
def self.create(path = nil, testmode: false)
|
25
|
-
|
27
|
+
msg = "path must be nil or String, got #{path.class}"
|
28
|
+
assume(msg) { path.nil? || path.is_a?(String) }
|
26
29
|
# Handle backward compatibility: boolean true means testing mode
|
27
30
|
if testmode == true
|
28
31
|
Scriptorium::Repo.testing = path
|
@@ -32,12 +35,9 @@ class Scriptorium::Repo
|
|
32
35
|
home = ENV['HOME']
|
33
36
|
@predef = Scriptorium::StandardFiles.new
|
34
37
|
@root = path || "#{home}/.scriptorium"
|
35
|
-
parent = path ? "." : home
|
36
|
-
file = path || ".scriptorium"
|
37
|
-
@root = parent/file
|
38
38
|
raise self.RepoDirAlreadyExists(@root) if Dir.exist?(@root)
|
39
|
-
make_tree(
|
40
|
-
|
39
|
+
make_tree(@root, <<~EOS)
|
40
|
+
.
|
41
41
|
├── config/ # Global config files
|
42
42
|
├── views/ # Views
|
43
43
|
├── drafts/ # Draft posts (global)
|
@@ -50,10 +50,18 @@ class Scriptorium::Repo
|
|
50
50
|
postnum_file = "#@root/config/last_post_num.txt"
|
51
51
|
write_file(postnum_file, "0")
|
52
52
|
write_file(@root/:config/"global-head.txt", @predef.html_head_content)
|
53
|
-
|
54
|
-
|
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
55
|
write_file(@root/:config/"common.js", @predef.common_js)
|
56
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
|
+
|
57
65
|
Scriptorium::Theme.create_standard(@root) # Theme: templates, etc.
|
58
66
|
|
59
67
|
# Copy application-wide gem assets to library
|
@@ -63,20 +71,22 @@ class Scriptorium::Repo
|
|
63
71
|
generate_os_helpers(@root)
|
64
72
|
|
65
73
|
@repo = self.open(@root)
|
66
|
-
Scriptorium::View.create_sample_view(repo)
|
74
|
+
Scriptorium::View.create_sample_view(@repo)
|
67
75
|
verify { @repo.is_a?(Scriptorium::Repo) }
|
68
|
-
return repo
|
76
|
+
return @repo
|
69
77
|
end
|
70
78
|
|
71
79
|
def self.open(root)
|
72
|
-
|
80
|
+
msg = "root must be a non-empty String, got #{root.class} (#{root.inspect})"
|
81
|
+
assume(msg) { root.is_a?(String) && !root.empty? }
|
73
82
|
repo = Scriptorium::Repo.new(root)
|
74
83
|
verify { repo.is_a?(Scriptorium::Repo) }
|
75
84
|
repo
|
76
85
|
end
|
77
86
|
|
78
87
|
def self.destroy
|
79
|
-
|
88
|
+
msg = "Repo.testing must be true in test mode"
|
89
|
+
assume(msg) { Scriptorium::Repo.testing }
|
80
90
|
raise self.TestModeOnly unless Scriptorium::Repo.testing
|
81
91
|
system!("rm -rf #@root", "destroying repository")
|
82
92
|
verify { !Dir.exist?(@root) }
|
@@ -94,7 +104,8 @@ class Scriptorium::Repo
|
|
94
104
|
end
|
95
105
|
|
96
106
|
def initialize(root) # repo
|
97
|
-
|
107
|
+
msg = "root must be a non-empty String, got #{root.class} (#{root.inspect})"
|
108
|
+
assume(msg) { root.is_a?(String) && !root.empty? }
|
98
109
|
@root = root
|
99
110
|
@predef = Scriptorium::StandardFiles.new
|
100
111
|
# Scriptorium::Repo.class_eval { @root, @repo = root, self }
|
@@ -117,7 +128,7 @@ class Scriptorium::Repo
|
|
117
128
|
view_name = read_file(cview_file).chomp
|
118
129
|
begin
|
119
130
|
@current_view = lookup_view(view_name)
|
120
|
-
rescue
|
131
|
+
rescue
|
121
132
|
# If the saved view doesn't exist, just leave current_view as nil
|
122
133
|
# It will be set when a view is created or selected
|
123
134
|
end
|
@@ -138,9 +149,14 @@ class Scriptorium::Repo
|
|
138
149
|
end
|
139
150
|
|
140
151
|
private def validate_view_target(target)
|
141
|
-
raise
|
152
|
+
raise ViewTargetNil if target.nil?
|
153
|
+
|
154
|
+
raise ViewTargetEmpty if target.to_s.strip.empty?
|
142
155
|
|
143
|
-
|
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
|
144
160
|
end
|
145
161
|
|
146
162
|
def view(change = nil) # get/set current view
|
@@ -160,19 +176,21 @@ class Scriptorium::Repo
|
|
160
176
|
end
|
161
177
|
|
162
178
|
def create_view(name, title, subtitle = "", theme: "standard")
|
163
|
-
|
164
|
-
assume {
|
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) }
|
165
183
|
validate_view_name(name)
|
166
184
|
validate_view_title(title)
|
167
185
|
|
168
186
|
# Validate name format (only allow alphanumeric, hyphen, underscore)
|
169
187
|
unless name.match?(/^[a-zA-Z0-9_-]+$/)
|
170
|
-
raise
|
188
|
+
raise ViewNameInvalid(name)
|
171
189
|
end
|
172
190
|
|
173
191
|
raise ViewDirAlreadyExists(name) if view_exist?(name)
|
174
|
-
make_tree(@root/:views, <<~EOS)
|
175
|
-
|
192
|
+
make_tree(@root/:views/name, <<~EOS)
|
193
|
+
.
|
176
194
|
├── config/ # View-specific config files
|
177
195
|
│ ├── layout.txt # Overall layout for front page
|
178
196
|
│ ├── footer.txt # Content for footer.html
|
@@ -193,6 +211,9 @@ class Scriptorium::Repo
|
|
193
211
|
│ │ ├── main.html # Generated from main.txt
|
194
212
|
│ │ └── right.html # Generated from right.txt
|
195
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)
|
196
217
|
├── widgets/ # Widgets for view
|
197
218
|
└── staging/ # Staging area prior to deployment
|
198
219
|
EOS
|
@@ -200,19 +221,62 @@ class Scriptorium::Repo
|
|
200
221
|
###
|
201
222
|
|
202
223
|
dir = "#@root/views/#{name}"
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
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
|
216
280
|
@views -= [view]
|
217
281
|
@views << view
|
218
282
|
@current_view = view
|
@@ -221,13 +285,18 @@ class Scriptorium::Repo
|
|
221
285
|
theme_config = @root/:themes/theme/:layout/:config
|
222
286
|
containers = %w[header.txt footer.txt left.txt right.txt main.txt]
|
223
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
|
+
|
224
292
|
view.apply_theme(theme)
|
225
293
|
verify { view.is_a?(Scriptorium::View) }
|
226
294
|
return view
|
227
295
|
end
|
228
296
|
|
229
297
|
def open_view(name)
|
230
|
-
|
298
|
+
config_file = view_dir(name)/"config.txt"
|
299
|
+
vhash = getvars(config_file)
|
231
300
|
title, subtitle, theme = vhash.values_at(:title, :subtitle, :theme)
|
232
301
|
view = Scriptorium::View.new(name, title, subtitle, theme)
|
233
302
|
@views -= [view]
|
@@ -245,18 +314,16 @@ class Scriptorium::Repo
|
|
245
314
|
|
246
315
|
# Whoa - what if different views have different themes??? FIXME
|
247
316
|
# Maybe solution is as simple as: Initial post is not theme-dependent
|
248
|
-
theme = @current_view.theme
|
249
317
|
views ||= @current_view.name # initial_post wants a String!
|
250
318
|
views, tags = Array(views), Array(tags)
|
251
|
-
id = incr_post_num
|
252
319
|
|
253
320
|
# Create content file (no ID, no created date)
|
254
|
-
content = @predef.
|
255
|
-
|
321
|
+
content = @predef.initial_post(:filled, title: title, blurb: blurb,
|
322
|
+
views: views, tags: tags, body: body)
|
256
323
|
write_file(content_name, content)
|
257
324
|
|
258
|
-
# Create metadata file (
|
259
|
-
metadata = @predef.initial_post_metadata(
|
325
|
+
# Create metadata file (no ID for drafts)
|
326
|
+
metadata = @predef.initial_post_metadata(title: title, blurb: blurb,
|
260
327
|
views: views, tags: tags)
|
261
328
|
write_file(metadata_name, metadata)
|
262
329
|
|
@@ -275,7 +342,7 @@ class Scriptorium::Repo
|
|
275
342
|
end
|
276
343
|
|
277
344
|
def finish_draft(name)
|
278
|
-
id =
|
345
|
+
id = incr_post_num
|
279
346
|
id4 = d4(id)
|
280
347
|
posts = @root/:posts
|
281
348
|
make_dir(posts/id4)
|
@@ -297,28 +364,6 @@ class Scriptorium::Repo
|
|
297
364
|
end
|
298
365
|
|
299
366
|
|
300
|
-
private def copy_post_assets_to_view(num, view)
|
301
|
-
id4 = d4(num)
|
302
|
-
post_assets_dir = @root/:posts/id4/"assets"
|
303
|
-
view_assets_dir = view.dir/:output/"assets"
|
304
|
-
|
305
|
-
# Only copy if post has assets
|
306
|
-
return unless Dir.exist?(post_assets_dir)
|
307
|
-
|
308
|
-
# Create view assets directory if it doesn't exist
|
309
|
-
make_dir(view_assets_dir)
|
310
|
-
|
311
|
-
# Copy all files from post assets to view assets
|
312
|
-
Dir.glob(post_assets_dir/"*").each do |file|
|
313
|
-
next unless File.file?(file)
|
314
|
-
filename = File.basename(file)
|
315
|
-
target_file = view_assets_dir/filename
|
316
|
-
|
317
|
-
# Copy file, overwriting if it exists (post assets take precedence)
|
318
|
-
FileUtils.cp(file, target_file)
|
319
|
-
end
|
320
|
-
end
|
321
|
-
|
322
367
|
private def write_post_metadata(data, view)
|
323
368
|
num, title = data.values_at(:"post.id", :"post.title")
|
324
369
|
metadata_file = @root/:posts/d4(num)/"meta.txt"
|
@@ -356,83 +401,269 @@ class Scriptorium::Repo
|
|
356
401
|
# view/.../output/permalink/0123-this-is-me.html (for direct access)
|
357
402
|
permalink_path = view.dir/:output/:permalink/slug
|
358
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, "")
|
359
406
|
# Write the permalink version with "Visit Blog" link and "Copy link" button
|
360
|
-
permalink_content =
|
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>"
|
361
408
|
write_file(permalink_path, permalink_content)
|
362
409
|
|
363
|
-
# Create
|
410
|
+
# Create copy for clean URL (without numeric prefix)
|
364
411
|
clean_slug = clean_slugify(title) + ".html"
|
365
|
-
|
412
|
+
clean_copy_path = view.dir/:output/:permalink/clean_slug
|
366
413
|
|
367
|
-
# Remove existing
|
368
|
-
File.delete(
|
369
|
-
|
370
|
-
# Create symlink (relative path from clean_symlink_path to slug)
|
371
|
-
begin
|
372
|
-
File.symlink(slug, clean_symlink_path)
|
373
|
-
rescue Errno::EEXIST => e
|
374
|
-
# If symlink already exists (not a symlink), remove it and try again
|
375
|
-
File.delete(clean_symlink_path) if File.exist?(clean_symlink_path)
|
376
|
-
File.symlink(slug, clean_symlink_path)
|
377
|
-
end
|
414
|
+
# Remove existing file if it exists
|
415
|
+
File.delete(clean_copy_path) if File.exist?(clean_copy_path)
|
378
416
|
|
379
|
-
# Copy
|
380
|
-
|
417
|
+
# Copy the permalink file to create clean URL
|
418
|
+
FileUtils.cp(permalink_path, clean_copy_path)
|
381
419
|
end
|
382
420
|
|
383
421
|
def create_post(title: nil, views: nil, tags: nil, body: nil, blurb: nil)
|
384
|
-
|
385
|
-
assume {
|
386
|
-
|
387
|
-
assume {
|
388
|
-
|
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) }
|
389
432
|
name = create_draft(title: title, views: views, tags: tags, body: body, blurb: blurb)
|
390
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
|
+
|
391
449
|
generate_post(num)
|
392
450
|
post = self.post(num) # Return the Post object
|
393
451
|
verify { post.is_a?(Scriptorium::Post) }
|
394
452
|
post
|
395
453
|
end
|
396
454
|
|
397
|
-
def publish_post(num)
|
455
|
+
def publish_post(num, view = nil)
|
398
456
|
validate_post_id(num)
|
457
|
+
|
458
|
+
# Check if post exists in normal location first
|
399
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
|
400
468
|
|
401
|
-
# Read current metadata
|
402
|
-
metadata =
|
403
|
-
metadata = getvars(metadata_file) if File.exist?(metadata_file)
|
469
|
+
# Read current metadata
|
470
|
+
metadata = getvars(metadata_file)
|
404
471
|
|
405
|
-
|
406
|
-
|
407
|
-
|
472
|
+
if view.nil?
|
473
|
+
# Use current view if no view specified
|
474
|
+
view = @current_view&.name || "sample"
|
408
475
|
end
|
409
476
|
|
410
|
-
#
|
411
|
-
|
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
|
412
483
|
|
413
|
-
#
|
414
|
-
|
415
|
-
write_file(metadata_file, lines.join("\n"))
|
484
|
+
# View-specific publish - only update the specified view's state
|
485
|
+
remove_post_from_state_file(unpublished_file, num)
|
416
486
|
|
417
|
-
#
|
418
|
-
|
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
|
419
495
|
|
420
496
|
self.post(num)
|
421
497
|
end
|
422
|
-
|
423
|
-
def
|
498
|
+
|
499
|
+
def mark_post_deployed(num, view = nil)
|
424
500
|
validate_post_id(num)
|
501
|
+
|
502
|
+
# Check if post exists in normal location first
|
425
503
|
metadata_file = @root/:posts/d4(num)/"meta.txt"
|
426
|
-
|
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
|
427
512
|
|
428
|
-
|
429
|
-
|
430
|
-
|
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
|
431
545
|
end
|
432
|
-
|
433
|
-
def
|
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)
|
434
589
|
all_posts = all_posts(view)
|
435
|
-
|
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"
|
436
667
|
end
|
437
668
|
|
438
669
|
def generate_post(num)
|
@@ -443,43 +674,54 @@ class Scriptorium::Repo
|
|
443
674
|
|
444
675
|
# Read content file
|
445
676
|
vars = { View: @current_view.name, :"post.id" => num }
|
446
|
-
#
|
447
|
-
|
448
|
-
# vars, _body = live.vars.vars, live.body
|
677
|
+
# Mark transform as post-context
|
678
|
+
vars[:"post.context"] = "post"
|
449
679
|
|
450
|
-
|
451
|
-
body, vars = live.process(file: content_file)
|
452
|
-
|
453
|
-
# Create or update metadata from post content
|
680
|
+
# Merge metadata into vars if metadata file exists
|
454
681
|
if File.exist?(metadata_file)
|
455
|
-
|
456
|
-
|
457
|
-
metadata_vars = create_metadata_from_content(num, vars)
|
458
|
-
# Merge existing metadata over defaults
|
459
|
-
existing_metadata.each do |key, value|
|
460
|
-
metadata_vars[key] = value
|
461
|
-
end
|
462
|
-
else
|
463
|
-
# Create new metadata
|
464
|
-
metadata_vars = create_metadata_from_content(num, vars)
|
682
|
+
metadata = getvars(metadata_file)
|
683
|
+
vars.merge!(metadata)
|
465
684
|
end
|
466
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
|
+
|
467
711
|
# Write metadata file
|
468
712
|
lines = metadata_vars.map { |k, v| sprintf("%-18s %s", k, v) }
|
469
713
|
write_file(metadata_file, lines.join("\n"))
|
470
714
|
|
471
|
-
|
472
|
-
metadata_vars.each { |key, value| vars[key] = value unless vars.key?(key) }
|
473
|
-
|
474
|
-
views = vars[:"post.views"].strip.split(/\s+/)
|
715
|
+
views = (vars[:"post.views"] || "").strip.split(/\s+/)
|
475
716
|
vars[:"post.views"] = views.join(" ") # Ensure post.views is set in vars
|
476
717
|
views.each do |view|
|
477
718
|
view = lookup_view(view)
|
478
|
-
theme = view.theme
|
479
719
|
vars[:"post.id"] = num.to_s # Always use the post number as ID
|
480
720
|
vars[:"post.body"] = body
|
481
|
-
|
482
|
-
|
721
|
+
vars[:"post.date"] = self.post(num).date # Set post.date for templates
|
722
|
+
|
723
|
+
|
724
|
+
template = support_data('templates/post.lt3')
|
483
725
|
# Add Reddit button if enabled
|
484
726
|
vars[:"reddit_button"] = view.generate_reddit_button(vars)
|
485
727
|
final = substitute(vars, template)
|
@@ -487,31 +729,7 @@ class Scriptorium::Repo
|
|
487
729
|
end
|
488
730
|
end
|
489
731
|
|
490
|
-
private def create_metadata_from_content(num, vars)
|
491
|
-
metadata = {}
|
492
|
-
|
493
|
-
# Set required fields
|
494
|
-
metadata[:"post.id"] = d4(num)
|
495
|
-
metadata[:"post.created"] = ymdhms
|
496
|
-
metadata[:"post.published"] = "no" # Default to unpublished
|
497
|
-
metadata[:"post.deployed"] = "no"
|
498
|
-
|
499
|
-
# Copy fields from content vars
|
500
|
-
metadata[:"post.title"] = vars[:"post.title"] || "ADD TITLE HERE"
|
501
|
-
metadata[:"post.blurb"] = vars[:"post.blurb"] || "ADD BLURB HERE"
|
502
|
-
metadata[:"post.views"] = vars[:"post.views"] || "sample"
|
503
|
-
metadata[:"post.tags"] = vars[:"post.tags"] || ""
|
504
|
-
|
505
|
-
metadata
|
506
|
-
end
|
507
732
|
|
508
|
-
private def set_pubdate(vars) # Not Post#set_pubdate
|
509
|
-
t = Time.now
|
510
|
-
vars[:"post.pubdate"] = t.strftime("%Y-%m-%d %H:%M:%S")
|
511
|
-
vars[:"post.pubdate.month"] = t.strftime("%B")
|
512
|
-
vars[:"post.pubdate.day"] = t.strftime("%d")
|
513
|
-
vars[:"post.pubdate.year"] = t.strftime("%Y")
|
514
|
-
end
|
515
733
|
|
516
734
|
def all_posts(view = nil)
|
517
735
|
posts = []
|
@@ -525,6 +743,31 @@ class Scriptorium::Repo
|
|
525
743
|
view = lookup_view(view)
|
526
744
|
posts.select {|x| x.views.include?(view.name) }
|
527
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
|
528
771
|
|
529
772
|
def generate_post_index(view)
|
530
773
|
view = lookup_view(view)
|
@@ -543,16 +786,81 @@ class Scriptorium::Repo
|
|
543
786
|
return Scriptorium::Post.new(self, id) if File.exist?(deleted_meta)
|
544
787
|
|
545
788
|
# Post not found in either location
|
546
|
-
|
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
|
547
853
|
end
|
854
|
+
|
855
|
+
|
548
856
|
|
549
857
|
private def validate_post_id(id)
|
550
|
-
raise
|
858
|
+
raise PostIdNil if id.nil?
|
551
859
|
|
552
|
-
raise
|
860
|
+
raise PostIdEmpty if id.to_s.strip.empty?
|
553
861
|
|
554
862
|
unless id.to_s.match?(/^\d+$/)
|
555
|
-
raise
|
863
|
+
raise PostIdInvalid(id)
|
556
864
|
end
|
557
865
|
end
|
558
866
|
|
@@ -574,17 +882,23 @@ class Scriptorium::Repo
|
|
574
882
|
reddit.configured?
|
575
883
|
end
|
576
884
|
|
885
|
+
|
886
|
+
|
577
887
|
private def validate_view_name(name)
|
578
|
-
raise
|
888
|
+
raise ViewNameNil if name.nil?
|
579
889
|
|
580
|
-
raise
|
890
|
+
raise ViewNameEmpty if name.to_s.strip.empty?
|
581
891
|
end
|
582
892
|
|
893
|
+
|
894
|
+
|
583
895
|
private def validate_view_title(title)
|
584
|
-
raise
|
896
|
+
raise ViewTitleNil if title.nil?
|
585
897
|
|
586
|
-
raise
|
898
|
+
raise ViewTitleEmpty if title.to_s.strip.empty?
|
587
899
|
end
|
900
|
+
|
901
|
+
|
588
902
|
|
589
903
|
def self.generate_os_helpers(root)
|
590
904
|
os_code = case RbConfig::CONFIG['host_os']
|