marv 0.2.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 (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