hyde_admin 0.0.11 → 0.0.14

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.
@@ -0,0 +1,92 @@
1
+ require 'rack/test'
2
+
3
+ RSpec.describe "Create files and directories", type: :system do
4
+ include Rack::Test::Methods
5
+
6
+ def app
7
+ Mid
8
+ end
9
+
10
+ let(:tmpdir) { Dir.mktmpdir }
11
+
12
+ before do
13
+ FileUtils.cp(File.join(File.dirname(__FILE__), '../../bin/hyde_admin.yml'), File.join(tmpdir, 'hyde_admin.yml'))
14
+ allow(Dir).to receive(:pwd).and_return(tmpdir)
15
+ end
16
+
17
+ after do
18
+ FileUtils.remove_entry(tmpdir)
19
+ end
20
+
21
+ describe "directory creation" do
22
+ it "creates a new directory" do
23
+ post "/files/create_dir?dir_path=#{tmpdir}", {
24
+ directory_name: "my_new_folder"
25
+ }
26
+
27
+ expect(last_response).to be_redirect
28
+
29
+ new_dir = File.join(tmpdir, 'my_new_folder')
30
+ expect(File.directory?(new_dir)).to be true
31
+ end
32
+
33
+ it "creates a nested directory inside an existing one" do
34
+ parent = File.join(tmpdir, 'parent')
35
+ FileUtils.mkdir_p(parent)
36
+
37
+ post "/files/create_dir?dir_path=#{parent}", {
38
+ directory_name: "child"
39
+ }
40
+
41
+ expect(File.directory?(File.join(parent, 'child'))).to be true
42
+ end
43
+ end
44
+
45
+ describe "file creation" do
46
+ it "creates an empty file" do
47
+ post "/files/create_file?dir_path=#{tmpdir}", {
48
+ file_name: "new_page.html"
49
+ }
50
+
51
+ expect(last_response).to be_redirect
52
+
53
+ new_file = File.join(tmpdir, 'new_page.html')
54
+ expect(File.exist?(new_file)).to be true
55
+ expect(File.read(new_file)).to eq("")
56
+ end
57
+
58
+ it "creates a file inside a subdirectory" do
59
+ subdir = File.join(tmpdir, 'assets')
60
+ FileUtils.mkdir_p(subdir)
61
+
62
+ post "/files/create_file?dir_path=#{subdir}", {
63
+ file_name: "style.css"
64
+ }
65
+
66
+ new_file = File.join(subdir, 'style.css')
67
+ expect(File.exist?(new_file)).to be true
68
+ end
69
+ end
70
+
71
+ describe "file upload" do
72
+ it "uploads a file" do
73
+ tempfile = Tempfile.new(['upload', '.txt'])
74
+ tempfile.write("file content here")
75
+ tempfile.rewind
76
+
77
+ uploaded_file = Rack::Test::UploadedFile.new(tempfile.path, "text/plain", false)
78
+
79
+ post "/files/create?dir_path=#{tmpdir}", {
80
+ files: uploaded_file
81
+ }
82
+
83
+ expect(last_response).to be_redirect
84
+
85
+ uploaded = File.join(tmpdir, File.basename(tempfile.path))
86
+ expect(File.exist?(uploaded)).to be true
87
+ expect(File.read(uploaded)).to eq("file content here")
88
+
89
+ tempfile.close!
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,105 @@
1
+ RSpec.describe Mid do
2
+ describe ".transliterate_title_for_url" do
3
+ it "converts a simple title to a URL-friendly slug" do
4
+ expect(Mid.transliterate_title_for_url("Mon article")).to eq("mon-article")
5
+ end
6
+
7
+ it "removes trailing hyphens" do
8
+ expect(Mid.transliterate_title_for_url("Mon fichier !")).to eq("mon-fichier")
9
+ end
10
+
11
+ it "removes leading and trailing spaces" do
12
+ expect(Mid.transliterate_title_for_url(" Hello World ")).to eq("hello-world")
13
+ end
14
+
15
+ it "collapses multiple spaces into a single hyphen" do
16
+ expect(Mid.transliterate_title_for_url("Hello World")).to eq("hello-world")
17
+ end
18
+
19
+ it "handles accented characters" do
20
+ expect(Mid.transliterate_title_for_url("Écrire un résumé")).to eq("ecrire-un-resume")
21
+ end
22
+
23
+ it "preserves numbers" do
24
+ expect(Mid.transliterate_title_for_url("Article 2026")).to eq("article-2026")
25
+ end
26
+
27
+ it "handles titles with only special characters" do
28
+ expect(Mid.transliterate_title_for_url("!!!")).to eq("")
29
+ end
30
+
31
+ it "handles titles ending with special characters" do
32
+ expect(Mid.transliterate_title_for_url("Mon titre...")).to eq("mon-titre")
33
+ end
34
+
35
+ it "removes punctuation" do
36
+ expect(Mid.transliterate_title_for_url("Hello, World!")).to eq("hello-world")
37
+ end
38
+ end
39
+
40
+ describe ".urlize" do
41
+ it "generates a filename with date for posts" do
42
+ expect(Mid.urlize("2026-03-17", "Mon article", true)).to eq("2026-03-17-mon-article")
43
+ end
44
+
45
+ it "generates a filename without date for pages" do
46
+ expect(Mid.urlize("2026-03-17", "Ma page", false)).to eq("ma-page")
47
+ end
48
+
49
+ it "does not produce trailing hyphens" do
50
+ result = Mid.urlize("2026-03-17", "Mon fichier !", true)
51
+ expect(result).to eq("2026-03-17-mon-fichier")
52
+ expect(result).not_to end_with("-")
53
+ end
54
+ end
55
+
56
+ describe ".extract_header_str" do
57
+ it "extracts YAML front matter" do
58
+ content = "---\ntitle: Hello\nlayout: post\n---\nBody content"
59
+ expect(Mid.extract_header_str(content)).to eq("\ntitle: Hello\nlayout: post\n")
60
+ end
61
+
62
+ it "returns nil when no front matter" do
63
+ expect(Mid.extract_header_str("No front matter here")).to be_nil
64
+ end
65
+ end
66
+
67
+ describe ".extract_header" do
68
+ it "parses front matter into a hash" do
69
+ content = "---\ntitle: Hello\nlayout: post\n---\nBody"
70
+ result = Mid.extract_header(content)
71
+ expect(result).to eq({ "title" => "Hello", "layout" => "post" })
72
+ end
73
+
74
+ it "returns empty hash for content without front matter" do
75
+ expect(Mid.extract_header("No front matter")).to eq({})
76
+ end
77
+ end
78
+
79
+ describe ".remove_header" do
80
+ it "removes YAML front matter from content" do
81
+ content = "---\ntitle: Hello\n---\nBody content"
82
+ expect(Mid.remove_header(content).strip).to eq("Body content")
83
+ end
84
+ end
85
+
86
+ describe ".extract_tags" do
87
+ it "parses comma-separated tags" do
88
+ expect(Mid.extract_tags("ruby,jekyll,web")).to eq(["ruby", "jekyll", "web"])
89
+ end
90
+
91
+ it "handles bracketed tags" do
92
+ expect(Mid.extract_tags("[ruby,jekyll]")).to eq(["ruby", "jekyll"])
93
+ end
94
+ end
95
+
96
+ describe ".safe_path?" do
97
+ it "accepts paths within the working directory" do
98
+ expect(Mid.safe_path?(File.join(Dir.pwd, "some_file.md"))).to be true
99
+ end
100
+
101
+ it "rejects paths outside the working directory" do
102
+ expect(Mid.safe_path?("/etc/passwd")).to be false
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,77 @@
1
+ require 'rack/test'
2
+
3
+ RSpec.describe "Create a draft", type: :system do
4
+ include Rack::Test::Methods
5
+
6
+ def app
7
+ Mid
8
+ end
9
+
10
+ let(:tmpdir) { Dir.mktmpdir }
11
+ let(:drafts_dir) { File.join(tmpdir, '_drafts') }
12
+ let(:posts_dir) { File.join(tmpdir, '_posts') }
13
+ let(:layouts_dir) { File.join(tmpdir, '_layouts') }
14
+
15
+ before do
16
+ FileUtils.mkdir_p(drafts_dir)
17
+ FileUtils.mkdir_p(posts_dir)
18
+ FileUtils.mkdir_p(layouts_dir)
19
+ File.write(File.join(layouts_dir, 'post.html'), '<html>{{ content }}</html>')
20
+ FileUtils.cp(File.join(File.dirname(__FILE__), '../../bin/hyde_admin.yml'), File.join(tmpdir, 'hyde_admin.yml'))
21
+
22
+ allow(Dir).to receive(:pwd).and_return(tmpdir)
23
+ end
24
+
25
+ after do
26
+ FileUtils.remove_entry(tmpdir)
27
+ end
28
+
29
+ it "creates a draft file in _drafts with date prefix" do
30
+ post "/drafts", {
31
+ file: "",
32
+ title: "Work in progress",
33
+ date: "2026-04-10 14:00:00 +0200",
34
+ tags: "draft",
35
+ layout: "post",
36
+ format: "md",
37
+ content: "This is still a draft.",
38
+ new_file: ""
39
+ }
40
+
41
+ expect(last_response).to be_redirect
42
+
43
+ created_files = Dir.glob(File.join(drafts_dir, '*.md'))
44
+ expect(created_files.size).to eq(1)
45
+
46
+ filename = File.basename(created_files.first)
47
+ expect(filename).to eq("2026-04-10-work-in-progress.md")
48
+
49
+ content = File.read(created_files.first)
50
+ headers = Mid.extract_header(content)
51
+
52
+ expect(headers['layout']).to eq("post")
53
+ expect(headers['title']).to eq("Work in progress")
54
+
55
+ body = Mid.remove_header(content)
56
+ expect(body).to include("This is still a draft.")
57
+ end
58
+
59
+ it "publishes a draft by moving it to _posts" do
60
+ post "/drafts", {
61
+ file: "",
62
+ title: "Ready to publish",
63
+ date: "2026-04-10 14:00:00 +0200",
64
+ tags: "news",
65
+ layout: "post",
66
+ format: "md",
67
+ content: "This draft is ready.",
68
+ publish: "publish",
69
+ new_file: ""
70
+ }
71
+
72
+ expect(Dir.glob(File.join(drafts_dir, '*'))).to be_empty
73
+ published_files = Dir.glob(File.join(posts_dir, '*.md'))
74
+ expect(published_files.size).to eq(1)
75
+ expect(File.basename(published_files.first)).to eq("2026-04-10-ready-to-publish.md")
76
+ end
77
+ end
@@ -0,0 +1,75 @@
1
+ require 'rack/test'
2
+
3
+ RSpec.describe "Create a page", type: :system do
4
+ include Rack::Test::Methods
5
+
6
+ def app
7
+ Mid
8
+ end
9
+
10
+ let(:tmpdir) { Dir.mktmpdir }
11
+ let(:pages_dir) { File.join(tmpdir, '_pages') }
12
+ let(:layouts_dir) { File.join(tmpdir, '_layouts') }
13
+
14
+ before do
15
+ FileUtils.mkdir_p(pages_dir)
16
+ FileUtils.mkdir_p(layouts_dir)
17
+ File.write(File.join(layouts_dir, 'default.html'), '<html>{{ content }}</html>')
18
+ FileUtils.cp(File.join(File.dirname(__FILE__), '../../bin/hyde_admin.yml'), File.join(tmpdir, 'hyde_admin.yml'))
19
+
20
+ allow(Dir).to receive(:pwd).and_return(tmpdir)
21
+ end
22
+
23
+ after do
24
+ FileUtils.remove_entry(tmpdir)
25
+ end
26
+
27
+ it "creates a page file without date prefix in filename" do
28
+ post "/pages", {
29
+ file: "",
30
+ title: "About me",
31
+ date: "2026-03-17 10:00:00 +0100",
32
+ tags: "info",
33
+ layout: "default",
34
+ format: "html",
35
+ content: "This is the about page.",
36
+ new_file: ""
37
+ }
38
+
39
+ expect(last_response).to be_redirect
40
+
41
+ created_files = Dir.glob(File.join(pages_dir, '*'))
42
+ expect(created_files.size).to eq(1)
43
+
44
+ filename = File.basename(created_files.first)
45
+ expect(filename).to eq("about-me.html")
46
+ expect(filename).not_to match(/^\d{4}-\d{2}-\d{2}/)
47
+
48
+ content = File.read(created_files.first)
49
+ headers = Mid.extract_header(content)
50
+
51
+ expect(headers['layout']).to eq("default")
52
+ expect(headers['title']).to eq("About me")
53
+ expect(headers['date']).to eq("2026-03-17 10:00:00 +0100")
54
+
55
+ body = Mid.remove_header(content)
56
+ expect(body).to include("This is the about page.")
57
+ end
58
+
59
+ it "creates a page in markdown format" do
60
+ post "/pages", {
61
+ file: "",
62
+ title: "Contact",
63
+ date: "2026-05-01 09:00:00 +0200",
64
+ tags: "",
65
+ layout: "default",
66
+ format: "md",
67
+ content: "# Contact\n\nSend me an email.",
68
+ new_file: ""
69
+ }
70
+
71
+ created_files = Dir.glob(File.join(pages_dir, '*.md'))
72
+ expect(created_files.size).to eq(1)
73
+ expect(File.basename(created_files.first)).to eq("contact.md")
74
+ end
75
+ end
@@ -0,0 +1,100 @@
1
+ require 'rack/test'
2
+
3
+ RSpec.describe "Create a post", type: :system do
4
+ include Rack::Test::Methods
5
+
6
+ def app
7
+ Mid
8
+ end
9
+
10
+ let(:tmpdir) { Dir.mktmpdir }
11
+ let(:posts_dir) { File.join(tmpdir, '_posts') }
12
+ let(:layouts_dir) { File.join(tmpdir, '_layouts') }
13
+
14
+ before do
15
+ FileUtils.mkdir_p(posts_dir)
16
+ FileUtils.mkdir_p(layouts_dir)
17
+ File.write(File.join(layouts_dir, 'post.html'), '<html>{{ content }}</html>')
18
+ File.write(File.join(layouts_dir, 'default.html'), '<html>{{ content }}</html>')
19
+ FileUtils.cp(File.join(File.dirname(__FILE__), '../../bin/hyde_admin.yml'), File.join(tmpdir, 'hyde_admin.yml'))
20
+
21
+ allow(Dir).to receive(:pwd).and_return(tmpdir)
22
+ end
23
+
24
+ after do
25
+ FileUtils.remove_entry(tmpdir)
26
+ end
27
+
28
+ it "creates a post file with correct date, format, layout and content" do
29
+ post "/posts", {
30
+ file: "",
31
+ title: "Mon premier article",
32
+ date: "2026-03-17 10:30:00 +0100",
33
+ tags: "ruby,jekyll",
34
+ layout: "post",
35
+ format: "md",
36
+ content: "Ceci est le contenu de mon article.",
37
+ new_file: ""
38
+ }
39
+
40
+ expect(last_response).to be_redirect
41
+
42
+ created_files = Dir.glob(File.join(posts_dir, '*.md'))
43
+ expect(created_files.size).to eq(1)
44
+
45
+ filename = File.basename(created_files.first)
46
+ expect(filename).to eq("2026-03-17-mon-premier-article.md")
47
+
48
+ content = File.read(created_files.first)
49
+
50
+ expect(content).to start_with("---\n")
51
+ expect(content).to match(/^---\n.*^---$/m)
52
+
53
+ headers = Mid.extract_header(content)
54
+
55
+ expect(headers['date']).to eq("2026-03-17 10:30:00 +0100")
56
+ expect(headers['layout']).to eq("post")
57
+ expect(headers['title']).to eq("Mon premier article")
58
+ expect(headers['tags']).to eq("ruby,jekyll")
59
+
60
+ body = Mid.remove_header(content)
61
+ expect(body).to include("Ceci est le contenu de mon article.")
62
+ end
63
+
64
+ it "creates a post in html format" do
65
+ post "/posts", {
66
+ file: "",
67
+ title: "Article HTML",
68
+ date: "2026-01-15 08:00:00 +0100",
69
+ tags: "web",
70
+ layout: "default",
71
+ format: "html",
72
+ content: "<p>Some HTML content</p>",
73
+ new_file: ""
74
+ }
75
+
76
+ created_files = Dir.glob(File.join(posts_dir, '*.html'))
77
+ expect(created_files.size).to eq(1)
78
+ expect(File.basename(created_files.first)).to eq("2026-01-15-article-html.html")
79
+
80
+ headers = Mid.extract_header(File.read(created_files.first))
81
+ expect(headers['layout']).to eq("default")
82
+ end
83
+
84
+ it "creates a post with accented title" do
85
+ post "/posts", {
86
+ file: "",
87
+ title: "Écrire un résumé",
88
+ date: "2026-06-01 12:00:00 +0200",
89
+ tags: "french",
90
+ layout: "post",
91
+ format: "md",
92
+ content: "Some accented content.",
93
+ new_file: ""
94
+ }
95
+
96
+ created_files = Dir.glob(File.join(posts_dir, '*.md'))
97
+ expect(created_files.size).to eq(1)
98
+ expect(File.basename(created_files.first)).to eq("2026-06-01-ecrire-un-resume.md")
99
+ end
100
+ end
@@ -0,0 +1,92 @@
1
+ require 'rack/test'
2
+
3
+ RSpec.describe "Global search", type: :system do
4
+ include Rack::Test::Methods
5
+
6
+ def app
7
+ Mid
8
+ end
9
+
10
+ let(:tmpdir) { Dir.mktmpdir }
11
+ let(:posts_dir) { File.join(tmpdir, '_posts') }
12
+ let(:pages_dir) { File.join(tmpdir, '_pages') }
13
+ let(:drafts_dir) { File.join(tmpdir, '_drafts') }
14
+ let(:layouts_dir) { File.join(tmpdir, '_layouts') }
15
+
16
+ before do
17
+ FileUtils.mkdir_p(posts_dir)
18
+ FileUtils.mkdir_p(pages_dir)
19
+ FileUtils.mkdir_p(drafts_dir)
20
+ FileUtils.mkdir_p(layouts_dir)
21
+ File.write(File.join(layouts_dir, 'default.html'), '<html>{{ content }}</html>')
22
+ FileUtils.cp(File.join(File.dirname(__FILE__), '../../bin/hyde_admin.yml'), File.join(tmpdir, 'hyde_admin.yml'))
23
+
24
+ File.write(File.join(posts_dir, '2026-03-17-hello-world.md'),
25
+ "---\ntitle: Hello World\ntags: ruby,test\nlayout: default\n---\nThis is the body of hello world.")
26
+ File.write(File.join(posts_dir, '2026-03-16-another-post.md'),
27
+ "---\ntitle: Another Post\ntags: jekyll\nlayout: default\n---\nSomething else entirely.")
28
+ File.write(File.join(pages_dir, 'about.html'),
29
+ "---\ntitle: About Me\ntags: info\nlayout: default\n---\n<p>About page content</p>")
30
+ File.write(File.join(drafts_dir, '2026-03-17-draft-idea.md'),
31
+ "---\ntitle: Draft Idea\ntags: draft\nlayout: default\n---\nWork in progress content.")
32
+
33
+ allow(Dir).to receive(:pwd).and_return(tmpdir)
34
+ end
35
+
36
+ after do
37
+ FileUtils.remove_entry(tmpdir)
38
+ end
39
+
40
+ it "returns matching posts by title" do
41
+ get "/search", q: "Hello"
42
+
43
+ expect(last_response).to be_ok
44
+ expect(last_response.body).to include("Hello World")
45
+ expect(last_response.body).not_to include("Another Post")
46
+ end
47
+
48
+ it "returns matching posts by tag" do
49
+ get "/search", q: "jekyll"
50
+
51
+ expect(last_response).to be_ok
52
+ expect(last_response.body).to include("Another Post")
53
+ expect(last_response.body).not_to include("Hello World")
54
+ end
55
+
56
+ it "returns matching posts by body content" do
57
+ get "/search", q: "entirely"
58
+
59
+ expect(last_response).to be_ok
60
+ expect(last_response.body).to include("Another Post")
61
+ end
62
+
63
+ it "searches across posts, pages and drafts" do
64
+ get "/search", q: "content"
65
+
66
+ expect(last_response).to be_ok
67
+ expect(last_response.body).to include("About Me")
68
+ expect(last_response.body).to include("Draft Idea")
69
+ end
70
+
71
+ it "searches case-insensitively" do
72
+ get "/search", q: "hello"
73
+
74
+ expect(last_response).to be_ok
75
+ expect(last_response.body).to include("Hello World")
76
+ end
77
+
78
+ it "returns no results for unknown query" do
79
+ get "/search", q: "zzzznotfound"
80
+
81
+ expect(last_response).to be_ok
82
+ expect(last_response.body).to include("0")
83
+ end
84
+
85
+ it "ignores queries shorter than 2 characters" do
86
+ get "/search", q: "a"
87
+
88
+ expect(last_response).to be_ok
89
+ expect(last_response.body).not_to include("Hello World")
90
+ expect(last_response.body).not_to include("About Me")
91
+ end
92
+ end
@@ -0,0 +1,25 @@
1
+ require 'yaml'
2
+ require 'fileutils'
3
+ require 'i18n'
4
+ require 'date'
5
+ require 'cgi'
6
+ require 'roda'
7
+ require_relative '../lib/hyde_admin/version'
8
+
9
+ # Load hyde_admin.ru but skip the `run` call at the end
10
+ app_code = File.read(File.expand_path('../../bin/hyde_admin.ru', __FILE__))
11
+ # Remove the final `run ...` line that starts the Rack server
12
+ app_code = app_code.sub(/^run\s+.*\z/m, '')
13
+ eval(app_code, binding, File.expand_path('../../bin/hyde_admin.ru', __FILE__), 1)
14
+
15
+ RSpec.configure do |config|
16
+ config.expect_with :rspec do |expectations|
17
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
18
+ end
19
+
20
+ config.mock_with :rspec do |mocks|
21
+ mocks.verify_partial_doubles = true
22
+ end
23
+
24
+ config.shared_context_metadata_behavior = :apply_to_host_groups
25
+ end