machined 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/.gitignore +4 -0
  2. data/Gemfile +2 -0
  3. data/LICENSE +20 -0
  4. data/README.md +49 -0
  5. data/Rakefile +6 -0
  6. data/bin/machined +4 -0
  7. data/lib/machined.rb +23 -0
  8. data/lib/machined/cli.rb +99 -0
  9. data/lib/machined/context.rb +52 -0
  10. data/lib/machined/environment.rb +297 -0
  11. data/lib/machined/helpers/asset_tag_helpers.rb +135 -0
  12. data/lib/machined/helpers/locals_helpers.rb +57 -0
  13. data/lib/machined/helpers/output_helpers.rb +43 -0
  14. data/lib/machined/helpers/render_helpers.rb +138 -0
  15. data/lib/machined/processors/front_matter_processor.rb +35 -0
  16. data/lib/machined/processors/layout_processor.rb +71 -0
  17. data/lib/machined/server.rb +37 -0
  18. data/lib/machined/sprocket.rb +55 -0
  19. data/lib/machined/static_compiler.rb +71 -0
  20. data/lib/machined/templates/site/Gemfile.tt +10 -0
  21. data/lib/machined/templates/site/assets/images/.empty_directory +0 -0
  22. data/lib/machined/templates/site/assets/javascripts/main.js.coffee +0 -0
  23. data/lib/machined/templates/site/assets/stylesheets/main.css.scss +0 -0
  24. data/lib/machined/templates/site/config.ru +2 -0
  25. data/lib/machined/templates/site/machined.rb +17 -0
  26. data/lib/machined/templates/site/pages/index.html.erb +5 -0
  27. data/lib/machined/templates/site/public/.empty_directory +0 -0
  28. data/lib/machined/templates/site/views/layouts/main.html.erb +12 -0
  29. data/lib/machined/utils.rb +31 -0
  30. data/lib/machined/version.rb +3 -0
  31. data/machined.gemspec +39 -0
  32. data/spec/machined/cli_spec.rb +154 -0
  33. data/spec/machined/context_spec.rb +20 -0
  34. data/spec/machined/environment_spec.rb +202 -0
  35. data/spec/machined/helpers/asset_tag_helpers_spec.rb +95 -0
  36. data/spec/machined/helpers/locals_helper_spec.rb +37 -0
  37. data/spec/machined/helpers/output_helpers_spec.rb +81 -0
  38. data/spec/machined/helpers/render_helpers_spec.rb +53 -0
  39. data/spec/machined/processors/front_matter_processor_spec.rb +42 -0
  40. data/spec/machined/processors/layout_processor_spec.rb +32 -0
  41. data/spec/machined/server_spec.rb +77 -0
  42. data/spec/machined/sprocket_spec.rb +36 -0
  43. data/spec/machined/static_compiler_spec.rb +85 -0
  44. data/spec/machined/utils_spec.rb +31 -0
  45. data/spec/spec_helper.rb +16 -0
  46. data/spec/support/helpers.rb +59 -0
  47. data/spec/support/match_paths_matcher.rb +20 -0
  48. metadata +389 -0
@@ -0,0 +1,135 @@
1
+ require "active_support/concern"
2
+
3
+ module Machined
4
+ module Helpers
5
+ module AssetTagHelpers
6
+ extend ActiveSupport::Concern
7
+ include Padrino::Helpers::AssetTagHelpers
8
+
9
+ # Pattern for checking if a given path
10
+ # is an external URI.
11
+ URI_MATCH = %r(^[-a-z]+://|^cid:|^//)
12
+
13
+ # Returns a path to an asset, either in the output path
14
+ # or in the assets environment. It will default to appending
15
+ # the old-school timestamp.
16
+ def asset_path(kind, source)
17
+ return source if source =~ URI_MATCH
18
+
19
+ # Append extension if necessary.
20
+ if [:css, :js].include?(kind)
21
+ source << ".#{kind}" unless source =~ /\.#{kind}$/
22
+ end
23
+
24
+ # If the source points to an asset in the assets
25
+ # environment use `AssetPath` to generate the full path.
26
+ machined.assets.resolve(source) do |path|
27
+ return AssetPath.new(machined, machined.assets.find_asset(path)).to_s
28
+ end
29
+
30
+ # Default to using a basic `FilePath` to generate the
31
+ # full path.
32
+ FilePath.new(machined, source, kind).to_s
33
+ end
34
+
35
+ # `FilePath` generates a full path for a regular file
36
+ # in the output path. It's used by #asset_path to generate
37
+ # paths when using asset tags like #javascript_include_tag,
38
+ # #stylesheet_link_tag, and #image_tag
39
+ class FilePath
40
+ # A reference to the Machined environment.
41
+ attr_reader :machined
42
+
43
+ # The path from which to generate the full path to the asset.
44
+ attr_reader :source
45
+
46
+ # The expected kind of file (:css, :js, :images).
47
+ attr_reader :kind
48
+
49
+ #
50
+ def initialize(machined, source, kind)
51
+ @machined = machined
52
+ @source = source.to_s
53
+ @kind = kind
54
+ end
55
+
56
+ # Returns the full path to the asset, complete with
57
+ # timestamp.
58
+ def to_s
59
+ path = rewrite_base_path(source)
60
+ path = rewrite_timestamp(path)
61
+ path
62
+ end
63
+
64
+ protected
65
+
66
+ # Prepends the base path if the path is not
67
+ # already an absolute path.
68
+ def rewrite_base_path(path) # :nodoc:
69
+ if path =~ %r(^/)
70
+ path
71
+ else
72
+ File.join(base_path, path)
73
+ end
74
+ end
75
+
76
+ # Appends an asset timestamp based on the
77
+ # modification time of the asset.
78
+ def rewrite_timestamp(path) # :nodoc:
79
+ if timestamp = mtime(path)
80
+ "#{path}?#{timestamp.to_i}" unless path =~ /\?\d+$/
81
+ else
82
+ path
83
+ end
84
+ end
85
+
86
+ # Returns the expected base path for this asset.
87
+ def base_path # :nodoc:
88
+ case kind
89
+ when :css then "/stylesheets"
90
+ when :js then "/javascripts"
91
+ else
92
+ "/#{kind}"
93
+ end
94
+ end
95
+
96
+ # Returns the mtime for the given path (relative to
97
+ # the output path). Returns nil if the file doesn't exist.
98
+ def mtime(path) # :nodoc:
99
+ output_path = File.join(machined.output_path.to_s, path)
100
+ File.exist?(output_path) ? File.mtime(output_path) : nil
101
+ end
102
+ end
103
+
104
+ # `AssetPath` generates a full path for an asset
105
+ # that exists in Machined's `assets` environment.
106
+ class AssetPath < FilePath
107
+ attr_reader :asset
108
+
109
+ def initialize(machined, asset)
110
+ @machined = machined
111
+ @asset = asset
112
+ @source = digest? ? asset.digest_path : asset.logical_path
113
+ end
114
+
115
+ protected
116
+
117
+ def rewrite_timestamp(path)
118
+ digest? ? path : super
119
+ end
120
+
121
+ def digest?
122
+ machined.config.digest_assets
123
+ end
124
+
125
+ def base_path
126
+ machined.assets.config.url
127
+ end
128
+
129
+ def mtime(path)
130
+ asset.mtime
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,57 @@
1
+ require "active_support/concern"
2
+ require "active_support/hash_with_indifferent_access"
3
+
4
+ module Machined
5
+ module Helpers
6
+ module LocalsHelpers
7
+ extend ActiveSupport::Concern
8
+
9
+ # Adds psuedo local variables from the given hash, where
10
+ # the key is the name of the variable. This is provided so
11
+ # processors can add local variables without having access
12
+ # to the next processor or template.
13
+ def locals=(locals)
14
+ if locals.nil?
15
+ @locals = nil
16
+ else
17
+ self.locals.merge! locals
18
+ end
19
+ end
20
+
21
+ # Returns the locals hash. It's actually an instance
22
+ # of `ActiveSupport::HashWithIndifferentAccess`, so strings
23
+ # and symbols can be used interchangeably.
24
+ def locals
25
+ @locals ||= ActiveSupport::HashWithIndifferentAccess.new
26
+ end
27
+
28
+ # Returns true if the given +name+ has been set as a local
29
+ # variable.
30
+ def has_local?(name)
31
+ locals.key? name
32
+ end
33
+
34
+ # Returns the default layout, unless overridden by
35
+ # the YAML front matter.
36
+ def layout
37
+ if has_local?(:layout)
38
+ locals[:layout]
39
+ else
40
+ machined.config.layout
41
+ end
42
+ end
43
+
44
+ def method_missing(method, *args, &block) # :nodoc:
45
+ if args.empty? && has_local?(method)
46
+ locals[method]
47
+ else
48
+ super
49
+ end
50
+ end
51
+
52
+ def respond_to?(method) # :nodoc:
53
+ super or has_local?(method)
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,43 @@
1
+ require "active_support/concern"
2
+ require "active_support/memoizable"
3
+ require "tilt"
4
+
5
+ # We need to ensure that Tilt's ERB template uses
6
+ # the same output variable that Padrino's helpers expect.
7
+ Tilt::ERBTemplate.default_output_variable = "@_out_buf"
8
+
9
+ module Machined
10
+ module Helpers
11
+ module OutputHelpers
12
+ extend ActiveSupport::Concern
13
+ extend ActiveSupport::Memoizable
14
+
15
+ # A hash of Tilt templates that support
16
+ # capture blocks where the key is the name
17
+ # of the template.
18
+ CAPTURE_ENGINES = {
19
+ "Tilt::HamlTemplate" => :haml,
20
+ "Tilt::ERBTemplate" => :erb,
21
+ "Tilt::ErubisTemplate" => :erubis,
22
+ "Slim::Template" => :slim
23
+ }
24
+
25
+ protected
26
+
27
+ # Attempts to return the current engine based on
28
+ # the processors for this file. This is used by
29
+ # Padrino's helpers to determine which type of template
30
+ # engine to use when capturing blocks.
31
+ def current_engine
32
+ processors = environment.attributes_for(self.pathname).processors
33
+ processors or return
34
+ processors.each do |processor|
35
+ engine = CAPTURE_ENGINES[processor.to_s] and return engine
36
+ end
37
+
38
+ nil
39
+ end
40
+ memoize :current_engine
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,138 @@
1
+ require "pathname"
2
+ require "active_support/concern"
3
+
4
+ module Machined
5
+ module Helpers
6
+ module RenderHelpers
7
+ extend ActiveSupport::Concern
8
+ include LocalsHelpers
9
+
10
+ # This is the short form of both #render_partial and #render_collection.
11
+ # It works exactly like #render_partial, except if you pass the
12
+ # +:collection+ option:
13
+ #
14
+ # <%= render "ad", :collection => advertisements %>
15
+ # # is the same as:
16
+ # <%= render_collection advertisements, "ad" %>
17
+ #
18
+ def render(partial, options = {})
19
+ if collection = options.delete(:collection)
20
+ render_collection collection, partial, options
21
+ else
22
+ render_partial partial, options
23
+ end
24
+ end
25
+
26
+ # Renders the given +collection+ of objects with the given
27
+ # +partial+ template. This follows the same conventions
28
+ # of Rails' partial rendering, where the individual objects
29
+ # will be set as local variables based on the name of the partial:
30
+ #
31
+ # <%= render_collection advertisements, "ad" %>
32
+ #
33
+ # This will render the "ad" template and pass the local variable
34
+ # +ad+ to the template for display. An iteration counter will automatically
35
+ # be made available to the template with a name of the form
36
+ # +partial_name_counter+. In the case of the example above, the
37
+ # template would be fed +ad_counter+.
38
+ def render_collection(collection, partial, options = {})
39
+ return if collection.nil? || collection.empty?
40
+
41
+ template = resolve_partial(partial)
42
+ counter = 0
43
+ collection.inject('') do |output, object|
44
+ counter += 1
45
+ output << render_partial(template, options.merge(:object => object, :counter => counter))
46
+ end
47
+ end
48
+
49
+ # Renders a single +partial+. The primary options are:
50
+ #
51
+ # * <tt>:locals</tt> - A hash of local variables to use when
52
+ # rendering the partial.
53
+ # * <tt>:object</tt> - The object rendered in the partial.
54
+ # * <tt>:as</tt> - The name of the object to use.
55
+ #
56
+ # == Some Examples
57
+ #
58
+ # <%= render_partial "account" %>
59
+ #
60
+ # This will look for a template in the views paths with the name
61
+ # "account" or "_account". The files can be any processable Tilt
62
+ # template files, like ".erb", ".md", or ".haml" - or just plain ".html".
63
+ #
64
+ # <%= render_partial "account", :locals => { :account => buyer } %>
65
+ #
66
+ # This will set `buyer` as a local variable named "account". This can
67
+ # actually be written a few different ways:
68
+ #
69
+ # <%= render_partial "account", :account => buyer %>
70
+ # # Leftover options are assumed to be locals.
71
+ # <%= render_partial "account", :object => buyer %>
72
+ # # The local variable name "account" is inferred.
73
+ #
74
+ # As mentioned above, any options that are not used by #render_partial
75
+ # are assumed to be locals when the +:locals+ option is not set.
76
+ #
77
+ # Also mentioned above, the +:object+ option works like in Rails,
78
+ # where the local variable name will be inferred from the partial name.
79
+ # This can be overridden with the +:as+ option:
80
+ #
81
+ # <%= render_partial "account", :object => buyer, :as => "user" %>
82
+ #
83
+ # This is equivalent to:
84
+ #
85
+ # <%= render_partial "account", :locals => { :user => buyer } %>
86
+ #
87
+ def render_partial(partial, options = {})
88
+ template = resolve_partial(partial)
89
+ depend_on template
90
+
91
+ # Add object with the name of the partial
92
+ # as the local variable name.
93
+ if object = options.delete(:object)
94
+ object_name = options.delete(:as) || template.to_s[/_?(\w+)(\.\w+)*$/, 1]
95
+ self.locals[object_name] = object
96
+ self.locals["#{object_name}_counter"] = options.delete(:counter)
97
+ end
98
+
99
+ # Add locals from leftover options
100
+ if locals = options.delete(:locals) || options
101
+ self.locals = locals
102
+ end
103
+
104
+ evaluate_without_processor template, Machined::Processors::LayoutProcessor
105
+ end
106
+
107
+ protected
108
+
109
+ # Attempts to find a view with the given path,
110
+ # while also looking for a version with a partial-style
111
+ # name (prefixed with an "_").
112
+ def resolve_partial(path) # :nodoc:
113
+ path = Pathname.new(path)
114
+ path.absolute? and return path
115
+
116
+ # First look for the normal path
117
+ machined.views.resolve(path) { |found| return found }
118
+
119
+ # Then look for the partial-style version
120
+ unless path.basename.to_s =~ /^_/
121
+ partial = path.dirname.join("_#{path.basename}")
122
+ machined.views.resolve(partial) { |found| return found }
123
+ end
124
+
125
+ raise Sprockets::FileNotFound, "couldn't find file '#{path}'"
126
+ end
127
+
128
+ # Evaluates the given path without using the given processor.
129
+ # This is used to evaluate templates without wrapping
130
+ # them in layouts.
131
+ def evaluate_without_processor(path, processor) # :nodoc:
132
+ processors = environment.attributes_for(path).processors.dup
133
+ processors.delete processor
134
+ evaluate path, :processors => processors
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,35 @@
1
+ require "yaml"
2
+ require "tilt"
3
+
4
+ module Machined
5
+ module Processors
6
+ class FrontMatterProcessor < Tilt::Template
7
+ # The Regexp that separates the YAML
8
+ # front matter from the content.
9
+ FRONT_MATTER_PARSER = /
10
+ (
11
+ \A\s* # Beginning of file
12
+ ^---\s*$\n* # Start YAML Block
13
+ (.*?)\n* # YAML data
14
+ ^---\s*$\n* # End YAML Block
15
+ )
16
+ (.*)\Z # Rest of File
17
+ /mx
18
+
19
+ # See `Tilt::Template#prepare`.
20
+ def prepare
21
+ end
22
+
23
+ # See `Tilt::Template#evaluate`.
24
+ def evaluate(context, locals = {}, &block)
25
+ output = data
26
+ if FRONT_MATTER_PARSER.match data
27
+ locals = YAML.load $2
28
+ context.locals = locals if locals
29
+ output = $3
30
+ end
31
+ output
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,71 @@
1
+ require "tilt"
2
+
3
+ module Machined
4
+ module Processors
5
+ class LayoutProcessor < Tilt::Template
6
+ # A reference to the Sprockets context
7
+ attr_reader :context
8
+
9
+ # Path to the layout file
10
+ attr_reader :layout_path
11
+
12
+ # See `Tilt::Template#prepare`.
13
+ def prepare
14
+ end
15
+
16
+ # See `Tilt::Template#evaluate`.
17
+ def evaluate(context, locals, &block)
18
+ @context = context
19
+ if layout? && @layout_path = resolve_layout
20
+ context.depend_on @layout_path
21
+ evaluate_layout
22
+ else
23
+ data
24
+ end
25
+ end
26
+
27
+ protected
28
+
29
+ # A reference to the Views sprocket, where the
30
+ # layout asset will be.
31
+ def views
32
+ context.machined.views
33
+ end
34
+
35
+ # Determine if we should attempt to wrap the
36
+ # content with a layout.
37
+ def layout?
38
+ context.layout != false
39
+ end
40
+
41
+ # Attempt to find the layout file in the Views
42
+ # sprocket.
43
+ def resolve_layout
44
+ views.resolve "layouts/#{context.layout}", :content_type => context.content_type
45
+ rescue Sprockets::FileNotFound, Sprockets::ContentTypeMismatch
46
+ nil
47
+ end
48
+
49
+ # Recreate `Sprockets::Context#evaluate`, because it doesn't
50
+ # support yielding. I'm not even sure it's necessary to
51
+ # support multiple processors for a layout, though.
52
+ def evaluate_layout
53
+ processors = views.attributes_for(layout_path).processors
54
+ result = Sprockets::Utils.read_unicode layout_path
55
+
56
+ processors.each do |processor|
57
+ begin
58
+ template = processor.new(layout_path.to_s) { result }
59
+ result = template.render(context, {}) { data }
60
+ rescue Exception => e
61
+ context.annotate_exception! e
62
+ raise
63
+ end
64
+ end
65
+
66
+ result
67
+ end
68
+ end
69
+ end
70
+ end
71
+