machined 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 (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
+