machined 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/Gemfile +2 -0
- data/LICENSE +20 -0
- data/README.md +49 -0
- data/Rakefile +6 -0
- data/bin/machined +4 -0
- data/lib/machined.rb +23 -0
- data/lib/machined/cli.rb +99 -0
- data/lib/machined/context.rb +52 -0
- data/lib/machined/environment.rb +297 -0
- data/lib/machined/helpers/asset_tag_helpers.rb +135 -0
- data/lib/machined/helpers/locals_helpers.rb +57 -0
- data/lib/machined/helpers/output_helpers.rb +43 -0
- data/lib/machined/helpers/render_helpers.rb +138 -0
- data/lib/machined/processors/front_matter_processor.rb +35 -0
- data/lib/machined/processors/layout_processor.rb +71 -0
- data/lib/machined/server.rb +37 -0
- data/lib/machined/sprocket.rb +55 -0
- data/lib/machined/static_compiler.rb +71 -0
- data/lib/machined/templates/site/Gemfile.tt +10 -0
- data/lib/machined/templates/site/assets/images/.empty_directory +0 -0
- data/lib/machined/templates/site/assets/javascripts/main.js.coffee +0 -0
- data/lib/machined/templates/site/assets/stylesheets/main.css.scss +0 -0
- data/lib/machined/templates/site/config.ru +2 -0
- data/lib/machined/templates/site/machined.rb +17 -0
- data/lib/machined/templates/site/pages/index.html.erb +5 -0
- data/lib/machined/templates/site/public/.empty_directory +0 -0
- data/lib/machined/templates/site/views/layouts/main.html.erb +12 -0
- data/lib/machined/utils.rb +31 -0
- data/lib/machined/version.rb +3 -0
- data/machined.gemspec +39 -0
- data/spec/machined/cli_spec.rb +154 -0
- data/spec/machined/context_spec.rb +20 -0
- data/spec/machined/environment_spec.rb +202 -0
- data/spec/machined/helpers/asset_tag_helpers_spec.rb +95 -0
- data/spec/machined/helpers/locals_helper_spec.rb +37 -0
- data/spec/machined/helpers/output_helpers_spec.rb +81 -0
- data/spec/machined/helpers/render_helpers_spec.rb +53 -0
- data/spec/machined/processors/front_matter_processor_spec.rb +42 -0
- data/spec/machined/processors/layout_processor_spec.rb +32 -0
- data/spec/machined/server_spec.rb +77 -0
- data/spec/machined/sprocket_spec.rb +36 -0
- data/spec/machined/static_compiler_spec.rb +85 -0
- data/spec/machined/utils_spec.rb +31 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/support/helpers.rb +59 -0
- data/spec/support/match_paths_matcher.rb +20 -0
- 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
|
+
|