perron 0.18.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +11 -15
- data/app/controllers/perron/concierge_controller.rb +16 -0
- data/app/helpers/perron/markdown_helper.rb +3 -2
- data/app/helpers/perron/meta_tags_helper.rb +3 -9
- data/app/helpers/perron/paginate_helper.rb +40 -0
- data/app/views/perron/concierge/show.html.erb +271 -0
- data/lib/generators/rails/content/USAGE +21 -4
- data/lib/generators/rails/content/content_generator.rb +16 -12
- data/lib/generators/rails/content/templates/controller.rb.tt +6 -0
- data/lib/generators/rails/content/templates/model.rb.tt +1 -1
- data/lib/perron/assets/icon.png +0 -0
- data/lib/perron/assets/icon.svg +1 -0
- data/lib/perron/collection.rb +2 -1
- data/lib/perron/configuration.rb +27 -2
- data/lib/perron/data_source/class_methods.rb +8 -0
- data/lib/perron/data_source.rb +20 -33
- data/lib/perron/development_feed_server.rb +69 -0
- data/lib/perron/engine.rb +32 -1
- data/lib/perron/errors.rb +2 -0
- data/lib/perron/feeds.rb +4 -3
- data/lib/perron/html_processor/absolute_urls.rb +27 -0
- data/lib/perron/html_processor/base.rb +2 -2
- data/lib/perron/html_processor.rb +7 -11
- data/lib/{generators/perron/templates → perron/install}/README.md.tt +7 -9
- data/lib/perron/install/deploy.yml +15 -0
- data/lib/perron/install.rb +26 -0
- data/lib/perron/markdown.rb +2 -2
- data/lib/perron/output_server.rb +9 -0
- data/lib/perron/paginate.rb +58 -0
- data/lib/perron/relation.rb +24 -6
- data/lib/perron/resource/adjacency.rb +70 -0
- data/lib/perron/resource/associations.rb +1 -1
- data/lib/perron/resource/class_methods.rb +6 -0
- data/lib/perron/resource/configuration.rb +12 -4
- data/lib/perron/resource/metadata.rb +19 -4
- data/lib/perron/resource/publishable.rb +2 -0
- data/lib/perron/resource/related.rb +32 -31
- data/lib/perron/resource/sourceable.rb +98 -16
- data/lib/perron/resource.rb +8 -0
- data/lib/perron/site/builder/assets.rb +1 -1
- data/lib/perron/site/builder/feeds/atom.erb +44 -0
- data/lib/perron/site/builder/feeds/atom.rb +41 -0
- data/lib/perron/site/builder/feeds/json.erb +19 -0
- data/lib/perron/site/builder/feeds/json.rb +7 -33
- data/lib/perron/site/builder/feeds/rss.erb +28 -0
- data/lib/perron/site/builder/feeds/rss.rb +6 -28
- data/lib/perron/site/builder/feeds/template.rb +63 -0
- data/lib/perron/site/builder/feeds.rb +8 -3
- data/lib/perron/site/builder/page.rb +19 -4
- data/lib/perron/site/builder/paths.rb +75 -13
- data/lib/perron/site/builder/route_resources.rb +79 -0
- data/lib/perron/site/builder/sitemap.rb +71 -20
- data/lib/perron/site/builder.rb +25 -1
- data/lib/perron/site/validate.rb +19 -7
- data/lib/perron/site.rb +7 -0
- data/lib/perron/tasks/build.rake +6 -7
- data/lib/perron/tasks/deploy.rake +58 -0
- data/lib/perron/tasks/install.rake +12 -0
- data/lib/perron/version.rb +1 -1
- data/lib/perron.rb +1 -0
- data/perron.gemspec +1 -1
- metadata +25 -8
- data/lib/generators/perron/install_generator.rb +0 -32
- data/lib/perron/html_processor/syntax_highlight.rb +0 -32
- /data/lib/{generators/perron/templates → perron/install}/initializer.rb.tt +0 -0
data/lib/perron/data_source.rb
CHANGED
|
@@ -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
|
|
@@ -52,23 +66,15 @@ module Perron
|
|
|
52
66
|
end
|
|
53
67
|
|
|
54
68
|
data.map.with_index do |item, index|
|
|
55
|
-
|
|
56
|
-
|
|
69
|
+
if item.is_a?(Hash)
|
|
70
|
+
Item.new(item, identifier: @identifier)
|
|
71
|
+
elsif item.is_a?(String)
|
|
72
|
+
item
|
|
73
|
+
else
|
|
74
|
+
raise Errors::DataParseError, "Item at index #{index} in `#{@file_path}` must be a hash/object or string, got #{item.class}"
|
|
57
75
|
end
|
|
58
|
-
|
|
59
|
-
Item.new(item, identifier: @identifier)
|
|
60
76
|
end
|
|
61
77
|
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
78
|
|
|
73
79
|
def rendered_from(path)
|
|
74
80
|
raw_content = File.read(path)
|
|
@@ -86,16 +92,6 @@ module Perron
|
|
|
86
92
|
|
|
87
93
|
send(parser_method, content, path)
|
|
88
94
|
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
95
|
|
|
100
96
|
def render_erb(content) = ERB.new(content).result(HelperContext.instance.get_binding)
|
|
101
97
|
|
|
@@ -107,9 +103,6 @@ module Perron
|
|
|
107
103
|
|
|
108
104
|
raise Errors::DataParseError, "Invalid YAML syntax in `#{path}`#{line_info}#{column_info}: #{error.problem}"
|
|
109
105
|
end
|
|
110
|
-
# def parse_yaml(content)
|
|
111
|
-
# YAML.safe_load(content, permitted_classes: [Symbol, Time], aliases: true)
|
|
112
|
-
# end
|
|
113
106
|
|
|
114
107
|
def parse_json(content, path)
|
|
115
108
|
JSON.parse(content, symbolize_names: true)
|
|
@@ -119,9 +112,6 @@ module Perron
|
|
|
119
112
|
|
|
120
113
|
raise Errors::DataParseError, "Invalid JSON syntax in `#{path}`#{line_info}: #{error.message}"
|
|
121
114
|
end
|
|
122
|
-
# def parse_json(content)
|
|
123
|
-
# JSON.parse(content, symbolize_names: true)
|
|
124
|
-
# end
|
|
125
115
|
|
|
126
116
|
def parse_csv(content, path)
|
|
127
117
|
expected_headers = nil
|
|
@@ -148,8 +138,5 @@ module Perron
|
|
|
148
138
|
|
|
149
139
|
raise Errors::DataParseError, "Malformed CSV in `#{path}`#{line_info}: #{error.message}"
|
|
150
140
|
end
|
|
151
|
-
# def parse_csv(content)
|
|
152
|
-
# CSV.new(content, headers: true, header_converters: :symbol).to_a.map(&:to_h)
|
|
153
|
-
# end
|
|
154
141
|
end
|
|
155
142
|
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
|
data/lib/perron/engine.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "perron/output_server"
|
|
4
|
+
require "perron/development_feed_server"
|
|
4
5
|
require "mata"
|
|
5
6
|
|
|
6
7
|
module Perron
|
|
@@ -10,7 +11,11 @@ module Perron
|
|
|
10
11
|
end
|
|
11
12
|
|
|
12
13
|
initializer "perron.output_server" do |app|
|
|
13
|
-
app.middleware.use Perron::OutputServer
|
|
14
|
+
app.middleware.use Perron::OutputServer if Rails.env.development?
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
initializer "perron.development_feed_server" do |app|
|
|
18
|
+
app.middleware.use Perron::DevelopmentFeedServer if Rails.env.development?
|
|
14
19
|
end
|
|
15
20
|
|
|
16
21
|
initializer "perron.configure_hmr", after: :load_config_initializers do |app|
|
|
@@ -24,11 +29,37 @@ module Perron
|
|
|
24
29
|
end
|
|
25
30
|
end
|
|
26
31
|
|
|
32
|
+
initializer "perron.concierge", before: :add_builtin_route do |app|
|
|
33
|
+
if Rails.env.development?
|
|
34
|
+
app.config.after_initialize do
|
|
35
|
+
app.routes.append do
|
|
36
|
+
namespace :perron do
|
|
37
|
+
post :run_command, to: "concierge#run_command"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
root to: "perron/concierge#show" unless app.routes.named_routes.key?(:root)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
app.routes.finalize!
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
initializer "perron.inflections" do
|
|
49
|
+
ActiveSupport::Inflector.inflections(:en) do |inflect|
|
|
50
|
+
inflect.acronym "RSS"
|
|
51
|
+
inflect.acronym "Atom"
|
|
52
|
+
inflect.acronym "Json"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
27
56
|
rake_tasks do
|
|
28
57
|
load File.expand_path("../tasks/build.rake", __FILE__)
|
|
29
58
|
load File.expand_path("../tasks/clobber.rake", __FILE__)
|
|
59
|
+
load File.expand_path("../tasks/install.rake", __FILE__)
|
|
30
60
|
load File.expand_path("../tasks/sync_sources.rake", __FILE__)
|
|
31
61
|
load File.expand_path("../tasks/validate.rake", __FILE__)
|
|
62
|
+
load File.expand_path("../tasks/deploy.rake", __FILE__)
|
|
32
63
|
end
|
|
33
64
|
end
|
|
34
65
|
end
|
data/lib/perron/errors.rb
CHANGED
data/lib/perron/feeds.rb
CHANGED
|
@@ -19,7 +19,7 @@ module Perron
|
|
|
19
19
|
next unless feed.enabled && feed.path && MIME_TYPES.key?(type)
|
|
20
20
|
|
|
21
21
|
absolute_url = URI.join(url.root_url, feed.path).to_s
|
|
22
|
-
title = "#{collection.name.humanize} #{type.to_s.
|
|
22
|
+
title = "#{collection.name.humanize} #{type.to_s.humanize} Feed"
|
|
23
23
|
|
|
24
24
|
html_tags << tag(:link, rel: "alternate", type: MIME_TYPES[type], title: title, href: absolute_url)
|
|
25
25
|
end
|
|
@@ -32,8 +32,9 @@ module Perron
|
|
|
32
32
|
private
|
|
33
33
|
|
|
34
34
|
MIME_TYPES = {
|
|
35
|
-
|
|
36
|
-
json: "application/json"
|
|
35
|
+
atom: "application/atom+xml",
|
|
36
|
+
json: "application/json",
|
|
37
|
+
rss: "application/rss+xml"
|
|
37
38
|
}
|
|
38
39
|
end
|
|
39
40
|
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Perron
|
|
4
|
+
class HtmlProcessor
|
|
5
|
+
class AbsoluteUrls < HtmlProcessor::Base
|
|
6
|
+
def process
|
|
7
|
+
@html.css("img").each do |image|
|
|
8
|
+
src = image["src"]
|
|
9
|
+
|
|
10
|
+
next if src.blank? || absolute_url?(src)
|
|
11
|
+
|
|
12
|
+
image["src"] = base_url + src
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def absolute_url?(src)
|
|
19
|
+
src.start_with?("http://", "https://", "//")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def base_url
|
|
23
|
+
Perron.configuration.url.delete_suffix("/")
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -2,17 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
require "perron/html_processor/target_blank"
|
|
4
4
|
require "perron/html_processor/lazy_load_images"
|
|
5
|
+
require "perron/html_processor/absolute_urls"
|
|
5
6
|
|
|
6
7
|
module Perron
|
|
7
8
|
class HtmlProcessor
|
|
8
|
-
def initialize(html, processors: [])
|
|
9
|
+
def initialize(html, processors: [], resource: nil)
|
|
9
10
|
@html = html
|
|
11
|
+
@resource = resource
|
|
10
12
|
@processors = processors.map { find_by(it) }
|
|
11
13
|
end
|
|
12
14
|
|
|
13
15
|
def process
|
|
14
16
|
Nokogiri::HTML::DocumentFragment.parse(@html).tap do |document|
|
|
15
|
-
@processors.each { it.new(document).process }
|
|
17
|
+
@processors.each { it.new(document, resource: @resource).process }
|
|
16
18
|
end.to_html
|
|
17
19
|
end
|
|
18
20
|
|
|
@@ -20,14 +22,9 @@ module Perron
|
|
|
20
22
|
|
|
21
23
|
BUILT_IN = {
|
|
22
24
|
"target_blank" => Perron::HtmlProcessor::TargetBlank,
|
|
23
|
-
"lazy_load_images" => Perron::HtmlProcessor::LazyLoadImages
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
require "perron/html_processor/syntax_highlight"
|
|
27
|
-
|
|
28
|
-
processors["syntax_highlight"] = Perron::HtmlProcessor::SyntaxHighlight
|
|
29
|
-
rescue LoadError
|
|
30
|
-
end
|
|
25
|
+
"lazy_load_images" => Perron::HtmlProcessor::LazyLoadImages,
|
|
26
|
+
"absolute_urls" => Perron::HtmlProcessor::AbsoluteUrls
|
|
27
|
+
}
|
|
31
28
|
|
|
32
29
|
def find_by(identifier)
|
|
33
30
|
case identifier
|
|
@@ -47,7 +44,6 @@ module Perron
|
|
|
47
44
|
|
|
48
45
|
return processor if processor
|
|
49
46
|
|
|
50
|
-
raise Perron::Errors::ProcessorNotFoundError, "The `syntax_highlight` processor requires `rouge`. Run `bundle add rouge` to add it to your Gemfile." if name.inquiry.syntax_highlight?
|
|
51
47
|
raise Perron::Errors::ProcessorNotFoundError, "Could not find processor `#{name}`. It is not a Perron-included processor and the constant `#{name.camelize}` could not be found."
|
|
52
48
|
end
|
|
53
49
|
end
|
|
@@ -7,10 +7,10 @@ Perron can consume structured data from YML, JSON or CSV files, making them avai
|
|
|
7
7
|
|
|
8
8
|
Access data sources using the `Content::Data` namespace with the class name matching the file's basename:
|
|
9
9
|
```erb
|
|
10
|
-
|
|
11
|
-
<h4
|
|
12
|
-
<p
|
|
13
|
-
|
|
10
|
+
<% Content::Data::Features.all.each do |feature| %>
|
|
11
|
+
<h4><%= feature.name %></h4>
|
|
12
|
+
<p><%= feature.description %></p>
|
|
13
|
+
<% end %>
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
Look up a single entry with `Content::Data::Features.find("advanced-search")`, where `"advanced-search"` matches the value of the entry's `id` field.
|
|
@@ -34,15 +34,15 @@ feature[:name]
|
|
|
34
34
|
|
|
35
35
|
Render data collections directly using Rails-like partial rendering:
|
|
36
36
|
```erb
|
|
37
|
-
|
|
37
|
+
<%= render Content::Data::Features.all %>
|
|
38
38
|
```
|
|
39
39
|
|
|
40
40
|
This expects a partial at `app/views/content/features/_feature.html.erb` that will be rendered once for each item in `Content::Data::Features.all`. The individual record is made available as a local variable matching the singular form of the collection name.
|
|
41
41
|
```erb
|
|
42
42
|
<!-- app/views/content/features/_feature.html.erb -->
|
|
43
43
|
<div class="feature">
|
|
44
|
-
<h4
|
|
45
|
-
<p
|
|
44
|
+
<h4><%= feature.name %></h4>
|
|
45
|
+
<p><%= feature.description %></p>
|
|
46
46
|
</div>
|
|
47
47
|
```
|
|
48
48
|
|
|
@@ -64,6 +64,4 @@ Data resources must contain an array of objects. Each record should include an `
|
|
|
64
64
|
|
|
65
65
|
## Enumerable methods
|
|
66
66
|
|
|
67
|
-
[!label v0.17.0+]
|
|
68
|
-
|
|
69
67
|
All data objects support enumerable methods like `select`, `sort_by`, `first` and `count`. See [Enumerable methods](/docs/rendering/#enumerable-methods) for the full list of available methods.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Deploy configuration for Perron
|
|
2
|
+
# Requires Beam Up (`bundle add beam_up`)
|
|
3
|
+
# See also: https://perron.railsdesigner.com/docs/deploy/
|
|
4
|
+
|
|
5
|
+
# Uncomment and configure your provider:
|
|
6
|
+
# provider: netlify
|
|
7
|
+
|
|
8
|
+
# netlify:
|
|
9
|
+
# api_token: your_token_here
|
|
10
|
+
# site_id: your_site_id
|
|
11
|
+
|
|
12
|
+
before_actions:
|
|
13
|
+
- RAILS_ENV=production bundle exec rails perron:build
|
|
14
|
+
after_actions:
|
|
15
|
+
- bundle exec rails perron:clobber
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
say "Install Perron in your Rails app"
|
|
2
|
+
|
|
3
|
+
say "Create Perron initializer"
|
|
4
|
+
copy_file "#{__dir__}/install/initializer.rb", "config/initializers/perron.rb"
|
|
5
|
+
|
|
6
|
+
say "Create content data directory"
|
|
7
|
+
copy_file "#{__dir__}/install/README.md", "app/content/data/README.md"
|
|
8
|
+
|
|
9
|
+
say "Create deploy configuration"
|
|
10
|
+
copy_file "#{__dir__}/install/deploy.yml", "config/deploy.yml"
|
|
11
|
+
|
|
12
|
+
say "Add Markdown gem options to Gemfile"
|
|
13
|
+
append_to_file "Gemfile", <<~RUBY
|
|
14
|
+
|
|
15
|
+
# Perron supports Markdown rendering using one of the following gems.
|
|
16
|
+
# Uncomment your preferred choice and run `bundle install`
|
|
17
|
+
# gem "commonmarker"
|
|
18
|
+
# gem "kramdown"
|
|
19
|
+
# gem "redcarpet"
|
|
20
|
+
RUBY
|
|
21
|
+
|
|
22
|
+
copy_file "#{__dir__}/assets/icon.png", "public/icon.png", force: true
|
|
23
|
+
copy_file "#{__dir__}/assets/icon.svg", "public/icon.svg", force: true
|
|
24
|
+
|
|
25
|
+
say "Add output folder to .gitignore"
|
|
26
|
+
append_to_file ".gitignore", "/#{Perron.configuration.output}/\n"
|
data/lib/perron/markdown.rb
CHANGED
|
@@ -5,9 +5,9 @@ require "perron/html_processor"
|
|
|
5
5
|
module Perron
|
|
6
6
|
class Markdown
|
|
7
7
|
class << self
|
|
8
|
-
def render(text, processors: [])
|
|
8
|
+
def render(text, processors: [], resource: nil)
|
|
9
9
|
parser.parse(text)
|
|
10
|
-
.then { Perron::HtmlProcessor.new(it, processors: processors).process }
|
|
10
|
+
.then { Perron::HtmlProcessor.new(it, processors: processors, resource: resource).process }
|
|
11
11
|
.html_safe
|
|
12
12
|
end
|
|
13
13
|
|
data/lib/perron/output_server.rb
CHANGED
|
@@ -8,6 +8,7 @@ module Perron
|
|
|
8
8
|
|
|
9
9
|
def call(environment)
|
|
10
10
|
return @app.call(environment) if disabled?
|
|
11
|
+
return not_found if !static_file(environment) && Perron.configuration.output_server_strict
|
|
11
12
|
|
|
12
13
|
static_file(environment).then do |file|
|
|
13
14
|
file ? serve(file) : @app.call(environment)
|
|
@@ -41,6 +42,14 @@ module Perron
|
|
|
41
42
|
]
|
|
42
43
|
end
|
|
43
44
|
|
|
45
|
+
def not_found
|
|
46
|
+
[
|
|
47
|
+
404,
|
|
48
|
+
{"Content-Type" => "text/plain"},
|
|
49
|
+
["Not Found"]
|
|
50
|
+
]
|
|
51
|
+
end
|
|
52
|
+
|
|
44
53
|
def enabled? = Dir.exist?(output_path)
|
|
45
54
|
|
|
46
55
|
def inject_preview_indicator(content)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Perron
|
|
4
|
+
class Paginate
|
|
5
|
+
def initialize(collection, page:, per_page:, base_path: nil, page_path_template: nil, use_query_params: false)
|
|
6
|
+
@collection = collection
|
|
7
|
+
@per_page = per_page
|
|
8
|
+
@base_path = base_path
|
|
9
|
+
@page_path_template = page_path_template || "/page/:page/"
|
|
10
|
+
@use_query_params = use_query_params
|
|
11
|
+
|
|
12
|
+
@total_items = collection.size
|
|
13
|
+
@total_pages = total_items.zero? ? 0 : (total_items.to_f / per_page).ceil
|
|
14
|
+
@current_page = page.clamp(1, total_pages.zero? ? 1 : total_pages)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
attr_reader :current_page, :total_pages, :total_items, :per_page
|
|
18
|
+
|
|
19
|
+
def items
|
|
20
|
+
offset = (@current_page - 1) * @per_page
|
|
21
|
+
|
|
22
|
+
@collection[offset, @per_page] || []
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def next? = @current_page < @total_pages
|
|
26
|
+
|
|
27
|
+
def previous? = @current_page > 1
|
|
28
|
+
|
|
29
|
+
def next
|
|
30
|
+
return unless next?
|
|
31
|
+
|
|
32
|
+
page_path(@current_page + 1)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def previous
|
|
36
|
+
return unless previous?
|
|
37
|
+
|
|
38
|
+
target = (@current_page == 2) ? 1 : @current_page - 1
|
|
39
|
+
|
|
40
|
+
page_path(target)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
attr_reader :use_query_params
|
|
46
|
+
|
|
47
|
+
def page_path(number)
|
|
48
|
+
return if number < 1
|
|
49
|
+
return if number > @total_pages && @total_pages > 0
|
|
50
|
+
|
|
51
|
+
return @base_path if number <= 1
|
|
52
|
+
|
|
53
|
+
return "#{@base_path}?page=#{number}" if @use_query_params
|
|
54
|
+
|
|
55
|
+
@base_path.sub(/\/$/, "") + @page_path_template.sub(":page", number.to_s)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
data/lib/perron/relation.rb
CHANGED
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
module Perron
|
|
4
4
|
class Relation < Array
|
|
5
|
-
def initialize(resources = [])
|
|
6
|
-
super
|
|
5
|
+
def initialize(resources = [], model_class = nil)
|
|
6
|
+
super(resources)
|
|
7
|
+
|
|
8
|
+
@model_class = model_class
|
|
7
9
|
end
|
|
10
|
+
attr_reader :model_class
|
|
8
11
|
|
|
9
12
|
def where(**conditions)
|
|
10
13
|
filtered = select do |resource|
|
|
@@ -19,12 +22,12 @@ module Perron
|
|
|
19
22
|
end
|
|
20
23
|
end
|
|
21
24
|
|
|
22
|
-
Relation.new(filtered)
|
|
25
|
+
Relation.new(filtered, @model_class)
|
|
23
26
|
end
|
|
24
27
|
|
|
25
|
-
def limit(count) = Relation.new(first(count))
|
|
28
|
+
def limit(count) = Relation.new(first(count), @model_class)
|
|
26
29
|
|
|
27
|
-
def offset(count) = Relation.new(drop(count))
|
|
30
|
+
def offset(count) = Relation.new(drop(count), @model_class)
|
|
28
31
|
|
|
29
32
|
def order(attribute, direction = :asc)
|
|
30
33
|
if attribute.is_a?(Hash)
|
|
@@ -33,7 +36,7 @@ module Perron
|
|
|
33
36
|
|
|
34
37
|
sorted = sort_by { it.public_send(attribute) }
|
|
35
38
|
|
|
36
|
-
Relation.new((direction == :desc) ? sorted.reverse : sorted)
|
|
39
|
+
Relation.new((direction == :desc) ? sorted.reverse : sorted, @model_class)
|
|
37
40
|
end
|
|
38
41
|
|
|
39
42
|
def pluck(*attributes)
|
|
@@ -47,5 +50,20 @@ module Perron
|
|
|
47
50
|
end
|
|
48
51
|
end
|
|
49
52
|
end
|
|
53
|
+
|
|
54
|
+
def in_order_of(attribute, values, filter: true)
|
|
55
|
+
return Relation.new([]) if values.empty?
|
|
56
|
+
|
|
57
|
+
indexed = values.each_with_index.to_h
|
|
58
|
+
|
|
59
|
+
resources = if filter
|
|
60
|
+
select { indexed.key?(it.public_send(attribute)) }
|
|
61
|
+
.sort_by { indexed[it.public_send(attribute)] }
|
|
62
|
+
else
|
|
63
|
+
sort_by { indexed[it.public_send(attribute)] || Float::INFINITY }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
Relation.new(resources)
|
|
67
|
+
end
|
|
50
68
|
end
|
|
51
69
|
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Perron
|
|
4
|
+
class Resource
|
|
5
|
+
module Adjacency
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
class_methods do
|
|
9
|
+
def adjacent_by(position_method, within: {})
|
|
10
|
+
return unless within.present?
|
|
11
|
+
|
|
12
|
+
grouping_method = within.is_a?(Hash) ? within.keys.first : within
|
|
13
|
+
grouping_order = within.is_a?(Hash) ? within.values.first : nil
|
|
14
|
+
|
|
15
|
+
define_method(:next) do
|
|
16
|
+
resources = self.class.all.sort_by do |resource|
|
|
17
|
+
group_value = resource.public_send(grouping_method)
|
|
18
|
+
|
|
19
|
+
group_order = if grouping_order
|
|
20
|
+
grouping_order.index(group_value.to_s) || grouping_order.index(group_value.to_sym) || Float::INFINITY
|
|
21
|
+
else
|
|
22
|
+
group_value.to_s
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
[group_order, resource.public_send(position_method)]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
return if (position = resources.index { it.id == id }).nil? || position >= resources.size - 1
|
|
29
|
+
|
|
30
|
+
resources[position + 1]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
define_method(:previous) do
|
|
34
|
+
resources = self.class.all.sort_by do |resource|
|
|
35
|
+
group_value = resource.public_send(grouping_method)
|
|
36
|
+
|
|
37
|
+
group_order = if grouping_order
|
|
38
|
+
grouping_order.index(group_value.to_s) || grouping_order.index(group_value.to_sym) || Float::INFINITY
|
|
39
|
+
else
|
|
40
|
+
group_value.to_s
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
[group_order, resource.public_send(position_method)]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
return if (position = resources.index { it.id == id }).nil? || position <= 0
|
|
47
|
+
|
|
48
|
+
resources[position - 1]
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
included do
|
|
54
|
+
define_method(:next) do
|
|
55
|
+
resources = self.class.all
|
|
56
|
+
return if (position = resources.index { it.id == id }).nil? || position >= resources.size - 1
|
|
57
|
+
|
|
58
|
+
resources[position + 1]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
define_method(:previous) do
|
|
62
|
+
resources = self.class.all
|
|
63
|
+
return if (position = resources.index { it.id == id }).nil? || position <= 0
|
|
64
|
+
|
|
65
|
+
resources[position - 1]
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -22,6 +22,8 @@ module Perron
|
|
|
22
22
|
|
|
23
23
|
def order(attribute, direction = :asc) = all.order(attribute, direction)
|
|
24
24
|
|
|
25
|
+
def in_order_of(attribute, values, filter: true) = all.in_order_of(attribute, values, filter:)
|
|
26
|
+
|
|
25
27
|
def first(n = nil)
|
|
26
28
|
n ? all.first(n) : all[0]
|
|
27
29
|
end
|
|
@@ -44,6 +46,10 @@ module Perron
|
|
|
44
46
|
|
|
45
47
|
def root = all.find(&:root?)
|
|
46
48
|
|
|
49
|
+
def destroy_all
|
|
50
|
+
all.each(&:destroy)
|
|
51
|
+
end
|
|
52
|
+
|
|
47
53
|
def model_name
|
|
48
54
|
@model_name ||= ActiveModel::Name.new(self, nil, name.demodulize.to_s)
|
|
49
55
|
end
|