perron 0.11.0 → 0.13.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: 1757232a5830a600f00f1517b0aa26c0f95cc55005c32cdb7865282b522cefec
4
- data.tar.gz: ba9a09ab06d80c8cc266c210d51770c5577d079dbfab6226f058c1f39a974505
3
+ metadata.gz: 4a0f1b173ae94ac69df1bf9fe680062fa7d07ef6af132677313e80a1d909cb68
4
+ data.tar.gz: 6e6c7865e13a312b6bdac601d6d8878f13136c8d70734ceb5c03fa5ea8d83159
5
5
  SHA512:
6
- metadata.gz: 82e81dbe7b346d47d539ab50b098c1b2542241d8864e3ed0cddf3e565b39785ae6710718a3db158cacaa27e67ea436960b88b012851ec5e85d7c91d3d0baa718
7
- data.tar.gz: bdc619b82756afede00259a7f89970bac482a2e7204f2b8a66ad80b798467604b934462fe69d93e9d70e359abc298b0f72fcbf6842dde0258b7aaabe14a5042c
6
+ metadata.gz: '066983a2578dabbc1d1033f8e9d36065ff03cd9d46fde15cc77348086c114e23c9052aff9e2087bc7e429584bcdb99821bdb1195ba908053f7b5d80966b6133c'
7
+ data.tar.gz: 98eafaeed24b4395c55f4847cf672941f99f551833f15c29cf6a8e3974c8ff0d4da7facc6af87e400e95449b6b76687f3f3c89e14ee3ef60cd1b89583271f0f1
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- perron (0.11.0)
4
+ perron (0.13.0)
5
5
  csv
6
6
  json
7
7
  psych
data/README.md CHANGED
@@ -13,455 +13,9 @@ A Rails-based static site generator.
13
13
  </a>
14
14
 
15
15
 
16
- ## Getting started
16
+ ## Documentation
17
17
 
18
- ### Installation
19
-
20
- Start by adding Perron:
21
- ```bash
22
- bundle add perron
23
- ```
24
-
25
- Then generate the initializer:
26
- ```bash
27
- rails generate perron:install
28
- ```
29
-
30
-
31
- This creates an initializer:
32
- ```ruby
33
- Perron.configure do |config|
34
- config.site_name = "Helptail"
35
- end
36
- ```
37
-
38
-
39
- ## Mode
40
-
41
- Perron can operate in two modes, configured via `config.mode`. This allows you to build either a full static site or integrate pages into a dynamic Rails application.
42
-
43
- | **Mode** | `:standalone` (default) | `:integrated` |
44
- | :--- | :--- | :--- |
45
- | **Use Case** | Full static site for hosts like Netlify/Vercel | Add static pages to a live Rails app |
46
- | **Output** | `output/` directory | `public/` directory |
47
- | **Asset Handling** | Via Perron | Via Asset Pipeline |
48
-
49
-
50
- ## Collections
51
-
52
- Perron is, just like Rails, designed with convention over configuration in mind. Content is stored in `app/content/*/*.{erb,md,*}` and backed by a class, located in `app/models/content/` that inherits from `Perron::Resource`.
53
-
54
- The controllers are located in `app/controllers/content/`. To make them available, create a route: `resources :posts, module: :content, only: %w[index show]`.
55
-
56
-
57
- ### Create content
58
-
59
- ```bash
60
- bin/rails generate content Post
61
- ```
62
-
63
- This will create the following files:
64
-
65
- * `app/models/content/post.rb`
66
- * `app/controllers/content/posts_controller.rb`
67
- * `app/views/content/posts/index.html.erb`
68
- * `app/views/content/posts/show.html.erb`
69
- * Adds route: `resources :posts, module: :content, only: %w[index show]`
70
-
71
-
72
- ### Setting a root page
73
-
74
- To set a root page, include `Perron::Root` in your `Content::PagesController` and add a `app/content/pages/root.{md,erb,*}` file. This is automatically added for you when you create a `Page` collection.
75
-
76
-
77
- ## Markdown support
78
-
79
- Perron supports markdown with the `markdownify` helper.
80
-
81
- There are no markdown gems bundled by default, so you'll need to add one of these to your `Gemfile`:
82
-
83
- - `commonmarker`
84
- - `kramdown`
85
- - `redcarpet`
86
-
87
- ```bash
88
- bundle add {commonmarker,kramdown,redcarpet}
89
- ```
90
-
91
- ### Configuration
92
-
93
- To pass options to the parser, set `markdown_options` in `config/initializers/perron.rb`. The options hash is passed directly to the chosen library.
94
-
95
- **Commonmarker**
96
- ```ruby
97
- # Options are passed as keyword arguments.
98
- Perron.configuration.markdown_options = { options: [:HARDBREAKS], extensions: [:table] }
99
- ```
100
-
101
- **Kramdown**
102
- ```ruby
103
- # Options are passed as a standard hash.
104
- Perron.configuration.markdown_options = { input: "GFM", smart_quotes: "apos,quot" }
105
- ```
106
-
107
- **Redcarpet**
108
- ```ruby
109
- # Options are nested under :renderer_options and :markdown_options.
110
- Perron.configuration.markdown_options = {
111
- renderer_options: { hard_wrap: true },
112
- markdown_options: { tables: true, autolink: true }
113
- }
114
- ```
115
-
116
-
117
- ## HTML transformations
118
-
119
- Perron can post-process the HTML generated from your Markdown content.
120
-
121
-
122
- ### Usage
123
-
124
- Apply transformations by passing an array of processor names or classes to the `markdownify` helper via the `process` option.
125
- ```erb
126
- <%= markdownify @resource.content, process: %w[lazy_load_images syntax_highlight target_blank] %>
127
- ```
128
-
129
-
130
- ### Available processors
131
-
132
- The following processors are built-in and can be activated by passing their string name:
133
-
134
- - `target_blank`: Adds `target="_blank"` to all external links;
135
- - `lazy_load_images`: Adds `loading="lazy"` to all `<img>` tags.
136
- - `syntax_highlight`: Applies syntax highlighting to fenced code blocks (e.g., \`\`\`ruby). This requires adding the `rouge` gem to your Gemfile (`bundle add rouge`). You will also need to include a Rouge CSS theme for colors to appear.
137
-
138
-
139
- ### Creating your own processors
140
-
141
- You can create your own processor by defining a class that inherits from `Perron::HtmlProcessor::Base` and implements a `process` method.
142
- Then, pass the class constant directly in the `process` array.
143
-
144
- ```ruby
145
- # app/processors/add_nofollow_processor.rb
146
- class AddNofollowProcessor < Perron::HtmlProcessor::Base
147
- def process
148
- @html.css("a[target=_blank]").each { it["rel"] = "nofollow" }
149
- end
150
- end
151
- ```
152
-
153
- ```erb
154
- <%= markdownify @resource.content, process: ["target_blank", AddNofollowProcessor] %>
155
- ```
156
-
157
-
158
- ### Embed Ruby
159
-
160
- Perron provides flexible options for embedding dynamic Ruby code in your content using ERB.
161
-
162
-
163
- #### 1. File extension
164
-
165
- Any content file with a `.erb` extension (e.g., `about.erb`) will automatically have its content processed as ERB.
166
-
167
-
168
- #### 2. Frontmatter
169
-
170
- You can enable ERB processing on a per-file basis, even for standard `.md` files, by adding `erb: true` to the file's frontmatter.
171
- ```markdown
172
- ---
173
- title: Dynamic Page
174
- erb: true
175
- ---
176
-
177
- This entire page will be processed by ERB.
178
- The current time is: <%= Time.current.to_fs(:long_ordinal) %>.
179
- ```
180
-
181
-
182
- #### 3. `erbify` helper
183
-
184
- For the most granular control, the `erbify` helper allows to process specific sections of a file as ERB.
185
- This is ideal for generating dynamic content like lists or tables from your resource's metadata, without needing to enable ERB for the entire file. The `erbify` helper can be used with a string or, more commonly, a block.
186
-
187
- **Example:** Generating a list from frontmatter data in a standard `.md` file.
188
- ```markdown
189
- ---
190
- title: Features
191
- features:
192
- - Rails based
193
- - SEO friendly
194
- - Markdown first
195
- - ERB support
196
- ---
197
-
198
- Check out our amazing features:
199
-
200
- <%= erbify do %>
201
- <ul>
202
- <% @resource.metadata.features.each do |feature| %>
203
- <li>
204
- <%= feature %>
205
- </li>
206
- <% end %>
207
- </ul>
208
- <% end %>
209
- ```
210
-
211
-
212
- ## Data files
213
-
214
- Perron can consume structured data from YML, JSON, or CSV files, making them available within your templates.
215
- This is useful for populating features, team members, or any other repeated data structure.
216
-
217
- ### Usage
218
-
219
- To use a data file, instantiate `Perron::Site.data` with the basename of the file and iterate over the result.
220
- ```erb
221
- <% Perron::Site.data.features.each do |feature| %>
222
- <h4><%= feature.name %></h4>
223
- <p><%= feature.description %></p>
224
- <% end %>
225
- ```
226
-
227
- ### File location and formats
228
-
229
- By default, Perron looks up `app/content/data/` for files with a `.yml`, `.json`, or `.csv` extension.
230
- For a `features` call, it would find `features.yml`, `features.json`, or `features.csv`. You can also provide a path to any data file, via `Perron::Data.new("path/to/data.json")`.
231
-
232
- ### Accessing data
233
-
234
- The wrapper object provides flexible, read-only access to each record's attributes. Both dot notation and hash-like key access are supported.
235
- ```ruby
236
- feature.name
237
- feature[:name]
238
- ```
239
-
240
- ### Rendering
241
-
242
- You can render data collections directly using Rails-like partial rendering. When you call `render` on a data collection, Perron will automatically render a partial for each item.
243
- ```erb
244
- <%= render Perron::Site.data.features %>
245
- ```
246
-
247
- This expects a partial at `app/views/features/_feature.html.erb` that will be rendered once for each feature in your data file. The individual record is made available as a local variable matching the singular form of the collection name.
248
- ```erb
249
- <!-- app/views/features/_feature.html.erb -->
250
- <div class="feature">
251
- <h4><%= feature.name %></h4>
252
- <p><%= feature.description %></p>
253
- </div>
254
- ```
255
-
256
-
257
- ## Feeds
258
-
259
- The `feeds` helper automatically generates HTML `<link>` tags for your site's RSS and JSON feeds.
260
-
261
-
262
- ### Usage
263
-
264
- In your layout (e.g., `app/views/layouts/application.html.erb`), add the helper to the `<head>` section:
265
- ```erb
266
- <head>
267
-
268
- <%= feeds %>
269
-
270
- </head>
271
- ```
272
-
273
- To render feeds for specific collections, such as `posts`:
274
- ```erb
275
- <%= feeds only: %w[posts] %>
276
- ```
277
-
278
- Similarly, you can exclude collections:
279
- ```erb
280
- <%= feeds except: %w[pages] %>
281
- ```
282
-
283
-
284
- ### Configuration
285
-
286
- Feeds are configured within the `Resource` class corresponding to a collection:
287
- ```ruby
288
- # app/models/content/post.rb
289
- class Content::Post < Perron::Resource
290
- configure do |config|
291
- config.feeds.rss.enabled = true
292
- # config.feeds.rss.path = "path-to-feed.xml"
293
- # config.feeds.rss.max_items = 25
294
- config.feeds.json.enabled = true
295
- # config.feeds.json.max_items = 15
296
- # config.feeds.json.path = "path-to-feed.json"
297
- end
298
- end
299
- ```
300
-
301
-
302
- ## Metatags
303
-
304
- The `meta_tags` helper automatically generates SEO and social sharing meta tags for your pages.
305
-
306
- ### Usage
307
-
308
- In your layout (e.g., `app/views/layouts/application.html.erb`), add the helper to the `<head>` section:
309
- ```erb
310
- <head>
311
-
312
- <%= meta_tags %>
313
-
314
- </head>
315
- ```
316
-
317
- You can render specific subsets of tags:
318
- ```erb
319
- <%= meta_tags only: %w[title description] %>
320
- ```
321
-
322
- Or exclude certain tags:
323
- ```erb
324
- <%= meta_tags except: %w[twitter_card twitter_image] %>
325
- ```
326
-
327
- ### Priority
328
-
329
- Values are determined with the following precedence, from highest to lowest:
330
-
331
- #### 1. Controller action
332
-
333
- Define a `@metadata` instance variable in your controller:
334
- ```ruby
335
- class Content::PostsController < ApplicationController
336
- def index
337
- @metadata = {
338
- title: "All Blog Posts",
339
- description: "A collection of our articles."
340
- }
341
- @resources = Content::Post.all
342
- end
343
- end
344
- ```
345
-
346
- #### 2. Page frontmatter
347
-
348
- Add values to the YAML frontmatter in content files:
349
- ```yaml
350
- ---
351
- title: My Awesome Post
352
- description: A deep dive into how meta tags work.
353
- image: /assets/images/my-awesome-post.png
354
- author: Kendall
355
- ---
356
-
357
- Your content here…
358
- ```
359
-
360
- #### 3. Collection configuration
361
-
362
- Set collection defaults in the resource model:
363
- ```ruby
364
- class Content::Post < Perron::Resource
365
- # …
366
-
367
- config.metadata.description = "Put your routine tasks on autopilot"
368
- config.metadata.author = "Helptail team"
369
- end
370
- ```
371
-
372
- #### 4. Default values
373
-
374
- Set site-wide defaults in the initializer:
375
- ```ruby
376
- Perron.configure do |config|
377
- # …
378
-
379
- config.metadata.description = "Put your routine tasks on autopilot"
380
- config.metadata.author = "Helptail team"
381
- end
382
- ```
383
-
384
-
385
- ## Related resources
386
-
387
- The `related_resources` method allows to find and display a list of similar resources
388
- from the sme collection. Similarity is calculated using the **[TF-IDF](https://en.wikipedia.org/wiki/Tf%E2%80%93idf)** algorithm on the content of each resource.
389
-
390
-
391
- ### Basic usage
392
-
393
- To get a list of the 5 most similar resources, call the method on any resource instance.
394
- ```ruby
395
- # app/views/content/posts/show.html.erb
396
- @resource.related_resources
397
-
398
- # Just the 3 most similar resources
399
- @resource.related_resources(limit: 3)
400
- ```
401
-
402
-
403
- ## XML sitemap
404
-
405
- A sitemap is a XML file that lists all the pages of a website to help search engines discover and index content more efficiently, typically containing URLs, last modification dates, change frequency, and priority values.
406
-
407
- Enable it with the following line in the Perron configuration:
408
- ```ruby
409
- Perron.configure do |config|
410
- # …
411
- config.sitemap.enabled = true
412
- # config.sitemap.priority = 0.8
413
- # config.sitemap.change_frequency = :monthly
414
- # …
415
- end
416
- ```
417
-
418
- Values can be overridden per collection…
419
- ```ruby
420
- # app/models/content/post.rb
421
- class Content::Post < Perron::Resource
422
- configure do |config|
423
- config.sitemap.enabled = false
424
- config.sitemap.priority = 0.5
425
- config.sitemap.change_frequency = :weekly
426
- end
427
- end
428
- ```
429
-
430
- …or on a resource basis:
431
- ```ruby
432
- # app/content/posts/my-first-post.md
433
- ---
434
- sitemap_priority: 0.25
435
- sitemap_change_frequency: :daily
436
- ---
437
- ```
438
-
439
-
440
- ## Building your static site
441
-
442
- When in `standalone` mode and you're ready to generate your static site, run:
443
- ```bash
444
- RAILS_ENV=production rails perron:build
445
- ```
446
-
447
- This will create your static site in the configured output directory (`output` by default).
448
-
449
-
450
- ## Sites using Perron
451
-
452
- Sites that use Perron.
453
-
454
- ### Standalone (as a SSG)
455
- - [AppRefresher](https://apprefresher.com)
456
- - [Helptail](https://helptail.com)
457
-
458
- ### Integrated (part of a Rails app)
459
- - [Rails Designers (private community for Rails UI engineers)](https://railsdesigners.com)
460
-
461
-
462
- ## Contributing
463
-
464
- This project uses [Standard](https://github.com/testdouble/standard) for formatting Ruby code. Please run `be standardrb` before submitting pull requests. Run tests with `rails test`.
18
+ 📑 [See the docs site (built with Perron!)](https://perron.railsdesigner.com/docs/)
465
19
 
466
20
 
467
21
  ## License
@@ -0,0 +1,16 @@
1
+ Description:
2
+ Creates a new content model with specified actions
3
+
4
+ Example:
5
+ rails generate content post
6
+
7
+ This will create:
8
+ - app/models/content/post.rb
9
+ - app/controllers/content/posts_controller.rb
10
+ - app/views/content/posts/index.html.erb
11
+ - app/views/content/posts/show.html.erb
12
+ - …and add `resource :posts, module: :content, only: %w[index show]` to `config/routes.rb`
13
+
14
+ Arguments:
15
+ NAME: Name of the content model
16
+ actions: List of actions to generate (index or show)
@@ -5,7 +5,9 @@ require "rails/generators/base"
5
5
  class ContentGenerator < Rails::Generators::NamedBase
6
6
  source_root File.expand_path("templates", __dir__)
7
7
 
8
- argument :actions, type: :array, default: %w[index show], desc: "Specify which actions to generate (index/show)"
8
+ class_option :force_plural, type: :boolean, default: false, desc: "Forces the use of a plural model name and class"
9
+
10
+ argument :actions, type: :array, default: %w[index show], banner: "actions", desc: "Specify which actions to generate (index/show)"
9
11
 
10
12
  def create_model
11
13
  template "model.rb.tt", File.join("app/models/content", "#{file_name}.rb")
@@ -35,8 +37,23 @@ class ContentGenerator < Rails::Generators::NamedBase
35
37
  route "resources :#{plural_file_name}, module: :content, only: %w[#{actions.join(" ")}]"
36
38
  end
37
39
 
40
+ def add_root_route
41
+ return unless pages_controller?
42
+ return if root_route_exists?
43
+
44
+ inject_into_file "config/routes.rb", " root to: \"content/pages#show\"\n", before: /^\s*end\s*$/
45
+ end
46
+
38
47
  private
39
48
 
49
+ def file_name
50
+ options[:force_plural] ? super.pluralize : super.singularize
51
+ end
52
+
53
+ def class_name
54
+ options[:force_plural] ? super.pluralize : super.singularize
55
+ end
56
+
40
57
  def view_directory = Rails.root.join("app", "views", "content", plural_file_name)
41
58
 
42
59
  def content_directory = Rails.root.join("app", "content", plural_file_name)
@@ -44,4 +61,12 @@ class ContentGenerator < Rails::Generators::NamedBase
44
61
  def plural_class_name = plural_name.camelize
45
62
 
46
63
  def pages_controller? = plural_file_name == "pages"
64
+
65
+ def root_route_exists?
66
+ routes = Rails.root.join("config", "routes.rb")
67
+
68
+ return false unless File.exist?(routes)
69
+
70
+ File.read(routes).match?(/\broot\s+to:/)
71
+ end
47
72
  end
@@ -4,6 +4,8 @@ module Perron
4
4
  class InstallGenerator < Rails::Generators::Base
5
5
  source_root File.expand_path("templates", __dir__)
6
6
 
7
+ desc "Install Perron in your Rails app"
8
+
7
9
  def copy_initializer
8
10
  template "initializer.rb.tt", "config/initializers/perron.rb"
9
11
  end
@@ -25,5 +27,9 @@ module Perron
25
27
  # gem "redcarpet"
26
28
  RUBY
27
29
  end
30
+
31
+ def gitignore_output_folder
32
+ append_to_file ".gitignore", "/#{Perron.configuration.output}/\n"
33
+ end
28
34
  end
29
35
  end
@@ -17,7 +17,7 @@ To use a data file, you can access it through the `Perron::Site.data` object fol
17
17
 
18
18
  This is a convenient shorthand for `Perron::Data.new("features")`, which can also be used directly:
19
19
  ```ruby
20
- <%% Perron::Data.new("features").each do |feature| %>
20
+ <%% Perron::Data.new("features").each do |feature| %>
21
21
  <h4><%%= feature.name %></h4>
22
22
 
23
23
  <p><%%= feature.description %></p>
@@ -38,3 +38,20 @@ The wrapper object provides flexible, read-only access to each record's attribut
38
38
  feature.name
39
39
  feature[:name]
40
40
  ```
41
+
42
+
43
+ ## Rendering
44
+
45
+ You can render data collections directly using Rails-like partial rendering. When you call `render` on a data collection, Perron will automatically render a partial for each item.
46
+ ```erb
47
+ <%= render Perron::Site.data.features %>
48
+ ```
49
+
50
+ This expects a partial at `app/views/content/features/_feature.html.erb` that will be rendered once for each feature in your data file. The individual record is made available as a local variable matching the singular form of the collection name.
51
+ ```erb
52
+ <!-- app/views/content/features/_feature.html.erb -->
53
+ <div class="feature">
54
+ <h4><%= feature.name %></h4>
55
+ <p><%= feature.description %></p>
56
+ </div>
57
+ ```
@@ -12,7 +12,7 @@ module Perron
12
12
  end
13
13
 
14
14
  def configuration(resource_class = "Content::#{name.classify}".safe_constantize)
15
- resource_class.configuration
15
+ resource_class&.configuration
16
16
  end
17
17
 
18
18
  def all(resource_class = "Content::#{name.classify}".safe_constantize)
data/lib/perron/data.rb CHANGED
@@ -94,6 +94,8 @@ module Perron
94
94
  end
95
95
 
96
96
  def get_binding = binding
97
+
98
+ def default_url_options = Perron.configuration.default_url_options || {}
97
99
  end
98
100
  private_constant :HelperContext
99
101
 
data/lib/perron/feeds.rb CHANGED
@@ -13,6 +13,7 @@ module Perron
13
13
 
14
14
  next if options[:only]&.map(&:to_s)&.exclude?(collection_name)
15
15
  next if options[:except]&.map(&:to_s)&.include?(collection_name)
16
+ next if collection.configuration.blank?
16
17
 
17
18
  collection.configuration.feeds.each do |type, feed|
18
19
  next unless feed.enabled && feed.path && MIME_TYPES.key?(type)
@@ -1,11 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Perron
2
- module SuffixStripping
3
- refine String do
4
- def delete_suffixes(suffixes)
5
- suffixes
6
- .sort_by(&:length)
7
- .reverse_each
8
- .reduce(self, :delete_suffix)
4
+ module Refinements
5
+ module DeleteSuffixes
6
+ refine String do
7
+ def delete_suffixes(suffixes)
8
+ suffixes
9
+ .sort_by(&:length)
10
+ .reverse_each
11
+ .reduce(self, :delete_suffix)
12
+ end
9
13
  end
10
14
  end
11
15
  end
@@ -70,6 +70,7 @@ module Perron
70
70
  @tokenized_content ||= {}
71
71
 
72
72
  return @tokenized_content[target_resource] if @tokenized_content.key?(target_resource)
73
+ return [] if target_resource.content.blank?
73
74
 
74
75
  content = target_resource.content.gsub(/<[^>]*>/, " ")
75
76
  tokens = content.downcase.scan(/\w+/).reject { StopWords.all.include?(it) || it.length < 3 }
@@ -5,7 +5,7 @@ require "perron/refinements/delete_suffixes"
5
5
  module Perron
6
6
  class Resource
7
7
  class Slug
8
- using Perron::SuffixStripping
8
+ using Perron::Refinements::DeleteSuffixes
9
9
 
10
10
  def initialize(resource, frontmatter)
11
11
  @resource = resource
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Perron
4
+ class Resource
5
+ module TableOfContent
6
+ extend ActiveSupport::Concern
7
+
8
+ def table_of_content(levels: %w[h1 h2 h3 h4 h5 h6])
9
+ return [] if content.blank? || metadata.toc == false
10
+
11
+ document = Nokogiri::HTML::DocumentFragment.parse(Markdown.render(content))
12
+ headings = extract_headings from: document, levels: levels.join(", ")
13
+
14
+ Builder.new.build(headings)
15
+ end
16
+ alias_method :table_of_contents, :table_of_content
17
+ alias_method :toc, :table_of_content
18
+
19
+ private
20
+
21
+ Item = ::Data.define(:id, :text, :level, :children)
22
+
23
+ def extract_headings(from:, levels:)
24
+ from.css(levels).each_with_object([]) do |heading, headings|
25
+ heading.tap do |node|
26
+ heading_text = node.text.strip
27
+ id = node["id"] || node.at("a")&.[]("id")
28
+
29
+ next if heading_text.empty? || id.blank?
30
+
31
+ headings << Item.new(
32
+ id: id,
33
+ text: heading_text,
34
+ level: node.name[1..].to_i,
35
+ children: []
36
+ )
37
+ end
38
+ end
39
+ end
40
+
41
+ class Builder
42
+ def build(headings)
43
+ parents = {0 => {children: []}}
44
+
45
+ headings.each_with_object(parents[0][:children]) do |heading, _|
46
+ parents.delete_if { |level, _| level >= heading.level }
47
+
48
+ parent = parents[parents.keys.select { it < heading.level }.max || 0]
49
+
50
+ (parent.is_a?(Hash) ? parent[:children] : parent.children) << heading
51
+
52
+ parents[heading.level] = heading
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -9,6 +9,7 @@ require "perron/resource/related"
9
9
  require "perron/resource/renderer"
10
10
  require "perron/resource/slug"
11
11
  require "perron/resource/separator"
12
+ require "perron/resource/table_of_content"
12
13
 
13
14
  module Perron
14
15
  class Resource
@@ -18,6 +19,7 @@ module Perron
18
19
  include Perron::Resource::Core
19
20
  include Perron::Resource::ClassMethods
20
21
  include Perron::Resource::Publishable
22
+ include Perron::Resource::TableOfContent
21
23
 
22
24
  attr_reader :file_path, :id
23
25
 
@@ -34,14 +36,6 @@ module Perron
34
36
  alias_method :path, :slug
35
37
  alias_method :to_param, :slug
36
38
 
37
- def content
38
- page_content = Perron::Resource::Separator.new(raw_content).content
39
-
40
- return page_content unless erb_processing?
41
-
42
- Perron::Resource::Renderer.erb(page_content, {resource: self})
43
- end
44
-
45
39
  def metadata
46
40
  Perron::Resource::Metadata.new(
47
41
  resource: self,
@@ -53,6 +47,14 @@ module Perron
53
47
  def raw_content = File.read(@file_path)
54
48
  alias_method :raw, :raw_content
55
49
 
50
+ def content
51
+ page_content = Perron::Resource::Separator.new(raw_content).content
52
+
53
+ return Perron::Resource::Renderer.erb(page_content, resource: self) if erb_processing?
54
+
55
+ render_inline_erb using: page_content
56
+ end
57
+
56
58
  def to_partial_path
57
59
  @to_partial_path ||= begin
58
60
  element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(self.class.model_name))
@@ -62,10 +64,6 @@ module Perron
62
64
  end
63
65
  end
64
66
 
65
- def root?
66
- collection.name.inquiry.pages? && File.basename(filename) == "root"
67
- end
68
-
69
67
  def collection = Collection.new(self.class.model_name.collection)
70
68
 
71
69
  def related_resources(limit: 5) = Perron::Site::Resource::Related.new(self).find(limit:)
@@ -77,14 +75,24 @@ module Perron
77
75
  @frontmatter ||= Perron::Resource::Separator.new(raw_content).frontmatter
78
76
  end
79
77
 
80
- def erb_processing?
81
- @file_path.ends_with?(".erb") || metadata.erb == true
82
- end
83
-
84
78
  def generate_id
85
79
  Digest::SHA1.hexdigest(
86
80
  @file_path.delete_prefix(Perron.configuration.input.to_s).parameterize
87
81
  ).first(ID_LENGTH)
88
82
  end
83
+
84
+ def render_inline_erb(using:)
85
+ using.gsub(/<%=\s*erbify\s+do\s*%>(.*?)<%\s*end\s*%>/m) do
86
+ Perron::Resource::Renderer.erb(Regexp.last_match(1).strip_heredoc, resource: self)
87
+ end
88
+ end
89
+
90
+ def erb_processing?
91
+ @file_path.ends_with?(".erb") || metadata.erb == true
92
+ end
93
+
94
+ def root?
95
+ collection.name.inquiry.pages? && File.basename(filename) == "root"
96
+ end
89
97
  end
90
98
  end
@@ -29,7 +29,8 @@ module Perron
29
29
  end
30
30
 
31
31
  FileUtils.mkdir_p(destination)
32
- FileUtils.cp_r(Dir.glob("#{source}/*"), destination)
32
+ FileUtils.move(Dir.glob("#{source}/*"), destination, force: true)
33
+ FileUtils.remove_dir(source)
33
34
 
34
35
  puts " Copied assets to `#{destination.relative_path_from(Rails.root)}`"
35
36
 
@@ -17,15 +17,16 @@ module Perron
17
17
 
18
18
  hash = Rails.application.routes.url_helpers.with_options(@configuration.default_url_options) do |url|
19
19
  {
20
+ generator: "Perron (#{Perron::VERSION})",
20
21
  version: "https://jsonfeed.org/version/1.1",
21
- title: @configuration.site_name,
22
22
  home_page_url: @configuration.url,
23
- description: @configuration.site_description,
23
+ title: feed_configuration.title.presence || @configuration.site_name,
24
+ description: feed_configuration.description.presence || @configuration.site_description,
24
25
  items: resources.map do |resource|
25
26
  {
26
27
  id: resource.id,
27
28
  url: url.polymorphic_url(resource),
28
- date_published: (resource.metadata.published_at || resource.metadata.updated_at)&.iso8601,
29
+ date_published: resource.published_at&.iso8601,
29
30
  title: resource.metadata.title,
30
31
  content_html: Perron::Markdown.render(resource.content)
31
32
  }
@@ -43,8 +44,10 @@ module Perron
43
44
  .reject { it.metadata.feed == false }
44
45
  .sort_by { it.metadata.published_at || it.metadata.updated_at || Time.current }
45
46
  .reverse
46
- .take(@collection.configuration.feeds.json.max_items)
47
+ .take(feed_configuration.max_items)
47
48
  end
49
+
50
+ def feed_configuration = @collection.configuration.feeds.json
48
51
  end
49
52
  end
50
53
  end
@@ -18,17 +18,17 @@ module Perron
18
18
  Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
19
19
  xml.rss(:version => "2.0", "xmlns:atom" => "http://www.w3.org/2005/Atom") do
20
20
  xml.channel do
21
- xml.title @configuration.site_name
22
- xml.description @configuration.site_description
23
- xml.link @configuration.url
24
21
  xml.generator "Perron (#{Perron::VERSION})"
22
+ xml.title feed_configuration.title.presence || @configuration.site_name
23
+ xml.description feed_configuration.description.presence || @configuration.site_description
24
+ xml.link @configuration.url
25
25
 
26
26
  Rails.application.routes.url_helpers.with_options(@configuration.default_url_options) do |url|
27
27
  resources.each do |resource|
28
28
  xml.item do
29
29
  xml.guid resource.id
30
30
  xml.link url.polymorphic_url(resource), isPermaLink: true
31
- xml.pubDate((resource.metadata.published_at || resource.metadata.updated_at)&.rfc822)
31
+ xml.pubDate(resource.published_at&.rfc822)
32
32
  xml.title resource.metadata.title
33
33
  xml.description { xml.cdata(Perron::Markdown.render(resource.content)) }
34
34
  end
@@ -46,8 +46,10 @@ module Perron
46
46
  .reject { it.metadata.feed == false }
47
47
  .sort_by { it.metadata.published_at || it.metadata.updated_at || Time.current }
48
48
  .reverse
49
- .take(@collection.configuration.feeds.rss.max_items)
49
+ .take(feed_configuration.max_items)
50
50
  end
51
+
52
+ def feed_configuration = @collection.configuration.feeds.rss
51
53
  end
52
54
  end
53
55
  end
@@ -13,6 +13,8 @@ module Perron
13
13
 
14
14
  def generate
15
15
  Perron::Site.collections.each do |collection|
16
+ next if collection.configuration.blank?
17
+
16
18
  config = collection.configuration.feeds
17
19
 
18
20
  if config.rss.enabled
@@ -29,10 +29,12 @@ module Perron
29
29
  private
30
30
 
31
31
  def save_html(html)
32
- directory_path = @output_path.join(@path.delete_prefix("/"))
33
- file_path = directory_path.join("index.html")
32
+ prefixless_path = @path.delete_prefix("/")
34
33
 
35
- FileUtils.mkdir_p(directory_path)
34
+ file_path = @output_path.join(prefixless_path)
35
+ file_path = file_path.join("index.html") if File.extname(prefixless_path).empty?
36
+
37
+ FileUtils.mkdir_p(file_path.dirname)
36
38
  File.write(file_path, html)
37
39
 
38
40
  print "\e[32m.\e[0m"
@@ -18,6 +18,10 @@ module Perron
18
18
  next if skip? root
19
19
 
20
20
  @paths << (root ? routes.root_path : routes.public_send(show_path, resource))
21
+
22
+ (resource.class.try(:nested_routes) || []).each do |nested|
23
+ @paths << routes.polymorphic_path([resource, nested])
24
+ end
21
25
  end
22
26
  end
23
27
  end
@@ -25,6 +25,7 @@ module Perron
25
25
  private
26
26
 
27
27
  def add_urls_for(collection, with:)
28
+ return if collection.configuration.blank?
28
29
  return if collection.configuration.sitemap.exclude == true
29
30
 
30
31
  collection.resources.each do |resource|
@@ -39,7 +39,7 @@ module Perron
39
39
  def paths
40
40
  Set.new.tap do |paths|
41
41
  Perron::Site.collections.each { Perron::Site::Builder::Paths.new(it, paths).get }
42
- end.to_a
42
+ end
43
43
  end
44
44
 
45
45
  def render_page(path) = Perron::Site::Builder::Page.new(path).render
@@ -1,3 +1,3 @@
1
1
  module Perron
2
- VERSION = "0.11.0"
2
+ VERSION = "0.13.0"
3
3
  end
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: 0.11.0
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rails Designer Developers
@@ -86,6 +86,7 @@ files:
86
86
  - bin/rails
87
87
  - bin/release
88
88
  - bin/setup
89
+ - lib/generators/content/USAGE
89
90
  - lib/generators/content/content_generator.rb
90
91
  - lib/generators/content/templates/controller.rb.tt
91
92
  - lib/generators/content/templates/index.html.erb.tt
@@ -122,6 +123,7 @@ files:
122
123
  - lib/perron/resource/renderer.rb
123
124
  - lib/perron/resource/separator.rb
124
125
  - lib/perron/resource/slug.rb
126
+ - lib/perron/resource/table_of_content.rb
125
127
  - lib/perron/root.rb
126
128
  - lib/perron/site.rb
127
129
  - lib/perron/site/builder.rb