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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +17 -0
  3. data/Gemfile.lock +273 -0
  4. data/README.md +221 -0
  5. data/Rakefile +9 -0
  6. data/app/helpers/meta_tags_helper.rb +17 -0
  7. data/app/helpers/perron/markdown_helper.rb +9 -0
  8. data/bin/console +10 -0
  9. data/bin/rails +17 -0
  10. data/bin/release +19 -0
  11. data/bin/setup +8 -0
  12. data/lib/generators/content/content_generator.rb +45 -0
  13. data/lib/generators/content/templates/controller.rb.tt +17 -0
  14. data/lib/generators/content/templates/index.html.erb.tt +11 -0
  15. data/lib/generators/content/templates/model.rb.tt +2 -0
  16. data/lib/generators/content/templates/root.erb.tt +5 -0
  17. data/lib/generators/content/templates/show.html.erb.tt +7 -0
  18. data/lib/generators/perron/install_generator.rb +16 -0
  19. data/lib/generators/perron/templates/README.md.tt +30 -0
  20. data/lib/generators/perron/templates/initializer.rb.tt +22 -0
  21. data/lib/perron/configuration.rb +63 -0
  22. data/lib/perron/engine.rb +13 -0
  23. data/lib/perron/errors.rb +13 -0
  24. data/lib/perron/html_processor/target_blank.rb +23 -0
  25. data/lib/perron/html_processor.rb +28 -0
  26. data/lib/perron/markdown.rb +54 -0
  27. data/lib/perron/metatags.rb +82 -0
  28. data/lib/perron/refinements/delete_suffixes.rb +12 -0
  29. data/lib/perron/root.rb +13 -0
  30. data/lib/perron/site/builder/assets.rb +67 -0
  31. data/lib/perron/site/builder/page.rb +51 -0
  32. data/lib/perron/site/builder/paths.rb +40 -0
  33. data/lib/perron/site/builder/public_files.rb +40 -0
  34. data/lib/perron/site/builder.rb +44 -0
  35. data/lib/perron/site/collection.rb +28 -0
  36. data/lib/perron/site/data.rb +144 -0
  37. data/lib/perron/site/resource/class_methods.rb +43 -0
  38. data/lib/perron/site/resource/core.rb +15 -0
  39. data/lib/perron/site/resource/publishable.rb +51 -0
  40. data/lib/perron/site/resource/separator.rb +31 -0
  41. data/lib/perron/site/resource/slug.rb +20 -0
  42. data/lib/perron/site/resource.rb +62 -0
  43. data/lib/perron/site.rb +34 -0
  44. data/lib/perron/tasks/perron.rake +12 -0
  45. data/lib/perron/version.rb +3 -0
  46. data/lib/perron.rb +11 -1
  47. data/perron.gemspec +26 -0
  48. metadata +112 -6
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "csv"
4
+
5
+ module Perron
6
+ class Data < SimpleDelegator
7
+ def initialize(identifier)
8
+ @file_path = path_for(identifier)
9
+ @records = records
10
+
11
+ super(records)
12
+ end
13
+
14
+ private
15
+
16
+ PARSER_METHODS = {
17
+ ".yml" => :parse_yaml, ".yaml" => :parse_yaml,
18
+ ".json" => :parse_json, ".csv" => :parse_csv
19
+ }.freeze
20
+ SUPPORTED_EXTENSIONS = PARSER_METHODS.keys
21
+
22
+ def path_for(identifier)
23
+ path = Pathname.new(identifier)
24
+
25
+ return path.to_s if path.file? && path.absolute?
26
+
27
+ path = SUPPORTED_EXTENSIONS.lazy.map { Rails.root.join("app", "content", "data").join("#{identifier}#{it}") }.find(&:exist?)
28
+ path&.to_s or raise Errors::FileNotFoundError, "No data file found for '#{identifier}'"
29
+ end
30
+
31
+ def records
32
+ content = File.read(@file_path)
33
+ extension = File.extname(@file_path)
34
+ parser = PARSER_METHODS.fetch(extension) do
35
+ raise Errors::UnsupportedDataFormatError, "Unsupported data format: #{extension}"
36
+ end
37
+
38
+ data = send(parser, content)
39
+
40
+ unless data.is_a?(Array)
41
+ raise Errors::DataParseError, "Data in '#{@file_path}' must be an array of objects."
42
+ end
43
+
44
+ data.map { Item.new(it) }
45
+ rescue Psych::SyntaxError, JSON::ParserError, CSV::MalformedCSVError => error
46
+ raise Errors::DataParseError, "Failed to parse '#{@file_path}': #{error.message}"
47
+ end
48
+
49
+ def parse_yaml(content)
50
+ YAML.safe_load(content, permitted_classes: [Symbol], aliases: true)
51
+ end
52
+
53
+ def parse_json(content)
54
+ JSON.parse(content, symbolize_names: true)
55
+ end
56
+
57
+ def parse_csv(content)
58
+ CSV.new(content, headers: true, header_converters: :symbol).to_a.map(&:to_h)
59
+ end
60
+
61
+ class Item
62
+ def initialize(attributes)
63
+ @attributes = attributes.transform_keys(&:to_sym)
64
+ end
65
+
66
+ def [](key) = @attributes[key.to_sym]
67
+
68
+ def method_missing(method_name, *arguments, &block)
69
+ return super if !@attributes.key?(method_name) || arguments.any? || block
70
+
71
+ @attributes[method_name]
72
+ end
73
+
74
+ def respond_to_missing?(method_name, include_private = false)
75
+ @attributes.key?(method_name) || super
76
+ end
77
+ end
78
+ private_constant :Item
79
+ end
80
+ end
81
+
82
+ # require "csv"
83
+
84
+ # module Perron
85
+ # class Data
86
+ # include Enumerable
87
+
88
+ # def initialize(resource)
89
+ # @file_path = path_for(resource)
90
+ # @data = data
91
+ # end
92
+
93
+ # def each(&block)
94
+ # @data.each(&block)
95
+ # end
96
+
97
+ # private
98
+
99
+ # PARSER_METHODS = {
100
+ # ".csv" => :parse_csv,
101
+ # ".json" => :parse_json,
102
+ # ".yaml" => :parse_yaml,
103
+ # ".yml" => :parse_yaml
104
+ # }.freeze
105
+ # SUPPORTED_EXTENSIONS = PARSER_METHODS.keys.freeze
106
+
107
+ # def path_for(identifier)
108
+ # path = Pathname.new(identifier)
109
+
110
+ # return path.to_s if path.file? && path.absolute?
111
+
112
+ # found_path = SUPPORTED_EXTENSIONS.lazy.map do |extension|
113
+ # Rails.root.join("app", "content", "data").join("#{identifier}#{extension}")
114
+ # end.find(&:exist?)
115
+
116
+ # found_path&.to_s or raise Errors::FileNotFoundError, "No data file found for '#{identifier}'"
117
+ # end
118
+
119
+ # def data
120
+ # content = File.read(@file_path)
121
+ # extension = File.extname(@file_path)
122
+ # parser = PARSER_METHODS.fetch(extension) do
123
+ # raise Errors::UnsupportedDataFormatError, "Unsupported data format: #{extension}"
124
+ # end
125
+
126
+ # raw_data = send(parser, content)
127
+
128
+ # unless raw_data.is_a?(Array)
129
+ # raise Errors::DataParseError, "Data in '#{@file_path}' must be an array of objects."
130
+ # end
131
+
132
+ # struct = Struct.new(*raw_data.first.keys, keyword_init: true)
133
+ # raw_data.map { struct.new(**it) }
134
+ # rescue Psych::SyntaxError, JSON::ParserError, CSV::MalformedCSVError => error
135
+ # raise Errors::DataParseError, "Failed to parse '#{@file_path}': #{error.message}"
136
+ # end
137
+
138
+ # def parse_yaml(content) = YAML.safe_load(content, permitted_classes: [Symbol], aliases: true)
139
+
140
+ # def parse_json(content) = JSON.parse(content, symbolize_names: true)
141
+
142
+ # def parse_csv(content) = CSV.new(content, headers: true, header_converters: :symbol).to_a.map(&:to_h)
143
+ # end
144
+ # end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Perron
4
+ class Resource
5
+ module ClassMethods
6
+ extend ActiveSupport::Concern
7
+
8
+ class_methods do
9
+ def find(slug) = collection.find(slug, name.constantize)
10
+
11
+ def all = collection.all(self)
12
+
13
+ def count = all.size
14
+
15
+ def first = all[0]
16
+
17
+ def second = all[1]
18
+
19
+ def third = all[2]
20
+
21
+ def fourth = all[3]
22
+
23
+ def fifth = all[4]
24
+
25
+ def forty_two = all[41]
26
+
27
+ def last = all.last
28
+
29
+ def take(n) = all.first(n)
30
+
31
+ def collection = Collection.new(collection_name)
32
+
33
+ def model_name
34
+ @model_name ||= ActiveModel::Name.new(self, nil, name.demodulize.to_s)
35
+ end
36
+
37
+ private
38
+
39
+ def collection_name = name.demodulize.underscore.pluralize
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Perron
4
+ class Resource
5
+ module Core
6
+ extend ActiveSupport::Concern
7
+
8
+ def persisted? = true
9
+
10
+ def to_model = self
11
+
12
+ def model_name = self.class.model_name
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Perron
4
+ class Resource
5
+ module Publishable
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ def published?
10
+ return true if Rails.env.development?
11
+
12
+ return false if metadata.draft == true
13
+ return false if metadata.published == false
14
+ return false if publication_date&.after?(Time.current)
15
+
16
+ true
17
+ end
18
+
19
+ def scheduled? = publication_date&.after?(Time.current)
20
+
21
+ def publication_date
22
+ @publication_date ||= begin
23
+ from_meta = metadata.published_at.present? ? begin
24
+ Time.zone.parse(metadata.published_at.to_s)
25
+ rescue
26
+ nil
27
+ end : nil
28
+
29
+ from_meta || date_from_filename
30
+ end
31
+ end
32
+ alias_method :published_at, :publication_date
33
+
34
+ def scheduled_at
35
+ publication_date if scheduled?
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ DATE_REGEX = /^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})-/
42
+
43
+ def date_from_filename
44
+ return @date_from_filename if defined?(@date_from_filename)
45
+
46
+ match = File.basename(file_path).match(DATE_REGEX)
47
+ @date_from_filename = match ? Date.new(match[:year].to_i, match[:month].to_i, match[:day].to_i).in_time_zone : nil
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Perron
4
+ class Resource
5
+ class Separator
6
+ attr_reader :content
7
+
8
+ def initialize(content)
9
+ parsed(content)
10
+ end
11
+
12
+ def metadata
13
+ @metadata_with_dot_access ||= ActiveSupport::OrderedOptions.new.tap do |options|
14
+ @metadata.each { |key, value| options[key] = value }
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def parsed(content)
21
+ if content =~ /\A---\s*(.*?)\s*---\s*(.*)/m
22
+ @metadata = YAML.safe_load($1, permitted_classes: [Date, Time]) || {}
23
+ @content = $2.strip
24
+ else
25
+ @metadata = {}
26
+ @content = content
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "perron/refinements/delete_suffixes"
4
+
5
+ module Perron
6
+ class Resource
7
+ class Slug
8
+ using Perron::SuffixStripping
9
+
10
+ def initialize(resource)
11
+ @resource = resource
12
+ @metadata = resource.metadata
13
+ end
14
+
15
+ def create
16
+ @metadata.slug.presence || @resource.filename.sub(/^[\d-]+-/, "").delete_suffixes(Perron.configuration.allowed_extensions)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "perron/site/resource/core"
4
+ require "perron/site/resource/class_methods"
5
+ require "perron/site/resource/publishable"
6
+ require "perron/site/resource/slug"
7
+ require "perron/site/resource/separator"
8
+
9
+ module Perron
10
+ class Resource
11
+ ID_LENGTH = 8
12
+
13
+ include Perron::Resource::Core
14
+ include Perron::Resource::ClassMethods
15
+ include Perron::Resource::Publishable
16
+
17
+ attr_reader :file_path, :id
18
+
19
+ def initialize(file_path)
20
+ @file_path = file_path
21
+ @id = generate_id
22
+
23
+ raise Errors::FileNotFoundError, "No such file: #{file_path}" unless File.exist?(file_path)
24
+ end
25
+
26
+ def filename = File.basename(@file_path)
27
+
28
+ def slug = Perron::Resource::Slug.new(self).create
29
+ alias_method :path, :slug
30
+ alias_method :to_param, :slug
31
+
32
+ def content
33
+ return Perron::Resource::Separator.new(raw_content).content unless processable?
34
+
35
+ ::ApplicationController
36
+ .renderer
37
+ .render(
38
+ inline: Perron::Resource::Separator.new(raw_content).content,
39
+ assigns: {resource: self}
40
+ )
41
+ end
42
+
43
+ def metadata = Perron::Resource::Separator.new(raw_content).metadata
44
+
45
+ def raw_content = File.read(@file_path)
46
+ alias_method :raw, :raw_content
47
+
48
+ def collection = Collection.new(self.class.model_name.collection)
49
+
50
+ private
51
+
52
+ def processable?
53
+ @file_path.ends_with?(".erb") || metadata.erb == true
54
+ end
55
+
56
+ def generate_id
57
+ Digest::SHA1.hexdigest(
58
+ @file_path.delete_prefix(Perron.configuration.input).parameterize
59
+ ).first(ID_LENGTH)
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "perron/site/builder"
4
+ require "perron/site/collection"
5
+ require "perron/site/resource"
6
+ require "perron/site/data"
7
+
8
+ module Perron
9
+ module Site
10
+ module_function
11
+
12
+ def build = Perron::Site::Builder.new.build
13
+
14
+ def name = Perron.configuration.site_name
15
+
16
+ def email = Perron.configuration.site_email
17
+
18
+ def url
19
+ options = Perron.configuration.default_url_options
20
+
21
+ "#{options[:protocol]}://#{options[:host]}"
22
+ end
23
+
24
+ def collections
25
+ @collections ||= Dir.children(Perron.configuration.input)
26
+ .select { File.directory?(File.join(Perron.configuration.input, it)) }
27
+ .map { Collection.new(it) }
28
+ end
29
+
30
+ def collection(name)
31
+ Collection.new(name)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,12 @@
1
+ namespace :perron do
2
+ desc "Generate static HTML files from Perron collections"
3
+ task build: :environment do
4
+ Perron::Site.build
5
+ end
6
+ end
7
+
8
+ Rake::Task["assets:precompile"].enhance do
9
+ next if Perron.configuration.mode.standalone?
10
+
11
+ Perron::Site.build
12
+ end
@@ -0,0 +1,3 @@
1
+ module Perron
2
+ VERSION = "0.6.0"
3
+ end
data/lib/perron.rb CHANGED
@@ -1,3 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "perron/version"
4
+ require "perron/configuration"
5
+ require "perron/errors"
6
+ require "perron/root"
7
+ require "perron/site"
8
+ require "perron/markdown"
9
+ require "perron/metatags"
10
+ require "perron/engine"
11
+
1
12
  module Perron
2
- # TBD
3
13
  end
data/perron.gemspec ADDED
@@ -0,0 +1,26 @@
1
+ require_relative "lib/perron/version"
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "perron"
5
+ spec.version = Perron::VERSION
6
+ spec.authors = ["Rails Designer Developers"]
7
+ spec.email = "devs@railsdeigner.com"
8
+
9
+ spec.summary = "Rails-based static site generator"
10
+ spec.description = "Perron is a Rails-based static site generator that follows Rails conventions. It allows you to create content collections with markdown or ERB, configure SEO metadata, and build production-ready static sites while leveraging your existing Rails knowledge with familiar patterns and minimal configuration."
11
+ spec.homepage = "https://github.com/Rails-Designer/perron"
12
+ spec.license = "MIT"
13
+
14
+ spec.metadata["homepage_uri"] = spec.homepage
15
+ spec.metadata["source_code_uri"] = "https://github.com/Rails-Designer/perron/"
16
+
17
+ spec.files = Dir["{bin,app,lib}/**/*", "Rakefile", "README.md", "perron.gemspec", "Gemfile", "Gemfile.lock"]
18
+
19
+ spec.required_ruby_version = ">= 3.4.0"
20
+
21
+ spec.add_dependency "rails", ">= 7.2.0"
22
+
23
+ spec.add_runtime_dependency "csv"
24
+ spec.add_runtime_dependency "json"
25
+ spec.add_runtime_dependency "psych"
26
+ end
metadata CHANGED
@@ -1,25 +1,131 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: perron
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rails Designer Developers
8
8
  bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
- dependencies: []
12
- description: Coming soon. Turn your Rails app to a static site.
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rails
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 7.2.0
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 7.2.0
26
+ - !ruby/object:Gem::Dependency
27
+ name: csv
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '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'
40
+ - !ruby/object:Gem::Dependency
41
+ name: json
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: psych
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ description: Perron is a Rails-based static site generator that follows Rails conventions.
69
+ It allows you to create content collections with markdown or ERB, configure SEO
70
+ metadata, and build production-ready static sites while leveraging your existing
71
+ Rails knowledge with familiar patterns and minimal configuration.
13
72
  email: devs@railsdeigner.com
14
73
  executables: []
15
74
  extensions: []
16
75
  extra_rdoc_files: []
17
76
  files:
77
+ - Gemfile
78
+ - Gemfile.lock
79
+ - README.md
80
+ - Rakefile
81
+ - app/helpers/meta_tags_helper.rb
82
+ - app/helpers/perron/markdown_helper.rb
83
+ - bin/console
84
+ - bin/rails
85
+ - bin/release
86
+ - bin/setup
87
+ - lib/generators/content/content_generator.rb
88
+ - lib/generators/content/templates/controller.rb.tt
89
+ - lib/generators/content/templates/index.html.erb.tt
90
+ - lib/generators/content/templates/model.rb.tt
91
+ - lib/generators/content/templates/root.erb.tt
92
+ - lib/generators/content/templates/show.html.erb.tt
93
+ - lib/generators/perron/install_generator.rb
94
+ - lib/generators/perron/templates/README.md.tt
95
+ - lib/generators/perron/templates/initializer.rb.tt
18
96
  - lib/perron.rb
97
+ - lib/perron/configuration.rb
98
+ - lib/perron/engine.rb
99
+ - lib/perron/errors.rb
100
+ - lib/perron/html_processor.rb
101
+ - lib/perron/html_processor/target_blank.rb
102
+ - lib/perron/markdown.rb
103
+ - lib/perron/metatags.rb
104
+ - lib/perron/refinements/delete_suffixes.rb
105
+ - lib/perron/root.rb
106
+ - lib/perron/site.rb
107
+ - lib/perron/site/builder.rb
108
+ - lib/perron/site/builder/assets.rb
109
+ - lib/perron/site/builder/page.rb
110
+ - lib/perron/site/builder/paths.rb
111
+ - lib/perron/site/builder/public_files.rb
112
+ - lib/perron/site/collection.rb
113
+ - lib/perron/site/data.rb
114
+ - lib/perron/site/resource.rb
115
+ - lib/perron/site/resource/class_methods.rb
116
+ - lib/perron/site/resource/core.rb
117
+ - lib/perron/site/resource/publishable.rb
118
+ - lib/perron/site/resource/separator.rb
119
+ - lib/perron/site/resource/slug.rb
120
+ - lib/perron/tasks/perron.rake
121
+ - lib/perron/version.rb
122
+ - perron.gemspec
19
123
  homepage: https://github.com/Rails-Designer/perron
20
124
  licenses:
21
125
  - MIT
22
- metadata: {}
126
+ metadata:
127
+ homepage_uri: https://github.com/Rails-Designer/perron
128
+ source_code_uri: https://github.com/Rails-Designer/perron/
23
129
  rdoc_options: []
24
130
  require_paths:
25
131
  - lib
@@ -27,7 +133,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
27
133
  requirements:
28
134
  - - ">="
29
135
  - !ruby/object:Gem::Version
30
- version: '0'
136
+ version: 3.4.0
31
137
  required_rubygems_version: !ruby/object:Gem::Requirement
32
138
  requirements:
33
139
  - - ">="
@@ -36,5 +142,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
36
142
  requirements: []
37
143
  rubygems_version: 3.6.7
38
144
  specification_version: 4
39
- summary: Rails-bases Static Site Generator
145
+ summary: Rails-based static site generator
40
146
  test_files: []