foxpage 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a7eb6f55b4154a03fc2be9a3a623bedfbf03b4436fab184377b66d11310df899
4
- data.tar.gz: 20b6e0f2ee86a2064afc07eb84ac812347895db02638be7ce9441c16ae1c675c
3
+ metadata.gz: 98eb0f68d2163136732589c5da391975f8164e5c4e985dccd0bd24e893bc3045
4
+ data.tar.gz: e45d62a4c2a508010d066eab5fe822f8bc0a740c89b219a08d5a93e587b1ed8d
5
5
  SHA512:
6
- metadata.gz: a1ca9bb683c35b9b3bc661b288072a2cfe73d5e4eb01f517ea0e48cd0ee3fa1ccc31954c0028237cc490fb903ed227eadd01548edcf3e2161df40ff4ae768e2d
7
- data.tar.gz: ff733675525bf7fc4f31c593a6bc13670e71fea8349e7c1ae9ba06979e2476bff2104968990970be7c0aca03365dd965a0d0de80e2ca614992ab7d0c972c9754
6
+ metadata.gz: e6ae9e51e8c3defd140e169df2d9126e2227922af918dab4d4cdd69ed356214e680850f941f15d450b9656b130fc2a8ec03321864060f42eebe0d86dac33eeb3
7
+ data.tar.gz: 6dd3289ed9aba23e9b74f14fcdbc46d902f9ce4ed05c291b20707525b5061a6666ff2cd60f9dd15c238c8dfac866e4315a09dcb225ffa5b2d5672133e1024bf7
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- foxpage (0.1.0)
4
+ foxpage (0.2.0)
5
5
  haml (~> 5.1)
6
6
  listen (~> 3.1)
7
7
  sassc (~> 2.0)
@@ -18,7 +18,7 @@ GEM
18
18
  concurrent-ruby (1.1.5)
19
19
  diff-lcs (1.3)
20
20
  ffi (1.11.1)
21
- haml (5.1.1)
21
+ haml (5.1.2)
22
22
  temple (>= 0.8.0)
23
23
  tilt
24
24
  jaro_winkler (1.5.2)
@@ -57,18 +57,17 @@ GEM
57
57
  unicode-display_width (>= 1.4.0, < 1.6)
58
58
  ruby-progressbar (1.10.1)
59
59
  ruby_dep (1.5.0)
60
- sassc (2.0.1)
60
+ sassc (2.2.1)
61
61
  ffi (~> 1.9)
62
- rake
63
- sprockets (4.0.0.beta9)
62
+ sprockets (4.0.0.beta10)
64
63
  concurrent-ruby (~> 1.0)
65
64
  rack (> 1, < 3)
66
- temple (0.8.1)
65
+ temple (0.8.2)
67
66
  thor (0.20.3)
68
- tilt (2.0.9)
67
+ tilt (2.0.10)
69
68
  unicode-display_width (1.5.0)
70
69
  webrick (1.4.2)
71
- zeitwerk (2.1.6)
70
+ zeitwerk (2.1.10)
72
71
 
73
72
  PLATFORMS
74
73
  ruby
@@ -25,6 +25,7 @@ module FoxPage
25
25
  # Set up application code loader
26
26
  @code_loader = Zeitwerk::Loader.new.tap do |loader|
27
27
  loader.push_dir(@root.join("app/controllers"))
28
+ loader.push_dir(@root.join("app/models"))
28
29
  loader.push_dir(@root.join("app/helpers"))
29
30
  loader.enable_reloading
30
31
  loader.setup
@@ -20,7 +20,7 @@ module FoxPage
20
20
 
21
21
  def image_assets
22
22
  image_assets_path = app.root.join("app/assets/images")
23
- Dir.glob("#{image_assets_path}/**/*.{png,jpg,gif,jpeg}")
23
+ Dir.glob("#{image_assets_path}/**/*.{png,jpg,gif,jpeg,svg}")
24
24
  .map { |full_path| full_path.sub(%r{\A#{image_assets_path}/}, "") }
25
25
  end
26
26
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FoxPage
4
+ module Builders
5
+ module Models
6
+ def load_models
7
+ inject_app_to_models
8
+ end
9
+
10
+ private
11
+
12
+ def inject_app_to_models
13
+ return if already_injected?
14
+
15
+ FoxPage::Model.instance_variable_set(:@__app, app)
16
+ end
17
+
18
+ def already_injected?
19
+ FoxPage::Model.instance_variable_get(:@__app)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -7,36 +7,66 @@ module FoxPage
7
7
  module Builders
8
8
  module Pages
9
9
  using Refinements::Camelize
10
+ using Refinements::Constantize
10
11
 
11
12
  def build_pages
12
13
  app.routes.each do |path, route|
13
- puts "PAGE\t#{path} => #{route.base_name}"
14
+ if route.generate_all
15
+ model = route.generate_all.camelize.constantize
14
16
 
15
- target_directory = File.join(output_directory, path)
16
- FileUtils.mkdir_p(target_directory)
17
-
18
- File.open(File.join(target_directory, "index.html"), "w") do |f|
19
- f.puts render_route(route)
17
+ model.all.each do |item|
18
+ target_path = format(path, id: item.id)
19
+ build_single_page(target_path, route, id: item.id)
20
+ end
21
+ next
20
22
  end
23
+
24
+ build_single_page(path, route)
25
+ end
26
+ end
27
+
28
+ def build_single_page(target_path, route, params = {})
29
+ if params.empty?
30
+ params_log_str = ""
31
+ else
32
+ params_log_str = "(#{params.inspect})"
33
+ route = route.dup
34
+ route.params = OpenStruct.new(route.params.to_h.merge(params))
35
+ end
36
+
37
+ puts "PAGE\t#{target_path} => #{route.base_name}##{route.method_name}#{params_log_str}"
38
+
39
+ target_file = File.join(output_directory, target_path)
40
+ unless route.single_file
41
+ FileUtils.mkdir_p(target_file)
42
+ target_file = File.join(target_file, "index.html")
43
+ end
44
+
45
+ File.open(target_file, "w") do |f|
46
+ f.puts render_route(route, target_path)
21
47
  end
22
48
  end
23
49
 
24
- def render_route(route)
25
- controller = spiced_controller(route).new
50
+ def render_route(route, path)
51
+ controller = spiced_controller(route, path).new
26
52
  controller.method(route.method_name).call
27
53
 
28
- layout = Tilt.new(layout_path(controller))
54
+ layout = layout_for(controller, route)
29
55
  page = Tilt.new(page_path(route))
30
56
 
31
57
  controller.instance_eval do
32
- layout.render(self) { page.render(self) }
58
+ if layout
59
+ layout.render(self) { page.render(self) }
60
+ else
61
+ page.render(self)
62
+ end
33
63
  end
34
64
  end
35
65
 
36
66
  # for the sake of keeping the original classes sane while building, we
37
67
  # create a subclass of the original dynamically and inject common helpers
38
68
  # to it and also run before_actions
39
- def spiced_controller(route) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/LineLength
69
+ def spiced_controller(route, path) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/LineLength
40
70
  Class.new(route.controller).tap do |klass| # rubocop:disable Metrics/BlockLength, Metrics/LineLength
41
71
  klass.include(Helpers::AppHelper.new(app))
42
72
  klass.include(Helpers::AssetsHelper)
@@ -57,6 +87,18 @@ module FoxPage
57
87
  # same difference
58
88
  end
59
89
 
90
+ klass.define_method(:params) do
91
+ route.params
92
+ end
93
+
94
+ klass.define_method(:current_path) do
95
+ path
96
+ end
97
+
98
+ klass.define_method(:current_controller_name) do
99
+ route.base_name
100
+ end
101
+
60
102
  klass.define_method(:inspect) do |*args|
61
103
  # report that we are actually the controller, not some random
62
104
  # anonymous class
@@ -89,9 +131,18 @@ module FoxPage
89
131
  end
90
132
  end
91
133
 
92
- def layout_path(controller)
134
+ def layout_for(controller, route)
135
+ layout = controller.class.superclass.instance_variable_get(:@__use_layout_for)&.[](route.method_name)
136
+ return if layout == false
137
+
138
+ Tilt.new(layout_path(controller, layout))
139
+ end
140
+
141
+ def layout_path(controller, layout)
142
+ layout ||= controller.class.layout
143
+
93
144
  File
94
- .join(views_path, controller.class.layout)
145
+ .join(views_path, layout)
95
146
  .tap(&method(:validate_file_exists))
96
147
  end
97
148
 
@@ -13,5 +13,38 @@ module FoxPage
13
13
  @__before_actions ||= []
14
14
  @__before_actions << method_name
15
15
  end
16
+
17
+ # Instructs the site builder to generate pages for all records of `model`.
18
+ def self.generate_all(model)
19
+ @__generate_all = model
20
+ end
21
+
22
+ def self.use_layout(layout)
23
+ @__use_layout = layout
24
+ end
25
+
26
+ def self.method_added(method_name)
27
+ return unless @__generate_all.nil? || @__use_layout.nil?
28
+
29
+ set_method_option(method_name, "generate_all")
30
+ set_method_option(method_name, "use_layout")
31
+ end
32
+
33
+ def self.set_method_option(method_name, option)
34
+ ivar_name = :"@__#{option}"
35
+ ivar_for_name = :"@__#{option}_for"
36
+
37
+ ivar_val = instance_variable_get(ivar_name)
38
+ return if ivar_val.nil?
39
+
40
+ instance_variable_set(ivar_name, nil)
41
+ unless instance_variable_get(ivar_for_name)
42
+ instance_variable_set(ivar_for_name, {})
43
+ end
44
+
45
+ instance_variable_get(ivar_for_name)[method_name] = ivar_val
46
+ end
47
+
48
+ private_class_method :set_method_option
16
49
  end
17
50
  end
@@ -3,14 +3,14 @@
3
3
  module FoxPage
4
4
  module Helpers
5
5
  module RenderHelper
6
- def render(view)
6
+ def render(view, params = {})
7
7
  full_path = Dir.glob(app.root.join("app/views/#{view}.*")).first
8
8
 
9
9
  unless full_path
10
10
  raise ArgumentError, "Could not find template for #{view}"
11
11
  end
12
12
 
13
- Tilt.new(full_path).render(self)
13
+ Tilt.new(full_path).render(self, params)
14
14
  end
15
15
  end
16
16
  end
@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module FoxPage
6
+ class Model
7
+ using Refinements::Camelize
8
+ using Refinements::Pluralize
9
+ using Refinements::Singularize
10
+ using Refinements::ToDeepOpenStruct
11
+ using Refinements::Underscore
12
+
13
+ @__data = []
14
+
15
+ VALID_STORAGE_TYPES = %i[yaml dir].freeze
16
+ DEFAULT_STORAGE_TYPE = :yaml
17
+ private_constant :VALID_STORAGE_TYPES
18
+ private_constant :DEFAULT_STORAGE_TYPE
19
+
20
+ def self.[](storage_type = DEFAULT_STORAGE_TYPE, storage_type_opts = {})
21
+ unless VALID_STORAGE_TYPES.include?(storage_type)
22
+ raise ArgumentError,
23
+ "type must be one of #{VALID_STORAGE_TYPES.join(',')}"
24
+ end
25
+
26
+ @__tmp_storage_type = storage_type
27
+ @__tmp_storage_type_opts = storage_type_opts
28
+
29
+ self
30
+ end
31
+
32
+ def self.inherited(subclass)
33
+ set_ivar_if_unset subclass, :storage_type, DEFAULT_STORAGE_TYPE
34
+ set_ivar_if_unset subclass, :storage_type_opts, {}
35
+
36
+ subclass.reload_all(@__app)
37
+ end
38
+
39
+ def self.set_ivar_if_unset(subclass, ivar, default)
40
+ target_ivar = :"@__#{ivar}"
41
+ return if subclass.instance_variable_get(target_ivar)
42
+
43
+ tmp_ivar = :"@__tmp_#{ivar}"
44
+ value = instance_variable_get tmp_ivar
45
+ instance_variable_set tmp_ivar, nil
46
+
47
+ subclass.instance_variable_set target_ivar, value || default
48
+ end
49
+ private_class_method :set_ivar_if_unset
50
+
51
+ def self.reload_all(app)
52
+ data_name = name.to_s.underscore.pluralize
53
+ puts "MODEL\t#{data_name}"
54
+
55
+ case @__storage_type
56
+ when :yaml
57
+ @__data = YAML.load_file(
58
+ app.root.join("data", data_name + ".yml")
59
+ ).to_deep_ostruct.map { |ostruct| new(ostruct) }
60
+ when :dir
61
+ default_opts = { extension: :md }
62
+ opts = default_opts.merge(@__storage_type_opts)
63
+ files = Dir[app.root.join("data", data_name, "*.#{opts.fetch(:extension)}")]
64
+
65
+ @__data = files.map do |fn|
66
+ id = File.basename(fn, ".#{opts.fetch(:extension)}")
67
+ content = IO.read(fn)
68
+
69
+ front_matter = {}
70
+ if content =~ /\A(---\n.*\n)^(?:---)\s*$\n?/m
71
+ content = Regexp.last_match.post_match
72
+ front_matter = YAML.safe_load(Regexp.last_match[1], [Time])
73
+ end
74
+
75
+ new front_matter.merge(id: id, content: content).to_deep_ostruct
76
+ end
77
+ end
78
+ end
79
+
80
+ # e.g. in ProjectCategory: has_many :projects, referenced_by: nil
81
+ #
82
+ # nil = default of self.class.name underscored
83
+ def self.has_many(what, referenced_by: nil) # rubocop:disable Naming/PredicateName, Metrics/LineLength
84
+ referenced_by ||= name.to_s.underscore
85
+ association_class = Kernel.const_get(what.to_s.singularize.camelize)
86
+
87
+ define_method(what.to_s) do
88
+ association_class.where(referenced_by => name.to_s)
89
+ end
90
+ end
91
+
92
+ # e.g. in Project: belongs_to :project_category, referenced_by: :name
93
+ def self.belongs_to(what, referenced_by: :name)
94
+ association_class = Kernel.const_get(what.to_s.camelize)
95
+
96
+ define_method("__#{what}_value") do
97
+ @__ostruct[what]
98
+ end
99
+
100
+ define_method(what) do
101
+ association_class.find(referenced_by => public_send("__#{what}_value"))
102
+ end
103
+ end
104
+
105
+ # define a parser method for attributes
106
+ # @example
107
+ # require "time"
108
+ #
109
+ # class BlogPost < FoxPage::Model[:dir]
110
+ # def_parser :date do |date|
111
+ # Time.parse(date)
112
+ # end
113
+ # end
114
+ #
115
+ # # in the blog posts' front matter:
116
+ # # ---
117
+ # # title: foo
118
+ # # date: Sat 13 Jul 13:38:43 CEST 2019
119
+ # # ---
120
+ #
121
+ # # then, anywhere else:
122
+ # blog_post.date # => 2019-07-13 13:38:43 +0200
123
+ # blog_post.date.class # => Time
124
+ def self.def_parser(attribute, &parser)
125
+ define_method(attribute) do
126
+ parser.call(@__ostruct[attribute])
127
+ end
128
+ end
129
+
130
+ def self.all
131
+ @__data
132
+ end
133
+
134
+ def self.each(&block)
135
+ @__data.each(&block)
136
+ end
137
+
138
+ def self.find(filter)
139
+ @__data.find do |object|
140
+ filter.all? do |key, value|
141
+ value_key_name = "__#{key}_value"
142
+ key = value_key_name if object.respond_to?(value_key_name)
143
+
144
+ object.public_send(key) == value
145
+ end
146
+ end
147
+ end
148
+
149
+ def self.where(filter)
150
+ @__data.select do |object|
151
+ filter.all? do |key, value|
152
+ value_key_name = "__#{key}_value"
153
+ key = value_key_name if object.respond_to?(value_key_name)
154
+
155
+ object.public_send(key) == value
156
+ end
157
+ end
158
+ end
159
+
160
+ def initialize(ostruct)
161
+ @__ostruct = ostruct
162
+ end
163
+
164
+ def method_missing(method, *args, &block)
165
+ hash_ostruct = @__ostruct.to_h
166
+ return super unless hash_ostruct.key?(method.to_sym)
167
+
168
+ hash_ostruct[method.to_sym]
169
+ end
170
+
171
+ def respond_to_missing?(method, *)
172
+ return true if @__ostruct.to_h.key?(method.to_sym)
173
+
174
+ super
175
+ end
176
+ end
177
+ end
@@ -8,6 +8,12 @@ module FoxPage
8
8
  split("_").map(&:capitalize).join
9
9
  end
10
10
  end
11
+
12
+ refine Symbol do
13
+ def camelize
14
+ to_s.camelize.to_sym
15
+ end
16
+ end
11
17
  end
12
18
  end
13
19
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FoxPage
4
+ module Refinements
5
+ module Constantize
6
+ refine String do
7
+ def constantize
8
+ Kernel.const_get(self)
9
+ end
10
+ end
11
+
12
+ refine Symbol do
13
+ def constantize
14
+ to_s.constantize
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FoxPage
4
+ module Refinements
5
+ module Pluralize
6
+ refine String do
7
+ def pluralize
8
+ return sub(/y$/, "ies") if end_with?("y")
9
+
10
+ "#{self}s"
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FoxPage
4
+ module Refinements
5
+ module Singularize
6
+ refine String do
7
+ def singularize
8
+ return sub(/ies$/, "y") if end_with?("ies")
9
+
10
+ sub(/s$/, "")
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FoxPage
4
+ module Refinements
5
+ module Underscore
6
+ refine String do
7
+ def underscore
8
+ gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
9
+ end
10
+ end
11
+
12
+ refine Symbol do
13
+ def underscore
14
+ to_s.underscore.to_sym
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -21,36 +21,90 @@ module FoxPage
21
21
  routes
22
22
  end
23
23
 
24
- def root(target)
25
- routes["/"] = parse_target(target)
24
+ def root(target, params: {})
25
+ routes["/"] = parse_target(target, params: params)
26
26
  end
27
27
 
28
- def map(mapping)
28
+ def map(mapping, params: {}, single_file: false)
29
29
  mapping.each do |path, target|
30
- routes[path] = parse_target(target)
30
+ routes[path] = parse_target(target, params: params, single_file: single_file)
31
+ end
32
+ end
33
+
34
+ RESOURCE_ACTIONS = %i[index show].freeze
35
+
36
+ def resources(name, path: name, only: RESOURCE_ACTIONS)
37
+ actions = only.map(&:to_sym)
38
+ base_name = name.to_s
39
+ controller = controller_for(base_name)
40
+ base_path = "/#{path}"
41
+
42
+ # show action needs some additional stuff to make it work
43
+ # since we have to know all the ids beforehand
44
+ if actions.delete :show
45
+ method_name = :show
46
+ validate_controller_method(controller, method_name)
47
+ route_path = "#{base_path}/%<id>s"
48
+
49
+ routes[route_path] = make_target(
50
+ base_name: base_name,
51
+ controller: controller,
52
+ method_name: method_name,
53
+ params: {},
54
+ generate_all: controller.instance_variable_get(:@__generate_all_for)&.[](method_name)
55
+ )
56
+ end
57
+
58
+ actions.each do |action|
59
+ method_name = action
60
+ validate_controller_method(controller, method_name)
61
+ route_path = method_name == :index ? base_path : "#{base_path}/#{method_name}"
62
+
63
+ routes[route_path] = make_target(
64
+ base_name: base_name,
65
+ controller: controller,
66
+ method_name: method_name
67
+ )
31
68
  end
32
69
  end
33
70
 
34
71
  private
35
72
 
36
- def parse_target(target)
73
+ def parse_target(target, params: {}, single_file: false)
37
74
  base_name, method_name = target.split("#")
38
- controller = Kernel.const_get("#{base_name}_controller".camelize)
75
+ controller = controller_for(base_name)
39
76
  method_name = method_name.to_sym
40
77
 
41
78
  validate_controller_method(controller, method_name)
42
79
 
43
- OpenStruct.new(
80
+ make_target(
44
81
  base_name: base_name,
45
82
  controller: controller,
46
- method_name: method_name
83
+ method_name: method_name,
84
+ params: params,
85
+ single_file: single_file
47
86
  )
48
87
  end
49
88
 
89
+ def controller_for(base_name)
90
+ Kernel.const_get("#{base_name}_controller".camelize)
91
+ end
92
+
50
93
  def validate_controller_method(controller, method_name)
51
94
  return if controller.instance_methods.include?(method_name)
52
95
 
53
96
  raise ArgumentError, "#{controller} does not define ##{method_name}"
54
97
  end
98
+
99
+ def make_target(base_name:, controller:, method_name:, params: {}, generate_all: nil, single_file: false)
100
+ OpenStruct.new(
101
+ base_name: base_name,
102
+ controller: controller,
103
+ method_name: method_name,
104
+ params: OpenStruct.new(params),
105
+ generate_all: generate_all,
106
+ single_file: single_file
107
+ )
108
+ end
55
109
  end
56
110
  end
@@ -8,6 +8,7 @@ module FoxPage
8
8
  def initialize(app)
9
9
  @app = app
10
10
  @listener = Listen.to(app.root.join("app"),
11
+ app.root.join("data"),
11
12
  app.root.join("public"),
12
13
  &method(:handle_modified_app))
13
14
  @server = WEBrick::HTTPServer.new(
@@ -6,6 +6,7 @@ module FoxPage
6
6
  class SiteBuilder
7
7
  include Builders::Assets
8
8
  include Builders::FileCopy
9
+ include Builders::Models
9
10
  include Builders::Pages
10
11
 
11
12
  def self.build(app)
@@ -24,6 +25,7 @@ module FoxPage
24
25
 
25
26
  FileUtils.mkdir_p output_directory
26
27
 
28
+ load_models
27
29
  build_assets
28
30
  build_pages
29
31
  copy_public_files
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FoxPage
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: foxpage
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Georg Gadinger
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-06-03 00:00:00.000000000 Z
11
+ date: 2019-10-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -228,6 +228,7 @@ files:
228
228
  - lib/fox_page/application.rb
229
229
  - lib/fox_page/builders/assets.rb
230
230
  - lib/fox_page/builders/file_copy.rb
231
+ - lib/fox_page/builders/models.rb
231
232
  - lib/fox_page/builders/pages.rb
232
233
  - lib/fox_page/cli.rb
233
234
  - lib/fox_page/controller.rb
@@ -235,8 +236,13 @@ files:
235
236
  - lib/fox_page/helpers/app_helper.rb
236
237
  - lib/fox_page/helpers/assets_helper.rb
237
238
  - lib/fox_page/helpers/render_helper.rb
239
+ - lib/fox_page/model.rb
238
240
  - lib/fox_page/refinements/camelize.rb
241
+ - lib/fox_page/refinements/constantize.rb
242
+ - lib/fox_page/refinements/pluralize.rb
243
+ - lib/fox_page/refinements/singularize.rb
239
244
  - lib/fox_page/refinements/to_deep_open_struct.rb
245
+ - lib/fox_page/refinements/underscore.rb
240
246
  - lib/fox_page/router.rb
241
247
  - lib/fox_page/server.rb
242
248
  - lib/fox_page/site_builder.rb