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.
- 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
|
+
|