bunko 0.2.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 +7 -0
- data/.standard.yml +3 -0
- data/CHANGELOG.md +41 -0
- data/CLAUDE.md +351 -0
- data/LICENSE.txt +21 -0
- data/README.md +641 -0
- data/ROADMAP.md +519 -0
- data/Rakefile +10 -0
- data/lib/bunko/configuration.rb +180 -0
- data/lib/bunko/controllers/acts_as.rb +22 -0
- data/lib/bunko/controllers/collection.rb +160 -0
- data/lib/bunko/controllers.rb +5 -0
- data/lib/bunko/models/acts_as.rb +24 -0
- data/lib/bunko/models/post_methods/publishable.rb +51 -0
- data/lib/bunko/models/post_methods/sluggable.rb +47 -0
- data/lib/bunko/models/post_methods/word_countable.rb +76 -0
- data/lib/bunko/models/post_methods.rb +75 -0
- data/lib/bunko/models/post_type_methods.rb +18 -0
- data/lib/bunko/models.rb +6 -0
- data/lib/bunko/railtie.rb +22 -0
- data/lib/bunko/routing/mapper_methods.rb +103 -0
- data/lib/bunko/routing.rb +4 -0
- data/lib/bunko/version.rb +5 -0
- data/lib/bunko.rb +11 -0
- data/lib/tasks/bunko/add.rake +259 -0
- data/lib/tasks/bunko/helpers.rb +25 -0
- data/lib/tasks/bunko/install.rake +125 -0
- data/lib/tasks/bunko/sample_data.rake +128 -0
- data/lib/tasks/bunko/setup.rake +186 -0
- data/lib/tasks/support/sample_data_generator.rb +399 -0
- data/lib/tasks/templates/INSTALL.md +62 -0
- data/lib/tasks/templates/config/initializers/bunko.rb.tt +45 -0
- data/lib/tasks/templates/controllers/controller.rb.tt +25 -0
- data/lib/tasks/templates/controllers/pages_controller.rb.tt +29 -0
- data/lib/tasks/templates/db/migrate/create_post_types.rb.tt +14 -0
- data/lib/tasks/templates/db/migrate/create_posts.rb.tt +31 -0
- data/lib/tasks/templates/models/post.rb.tt +8 -0
- data/lib/tasks/templates/models/post_type.rb.tt +8 -0
- data/lib/tasks/templates/views/collections/index.html.erb.tt +67 -0
- data/lib/tasks/templates/views/collections/show.html.erb.tt +39 -0
- data/lib/tasks/templates/views/layouts/bunko_footer.html.erb.tt +3 -0
- data/lib/tasks/templates/views/layouts/bunko_nav.html.erb.tt +9 -0
- data/lib/tasks/templates/views/layouts/bunko_styles.html.erb.tt +3 -0
- data/lib/tasks/templates/views/pages/show.html.erb.tt +16 -0
- data/sig/bunko.rbs +4 -0
- metadata +116 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require_relative "helpers"
|
|
5
|
+
|
|
6
|
+
namespace :bunko do
|
|
7
|
+
include Bunko::RakeHelpers
|
|
8
|
+
|
|
9
|
+
desc "Set up Bunko by creating all configured PostTypes and Collections"
|
|
10
|
+
task setup: :environment do
|
|
11
|
+
puts "Setting up Bunko..."
|
|
12
|
+
puts ""
|
|
13
|
+
|
|
14
|
+
post_types = Bunko.configuration.post_types
|
|
15
|
+
collections = Bunko.configuration.collections
|
|
16
|
+
allow_static_pages = Bunko.configuration.allow_static_pages
|
|
17
|
+
|
|
18
|
+
if post_types.empty? && !allow_static_pages
|
|
19
|
+
puts "⚠️ No post types configured and static pages are disabled."
|
|
20
|
+
puts " Either enable static pages or add post types to config/initializers/bunko.rb"
|
|
21
|
+
puts ""
|
|
22
|
+
puts " Example:"
|
|
23
|
+
puts " config.allow_static_pages = true"
|
|
24
|
+
puts " # OR"
|
|
25
|
+
puts " config.post_type \"blog\""
|
|
26
|
+
puts " config.post_type \"docs\" do |type|"
|
|
27
|
+
puts " type.title = \"Documentation\""
|
|
28
|
+
puts " end"
|
|
29
|
+
exit
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Generate shared partials once
|
|
33
|
+
puts "Generating shared partials..."
|
|
34
|
+
generate_shared_nav
|
|
35
|
+
generate_shared_styles
|
|
36
|
+
generate_shared_footer
|
|
37
|
+
puts ""
|
|
38
|
+
|
|
39
|
+
# Set up static pages if enabled
|
|
40
|
+
if allow_static_pages
|
|
41
|
+
puts "Setting up static pages..."
|
|
42
|
+
setup_static_pages
|
|
43
|
+
puts ""
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Add all post types
|
|
47
|
+
post_types.each do |pt_config|
|
|
48
|
+
Rake::Task["bunko:add"].reenable
|
|
49
|
+
Rake::Task["bunko:add"].invoke(pt_config[:name])
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Add all collections
|
|
53
|
+
collections.each do |collection_config|
|
|
54
|
+
Rake::Task["bunko:add"].reenable
|
|
55
|
+
Rake::Task["bunko:add"].invoke(collection_config[:name])
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
puts "=" * 79
|
|
59
|
+
puts "Setup complete!"
|
|
60
|
+
puts ""
|
|
61
|
+
puts "Next steps:"
|
|
62
|
+
puts " 1. Create your first post in the Rails console or admin panel"
|
|
63
|
+
puts " 2. Visit your collections:"
|
|
64
|
+
|
|
65
|
+
# Show PostType routes
|
|
66
|
+
post_types.each do |pt|
|
|
67
|
+
url_path = pt[:name].tr("_", "-")
|
|
68
|
+
puts " http://localhost:3000/#{url_path}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Show Collection routes
|
|
72
|
+
collections.each do |c|
|
|
73
|
+
url_path = c[:name].tr("_", "-")
|
|
74
|
+
puts " http://localhost:3000/#{url_path} (collection: #{c[:post_types].join(", ")})"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
puts "=" * 79
|
|
78
|
+
puts ""
|
|
79
|
+
puts "To add more later, update your initializer and run:"
|
|
80
|
+
puts " rails bunko:add[name]"
|
|
81
|
+
puts "=" * 79
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Helper methods
|
|
85
|
+
|
|
86
|
+
def generate_shared_nav
|
|
87
|
+
shared_dir = Rails.root.join("app/views/shared")
|
|
88
|
+
nav_file = shared_dir.join("_bunko_nav.html.erb")
|
|
89
|
+
|
|
90
|
+
if File.exist?(nav_file)
|
|
91
|
+
puts " - _bunko_nav.html.erb already exists (skipped)"
|
|
92
|
+
return false
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
FileUtils.mkdir_p(shared_dir)
|
|
96
|
+
|
|
97
|
+
nav_content = render_template("views/layouts/bunko_nav.html.erb.tt", {})
|
|
98
|
+
File.write(nav_file, nav_content)
|
|
99
|
+
|
|
100
|
+
puts " ✓ Created shared/_bunko_nav.html.erb"
|
|
101
|
+
true
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def generate_shared_styles
|
|
105
|
+
shared_dir = Rails.root.join("app/views/shared")
|
|
106
|
+
styles_file = shared_dir.join("_bunko_styles.html.erb")
|
|
107
|
+
|
|
108
|
+
if File.exist?(styles_file)
|
|
109
|
+
puts " - _bunko_styles.html.erb already exists (skipped)"
|
|
110
|
+
return false
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
FileUtils.mkdir_p(shared_dir)
|
|
114
|
+
|
|
115
|
+
styles_content = render_template("views/layouts/bunko_styles.html.erb.tt", {})
|
|
116
|
+
File.write(styles_file, styles_content)
|
|
117
|
+
|
|
118
|
+
puts " ✓ Created shared/_bunko_styles.html.erb"
|
|
119
|
+
true
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def generate_shared_footer
|
|
123
|
+
shared_dir = Rails.root.join("app/views/shared")
|
|
124
|
+
footer_file = shared_dir.join("_bunko_footer.html.erb")
|
|
125
|
+
|
|
126
|
+
if File.exist?(footer_file)
|
|
127
|
+
puts " - _bunko_footer.html.erb already exists (skipped)"
|
|
128
|
+
return false
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
FileUtils.mkdir_p(shared_dir)
|
|
132
|
+
|
|
133
|
+
footer_content = render_template("views/layouts/bunko_footer.html.erb.tt", {})
|
|
134
|
+
File.write(footer_file, footer_content)
|
|
135
|
+
|
|
136
|
+
puts " ✓ Created shared/_bunko_footer.html.erb"
|
|
137
|
+
true
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def setup_static_pages
|
|
141
|
+
# Create "pages" PostType in database
|
|
142
|
+
PostType.find_or_create_by!(name: "pages") do |pt|
|
|
143
|
+
pt.title = "Pages"
|
|
144
|
+
end
|
|
145
|
+
puts " ✓ Created 'pages' PostType in database"
|
|
146
|
+
|
|
147
|
+
# Generate PagesController
|
|
148
|
+
generate_pages_controller
|
|
149
|
+
|
|
150
|
+
# Generate pages/show.html.erb view
|
|
151
|
+
generate_pages_show_view
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def generate_pages_controller
|
|
155
|
+
controller_path = Rails.root.join("app/controllers/pages_controller.rb")
|
|
156
|
+
|
|
157
|
+
if File.exist?(controller_path)
|
|
158
|
+
puts " - pages_controller.rb already exists (skipped)"
|
|
159
|
+
return false
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
controller_content = render_template("controllers/pages_controller.rb.tt", {})
|
|
163
|
+
File.write(controller_path, controller_content)
|
|
164
|
+
|
|
165
|
+
puts " ✓ Created app/controllers/pages_controller.rb"
|
|
166
|
+
true
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def generate_pages_show_view
|
|
170
|
+
views_dir = Rails.root.join("app/views/pages")
|
|
171
|
+
show_file = views_dir.join("show.html.erb")
|
|
172
|
+
|
|
173
|
+
if File.exist?(show_file)
|
|
174
|
+
puts " - pages/show.html.erb already exists (skipped)"
|
|
175
|
+
return false
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
FileUtils.mkdir_p(views_dir)
|
|
179
|
+
|
|
180
|
+
show_content = render_template("views/pages/show.html.erb.tt", {})
|
|
181
|
+
File.write(show_file, show_content)
|
|
182
|
+
|
|
183
|
+
puts " ✓ Created app/views/pages/show.html.erb"
|
|
184
|
+
true
|
|
185
|
+
end
|
|
186
|
+
end
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bunko
|
|
4
|
+
# Simple sample data generator for creating realistic-looking posts
|
|
5
|
+
# No external dependencies - uses built-in Ruby randomization
|
|
6
|
+
module SampleDataGenerator
|
|
7
|
+
# Word pools for generating varied content
|
|
8
|
+
NOUNS = %w[system interface component module feature service platform application framework
|
|
9
|
+
solution architecture database network security authentication authorization deployment
|
|
10
|
+
integration workflow pipeline process functionality capability performance scalability
|
|
11
|
+
infrastructure configuration management monitoring analytics documentation implementation
|
|
12
|
+
optimization validation testing deployment].freeze
|
|
13
|
+
|
|
14
|
+
VERBS = %w[build create develop implement integrate configure optimize enhance improve streamline
|
|
15
|
+
automate manage deploy monitor analyze validate test debug refactor scale maintain
|
|
16
|
+
upgrade migrate extend customize adapt transform modernize accelerate simplify].freeze
|
|
17
|
+
|
|
18
|
+
ADJECTIVES = %w[efficient powerful flexible robust scalable secure reliable fast modern advanced
|
|
19
|
+
comprehensive intuitive seamless integrated automated intelligent dynamic responsive
|
|
20
|
+
innovative cutting-edge enterprise production-ready cloud-native distributed].freeze
|
|
21
|
+
|
|
22
|
+
TECH_TERMS = %w[API REST GraphQL microservice container orchestration Kubernetes Docker CI/CD
|
|
23
|
+
authentication JWT OAuth serverless lambda function middleware cache Redis
|
|
24
|
+
PostgreSQL MongoDB WebSocket HTTP HTTPS SSL TLS encryption algorithm].freeze
|
|
25
|
+
|
|
26
|
+
COMPANIES = %w[TechCorp DataSystems CloudWorks InnovateLabs ScaleUp DevOps Solutions
|
|
27
|
+
Enterprise Digital Ventures Analytics Group Platform Technologies
|
|
28
|
+
NetworkPro SecureBase CodeCraft BuildTools DeployFirst].freeze
|
|
29
|
+
|
|
30
|
+
LOREM_WORDS = %w[lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor
|
|
31
|
+
incididunt ut labore et dolore magna aliqua enim ad minim veniam quis nostrud
|
|
32
|
+
exercitation ullamco laboris nisi aliquip ex ea commodo consequat duis aute
|
|
33
|
+
irure in reprehenderit voluptate velit esse cillum fugiat nulla pariatur
|
|
34
|
+
excepteur sint occaecat cupidatat non proident sunt culpa qui officia deserunt
|
|
35
|
+
mollit anim id est laborum].freeze
|
|
36
|
+
|
|
37
|
+
# Safe external links for sample content
|
|
38
|
+
SAFE_LINKS = [
|
|
39
|
+
{url: "https://github.com/kanejamison/bunko", text: "Bunko on GitHub"},
|
|
40
|
+
{url: "https://rubyonrails.org", text: "Ruby on Rails"},
|
|
41
|
+
{url: "https://www.ruby-lang.org", text: "Ruby Language"},
|
|
42
|
+
{url: "https://rubygems.org", text: "RubyGems"}
|
|
43
|
+
].freeze
|
|
44
|
+
|
|
45
|
+
# Random image dimensions from picsum.photos
|
|
46
|
+
IMAGE_SIZES = [
|
|
47
|
+
{width: 800, height: 400, type: :hero},
|
|
48
|
+
{width: 1200, height: 600, type: :hero},
|
|
49
|
+
{width: 700, height: 500, type: :content},
|
|
50
|
+
{width: 600, height: 400, type: :content},
|
|
51
|
+
{width: 500, height: 350, type: :content},
|
|
52
|
+
{width: 400, height: 300, type: :inline}
|
|
53
|
+
].freeze
|
|
54
|
+
|
|
55
|
+
# Supported content formats
|
|
56
|
+
FORMATS = [:markdown, :html].freeze
|
|
57
|
+
|
|
58
|
+
class << self
|
|
59
|
+
# Generate a random word from various pools
|
|
60
|
+
def word(type = :general)
|
|
61
|
+
case type
|
|
62
|
+
when :noun then NOUNS.sample
|
|
63
|
+
when :verb then VERBS.sample
|
|
64
|
+
when :adjective then ADJECTIVES.sample
|
|
65
|
+
when :tech then TECH_TERMS.sample
|
|
66
|
+
when :company then COMPANIES.sample
|
|
67
|
+
else LOREM_WORDS.sample
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Generate a sentence with specified word count and optional formatting
|
|
72
|
+
def sentence(word_count: nil, capitalize: true, format: :plain)
|
|
73
|
+
# Vary sentence length: 20% short (3-5 words), 80% medium (8-15 words)
|
|
74
|
+
word_count ||= (rand < 0.2) ? rand(3..5) : rand(8..15)
|
|
75
|
+
|
|
76
|
+
words = Array.new(word_count) { LOREM_WORDS.sample }
|
|
77
|
+
sentence = words.join(" ")
|
|
78
|
+
sentence = sentence.capitalize if capitalize
|
|
79
|
+
sentence = "#{sentence}."
|
|
80
|
+
|
|
81
|
+
# Randomly apply inline formatting (30% chance)
|
|
82
|
+
if format != :plain && rand < 0.3
|
|
83
|
+
sentence = apply_inline_formatting(sentence, format)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
sentence
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Generate a paragraph with specified sentence count and optional formatting
|
|
90
|
+
def paragraph(sentence_count: nil, format: :plain)
|
|
91
|
+
# Vary paragraph length: 25% short (1-2 sentences), 75% medium (4-8 sentences)
|
|
92
|
+
sentence_count ||= (rand < 0.25) ? rand(1..2) : rand(4..8)
|
|
93
|
+
|
|
94
|
+
text = Array.new(sentence_count) { sentence(format: format) }.join(" ")
|
|
95
|
+
|
|
96
|
+
# Wrap in paragraph tags for HTML (20% chance for class)
|
|
97
|
+
if format == :html
|
|
98
|
+
css_class = (rand < 0.2) ? " class=\"content-paragraph\"" : ""
|
|
99
|
+
text = "<p#{css_class}>#{text}</p>"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
text
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Generate multiple paragraphs with optional formatting
|
|
106
|
+
def paragraphs(count: 3, target_words: nil, format: :plain)
|
|
107
|
+
paras = if target_words
|
|
108
|
+
# Calculate sentences needed (avg 12 words per sentence)
|
|
109
|
+
sentences_needed = (target_words / 12.0).ceil
|
|
110
|
+
# Group into paragraphs (4-8 sentences each)
|
|
111
|
+
paragraph_count = [(sentences_needed / 6.0).ceil, 1].max
|
|
112
|
+
|
|
113
|
+
Array.new(paragraph_count) do
|
|
114
|
+
sentences_in_paragraph = [sentences_needed / paragraph_count, 1].max
|
|
115
|
+
paragraph(sentence_count: sentences_in_paragraph, format: format)
|
|
116
|
+
end
|
|
117
|
+
else
|
|
118
|
+
Array.new(count) { paragraph(format: format) }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Randomly add special elements (lists, blockquotes, links)
|
|
122
|
+
paras = inject_special_elements(paras, format) if format != :plain
|
|
123
|
+
|
|
124
|
+
paras.join("\n\n")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Generate a random date in the past
|
|
128
|
+
def past_date(years_ago: 2)
|
|
129
|
+
seconds_ago = rand(0..(years_ago * 365 * 24 * 60 * 60))
|
|
130
|
+
Time.now - seconds_ago
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Generate a random date in the future
|
|
134
|
+
def future_date(months_ahead: 3)
|
|
135
|
+
seconds_ahead = rand(0..(months_ahead * 30 * 24 * 60 * 60))
|
|
136
|
+
Time.now + seconds_ahead
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Generate a title
|
|
140
|
+
def title_for(post_type_name)
|
|
141
|
+
[
|
|
142
|
+
"#{VERBS.sample.capitalize} Your #{NOUNS.sample.capitalize} with #{ADJECTIVES.sample.capitalize} #{NOUNS.sample.capitalize}",
|
|
143
|
+
"How to #{VERBS.sample.capitalize} #{ADJECTIVES.sample.capitalize} #{NOUNS.sample.capitalize}",
|
|
144
|
+
"The Complete Guide to #{NOUNS.sample.capitalize} #{NOUNS.sample.capitalize}",
|
|
145
|
+
"Understanding #{ADJECTIVES.sample.capitalize} #{NOUNS.sample.capitalize}",
|
|
146
|
+
"#{rand(5..10)} Ways to #{VERBS.sample.capitalize} Your #{NOUNS.sample.capitalize}",
|
|
147
|
+
"#{ADJECTIVES.sample.capitalize} #{NOUNS.sample.capitalize} for #{NOUNS.sample.capitalize}",
|
|
148
|
+
"#{VERBS.sample.capitalize} #{NOUNS.sample.capitalize} Like a Pro",
|
|
149
|
+
"A Deep Dive into #{ADJECTIVES.sample.capitalize} #{NOUNS.sample.capitalize}"
|
|
150
|
+
].sample
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Generate content structure
|
|
154
|
+
def content_for(post_type_name, target_words:, format: :plain)
|
|
155
|
+
default_content(target_words, format)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
private
|
|
159
|
+
|
|
160
|
+
# Content generator
|
|
161
|
+
def default_content(target_words, format = :plain)
|
|
162
|
+
section_words = target_words / 5
|
|
163
|
+
[
|
|
164
|
+
paragraphs(target_words: section_words * 0.5, format: format),
|
|
165
|
+
format_hero_image(format),
|
|
166
|
+
format_heading("#{ADJECTIVES.sample.capitalize} #{NOUNS.sample.capitalize}", format),
|
|
167
|
+
paragraphs(target_words: section_words * 0.5, format: format),
|
|
168
|
+
format_subheading("Key Points", format),
|
|
169
|
+
paragraphs(target_words: section_words * 0.4, format: format),
|
|
170
|
+
format_heading("#{VERBS.sample.capitalize} #{NOUNS.sample.capitalize}", format),
|
|
171
|
+
paragraphs(target_words: section_words * 0.6, format: format),
|
|
172
|
+
format_heading("#{ADJECTIVES.sample.capitalize} Approach", format),
|
|
173
|
+
paragraphs(target_words: section_words * 0.5, format: format),
|
|
174
|
+
format_subheading("Implementation Details", format),
|
|
175
|
+
paragraphs(target_words: section_words * 0.5, format: format),
|
|
176
|
+
format_heading("Summary", format),
|
|
177
|
+
paragraphs(target_words: section_words * 0.4, format: format)
|
|
178
|
+
].join("\n\n")
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def code_example(format = :plain)
|
|
182
|
+
method = VERBS.sample
|
|
183
|
+
obj = NOUNS.sample
|
|
184
|
+
param = NOUNS.sample
|
|
185
|
+
comment = sentence(word_count: rand(5..8)).chomp(".")
|
|
186
|
+
|
|
187
|
+
code_content = "# #{comment}\n#{obj} = #{obj.capitalize}.new(#{param}: '#{word(:adjective)}')\n#{obj}.#{method}!"
|
|
188
|
+
|
|
189
|
+
case format
|
|
190
|
+
when :markdown
|
|
191
|
+
"```ruby\n#{code_content}\n```"
|
|
192
|
+
when :html
|
|
193
|
+
css_class = (rand < 0.3) ? " class=\"code-block\"" : ""
|
|
194
|
+
"<pre#{css_class}><code>#{code_content}</code></pre>"
|
|
195
|
+
else
|
|
196
|
+
code_content
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Formatting helpers
|
|
201
|
+
def format_heading(text, format)
|
|
202
|
+
if format == :html
|
|
203
|
+
css_class = (rand < 0.3) ? " class=\"section-heading\"" : ""
|
|
204
|
+
"<h2#{css_class}>#{text}</h2>"
|
|
205
|
+
else
|
|
206
|
+
"## #{text}"
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def format_subheading(text, format)
|
|
211
|
+
if format == :html
|
|
212
|
+
css_class = (rand < 0.3) ? " class=\"subsection-heading\"" : ""
|
|
213
|
+
"<h3#{css_class}>#{text}</h3>"
|
|
214
|
+
else
|
|
215
|
+
"### #{text}"
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def apply_inline_formatting(text, format)
|
|
220
|
+
# Pick a random formatting style
|
|
221
|
+
style = [:bold, :italic, :underline].sample
|
|
222
|
+
|
|
223
|
+
# Find words to format (avoid the last word with period)
|
|
224
|
+
words = text.chomp(".").split
|
|
225
|
+
return text if words.length < 3
|
|
226
|
+
|
|
227
|
+
# Pick 1-5 consecutive words to format
|
|
228
|
+
num_words_to_format = [rand(1..5), words.length - 2].min
|
|
229
|
+
start_index = rand(1...(words.length - num_words_to_format))
|
|
230
|
+
words_to_format = words[start_index, num_words_to_format].join(" ")
|
|
231
|
+
|
|
232
|
+
formatted_text = case format
|
|
233
|
+
when :markdown
|
|
234
|
+
case style
|
|
235
|
+
when :bold then "**#{words_to_format}**"
|
|
236
|
+
when :italic then "_#{words_to_format}_"
|
|
237
|
+
when :underline then words_to_format # Markdown doesn't have underline
|
|
238
|
+
end
|
|
239
|
+
when :html
|
|
240
|
+
case style
|
|
241
|
+
when :bold then "<strong>#{words_to_format}</strong>"
|
|
242
|
+
when :italic then "<em>#{words_to_format}</em>"
|
|
243
|
+
when :underline then "<u>#{words_to_format}</u>"
|
|
244
|
+
end
|
|
245
|
+
else
|
|
246
|
+
words_to_format
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Replace the words with formatted version
|
|
250
|
+
words[start_index, num_words_to_format] = [formatted_text]
|
|
251
|
+
"#{words.join(" ")}."
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def inject_special_elements(paragraphs, format)
|
|
255
|
+
return paragraphs if paragraphs.length < 2
|
|
256
|
+
|
|
257
|
+
# Randomly inject a blockquote (20% chance)
|
|
258
|
+
if rand < 0.2
|
|
259
|
+
quote_index = rand(1...paragraphs.length)
|
|
260
|
+
quote_text = sentence(word_count: rand(10..15), format: format)
|
|
261
|
+
paragraphs.insert(quote_index, format_blockquote(quote_text, format))
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Randomly inject a list (30% chance)
|
|
265
|
+
if rand < 0.3
|
|
266
|
+
list_index = rand(1...paragraphs.length)
|
|
267
|
+
paragraphs.insert(list_index, format_list(format))
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Randomly inject a code block (25% chance)
|
|
271
|
+
if rand < 0.25
|
|
272
|
+
code_index = rand(1...paragraphs.length)
|
|
273
|
+
paragraphs.insert(code_index, code_example(format))
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Randomly inject images (50% chance for 1-2 images)
|
|
277
|
+
# Skip first 3 paragraphs to give space after hero image
|
|
278
|
+
if rand < 0.5 && paragraphs.length > 3
|
|
279
|
+
num_images = rand(1..2)
|
|
280
|
+
num_images.times do
|
|
281
|
+
# Start from index 3 to avoid hero image area
|
|
282
|
+
image_index = rand(3...paragraphs.length)
|
|
283
|
+
paragraphs.insert(image_index, format_image(format))
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Randomly inject a link into one paragraph (40% chance)
|
|
288
|
+
if rand < 0.4 && paragraphs.any?
|
|
289
|
+
link_para_index = rand(0...paragraphs.length)
|
|
290
|
+
paragraphs[link_para_index] = inject_link(paragraphs[link_para_index], format)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
paragraphs
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def format_blockquote(text, format)
|
|
297
|
+
case format
|
|
298
|
+
when :markdown
|
|
299
|
+
"> #{text}"
|
|
300
|
+
when :html
|
|
301
|
+
css_class = (rand < 0.3) ? " class=\"content-quote\"" : ""
|
|
302
|
+
"<blockquote#{css_class}>#{text}</blockquote>"
|
|
303
|
+
else
|
|
304
|
+
text
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def format_list(format)
|
|
309
|
+
items = rand(3..5).times.map { "#{VERBS.sample.capitalize} #{NOUNS.sample}" }
|
|
310
|
+
# 50% chance for ordered list, 50% for unordered
|
|
311
|
+
ordered = rand < 0.5
|
|
312
|
+
|
|
313
|
+
case format
|
|
314
|
+
when :markdown
|
|
315
|
+
if ordered
|
|
316
|
+
items.map.with_index(1) { |item, i| "#{i}. #{item}" }.join("\n")
|
|
317
|
+
else
|
|
318
|
+
items.map { |item| "- #{item}" }.join("\n")
|
|
319
|
+
end
|
|
320
|
+
when :html
|
|
321
|
+
css_class = (rand < 0.3) ? " class=\"content-list\"" : ""
|
|
322
|
+
list_items = items.map { |item| "<li>#{item}</li>" }.join("\n")
|
|
323
|
+
if ordered
|
|
324
|
+
"<ol#{css_class}>\n#{list_items}\n</ol>"
|
|
325
|
+
else
|
|
326
|
+
"<ul#{css_class}>\n#{list_items}\n</ul>"
|
|
327
|
+
end
|
|
328
|
+
else
|
|
329
|
+
items.join(", ")
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def inject_link(text, format)
|
|
334
|
+
link_data = SAFE_LINKS.sample
|
|
335
|
+
|
|
336
|
+
case format
|
|
337
|
+
when :markdown
|
|
338
|
+
# Append link after paragraph
|
|
339
|
+
"#{text} Learn more about [#{link_data[:text]}](#{link_data[:url]})."
|
|
340
|
+
when :html
|
|
341
|
+
# Inject link inside the paragraph tag (before closing </p>)
|
|
342
|
+
if text.include?("</p>")
|
|
343
|
+
text.sub("</p>", " Learn more about <a href=\"#{link_data[:url]}\">#{link_data[:text]}</a>.</p>")
|
|
344
|
+
else
|
|
345
|
+
# Fallback for non-paragraph HTML
|
|
346
|
+
"#{text} Learn more about <a href=\"#{link_data[:url]}\">#{link_data[:text]}</a>."
|
|
347
|
+
end
|
|
348
|
+
else
|
|
349
|
+
text
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def format_hero_image(format)
|
|
354
|
+
# Hero images are always the largest sizes
|
|
355
|
+
hero_sizes = IMAGE_SIZES.select { |img| img[:type] == :hero }
|
|
356
|
+
image = hero_sizes.sample
|
|
357
|
+
width = image[:width]
|
|
358
|
+
height = image[:height]
|
|
359
|
+
|
|
360
|
+
# Generate random seed for consistent random image
|
|
361
|
+
seed = rand(1..1000)
|
|
362
|
+
image_url = "https://picsum.photos/#{width}/#{height}?random=#{seed}"
|
|
363
|
+
alt_text = "#{ADJECTIVES.sample.capitalize} #{NOUNS.sample}"
|
|
364
|
+
|
|
365
|
+
case format
|
|
366
|
+
when :markdown
|
|
367
|
+
""
|
|
368
|
+
when :html
|
|
369
|
+
"<img src=\"#{image_url}\" alt=\"#{alt_text}\" width=\"#{width}\" height=\"#{height}\" class=\"hero-image\">"
|
|
370
|
+
else
|
|
371
|
+
""
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def format_image(format)
|
|
376
|
+
# Select a random image size (excluding hero sizes for inline images)
|
|
377
|
+
non_hero_sizes = IMAGE_SIZES.reject { |img| img[:type] == :hero }
|
|
378
|
+
image = non_hero_sizes.sample
|
|
379
|
+
width = image[:width]
|
|
380
|
+
height = image[:height]
|
|
381
|
+
|
|
382
|
+
# Generate random seed for consistent random image
|
|
383
|
+
seed = rand(1..1000)
|
|
384
|
+
image_url = "https://picsum.photos/#{width}/#{height}?random=#{seed}"
|
|
385
|
+
alt_text = "#{ADJECTIVES.sample.capitalize} #{NOUNS.sample}"
|
|
386
|
+
|
|
387
|
+
case format
|
|
388
|
+
when :markdown
|
|
389
|
+
""
|
|
390
|
+
when :html
|
|
391
|
+
css_class = (rand < 0.3) ? " class=\"content-image\"" : ""
|
|
392
|
+
"<img src=\"#{image_url}\" alt=\"#{alt_text}\" width=\"#{width}\" height=\"#{height}\"#{css_class}>"
|
|
393
|
+
else
|
|
394
|
+
""
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
===============================================================================
|
|
2
|
+
|
|
3
|
+
Bunko has been installed!
|
|
4
|
+
|
|
5
|
+
Migrations, models, and initializer have been created.
|
|
6
|
+
|
|
7
|
+
Next steps:
|
|
8
|
+
|
|
9
|
+
1. (Optional) Customize your post types in config/initializers/bunko.rb
|
|
10
|
+
|
|
11
|
+
We've configured "blog" for you, but you can change that or add more:
|
|
12
|
+
|
|
13
|
+
config.post_type "blog" # Title auto-generated as "Blog"
|
|
14
|
+
|
|
15
|
+
config.post_type "docs" do |type|
|
|
16
|
+
type.title = "Documentation"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
config.post_type "changelog"
|
|
20
|
+
|
|
21
|
+
2. Run the migrations:
|
|
22
|
+
|
|
23
|
+
$ rails db:migrate
|
|
24
|
+
|
|
25
|
+
3. Run the setup task:
|
|
26
|
+
|
|
27
|
+
$ rails bunko:setup
|
|
28
|
+
|
|
29
|
+
This will:
|
|
30
|
+
- Create PostTypes from your config
|
|
31
|
+
- Generate controllers for each post type
|
|
32
|
+
- Generate views (index, show) for each post type
|
|
33
|
+
- Add routes for each post type
|
|
34
|
+
|
|
35
|
+
Safe to re-run if you add more types later!
|
|
36
|
+
|
|
37
|
+
4. Create your first post (Rails console or admin panel):
|
|
38
|
+
|
|
39
|
+
blog_type = PostType.find_by(name: "blog")
|
|
40
|
+
Post.create!(
|
|
41
|
+
title: "Welcome to Bunko",
|
|
42
|
+
content: "Your first blog post!",
|
|
43
|
+
post_type: blog_type,
|
|
44
|
+
status: "published",
|
|
45
|
+
published_at: Time.current
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
5. Start your server and visit:
|
|
49
|
+
|
|
50
|
+
http://localhost:3000/blog
|
|
51
|
+
|
|
52
|
+
Want to add more collections later?
|
|
53
|
+
1. Add the post type to config/initializers/bunko.rb
|
|
54
|
+
2. Run either:
|
|
55
|
+
- rails bunko:setup[product] (set up just the new "product" collection)
|
|
56
|
+
- rails bunko:setup (set up all collections, safe to re-run unless you delete any setup files on other models)
|
|
57
|
+
3. Done! Controllers, views, and routes are generated automatically.
|
|
58
|
+
|
|
59
|
+
Need help? Check out the documentation at:
|
|
60
|
+
https://github.com/kanejamison/bunko
|
|
61
|
+
|
|
62
|
+
===============================================================================
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Bunko.configure do |config|
|
|
4
|
+
# Define your post types (use lowercase with underscores)
|
|
5
|
+
# These will be created when you run: rails bunko:setup
|
|
6
|
+
config.post_type "blog" # Title will be auto-generated as "Blog"
|
|
7
|
+
|
|
8
|
+
# Want more? Add additional post types:
|
|
9
|
+
# config.post_type "docs" do |type|
|
|
10
|
+
# type.title = "Documentation" # Custom title (optional)
|
|
11
|
+
# end
|
|
12
|
+
#
|
|
13
|
+
# config.post_type "changelog" # Title: "Changelog"
|
|
14
|
+
#
|
|
15
|
+
# config.post_type "case_studies" do |type|
|
|
16
|
+
# type.title = "Case Studies" # Custom title
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# Note: Names use underscores, URLs automatically use hyphens (/case-studies/)
|
|
20
|
+
|
|
21
|
+
# Smart collections - aggregate or filter posts from multiple post types
|
|
22
|
+
# config.collection "resources", post_types: ["articles", "videos", "tutorials"]
|
|
23
|
+
# config.collection "long_reads" do |c|
|
|
24
|
+
# c.post_types = ["articles", "tutorials"]
|
|
25
|
+
# c.scope = -> { where("word_count > ?", 1500) }
|
|
26
|
+
# end
|
|
27
|
+
|
|
28
|
+
# Enable standalone pages feature (About, Contact, Privacy, etc.)
|
|
29
|
+
# When enabled, rails bunko:setup creates a PagesController and pages PostType
|
|
30
|
+
# Use bunko_page :about in routes to create single-page routes
|
|
31
|
+
# Default: true
|
|
32
|
+
# config.allow_static_pages = true
|
|
33
|
+
|
|
34
|
+
# Reading speed for calculating estimated reading time (in words per minute)
|
|
35
|
+
# Default: 250
|
|
36
|
+
# config.reading_speed = 250
|
|
37
|
+
|
|
38
|
+
# Excerpt length for post.excerpt method (in characters)
|
|
39
|
+
# Default: 160
|
|
40
|
+
# config.excerpt_length = 160
|
|
41
|
+
|
|
42
|
+
# Automatically update word_count when content changes
|
|
43
|
+
# Default: true
|
|
44
|
+
# config.auto_update_word_count = true
|
|
45
|
+
end
|