perron 0.9.1 → 0.11.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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -0
  3. data/Gemfile.lock +3 -1
  4. data/README.md +129 -36
  5. data/app/helpers/meta_tags_helper.rb +1 -1
  6. data/app/helpers/perron/erb_helper.rb +9 -0
  7. data/lib/generators/content/content_generator.rb +2 -0
  8. data/lib/generators/content/templates/root.erb.tt +0 -1
  9. data/lib/generators/perron/install_generator.rb +13 -0
  10. data/lib/generators/perron/templates/initializer.rb.tt +9 -6
  11. data/lib/perron/{site/collection.rb → collection.rb} +7 -1
  12. data/lib/perron/configuration.rb +10 -3
  13. data/lib/perron/data/proxy.rb +47 -0
  14. data/lib/perron/{site/data.rb → data.rb} +36 -16
  15. data/lib/perron/html_processor/syntax_highlight.rb +30 -0
  16. data/lib/perron/html_processor.rb +12 -8
  17. data/lib/perron/markdown.rb +6 -5
  18. data/lib/perron/metatags.rb +33 -59
  19. data/lib/perron/{site/resource → resource}/class_methods.rb +5 -1
  20. data/lib/perron/resource/metadata.rb +67 -0
  21. data/lib/perron/{site/resource → resource}/publishable.rb +5 -5
  22. data/lib/perron/{site/resource → resource}/related.rb +1 -1
  23. data/lib/perron/resource/renderer.rb +18 -0
  24. data/lib/perron/{site/resource → resource}/separator.rb +5 -5
  25. data/lib/perron/resource/slug.rb +27 -0
  26. data/lib/perron/resource.rb +90 -0
  27. data/lib/perron/root.rb +1 -1
  28. data/lib/perron/site/builder/assets.rb +2 -2
  29. data/lib/perron/site/builder/page.rb +1 -1
  30. data/lib/perron/site/builder/public_files.rb +1 -1
  31. data/lib/perron/site/builder.rb +2 -4
  32. data/lib/perron/site.rb +3 -14
  33. data/lib/perron/version.rb +1 -1
  34. data/lib/perron.rb +1 -0
  35. data/perron.gemspec +1 -1
  36. metadata +19 -15
  37. data/lib/perron/site/data/proxy.rb +0 -17
  38. data/lib/perron/site/resource/slug.rb +0 -20
  39. data/lib/perron/site/resource.rb +0 -68
  40. /data/lib/perron/{site/resource → resource}/configuration.rb +0 -0
  41. /data/lib/perron/{site/resource → resource}/core.rb +0 -0
  42. /data/lib/perron/{site/resource → resource}/related/stop_words.rb +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cce790442290a0fedc4e4db328e3358b60a7edafafc6a0c980cec22d6f02fba5
4
- data.tar.gz: c0a48eecc48c7a3230de6c36bf3a9180c9125b389282b0f311c02638b2db41cf
3
+ metadata.gz: 1757232a5830a600f00f1517b0aa26c0f95cc55005c32cdb7865282b522cefec
4
+ data.tar.gz: ba9a09ab06d80c8cc266c210d51770c5577d079dbfab6226f058c1f39a974505
5
5
  SHA512:
6
- metadata.gz: e3f7bce10120b2feaf667051c8b67efd58adffb83a91d7d853b79b408bfa04cf5e004f40e99357059871c51f42aaf8ee26d32e4b36b776fcb9936309d1a7e94a
7
- data.tar.gz: 9324e15f3d236809cf9422da8e8500d302e64a980352c5a9163734d908f8ecd1774cd1350d762487e9795df887d026858e3bd3bcab440c8f9fb1c547913295d9
6
+ metadata.gz: 82e81dbe7b346d47d539ab50b098c1b2542241d8864e3ed0cddf3e565b39785ae6710718a3db158cacaa27e67ea436960b88b012851ec5e85d7c91d3d0baa718
7
+ data.tar.gz: bdc619b82756afede00259a7f89970bac482a2e7204f2b8a66ad80b798467604b934462fe69d93e9d70e359abc298b0f72fcbf6842dde0258b7aaabe14a5042c
data/Gemfile CHANGED
@@ -14,4 +14,5 @@ group :development, :test do
14
14
  gem "rake", "~> 13.3.0"
15
15
  gem "minitest", "~> 5.25", ">= 5.25.5"
16
16
  gem "debug", "~> 1.11.0"
17
+ gem "rouge", "~> 4.6.0"
17
18
  end
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- perron (0.9.1)
4
+ perron (0.11.0)
5
5
  csv
6
6
  json
7
7
  psych
@@ -204,6 +204,7 @@ GEM
204
204
  regexp_parser (2.10.0)
205
205
  reline (0.6.1)
206
206
  io-console (~> 0.5)
207
+ rouge (4.6.0)
207
208
  rubocop (1.75.8)
208
209
  json (~> 2.3)
209
210
  language_server-protocol (~> 3.17.0.2)
@@ -267,6 +268,7 @@ DEPENDENCIES
267
268
  perron!
268
269
  rails (~> 7.2.0)
269
270
  rake (~> 13.3.0)
271
+ rouge (~> 4.6.0)
270
272
  standard (~> 1.50.0)
271
273
 
272
274
  BUNDLED WITH
data/README.md CHANGED
@@ -13,7 +13,7 @@ A Rails-based static site generator.
13
13
  </a>
14
14
 
15
15
 
16
- ## Getting Started
16
+ ## Getting started
17
17
 
18
18
  ### Installation
19
19
 
@@ -31,7 +31,7 @@ rails generate perron:install
31
31
  This creates an initializer:
32
32
  ```ruby
33
33
  Perron.configure do |config|
34
- config.site_name = "AppRefresher"
34
+ config.site_name = "Helptail"
35
35
  end
36
36
  ```
37
37
 
@@ -47,14 +47,14 @@ Perron can operate in two modes, configured via `config.mode`. This allows you t
47
47
  | **Asset Handling** | Via Perron | Via Asset Pipeline |
48
48
 
49
49
 
50
- ## Creating Content
50
+ ## Collections
51
51
 
52
- Perron is, just like Rails, designed with convention over configuration in mind. Content is stored in `app/content/*/*.{erb,md}`. Content is backed by a class, located in `app/models/content/` that inherits from `Perron::Resource`.
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
53
 
54
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
55
 
56
56
 
57
- ### Collections
57
+ ### Create content
58
58
 
59
59
  ```bash
60
60
  bin/rails generate content Post
@@ -69,29 +69,52 @@ This will create the following files:
69
69
  * Adds route: `resources :posts, module: :content, only: %w[index show]`
70
70
 
71
71
 
72
- ### Setting a Root Page
72
+ ### Setting a root page
73
73
 
74
- To set a root page, include `Perron::Root` in your `Content::PagesController` and add a `app/content/pages/root.[md,erb]` file (make sure to set `slug: "/"` in its frontmatter).
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
75
 
76
- This is automatically added when you create a `Page` collection.
77
76
 
78
-
79
- ## Markdown Support
77
+ ## Markdown support
80
78
 
81
79
  Perron supports markdown with the `markdownify` helper.
82
80
 
83
- There are no markdown gems bundled by default, so you'll need to add one of these:
81
+ There are no markdown gems bundled by default, so you'll need to add one of these to your `Gemfile`:
84
82
 
85
- - CommonMarker
86
- - Kramdown
87
- - Redcarpet
83
+ - `commonmarker`
84
+ - `kramdown`
85
+ - `redcarpet`
88
86
 
89
87
  ```bash
90
88
  bundle add {commonmarker,kramdown,redcarpet}
91
89
  ```
92
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
+
93
116
 
94
- ## HTML Transformations
117
+ ## HTML transformations
95
118
 
96
119
  Perron can post-process the HTML generated from your Markdown content.
97
120
 
@@ -100,19 +123,20 @@ Perron can post-process the HTML generated from your Markdown content.
100
123
 
101
124
  Apply transformations by passing an array of processor names or classes to the `markdownify` helper via the `process` option.
102
125
  ```erb
103
- <%= markdownify @resource.content, process: %w[target_blank lazy_load_images] %>
126
+ <%= markdownify @resource.content, process: %w[lazy_load_images syntax_highlight target_blank] %>
104
127
  ```
105
128
 
106
129
 
107
- ### Available Processors
130
+ ### Available processors
108
131
 
109
132
  The following processors are built-in and can be activated by passing their string name:
110
133
 
111
134
  - `target_blank`: Adds `target="_blank"` to all external links;
112
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.
113
137
 
114
138
 
115
- ### Creating Your Own
139
+ ### Creating your own processors
116
140
 
117
141
  You can create your own processor by defining a class that inherits from `Perron::HtmlProcessor::Base` and implements a `process` method.
118
142
  Then, pass the class constant directly in the `process` array.
@@ -131,7 +155,61 @@ end
131
155
  ```
132
156
 
133
157
 
134
- ## Data Files
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
135
213
 
136
214
  Perron can consume structured data from YML, JSON, or CSV files, making them available within your templates.
137
215
  This is useful for populating features, team members, or any other repeated data structure.
@@ -146,12 +224,12 @@ To use a data file, instantiate `Perron::Site.data` with the basename of the fil
146
224
  <% end %>
147
225
  ```
148
226
 
149
- ### File Location and Formats
227
+ ### File location and formats
150
228
 
151
229
  By default, Perron looks up `app/content/data/` for files with a `.yml`, `.json`, or `.csv` extension.
152
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")`.
153
231
 
154
- ### Accessing Data
232
+ ### Accessing data
155
233
 
156
234
  The wrapper object provides flexible, read-only access to each record's attributes. Both dot notation and hash-like key access are supported.
157
235
  ```ruby
@@ -159,6 +237,22 @@ feature.name
159
237
  feature[:name]
160
238
  ```
161
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
+
162
256
 
163
257
  ## Feeds
164
258
 
@@ -234,7 +328,7 @@ Or exclude certain tags:
234
328
 
235
329
  Values are determined with the following precedence, from highest to lowest:
236
330
 
237
- #### 1. Controller Action
331
+ #### 1. Controller action
238
332
 
239
333
  Define a `@metadata` instance variable in your controller:
240
334
  ```ruby
@@ -249,10 +343,9 @@ class Content::PostsController < ApplicationController
249
343
  end
250
344
  ```
251
345
 
252
- #### 2. Page Frontmatter
346
+ #### 2. Page frontmatter
253
347
 
254
348
  Add values to the YAML frontmatter in content files:
255
-
256
349
  ```yaml
257
350
  ---
258
351
  title: My Awesome Post
@@ -266,36 +359,36 @@ Your content here…
266
359
 
267
360
  #### 3. Collection configuration
268
361
 
269
- Set site-wide defaults in the initializer:
362
+ Set collection defaults in the resource model:
270
363
  ```ruby
271
364
  class Content::Post < Perron::Resource
272
365
  # …
273
366
 
274
- config.metadata.description = "AI-powered tool to keep your knowledge base articles images/screenshots and content up-to-date"
275
- config.metadata.author = "Rails Designer"
367
+ config.metadata.description = "Put your routine tasks on autopilot"
368
+ config.metadata.author = "Helptail team"
276
369
  end
277
370
  ```
278
371
 
279
- #### 4. Default Values
372
+ #### 4. Default values
280
373
 
281
374
  Set site-wide defaults in the initializer:
282
375
  ```ruby
283
376
  Perron.configure do |config|
284
377
  # …
285
378
 
286
- config.metadata.description = "AI-powered tool to keep your knowledge base articles images/screenshots and content up-to-date"
287
- config.metadata.author = "Rails Designer"
379
+ config.metadata.description = "Put your routine tasks on autopilot"
380
+ config.metadata.author = "Helptail team"
288
381
  end
289
382
  ```
290
383
 
291
384
 
292
- ## Related Resources
385
+ ## Related resources
293
386
 
294
387
  The `related_resources` method allows to find and display a list of similar resources
295
- from the same collection. Similarity is calculated using the **TF-IDF** algorithm on the content of each resource.
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.
296
389
 
297
390
 
298
- ### Basic Usage
391
+ ### Basic usage
299
392
 
300
393
  To get a list of the 5 most similar resources, call the method on any resource instance.
301
394
  ```ruby
@@ -307,9 +400,9 @@ To get a list of the 5 most similar resources, call the method on any resource i
307
400
  ```
308
401
 
309
402
 
310
- ## XML Sitemap
403
+ ## XML sitemap
311
404
 
312
- A sitemap is an 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.
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.
313
406
 
314
407
  Enable it with the following line in the Perron configuration:
315
408
  ```ruby
@@ -344,7 +437,7 @@ sitemap_change_frequency: :daily
344
437
  ```
345
438
 
346
439
 
347
- ## Building Your Static Site
440
+ ## Building your static site
348
441
 
349
442
  When in `standalone` mode and you're ready to generate your static site, run:
350
443
  ```bash
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MetaTagsHelper
4
- def meta_tags(options = {}) = Perron::Metatags.new(resource).render(options)
4
+ def meta_tags(options = {}) = Perron::Metatags.new(resource.metadata).render(options)
5
5
 
6
6
  private
7
7
 
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Perron
4
+ module ErbHelper
5
+ def erbify(content = nil, options = {}, &block)
6
+ Perron::Resource::Renderer.erb(content || capture(&block).strip_heredoc, {resource: @resource})
7
+ end
8
+ end
9
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "rails/generators/base"
2
4
 
3
5
  class ContentGenerator < Rails::Generators::NamedBase
@@ -1,5 +1,4 @@
1
1
  ---
2
- slug: "/"
3
2
  ---
4
3
 
5
4
  Find me in `app/content/pages/root.erb`
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Perron
2
4
  class InstallGenerator < Rails::Generators::Base
3
5
  source_root File.expand_path("templates", __dir__)
@@ -12,5 +14,16 @@ module Perron
12
14
 
13
15
  template "README.md.tt", File.join(data_directory, "README.md")
14
16
  end
17
+
18
+ def add_markdown_gems
19
+ append_to_file "Gemfile", <<~RUBY
20
+
21
+ # Perron supports Markdown rendering using one of the following gems.
22
+ # Uncomment your preferred choice and run `bundle install`
23
+ # gem "commonmarker"
24
+ # gem "kramdown"
25
+ # gem "redcarpet"
26
+ RUBY
27
+ end
15
28
  end
16
29
  end
@@ -1,20 +1,23 @@
1
1
  Perron.configure do |config|
2
2
  # config.output = "output"
3
3
 
4
- # config.site_name = "AppRefresher"
4
+ # config.site_name = "Helptail"
5
5
 
6
- # The build mode for Perron. Can be :standalone or :integrated.
6
+ # The build mode for Perron. Can be :standalone or :integrated
7
7
  # config.mode = :standalone
8
8
 
9
- # In `integrated` mode, the root is skipped by default. Set to `true` to enable.
9
+ # In `integrated` mode, the root is skipped by default. Set to `true` to enable
10
10
  # config.include_root = false
11
11
 
12
- # config.default_url_options = {host: "apprefresher.com", protocol: "https", trailing_slash: true}
12
+ # config.default_url_options = {host: "helptail.com", protocol: "https", trailing_slash: true}
13
+
14
+ # The options hash is passed directly to the chosen library
15
+ # config.markdown_options = {}
13
16
 
14
17
  # Set default meta values
15
18
  # Examples:
16
- # - `config.metadata.description = "AI-powered tool to keep your knowledge base articles images/screenshots and content up-to-date"`
17
- # - `config.metadata.author = "Rails Designer"`
19
+ # - `config.metadata.description = "Put your routine tasks on autopilot"`
20
+ # - `config.metadata.author = "Helptail Team"`
18
21
 
19
22
  # Set meta title suffix
20
23
  # config.metadata.title_suffix = nil
@@ -5,7 +5,7 @@ module Perron
5
5
  attr_reader :name
6
6
 
7
7
  def initialize(name)
8
- @name = name
8
+ @name = name.inquiry
9
9
  @collection_path = File.join(Perron.configuration.input, name)
10
10
 
11
11
  raise Errors::CollectionNotFoundError, "No such collection: #{name}" unless File.exist?(@collection_path) && File.directory?(@collection_path)
@@ -29,5 +29,11 @@ module Perron
29
29
 
30
30
  raise Errors::ResourceNotFoundError, "Resource not found with slug: #{slug}"
31
31
  end
32
+
33
+ def find_by_file_name(file_name, resource_class = Resource)
34
+ resource_class.new(
35
+ Perron.configuration.allowed_extensions.lazy.map { File.join(@collection_path, [file_name, it].join(".")) }.find { File.exist?(it) }
36
+ )
37
+ end
32
38
  end
33
39
  end
@@ -13,24 +13,31 @@ module Perron
13
13
  def initialize
14
14
  @config = ActiveSupport::OrderedOptions.new
15
15
 
16
+ @config.site_name = nil
17
+ @config.site_description = nil
18
+
19
+ @config.site_email = nil
20
+
16
21
  @config.output = "output"
17
22
 
18
23
  @config.mode = :standalone
19
24
  @config.include_root = false
20
25
 
21
- @config.site_name = nil
22
- @config.site_description = nil
26
+ @config.allowed_extensions = %w[erb md]
23
27
 
24
- @config.allowed_extensions = [".erb", ".md"]
25
28
  @config.exclude_from_public = %w[assets storage]
26
29
  @config.excluded_assets = %w[action_cable actioncable actiontext activestorage rails-ujs trix turbo]
27
30
 
31
+ @config.view_unpublished = Rails.env.development?
32
+
28
33
  @config.default_url_options = {
29
34
  host: ENV.fetch("PERRON_HOST", "localhost:3000"),
30
35
  protocol: ENV.fetch("PERRON_PROTOCOL", "http"),
31
36
  trailing_slash: ENV.fetch("PERRON_TRAILING_SLASH", "true") == "true"
32
37
  }
33
38
 
39
+ @config.markdown_options = {}
40
+
34
41
  @config.sitemap = ActiveSupport::OrderedOptions.new
35
42
  @config.sitemap.enabled = false
36
43
  @config.sitemap.priority = 0.5
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Perron
4
+ class Data
5
+ class Proxy
6
+ include Enumerable
7
+
8
+ def initialize(parts = [])
9
+ @parts = parts
10
+ @data = data_for_proxy
11
+ end
12
+
13
+ def each(&block) = @data.each(&block)
14
+
15
+ def inspect = @data.inspect
16
+
17
+ def respond_to_missing?(name, include_private = false)
18
+ identifier = File.join(*@parts, name.to_s)
19
+
20
+ Perron::Data.directory?(identifier) || Perron::Data.path_for(identifier) || super
21
+ end
22
+
23
+ def method_missing(name, *arguments, &block)
24
+ raise ArgumentError, "Data access does not accept arguments" if arguments.any? || block
25
+
26
+ new_parts = @parts + [name.to_s]
27
+ identifier = File.join(*new_parts)
28
+
29
+ return Proxy.new(new_parts) if Perron::Data.directory?(identifier)
30
+ return Perron::Data.new(identifier) if Perron::Data.path_for(identifier)
31
+
32
+ super
33
+ end
34
+
35
+ private
36
+
37
+ def data_for_proxy
38
+ return [] if @parts.empty?
39
+
40
+ identifier = File.join(*@parts)
41
+ data_path = Perron::Data.path_for(identifier) || Perron::Data.path_for(File.join(identifier, "index"))
42
+
43
+ data_path ? Perron::Data.new(data_path) : []
44
+ end
45
+ end
46
+ end
47
+ end
@@ -1,19 +1,37 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "erb"
4
- require "singleton"
5
-
6
3
  require "csv"
7
4
 
8
5
  module Perron
9
6
  class Data < SimpleDelegator
10
7
  def initialize(identifier)
11
- @file_path = path_for(identifier)
8
+ @identifier = identifier
9
+ @file_path = self.class.path_for!(identifier)
12
10
  @records = records
13
11
 
14
12
  super(records)
15
13
  end
16
14
 
15
+ class << self
16
+ def path_for(identifier)
17
+ path = Pathname.new(identifier)
18
+
19
+ return path.to_s if path.file? && path.absolute?
20
+
21
+ base_path = Rails.root.join("app", "content", "data")
22
+
23
+ SUPPORTED_EXTENSIONS.lazy.map { base_path.join("#{identifier}#{it}") }.find(&:exist?)&.to_s
24
+ end
25
+
26
+ def path_for!(identifier)
27
+ path_for(identifier).tap do |path|
28
+ raise Errors::FileNotFoundError, "No data file found for `#{identifier}`" unless path
29
+ end
30
+ end
31
+
32
+ def directory?(identifier) = Dir.exist?(Rails.root.join("app", "content", "data", identifier))
33
+ end
34
+
17
35
  private
18
36
 
19
37
  PARSER_METHODS = {
@@ -22,24 +40,15 @@ module Perron
22
40
  }.freeze
23
41
  SUPPORTED_EXTENSIONS = PARSER_METHODS.keys
24
42
 
25
- def path_for(identifier)
26
- path = Pathname.new(identifier)
27
-
28
- return path.to_s if path.file? && path.absolute?
29
-
30
- path = SUPPORTED_EXTENSIONS.lazy.map { Rails.root.join("app", "content", "data").join("#{identifier}#{it}") }.find(&:exist?)
31
- path&.to_s or raise Errors::FileNotFoundError, "No data file found for '#{identifier}'"
32
- end
33
-
34
43
  def records
35
44
  content = rendered_from(@file_path)
36
45
  data = parsed_from(content, @file_path)
37
46
 
38
47
  unless data.is_a?(Array)
39
- raise Errors::DataParseError, "Data in '#{@file_path}' must be an array of objects."
48
+ raise Errors::DataParseError, "Data in `#{@file_path}` must be an array of objects."
40
49
  end
41
50
 
42
- data.map { Item.new(it) }
51
+ data.map { Item.new(it, identifier: @identifier) }
43
52
  end
44
53
 
45
54
  def rendered_from(path)
@@ -89,12 +98,23 @@ module Perron
89
98
  private_constant :HelperContext
90
99
 
91
100
  class Item
92
- def initialize(attributes)
101
+ def initialize(attributes, identifier:)
93
102
  @attributes = attributes.transform_keys(&:to_sym)
103
+ @identifier = identifier
94
104
  end
95
105
 
96
106
  def [](key) = @attributes[key.to_sym]
97
107
 
108
+ def to_partial_path
109
+ @to_partial_path ||= begin
110
+ identifier = @identifier.to_s
111
+ collection = File.extname(identifier).present? ? File.basename(identifier, ".*") : identifier
112
+ element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.singularize(File.basename(collection)))
113
+
114
+ File.join("content", collection, element)
115
+ end
116
+ end
117
+
98
118
  def method_missing(method_name, *arguments, &block)
99
119
  return super if !@attributes.key?(method_name) || arguments.any? || block
100
120
 
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rouge"
4
+ require "perron/html_processor/base"
5
+
6
+ module Perron
7
+ class HtmlProcessor
8
+ class SyntaxHighlight < HtmlProcessor::Base
9
+ def process
10
+ @html.css('pre > code[class*="language-"]').each do |code_block|
11
+ language = code_block[:class][/(?<=language-)\S+/]
12
+
13
+ next if language.blank?
14
+
15
+ code_block.parent.replace(
16
+ highlight(code_block.text, with: language)
17
+ )
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def highlight(code_block, with:)
24
+ lexer = Rouge::Lexer.find(with) || Rouge::Lexers::PlainText.new
25
+
26
+ Rouge::Formatters::HTMLPygments.new(::Rouge::Formatters::HTML.new).format(lexer.lex(code_block))
27
+ end
28
+ end
29
+ end
30
+ end