foxpage 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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