siru 0.3.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a129c8c8137e9ef0ffef0839973bbf761105a083edbbd3ba1b519e61a521dfbe
4
+ data.tar.gz: 8341a02414f843007b01dba2527c6d6a21ef96c0dd447eb4eb91353200e50e23
5
+ SHA512:
6
+ metadata.gz: f4d9b74fa3257a7ecb91b1ff0b188949cfa2db8b6012957e3e7735f64a0d37648cea45d6cbb00a6adf4bed78a1c18f685112fd7f3710060d18a526c1e34f6879
7
+ data.tar.gz: 2afb22464e6303caee943ddfc2d2dc24ec35df002be29b6f07f4bafbeefa4e0c13c5ccb11fc896931aea948cb8ff7f40e29c5852656684f54c242819adb34e4c
data/Gemfile ADDED
@@ -0,0 +1,19 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'redcarpet', '~> 3.6'
4
+ gem 'liquid', '~> 5.4'
5
+ gem 'toml', '~> 0.3'
6
+ gem 'sassc', '~> 2.4'
7
+ gem 'listen', '~> 3.8'
8
+ gem 'webrick', '~> 1.8'
9
+ gem 'front_matter_parser', '~> 1.0'
10
+ gem 'fileutils'
11
+ gem 'pathname'
12
+ gem 'yaml'
13
+ gem 'base64'
14
+ gem 'logger'
15
+
16
+ group :development do
17
+ gem 'rspec', '~> 3.12'
18
+ gem 'rubocop', '~> 1.56'
19
+ end
data/README.md ADDED
@@ -0,0 +1,164 @@
1
+ # Siru
2
+
3
+ Siru is a static site generator inspired by [Hugo](https://gohugo.io/), built with Ruby.
4
+
5
+ ## Features
6
+ - Markdown content rendering
7
+ - Theme support
8
+ - Live server with reload
9
+
10
+ ## Installation
11
+
12
+ ### Via APT Repository (Ubuntu/Debian)
13
+
14
+ ```bash
15
+ # Add the repository
16
+ echo "deb [trusted=yes arch=amd64] https://raw.githubusercontent.com/timappledotcom/siru-apt-repo/main/ stable main" | sudo tee /etc/apt/sources.list.d/siru.list
17
+
18
+ # Update package list
19
+ sudo apt update
20
+
21
+ # Install Siru
22
+ sudo apt install siru
23
+ ```
24
+
25
+ ### Via .deb Package
26
+
27
+ ```bash
28
+ wget https://github.com/timappledotcom/siru/releases/download/v0.2.0/siru_0.2.0_all.deb
29
+ sudo dpkg -i siru_0.2.0_all.deb
30
+ ```
31
+
32
+ ### Via Flatpak
33
+
34
+ ```bash
35
+ flatpak install --user siru-0.2.0.flatpak
36
+ ```
37
+
38
+ ## Usage
39
+
40
+ 1. **Create a new site**
41
+ ```
42
+ siru new [sitename]
43
+ ```
44
+
45
+ 2. **Create a new post**
46
+
47
+ **Quick method (auto-generates and opens in editor):**
48
+ ```
49
+ siru new post "My Post Title"
50
+ siru new post "My Draft Post" --draft
51
+ ```
52
+
53
+ This command will:
54
+ - Create a new markdown file in `content/posts/`
55
+ - Generate a filename from the title (e.g., "my-post-title.md")
56
+ - Add proper front matter with title, date, and draft status
57
+ - Open the file in your default editor (set via `$EDITOR` or `$VISUAL`)
58
+
59
+ **Manual method:**
60
+ - Navigate to the `content/posts/` directory.
61
+ - Create a new Markdown file with TOML or YAML front matter:
62
+
63
+ ```toml
64
+ +++
65
+ title = "My Awesome Post"
66
+ date = "2025-07-14"
67
+ draft = true
68
+ +++
69
+
70
+ Content goes here.
71
+ ```
72
+
73
+ 3. **Create a new page**
74
+ - Navigate to the `content/` directory (not inside `posts`).
75
+ - Create a new Markdown file:
76
+
77
+ ```toml
78
+ +++
79
+ title = "About Us"
80
+ date = "2025-07-14"
81
+ draft = true
82
+ +++
83
+
84
+ About us content.
85
+ ```
86
+
87
+ 4. **Build the site**
88
+ ```
89
+ cd [sitename]
90
+ siru build
91
+ ```
92
+
93
+ 5. **Serve the site**
94
+ ```
95
+ siru serve
96
+ ```
97
+
98
+ ### Drafts
99
+
100
+ - **Draft Mode**: To include draft posts in your build or serve, add the `--draft` option.
101
+
102
+ ```bash
103
+ siru build --draft
104
+ siru serve --draft
105
+ ```
106
+
107
+ - **Draft Status**: Posts with `draft = true` in their front matter will not be published unless the `--draft` option is used.
108
+
109
+ ## Configuration
110
+
111
+ Siru sites are configured through a `config.toml` file in the root directory:
112
+
113
+ ```toml
114
+ baseURL = "https://yoursite.com/"
115
+ languageCode = "en-us"
116
+ title = "My Awesome Site"
117
+ theme = "paper"
118
+
119
+ [params]
120
+ color = "linen" # Theme color: linen, wheat, gray, light
121
+ bio = "Welcome to my blog!"
122
+ twitter = "yourusername"
123
+ github = "yourusername"
124
+ mastodon = "https://mastodon.social/@yourusername"
125
+ bluesky = "yourusername.bsky.social"
126
+ ```
127
+
128
+ ## Front Matter
129
+
130
+ Siru supports both TOML and YAML front matter:
131
+
132
+ ### TOML Front Matter
133
+ ```toml
134
+ +++
135
+ title = "Post Title"
136
+ date = "2025-07-14"
137
+ draft = false
138
+ tags = ["ruby", "static-site"]
139
+ summary = "A brief summary of the post"
140
+ +++
141
+ ```
142
+
143
+ ### YAML Front Matter
144
+ ```yaml
145
+ ---
146
+ title: "Post Title"
147
+ date: "2025-07-14"
148
+ draft: false
149
+ tags: ["ruby", "static-site"]
150
+ summary: "A brief summary of the post"
151
+ ---
152
+ ```
153
+
154
+ ### Available Front Matter Fields
155
+ - `title`: Post/page title
156
+ - `date`: Publication date
157
+ - `draft`: Whether the content is a draft (true/false)
158
+ - `tags`: Array of tags for the post
159
+ - `summary`: Brief description (used in post lists)
160
+ - `slug`: Custom URL slug (optional)
161
+
162
+ ## License
163
+
164
+ This project is licensed under the terms of the [GPL-3.0 license](LICENSE).
data/bin/siru ADDED
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/siru'
4
+
5
+ require 'optparse'
6
+
7
+ options = {}
8
+ OptionParser.new do |opts|
9
+ opts.banner = "Usage: siru [options] [command]"
10
+
11
+ opts.separator ""
12
+ opts.separator "Commands:"
13
+ opts.separator " new SITENAME Create a new site"
14
+ opts.separator " new post TITLE Create a new post"
15
+ opts.separator " build Build the site"
16
+ opts.separator " serve Serve the site locally"
17
+ opts.separator " help Show this help"
18
+ opts.separator ""
19
+ opts.separator "Options:"
20
+
21
+ opts.on("-p", "--port PORT", "Port to serve on (default: 3000)") do |port|
22
+ options[:port] = port.to_i
23
+ end
24
+
25
+ opts.on("-w", "--watch", "Watch for changes and rebuild") do
26
+ options[:watch] = true
27
+ end
28
+
29
+ opts.on("-d", "--draft", "Include draft posts") do
30
+ options[:draft] = true
31
+ end
32
+
33
+ opts.on("-h", "--help", "Show this help") do
34
+ puts opts
35
+ exit
36
+ end
37
+ end.parse!
38
+
39
+ command = ARGV[0]
40
+
41
+ case command
42
+ when 'new'
43
+ subcommand = ARGV[1]
44
+ case subcommand
45
+ when 'post'
46
+ post_title = ARGV[2..-1].join(' ')
47
+ if post_title.empty?
48
+ puts "Error: Please provide a post title"
49
+ puts "Usage: siru new post TITLE"
50
+ exit 1
51
+ end
52
+ Siru::CLI.new_post(post_title, options)
53
+ else
54
+ # Treat as site name for backward compatibility
55
+ site_name = subcommand
56
+ if site_name.nil?
57
+ puts "Error: Please provide a site name or 'post' subcommand"
58
+ puts "Usage: siru new SITENAME"
59
+ puts " siru new post TITLE"
60
+ exit 1
61
+ end
62
+ Siru::CLI.new_site(site_name)
63
+ end
64
+ when 'build'
65
+ Siru::CLI.build(options)
66
+ when 'serve'
67
+ Siru::CLI.serve(options)
68
+ when 'help', nil
69
+ puts "Siru - A Hugo-inspired static site generator"
70
+ puts ""
71
+ puts "Usage: siru [command] [options]"
72
+ puts ""
73
+ puts "Commands:"
74
+ puts " new SITENAME Create a new site"
75
+ puts " new post TITLE Create a new post"
76
+ puts " build Build the site"
77
+ puts " serve Serve the site locally"
78
+ puts " help Show this help"
79
+ puts ""
80
+ puts "Options:"
81
+ puts " -p, --port PORT Port to serve on (default: 3000)"
82
+ puts " -w, --watch Watch for changes and rebuild"
83
+ puts " -d, --draft Include draft posts"
84
+ puts " -h, --help Show this help"
85
+ else
86
+ puts "Unknown command: #{command}"
87
+ puts "Run 'siru help' for usage information"
88
+ exit 1
89
+ end
@@ -0,0 +1,126 @@
1
+ module Siru
2
+ class Builder
3
+ def initialize(site, options = {})
4
+ @site = site
5
+ @options = options
6
+ @output_dir = 'public'
7
+ end
8
+
9
+ def build
10
+ puts "Building site..."
11
+
12
+ # Clean output directory
13
+ FileUtils.rm_rf(@output_dir)
14
+ FileUtils.mkdir_p(@output_dir)
15
+
16
+ # Copy static files
17
+ copy_static_files
18
+
19
+ # Build pages
20
+ build_home_page
21
+ build_post_pages
22
+
23
+ puts "Site built successfully in #{@output_dir}/"
24
+ end
25
+
26
+ def clean
27
+ FileUtils.rm_rf(@output_dir)
28
+ puts "Cleaned output directory: #{@output_dir}"
29
+ end
30
+
31
+ private
32
+
33
+ def copy_static_files
34
+ # Copy theme static files
35
+ @site.theme.static_files.each do |file|
36
+ relative_path = file.gsub(@site.theme.path + '/static/', '')
37
+ output_path = File.join(@output_dir, relative_path)
38
+
39
+ FileUtils.mkdir_p(File.dirname(output_path))
40
+ FileUtils.cp(file, output_path)
41
+ end
42
+
43
+ # Copy site static files
44
+ static_dir = 'static'
45
+ if Dir.exist?(static_dir)
46
+ Dir.glob(File.join(static_dir, '**', '*')).each do |file|
47
+ next unless File.file?(file)
48
+
49
+ relative_path = file.gsub(static_dir + '/', '')
50
+ output_path = File.join(@output_dir, relative_path)
51
+
52
+ FileUtils.mkdir_p(File.dirname(output_path))
53
+ FileUtils.cp(file, output_path)
54
+ end
55
+ end
56
+ end
57
+
58
+ def build_home_page
59
+ variables = {
60
+ 'site' => site_variables,
61
+ 'posts' => @site.posts.map { |post| post_variables(post) },
62
+ 'page_title' => 'Home',
63
+ 'bg_color' => color_for_theme
64
+ }
65
+
66
+ content = @site.theme.render_layout('index', variables)
67
+ html = @site.theme.render_layout('baseof', variables.merge('content' => content))
68
+
69
+ File.write(File.join(@output_dir, 'index.html'), html)
70
+ end
71
+
72
+ def build_post_pages
73
+ @site.posts.each do |post|
74
+ variables = {
75
+ 'site' => site_variables,
76
+ 'post' => post_variables(post),
77
+ 'page_title' => post.title,
78
+ 'bg_color' => color_for_theme
79
+ }
80
+
81
+ content = @site.theme.render_layout('single', variables)
82
+ html = @site.theme.render_layout('baseof', variables.merge('content' => content))
83
+
84
+ # Create directory structure
85
+ post_dir = File.join(@output_dir, 'posts', post.slug)
86
+ FileUtils.mkdir_p(post_dir)
87
+
88
+ File.write(File.join(post_dir, 'index.html'), html)
89
+ end
90
+ end
91
+
92
+ def site_variables
93
+ {
94
+ 'title' => @site.title,
95
+ 'base_url' => @site.base_url,
96
+ 'language_code' => @site.language_code,
97
+ 'params' => @site.params
98
+ }
99
+ end
100
+
101
+ def post_variables(post)
102
+ {
103
+ 'title' => post.title,
104
+ 'date' => post.date,
105
+ 'slug' => post.slug,
106
+ 'url' => post.url,
107
+ 'summary' => post.summary,
108
+ 'tags' => post.tags,
109
+ 'rendered_content' => post.rendered_content,
110
+ 'content' => post.content
111
+ }
112
+ end
113
+
114
+ def color_for_theme
115
+ color_map = {
116
+ 'linen' => '#faf8f1',
117
+ 'wheat' => '#f8f5d7',
118
+ 'gray' => '#fbfbfb',
119
+ 'light' => '#fff',
120
+ 'catppuccin-mocha' => '#1e1e2e'
121
+ }
122
+
123
+ color_map[@site.params['color']] || color_map['linen']
124
+ end
125
+ end
126
+ end
data/lib/siru/cli.rb ADDED
@@ -0,0 +1,155 @@
1
+ module Siru
2
+ class CLI
3
+ def self.new_site(name)
4
+ puts "Creating new site: #{name}"
5
+
6
+ # Get the original working directory from environment variable or current directory
7
+ original_dir = ENV['CD'] || Dir.pwd
8
+ target_dir = File.expand_path(name, original_dir)
9
+
10
+ FileUtils.mkdir_p(target_dir)
11
+ Dir.chdir(target_dir) do
12
+ # Create directory structure
13
+ %w[content content/posts static themes public].each do |dir|
14
+ FileUtils.mkdir_p(dir)
15
+ end
16
+
17
+ # Create config file
18
+ config = {
19
+ 'baseURL' => 'http://localhost:3000/',
20
+ 'languageCode' => 'en-us',
21
+ 'title' => name.capitalize,
22
+ 'theme' => 'paper',
23
+ 'params' => {
24
+ 'color' => 'linen',
25
+ 'bio' => 'A blog powered by Siru',
26
+ 'disableHLJS' => true,
27
+ 'disablePostNavigation' => true,
28
+ 'monoDarkIcon' => true,
29
+ 'math' => true,
30
+ 'localKatex' => false
31
+ }
32
+ }
33
+
34
+ File.write('config.toml', TOML::Generator.new(config).body)
35
+
36
+ # Create sample post
37
+ sample_post = <<~POST
38
+ +++
39
+ title = "Hello Siru"
40
+ date = "#{Date.today.strftime('%Y-%m-%d')}"
41
+ draft = false
42
+ +++
43
+
44
+ Welcome to your new Siru site!
45
+
46
+ This is your first post. Edit or delete it and start blogging!
47
+ POST
48
+
49
+ File.write('content/posts/hello-siru.md', sample_post)
50
+
51
+ # Copy theme files
52
+ siru_gem_dir = File.expand_path('../../..', __FILE__)
53
+ source_theme_dir = File.join(siru_gem_dir, 'themes', 'paper')
54
+ target_theme_dir = File.join('themes', 'paper')
55
+
56
+ if Dir.exist?(source_theme_dir)
57
+ FileUtils.cp_r(source_theme_dir, 'themes/')
58
+ puts "Theme 'paper' copied successfully"
59
+ else
60
+ puts "Warning: Theme 'paper' not found in #{source_theme_dir}"
61
+ end
62
+
63
+ puts "Site created successfully!"
64
+ puts "To get started:"
65
+ puts " cd #{name}"
66
+ puts " siru serve"
67
+ end
68
+ end
69
+
70
+ def self.build(options = {})
71
+ # Get the original working directory from environment variable or current directory
72
+ original_dir = ENV['CD'] || Dir.pwd
73
+ config_path = File.join(original_dir, 'config.toml')
74
+
75
+ unless File.exist?(config_path)
76
+ puts "Error: Not in a Siru site directory. Run 'siru new SITENAME' first."
77
+ exit 1
78
+ end
79
+
80
+ Dir.chdir(original_dir) do
81
+ config = Config.load
82
+ site = Site.new(config, options)
83
+ builder = Builder.new(site, options)
84
+ builder.build
85
+ end
86
+ end
87
+
88
+ def self.serve(options = {})
89
+ # Get the original working directory from environment variable or current directory
90
+ original_dir = ENV['CD'] || Dir.pwd
91
+ config_path = File.join(original_dir, 'config.toml')
92
+
93
+ unless File.exist?(config_path)
94
+ puts "Error: Not in a Siru site directory. Run 'siru new SITENAME' first."
95
+ exit 1
96
+ end
97
+
98
+ Dir.chdir(original_dir) do
99
+ config = Config.load
100
+ site = Site.new(config, options)
101
+ server = Server.new(site, options)
102
+ server.start
103
+ end
104
+ end
105
+
106
+ def self.new_post(title, options = {})
107
+ # Get the original working directory from environment variable or current directory
108
+ original_dir = ENV['CD'] || Dir.pwd
109
+ config_path = File.join(original_dir, 'config.toml')
110
+
111
+ unless File.exist?(config_path)
112
+ puts "Error: Not in a Siru site directory. Run 'siru new SITENAME' first."
113
+ exit 1
114
+ end
115
+
116
+ # Generate filename from title
117
+ filename = title.downcase.gsub(/\s+/, '-').gsub(/[^a-z0-9\-]/, '') + '.md'
118
+ filepath = File.join(original_dir, 'content', 'posts', filename)
119
+
120
+ # Check if file already exists
121
+ if File.exist?(filepath)
122
+ puts "Error: Post '#{filepath}' already exists."
123
+ exit 1
124
+ end
125
+
126
+ # Create the posts directory if it doesn't exist
127
+ FileUtils.mkdir_p(File.dirname(filepath))
128
+
129
+ # Generate post content
130
+ date = Date.today.strftime('%Y-%m-%d')
131
+ draft = options[:draft] ? 'true' : 'false'
132
+
133
+ # Clean up the title (remove any surrounding quotes)
134
+ clean_title = title.gsub(/^["']|["']$/, '')
135
+
136
+ post_content = <<~POST
137
+ +++
138
+ title = "#{clean_title}"
139
+ date = "#{date}"
140
+ draft = #{draft}
141
+ +++
142
+
143
+ Write your post content here.
144
+ POST
145
+
146
+ # Write the file
147
+ File.write(filepath, post_content)
148
+ puts "Created new post: #{filepath}"
149
+
150
+ # Open in editor
151
+ editor = ENV['EDITOR'] || ENV['VISUAL'] || 'nano'
152
+ system("#{editor} #{filepath}")
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,92 @@
1
+ module Siru
2
+ class Config
3
+ class ConfigError < StandardError; end
4
+
5
+ attr_reader :data
6
+
7
+ def self.load(path = 'config.toml', strict: false)
8
+ new(path, strict: strict)
9
+ end
10
+
11
+ def initialize(path = 'config.toml', strict: false)
12
+ @path = path
13
+ @strict = strict
14
+ @data = load_config
15
+ end
16
+
17
+ def [](key)
18
+ @data[key]
19
+ end
20
+
21
+ def []=(key, value)
22
+ @data[key] = value
23
+ end
24
+
25
+ def fetch(key, default = nil)
26
+ @data.fetch(key, default)
27
+ end
28
+
29
+ def get(key)
30
+ @data[key]
31
+ end
32
+
33
+ def param(key)
34
+ return nil unless @data['params']
35
+ @data['params'][key]
36
+ end
37
+
38
+ def params
39
+ @data['params'] || {}
40
+ end
41
+
42
+ # Dynamic attribute access for config keys
43
+ def method_missing(method_name, *args, &block)
44
+ if args.empty? && @data.key?(method_name.to_s)
45
+ @data[method_name.to_s]
46
+ else
47
+ super
48
+ end
49
+ end
50
+
51
+ def respond_to_missing?(method_name, include_private = false)
52
+ @data.key?(method_name.to_s) || super
53
+ end
54
+
55
+ private
56
+
57
+ def load_config
58
+ unless File.exist?(@path)
59
+ if @strict
60
+ raise ConfigError, "Configuration file #{@path} not found"
61
+ else
62
+ return default_config
63
+ end
64
+ end
65
+
66
+ case File.extname(@path)
67
+ when '.toml'
68
+ TOML.load_file(@path)
69
+ when '.yaml', '.yml'
70
+ YAML.load_file(@path)
71
+ else
72
+ raise ConfigError, "Unsupported config format: #{@path}"
73
+ end
74
+ rescue Parslet::ParseFailed => e
75
+ raise ConfigError, "Error parsing TOML config: #{e.message}"
76
+ rescue Psych::SyntaxError => e
77
+ raise ConfigError, "Error parsing YAML config: #{e.message}"
78
+ end
79
+
80
+ def default_config
81
+ {
82
+ 'baseURL' => 'http://localhost:3000/',
83
+ 'languageCode' => 'en-us',
84
+ 'title' => 'My Siru Site',
85
+ 'theme' => 'paper',
86
+ 'params' => {
87
+ 'color' => 'linen'
88
+ }
89
+ }
90
+ end
91
+ end
92
+ end