perron 0.18.0 → 1.0.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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/app/controllers/perron/concierge_controller.rb +13 -0
  4. data/app/helpers/perron/markdown_helper.rb +2 -2
  5. data/app/views/perron/concierge/show.html.erb +271 -0
  6. data/lib/generators/rails/content/USAGE +21 -4
  7. data/lib/generators/rails/content/content_generator.rb +16 -12
  8. data/lib/generators/rails/content/templates/controller.rb.tt +6 -0
  9. data/lib/generators/rails/content/templates/model.rb.tt +1 -1
  10. data/lib/perron/assets/icon.png +0 -0
  11. data/lib/perron/assets/icon.svg +1 -0
  12. data/lib/perron/configuration.rb +4 -0
  13. data/lib/perron/data_source/class_methods.rb +8 -0
  14. data/lib/perron/data_source.rb +14 -29
  15. data/lib/perron/development_feed_server.rb +69 -0
  16. data/lib/perron/engine.rb +29 -1
  17. data/lib/perron/errors.rb +2 -0
  18. data/lib/perron/feeds.rb +4 -3
  19. data/lib/perron/html_processor/absolute_urls.rb +27 -0
  20. data/lib/perron/html_processor/base.rb +2 -2
  21. data/lib/perron/html_processor.rb +7 -11
  22. data/lib/{generators/perron/templates → perron/install}/README.md.tt +7 -9
  23. data/lib/perron/install.rb +23 -0
  24. data/lib/perron/markdown.rb +2 -2
  25. data/lib/perron/output_server.rb +9 -0
  26. data/lib/perron/resource/adjacency.rb +70 -0
  27. data/lib/perron/resource/associations.rb +1 -1
  28. data/lib/perron/resource/configuration.rb +9 -4
  29. data/lib/perron/resource/metadata.rb +10 -1
  30. data/lib/perron/resource/publishable.rb +2 -0
  31. data/lib/perron/resource/related.rb +32 -31
  32. data/lib/perron/resource/sourceable.rb +39 -9
  33. data/lib/perron/resource.rb +2 -0
  34. data/lib/perron/site/builder/assets.rb +1 -1
  35. data/lib/perron/site/builder/feeds/atom.erb +44 -0
  36. data/lib/perron/site/builder/feeds/atom.rb +41 -0
  37. data/lib/perron/site/builder/feeds/json.erb +19 -0
  38. data/lib/perron/site/builder/feeds/json.rb +7 -33
  39. data/lib/perron/site/builder/feeds/rss.erb +28 -0
  40. data/lib/perron/site/builder/feeds/rss.rb +6 -28
  41. data/lib/perron/site/builder/feeds/template.rb +63 -0
  42. data/lib/perron/site/builder/feeds.rb +8 -3
  43. data/lib/perron/site/builder/paths.rb +58 -14
  44. data/lib/perron/site/builder/route_resources.rb +79 -0
  45. data/lib/perron/site/builder/sitemap.rb +71 -20
  46. data/lib/perron/site/builder.rb +1 -1
  47. data/lib/perron/site/validate.rb +1 -2
  48. data/lib/perron/site.rb +7 -0
  49. data/lib/perron/tasks/build.rake +6 -7
  50. data/lib/perron/tasks/install.rake +12 -0
  51. data/lib/perron/version.rb +1 -1
  52. metadata +18 -5
  53. data/lib/generators/perron/install_generator.rb +0 -32
  54. data/lib/perron/html_processor/syntax_highlight.rb +0 -32
  55. /data/lib/{generators/perron/templates → perron/install}/initializer.rb.tt +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4b7d197a1f26d5fae6c488fe7be6aef6c3818e408d97b5d789f46111db3207be
4
- data.tar.gz: 0cb809db9260c8e5ded0626f9d790dfb003b937892c1bbce022bcbdac3e5053d
3
+ metadata.gz: 72473e733aa6ec4313dc71e66b2a949dea6497b427e304cb4656be0d0a5fbc86
4
+ data.tar.gz: 37a57b14891cf3fef0dfb1a4c98b414852fdd1bd3f3e377c674e15673f6d2547
5
5
  SHA512:
6
- metadata.gz: cc876ae9e8fb6daf1c6496c381efc05eba46fe5bbff9174502480488c34a4dbf5c94d9a360752942a53c395d7df6cb3b9e8342936e8e795b3ab3ccc20f6b3cdd
7
- data.tar.gz: 99681d91d5d5ea2ffb8488ba4c93c27b2115aebde8132d6f0c8376a6b3e7819c62aef196b424e244af04d8ada7a12080c181cd0f44cf9d52c79125263c3f849b
6
+ metadata.gz: 478b181f494c2c0ac54e996c0d9f289c7694bedbcd203fd435c9222ecdd07ad6c5a3f5e8baeb497ee2f77499e3580f0f915439d2feba1edf8ab42385285a7e96
7
+ data.tar.gz: 19750396f542ac909322aa5cd3ed247dc10a03509ab521bdb75dd0ae705c502e80b3431737d5c34eb42bcacda17c0ef0c35889cb65561339646f6eb7a2ee1e58
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- perron (0.18.0)
4
+ perron (1.0.0)
5
5
  csv
6
6
  json
7
7
  mata (~> 0.8.0)
@@ -0,0 +1,13 @@
1
+ module Perron
2
+ class ConciergeController < ActionController::Base
3
+ def show
4
+ render :show
5
+ end
6
+
7
+ def run_command
8
+ system(params[:command])
9
+
10
+ redirect_back fallback_location: root_path
11
+ end
12
+ end
13
+ end
@@ -4,10 +4,10 @@ require "perron/markdown"
4
4
 
5
5
  module Perron
6
6
  module MarkdownHelper
7
- def markdownify(content = nil, process: [], &block)
7
+ def markdownify(content = nil, process: [], resource: nil, &block)
8
8
  text = block_given? ? capture(&block).strip_heredoc : content
9
9
 
10
- Perron::Markdown.render(text, processors: process)
10
+ Perron::Markdown.render(text, processors: process, resource: resource || @resource)
11
11
  end
12
12
  end
13
13
  end
@@ -0,0 +1,271 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <title>Welcome to Perron</title>
5
+ <meta charset="utf-8">
6
+
7
+ <link rel="icon" href="/icon.png" type="image/png">
8
+ <link rel="apple-touch-icon" href="/icon.png" sizes="512x512">
9
+
10
+ <style>
11
+ * { margin: 0; padding: 0; box-sizing: border-box; }
12
+ ul { list-style: none; }
13
+
14
+ a {
15
+ color: #1f2937;
16
+
17
+ &:hover: { text-decoration: none; }
18
+ }
19
+
20
+ body {
21
+ padding: 2rem 1rem;
22
+ min-height: 100vh;
23
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
24
+ line-height: 1.6;
25
+ color: #1f2937;
26
+ background: linear-gradient(135deg, #f9fafb 0%, #f3f4f6 100%);
27
+ }
28
+
29
+ .hero {
30
+ max-width: 72rem;
31
+ margin: 0 auto 4rem;
32
+ padding: 3rem 0;
33
+
34
+ h1 {
35
+ font-size: clamp(2rem, 5vw, 3.5rem);
36
+ font-weight: 900;
37
+ text-align: center;
38
+ color: #111827;
39
+ letter-spacing: -.025em;
40
+ }
41
+
42
+ ul {
43
+ display: grid;
44
+ row-gap: 1rem;
45
+ margin: 1rem auto 0;
46
+ max-width: 48rem;
47
+ padding: 1rem;
48
+ background: white;
49
+ border-radius: .5rem;
50
+ border: 1px solid #e5e7eb;
51
+
52
+ li {
53
+ border-radius: .5rem;
54
+
55
+ p { font-weight: 600; }
56
+
57
+ div {
58
+ display: grid;
59
+ grid-template-columns: 1fr min-content;
60
+ margin-top: .25rem;
61
+ padding: .75rem 1rem;
62
+ background-color: rgb(248 250 252);
63
+ border-radius: .5rem;
64
+ }
65
+
66
+ code {
67
+ font-size: .875rem;
68
+ font-family: "SF Mono", Monaco, "Cascadia Code", monospace;
69
+ font-weight: 500;
70
+ color: rgb(51 65 85);
71
+ overflow: auto;
72
+ white-space: nowrap;
73
+ user-select: all;
74
+ }
75
+
76
+ svg {
77
+ width: 1.125rem;
78
+ aspect-ratio: 1/1;
79
+ color: rgb(100 116 139);
80
+ }
81
+
82
+ input[type=submit] {
83
+ border: 0;
84
+ padding: .25rem 1rem;
85
+ font-size: .875rem;
86
+ font-weight: 300;
87
+ color: white;
88
+ background-color: rgb(249 115 22);
89
+ border-radius: .25rem;
90
+ transition: background-color 300ms ease;
91
+ cursor: pointer;
92
+
93
+ &:hover {
94
+ background-color: rgb(251 146 60);
95
+ }
96
+ }
97
+ }
98
+ }
99
+ }
100
+
101
+ .info {
102
+ display: grid;
103
+ gap: 2rem;
104
+ max-width: 6xl;
105
+ margin: 0 auto;
106
+ overflow-x: clip;
107
+
108
+ @media (min-width: 768px) {
109
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
110
+ }
111
+
112
+ li {
113
+ &:nth-child(odd) {
114
+ @media (min-width: 640px) {
115
+ transform: rotate(-1deg);
116
+ }
117
+ }
118
+
119
+ &:nth-child(even) {
120
+ @media (min-width: 640px) {
121
+ transform: rotate(1deg);
122
+ }
123
+ }
124
+ }
125
+
126
+ a {
127
+ display: block;
128
+ padding: 2rem 1.5rem;
129
+ text-decoration: none;
130
+ background: white;
131
+ border: 1px solid #e5e7eb;
132
+ border-radius: 1rem;
133
+ transition: all 300ms ease;
134
+
135
+ &:hover {
136
+ transform: translateY(-4px);
137
+ box-shadow: 0 20px 25px -5px rgb(0 0 0 / .1);
138
+ }
139
+
140
+ h2 {
141
+ font-size: 1.25rem;
142
+ font-weight: 700;
143
+ color: #111827;
144
+ }
145
+
146
+ p {
147
+ margin-top: .25rem;
148
+ color: #4b5563;
149
+ line-height: 1.7;
150
+
151
+ a {
152
+ color: #f97316;
153
+ text-decoration: none;
154
+ font-weight: 500;
155
+ transition: color 200ms ease;
156
+
157
+ &:hover {
158
+ color: #ea580c;
159
+ text-decoration: underline;
160
+ }
161
+ }
162
+ }
163
+ }
164
+ }
165
+
166
+ .versions {
167
+ display: flex;
168
+ align-items: center;
169
+ justify-content: center;
170
+ column-gap: 1.5rem;
171
+ margin-top: 2rem;
172
+ font-family: "SF Mono", Monaco, "Cascadia Code", monospace;
173
+ text-align: center;
174
+ color: #6b7280;
175
+ font-size: .75rem;
176
+ }
177
+ </style>
178
+ </head>
179
+
180
+ <body>
181
+ <section class="hero">
182
+ <h1>Welcome to Perron</h1>
183
+
184
+ <ul>
185
+ <li>
186
+ <p>
187
+ Create a collection
188
+ </p>
189
+
190
+ <div>
191
+ <code>bin/rails generate content Post</code>
192
+
193
+ <%= form_with url: perron_run_command_path do |form| %>
194
+ <%= form.hidden_field :command, value: "bin/rails generate content Post" %>
195
+
196
+ <% if defined?(Content::Post) %>
197
+ <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>
198
+ <% else %>
199
+ <%= form.submit "Run" %>
200
+ <% end %>
201
+ <% end %>
202
+ </div>
203
+ </li>
204
+
205
+ <li>
206
+ <p>
207
+ Create your first content
208
+ </p>
209
+
210
+ <div>
211
+ <code>bin/rails generate content Post --new "My first post"</code>
212
+
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"' %>
215
+
216
+ <% if File.file?("app/content/posts/untitled.md") %>
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>
218
+ <% else %>
219
+ <%= form.submit "Run" %>
220
+ <% end %>
221
+ <% end %>
222
+ </div>
223
+ </li>
224
+
225
+ <li>
226
+ <p>
227
+ Run the server, and visit <code><a href="/posts/">http://localhost:3000/posts/</a></code>
228
+ </p>
229
+
230
+ <div>
231
+ <code>bin/dev</code>
232
+ </div>
233
+ </li>
234
+ </ul>
235
+ </section>
236
+
237
+ <ul class="info">
238
+ <li>
239
+ <a href="https://perron.railsdesigner.com/docs/">
240
+ <h2>Documentation</h2>
241
+
242
+ <p>Visit the docs for more</p>
243
+ </a>
244
+ </li>
245
+
246
+ <li>
247
+ <a href="https://perron.railsdesigner.com/library/">
248
+ <h2>Library</h2>
249
+
250
+ <p>Check out the library for snippets, components and templates</p>
251
+ </a>
252
+ </li>
253
+
254
+ <li>
255
+ <a href="https://github.com/Rails-Designer/perron">
256
+ <h2>GitHub</h2>
257
+
258
+ <p>Check out Perron's source</p>
259
+ </a>
260
+ </li>
261
+ </ul>
262
+
263
+ <div class="versions">
264
+ <span>Ruby <%= RUBY_VERSION %></span>
265
+
266
+ <span>Rails <%= Rails.version %></span>
267
+
268
+ <span>Perron <%= Perron::VERSION %></span>
269
+ </div>
270
+ </body>
271
+ </html>
@@ -12,6 +12,7 @@ Options:
12
12
  --force-plural Use plural form for model name and class
13
13
  --[no-]include-root Include root action and route
14
14
  (default: true for pages, false otherwise)
15
+ --inline Render show action inline instead of using view template
15
16
 
16
17
  Examples:
17
18
  Generate basic content scaffold:
@@ -29,6 +30,17 @@ Examples:
29
30
  Adds route:
30
31
  resources :posts, module: :content, only: %w[index show]
31
32
 
33
+ Generate inline show action:
34
+ rails generate content Post --inline
35
+
36
+ Creates controller with inline rendering:
37
+ def show
38
+ @resource = Content::Post.find!(params[:id])
39
+ render @resource.inline
40
+ end
41
+
42
+ Skips creating app/views/content/posts/show.html.erb
43
+
32
44
  Generate pages scaffold with root:
33
45
  rails generate content Page
34
46
 
@@ -48,17 +60,22 @@ Examples:
48
60
  Creates data source files in app/content/data/ and adds
49
61
  .sources and .template_source class methods to the model.
50
62
 
51
- Adds .sources and .template_source class methods to model.
52
-
53
63
  Create new content file from template:
54
64
  rails generate content Post --new
55
65
  rails generate content Post --new "My First Post"
56
66
 
57
67
  Creates a new content file in app/content/posts/ using:
58
- 1. YYYY-MM-DD-template.*.tt (if exists, with date prefix)
59
- 2. template.*.tt (if exists, without date prefix)
68
+ 1. Template file with strftime patterns (e.g., %Y-%m-%d-title.md.tt)
69
+ 2. Template file without strftime patterns (e.g., template.md.tt)
60
70
  3. Empty file with frontmatter dashes (if no template)
61
71
 
72
+ Template filename examples:
73
+ %Y-%m-%d-title.md.tt → 2026-03-10-my-post.md
74
+ %s-title.md.tt → 1741737600-my-post.md
75
+ %d-title.md.tt → 10-my-post.md
76
+ title.md.tt → my-post.md
77
+ %Y-%m.md.tt → 2026-03.md
78
+
62
79
  Template files support ERB:
63
80
  ---
64
81
  title: <%= @title %>
@@ -13,6 +13,7 @@ module Rails
13
13
  desc: "Create a new content file from template instead of generating scaffold"
14
14
  class_option :data, type: :array, default: [], banner: "source1(.ext) source2(.ext)",
15
15
  desc: "Specify data sources with optional extensions (defaults to .yml)"
16
+ class_option :inline, type: :boolean, default: false, desc: "Render show action inline instead of using a view template"
16
17
 
17
18
  argument :actions, type: :array, default: %w[index show], banner: "actions", desc: "Specify which actions to generate (index/show)"
18
19
 
@@ -53,6 +54,8 @@ module Rails
53
54
  empty_directory view_directory
54
55
 
55
56
  actions.each do |action|
57
+ next if action == "show" && options[:inline]
58
+
56
59
  template "#{action}.html.erb.tt", File.join(view_directory, "#{action}.html.erb")
57
60
  end
58
61
  end
@@ -84,15 +87,12 @@ module Rails
84
87
  return if @content_mode
85
88
  return unless should_include_root?
86
89
 
87
- inject_into_class "app/controllers/content/#{plural_file_name}_controller.rb", "Content::#{plural_class_name}Controller" do
88
- <<~RUBY
89
- def root
90
- @resource = Content::#{class_name}.root
90
+ controller_file = "app/controllers/content/#{plural_file_name}_controller.rb"
91
+ return unless File.exist?(File.join(destination_root, controller_file))
91
92
 
92
- render :show
93
- end
94
- RUBY
95
- end
93
+ root_action = " def root\n @resource = Content::#{class_name}.root\n\n render :show\n end\n\n"
94
+
95
+ inject_into_file controller_file, root_action, after: "class Content::#{plural_class_name}Controller < ApplicationController\n"
96
96
  end
97
97
 
98
98
  def create_root_content_file
@@ -129,16 +129,20 @@ module Rails
129
129
  def pages_controller? = plural_file_name == "pages"
130
130
 
131
131
  def template_file
132
- @template_file ||= Dir.glob(File.join(content_directory, "{YYYY-MM-DD-,}template.*.tt")).first
132
+ @template_file ||= Dir.glob(File.join(content_directory, "*.tt")).first
133
133
  end
134
134
 
135
135
  def filename_from_template
136
136
  @filename_from_template ||= begin
137
137
  return "untitled.md" unless template_file
138
138
 
139
- File.basename(template_file, ".tt").tap do |name|
140
- name.gsub!("YYYY-MM-DD", Time.current.strftime("%Y-%m-%d"))
141
- name.sub!("template", @content_title ? @content_title.parameterize : "untitled")
139
+ name = File.basename(template_file, ".tt")
140
+ name = Time.current.strftime(name)
141
+
142
+ if name.include?("title")
143
+ name.sub("title", @content_title ? @content_title.parameterize : "untitled")
144
+ else
145
+ name
142
146
  end
143
147
  end
144
148
  end
@@ -3,11 +3,17 @@ class Content::<%= plural_class_name %>Controller < ApplicationController
3
3
  def index
4
4
  @resources = Content::<%= class_name %>.all
5
5
  end
6
+ <%- if actions.include?("show") -%>
6
7
 
8
+ <%- end -%>
7
9
  <%- end -%>
8
10
  <%- if actions.include?("show") -%>
9
11
  def show
10
12
  @resource = Content::<%= class_name %>.find!(params[:id])
13
+ <%- if options[:inline] -%>
14
+
15
+ render @resource.inline
16
+ <%- end -%>
11
17
  end
12
18
  <%- end -%>
13
19
  end
@@ -2,7 +2,7 @@ class Content::<%= class_name %> < Perron::Resource
2
2
  <% if data_sources? -%>
3
3
  sources <%= data_sources.map { ":#{it}" }.join(", ") %>
4
4
 
5
- def self.source_template(sources)
5
+ def self.source_template(source)
6
6
  <<~MARKDOWN
7
7
  ---
8
8
  ---
Binary file
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="400" height="400" fill="none" viewBox="0 0 400 400"><path fill="#f97316" d="M177.101 221.582a8.59 8.59 0 0 1 8.587-8.587h68.524q23.93 0 40.424-16.458 16.728-16.697 16.728-46.99 0-22.66-17.425-40.549-17.424-18.129-39.727-18.128H126.636c-8.836 0-16 7.163-16 16v244.766a8.364 8.364 0 1 1-16.727 0V89.696c0-8.837 7.164-16 16-16h144.303q29.272 0 51.576 23.137 22.303 23.136 22.303 52.714 0 36.495-21.606 58.678-21.606 21.944-52.273 21.944h-68.524a8.587 8.587 0 0 1-8.587-8.587m20 34.348a8.59 8.59 0 0 1 8.587-8.587h48.524q24.162 0 44.606-11.926 20.678-12.165 33.222-34.825 12.778-22.66 12.778-51.045 0-36.733-27.414-64.88-27.182-28.145-63.192-28.145H93.182c-8.837 0-16 7.163-16 16v299.114a8.364 8.364 0 1 1-16.727 0V55.348c0-8.837 7.163-16 16-16h177.757q28.111 0 52.97 15.266t39.495 40.788q14.868 25.522 14.868 54.145 0 32.44-14.868 59.155-14.87 26.477-39.495 41.265-24.394 14.55-52.97 14.55h-48.524a8.59 8.59 0 0 1-8.587-8.587m20 34.348a8.587 8.587 0 0 1 8.587-8.587h28.524q24.86 0 47.626-10.018 22.768-10.257 39.495-27.431 16.96-17.412 26.95-41.981 9.99-24.806 9.99-52.714 0-33.393-16.96-62.732-16.959-29.578-45.768-46.99-28.575-17.65-61.333-17.651H59.727c-8.836 0-16 7.163-16 16v353.462a8.364 8.364 0 0 1-16.727 0V21c0-8.837 7.163-16 16-16h211.212q37.172 0 69.697 20.036 32.526 19.799 51.808 53.192Q395 111.62 395 149.547q0 33.871-11.384 62.494-11.383 28.385-30.667 47.228-19.283 18.844-44.838 29.339-25.555 10.257-53.899 10.257h-28.524a8.59 8.59 0 0 1-8.587-8.587m-73.01 41.358a8.364 8.364 0 0 1-16.727 0V124.043c0-8.836 7.163-16 16-16h110.848q16.495 0 28.344 12.404 12.08 12.404 12.08 29.1 0 46.274-40.424 46.274h-88.524a8.587 8.587 0 1 1 0-17.174h88.524q11.152 0 17.424-6.44t6.273-22.66q0-10.495-6.97-17.412-6.737-6.918-16.727-6.918h-94.121c-8.837 0-16 7.164-16 16z" style="mix-blend-mode:multiply"/></svg>
@@ -15,6 +15,8 @@ module Perron
15
15
 
16
16
  @config.output = "output"
17
17
 
18
+ @config.output_server_strict = true
19
+
18
20
  @config.mode = :standalone
19
21
 
20
22
  @config.live_reload = false
@@ -37,6 +39,8 @@ module Perron
37
39
 
38
40
  @config.search_scope = []
39
41
 
42
+ @config.cache_data_sources = false
43
+
40
44
  @config.sitemap = ActiveSupport::OrderedOptions.new
41
45
  @config.sitemap.enabled = false
42
46
  @config.sitemap.priority = 0.5
@@ -17,6 +17,14 @@ module Perron
17
17
  all.find { it[:id] == id || it["id"] == id }
18
18
  end
19
19
 
20
+ def find!(id)
21
+ data_source = all.find { it[:id] == id || it["id"] == id }
22
+
23
+ return data_source if data_source
24
+
25
+ raise Errors::DataSourceNotFoundError, "Row not found with id: #{id}"
26
+ end
27
+
20
28
  def count = all.size
21
29
 
22
30
  def first = all.first
@@ -20,6 +20,20 @@ module Perron
20
20
  super(records)
21
21
  end
22
22
 
23
+ def self.all
24
+ identifier = name.to_s.split("::").drop(2).map { it.underscore }.join("/")
25
+ identifier = name.demodulize.underscore if identifier.empty?
26
+
27
+ return cached(identifier) if Perron.configuration.cache_data_sources
28
+
29
+ new(identifier)
30
+ end
31
+
32
+ def self.cached(identifier)
33
+ @_data_sources ||= {}
34
+ @_data_sources[identifier] ||= new(identifier)
35
+ end
36
+
23
37
  def each(&block) = @records.each(&block)
24
38
 
25
39
  def count = @records.count
@@ -59,16 +73,6 @@ module Perron
59
73
  Item.new(item, identifier: @identifier)
60
74
  end
61
75
  end
62
- # def records
63
- # content = rendered_from(@file_path)
64
- # data = parsed_from(content, @file_path)
65
-
66
- # unless data.is_a?(Array)
67
- # raise Errors::DataParseError, "Data in `#{@file_path}` must be an array of objects."
68
- # end
69
-
70
- # data.map { Item.new(it, identifier: @identifier) }
71
- # end
72
76
 
73
77
  def rendered_from(path)
74
78
  raw_content = File.read(path)
@@ -86,16 +90,6 @@ module Perron
86
90
 
87
91
  send(parser_method, content, path)
88
92
  end
89
- # def parsed_from(content, path)
90
- # extension = File.extname(path)
91
- # parser_method = PARSER_METHODS.fetch(extension) do
92
- # raise Errors::UnsupportedDataFormatError, "Unsupported data format: #{extension}"
93
- # end
94
-
95
- # send(parser_method, content)
96
- # rescue Psych::SyntaxError, JSON::ParserError, CSV::MalformedCSVError => error
97
- # raise Errors::DataParseError, "Failed to parse data format in `#{path}`: (#{error.class}) #{error.message}"
98
- # end
99
93
 
100
94
  def render_erb(content) = ERB.new(content).result(HelperContext.instance.get_binding)
101
95
 
@@ -107,9 +101,6 @@ module Perron
107
101
 
108
102
  raise Errors::DataParseError, "Invalid YAML syntax in `#{path}`#{line_info}#{column_info}: #{error.problem}"
109
103
  end
110
- # def parse_yaml(content)
111
- # YAML.safe_load(content, permitted_classes: [Symbol, Time], aliases: true)
112
- # end
113
104
 
114
105
  def parse_json(content, path)
115
106
  JSON.parse(content, symbolize_names: true)
@@ -119,9 +110,6 @@ module Perron
119
110
 
120
111
  raise Errors::DataParseError, "Invalid JSON syntax in `#{path}`#{line_info}: #{error.message}"
121
112
  end
122
- # def parse_json(content)
123
- # JSON.parse(content, symbolize_names: true)
124
- # end
125
113
 
126
114
  def parse_csv(content, path)
127
115
  expected_headers = nil
@@ -148,8 +136,5 @@ module Perron
148
136
 
149
137
  raise Errors::DataParseError, "Malformed CSV in `#{path}`#{line_info}: #{error.message}"
150
138
  end
151
- # def parse_csv(content)
152
- # CSV.new(content, headers: true, header_converters: :symbol).to_a.map(&:to_h)
153
- # end
154
139
  end
155
140
  end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Perron
4
+ class DevelopmentFeedServer
5
+ def initialize(app)
6
+ @app = app
7
+ end
8
+
9
+ def call(environment)
10
+ request = Rack::Request.new(environment)
11
+
12
+ if build_only_path?(request.path_info)
13
+ render_message(request.path_info)
14
+ else
15
+ @app.call(environment)
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def build_only_path?(path)
22
+ sitemap?(path) || feed?(path)
23
+ end
24
+
25
+ def render_message(path)
26
+ content_type = path.end_with?(".json") ? "application/json" : "application/xml"
27
+
28
+ [
29
+ 200,
30
+
31
+ {
32
+ "Content-Type" => "#{content_type}; charset=utf-8",
33
+ "Content-Length" => message(path).bytesize.to_s
34
+ },
35
+
36
+ [message(path)]
37
+ ]
38
+ end
39
+
40
+ def sitemap?(path)
41
+ path.match?(/\/sitemap\.xml$/)
42
+ end
43
+
44
+ def feed?(path)
45
+ feed_paths.any? { path.end_with?("/#{it}") || path == "/#{it}" }
46
+ end
47
+
48
+ def message(path)
49
+ if path.end_with?(".json")
50
+ "{ \"message\": \"This feed is generated during build\" }"
51
+ elsif sitemap?(path)
52
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n <!-- Sitemap is generated during build -->\n</urlset>"
53
+ else
54
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<feed xmlns=\"http://www.w3.org/2005/Atom\">\n <!-- Feed is generated during build -->\n</feed>"
55
+ end
56
+ end
57
+
58
+ def feed_paths
59
+ @feed_paths ||= Perron::Site.collections.flat_map do |collection|
60
+ config = collection.configuration
61
+ next [] unless config && config[:feeds]
62
+
63
+ config[:feeds].values.filter_map do |feed_config|
64
+ feed_config[:path] if feed_config[:enabled]
65
+ end
66
+ end.compact
67
+ end
68
+ end
69
+ end