foxpage 0.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.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +9 -0
  5. data/.ruby-version +1 -0
  6. data/CODE_OF_CONDUCT.md +74 -0
  7. data/Gemfile +6 -0
  8. data/Gemfile.lock +84 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +76 -0
  11. data/Rakefile +8 -0
  12. data/bin/console +15 -0
  13. data/bin/setup +8 -0
  14. data/exe/foxpage +6 -0
  15. data/foxpage.gemspec +59 -0
  16. data/lib/fox_page/app_parts/base.rb +15 -0
  17. data/lib/fox_page/app_parts/builder.rb +13 -0
  18. data/lib/fox_page/app_parts/configuration.rb +46 -0
  19. data/lib/fox_page/app_parts/routes.rb +25 -0
  20. data/lib/fox_page/app_parts/server.rb +13 -0
  21. data/lib/fox_page/app_parts/sprockets.rb +28 -0
  22. data/lib/fox_page/app_parts.rb +24 -0
  23. data/lib/fox_page/app_template/__dot__gitignore.tt +2 -0
  24. data/lib/fox_page/app_template/__dot__rubocop.yml.tt +9 -0
  25. data/lib/fox_page/app_template/app/assets/images/__dot__keep.tt +0 -0
  26. data/lib/fox_page/app_template/app/assets/stylesheets/application.scss.tt +8 -0
  27. data/lib/fox_page/app_template/app/controllers/application_controller.rb.tt +4 -0
  28. data/lib/fox_page/app_template/app/controllers/home_controller.rb.tt +13 -0
  29. data/lib/fox_page/app_template/app/helpers/application_helper.rb.tt +5 -0
  30. data/lib/fox_page/app_template/app/views/home/index.haml.tt +4 -0
  31. data/lib/fox_page/app_template/app/views/layouts/_footer.haml.tt +5 -0
  32. data/lib/fox_page/app_template/app/views/layouts/default.haml.tt +15 -0
  33. data/lib/fox_page/app_template/config/application.rb.tt +6 -0
  34. data/lib/fox_page/app_template/config/boot.rb.tt +6 -0
  35. data/lib/fox_page/app_template/config/environment.rb.tt +5 -0
  36. data/lib/fox_page/app_template/config/routes.rb.tt +12 -0
  37. data/lib/fox_page/app_template/config/site.yml.tt +41 -0
  38. data/lib/fox_page/app_template/gems.rb.tt +9 -0
  39. data/lib/fox_page/app_template/public/__dot__keep.tt +0 -0
  40. data/lib/fox_page/application.rb +31 -0
  41. data/lib/fox_page/builders/assets.rb +28 -0
  42. data/lib/fox_page/builders/file_copy.rb +22 -0
  43. data/lib/fox_page/builders/pages.rb +124 -0
  44. data/lib/fox_page/cli.rb +35 -0
  45. data/lib/fox_page/controller.rb +17 -0
  46. data/lib/fox_page/generator.rb +40 -0
  47. data/lib/fox_page/helpers/app_helper.rb +20 -0
  48. data/lib/fox_page/helpers/assets_helper.rb +17 -0
  49. data/lib/fox_page/helpers/render_helper.rb +17 -0
  50. data/lib/fox_page/refinements/camelize.rb +13 -0
  51. data/lib/fox_page/refinements/to_deep_open_struct.rb +33 -0
  52. data/lib/fox_page/router.rb +56 -0
  53. data/lib/fox_page/server.rb +47 -0
  54. data/lib/fox_page/site_builder.rb +32 -0
  55. data/lib/fox_page/version.rb +5 -0
  56. data/lib/fox_page.rb +20 -0
  57. data/lib/foxpage.rb +3 -0
  58. data/misc/foxpage.png +0 -0
  59. data/misc/foxpage.svg +28 -0
  60. metadata +272 -0
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler"
4
+ Bundler.setup :default
5
+
6
+ require "foxpage"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./application"
4
+
5
+ App.initialize!
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ App.draw_routes do
4
+ # This defines the landing page of your website.
5
+ root "home#index"
6
+
7
+ # Additional routes can be created by using `map`.
8
+ # For example to map `/projects` to the index method of ProjectsController,
9
+ # you can do this:
10
+ #
11
+ # map "/projects" => "projects#index"
12
+ end
@@ -0,0 +1,41 @@
1
+ ---
2
+ # This file defines everything that's available via `App.config` in your
3
+ # website.
4
+ #
5
+ # Here are some site defaults:
6
+ site:
7
+ title: <%= name.inspect %>
8
+ description: |-
9
+ Another site created with FoxPage.
10
+ url: "https://example.com" # the base hostname & protocol for your site
11
+
12
+ # You can also add your own items here. For example, here is what a list of
13
+ # social media sites could look like:
14
+ #
15
+ # social_media:
16
+ # - name: GitHub
17
+ # css_class: github
18
+ # url: "https://github.com/<yourusername>"
19
+ # - name: Mastodon
20
+ # css_class: mastodon
21
+ # url: "https://ruby.social/@<yourusername>"
22
+ #
23
+ # In a view, you can then iterate over it like this:
24
+ #
25
+ # - App.config.site.social_media.each do |site|
26
+ # %a{class: site.css_class, href: site.url}= site.name
27
+
28
+ # This list defines all the assets that should be built. By default it only
29
+ # includes the main application style sheet.
30
+ #
31
+ # All image assets will be added to the assets pipeline automatically.
32
+ #
33
+ # To use an asset in a view, you can use the `assets_path` helper, e.g:
34
+ #
35
+ # %img{src: asset_path("lisp-alien.png")}
36
+ #
37
+ # For style sheets, you can use the `stylesheet_link_tag` helper:
38
+ #
39
+ # = stylesheet_link_tag("application.css")
40
+ assets:
41
+ - application.css
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "foxpage", "~> <%= FoxPage::VERSION %>"
6
+
7
+ group :development do
8
+ gem "rubocop"
9
+ end
File without changes
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "singleton"
4
+ require "forwardable"
5
+ require "yaml"
6
+
7
+ module FoxPage
8
+ class Application
9
+ include Singleton
10
+
11
+ prepend AppParts::Builder
12
+ prepend AppParts::Configuration
13
+ prepend AppParts::Routes
14
+ prepend AppParts::Server
15
+ prepend AppParts::Sprockets
16
+
17
+ def initialize!
18
+ AppParts.initializers_for(self.class).each do |proc|
19
+ instance_eval(&proc)
20
+ end
21
+ end
22
+
23
+ # delegate _ALL_ the things!
24
+ self.class.extend Forwardable
25
+ (instance.public_methods - Object.methods)
26
+ .reject { |x| x.to_s.start_with?("_") }
27
+ .each do |meth|
28
+ self.class.def_delegator :instance, meth
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sprockets"
4
+
5
+ module FoxPage
6
+ module Builders
7
+ module Assets
8
+ def build_assets
9
+ all_assets.each do |asset|
10
+ puts "ASSET\t#{asset}"
11
+ app.sprockets.manifest.compile(asset)
12
+ end
13
+ end
14
+
15
+ private
16
+
17
+ def all_assets
18
+ app.config.assets + image_assets
19
+ end
20
+
21
+ def image_assets
22
+ image_assets_path = app.root.join("app/assets/images")
23
+ Dir.glob("#{image_assets_path}/**/*.{png,jpg,gif,jpeg}")
24
+ .map { |full_path| full_path.sub(%r{\A#{image_assets_path}/}, "") }
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FoxPage
4
+ module Builders
5
+ module FileCopy
6
+ def copy_public_files
7
+ puts "COPY\tpublic/* => #{OUTPUT_DIRECTORY}/"
8
+ FileUtils.cp_r public_path, output_path
9
+ end
10
+
11
+ private
12
+
13
+ def public_path
14
+ app.root.join("public", ".")
15
+ end
16
+
17
+ def output_path
18
+ app.root.join(OUTPUT_DIRECTORY)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tilt"
4
+ require "fileutils"
5
+
6
+ module FoxPage
7
+ module Builders
8
+ module Pages
9
+ using Refinements::Camelize
10
+
11
+ def build_pages
12
+ app.routes.each do |path, route|
13
+ puts "PAGE\t#{path} => #{route.base_name}"
14
+
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)
20
+ end
21
+ end
22
+ end
23
+
24
+ def render_route(route)
25
+ controller = spiced_controller(route).new
26
+ controller.method(route.method_name).call
27
+
28
+ layout = Tilt.new(layout_path(controller))
29
+ page = Tilt.new(page_path(route))
30
+
31
+ controller.instance_eval do
32
+ layout.render(self) { page.render(self) }
33
+ end
34
+ end
35
+
36
+ # for the sake of keeping the original classes sane while building, we
37
+ # create a subclass of the original dynamically and inject common helpers
38
+ # to it and also run before_actions
39
+ def spiced_controller(route) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/LineLength
40
+ Class.new(route.controller).tap do |klass| # rubocop:disable Metrics/BlockLength, Metrics/LineLength
41
+ klass.include(Helpers::AppHelper.new(app))
42
+ klass.include(Helpers::AssetsHelper)
43
+ klass.include(Helpers::RenderHelper)
44
+
45
+ # include global ApplicationHelper if possible
46
+ begin
47
+ klass.include(ApplicationHelper)
48
+ rescue NameError # rubocop:disable Lint/HandleExceptions
49
+ # we don't have a global ApplicationHelper... which is fine
50
+ end
51
+
52
+ # find a controller-specific helper class and include it if we can
53
+ begin
54
+ helper = Kernel.const_get("#{route.base_name}_helper".camelize)
55
+ klass.include(helper)
56
+ rescue NameError # rubocop:disable Lint/HandleExceptions
57
+ # same difference
58
+ end
59
+
60
+ klass.define_method(:inspect) do |*args|
61
+ # report that we are actually the controller, not some random
62
+ # anonymous class
63
+ # append a + to it to indicate that it's different than an ordinary
64
+ # class instance
65
+ super(*args).sub(/#<Class:[^>]+>/, "#{route.controller}+")
66
+ end
67
+
68
+ klass.define_singleton_method(:inspect) do
69
+ # for .ancestors to show up correctly
70
+ "#{route.controller}+"
71
+ end
72
+
73
+ klass.define_method(:to_s) do |*args|
74
+ # irb uses this method for displaying in the prompt
75
+ super(*args).sub(/#<Class:[^>]+>/, "#{route.controller}+")
76
+ end
77
+
78
+ # inject filters
79
+ route.controller.public_instance_methods(false).each do |method|
80
+ klass.define_method(method) do |*args|
81
+ # @__before_actions is set on the original class -- use it from
82
+ # that one
83
+ route.controller.instance_variable_get(:@__before_actions)&.each do |action| # rubocop:disable Metrics/LineLength
84
+ send(action)
85
+ end
86
+ super(*args)
87
+ end
88
+ end
89
+ end
90
+ end
91
+
92
+ def layout_path(controller)
93
+ File
94
+ .join(views_path, controller.class.layout)
95
+ .tap(&method(:validate_file_exists))
96
+ end
97
+
98
+ def page_path(route)
99
+ Dir
100
+ .glob(
101
+ File.join(views_path, route.base_name, "#{route.method_name}.*")
102
+ )
103
+ .first
104
+ .tap { |file| validate_file_exists(file, route) }
105
+ end
106
+
107
+ def views_path
108
+ @views_path ||= app.root.join("app", "views")
109
+ end
110
+
111
+ def validate_file_exists(file, route = nil)
112
+ return if file && File.exist?(file)
113
+
114
+ error_message = if route
115
+ "template for #{route.base_name}##{route.method_name}"
116
+ else
117
+ "layout template"
118
+ end
119
+
120
+ raise ArgumentError, "Could not find #{error_message}"
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+
5
+ module FoxPage
6
+ class Cli < Thor
7
+ desc "build", "Builds your website"
8
+ def build
9
+ app = require_application
10
+
11
+ app.build
12
+ end
13
+
14
+ desc "server", "Runs a server for quick development"
15
+ def server
16
+ app = require_application
17
+
18
+ app.server.start
19
+ end
20
+
21
+ register FoxPage::Generator,
22
+ "new", "new NAME",
23
+ "Create a new FoxPage website"
24
+
25
+ private
26
+
27
+ def require_application
28
+ require File.join(Bundler.root, "config", "environment")
29
+
30
+ ObjectSpace.each_object(Class).find do |klass|
31
+ klass.superclass == FoxPage::Application
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FoxPage
4
+ class Controller
5
+ DEFAULT_LAYOUT = "layouts/default.haml"
6
+ private_constant :DEFAULT_LAYOUT
7
+
8
+ def self.layout
9
+ DEFAULT_LAYOUT
10
+ end
11
+
12
+ def self.before_action(method_name)
13
+ @__before_actions ||= []
14
+ @__before_actions << method_name
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+
5
+ module FoxPage
6
+ class Generator < Thor::Group
7
+ include Thor::Actions
8
+
9
+ argument :name, type: :string, desc: "The name of your website"
10
+
11
+ def self.source_root
12
+ File.join(__dir__, "app_template")
13
+ end
14
+
15
+ def create_application
16
+ Dir[File.join(self.class.source_root, "**/*.tt")]
17
+ .map { |path| path.sub(self.class.source_root + "/", "") }
18
+ .each do |path|
19
+ template(path,
20
+ File.join(name,
21
+ path.sub(/\.tt$/, "")
22
+ .gsub(/__dot__/, ".")))
23
+ end
24
+ end
25
+
26
+ def run_bundle
27
+ Dir.chdir(name) do
28
+ system("bundle install")
29
+ system("bundle binstubs foxpage")
30
+ end
31
+ end
32
+
33
+ def init_git_repo
34
+ Dir.chdir(name) do
35
+ system("git init")
36
+ system("git add .")
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FoxPage
4
+ module Helpers
5
+ # the AppHelper module builder injects the core FoxPage::Application
6
+ # instance to the method `app`
7
+ class AppHelper < Module
8
+ def initialize(app)
9
+ @__app = app
10
+ define_method(:app) do
11
+ app
12
+ end
13
+ end
14
+
15
+ def inspect
16
+ "#{self.class.name}(#{@__app.class})"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sprockets"
4
+
5
+ module FoxPage
6
+ module Helpers
7
+ module AssetsHelper
8
+ def asset_path(source)
9
+ File.join("/assets", app.sprockets.manifest.assets[source])
10
+ end
11
+
12
+ def stylesheet_link_tag(source)
13
+ %(<link rel="stylesheet" href=#{asset_path(source).inspect} />)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FoxPage
4
+ module Helpers
5
+ module RenderHelper
6
+ def render(view)
7
+ full_path = Dir.glob(app.root.join("app/views/#{view}.*")).first
8
+
9
+ unless full_path
10
+ raise ArgumentError, "Could not find template for #{view}"
11
+ end
12
+
13
+ Tilt.new(full_path).render(self)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FoxPage
4
+ module Refinements
5
+ module Camelize
6
+ refine String do
7
+ def camelize
8
+ split("_").map(&:capitalize).join
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ostruct"
4
+
5
+ module FoxPage
6
+ module Refinements
7
+ module ToDeepOpenStruct
8
+ refine Array do
9
+ def to_deep_ostruct
10
+ map do |value|
11
+ next value unless value.is_a?(Hash) || value.is_a?(Array)
12
+
13
+ value.to_deep_ostruct
14
+ end
15
+ end
16
+ end
17
+
18
+ refine Hash do
19
+ def to_deep_ostruct
20
+ OpenStruct.new(
21
+ dup.tap do |hash|
22
+ hash.each do |key, value|
23
+ next unless value.is_a?(Hash) || value.is_a?(Array)
24
+
25
+ hash[key] = value.to_deep_ostruct
26
+ end
27
+ end
28
+ )
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ostruct"
4
+
5
+ module FoxPage
6
+ class Router
7
+ using Refinements::Camelize
8
+
9
+ def self.draw_routes(&block)
10
+ new.draw_routes(&block)
11
+ end
12
+
13
+ attr_reader :routes
14
+
15
+ def initialize
16
+ @routes = {}
17
+ end
18
+
19
+ def draw_routes(&block)
20
+ instance_eval(&block)
21
+ routes
22
+ end
23
+
24
+ def root(target)
25
+ routes["/"] = parse_target(target)
26
+ end
27
+
28
+ def map(mapping)
29
+ mapping.each do |path, target|
30
+ routes[path] = parse_target(target)
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def parse_target(target)
37
+ base_name, method_name = target.split("#")
38
+ controller = Kernel.const_get("#{base_name}_controller".camelize)
39
+ method_name = method_name.to_sym
40
+
41
+ validate_controller_method(controller, method_name)
42
+
43
+ OpenStruct.new(
44
+ base_name: base_name,
45
+ controller: controller,
46
+ method_name: method_name
47
+ )
48
+ end
49
+
50
+ def validate_controller_method(controller, method_name)
51
+ return if controller.instance_methods.include?(method_name)
52
+
53
+ raise ArgumentError, "#{controller} does not define ##{method_name}"
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "listen"
4
+ require "webrick"
5
+
6
+ module FoxPage
7
+ class Server
8
+ def initialize(app)
9
+ @app = app
10
+ @listener = Listen.to(app.root.join("app"),
11
+ app.root.join("public"),
12
+ &method(:handle_modified_app))
13
+ @server = WEBrick::HTTPServer.new(
14
+ BindAddress: ENV.fetch("APP_BIND", "127.0.0.1"),
15
+ Port: ENV.fetch("APP_PORT", 3000).to_i,
16
+ DocumentRoot: app.root.join(OUTPUT_DIRECTORY)
17
+ )
18
+ end
19
+
20
+ def start
21
+ puts "==> Starting up development server at " \
22
+ "http://#{@server.config[:BindAddress]}:#{@server.config[:Port]}"
23
+
24
+ trap "INT" do
25
+ @server.shutdown
26
+ end
27
+
28
+ @app.build
29
+ @listener.start
30
+ @server.start
31
+ end
32
+
33
+ def handle_modified_app(_modified, _added, _removed)
34
+ reload_code
35
+ @app.build
36
+ rescue Exception => e # rubocop:disable Lint/RescueException
37
+ # need to rescue Exception as syntax errors may cause the builds to break
38
+ puts "!!! An error occurred while building the app"
39
+ puts e.full_message
40
+ end
41
+
42
+ def reload_code
43
+ @app.code_loader.reload
44
+ @app.reload_routes!
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module FoxPage
6
+ class SiteBuilder
7
+ include Builders::Assets
8
+ include Builders::FileCopy
9
+ include Builders::Pages
10
+
11
+ def self.build(app)
12
+ new(app).build
13
+ end
14
+
15
+ attr_reader :app, :output_directory
16
+
17
+ def initialize(app)
18
+ @app = app
19
+ @output_directory = app.root.join(OUTPUT_DIRECTORY)
20
+ end
21
+
22
+ def build
23
+ puts "==> Building site #{App.config.site&.title}"
24
+
25
+ FileUtils.mkdir_p output_directory
26
+
27
+ build_assets
28
+ build_pages
29
+ copy_public_files
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FoxPage
4
+ VERSION = "0.1.0"
5
+ end
data/lib/fox_page.rb ADDED
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler"
4
+
5
+ require "zeitwerk"
6
+ loader = Zeitwerk::Loader.for_gem
7
+ begin
8
+ loader.ignore Bundler.root
9
+ rescue Bundler::GemfileNotFound # rubocop:disable Lint/HandleExceptions
10
+ # don't care ...
11
+ end
12
+ loader.setup
13
+
14
+ require_relative "./fox_page/version"
15
+
16
+ module FoxPage
17
+ class Error < StandardError; end
18
+
19
+ OUTPUT_DIRECTORY = "_site"
20
+ end
data/lib/foxpage.rb ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./fox_page"
data/misc/foxpage.png ADDED
Binary file