scriptorium 0.0.3 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.lt3 +324 -0
- data/README.md +3155 -1
- data/assets/.DS_Store +0 -0
- data/assets/README.md +44 -0
- data/assets/back-icon.png +0 -0
- data/assets/icons/facebook.svg +1 -0
- data/assets/icons/github.svg +1 -0
- data/assets/icons/instagram.svg +1 -0
- data/assets/icons/reddit.svg +1 -0
- data/assets/icons/ui/.DS_Store +0 -0
- data/assets/icons/ui/back.png +0 -0
- data/assets/icons/ui/copy.png +0 -0
- data/assets/icons/ui/down.png +0 -0
- data/assets/icons/ui/end.png +0 -0
- data/assets/icons/ui/exit.png +0 -0
- data/assets/icons/ui/foo +10 -0
- data/assets/icons/ui/home.png +0 -0
- data/assets/icons/ui/left.png +0 -0
- data/assets/icons/ui/next.png +0 -0
- data/assets/icons/ui/right.png +0 -0
- data/assets/icons/ui/start.png +0 -0
- data/assets/icons/ui/up.png +0 -0
- data/assets/icons/x.svg +1 -0
- data/assets/icons/youtube.svg +1 -0
- data/assets/samples/placeholder.svg +9 -0
- data/assets/themes/standard/favicon.svg +6 -0
- data/bin/scriptorium +1511 -0
- data/doc/README.txt +6 -0
- data/doc/anti-amnesia/20250727-054000-scriptorium-overview.md +95 -0
- data/doc/anti-amnesia/20250727-060000-api-design-tui-planning.md +34 -0
- data/doc/anti-amnesia/20250727-061000-runeblog-tui-analysis.md +50 -0
- data/doc/anti-amnesia/20250727-123000-anti-amnesia-conventions.md +31 -0
- data/doc/anti-amnesia/20250727-154000-livetext-plugin-file-stats.md +73 -0
- data/doc/anti-amnesia/20250727-172600-cursor-rbenv-ruby-version-mystery.md +64 -0
- data/doc/anti-amnesia/20250727-172600-unified-minitest-framework.md +70 -0
- data/doc/anti-amnesia/20250727-172900-ai-cognitive-assessment-capabilities.md +40 -0
- data/doc/anti-amnesia/20250727-173000-widget-testing-achievement.md +110 -0
- data/doc/anti-amnesia/20250727-180000-post-id-num-refactoring.md +73 -0
- data/doc/anti-amnesia/20250728-124243-aaa-syntax-clarification.md +46 -0
- data/doc/anti-amnesia/20250728-124421-conversation-summary-concise.md +124 -0
- data/doc/anti-amnesia/20250729-190000-scriptorium-tui-testing-complete.md +46 -0
- data/doc/anti-amnesia/20250729-200000-scriptorium-tui-testing-edit-file-workflow.md +97 -0
- data/doc/anti-amnesia/20250729-210000-reddit-autopost-integration-complete.md +158 -0
- data/doc/anti-amnesia/20250729-211500-dependency-management-system.md +211 -0
- data/doc/anti-amnesia/20250729-213000-python-virtual-environment-setup.md +141 -0
- data/doc/anti-amnesia/20250729-214500-theme-management-commands.md +211 -0
- data/doc/anti-amnesia/20250729-215000-version-update-to-0.6.0.md +134 -0
- data/doc/anti-amnesia/20250729-220000-user-guide-complete.md +41 -0
- data/doc/anti-amnesia/20250804-190500-cognitive-loop-bug.md +45 -0
- data/doc/anti-amnesia/20250804-190700-anti-amnesia-timestamping-fix.md +30 -0
- data/doc/anti-amnesia/20250804-213700-publishing-test-fix.md +49 -0
- data/doc/anti-amnesia/20250804-214400-additional-test-fixes.md +46 -0
- data/doc/anti-amnesia/20250804-220000-asset-function-logic-clarification.md +41 -0
- data/doc/anti-amnesia/20250806-202032-asset-function-logic-clarification.md +41 -0
- data/doc/anti-amnesia/20250807-213025.md +116 -0
- data/doc/anti-amnesia/20250813-082428-syntax-highlighting-and-navigation-improvements.md +256 -0
- data/doc/banner_svg_config.md +114 -0
- data/doc/contrib.lt3 +8 -0
- data/doc/dependencies.md +281 -0
- data/doc/hacker.lt3 +5 -0
- data/doc/reddit_credentials_template.json +8 -0
- data/doc/reddit_integration.md +207 -0
- data/doc/user.lt3 +38 -0
- data/doc/user_guide_section_1.md +137 -0
- data/doc/user_guide_section_10.md +515 -0
- data/doc/user_guide_section_11.md +708 -0
- data/doc/user_guide_section_2.md +233 -0
- data/doc/user_guide_section_3.md +5 -0
- data/doc/user_guide_section_4.md +221 -0
- data/doc/user_guide_section_5.md +243 -0
- data/doc/user_guide_section_6.md +147 -0
- data/doc/user_guide_section_7.md +311 -0
- data/doc/user_guide_section_8.md +224 -0
- data/doc/user_guide_section_9.md +375 -0
- data/doc/userdoc-toc.txt +88 -0
- data/lib/rouge/lexers/livetext.rb +74 -0
- data/lib/scriptorium/api.rb +640 -0
- data/lib/scriptorium/banner_svg.rb +742 -0
- data/lib/scriptorium/contract.rb +33 -0
- data/lib/scriptorium/exceptions.rb +170 -1
- data/lib/scriptorium/helpers.rb +475 -0
- data/lib/scriptorium/post.rb +195 -0
- data/lib/scriptorium/reddit.rb +83 -0
- data/lib/scriptorium/repo.rb +624 -0
- data/lib/scriptorium/standard_files.rb +515 -0
- data/lib/scriptorium/syntax_highlighter.rb +234 -0
- data/lib/scriptorium/theme.rb +179 -0
- data/lib/scriptorium/version.rb +2 -2
- data/lib/scriptorium/view.rb +976 -0
- data/lib/scriptorium/widgets/featured_posts.rb +149 -0
- data/lib/scriptorium/widgets/links.rb +112 -0
- data/lib/scriptorium/widgets/pages.rb +133 -0
- data/lib/scriptorium/widgets/widget.rb +133 -0
- data/lib/scriptorium.rb +21 -40
- data/lib/skeleton.rb +8 -2
- data/scriptorium.gemspec +15 -4
- data/test/README.md +69 -0
- data/test/all +43 -0
- data/test/api_demo.rb +99 -0
- data/test/assets/imagenotfound.jpg +0 -0
- data/test/assets/images/.DS_Store +0 -0
- data/test/assets/images/README.md +27 -0
- data/test/assets/images/odd_aspect.png +0 -0
- data/test/assets/images/perfect.png +0 -0
- data/test/assets/images/small.png +0 -0
- data/test/assets/images/tall.png +0 -0
- data/test/assets/images/very_tall.png +0 -0
- data/test/assets/images/very_wide.png +0 -0
- data/test/assets/images/wide.png +0 -0
- data/test/assets/testbanner.jpg +0 -0
- data/test/banner_svg/simple_helpers.rb +13 -0
- data/test/banner_svg/unit.rb +768 -0
- data/test/ed_test.rb +204 -0
- data/test/integration/cursor_banner_combinations.rb +193 -0
- data/test/integration/cursor_banner_features.rb +374 -0
- data/test/integration/integration_test.rb +326 -0
- data/test/livetext_plugin_test.rb +229 -0
- data/test/manual/asset_mgmt.rb +67 -0
- data/test/manual/banner-tests/config.txt +3 -0
- data/test/manual/banner-tests/index.html +45 -0
- data/test/manual/banner-tests/test01.html +58 -0
- data/test/manual/banner-tests/test02.html +58 -0
- data/test/manual/banner-tests/test03.html +58 -0
- data/test/manual/banner-tests/test04.html +65 -0
- data/test/manual/banner-tests/test05.html +65 -0
- data/test/manual/banner-tests/test06.html +65 -0
- data/test/manual/banner-tests/test07.html +65 -0
- data/test/manual/banner-tests/test08.html +59 -0
- data/test/manual/banner-tests/test09.html +59 -0
- data/test/manual/banner-tests/test10.html +59 -0
- data/test/manual/banner-tests/test11.html +59 -0
- data/test/manual/banner-tests/test12.html +59 -0
- data/test/manual/banner-tests/test13.html +59 -0
- data/test/manual/banner-tests/test14.html +59 -0
- data/test/manual/banner-tests/test15.html +58 -0
- data/test/manual/banner-tests/test16.html +58 -0
- data/test/manual/banner-tests/test17.html +58 -0
- data/test/manual/banner-tests/test18.html +68 -0
- data/test/manual/banner-tests/test19.html +68 -0
- data/test/manual/banner-tests/test20.html +68 -0
- data/test/manual/banner-tests/test21.html +68 -0
- data/test/manual/banner-tests/test22.html +68 -0
- data/test/manual/banner-tests/test23.html +68 -0
- data/test/manual/banner-tests/test24.html +68 -0
- data/test/manual/banner-tests/test25.html +67 -0
- data/test/manual/banner_environment.rb +192 -0
- data/test/manual/deploy_symlink_demo.rb +142 -0
- data/test/manual/environment.rb +67 -0
- data/test/manual/make_banner.rb +153 -0
- data/test/manual/sample_banner_config.txt +12 -0
- data/test/manual/symlink_demo.rb +117 -0
- data/test/manual/test1.rb +47 -0
- data/test/manual/test2.rb +12 -0
- data/test/manual/test3.rb +38 -0
- data/test/manual/test4.rb +40 -0
- data/test/manual/test5.rb +24 -0
- data/test/manual/test6.rb +73 -0
- data/test/manual/test_banner_combinations.rb +120 -0
- data/test/manual/test_banner_features.rb +306 -0
- data/test/manual/test_banner_from_file.rb +150 -0
- data/test/manual/test_banner_in_header.rb +35 -0
- data/test/manual/test_code_highlighting.rb +68 -0
- data/test/manual/test_complex_header.rb +74 -0
- data/test/manual/test_empty_header.rb +32 -0
- data/test/manual/test_radial_custom.rb +58 -0
- data/test/manual/test_radial_large_radius.rb +52 -0
- data/test/manual/test_svg_debug.rb +47 -0
- data/test/manual/test_syntax_highlighting.rb +147 -0
- data/test/pages-demo/config/currentview.txt +1 -0
- data/test/pages-demo/views/demo/config/bootstrap_css.txt +5 -0
- data/test/pages-demo/views/demo/config/bootstrap_js.txt +4 -0
- data/test/pages-demo/views/demo/config/common.js +57 -0
- data/test/pages-demo/views/demo/config/footer.txt +1 -0
- data/test/pages-demo/views/demo/config/global-head.txt +8 -0
- data/test/pages-demo/views/demo/config/header.txt +1 -0
- data/test/pages-demo/views/demo/config/layout.txt +1 -0
- data/test/pages-demo/views/demo/config/left.txt +1 -0
- data/test/pages-demo/views/demo/config/main.txt +1 -0
- data/test/pages-demo/views/demo/config/right.txt +1 -0
- data/test/pages-demo/views/demo/config.txt +3 -0
- data/test/pages-demo/views/demo/output/panes/footer.html +1 -0
- data/test/pages-demo/views/demo/output/panes/header.html +1 -0
- data/test/pages-demo/views/demo/output/panes/left.html +1 -0
- data/test/pages-demo/views/demo/output/panes/main.html +1 -0
- data/test/pages-demo/views/demo/output/panes/right.html +1 -0
- data/test/rubytext/rubytext_comprehensive_test.rb +307 -0
- data/test/rubytext/rubytext_demo_test.rb +42 -0
- data/test/rubytext/rubytext_testing_guide.md +277 -0
- data/test/run_automated_tests.rb +45 -0
- data/test/scriptorium-TEST-1754622690-146/config/bootstrap_css.txt +5 -0
- data/test/scriptorium-TEST-1754622690-146/config/bootstrap_js.txt +4 -0
- data/test/scriptorium-TEST-1754622690-146/config/common.js +57 -0
- data/test/scriptorium-TEST-1754622690-146/config/currentview.txt +1 -0
- data/test/scriptorium-TEST-1754622690-146/config/global-head.txt +9 -0
- data/test/scriptorium-TEST-1754622690-146/config/last_post_num.txt +1 -0
- data/test/scriptorium-TEST-1754622690-146/config/os_helpers.rb +4 -0
- data/test/scriptorium-TEST-1754622690-146/config/widgets.txt +3 -0
- data/test/scriptorium-TEST-1754622690-146/posts/0001/meta.txt +8 -0
- data/test/scriptorium-TEST-1754622690-146/posts/0001/source.lt3 +6 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/README.txt +1 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/config.txt +1 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/initial/post.lt3 +12 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/footer.txt +2 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/header.txt +4 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/left.txt +3 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/main.txt +5 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/right.txt +3 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/gen/text.css +1 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/layout.txt +5 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/index.lt3 +1 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/index_entry.lt3 +14 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/post.lt3 +13 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/widget.lt3 +1 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/bootstrap_css.txt +5 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/bootstrap_js.txt +4 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/common.js +57 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/deploy.txt +5 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/footer.txt +2 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/global-head.txt +9 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/header.txt +4 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/layout.txt +5 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/left.txt +3 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/main.txt +5 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/reddit.txt +10 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/right.txt +3 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/social.txt +7 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/status.txt +7 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config.txt +3 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/layout/footer.html +3 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/layout/header.html +3 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/layout/left.html +3 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/layout/main.html +3 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/layout/right.html +3 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/footer.html +1 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/header.html +1 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/left.html +1 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/main.html +1 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/right.html +1 -0
- data/test/staging/.DS_Store +0 -0
- data/test/syntax_highlighting_test.lt3 +124 -0
- data/test/test_helpers.rb +230 -0
- data/test/tui_editor_integration_test.rb +296 -0
- data/test/tui_integration_test.rb +637 -0
- data/test/unit/api.rb +1056 -0
- data/test/unit/asset_management.rb +245 -0
- data/test/unit/clipboard_test.rb +60 -0
- data/test/unit/contract_test.rb +91 -0
- data/test/unit/core.rb +857 -0
- data/test/unit/deploy_test.rb +187 -0
- data/test/unit/gem_asset_management.rb +189 -0
- data/test/unit/livetext_basic.rb +69 -0
- data/test/unit/livetext_compatibility.rb +89 -0
- data/test/unit/post.rb +244 -0
- data/test/unit/read_commented_file_test.rb +276 -0
- data/test/unit/reddit_test.rb +235 -0
- data/test/unit/repo.rb +548 -0
- data/test/unit/social_test.rb +369 -0
- data/test/unit/symlink_test.rb +213 -0
- data/test/unit/view.rb +431 -0
- data/test/unit/widgets.rb +669 -0
- data/test/wizard_test.rb +123 -0
- data/ui/README.md +67 -0
- data/ui/common/lib/ui_common.rb +8 -0
- data/ui/rubytext/README.md +191 -0
- data/ui/rubytext/bin/scriptorium-rubytext +402 -0
- data/ui/rubytext/lib/rubytext_ui.rb +300 -0
- data/ui/tui/bin/scriptorium +1420 -0
- data/ui/tui/test/tui_test.rb +23 -0
- data/ui/web/app/app.rb +1378 -0
- data/ui/web/app/error_helpers.rb +150 -0
- data/ui/web/app/views/advanced_config.erb +190 -0
- data/ui/web/app/views/asset_management.erb +589 -0
- data/ui/web/app/views/banner_config.erb +200 -0
- data/ui/web/app/views/configure_view.erb +401 -0
- data/ui/web/app/views/dashboard.erb +162 -0
- data/ui/web/app/views/deploy_config.erb +146 -0
- data/ui/web/app/views/edit_pages.erb +195 -0
- data/ui/web/app/views/edit_post.erb +54 -0
- data/ui/web/app/views/error_page.erb +29 -0
- data/ui/web/app/views/header_config.erb +155 -0
- data/ui/web/app/views/layout_config.erb +147 -0
- data/ui/web/app/views/navbar_config.erb +411 -0
- data/ui/web/app/views/view_dashboard.erb +138 -0
- data/ui/web/bin/scriptorium-web +153 -0
- data/ui/web/test/web_basic_test.rb +38 -0
- data/ui/web/test_navbar.txt +7 -0
- data/ui/web/tmp/web_server.log +5 -0
- data/ui/web/tmp/web_server.pid +1 -0
- metadata +359 -7
- data/lib/scriptorium/engine.rb +0 -22
- data/test/engine/unit.rb +0 -44
@@ -0,0 +1,640 @@
|
|
1
|
+
class Scriptorium::API
|
2
|
+
include Scriptorium::Exceptions
|
3
|
+
include Scriptorium::Helpers
|
4
|
+
include Scriptorium::Contract
|
5
|
+
|
6
|
+
attr_reader :repo, :current_view
|
7
|
+
|
8
|
+
# Invariants
|
9
|
+
def define_invariants
|
10
|
+
invariant { [true, false].include?(@testing) }
|
11
|
+
invariant { @repo.nil? || @repo.is_a?(Scriptorium::Repo) }
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(testmode: false)
|
15
|
+
assume { [true, false].include?(testmode) }
|
16
|
+
|
17
|
+
@testing = testmode
|
18
|
+
@repo = nil
|
19
|
+
|
20
|
+
define_invariants
|
21
|
+
verify { @testing == testmode }
|
22
|
+
check_invariants
|
23
|
+
end
|
24
|
+
|
25
|
+
def repo_exists?(path)
|
26
|
+
Dir.exist?(path)
|
27
|
+
end
|
28
|
+
|
29
|
+
def create_repo(path)
|
30
|
+
check_invariants
|
31
|
+
assume { path.is_a?(String) && !path.empty? }
|
32
|
+
|
33
|
+
raise RepoDirAlreadyExists if repo_exists?(path)
|
34
|
+
Scriptorium::Repo.create(path)
|
35
|
+
@repo = Scriptorium::Repo.open(path)
|
36
|
+
|
37
|
+
verify { @repo.is_a?(Scriptorium::Repo) }
|
38
|
+
check_invariants
|
39
|
+
end
|
40
|
+
|
41
|
+
def open_repo(path)
|
42
|
+
check_invariants
|
43
|
+
assume { path.is_a?(String) && !path.empty? }
|
44
|
+
|
45
|
+
@repo = Scriptorium::Repo.open(path)
|
46
|
+
|
47
|
+
verify { @repo.is_a?(Scriptorium::Repo) }
|
48
|
+
check_invariants
|
49
|
+
end
|
50
|
+
|
51
|
+
# View management
|
52
|
+
def create_view(name, title, subtitle = "", theme: "standard")
|
53
|
+
check_invariants
|
54
|
+
assume { name.is_a?(String) }
|
55
|
+
assume { title.is_a?(String) }
|
56
|
+
assume { subtitle.is_a?(String) }
|
57
|
+
assume { theme.is_a?(String) }
|
58
|
+
assume { @repo.is_a?(Scriptorium::Repo) }
|
59
|
+
|
60
|
+
@repo.create_view(name, title, subtitle, theme: theme)
|
61
|
+
|
62
|
+
verify { @repo.is_a?(Scriptorium::Repo) }
|
63
|
+
check_invariants
|
64
|
+
self
|
65
|
+
end
|
66
|
+
|
67
|
+
def current_view
|
68
|
+
@repo&.current_view
|
69
|
+
end
|
70
|
+
|
71
|
+
def root
|
72
|
+
@repo.root
|
73
|
+
end
|
74
|
+
|
75
|
+
def version
|
76
|
+
Scriptorium::VERSION
|
77
|
+
end
|
78
|
+
|
79
|
+
def apply_theme(theme)
|
80
|
+
@repo.view.apply_theme(theme)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Post management
|
84
|
+
def view(name = nil)
|
85
|
+
if name.nil?
|
86
|
+
@repo.current_view
|
87
|
+
else
|
88
|
+
result = @repo.view(name)
|
89
|
+
result
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def views
|
94
|
+
@repo&.views || []
|
95
|
+
end
|
96
|
+
|
97
|
+
def lookup_view(target)
|
98
|
+
@repo&.lookup_view(target)
|
99
|
+
end
|
100
|
+
|
101
|
+
def views_for(post_or_id)
|
102
|
+
post = post_or_id.is_a?(Integer) ? @repo.post(post_or_id) : post_or_id
|
103
|
+
post.views&.split(/\s+/) || []
|
104
|
+
end
|
105
|
+
|
106
|
+
# Post creation with convenience defaults
|
107
|
+
def create_post(title, body, views: nil, tags: nil, blurb: nil)
|
108
|
+
check_invariants
|
109
|
+
assume { title.is_a?(String) }
|
110
|
+
assume { body.is_a?(String) }
|
111
|
+
assume { views.nil? || views.is_a?(String) || views.is_a?(Array) }
|
112
|
+
assume { tags.nil? || tags.is_a?(String) || tags.is_a?(Array) }
|
113
|
+
assume { blurb.nil? || blurb.is_a?(String) }
|
114
|
+
assume { @repo.is_a?(Scriptorium::Repo) }
|
115
|
+
|
116
|
+
views ||= @repo.current_view&.name
|
117
|
+
raise "No view specified and no current view set" if views.nil?
|
118
|
+
|
119
|
+
post = @repo.create_post(
|
120
|
+
title: title,
|
121
|
+
body: body,
|
122
|
+
views: views,
|
123
|
+
tags: tags,
|
124
|
+
blurb: blurb
|
125
|
+
)
|
126
|
+
|
127
|
+
verify { post.is_a?(Scriptorium::Post) }
|
128
|
+
check_invariants
|
129
|
+
post
|
130
|
+
end
|
131
|
+
|
132
|
+
# Draft management
|
133
|
+
def draft(title: nil, body: nil, views: nil, tags: nil, blurb: nil)
|
134
|
+
views ||= @repo.current_view&.name
|
135
|
+
raise "No view specified and no current view set" if views.nil?
|
136
|
+
|
137
|
+
@repo.create_draft(
|
138
|
+
title: title,
|
139
|
+
body: body,
|
140
|
+
views: views,
|
141
|
+
tags: tags,
|
142
|
+
blurb: blurb
|
143
|
+
)
|
144
|
+
end
|
145
|
+
|
146
|
+
def create_draft(title: nil, body: nil, views: nil, tags: nil, blurb: nil)
|
147
|
+
views ||= @repo.current_view&.name
|
148
|
+
raise "No view specified and no current view set" if views.nil?
|
149
|
+
|
150
|
+
@repo.create_draft(
|
151
|
+
title: title,
|
152
|
+
body: body,
|
153
|
+
views: views,
|
154
|
+
tags: tags,
|
155
|
+
blurb: blurb
|
156
|
+
)
|
157
|
+
end
|
158
|
+
|
159
|
+
def finish_draft(draft_path)
|
160
|
+
@repo.finish_draft(draft_path)
|
161
|
+
end
|
162
|
+
|
163
|
+
# Generation
|
164
|
+
def generate_front_page(view = nil)
|
165
|
+
view ||= @repo.current_view&.name
|
166
|
+
raise "No view specified and no current view set" if view.nil?
|
167
|
+
|
168
|
+
@repo.generate_front_page(view)
|
169
|
+
end
|
170
|
+
|
171
|
+
def generate_post_index(view = nil)
|
172
|
+
view ||= @repo.current_view&.name
|
173
|
+
raise "No view specified and no current view set" if view.nil?
|
174
|
+
|
175
|
+
@repo.generate_post_index(view)
|
176
|
+
end
|
177
|
+
|
178
|
+
def generate_post(post_id)
|
179
|
+
# Check if the post directory exists first
|
180
|
+
post_dir = @repo.root/:posts/d4(post_id)
|
181
|
+
if Dir.exist?(post_dir)
|
182
|
+
# Post directory exists, proceed with generation
|
183
|
+
@repo.generate_post(post_id)
|
184
|
+
else
|
185
|
+
# Try to find the post through normal means
|
186
|
+
post = @repo.post(post_id)
|
187
|
+
raise "Post not found" if post.nil?
|
188
|
+
|
189
|
+
@repo.generate_post(post_id)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def lookup_view(view_name)
|
194
|
+
@repo.lookup_view(view_name)
|
195
|
+
end
|
196
|
+
|
197
|
+
# Publication system
|
198
|
+
def publish_post(num)
|
199
|
+
check_invariants
|
200
|
+
assume { num.is_a?(Integer) }
|
201
|
+
assume { @repo.is_a?(Scriptorium::Repo) }
|
202
|
+
|
203
|
+
post = @repo.publish_post(num)
|
204
|
+
|
205
|
+
verify { post.is_a?(Scriptorium::Post) }
|
206
|
+
check_invariants
|
207
|
+
post
|
208
|
+
end
|
209
|
+
|
210
|
+
def post_published?(num)
|
211
|
+
@repo.post_published?(num)
|
212
|
+
end
|
213
|
+
|
214
|
+
def get_published_posts(view = nil)
|
215
|
+
view ||= @repo.current_view&.name
|
216
|
+
@repo.get_published_posts(view)
|
217
|
+
end
|
218
|
+
|
219
|
+
# Post retrieval
|
220
|
+
def posts(view = nil)
|
221
|
+
view ||= @repo.current_view&.name
|
222
|
+
@repo.all_posts(view)
|
223
|
+
end
|
224
|
+
|
225
|
+
def post_attrs(post_id, *keys)
|
226
|
+
post = post_id.is_a?(Integer) ? @repo.post(post_id) : post_id
|
227
|
+
post.attrs(*keys)
|
228
|
+
end
|
229
|
+
|
230
|
+
def post(id)
|
231
|
+
@repo.post(id)
|
232
|
+
end
|
233
|
+
|
234
|
+
# Post management
|
235
|
+
def delete_post(id)
|
236
|
+
post = @repo.post(id)
|
237
|
+
old_path = @repo.root/:posts/post.num
|
238
|
+
new_path = @repo.root/:posts/"_#{post.num}"
|
239
|
+
FileUtils.mv(old_path, new_path)
|
240
|
+
|
241
|
+
# Set the deleted flag in metadata
|
242
|
+
post.meta["post.deleted"] = "true"
|
243
|
+
post.save_metadata
|
244
|
+
end
|
245
|
+
|
246
|
+
def undelete_post(id)
|
247
|
+
post = @repo.post(id)
|
248
|
+
old_path = @repo.root/:posts/"_#{post.num}"
|
249
|
+
new_path = @repo.root/:posts/post.num
|
250
|
+
FileUtils.mv(old_path, new_path)
|
251
|
+
|
252
|
+
# Clear the deleted flag in metadata
|
253
|
+
post.meta["post.deleted"] = "false"
|
254
|
+
post.save_metadata
|
255
|
+
end
|
256
|
+
|
257
|
+
def unlink_post(id, view = nil)
|
258
|
+
# Remove post from a specific view (or current view if none specified)
|
259
|
+
view ||= @repo.current_view&.name
|
260
|
+
raise "No view specified and no current view set" if view.nil?
|
261
|
+
|
262
|
+
post = @repo.post(id)
|
263
|
+
raise "Post not found" if post.nil?
|
264
|
+
|
265
|
+
# Get current views from metadata (split string into array)
|
266
|
+
current_views = post.views.strip.split(/\s+/)
|
267
|
+
|
268
|
+
# Remove the specified view
|
269
|
+
new_views = current_views - [view]
|
270
|
+
|
271
|
+
# Update the post with new views list
|
272
|
+
result = update_post(id, {views: new_views})
|
273
|
+
|
274
|
+
# Regenerate the post to update metadata
|
275
|
+
@repo.generate_post(id) if result
|
276
|
+
|
277
|
+
result
|
278
|
+
end
|
279
|
+
|
280
|
+
def link_post(id, view = nil)
|
281
|
+
# Add post to a specific view (or current view if none specified)
|
282
|
+
view ||= @repo.current_view&.name
|
283
|
+
raise "No view specified and no current view set" if view.nil?
|
284
|
+
|
285
|
+
post = @repo.post(id)
|
286
|
+
raise "Post not found" if post.nil?
|
287
|
+
|
288
|
+
current_views = post.views.strip.split(/\s+/)
|
289
|
+
new_views = current_views.include?(view) ? current_views : current_views + [view]
|
290
|
+
result = update_post(id, {views: new_views})
|
291
|
+
|
292
|
+
@repo.generate_post(id) if result
|
293
|
+
|
294
|
+
result
|
295
|
+
end
|
296
|
+
|
297
|
+
def post_add_view(id, view)
|
298
|
+
# Add a view to a post (view can be string or View object)
|
299
|
+
view_name = view.is_a?(String) ? view : view.name
|
300
|
+
link_post(id, view_name)
|
301
|
+
end
|
302
|
+
|
303
|
+
def post_remove_view(id, view)
|
304
|
+
# Remove a view from a post (view can be string or View object)
|
305
|
+
view_name = view.is_a?(String) ? view : view.name
|
306
|
+
unlink_post(id, view_name)
|
307
|
+
end
|
308
|
+
|
309
|
+
def post_add_tag(id, tag)
|
310
|
+
# Add a tag to a post
|
311
|
+
post = @repo.post(id)
|
312
|
+
raise "Post not found" if post.nil?
|
313
|
+
|
314
|
+
# Get current tags from metadata (split comma-separated string into array)
|
315
|
+
current_tags = post.tags.strip.split(/,\s*/)
|
316
|
+
|
317
|
+
# Add the tag (avoid duplicates)
|
318
|
+
new_tags = current_tags.include?(tag) ? current_tags : current_tags + [tag]
|
319
|
+
|
320
|
+
# Update the post with new tags list
|
321
|
+
result = update_post(id, {tags: new_tags})
|
322
|
+
|
323
|
+
# Regenerate the post to update metadata
|
324
|
+
@repo.generate_post(id) if result
|
325
|
+
|
326
|
+
result
|
327
|
+
end
|
328
|
+
|
329
|
+
def post_remove_tag(id, tag)
|
330
|
+
# Remove a tag from a post
|
331
|
+
post = @repo.post(id)
|
332
|
+
raise "Post not found" if post.nil?
|
333
|
+
|
334
|
+
# Get current tags from metadata (split comma-separated string into array)
|
335
|
+
current_tags = post.tags.strip.split(/,\s*/)
|
336
|
+
|
337
|
+
# Remove the tag
|
338
|
+
new_tags = current_tags - [tag]
|
339
|
+
|
340
|
+
# Update the post with new tags list
|
341
|
+
result = update_post(id, {tags: new_tags})
|
342
|
+
|
343
|
+
# Regenerate the post to update metadata
|
344
|
+
@repo.generate_post(id) if result
|
345
|
+
|
346
|
+
result
|
347
|
+
end
|
348
|
+
|
349
|
+
# Theme management
|
350
|
+
def themes_available
|
351
|
+
themes_dir = @repo.root/:themes
|
352
|
+
return [] unless Dir.exist?(themes_dir)
|
353
|
+
Dir.children(themes_dir).select { |d| Dir.exist?(themes_dir/d) }
|
354
|
+
end
|
355
|
+
|
356
|
+
# Widget management
|
357
|
+
def widgets_available
|
358
|
+
widgets_file = @repo.root/:config/"widgets.txt"
|
359
|
+
return [] unless File.exist?(widgets_file)
|
360
|
+
read_file(widgets_file, lines: true, chomp: true)
|
361
|
+
end
|
362
|
+
|
363
|
+
def generate_widget(widget_name)
|
364
|
+
# Generate a specific widget for the current view
|
365
|
+
# widget_name: string name of the widget (e.g., "links", "news")
|
366
|
+
# Returns true on success, raises error on failure
|
367
|
+
|
368
|
+
raise "No current view set" if @repo.current_view.nil?
|
369
|
+
raise "Widget name cannot be nil" if widget_name.nil?
|
370
|
+
raise "Widget name cannot be empty" if widget_name.to_s.strip.empty?
|
371
|
+
|
372
|
+
# Validate widget name format
|
373
|
+
unless widget_name.to_s.match?(/^[a-zA-Z0-9_]+$/)
|
374
|
+
raise "Invalid widget name: #{widget_name} (must be alphanumeric and underscore only)"
|
375
|
+
end
|
376
|
+
|
377
|
+
# Convert to class name (capitalize first letter)
|
378
|
+
widget_class_name = widget_name.to_s.capitalize
|
379
|
+
|
380
|
+
# Try to find the widget class
|
381
|
+
begin
|
382
|
+
widget_class = eval("Scriptorium::Widget::#{widget_class_name}")
|
383
|
+
rescue NameError
|
384
|
+
raise "Widget class not found: Scriptorium::Widget::#{widget_class_name}"
|
385
|
+
end
|
386
|
+
|
387
|
+
# Create widget instance and generate
|
388
|
+
widget = widget_class.new(@repo, @repo.current_view)
|
389
|
+
widget.generate
|
390
|
+
|
391
|
+
true
|
392
|
+
end
|
393
|
+
|
394
|
+
# Convenience file editing methods
|
395
|
+
|
396
|
+
def edit_layout(view = nil)
|
397
|
+
view ||= @repo.current_view&.name
|
398
|
+
raise "No view specified and no current view set" if view.nil?
|
399
|
+
edit_file("views/#{view}/layout.txt")
|
400
|
+
end
|
401
|
+
|
402
|
+
def edit_config(view = nil)
|
403
|
+
view ||= @repo.current_view&.name
|
404
|
+
raise "No view specified and no current view set" if view.nil?
|
405
|
+
edit_file("views/#{view}/config.txt")
|
406
|
+
end
|
407
|
+
|
408
|
+
def edit_widget_data(view = nil, widget)
|
409
|
+
view ||= @repo.current_view&.name
|
410
|
+
raise "No view specified and no current view set" if view.nil?
|
411
|
+
raise "Widget name cannot be nil" if widget.nil?
|
412
|
+
edit_file("views/#{view}/widgets/#{widget}/list.txt")
|
413
|
+
end
|
414
|
+
|
415
|
+
def edit_repo_config
|
416
|
+
edit_file("config/repo.txt")
|
417
|
+
end
|
418
|
+
|
419
|
+
def edit_deploy_config
|
420
|
+
edit_file("config/deploy.txt")
|
421
|
+
end
|
422
|
+
|
423
|
+
def edit_post(post_id)
|
424
|
+
post = @repo.post(post_id)
|
425
|
+
source_path = "posts/#{post.num}/source.lt3"
|
426
|
+
body_path = "posts/#{post.num}/body.html"
|
427
|
+
|
428
|
+
if File.exist?(source_path)
|
429
|
+
edit_file(source_path)
|
430
|
+
else
|
431
|
+
edit_file(body_path)
|
432
|
+
end
|
433
|
+
end
|
434
|
+
|
435
|
+
# File operations
|
436
|
+
|
437
|
+
def edit_file(path)
|
438
|
+
# Input validation
|
439
|
+
raise CannotEditFilePathNil if path.nil?
|
440
|
+
raise CannotEditFilePathEmpty if path.to_s.strip.empty?
|
441
|
+
|
442
|
+
editor = ENV['EDITOR'] || 'vim'
|
443
|
+
system!(editor, path)
|
444
|
+
end
|
445
|
+
|
446
|
+
# Post selection and search
|
447
|
+
def select_posts(&block)
|
448
|
+
# Filter posts using a block
|
449
|
+
# Returns array of posts that match the block condition
|
450
|
+
# Example: api.select_posts { |post| post.views.include?("alpha") }
|
451
|
+
|
452
|
+
all_posts = @repo.all_posts
|
453
|
+
all_posts.select(&block)
|
454
|
+
end
|
455
|
+
|
456
|
+
def search_posts(**criteria)
|
457
|
+
# Search posts using keyword criteria
|
458
|
+
# criteria: hash of {field: pattern} where field is :title, :body, :tags, :blurb
|
459
|
+
# pattern: string (exact match) or regex (pattern match)
|
460
|
+
# Example: api.search_posts(title: /Ruby/, tags: "scriptorium")
|
461
|
+
|
462
|
+
all_posts = @repo.all_posts
|
463
|
+
matching_posts = []
|
464
|
+
|
465
|
+
all_posts.each do |post|
|
466
|
+
matches_all_criteria = true
|
467
|
+
|
468
|
+
criteria.each do |field, pattern|
|
469
|
+
# Get the field value from the post
|
470
|
+
field_value = case field
|
471
|
+
when :title
|
472
|
+
post.title
|
473
|
+
when :body
|
474
|
+
# Read the body from the source file
|
475
|
+
body_file = post.dir/"body.html"
|
476
|
+
File.exist?(body_file) ? read_file(body_file) : ""
|
477
|
+
when :tags
|
478
|
+
post.tags
|
479
|
+
when :blurb
|
480
|
+
post.blurb
|
481
|
+
else
|
482
|
+
raise "Unknown search field: #{field}"
|
483
|
+
end
|
484
|
+
|
485
|
+
# Check if the pattern matches
|
486
|
+
if pattern.is_a?(Regexp)
|
487
|
+
matches_all_criteria = false unless field_value.match?(pattern)
|
488
|
+
else
|
489
|
+
matches_all_criteria = false unless field_value.include?(pattern.to_s)
|
490
|
+
end
|
491
|
+
|
492
|
+
break unless matches_all_criteria
|
493
|
+
end
|
494
|
+
|
495
|
+
matching_posts << post if matches_all_criteria
|
496
|
+
end
|
497
|
+
|
498
|
+
matching_posts
|
499
|
+
end
|
500
|
+
|
501
|
+
# Generation
|
502
|
+
def generate_view(view = nil)
|
503
|
+
view ||= @repo.current_view&.name
|
504
|
+
raise "No view specified and no current view set" if view.nil?
|
505
|
+
|
506
|
+
@repo.generate_front_page(view)
|
507
|
+
true
|
508
|
+
end
|
509
|
+
|
510
|
+
|
511
|
+
|
512
|
+
# Draft management
|
513
|
+
def drafts
|
514
|
+
drafts_dir = @repo.root/:drafts
|
515
|
+
return [] unless Dir.exist?(drafts_dir)
|
516
|
+
|
517
|
+
draft_files = Dir.children(drafts_dir).select { |f| f.end_with?('-draft.lt3') }
|
518
|
+
draft_files.map do |filename|
|
519
|
+
path = drafts_dir/filename
|
520
|
+
# Quick scan for title from the draft file
|
521
|
+
title = extract_title_from_draft(path)
|
522
|
+
{ path: path, title: title }
|
523
|
+
end
|
524
|
+
end
|
525
|
+
|
526
|
+
def delete_draft(draft_path)
|
527
|
+
# Delete a draft file
|
528
|
+
# draft_path: path to the draft file (e.g., from drafts() method)
|
529
|
+
|
530
|
+
raise "Draft path cannot be nil" if draft_path.nil?
|
531
|
+
raise "Draft path cannot be empty" if draft_path.to_s.strip.empty?
|
532
|
+
|
533
|
+
# Ensure it's actually a draft file
|
534
|
+
unless draft_path.to_s.end_with?('-draft.lt3')
|
535
|
+
raise "Not a valid draft file: #{draft_path}"
|
536
|
+
end
|
537
|
+
|
538
|
+
# Ensure it exists
|
539
|
+
unless File.exist?(draft_path)
|
540
|
+
raise "Draft file not found: #{draft_path}"
|
541
|
+
end
|
542
|
+
|
543
|
+
# Delete the file
|
544
|
+
File.delete(draft_path)
|
545
|
+
true
|
546
|
+
end
|
547
|
+
|
548
|
+
private def extract_title_from_draft(draft_path)
|
549
|
+
# Quick scan for .title line in draft file
|
550
|
+
return "Untitled" unless File.exist?(draft_path)
|
551
|
+
|
552
|
+
File.foreach(draft_path) do |line|
|
553
|
+
if line.strip.start_with?('.title')
|
554
|
+
title = line.strip.split(/\s+/, 2)[1]
|
555
|
+
return title || "Untitled"
|
556
|
+
end
|
557
|
+
end
|
558
|
+
"Untitled"
|
559
|
+
end
|
560
|
+
|
561
|
+
def update_post(id, fields)
|
562
|
+
# Update fields in the post's source.lt3 file
|
563
|
+
# fields: hash of {field: value} where field is livetext dotcmd (e.g., :views, :title, :tags)
|
564
|
+
# value: string or array of strings
|
565
|
+
|
566
|
+
post = @repo.post(id)
|
567
|
+
source_file = post.dir/"source.lt3"
|
568
|
+
return false unless File.exist?(source_file)
|
569
|
+
|
570
|
+
# Read the file
|
571
|
+
lines = read_file(source_file, lines: true, chomp: false)
|
572
|
+
updated = false
|
573
|
+
|
574
|
+
# Process each field
|
575
|
+
fields.each do |field, value|
|
576
|
+
# Convert value to array
|
577
|
+
value_array = Array(value)
|
578
|
+
|
579
|
+
# Handle different field types
|
580
|
+
case field
|
581
|
+
when :tags
|
582
|
+
# Tags should be comma-separated
|
583
|
+
new_value = value_array.join(", ")
|
584
|
+
else
|
585
|
+
# Other fields (views, etc.) should be space-separated
|
586
|
+
new_value = value_array.join(' ')
|
587
|
+
end
|
588
|
+
|
589
|
+
lines.map! do |line|
|
590
|
+
if line.strip.start_with?(".#{field}")
|
591
|
+
# Preserve trailing comments
|
592
|
+
comment_match = line.match(/(\s+#.*)$/)
|
593
|
+
comment = comment_match ? comment_match[1] : ""
|
594
|
+
|
595
|
+
# Add change comment
|
596
|
+
timestamp = Time.now.strftime("%Y/%m/%d %H:%M:%S")
|
597
|
+
change_comment = " # updated #{field} #{timestamp}"
|
598
|
+
|
599
|
+
updated = true
|
600
|
+
".#{field} #{new_value}#{comment}#{change_comment}\n"
|
601
|
+
else
|
602
|
+
line
|
603
|
+
end
|
604
|
+
end
|
605
|
+
end
|
606
|
+
|
607
|
+
return false unless updated
|
608
|
+
|
609
|
+
# Write the updated file
|
610
|
+
write_file(source_file, lines.join)
|
611
|
+
true
|
612
|
+
end
|
613
|
+
|
614
|
+
# TODO: Discuss later - complex metadata vs source conflict handling
|
615
|
+
# def update_post(id, attributes)
|
616
|
+
# # Need to decide: source of truth, update strategy, concurrency handling
|
617
|
+
# end
|
618
|
+
|
619
|
+
# TODO: Discuss later - publish draft workflow
|
620
|
+
# def publish_draft(draft_path)
|
621
|
+
# # finish_draft + generate_post combined?
|
622
|
+
# end
|
623
|
+
|
624
|
+
# Utility methods
|
625
|
+
|
626
|
+
# Convenience workflow methods
|
627
|
+
|
628
|
+
# # Delegate common repo methods
|
629
|
+
# def method_missing(method, *args, &block)
|
630
|
+
# if @repo.respond_to?(method)
|
631
|
+
# @repo.send(method, *args, &block)
|
632
|
+
# else
|
633
|
+
# super
|
634
|
+
# end
|
635
|
+
# end
|
636
|
+
#
|
637
|
+
# def respond_to_missing?(method, include_private = false)
|
638
|
+
# @repo.respond_to?(method, include_private) || super
|
639
|
+
# end
|
640
|
+
end
|