perron 1.0.0 → 1.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 72473e733aa6ec4313dc71e66b2a949dea6497b427e304cb4656be0d0a5fbc86
4
- data.tar.gz: 37a57b14891cf3fef0dfb1a4c98b414852fdd1bd3f3e377c674e15673f6d2547
3
+ metadata.gz: bdd54bac7e739c0199d3381c1f20aaf15dd5c4dfddec9f57635c0dcf41b66505
4
+ data.tar.gz: c04dc9b074c2a53803086ee6f9a5a7780a26f930661a0884513185323197ba03
5
5
  SHA512:
6
- metadata.gz: 478b181f494c2c0ac54e996c0d9f289c7694bedbcd203fd435c9222ecdd07ad6c5a3f5e8baeb497ee2f77499e3580f0f915439d2feba1edf8ab42385285a7e96
7
- data.tar.gz: 19750396f542ac909322aa5cd3ed247dc10a03509ab521bdb75dd0ae705c502e80b3431737d5c34eb42bcacda17c0ef0c35889cb65561339646f6eb7a2ee1e58
6
+ metadata.gz: 0fdac048810289509dc253a93839fd2df30358a90e463dac6acc3e0cad1171640698e973522d5321b53ca807c56f9a806aeb4bdc972f56773ee0b2693871009b
7
+ data.tar.gz: 8864e0a699f0cf04cc67c696dee53df33e70cd7abe9d57a34b75312613d332c09d0e675fc46a02a52870f30c0a556bb00c98fd7150c8769af5bd1a1ddb79429c
data/Gemfile.lock CHANGED
@@ -1,10 +1,10 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- perron (1.0.0)
4
+ perron (1.1.0)
5
5
  csv
6
6
  json
7
- mata (~> 0.8.0)
7
+ mata (~> 0.10.0)
8
8
  psych
9
9
  rails (>= 7.2.0)
10
10
 
@@ -136,11 +136,10 @@ GEM
136
136
  net-pop
137
137
  net-smtp
138
138
  marcel (1.0.4)
139
- mata (0.8.0)
139
+ mata (0.10.0)
140
140
  listen (~> 3.0)
141
141
  rack (>= 3.0)
142
142
  mini_mime (1.1.5)
143
- mini_portile2 (2.8.9)
144
143
  minitest (5.27.0)
145
144
  net-imap (0.5.9)
146
145
  date
@@ -152,24 +151,21 @@ GEM
152
151
  net-smtp (0.5.1)
153
152
  net-protocol
154
153
  nio4r (2.7.4)
155
- nokogiri (1.18.8)
156
- mini_portile2 (~> 2.8.2)
154
+ nokogiri (1.19.3-aarch64-linux-gnu)
157
155
  racc (~> 1.4)
158
- nokogiri (1.18.8-aarch64-linux-gnu)
156
+ nokogiri (1.19.3-aarch64-linux-musl)
159
157
  racc (~> 1.4)
160
- nokogiri (1.18.8-aarch64-linux-musl)
158
+ nokogiri (1.19.3-arm-linux-gnu)
161
159
  racc (~> 1.4)
162
- nokogiri (1.18.8-arm-linux-gnu)
160
+ nokogiri (1.19.3-arm-linux-musl)
163
161
  racc (~> 1.4)
164
- nokogiri (1.18.8-arm-linux-musl)
162
+ nokogiri (1.19.3-arm64-darwin)
165
163
  racc (~> 1.4)
166
- nokogiri (1.18.8-arm64-darwin)
164
+ nokogiri (1.19.3-x86_64-darwin)
167
165
  racc (~> 1.4)
168
- nokogiri (1.18.8-x86_64-darwin)
166
+ nokogiri (1.19.3-x86_64-linux-gnu)
169
167
  racc (~> 1.4)
170
- nokogiri (1.18.8-x86_64-linux-gnu)
171
- racc (~> 1.4)
172
- nokogiri (1.18.8-x86_64-linux-musl)
168
+ nokogiri (1.19.3-x86_64-linux-musl)
173
169
  racc (~> 1.4)
174
170
  parallel (1.27.0)
175
171
  parser (3.3.8.0)
@@ -5,8 +5,11 @@ module Perron
5
5
  end
6
6
 
7
7
  def run_command
8
- system(params[:command])
8
+ command = params[:command]
9
9
 
10
+ return redirect_back fallback_location: root_path unless command.start_with?("bin/rails generate content")
11
+
12
+ system(command)
10
13
  redirect_back fallback_location: root_path
11
14
  end
12
15
  end
@@ -4,10 +4,11 @@ require "perron/markdown"
4
4
 
5
5
  module Perron
6
6
  module MarkdownHelper
7
- def markdownify(content = nil, process: [], resource: nil, &block)
7
+ def markdownify(content = nil, process: nil, resource: nil, &block)
8
8
  text = block_given? ? capture(&block).strip_heredoc : content
9
+ processors = (process.nil? || process.empty?) ? Perron.configuration.default_processors : process
9
10
 
10
- Perron::Markdown.render(text, processors: process, resource: resource || @resource)
11
+ Perron::Markdown.render(text, processors: processors, resource: resource || @resource)
11
12
  end
12
13
  end
13
14
  end
@@ -2,16 +2,10 @@
2
2
 
3
3
  module Perron
4
4
  module MetaTagsHelper
5
- def meta_tags(options = {}) = Perron::Metatags.new(resource.metadata).render(options)
5
+ def meta_tags(options = {})
6
+ metadata = (@metadata || {}).merge(@resource&.metadata || {})
6
7
 
7
- private
8
-
9
- Resource = Data.define(:path, :collection, :metadata, :published_at)
10
-
11
- def resource
12
- return Resource.new(request.path, nil, @metadata, nil) if @metadata.present?
13
-
14
- @resource || Resource.new(request.path, nil, {}, nil)
8
+ Perron::Metatags.new(metadata).render(options)
15
9
  end
16
10
  end
17
11
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Perron
4
+ module PaginateHelper
5
+ def paginate(scope, page: nil, **options)
6
+ page ||= (params[:page] || 1).to_i
7
+ resource_class = scope.model_class
8
+
9
+ config = resource_class.configuration.pagination
10
+ per_page = options[:per_page] || config.per_page
11
+ page_path_template = options[:path_template] || config.path_template
12
+
13
+ route = find_index_route(resource_class)
14
+ base_path = route_path(route)
15
+
16
+ use_query_params = Rails.env.development? || Rails.env.local?
17
+ paginate = Paginate.new(scope, page: page, per_page: per_page, base_path: base_path, page_path_template: page_path_template, use_query_params: use_query_params)
18
+
19
+ [paginate, paginate.items]
20
+ end
21
+
22
+ private
23
+
24
+ def find_index_route(resource_class)
25
+ controller_name = resource_class.name.demodulize.sub("Controller", "").underscore.pluralize
26
+
27
+ Rails.application.routes.routes.find do |r|
28
+ r.defaults[:controller] == "content/#{controller_name}" &&
29
+ r.defaults[:action] == "index"
30
+ end
31
+ end
32
+
33
+ def route_path(route)
34
+ return "/#{route.name}/" unless route
35
+
36
+ path_spec = route.path.spec.to_s
37
+ path_spec.sub(/\(.*?\)/, "").gsub(/:[^\/]+/, "").sub(/\/$/, "") + "/"
38
+ end
39
+ end
40
+ end
@@ -211,7 +211,7 @@
211
211
  <code>bin/rails generate content Post --new "My first post"</code>
212
212
 
213
213
  <%= form_with url: perron_run_command_path do |form| %>
214
- <%= form.hidden_field :command, value: 'bin/rails generate content Post --new "My first post"' %>
214
+ <%= form.hidden_field :command, value: 'bin/rails generate content Post --new' %>
215
215
 
216
216
  <% if File.file?("app/content/posts/untitled.md") %>
217
217
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm45.66,85.66-56,56a8,8,0,0,1-11.32,0l-24-24a8,8,0,0,1,11.32-11.32L112,148.69l50.34-50.35a8,8,0,0,1,11.32,11.32Z"/></svg>
@@ -16,7 +16,7 @@ module Perron
16
16
  end
17
17
 
18
18
  def all(resource_class = "Content::#{name.classify}".safe_constantize)
19
- Perron::Relation.new(load_resources(resource_class).select(&:published?))
19
+ Perron::Relation.new(load_resources(resource_class).select(&:published?), resource_class)
20
20
  end
21
21
  alias_method :resources, :all
22
22
 
@@ -52,6 +52,7 @@ module Perron
52
52
 
53
53
  Dir.glob("#{@collection_path}/**/*.*")
54
54
  .select { allowed_extensions.include?(File.extname(it)) }
55
+ .reject { File.basename(it, ".*").downcase == "readme" }
55
56
  .map { resource_class.new(it) }
56
57
  end
57
58
  end
@@ -37,6 +37,8 @@ module Perron
37
37
 
38
38
  @config.markdown_options = {}
39
39
 
40
+ @config.default_processors = []
41
+
40
42
  @config.search_scope = []
41
43
 
42
44
  @config.cache_data_sources = false
@@ -51,6 +53,9 @@ module Perron
51
53
 
52
54
  @config.metadata = ActiveSupport::OrderedOptions.new
53
55
  @config.metadata.title_separator = " — "
56
+
57
+ @config.before_build = nil
58
+ @config.after_build = nil
54
59
  end
55
60
 
56
61
  def input = Rails.root.join("app", "content")
@@ -65,6 +70,22 @@ module Perron
65
70
  @additional_routes || (mode.integrated? ? [] : %w[root_path])
66
71
  end
67
72
 
73
+ def deploy
74
+ @deploy ||= ActiveSupport::OrderedOptions.new.tap do |config|
75
+ def config.method_missing(method_name, *args, &block)
76
+ if method_name.to_s.end_with?("=")
77
+ super
78
+ else
79
+ self[method_name] ||= ActiveSupport::OrderedOptions.new
80
+ end
81
+ end
82
+
83
+ def config.respond_to_missing?(method_name, include_private = false)
84
+ !method_name.to_s.end_with?("=") || super
85
+ end
86
+ end
87
+ end
88
+
68
89
  attr_writer :additional_routes
69
90
 
70
91
  def url
@@ -82,8 +103,8 @@ module Perron
82
103
  end
83
104
  end
84
105
 
85
- def respond_to_missing?(method_name)
86
- @config.respond_to?(method_name) || super
106
+ def respond_to_missing?(method_name, ...)
107
+ @config.respond_to?(method_name, ...) || super
87
108
  end
88
109
  end
89
110
  end
@@ -66,11 +66,13 @@ module Perron
66
66
  end
67
67
 
68
68
  data.map.with_index do |item, index|
69
- unless item.is_a?(Hash)
70
- raise Errors::DataParseError, "Item at index #{index} in `#{@file_path}` must be a hash/object, got #{item.class}"
69
+ if item.is_a?(Hash)
70
+ Item.new(item, identifier: @identifier)
71
+ elsif item.is_a?(String)
72
+ item
73
+ else
74
+ raise Errors::DataParseError, "Item at index #{index} in `#{@file_path}` must be a hash/object or string, got #{item.class}"
71
75
  end
72
-
73
- Item.new(item, identifier: @identifier)
74
76
  end
75
77
  end
76
78
 
data/lib/perron/engine.rb CHANGED
@@ -30,17 +30,19 @@ module Perron
30
30
  end
31
31
 
32
32
  initializer "perron.concierge", before: :add_builtin_route do |app|
33
- app.config.after_initialize do
34
- app.routes.append do
35
- namespace :perron do
36
- post :run_command, to: "concierge#run_command"
37
- end
33
+ if Rails.env.development?
34
+ app.config.after_initialize do
35
+ app.routes.append do
36
+ namespace :perron do
37
+ post :run_command, to: "concierge#run_command"
38
+ end
38
39
 
39
- root to: "perron/concierge#show" unless app.routes.named_routes.key?(:root)
40
+ root to: "perron/concierge#show" unless app.routes.named_routes.key?(:root)
41
+ end
40
42
  end
41
- end
42
43
 
43
- app.routes.finalize!
44
+ app.routes.finalize!
45
+ end
44
46
  end
45
47
 
46
48
  initializer "perron.inflections" do
@@ -57,6 +59,7 @@ module Perron
57
59
  load File.expand_path("../tasks/install.rake", __FILE__)
58
60
  load File.expand_path("../tasks/sync_sources.rake", __FILE__)
59
61
  load File.expand_path("../tasks/validate.rake", __FILE__)
62
+ load File.expand_path("../tasks/deploy.rake", __FILE__)
60
63
  end
61
64
  end
62
65
  end
@@ -0,0 +1,15 @@
1
+ # Deploy configuration for Perron
2
+ # Requires Beam Up (`bundle add beam_up`)
3
+ # See also: https://perron.railsdesigner.com/docs/deploy/
4
+
5
+ # Uncomment and configure your provider:
6
+ # provider: netlify
7
+
8
+ # netlify:
9
+ # api_token: your_token_here
10
+ # site_id: your_site_id
11
+
12
+ before_actions:
13
+ - RAILS_ENV=production bundle exec rails perron:build
14
+ after_actions:
15
+ - bundle exec rails perron:clobber
@@ -6,6 +6,9 @@ copy_file "#{__dir__}/install/initializer.rb", "config/initializers/perron.rb"
6
6
  say "Create content data directory"
7
7
  copy_file "#{__dir__}/install/README.md", "app/content/data/README.md"
8
8
 
9
+ say "Create deploy configuration"
10
+ copy_file "#{__dir__}/install/deploy.yml", "config/deploy.yml"
11
+
9
12
  say "Add Markdown gem options to Gemfile"
10
13
  append_to_file "Gemfile", <<~RUBY
11
14
 
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Perron
4
+ class Paginate
5
+ def initialize(collection, page:, per_page:, base_path: nil, page_path_template: nil, use_query_params: false)
6
+ @collection = collection
7
+ @per_page = per_page
8
+ @base_path = base_path
9
+ @page_path_template = page_path_template || "/page/:page/"
10
+ @use_query_params = use_query_params
11
+
12
+ @total_items = collection.size
13
+ @total_pages = total_items.zero? ? 0 : (total_items.to_f / per_page).ceil
14
+ @current_page = page.clamp(1, total_pages.zero? ? 1 : total_pages)
15
+ end
16
+
17
+ attr_reader :current_page, :total_pages, :total_items, :per_page
18
+
19
+ def items
20
+ offset = (@current_page - 1) * @per_page
21
+
22
+ @collection[offset, @per_page] || []
23
+ end
24
+
25
+ def next? = @current_page < @total_pages
26
+
27
+ def previous? = @current_page > 1
28
+
29
+ def next
30
+ return unless next?
31
+
32
+ page_path(@current_page + 1)
33
+ end
34
+
35
+ def previous
36
+ return unless previous?
37
+
38
+ target = (@current_page == 2) ? 1 : @current_page - 1
39
+
40
+ page_path(target)
41
+ end
42
+
43
+ private
44
+
45
+ attr_reader :use_query_params
46
+
47
+ def page_path(number)
48
+ return if number < 1
49
+ return if number > @total_pages && @total_pages > 0
50
+
51
+ return @base_path if number <= 1
52
+
53
+ return "#{@base_path}?page=#{number}" if @use_query_params
54
+
55
+ @base_path.sub(/\/$/, "") + @page_path_template.sub(":page", number.to_s)
56
+ end
57
+ end
58
+ end
@@ -2,9 +2,12 @@
2
2
 
3
3
  module Perron
4
4
  class Relation < Array
5
- def initialize(resources = [])
6
- super
5
+ def initialize(resources = [], model_class = nil)
6
+ super(resources)
7
+
8
+ @model_class = model_class
7
9
  end
10
+ attr_reader :model_class
8
11
 
9
12
  def where(**conditions)
10
13
  filtered = select do |resource|
@@ -19,12 +22,12 @@ module Perron
19
22
  end
20
23
  end
21
24
 
22
- Relation.new(filtered)
25
+ Relation.new(filtered, @model_class)
23
26
  end
24
27
 
25
- def limit(count) = Relation.new(first(count))
28
+ def limit(count) = Relation.new(first(count), @model_class)
26
29
 
27
- def offset(count) = Relation.new(drop(count))
30
+ def offset(count) = Relation.new(drop(count), @model_class)
28
31
 
29
32
  def order(attribute, direction = :asc)
30
33
  if attribute.is_a?(Hash)
@@ -33,7 +36,7 @@ module Perron
33
36
 
34
37
  sorted = sort_by { it.public_send(attribute) }
35
38
 
36
- Relation.new((direction == :desc) ? sorted.reverse : sorted)
39
+ Relation.new((direction == :desc) ? sorted.reverse : sorted, @model_class)
37
40
  end
38
41
 
39
42
  def pluck(*attributes)
@@ -47,5 +50,20 @@ module Perron
47
50
  end
48
51
  end
49
52
  end
53
+
54
+ def in_order_of(attribute, values, filter: true)
55
+ return Relation.new([]) if values.empty?
56
+
57
+ indexed = values.each_with_index.to_h
58
+
59
+ resources = if filter
60
+ select { indexed.key?(it.public_send(attribute)) }
61
+ .sort_by { indexed[it.public_send(attribute)] }
62
+ else
63
+ sort_by { indexed[it.public_send(attribute)] || Float::INFINITY }
64
+ end
65
+
66
+ Relation.new(resources)
67
+ end
50
68
  end
51
69
  end
@@ -22,6 +22,8 @@ module Perron
22
22
 
23
23
  def order(attribute, direction = :asc) = all.order(attribute, direction)
24
24
 
25
+ def in_order_of(attribute, values, filter: true) = all.in_order_of(attribute, values, filter:)
26
+
25
27
  def first(n = nil)
26
28
  n ? all.first(n) : all[0]
27
29
  end
@@ -44,6 +46,10 @@ module Perron
44
46
 
45
47
  def root = all.find(&:root?)
46
48
 
49
+ def destroy_all
50
+ all.each(&:destroy)
51
+ end
52
+
47
53
  def model_name
48
54
  @model_name ||= ActiveModel::Name.new(self, nil, name.demodulize.to_s)
49
55
  end
@@ -31,6 +31,9 @@ module Perron
31
31
  config.related_posts.enabled = false
32
32
  config.related_posts.max = 5
33
33
 
34
+ config.pagination = ActiveSupport::OrderedOptions.new
35
+ config.pagination.path_template = "/page/:page/"
36
+
34
37
  config.sitemap = ActiveSupport::OrderedOptions.new
35
38
  config.sitemap.exclude = false
36
39
  end
@@ -3,22 +3,28 @@
3
3
  module Perron
4
4
  class Resource
5
5
  class Metadata
6
- def initialize(resource:, frontmatter:, collection:)
6
+ def initialize(resource:, frontmatter:, collection:, controller_metadata: {})
7
7
  @resource = resource
8
8
  @frontmatter = frontmatter&.deep_symbolize_keys || {}
9
9
  @collection = collection
10
+ @controller_metadata = controller_metadata
10
11
  @config = Perron.configuration
11
12
  end
12
13
 
13
14
  def data
14
15
  @data ||= ActiveSupport::OrderedOptions
15
16
  .new
16
- .merge(apply_fallbacks_and_defaults(to: merged_site_collection_resource_frontmatter))
17
+ .merge(apply_fallbacks_and_defaults(to: merged_metadata))
17
18
  end
18
19
 
19
20
  private
20
21
 
21
- def merged_site_collection_resource_frontmatter = site_data.merge(collection_data).merge(@frontmatter)
22
+ def merged_metadata
23
+ site_data
24
+ .merge(collection_data)
25
+ .merge(@controller_metadata)
26
+ .merge(@frontmatter)
27
+ end
22
28
 
23
29
  def apply_fallbacks_and_defaults(to:)
24
30
  to[:title] ||= @config.site_name || Rails.application.name.underscore.camelize
@@ -5,6 +5,11 @@ module Perron
5
5
  module Sourceable
6
6
  extend ActiveSupport::Concern
7
7
 
8
+ MODES = {
9
+ single: ->(dataset) { dataset.zip },
10
+ combinations: ->(dataset) { dataset.to_a.combination(2).to_a }
11
+ }
12
+
8
13
  class_methods do
9
14
  def sources(*arguments)
10
15
  @source_definitions = parsed(*arguments)
@@ -32,7 +37,7 @@ module Perron
32
37
  def generate_from_sources!
33
38
  return unless source_backed?
34
39
 
35
- combinations.each do |combo|
40
+ derive.each do |combo|
36
41
  content = content_with combo
37
42
  filename = filename_with combo
38
43
 
@@ -48,7 +53,7 @@ module Perron
48
53
  def parsed(*arguments)
49
54
  return {} if arguments.empty?
50
55
 
51
- arguments.flat_map do |argument|
56
+ definitions = arguments.flat_map do |argument|
52
57
  case argument
53
58
  when Hash
54
59
  argument.to_a
@@ -58,11 +63,39 @@ module Perron
58
63
  [[argument, {primary_key: :id}]]
59
64
  end
60
65
  end.to_h
66
+
67
+ if definitions.values.any? { it[:mode] }
68
+ raise ArgumentError, "mode is only supported for single-source definitions" if definitions.size > 1
69
+ end
70
+
71
+ definitions
61
72
  end
62
73
 
63
- def combinations
74
+ def derive
64
75
  datasets = source_names.map { resolve it }
65
76
 
77
+ if single_source_with_mode?
78
+ derive_from_single_source(datasets.first)
79
+ else
80
+ derive_from_multiple_sources(datasets)
81
+ end
82
+ end
83
+
84
+ def derive_from_single_source(dataset)
85
+ source_name = source_names.first
86
+ mode = source_definitions[source_name][:mode] || :single
87
+ method = MODES[mode.to_sym] || raise(ArgumentError, "Unknown mode: #{mode}")
88
+
89
+ method.call(dataset).each do |combo|
90
+ primary_key = source_definitions[source_name][:primary_key] || :id
91
+
92
+ combo.each do |item|
93
+ raise Errors::DataParseError, "Primary key `#{primary_key}` is nil for row" if item.public_send(primary_key).nil?
94
+ end
95
+ end
96
+ end
97
+
98
+ def derive_from_multiple_sources(datasets)
66
99
  datasets.first.product(*datasets[1..]).each do |combo|
67
100
  combo.each_with_index do |item, index|
68
101
  name = source_names[index]
@@ -74,18 +107,37 @@ module Perron
74
107
  end
75
108
 
76
109
  def content_with(combo)
77
- data = source_names.each.with_index.to_h { |name, index| [name, combo[index]] }
110
+ data = if single_source_with_mode?
111
+ source_name = source_names.first
112
+ names = source_definitions[source_name][:as]&.map(&:to_sym) || (1..combo.size).map { :"#{source_name}_#{it}" }
113
+
114
+ combo.each_with_index.to_h { |item, index| [names[index], item] }
115
+ else
116
+ source_names.each_with_index.to_h { |name, index| [name, combo[index]] }
117
+ end
118
+
78
119
  source = Source.new(data)
79
120
 
80
121
  source_template(source)
81
122
  end
82
123
 
83
124
  def filename_with(combo)
84
- source_names.each_with_index.map do |name, index|
85
- primary_key = source_definitions[name][:primary_key]
125
+ if single_source_with_mode?
126
+ source_name = source_names.first
127
+ primary_key = source_definitions[source_name][:primary_key] || :id
128
+
129
+ combo.map { it.public_send(primary_key) }.join("-")
130
+ else
131
+ source_names.each_with_index.map do |name, index|
132
+ primary_key = source_definitions[name][:primary_key]
133
+
134
+ combo[index].public_send(primary_key)
135
+ end.join("-")
136
+ end
137
+ end
86
138
 
87
- combo[index].public_send(primary_key)
88
- end.join("-")
139
+ def single_source_with_mode?
140
+ source_names.one? && source_definitions[source_names.first][:mode]
89
141
  end
90
142
 
91
143
  def output_dir = Perron.configuration.input.join(model_name.collection)
@@ -96,6 +96,10 @@ module Perron
96
96
  slug == "/"
97
97
  end
98
98
 
99
+ def destroy
100
+ File.delete(@file_path) and self
101
+ end
102
+
99
103
  private
100
104
 
101
105
  ID_LENGTH = 8
@@ -117,6 +121,8 @@ module Perron
117
121
  end
118
122
 
119
123
  def erb_processing?
124
+ return false if metadata.erb == false
125
+
120
126
  @file_path.ends_with?(".erb") || metadata.erb == true
121
127
  end
122
128
  end
@@ -16,7 +16,7 @@
16
16
 
17
17
  <% resources.each do |resource| %>
18
18
  <entry>
19
- <id><%= url_for_resource.call(resource) || "#{@configuration.url}/posts/#{resource.id}" %></id>
19
+ <id><%= url_for_resource.call(resource) %></id>
20
20
  <title><%= resource.metadata.title %></title>
21
21
  <link href="<%= url_for_resource.call(resource) %>" rel="alternate" type="text/html"/>
22
22
  <published><%= resource.published_at&.iso8601 %></published>
@@ -11,13 +11,15 @@ module Perron
11
11
  end
12
12
 
13
13
  def render
14
- action = route_info[:action]
14
+ info = route_info
15
+ return puts " ❌ ERROR: No route matches '#{@path}'" unless info
16
+
15
17
  request = ActionDispatch::Request.new(env)
16
18
  response = ActionDispatch::Response.new
17
19
 
18
- request.path_parameters = route_info
20
+ request.path_parameters = info
19
21
 
20
- controller.dispatch(action, request, response)
22
+ controller.dispatch(info[:action], request, response)
21
23
 
22
24
  return puts " ❌ ERROR: Request failed for '#{@path}' (Status: #{response.status})" unless response.successful?
23
25
 
@@ -41,7 +43,20 @@ module Perron
41
43
  end
42
44
 
43
45
  def route_info
44
- @route_info ||= Rails.application.routes.recognize_path(@path)
46
+ @route_info ||= recognize_path_with_pagination_fallback(@path)
47
+ end
48
+
49
+ def recognize_path_with_pagination_fallback(path)
50
+ Rails.application.routes.recognize_path(path)
51
+ rescue ActionController::RoutingError
52
+ return nil unless (match = path.match(/\/(.+)\/page\/(\d+)\//))
53
+
54
+ base_path = "/#{match[1]}"
55
+ page_number = match[2].to_i
56
+
57
+ Rails.application.routes.recognize_path(base_path).tap do |route_info|
58
+ route_info[:page] = page_number
59
+ end
45
60
  end
46
61
 
47
62
  def env = Rack::MockRequest.env_for(@path, "HTTP_HOST" => Perron.configuration.default_url_options[:host])
@@ -28,12 +28,30 @@ module Perron
28
28
  raise "Route `#{route.name}` (#{route.path.spec}) is an index route but requires parameters #{required_params}. Perron doesn't know how to generate these parameters."
29
29
  end
30
30
 
31
- [routes.public_send("#{route.name}_path")]
31
+ base_path = routes.public_send("#{route.name}_path")
32
+ collection = collection_for(route)
33
+ return [base_path] unless collection
34
+
35
+ pagination = collection.configuration.pagination
36
+ return [base_path] unless pagination.per_page
37
+
38
+ total = collection.all.count
39
+ total_pages = (total.to_f / pagination.per_page).ceil
40
+ return [base_path] if total_pages <= 1
41
+
42
+ [base_path] + (2..total_pages).map do |page_number|
43
+ build_paginated_path(route, page_number, pagination.path_template)
44
+ end
32
45
  when "show" then show_paths_for(route)
33
46
  else []
34
47
  end
35
48
  end
36
49
 
50
+ def build_paginated_path(route, page_number, path_template)
51
+ routes.public_send("#{route.name}_path")
52
+ .sub(/\/$/, "") + path_template.sub(":page", page_number.to_s)
53
+ end
54
+
37
55
  def show_paths_for(route)
38
56
  resources_for(route).reject(&:root?).map do |resource|
39
57
  routes.public_send("#{route.name}_path", resource)
@@ -16,6 +16,8 @@ module Perron
16
16
  end
17
17
 
18
18
  def build
19
+ run_hook(:before_build)
20
+
19
21
  if Perron.configuration.mode.standalone?
20
22
  puts "🧹 Cleaning previous build…"
21
23
 
@@ -35,6 +37,8 @@ module Perron
35
37
  output_preview_urls
36
38
 
37
39
  puts "\n✅ Build complete"
40
+ ensure
41
+ run_hook(:after_build)
38
42
  end
39
43
 
40
44
  private
@@ -58,6 +62,26 @@ module Perron
58
62
  end
59
63
  end
60
64
  end
65
+
66
+ def run_hook(name)
67
+ return unless (hook = Perron.configuration.public_send(name))
68
+
69
+ context = Context.new(
70
+ output_path: @output_path.to_s,
71
+ mode: Perron.configuration.mode
72
+ )
73
+
74
+ hook.call(context)
75
+ end
76
+
77
+ class Context
78
+ attr_reader :output_path, :mode
79
+
80
+ def initialize(output_path:, mode:)
81
+ @output_path = output_path
82
+ @mode = mode
83
+ end
84
+ end
61
85
  end
62
86
  end
63
87
  end
@@ -36,7 +36,11 @@ module Perron
36
36
  def validate_collection(collection)
37
37
  collection.resources.each do |resource|
38
38
  resource.validate ? success : failed(resource)
39
+ rescue Psych::SyntaxError => error
40
+ render_yaml error, resource.file_path
39
41
  end
42
+ rescue Psych::SyntaxError => error
43
+ render_yaml error, "unknown"
40
44
  end
41
45
 
42
46
  def success = print "#{GREEN}.#{RESET}"
@@ -44,13 +48,22 @@ module Perron
44
48
  def failed(resource)
45
49
  print "#{RED}F#{RESET}"
46
50
 
47
- errors = []
51
+ @failures << Failure.new(
52
+ identifier: resource.file_path,
53
+ errors: resource.errors.respond_to?(:full_messages) ? resource.errors.full_messages : []
54
+ )
55
+ end
48
56
 
49
- if resource.respond_to?(:errors) && resource.errors.respond_to?(:full_messages) && resource.errors.any?
50
- errors = resource.errors.full_messages
51
- end
57
+ def render_yaml(error, identifier)
58
+ print "#{RED}F#{RESET}"
59
+
60
+ line_info = error.line ? " at line #{error.line}" : ""
61
+ column_info = error.column ? ", column #{error.column}" : ""
52
62
 
53
- @failures << Failure.new(identifier: resource.file_path, errors: errors)
63
+ @failures << Failure.new(
64
+ identifier: identifier,
65
+ errors: ["Invalid YAML#{line_info}#{column_info}: #{error.problem}"]
66
+ )
54
67
  end
55
68
 
56
69
  def failures_report
@@ -0,0 +1,58 @@
1
+ namespace :perron do
2
+ desc "Deploy static site using Beam Up"
3
+ task deploy: :environment do
4
+ begin
5
+ require "beam_up"
6
+ rescue LoadError
7
+ raise LoadError, <<~MSG
8
+ Beam Up is required for the deploy task to run.
9
+
10
+ Add it to your Gemfile:
11
+ gem "beam_up"
12
+
13
+ Read more: https://perron.railsdesigner.com/docs/deploy/
14
+ MSG
15
+ end
16
+
17
+ config = Rails.root.join("config/deploy.yml")
18
+
19
+ unless config.exist?
20
+ template = File.expand_path("../install/deploy.yml", __dir__)
21
+ FileUtils.cp(template, config)
22
+
23
+ puts "Created config/deploy.yml"
24
+ end
25
+
26
+ beamed = BeamUp.with_progress do
27
+ BeamUp.deploy!(
28
+ Perron.configuration.output,
29
+ config_file: "config/deploy.yml"
30
+ )
31
+ end
32
+
33
+ puts beamed.message
34
+ puts "Deploy ID: #{beamed.deploy_id}" if beamed.deploy_id
35
+ end
36
+
37
+ namespace :deploy do
38
+ desc "Initialize deploy configuration with Beam Up"
39
+ task :init, [:provider] do |task, arguments|
40
+ begin
41
+ require "beam_up"
42
+ rescue LoadError
43
+ raise LoadError, <<~MSG
44
+ Beam Up is required for the deploy task to run.
45
+
46
+ Add it to your Gemfile:
47
+ gem "beam_up"
48
+
49
+ See for more: https://perron.railsdesigner.com/docs/deploy/
50
+ MSG
51
+ end
52
+
53
+ path = BeamUp.init!(arguments[:provider], config_file: "config/deploy.yml")
54
+
55
+ puts "Configured Beam Up in #{path}"
56
+ end
57
+ end
58
+ end
@@ -1,3 +1,3 @@
1
1
  module Perron
2
- VERSION = "1.0.0"
2
+ VERSION = "1.1.0"
3
3
  end
data/lib/perron.rb CHANGED
@@ -6,6 +6,7 @@ require "perron/deprecator"
6
6
  require "perron/errors"
7
7
  require "perron/site"
8
8
  require "perron/relation"
9
+ require "perron/paginate"
9
10
  require "perron/content/data"
10
11
  require "perron/resource"
11
12
  require "perron/markdown"
data/perron.gemspec CHANGED
@@ -19,7 +19,7 @@ Gem::Specification.new do |spec|
19
19
  spec.required_ruby_version = ">= 3.4.0"
20
20
 
21
21
  spec.add_dependency "rails", ">= 7.2.0"
22
- spec.add_dependency "mata", "~> 0.8.0"
22
+ spec.add_dependency "mata", "~> 0.10.0"
23
23
 
24
24
  spec.add_runtime_dependency "csv"
25
25
  spec.add_runtime_dependency "json"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: perron
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rails Designer Developers
@@ -29,14 +29,14 @@ dependencies:
29
29
  requirements:
30
30
  - - "~>"
31
31
  - !ruby/object:Gem::Version
32
- version: 0.8.0
32
+ version: 0.10.0
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: 0.8.0
39
+ version: 0.10.0
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: csv
42
42
  requirement: !ruby/object:Gem::Requirement
@@ -98,6 +98,7 @@ files:
98
98
  - app/helpers/perron/feeds_helper.rb
99
99
  - app/helpers/perron/markdown_helper.rb
100
100
  - app/helpers/perron/meta_tags_helper.rb
101
+ - app/helpers/perron/paginate_helper.rb
101
102
  - app/views/perron/concierge/show.html.erb
102
103
  - bin/console
103
104
  - bin/rails
@@ -133,10 +134,12 @@ files:
133
134
  - lib/perron/html_processor/target_blank.rb
134
135
  - lib/perron/install.rb
135
136
  - lib/perron/install/README.md.tt
137
+ - lib/perron/install/deploy.yml
136
138
  - lib/perron/install/initializer.rb.tt
137
139
  - lib/perron/markdown.rb
138
140
  - lib/perron/metatags.rb
139
141
  - lib/perron/output_server.rb
142
+ - lib/perron/paginate.rb
140
143
  - lib/perron/refinements/delete_suffixes.rb
141
144
  - lib/perron/relation.rb
142
145
  - lib/perron/resource.rb
@@ -180,6 +183,7 @@ files:
180
183
  - lib/perron/site/validate.rb
181
184
  - lib/perron/tasks/build.rake
182
185
  - lib/perron/tasks/clobber.rake
186
+ - lib/perron/tasks/deploy.rake
183
187
  - lib/perron/tasks/install.rake
184
188
  - lib/perron/tasks/sync_sources.rake
185
189
  - lib/perron/tasks/validate.rake
@@ -205,7 +209,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
205
209
  - !ruby/object:Gem::Version
206
210
  version: '0'
207
211
  requirements: []
208
- rubygems_version: 4.0.6
212
+ rubygems_version: 4.0.14
209
213
  specification_version: 4
210
214
  summary: Rails-based static site generator
211
215
  test_files: []