marv 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. data/.document +5 -0
  2. data/.gitmodules +3 -0
  3. data/.rspec +1 -0
  4. data/CHANGELOG.md +8 -0
  5. data/Gemfile +29 -0
  6. data/Gemfile.lock +140 -0
  7. data/LICENSE +20 -0
  8. data/README.md +49 -0
  9. data/Rakefile +54 -0
  10. data/VERSION +1 -0
  11. data/bin/marv +12 -0
  12. data/features/step_definitions/marv_steps.rb +38 -0
  13. data/features/support/env.rb +17 -0
  14. data/layouts/config/config.tt +19 -0
  15. data/layouts/default/functions/functions.php.erb +34 -0
  16. data/layouts/default/javascripts/admin.js +1 -0
  17. data/layouts/default/javascripts/theme.js +1 -0
  18. data/layouts/default/stylesheets/_header.scss.erb +18 -0
  19. data/layouts/default/stylesheets/_reset.scss +5 -0
  20. data/layouts/default/stylesheets/_typography.scss +5 -0
  21. data/layouts/default/stylesheets/style.css.scss.erb +32 -0
  22. data/layouts/default/templates/404.php.erb +10 -0
  23. data/layouts/default/templates/archive.php.erb +11 -0
  24. data/layouts/default/templates/attachment.php.erb +32 -0
  25. data/layouts/default/templates/comments.php +2 -0
  26. data/layouts/default/templates/footer.php +9 -0
  27. data/layouts/default/templates/header.php.erb +30 -0
  28. data/layouts/default/templates/index.php +4 -0
  29. data/layouts/default/templates/page.php +14 -0
  30. data/layouts/default/templates/partials/loop.php.erb +36 -0
  31. data/layouts/default/templates/search.php.erb +12 -0
  32. data/layouts/default/templates/sidebar.php +9 -0
  33. data/layouts/default/templates/single.php.erb +30 -0
  34. data/lib/guard/marv/assets.rb +31 -0
  35. data/lib/guard/marv/config.rb +34 -0
  36. data/lib/guard/marv/functions.rb +36 -0
  37. data/lib/guard/marv/templates.rb +28 -0
  38. data/lib/marv.rb +11 -0
  39. data/lib/marv/builder.rb +282 -0
  40. data/lib/marv/cli.rb +86 -0
  41. data/lib/marv/config.rb +61 -0
  42. data/lib/marv/engines.rb +12 -0
  43. data/lib/marv/error.rb +8 -0
  44. data/lib/marv/generator.rb +138 -0
  45. data/lib/marv/guard.rb +65 -0
  46. data/lib/marv/project.rb +162 -0
  47. data/lib/marv/version.rb +3 -0
  48. data/marv.gemspec +145 -0
  49. data/spec/lib/marv/config_spec.rb +79 -0
  50. data/spec/lib/marv/project_spec.rb +34 -0
  51. data/spec/spec_helper.rb +13 -0
  52. metadata +404 -0
@@ -0,0 +1,282 @@
1
+ require 'sprockets'
2
+ require 'sprockets-sass'
3
+ require 'sass'
4
+ require 'less'
5
+ require 'zip'
6
+ require 'marv/engines'
7
+
8
+ module Marv
9
+ class Builder
10
+ def initialize(project)
11
+ @project = project
12
+ @task = project.task
13
+ @templates_path = @project.templates_path
14
+ @assets_path = @project.assets_path
15
+ @functions_path = @project.functions_path
16
+ @includes_path = @project.includes_path
17
+ @extras_path = @project.extras_path
18
+ @package_path = @project.package_path
19
+
20
+ init_sprockets
21
+ end
22
+
23
+ # Runs all the methods necessary to build a completed project
24
+ def build
25
+ clean_build_directory
26
+ copy_templates
27
+ copy_functions
28
+ copy_includes
29
+ copy_extras
30
+ build_assets
31
+ end
32
+
33
+ # Use the rubyzip library to build a zip from the generated source
34
+ def zip(filename=nil)
35
+ filename = filename || File.basename(@project.root)
36
+ project_base = File.basename(@project.root)
37
+
38
+ zip_filename = File.join(File.basename(@package_path), "#{filename}.zip")
39
+ # Create a temporary file for RubyZip to write to
40
+ temp_filename = "#{zip_filename}.tmp"
41
+
42
+ File.delete(temp_filename) if File.exists?(temp_filename)
43
+
44
+ # Wrapping the zip creation in Thor's create_file to get "overwrite" prompts
45
+ # Note: I could be overcomplicating this
46
+ @task.create_file(zip_filename) do
47
+ Zip::File.open(temp_filename, Zip::File::CREATE) do |zip|
48
+ # Get all filenames in the build directory recursively
49
+ filenames = Dir[File.join(@project.build_path, '**', '*')]
50
+
51
+ # Remove the build directory path from the filename
52
+ filenames.collect! {|path| path.gsub(/#{@project.build_path}\//, '')}
53
+
54
+ # Add each file in the build directory to the zip file
55
+ filenames.each do |filename|
56
+ zip.add File.join(project_base, filename), File.join(@project.build_path, filename)
57
+ end
58
+ end
59
+
60
+ # Give Thor contents of zip file for "overwrite" prompt
61
+ File.open(temp_filename, 'rb') { |f| f.read }
62
+ end
63
+
64
+ # Clean up the temp file
65
+ File.delete(temp_filename)
66
+ end
67
+
68
+ # Empty out the build directory
69
+ def clean_build_directory
70
+ FileUtils.rm_rf Dir.glob(File.join(@project.build_path, '*'))
71
+ end
72
+
73
+ def clean_templates
74
+ # TODO: cleaner way of removing templates only?
75
+ Dir.glob(File.join(@project.build_path, '*.php')).each do |path|
76
+ FileUtils.rm path unless path.include?('functions.php')
77
+ end
78
+ end
79
+
80
+ def copy_templates
81
+ template_paths.each do |template_path|
82
+ # Skip directories
83
+ next if File.directory?(template_path)
84
+
85
+ if template_path.end_with?('.erb')
86
+ # Chop the .erb extension off the filename
87
+ destination = File.join(@project.build_path, File.basename(template_path).slice(0..-5))
88
+
89
+ write_erb(template_path, destination)
90
+ else
91
+ # Regular old copy of PHP-only files
92
+ FileUtils.cp template_path, @project.build_path
93
+ end
94
+ end
95
+ end
96
+
97
+ def clean_functions
98
+ FileUtils.rm File.join(@project.build_path, 'functions.php')
99
+ FileUtils.rm_rf File.join(@project.build_path, 'functions')
100
+ end
101
+
102
+ def copy_functions
103
+ functions_erb_path = File.join(@functions_path, 'functions.php.erb')
104
+ functions_php_path = File.join(@functions_path, 'functions.php')
105
+
106
+ if File.exists?(functions_erb_path)
107
+ destination = File.join(@project.build_path, 'functions.php')
108
+ write_erb(functions_erb_path, destination)
109
+ elsif File.exists?(functions_php_path)
110
+ FileUtils.cp functions_php_path, @project.build_path
111
+ end
112
+
113
+ functions_paths = Dir.glob(File.join(@functions_path, '*')).reject do |filename|
114
+ [functions_erb_path, functions_php_path].include?(filename)
115
+ end
116
+
117
+ unless functions_paths.empty?
118
+ # Create the includes folder in the build directory
119
+ FileUtils.mkdir_p(File.join(@project.build_path, 'functions'))
120
+
121
+ # Iterate over all files in source/functions, skipping the actual functions.php file
122
+ paths = Dir.glob(File.join(@functions_path, '**', '*')).reject {|filename| [functions_erb_path, functions_php_path].include?(filename) }
123
+
124
+ copy_paths_with_erb(paths, @functions_path, File.join(@project.build_path, 'functions'))
125
+ end
126
+ end
127
+
128
+ def clean_includes
129
+ FileUtils.rm_rf File.join(@project.build_path, 'includes')
130
+ end
131
+
132
+ def copy_includes
133
+ unless Dir.glob(File.join(@includes_path, '*')).empty?
134
+ # Create the includes folder in the build directory
135
+ FileUtils.mkdir(File.join(@project.build_path, 'includes'))
136
+
137
+ # Iterate over all files in source/includes, so we can exclude if necessary
138
+ paths = Dir.glob(File.join(@includes_path, '**', '*'))
139
+ copy_paths_with_erb(paths, @includes_path, File.join(@project.build_path, 'includes'))
140
+ end
141
+ end
142
+
143
+ def copy_extras
144
+ unless Dir.glob(File.join(@extras_path, '*')).empty?
145
+
146
+ # Iterate over all files in source/extras, so we can exclude if necessary
147
+ paths = Dir.glob(File.join(@extras_path, '**', '*'))
148
+ copy_paths_with_erb(paths, @extras_path, File.join(@project.build_path, ''))
149
+ end
150
+ end
151
+
152
+ def clean_images
153
+ FileUtils.rm_rf File.join(@project.build_path, 'images')
154
+ end
155
+
156
+ def build_assets
157
+ [['style.css'], ['ie.css'], ['javascripts', 'theme.js'], ['javascripts', 'admin.js']].each do |asset|
158
+ destination = File.join(@project.build_path, asset)
159
+
160
+ sprocket = @sprockets.find_asset(asset.last)
161
+
162
+ # Catch any sprockets errors and continue the process
163
+ begin
164
+ @task.shell.mute do
165
+ FileUtils.mkdir_p(File.dirname(destination)) unless File.directory?(File.dirname(destination))
166
+ sprocket.write_to(destination) unless sprocket.nil?
167
+
168
+ if @project.config[:compress_js] && destination.end_with?('.js')
169
+ require "yui/compressor"
170
+
171
+ # Grab the initial sprockets output
172
+ sprockets_output = File.open(destination, 'r').read
173
+
174
+ # Re-write the file, minified
175
+ File.open(destination, 'w') do |file|
176
+ file.write(YUI::JavaScriptCompressor.new.compress(sprockets_output))
177
+ end
178
+ end
179
+ end
180
+ rescue Exception => e
181
+ @task.say "Error while building #{asset.last}:"
182
+ @task.say e.message, Thor::Shell::Color::RED
183
+ File.open(destination, 'w') do |file|
184
+ file.puts(e.message)
185
+ end
186
+
187
+ # Re-initializing sprockets to prevent further errors
188
+ # TODO: This is done for lack of a better solution
189
+ init_sprockets
190
+ end
191
+ end
192
+
193
+ # Copy the images directory over
194
+ FileUtils.cp_r(File.join(@assets_path, 'images'), @project.build_path) if File.exists?(File.join(@assets_path, 'images'))
195
+
196
+ # Check for screenshot and move it into main build directory
197
+ Dir.glob(File.join(@project.build_path, 'images', '*')).each do |filename|
198
+ if filename.index(/screenshot\.(png|jpg|jpeg|gif)/)
199
+ FileUtils.mv(filename, @project.build_path + File::SEPARATOR )
200
+ end
201
+ end
202
+ end
203
+
204
+ private
205
+
206
+ def copy_paths_with_erb(paths, source_dir, destination_dir)
207
+ paths.each do |path|
208
+ # Remove source directory from full file path to get the relative path
209
+ relative_path = path.gsub(source_dir, '')
210
+
211
+ destination = File.join(destination_dir, relative_path)
212
+
213
+ if destination.end_with?('.erb')
214
+ # Remove the .erb extension if the path was an erb file
215
+ destination = destination.slice(0..-5)
216
+ # And process it as an erb
217
+ write_erb(path, destination)
218
+ else
219
+ # Otherwise, we simply move the file over
220
+ FileUtils.mkdir_p(destination) if File.directory?(path)
221
+ FileUtils.cp path, destination unless File.directory?(path)
222
+ end
223
+ end
224
+ end
225
+
226
+ def init_sprockets
227
+ @sprockets = Sprockets::Environment.new
228
+
229
+ ['javascripts', 'stylesheets', 'lib'].each do |dir|
230
+ @sprockets.append_path File.join(@assets_path, dir)
231
+ end
232
+
233
+ # Add assets/styleshets to load path for Less Engine
234
+ Tilt::LessTemplateWithPaths.load_path = File.join(@assets_path, 'stylesheets')
235
+
236
+ @sprockets.register_engine '.less', Tilt::LessTemplateWithPaths
237
+
238
+ # Passing the @project instance variable to the Sprockets::Context instance
239
+ # used for processing the asset ERB files. Ruby meta-programming, FTW.
240
+ @sprockets.context_class.instance_exec(@project) do |project|
241
+ define_method :config do
242
+ project.config
243
+ end
244
+ end
245
+ end
246
+
247
+ def template_paths
248
+ Dir.glob(File.join(@templates_path, '**', '*'))
249
+ end
250
+
251
+ # Generate a unique filename for the zip output
252
+ def get_output_filename(basename)
253
+ package_path_base = File.basename(@package_path)
254
+ filename = File.join(package_path_base, "#{basename}.zip")
255
+
256
+ i = 1
257
+ while File.exists?(filename)
258
+ filename = File.join(package_path_base, "#{basename}(#{i}).zip")
259
+ i += 1
260
+ end
261
+
262
+ filename
263
+ end
264
+
265
+ protected
266
+
267
+ # Write an .erb from source to destination, catching and reporting errors along the way
268
+ def write_erb(source, destination)
269
+ begin
270
+ @task.shell.mute do
271
+ @task.create_file(destination) do
272
+ @project.parse_erb(source)
273
+ end
274
+ end
275
+ rescue Exception => e
276
+ @task.say "Error while building #{File.basename(source)}:"
277
+ @task.say e.message + "\n", Thor::Shell::Color::RED
278
+ exit
279
+ end
280
+ end
281
+ end
282
+ end
data/lib/marv/cli.rb ADDED
@@ -0,0 +1,86 @@
1
+ require 'thor'
2
+ require 'yaml'
3
+ require 'guard/marv/assets'
4
+ require 'guard/marv/config'
5
+ require 'guard/marv/templates'
6
+ require 'guard/marv/functions'
7
+
8
+ module Marv
9
+ class CLI < Thor
10
+ include Thor::Actions
11
+
12
+ def self.source_root
13
+ File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'layouts'))
14
+ end
15
+
16
+ desc "create DIRECTORY", "Creates a Marv project"
17
+ def create(dir)
18
+ theme = {}
19
+ theme[:name] = dir
20
+
21
+ project = Marv::Project.create(dir, theme, self)
22
+ end
23
+
24
+ desc "link PATH", "Create a symbolic link to the compilation directory"
25
+ long_desc "This command will symlink the compiled version of the theme to the specified path.\n\n"+
26
+ "To compile the theme use the `marv watch` command"
27
+ def link(path)
28
+ project = Marv::Project.new('.', self)
29
+
30
+ FileUtils.mkdir_p project.build_path unless File.directory?(project.build_path)
31
+
32
+ do_link(project, path)
33
+ end
34
+
35
+ desc "watch", "Start watch process"
36
+ long_desc "Watches the source directory in your project for changes, and reflects those changes in a compile folder"
37
+ method_option :config, :type => :string, :desc => "Name of alternate config file"
38
+ def watch
39
+ project = Marv::Project.new('.', self, nil, options[:config])
40
+
41
+ # Empty the build directory before starting up to clean out old files
42
+ FileUtils.rm_rf project.build_path
43
+ FileUtils.mkdir_p project.build_path
44
+
45
+ Marv::Guard.start(project, self)
46
+ end
47
+
48
+ desc "build DIRECTORY", "Build your theme into specified directory"
49
+ method_option :config, :type => :string, :desc => "Name of alternate config file"
50
+ def build(dir='build')
51
+ project = Marv::Project.new('.', self, nil, options[:config])
52
+
53
+ builder = Builder.new(project)
54
+ builder.build
55
+
56
+ Dir.glob(File.join(dir, '**', '*')).each do |file|
57
+ shell.mute { remove_file(file) }
58
+ end
59
+
60
+ directory(project.build_path, dir)
61
+ end
62
+
63
+ desc "package FILENAME", "Compile and zip your project to FILENAME.zip"
64
+ method_option :config, :type => :string, :desc => "Name of alternate config file"
65
+ def package(filename=nil)
66
+ project = Marv::Project.new('.', self, nil, options[:config])
67
+
68
+ builder = Builder.new(project)
69
+ builder.build
70
+ builder.zip(filename)
71
+ end
72
+
73
+ protected
74
+ def do_link(project, path)
75
+ begin
76
+ project.link(path)
77
+ rescue LinkSourceDirNotFound
78
+ say_status :error, "The path #{File.dirname(path)} does not exist", :red
79
+ exit 2
80
+ rescue Errno::EEXIST
81
+ say_status :error, "The path #{path} already exsts", :red
82
+ exit 2
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,61 @@
1
+ module Marv
2
+ # Reads/Writes a configuration file in the user's home directory
3
+ #
4
+ class Config
5
+
6
+ @config
7
+
8
+ attr_accessor :config
9
+
10
+ def initialize()
11
+ @config = {
12
+ :theme => {
13
+ :author => nil,
14
+ :author_url => nil,
15
+ },
16
+ :links => []
17
+ }
18
+ end
19
+
20
+ # Provides access to the config using the Hash square brackets
21
+ def [](var)
22
+ @config[var]
23
+ end
24
+
25
+ # Allows modifying variables through hash square brackets
26
+ def []=(var, value)
27
+ @config[var] = value
28
+ end
29
+
30
+ # Returns the path to the user's configuration file
31
+ def config_file
32
+ @config_file ||= File.expand_path(File.join('~', '.watch', 'config.yml'))
33
+ end
34
+
35
+ # Writes the configuration file
36
+ def write(options={})
37
+ # If we're unit testing then it helps to use a
38
+ # StringIO object instead of a file buffer
39
+ io = options[:io] || File.open(self.config_file, 'w')
40
+
41
+ io.write JSON.generate(@config)
42
+
43
+ io.close
44
+
45
+ self
46
+ end
47
+
48
+ # Loads config declarations in user's home dir
49
+ #
50
+ # If file does not exist then it will be created
51
+ def read
52
+ return write unless File.exists?(self.config_file)
53
+
54
+ data = File.open(self.config_file).read
55
+
56
+ @config = data.empty? ? {} : JSON.parse(data)
57
+
58
+ self
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,12 @@
1
+ module Tilt
2
+ class LessTemplateWithPaths < LessTemplate
3
+ class << self
4
+ attr_accessor :load_path
5
+ end
6
+
7
+ def prepare
8
+ parser = ::Less::Parser.new(:filename => eval_file, :line => line, :paths => [self.class.load_path])
9
+ @engine = parser.parse(data)
10
+ end
11
+ end
12
+ end
data/lib/marv/error.rb ADDED
@@ -0,0 +1,8 @@
1
+ module Marv
2
+ class Error < StandardError
3
+ end
4
+
5
+ # Raised when the link source could not be found
6
+ class LinkSourceDirNotFound < Error
7
+ end
8
+ end