perron 0.0.1 → 0.6.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 +17 -0
- data/Gemfile.lock +273 -0
- data/README.md +221 -0
- data/Rakefile +9 -0
- data/app/helpers/meta_tags_helper.rb +17 -0
- data/app/helpers/perron/markdown_helper.rb +9 -0
- data/bin/console +10 -0
- data/bin/rails +17 -0
- data/bin/release +19 -0
- data/bin/setup +8 -0
- data/lib/generators/content/content_generator.rb +45 -0
- data/lib/generators/content/templates/controller.rb.tt +17 -0
- data/lib/generators/content/templates/index.html.erb.tt +11 -0
- data/lib/generators/content/templates/model.rb.tt +2 -0
- data/lib/generators/content/templates/root.erb.tt +5 -0
- data/lib/generators/content/templates/show.html.erb.tt +7 -0
- data/lib/generators/perron/install_generator.rb +16 -0
- data/lib/generators/perron/templates/README.md.tt +30 -0
- data/lib/generators/perron/templates/initializer.rb.tt +22 -0
- data/lib/perron/configuration.rb +63 -0
- data/lib/perron/engine.rb +13 -0
- data/lib/perron/errors.rb +13 -0
- data/lib/perron/html_processor/target_blank.rb +23 -0
- data/lib/perron/html_processor.rb +28 -0
- data/lib/perron/markdown.rb +54 -0
- data/lib/perron/metatags.rb +82 -0
- data/lib/perron/refinements/delete_suffixes.rb +12 -0
- data/lib/perron/root.rb +13 -0
- data/lib/perron/site/builder/assets.rb +67 -0
- data/lib/perron/site/builder/page.rb +51 -0
- data/lib/perron/site/builder/paths.rb +40 -0
- data/lib/perron/site/builder/public_files.rb +40 -0
- data/lib/perron/site/builder.rb +44 -0
- data/lib/perron/site/collection.rb +28 -0
- data/lib/perron/site/data.rb +144 -0
- data/lib/perron/site/resource/class_methods.rb +43 -0
- data/lib/perron/site/resource/core.rb +15 -0
- data/lib/perron/site/resource/publishable.rb +51 -0
- data/lib/perron/site/resource/separator.rb +31 -0
- data/lib/perron/site/resource/slug.rb +20 -0
- data/lib/perron/site/resource.rb +62 -0
- data/lib/perron/site.rb +34 -0
- data/lib/perron/tasks/perron.rake +12 -0
- data/lib/perron/version.rb +3 -0
- data/lib/perron.rb +11 -1
- data/perron.gemspec +26 -0
- metadata +112 -6
@@ -0,0 +1,16 @@
|
|
1
|
+
module Perron
|
2
|
+
class InstallGenerator < Rails::Generators::Base
|
3
|
+
source_root File.expand_path("templates", __dir__)
|
4
|
+
|
5
|
+
def copy_initializer
|
6
|
+
template "initializer.rb.tt", "config/initializers/perron.rb"
|
7
|
+
end
|
8
|
+
|
9
|
+
def create_data_directory
|
10
|
+
data_directory = Rails.root.join("app", "views", "content", "data")
|
11
|
+
empty_directory data_directory
|
12
|
+
|
13
|
+
template "README.md.tt", File.join(data_directory, "README.md")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# Data
|
2
|
+
|
3
|
+
Perron can consume structured data from YML, JSON, or CSV files, making them available within your templates.
|
4
|
+
This is useful for populating features, team members, or any other repeated data structure.
|
5
|
+
|
6
|
+
|
7
|
+
## Usage
|
8
|
+
|
9
|
+
To use a data file, instantiate `Perron::Data` with the basename of the file and iterate over the result.
|
10
|
+
```erb
|
11
|
+
<% Perron::Data.new("features").each do |feature| %>
|
12
|
+
<h4><%= feature.name %></h4>
|
13
|
+
|
14
|
+
<p><%= feature.description %></p>
|
15
|
+
<% end %>
|
16
|
+
```
|
17
|
+
|
18
|
+
## File Location and Formats
|
19
|
+
|
20
|
+
By default, Perron looks up `app/content/data/` for files with a `.yml`, `.json`, or `.csv` extension.
|
21
|
+
For a `new("features")` call, it would find `features.yml`, `features.json`, or `features.csv`. You can also provide a full, absolute path to any data file.
|
22
|
+
|
23
|
+
|
24
|
+
## Accessing Data
|
25
|
+
|
26
|
+
The wrapper object provides flexible, read-only access to each record's attributes. Both dot notation and hash-like key access are supported.
|
27
|
+
```ruby
|
28
|
+
feature.name
|
29
|
+
feature[:name]
|
30
|
+
```
|
@@ -0,0 +1,22 @@
|
|
1
|
+
Perron.configure do |config|
|
2
|
+
# config.output = "output"
|
3
|
+
|
4
|
+
# config.site_name = "AppRefresher"
|
5
|
+
|
6
|
+
# The build mode for Perron. Can be :standalone or :integrated.
|
7
|
+
# config.mode = :standalone
|
8
|
+
|
9
|
+
# In `integrated` mode, the root is skipped by default. Set to `true` to enable.
|
10
|
+
# config.include_root = false
|
11
|
+
|
12
|
+
# config.default_url_options = {host: "apprefresher.com", protocol: "https", trailing_slash: true}
|
13
|
+
|
14
|
+
# Override the defaults (meta) title suffix
|
15
|
+
# Default: `— Perron.configuration.site_name`
|
16
|
+
# config.title_suffix = nil
|
17
|
+
|
18
|
+
# Set default meta values
|
19
|
+
# Examples:
|
20
|
+
# - `config.metadata.description = "AI-powered tool to keep your knowledge base articles images/screenshots and content up-to-date"`
|
21
|
+
# - `config.metadata.author = "Rails Designer"`
|
22
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Perron
|
4
|
+
def self.configuration
|
5
|
+
@configuration ||= Configuration.new
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.configure
|
9
|
+
yield(configuration)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.reset_configuration!
|
13
|
+
@configuration = Configuration.new
|
14
|
+
end
|
15
|
+
|
16
|
+
class Configuration
|
17
|
+
def initialize
|
18
|
+
@config = ActiveSupport::OrderedOptions.new
|
19
|
+
|
20
|
+
@config.output = "output"
|
21
|
+
|
22
|
+
@config.mode = :standalone
|
23
|
+
@config.include_root = false
|
24
|
+
|
25
|
+
@config.site_name = nil
|
26
|
+
@config.title_suffix = nil
|
27
|
+
|
28
|
+
@config.allowed_extensions = [".erb", ".md"]
|
29
|
+
@config.exclude_from_public = %w[assets storage]
|
30
|
+
@config.excluded_assets = %w[action_cable actioncable actiontext activestorage rails-ujs trix turbo]
|
31
|
+
|
32
|
+
@config.default_url_options = {
|
33
|
+
host: ENV.fetch("PERRON_HOST", "localhost:3000"),
|
34
|
+
protocol: ENV.fetch("PERRON_PROTOCOL", "http"),
|
35
|
+
trailing_slash: ENV.fetch("PERRON_TRAILING_SLASH", "true") == "true"
|
36
|
+
}
|
37
|
+
|
38
|
+
@config.metadata = ActiveSupport::OrderedOptions.new
|
39
|
+
end
|
40
|
+
|
41
|
+
def input = "app/content"
|
42
|
+
|
43
|
+
def output
|
44
|
+
mode.integrated? ? "public" : @config.output
|
45
|
+
end
|
46
|
+
|
47
|
+
def mode = @config.mode.to_s.inquiry
|
48
|
+
|
49
|
+
def exclude_root? = !@config.include_root
|
50
|
+
|
51
|
+
def method_missing(method_name, ...)
|
52
|
+
if @config.respond_to?(method_name)
|
53
|
+
@config.send(method_name, ...)
|
54
|
+
else
|
55
|
+
super
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def respond_to_missing?(method_name)
|
60
|
+
@config.respond_to?(method_name) || super
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Perron
|
4
|
+
class Engine < Rails::Engine
|
5
|
+
initializer "perron.default_url_options" do |app|
|
6
|
+
app.config.action_controller.default_url_options = Perron.configuration.default_url_options
|
7
|
+
end
|
8
|
+
|
9
|
+
rake_tasks do
|
10
|
+
load File.expand_path("../tasks/perron.rake", __FILE__)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Perron
|
2
|
+
module Errors
|
3
|
+
class CollectionNotFoundError < StandardError; end
|
4
|
+
|
5
|
+
class FileNotFoundError < StandardError; end
|
6
|
+
|
7
|
+
class ResourceNotFoundError < StandardError; end
|
8
|
+
|
9
|
+
class UnsupportedDataFormatError < StandardError; end
|
10
|
+
|
11
|
+
class DataParseError < StandardError; end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Perron
|
4
|
+
class HtmlProcessor
|
5
|
+
class TargetBlank
|
6
|
+
def initialize(html)
|
7
|
+
@html = html
|
8
|
+
end
|
9
|
+
|
10
|
+
def process
|
11
|
+
@html.css("a").each do |link|
|
12
|
+
href = link["href"]
|
13
|
+
|
14
|
+
next unless href
|
15
|
+
next if href.start_with?("/", "#", "mailto:")
|
16
|
+
|
17
|
+
link["target"] = "_blank"
|
18
|
+
link["rel"] = "noopener"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "perron/html_processor/target_blank"
|
4
|
+
|
5
|
+
module Perron
|
6
|
+
class HtmlProcessor
|
7
|
+
def initialize(html)
|
8
|
+
@html = html
|
9
|
+
end
|
10
|
+
|
11
|
+
def process
|
12
|
+
document = Nokogiri::HTML::DocumentFragment.parse(@html)
|
13
|
+
|
14
|
+
PROCESSORS.each do |processor|
|
15
|
+
processor.new(document).process
|
16
|
+
end
|
17
|
+
|
18
|
+
document.to_html
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
# TODO: should be a configuration option
|
24
|
+
PROCESSORS = [
|
25
|
+
Perron::HtmlProcessor::TargetBlank
|
26
|
+
]
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "perron/html_processor"
|
4
|
+
|
5
|
+
module Perron
|
6
|
+
class Markdown
|
7
|
+
class << self
|
8
|
+
def render(text)
|
9
|
+
parser.parse(text)
|
10
|
+
.then { Perron::HtmlProcessor.new(it).process }
|
11
|
+
.html_safe
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def parser
|
17
|
+
@parser ||= markdown_parser
|
18
|
+
end
|
19
|
+
|
20
|
+
def markdown_parser
|
21
|
+
if defined?(::CommonMarker)
|
22
|
+
CommonMarkerParser.new
|
23
|
+
elsif defined?(::Kramdown)
|
24
|
+
KramdownParser.new
|
25
|
+
elsif defined?(::Redcarpet)
|
26
|
+
RedcarpetParser.new
|
27
|
+
else
|
28
|
+
PlainTextParser.new
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class CommonMarkerParser
|
34
|
+
def parse(text) = CommonMarker.render_html(text, :DEFAULT)
|
35
|
+
end
|
36
|
+
|
37
|
+
class KramdownParser
|
38
|
+
def parse(text) = Kramdown::Document.new(text).to_html
|
39
|
+
end
|
40
|
+
|
41
|
+
class RedcarpetParser
|
42
|
+
def parse(text)
|
43
|
+
renderer = Redcarpet::Render::HTML.new(filter_html: true)
|
44
|
+
markdown = Redcarpet::Markdown.new(renderer, autolink: true, tables: true)
|
45
|
+
|
46
|
+
markdown.render(text)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class PlainTextParser
|
51
|
+
def parse(text) = text.to_s
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Perron
|
4
|
+
class Metatags
|
5
|
+
include ActionView::Helpers::TagHelper
|
6
|
+
|
7
|
+
def initialize(resource)
|
8
|
+
@resource = resource
|
9
|
+
@config = Perron.configuration
|
10
|
+
end
|
11
|
+
|
12
|
+
def render(options = {})
|
13
|
+
keys = tags.keys
|
14
|
+
.then { options[:only] ? it & options[:only].map(&:to_sym) : it }
|
15
|
+
.then { options[:except] ? it - options[:except].map(&:to_sym) : it }
|
16
|
+
|
17
|
+
safe_join(keys.filter_map { tags[it].presence }, "\n")
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
FRONTMATTER_KEY_MAP = {
|
23
|
+
"image" => %w[og:image twitter:image],
|
24
|
+
"author" => %w[og:author]
|
25
|
+
}.freeze
|
26
|
+
|
27
|
+
def tags
|
28
|
+
@tags ||= begin
|
29
|
+
frontmatter = @resource&.metadata&.stringify_keys || {}
|
30
|
+
defaults = @config.metadata
|
31
|
+
|
32
|
+
title = frontmatter["title"] || defaults["title"] || @config.site_name
|
33
|
+
description = frontmatter["description"] || defaults["description"]
|
34
|
+
author = frontmatter["author"] || defaults["author"]
|
35
|
+
image = frontmatter["image"] || defaults["image"]
|
36
|
+
og_image = frontmatter["og:image"] || image
|
37
|
+
twitter_image = frontmatter["twitter:image"] || og_image
|
38
|
+
|
39
|
+
{
|
40
|
+
title: title_tag(title),
|
41
|
+
description: meta_tag(name: "description", content: description),
|
42
|
+
|
43
|
+
og_type: meta_tag(property: "og:type", content: frontmatter["og:type"] || "article"),
|
44
|
+
og_title: meta_tag(property: "og:title", content: frontmatter["og:title"] || title),
|
45
|
+
og_description: meta_tag(property: "og:description", content: frontmatter["og:description"] || description),
|
46
|
+
og_site_name: meta_tag(property: "og:site_name", content: @config.site_name),
|
47
|
+
og_image: meta_tag(property: "og:image", content: og_image),
|
48
|
+
og_author: meta_tag(property: "og:author", content: frontmatter["og:author"] || author),
|
49
|
+
|
50
|
+
twitter_card: meta_tag(name: "twitter:card", content: frontmatter["twitter:card"] || "summary_large_image"),
|
51
|
+
twitter_title: meta_tag(name: "twitter:title", content: frontmatter["twitter:title"] || title),
|
52
|
+
twitter_description: meta_tag(name: "twitter:description", content: frontmatter["twitter:description"] || description),
|
53
|
+
twitter_image: meta_tag(name: "twitter:image", content: twitter_image),
|
54
|
+
article_published: meta_tag(property: "article:published_time", content: @resource&.published_at),
|
55
|
+
|
56
|
+
og_url: meta_tag(property: "og:url", content: canonical_url)
|
57
|
+
}
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def title_tag(content)
|
62
|
+
tag.title(
|
63
|
+
content.then { (it == @config.site_name) ? it : "#{it} #{@config.title_suffix || "— #{@config.site_name}"}" }
|
64
|
+
)
|
65
|
+
end
|
66
|
+
|
67
|
+
def meta_tag(attributes)
|
68
|
+
return if attributes[:content].blank?
|
69
|
+
|
70
|
+
tag.meta(**attributes)
|
71
|
+
end
|
72
|
+
|
73
|
+
def canonical_url
|
74
|
+
url_options = @config.default_url_options
|
75
|
+
base_url = "#{url_options[:protocol]}://#{url_options[:host]}"
|
76
|
+
url = URI.join(base_url, @resource&.path).to_s
|
77
|
+
has_extension = URI(url).path.split("/").last&.include?(".")
|
78
|
+
|
79
|
+
url.then { (url_options[:trailing_slash] && !it.end_with?("/") && !has_extension) ? "#{it}/" : it }
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
data/lib/perron/root.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Perron
|
4
|
+
module Site
|
5
|
+
class Builder
|
6
|
+
class Assets
|
7
|
+
def initialize
|
8
|
+
@output_path = Rails.root.join(Perron.configuration.output)
|
9
|
+
end
|
10
|
+
|
11
|
+
def prepare
|
12
|
+
puts "📦 Precompiling and copying assets…"
|
13
|
+
|
14
|
+
success = system("bundle exec rails assets:precompile", out: File::NULL, err: File::NULL)
|
15
|
+
|
16
|
+
unless success
|
17
|
+
puts "❌ ERROR: Asset precompilation failed"
|
18
|
+
|
19
|
+
exit(1)
|
20
|
+
end
|
21
|
+
|
22
|
+
source = Rails.root.join("public", "assets")
|
23
|
+
destination = @output_path.join("assets")
|
24
|
+
|
25
|
+
unless Dir.exist?(source)
|
26
|
+
puts "⚠️ WARNING: No assets found in `#{source}` to copy"
|
27
|
+
|
28
|
+
return
|
29
|
+
end
|
30
|
+
|
31
|
+
FileUtils.mkdir_p(destination)
|
32
|
+
FileUtils.cp_r(Dir.glob("#{source}/*"), destination)
|
33
|
+
|
34
|
+
puts " ✅ Copied assets to `#{destination.relative_path_from(Rails.root)}`"
|
35
|
+
|
36
|
+
prune_excluded_assets from: destination
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def prune_excluded_assets(from:)
|
42
|
+
return if exclusions.empty?
|
43
|
+
|
44
|
+
puts "Pruning excluded assets…"
|
45
|
+
|
46
|
+
pattern = /^(#{exclusions.join("|")})(\.esm|\.min)?-[a-f0-9]{8,}/
|
47
|
+
|
48
|
+
Dir.glob("#{from}/**/*").each do |path|
|
49
|
+
next if File.directory?(path)
|
50
|
+
|
51
|
+
filename = File.basename(path)
|
52
|
+
|
53
|
+
if filename.match?(pattern)
|
54
|
+
FileUtils.rm(path)
|
55
|
+
|
56
|
+
map_file = "#{path}.map"
|
57
|
+
|
58
|
+
FileUtils.rm(map_file) if File.exist?(map_file)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def exclusions = Perron.configuration.excluded_assets
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rack/mock"
|
4
|
+
|
5
|
+
module Perron
|
6
|
+
module Site
|
7
|
+
class Builder
|
8
|
+
class Page
|
9
|
+
def initialize(path)
|
10
|
+
@output_path, @path = Rails.root.join(Perron.configuration.output), path
|
11
|
+
end
|
12
|
+
|
13
|
+
def render
|
14
|
+
action = route_info[:action]
|
15
|
+
request = ActionDispatch::Request.new(env)
|
16
|
+
response = ActionDispatch::Response.new
|
17
|
+
|
18
|
+
request.path_parameters = route_info
|
19
|
+
|
20
|
+
controller.dispatch(action, request, response)
|
21
|
+
|
22
|
+
return puts " ❌ ERROR: Request failed for '#{@path}' (Status: #{response.status})" unless response.successful?
|
23
|
+
|
24
|
+
save_html(response.body)
|
25
|
+
rescue => error
|
26
|
+
puts " ❌ ERROR: Failed to generate page for '#{@path}'. Details: #{error.class} - #{error.message}\n#{error.backtrace.first(3).join("\n")}"
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def save_html(html)
|
32
|
+
directory_path = @output_path.join(@path.delete_prefix("/"))
|
33
|
+
file_path = directory_path.join("index.html")
|
34
|
+
|
35
|
+
FileUtils.mkdir_p(directory_path)
|
36
|
+
File.write(file_path, html)
|
37
|
+
|
38
|
+
puts "✅ Generated: #{@path} -> #{file_path.relative_path_from(@output_path)}"
|
39
|
+
end
|
40
|
+
|
41
|
+
def route_info
|
42
|
+
@route_info ||= Rails.application.routes.recognize_path(@path)
|
43
|
+
end
|
44
|
+
|
45
|
+
def env = Rack::MockRequest.env_for(@path, "HTTP_HOST" => Perron.configuration.default_url_options[:host])
|
46
|
+
|
47
|
+
def controller = "#{route_info[:controller]}_controller".classify.constantize.new
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Perron
|
4
|
+
module Site
|
5
|
+
class Builder
|
6
|
+
class Paths
|
7
|
+
def initialize(collection, paths)
|
8
|
+
@collection, @paths = collection, paths
|
9
|
+
end
|
10
|
+
|
11
|
+
def get
|
12
|
+
@paths << routes.public_send(index_path) if routes.respond_to?(index_path)
|
13
|
+
|
14
|
+
if routes.respond_to?(show_path)
|
15
|
+
@collection.all.each do |resource|
|
16
|
+
root = resource.slug == "/"
|
17
|
+
|
18
|
+
next if skip? root
|
19
|
+
|
20
|
+
@paths << (root ? routes.root_path : routes.public_send(show_path, resource))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def skip?(root)
|
28
|
+
root &&
|
29
|
+
Perron.configuration.mode.integrated? && Perron.configuration.exclude_root?
|
30
|
+
end
|
31
|
+
|
32
|
+
def routes = Rails.application.routes.url_helpers
|
33
|
+
|
34
|
+
def index_path = "#{@collection.name}_path"
|
35
|
+
|
36
|
+
def show_path = "#{@collection.name.singularize}_path"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Perron
|
4
|
+
module Site
|
5
|
+
class Builder
|
6
|
+
class PublicFiles
|
7
|
+
def initialize
|
8
|
+
@output_path = Rails.root.join(Perron.configuration.output)
|
9
|
+
@public_dir = Rails.root.join("public")
|
10
|
+
end
|
11
|
+
|
12
|
+
def copy
|
13
|
+
puts "📂 Copying public files…"
|
14
|
+
|
15
|
+
return unless Dir.exist?(@public_dir)
|
16
|
+
|
17
|
+
if paths.empty?
|
18
|
+
puts " - No public files to copy"
|
19
|
+
|
20
|
+
return
|
21
|
+
end
|
22
|
+
|
23
|
+
paths.each do |path|
|
24
|
+
FileUtils.cp_r(path, @output_path)
|
25
|
+
|
26
|
+
puts " ✅ Copied: #{File.basename(path)}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def paths
|
33
|
+
@paths ||= Dir.glob(File.join(@public_dir, "*")).reject do |path|
|
34
|
+
Set.new(Perron.configuration.exclude_from_public + %w[. ..]).include?(File.basename(path))
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "perron/site/builder/assets"
|
4
|
+
require "perron/site/builder/public_files"
|
5
|
+
require "perron/site/builder/paths"
|
6
|
+
require "perron/site/builder/page"
|
7
|
+
|
8
|
+
module Perron
|
9
|
+
module Site
|
10
|
+
class Builder
|
11
|
+
def initialize
|
12
|
+
@output_path = Rails.root.join(Perron.configuration.output)
|
13
|
+
end
|
14
|
+
|
15
|
+
def build
|
16
|
+
if Perron.configuration.mode.standalone?
|
17
|
+
puts "🧹 Cleaning previous build…"
|
18
|
+
FileUtils.rm_rf(Dir.glob("#{@output_path}/*"))
|
19
|
+
|
20
|
+
Perron::Site::Builder::Assets.new.prepare
|
21
|
+
Perron::Site::Builder::PublicFiles.new.copy
|
22
|
+
end
|
23
|
+
|
24
|
+
puts "🚀 Starting site build…"
|
25
|
+
puts "-" * 15
|
26
|
+
|
27
|
+
paths.each { render_page(it) }
|
28
|
+
|
29
|
+
puts "-" * 15
|
30
|
+
puts "✅ Build complete"
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def paths
|
36
|
+
Set.new.tap do |paths|
|
37
|
+
Perron::Site.collections.each { Perron::Site::Builder::Paths.new(it, paths).get }
|
38
|
+
end.to_a
|
39
|
+
end
|
40
|
+
|
41
|
+
def render_page(path) = Perron::Site::Builder::Page.new(path).render
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Perron
|
4
|
+
class Collection
|
5
|
+
attr_reader :name
|
6
|
+
|
7
|
+
def initialize(name)
|
8
|
+
@name = name
|
9
|
+
@collection_path = File.join(Rails.root, Perron.configuration.input, name)
|
10
|
+
|
11
|
+
raise Errors::CollectionNotFoundError, "No such collection: #{name}" unless File.exist?(@collection_path) && File.directory?(@collection_path)
|
12
|
+
end
|
13
|
+
|
14
|
+
def all(resource_class = "Content::#{name.classify}".safe_constantize)
|
15
|
+
@all ||= Dir.glob("#{@collection_path}/**/*.*").map do |file_path|
|
16
|
+
resource_class.new(file_path)
|
17
|
+
end.select(&:published?)
|
18
|
+
end
|
19
|
+
|
20
|
+
def find(slug, resource_class = Resource)
|
21
|
+
resource = all(resource_class).find { it.slug == slug }
|
22
|
+
|
23
|
+
return resource if resource
|
24
|
+
|
25
|
+
raise Errors::ResourceNotFoundError, "Resource not found with slug: #{slug}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|