perron 0.17.0 → 0.18.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -1
  3. data/Gemfile.lock +25 -2
  4. data/app/controllers/perron/searches_controller.rb +48 -0
  5. data/app/helpers/perron/feeds_helper.rb +7 -0
  6. data/app/helpers/perron/markdown_helper.rb +3 -3
  7. data/app/helpers/perron/meta_tags_helper.rb +17 -0
  8. data/bin/release +19 -4
  9. data/lib/generators/perron/templates/README.md.tt +31 -7
  10. data/lib/generators/perron/templates/initializer.rb.tt +8 -4
  11. data/lib/generators/rails/content/USAGE +28 -26
  12. data/lib/generators/rails/content/content_generator.rb +6 -7
  13. data/lib/generators/rails/content/templates/controller.rb.tt +1 -5
  14. data/lib/generators/rails/content/templates/model.rb.tt +3 -3
  15. data/lib/perron/collection.rb +10 -1
  16. data/lib/perron/configuration.rb +9 -4
  17. data/lib/perron/content/data.rb +6 -2
  18. data/lib/perron/data_source/class_methods.rb +58 -0
  19. data/lib/perron/data_source/helper_context.rb +20 -0
  20. data/lib/perron/data_source/item.rb +37 -0
  21. data/lib/perron/{data → data_source}/proxy.rb +1 -1
  22. data/lib/perron/data_source.rb +155 -0
  23. data/lib/perron/engine.rb +12 -0
  24. data/lib/perron/html_processor/syntax_highlight.rb +2 -0
  25. data/lib/perron/output_server.rb +7 -2
  26. data/lib/perron/relation.rb +51 -0
  27. data/lib/perron/resource/associations.rb +2 -2
  28. data/lib/perron/resource/class_methods.rb +10 -0
  29. data/lib/perron/resource/configuration.rb +8 -11
  30. data/lib/perron/resource/core.rb +11 -0
  31. data/lib/perron/resource/related/stop_words.rb +20 -20
  32. data/lib/perron/resource/related.rb +73 -52
  33. data/lib/perron/resource/scopes.rb +29 -0
  34. data/lib/perron/resource/searchable.rb +19 -0
  35. data/lib/perron/resource/sourceable.rb +2 -2
  36. data/lib/perron/resource/sweeper.rb +45 -0
  37. data/lib/perron/resource/table_of_content.rb +0 -18
  38. data/lib/perron/resource.rb +30 -20
  39. data/lib/perron/site.rb +3 -3
  40. data/lib/perron/tasks/build.rake +8 -1
  41. data/lib/perron/version.rb +1 -1
  42. data/lib/perron.rb +1 -0
  43. data/perron.gemspec +1 -0
  44. metadata +28 -6
  45. data/app/helpers/feeds_helper.rb +0 -5
  46. data/app/helpers/meta_tags_helper.rb +0 -15
  47. data/lib/perron/data.rb +0 -180
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Perron
4
+ class Resource
5
+ module Searchable
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ class_attribute :search_fields_list, default: []
10
+ end
11
+
12
+ class_methods do
13
+ def search_fields(*fields)
14
+ self.search_fields_list = fields
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -40,7 +40,7 @@ module Perron
40
40
  end
41
41
 
42
42
  def combinations
43
- datasets = source_names.map { Perron::Data.new(it.to_s) }
43
+ datasets = source_names.map { Perron::DataSource.new(it.to_s) }
44
44
 
45
45
  datasets.first.product(*datasets[1..])
46
46
  end
@@ -72,7 +72,7 @@ module Perron
72
72
  singular_name = name.to_s.singularize
73
73
  identifier = frontmatter["#{singular_name}_#{primary_key}"]
74
74
 
75
- hash[name] = Perron::Data.new(name.to_s).find { it.public_send(primary_key).to_s == identifier.to_s }
75
+ hash[name] = Perron::DataSource.new(name.to_s).find { it.public_send(primary_key).to_s == identifier.to_s }
76
76
  end
77
77
 
78
78
  Source.new(data)
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Perron
4
+ class Resource
5
+ module Sweeper
6
+ extend ActiveSupport::Concern
7
+
8
+ def extracted_headings
9
+ extract_heading_texts(from: rendered_document)
10
+ end
11
+
12
+ def sweeped_content
13
+ ActionView::Base.full_sanitizer.sanitize(
14
+ rendered_document.text.gsub(/\s+/, " ").strip
15
+ )
16
+ end
17
+
18
+ private
19
+
20
+ def rendered_document
21
+ @rendered_document ||= Nokogiri::HTML::DocumentFragment.parse(Markdown.render(content))
22
+ end
23
+
24
+ def extract_heading_texts(from:, levels: "h1, h2, h3, h4, h5, h6")
25
+ from.css(levels).map { it.text.strip }.compact_blank
26
+ end
27
+
28
+ def extract_headings(from:, levels:)
29
+ from.css(levels).each_with_object([]) do |heading, headings|
30
+ heading_text = heading.text.strip
31
+ id = heading["id"] || heading.at("a")&.[]("id")
32
+
33
+ next if heading_text.empty? || id.blank?
34
+
35
+ headings << TableOfContent::Item.new(
36
+ id: id,
37
+ text: heading_text,
38
+ level: heading.name[1..].to_i,
39
+ children: []
40
+ )
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -20,24 +20,6 @@ module Perron
20
20
 
21
21
  Item = ::Data.define(:id, :text, :level, :children)
22
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
23
  class Builder
42
24
  def build(headings)
43
25
  parents = {0 => {children: []}}
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "perron/relation"
3
4
  require "perron/resource/configuration"
4
5
  require "perron/resource/core"
5
6
  require "perron/resource/class_methods"
@@ -10,26 +11,30 @@ require "perron/resource/publishable"
10
11
  require "perron/resource/reading_time"
11
12
  require "perron/resource/related"
12
13
  require "perron/resource/renderer"
14
+ require "perron/resource/scopes"
13
15
  require "perron/resource/slug"
16
+ require "perron/resource/searchable"
14
17
  require "perron/resource/separator"
15
18
  require "perron/resource/sourceable"
19
+ require "perron/resource/sweeper"
16
20
  require "perron/resource/table_of_content"
17
21
 
18
22
  module Perron
19
23
  class Resource
20
- ID_LENGTH = 8
21
-
22
24
  include ActiveModel::Validations
23
25
 
24
- include Perron::Resource::Configuration
25
- include Perron::Resource::Core
26
- include Perron::Resource::ClassMethods
27
- include Perron::Resource::Associations
28
- include Perron::Resource::ReadingTime
29
- include Perron::Resource::Sourceable
30
- include Perron::Resource::Publishable
31
- include Perron::Resource::Previewable
32
- include Perron::Resource::TableOfContent
26
+ include Configuration
27
+ include Core
28
+ include ClassMethods
29
+ include Associations
30
+ include ReadingTime
31
+ include Searchable
32
+ include Sourceable
33
+ include Publishable
34
+ include Previewable
35
+ include Scopes
36
+ include Sweeper
37
+ include TableOfContent
33
38
 
34
39
  attr_reader :file_path, :id
35
40
 
@@ -41,6 +46,16 @@ module Perron
41
46
  raise Errors::FileNotFoundError, "No such file: #{file_path}" unless File.exist?(file_path)
42
47
  end
43
48
 
49
+ def pluck(*attributes)
50
+ raise ArgumentError, "wrong number of arguments (given 0, expected 1+)" if attributes.empty?
51
+
52
+ if attributes.size == 1
53
+ public_send(attributes.first)
54
+ else
55
+ attributes.map { |attr| public_send(attr) }
56
+ end
57
+ end
58
+
44
59
  def filename = File.basename(@file_path)
45
60
 
46
61
  def slug = Perron::Resource::Slug.new(self, frontmatter).create
@@ -66,15 +81,8 @@ module Perron
66
81
  render_inline_erb using: page_content
67
82
  end
68
83
 
69
- def association_value(key) = metadata[key]
70
-
71
- def to_partial_path
72
- @to_partial_path ||= begin
73
- element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(self.class.model_name))
74
- collection = ActiveSupport::Inflector.tableize(self.class.model_name)
75
-
76
- File.join("content", collection, element)
77
- end
84
+ def inline(layout: "application", **options)
85
+ {html: content, layout: layout}.merge(options)
78
86
  end
79
87
 
80
88
  def collection = Collection.new(self.class.model_name.collection)
@@ -88,6 +96,8 @@ module Perron
88
96
 
89
97
  private
90
98
 
99
+ ID_LENGTH = 8
100
+
91
101
  def frontmatter
92
102
  @frontmatter ||= Perron::Resource::Separator.new(raw_content).frontmatter
93
103
  end
data/lib/perron/site.rb CHANGED
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "perron/collection"
4
- require "perron/data"
5
- require "perron/data/proxy"
4
+ require "perron/data_source"
5
+ require "perron/data_source/proxy"
6
6
  require "perron/site/builder"
7
7
  require "perron/site/validate"
8
8
 
@@ -26,7 +26,7 @@ module Perron
26
26
  def data(name = nil)
27
27
  Perron.deprecator.deprecation_warning(:data, "Use Content::Data::ClassName instead, e.g. `Content::Data::Users.all`")
28
28
 
29
- (name && Perron::Data.new(name)) || Perron::Data::Proxy.new
29
+ (name && Perron::DataSource.new(name)) || Perron::DataSource::Proxy.new
30
30
  end
31
31
  end
32
32
  end
@@ -1,6 +1,13 @@
1
1
  namespace :perron do
2
+ task :set_production_env do
3
+ unless ENV["RAILS_ENV"]
4
+ ENV["RAILS_ENV"] = "production"
5
+ puts "RAILS_ENV not set, defaulting to production"
6
+ end
7
+ end
8
+
2
9
  desc "Generate static HTML files from Perron collections"
3
- task build: :environment do
10
+ task build: [:set_production_env, :environment] do
4
11
  Perron::Site.build
5
12
  end
6
13
  end
@@ -1,3 +1,3 @@
1
1
  module Perron
2
- VERSION = "0.17.0"
2
+ VERSION = "0.18.0"
3
3
  end
data/lib/perron.rb CHANGED
@@ -5,6 +5,7 @@ require "perron/configuration"
5
5
  require "perron/deprecator"
6
6
  require "perron/errors"
7
7
  require "perron/site"
8
+ require "perron/relation"
8
9
  require "perron/content/data"
9
10
  require "perron/resource"
10
11
  require "perron/markdown"
data/perron.gemspec CHANGED
@@ -19,6 +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
23
 
23
24
  spec.add_runtime_dependency "csv"
24
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: 0.17.0
4
+ version: 0.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rails Designer Developers
@@ -23,6 +23,20 @@ dependencies:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
25
  version: 7.2.0
26
+ - !ruby/object:Gem::Dependency
27
+ name: mata
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: 0.8.0
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: 0.8.0
26
40
  - !ruby/object:Gem::Dependency
27
41
  name: csv
28
42
  requirement: !ruby/object:Gem::Requirement
@@ -78,10 +92,11 @@ files:
78
92
  - Gemfile.lock
79
93
  - README.md
80
94
  - Rakefile
81
- - app/helpers/feeds_helper.rb
82
- - app/helpers/meta_tags_helper.rb
95
+ - app/controllers/perron/searches_controller.rb
83
96
  - app/helpers/perron/erb_helper.rb
97
+ - app/helpers/perron/feeds_helper.rb
84
98
  - app/helpers/perron/markdown_helper.rb
99
+ - app/helpers/perron/meta_tags_helper.rb
85
100
  - bin/console
86
101
  - bin/rails
87
102
  - bin/release
@@ -100,8 +115,11 @@ files:
100
115
  - lib/perron/collection.rb
101
116
  - lib/perron/configuration.rb
102
117
  - lib/perron/content/data.rb
103
- - lib/perron/data.rb
104
- - lib/perron/data/proxy.rb
118
+ - lib/perron/data_source.rb
119
+ - lib/perron/data_source/class_methods.rb
120
+ - lib/perron/data_source/helper_context.rb
121
+ - lib/perron/data_source/item.rb
122
+ - lib/perron/data_source/proxy.rb
105
123
  - lib/perron/deprecator.rb
106
124
  - lib/perron/engine.rb
107
125
  - lib/perron/errors.rb
@@ -115,6 +133,7 @@ files:
115
133
  - lib/perron/metatags.rb
116
134
  - lib/perron/output_server.rb
117
135
  - lib/perron/refinements/delete_suffixes.rb
136
+ - lib/perron/relation.rb
118
137
  - lib/perron/resource.rb
119
138
  - lib/perron/resource/associations.rb
120
139
  - lib/perron/resource/class_methods.rb
@@ -127,9 +146,12 @@ files:
127
146
  - lib/perron/resource/related.rb
128
147
  - lib/perron/resource/related/stop_words.rb
129
148
  - lib/perron/resource/renderer.rb
149
+ - lib/perron/resource/scopes.rb
150
+ - lib/perron/resource/searchable.rb
130
151
  - lib/perron/resource/separator.rb
131
152
  - lib/perron/resource/slug.rb
132
153
  - lib/perron/resource/sourceable.rb
154
+ - lib/perron/resource/sweeper.rb
133
155
  - lib/perron/resource/table_of_content.rb
134
156
  - lib/perron/site.rb
135
157
  - lib/perron/site/builder.rb
@@ -170,7 +192,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
170
192
  - !ruby/object:Gem::Version
171
193
  version: '0'
172
194
  requirements: []
173
- rubygems_version: 4.0.4
195
+ rubygems_version: 4.0.6
174
196
  specification_version: 4
175
197
  summary: Rails-based static site generator
176
198
  test_files: []
@@ -1,5 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module FeedsHelper
4
- def feeds(options = {}) = Perron::Feeds.new.render(options)
5
- end
@@ -1,15 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module MetaTagsHelper
4
- def meta_tags(options = {}) = Perron::Metatags.new(resource.metadata).render(options)
5
-
6
- private
7
-
8
- Resource = Data.define(:path, :collection, :metadata, :published_at)
9
-
10
- def resource
11
- return Resource.new(request.path, nil, @metadata, nil) if @metadata.present?
12
-
13
- @resource || Resource.new(request.path, nil, {}, nil)
14
- end
15
- end
data/lib/perron/data.rb DELETED
@@ -1,180 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "csv"
4
-
5
- module Perron
6
- class Data < SimpleDelegator
7
- include Enumerable
8
-
9
- def initialize(identifier)
10
- @identifier = identifier
11
- @file_path = self.class.path_for!(identifier)
12
- @records = records
13
-
14
- super(records)
15
- end
16
-
17
- def each(&block) = @records.each(&block)
18
-
19
- def count = @records.count
20
-
21
- def first(n = nil)
22
- n ? @records.first(n) : @records.first
23
- end
24
-
25
- def last = @records.last
26
-
27
- def [](index) = @records[index]
28
-
29
- def size = @records.size
30
- alias_method :length, :size
31
-
32
- class << self
33
- def all
34
- parts = name.to_s.split("::").drop(2)
35
- identifier = parts.empty? ? name.demodulize.underscore : parts.map(&:underscore).join("/")
36
-
37
- new(identifier)
38
- end
39
-
40
- def find(id)
41
- all.find { it[:id] == id || it["id"] == id }
42
- end
43
-
44
- def count = all.size
45
-
46
- def first = all.first
47
-
48
- def second = all[1]
49
-
50
- def third = all[2]
51
-
52
- def fourth = all[3]
53
-
54
- def fifth = all[4]
55
-
56
- def forty_two = all[41]
57
-
58
- def last = all.last
59
-
60
- def take(n) = all.first(n)
61
-
62
- def path_for(identifier)
63
- path = Pathname.new(identifier)
64
-
65
- return path.to_s if path.file? && path.absolute?
66
-
67
- base_path = Rails.root.join("app", "content", "data")
68
-
69
- SUPPORTED_EXTENSIONS.lazy.map { base_path.join("#{identifier}#{it}") }.find(&:exist?)&.to_s
70
- end
71
-
72
- def path_for!(identifier)
73
- path_for(identifier).tap do |path|
74
- raise Errors::FileNotFoundError, "No data file found for `#{identifier}`" unless path
75
- end
76
- end
77
-
78
- def directory?(identifier) = Dir.exist?(Rails.root.join("app", "content", "data", identifier))
79
- end
80
-
81
- private
82
-
83
- PARSER_METHODS = {
84
- ".yml" => :parse_yaml, ".yaml" => :parse_yaml,
85
- ".json" => :parse_json, ".csv" => :parse_csv
86
- }.freeze
87
- SUPPORTED_EXTENSIONS = PARSER_METHODS.keys
88
-
89
- def records
90
- content = rendered_from(@file_path)
91
- data = parsed_from(content, @file_path)
92
-
93
- unless data.is_a?(Array)
94
- raise Errors::DataParseError, "Data in `#{@file_path}` must be an array of objects."
95
- end
96
-
97
- data.map { Item.new(it, identifier: @identifier) }
98
- end
99
-
100
- def rendered_from(path)
101
- raw_content = File.read(path)
102
-
103
- render_erb(raw_content)
104
- rescue NameError, ArgumentError, SyntaxError => error
105
- raise Errors::DataParseError, "Failed to render ERB in `#{path}`: (#{error.class}) #{error.message}"
106
- end
107
-
108
- def parsed_from(content, path)
109
- extension = File.extname(path)
110
- parser_method = PARSER_METHODS.fetch(extension) do
111
- raise Errors::UnsupportedDataFormatError, "Unsupported data format: #{extension}"
112
- end
113
-
114
- send(parser_method, content)
115
- rescue Psych::SyntaxError, JSON::ParserError, CSV::MalformedCSVError => error
116
- raise Errors::DataParseError, "Failed to parse data format in `#{path}`: (#{error.class}) #{error.message}"
117
- end
118
-
119
- def render_erb(content) = ERB.new(content).result(HelperContext.instance.get_binding)
120
-
121
- def parse_yaml(content)
122
- YAML.safe_load(content, permitted_classes: [Symbol, Time], aliases: true)
123
- end
124
-
125
- def parse_json(content)
126
- JSON.parse(content, symbolize_names: true)
127
- end
128
-
129
- def parse_csv(content)
130
- CSV.new(content, headers: true, header_converters: :symbol).to_a.map(&:to_h)
131
- end
132
-
133
- class HelperContext
134
- include Singleton
135
-
136
- def initialize
137
- self.class.include ActionView::Helpers::AssetUrlHelper
138
- self.class.include ActionView::Helpers::DateHelper
139
- self.class.include Rails.application.routes.url_helpers
140
- end
141
-
142
- def get_binding = binding
143
-
144
- def default_url_options = Perron.configuration.default_url_options || {}
145
- end
146
- private_constant :HelperContext
147
-
148
- class Item
149
- def initialize(attributes, identifier:)
150
- @attributes = attributes.transform_keys(&:to_sym)
151
- @identifier = identifier
152
- end
153
-
154
- def [](key) = @attributes[key.to_sym]
155
-
156
- def association_value(key) = self[key]
157
-
158
- def to_partial_path
159
- @to_partial_path ||= begin
160
- identifier = @identifier.to_s
161
- collection = File.extname(identifier).present? ? File.basename(identifier, ".*") : identifier
162
- element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.singularize(File.basename(collection)))
163
-
164
- File.join("content", collection, element)
165
- end
166
- end
167
-
168
- def method_missing(method_name, *arguments, &block)
169
- return super if !@attributes.key?(method_name) || arguments.any? || block
170
-
171
- @attributes[method_name]
172
- end
173
-
174
- def respond_to_missing?(method_name, include_private = false)
175
- @attributes.key?(method_name) || super
176
- end
177
- end
178
- private_constant :Item
179
- end
180
- end