roger 0.0.1 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (89) hide show
  1. checksums.yaml +8 -8
  2. data/.gitignore +2 -0
  3. data/.travis.yml +12 -0
  4. data/CHANGELOG.md +102 -0
  5. data/Gemfile +5 -0
  6. data/MIT_LICENSE +20 -0
  7. data/README.md +10 -10
  8. data/Rakefile +9 -0
  9. data/bin/roger +5 -0
  10. data/doc/cli.md +46 -0
  11. data/doc/mockupfile.md +3 -0
  12. data/doc/templating.md +88 -0
  13. data/examples/default_template/.gitignore +2 -0
  14. data/examples/default_template/CHANGELOG +0 -0
  15. data/examples/default_template/Gemfile +3 -0
  16. data/examples/default_template/Mockupfile +1 -0
  17. data/examples/default_template/html/.empty_directory +0 -0
  18. data/examples/default_template/partials/.empty_directory +0 -0
  19. data/lib/roger/cli/command.rb +23 -0
  20. data/lib/roger/cli/generate.rb +5 -0
  21. data/lib/roger/cli/release.rb +10 -0
  22. data/lib/roger/cli/serve.rb +29 -0
  23. data/lib/roger/cli.rb +123 -0
  24. data/lib/roger/extractor.rb +95 -0
  25. data/lib/roger/generators/generator.rb +23 -0
  26. data/lib/roger/generators/new.rb +67 -0
  27. data/lib/roger/generators/templates/generator.tt +13 -0
  28. data/lib/roger/generators.rb +23 -0
  29. data/lib/roger/mockupfile.rb +63 -0
  30. data/lib/roger/project.rb +92 -0
  31. data/lib/roger/rack/html_validator.rb +26 -0
  32. data/lib/roger/rack/roger.rb +52 -0
  33. data/lib/roger/rack/sleep.rb +21 -0
  34. data/lib/roger/release/cleaner.rb +47 -0
  35. data/lib/roger/release/finalizers/dir.rb +29 -0
  36. data/lib/roger/release/finalizers/git_branch.rb +92 -0
  37. data/lib/roger/release/finalizers/rsync.rb +77 -0
  38. data/lib/roger/release/finalizers/zip.rb +42 -0
  39. data/lib/roger/release/finalizers.rb +19 -0
  40. data/lib/roger/release/injector.rb +99 -0
  41. data/lib/roger/release/processors/mockup.rb +93 -0
  42. data/lib/roger/release/processors/url_relativizer.rb +45 -0
  43. data/lib/roger/release/processors.rb +17 -0
  44. data/lib/roger/release/scm/git.rb +101 -0
  45. data/lib/roger/release/scm.rb +32 -0
  46. data/lib/roger/release.rb +363 -0
  47. data/lib/roger/resolver.rb +119 -0
  48. data/lib/roger/server.rb +117 -0
  49. data/lib/roger/template.rb +206 -0
  50. data/lib/roger/w3c_validator.rb +129 -0
  51. data/roger.gemspec +35 -0
  52. data/test/Mockupfile-syntax.rb +85 -0
  53. data/test/project/.rvmrc +1 -0
  54. data/test/project/Gemfile +7 -0
  55. data/test/project/Gemfile.lock +38 -0
  56. data/test/project/Mockupfile +13 -0
  57. data/test/project/html/formats/erb.html.erb +5 -0
  58. data/test/project/html/formats/index.html +1 -0
  59. data/test/project/html/formats/json.json.erb +0 -0
  60. data/test/project/html/formats/markdown.md +3 -0
  61. data/test/project/html/formats/mockup.html +5 -0
  62. data/test/project/html/front_matter/erb.html.erb +16 -0
  63. data/test/project/html/front_matter/markdown.md +7 -0
  64. data/test/project/html/layouts/content-for.html.erb +17 -0
  65. data/test/project/html/layouts/erb.html.erb +19 -0
  66. data/test/project/html/mockup/encoding.html +3 -0
  67. data/test/project/html/partials/erb.html.erb +10 -0
  68. data/test/project/html/partials/load_path.html.erb +3 -0
  69. data/test/project/html/partials/mockup.html +13 -0
  70. data/test/project/html/static/non-relative.html.erb +9 -0
  71. data/test/project/html/static/relative.html.erb +9 -0
  72. data/test/project/layouts/test.html.erb +34 -0
  73. data/test/project/layouts/yield.html.erb +1 -0
  74. data/test/project/lib/generators/test.rb +9 -0
  75. data/test/project/partials/formats/erb.html.erb +1 -0
  76. data/test/project/partials/partials-test.html.erb +1 -0
  77. data/test/project/partials/test/erb.html.erb +1 -0
  78. data/test/project/partials/test/front_matter.html.erb +1 -0
  79. data/test/project/partials/test/json.json.erb +1 -0
  80. data/test/project/partials/test/markdown.md +1 -0
  81. data/test/project/partials/test/mockup.part.html +1 -0
  82. data/test/project/partials/test/simple.html.erb +1 -0
  83. data/test/project/partials2/partials2-test.html.erb +1 -0
  84. data/test/unit/cli_test.rb +12 -0
  85. data/test/unit/generators_test.rb +75 -0
  86. data/test/unit/release/cleaner_test.rb +47 -0
  87. data/test/unit/resolver_test.rb +92 -0
  88. data/test/unit/template_test.rb +127 -0
  89. metadata +202 -8
@@ -0,0 +1,77 @@
1
+ require 'shellwords'
2
+
3
+ module Roger::Release::Finalizers
4
+
5
+ # Finalizes the release by uploading your mockup with rsync to a remote server
6
+ #
7
+ # @see RsyncFinalizer#initialize for options
8
+ #
9
+ class Rsync < Base
10
+
11
+ # @param Hash options The options
12
+ #
13
+ # @option options String :rsync The Rsync command to run (default is "rsync")
14
+ # @option options String :remote_path The remote path to upload to
15
+ # @option options String :host The remote host to upload to
16
+ # @option options String :username The remote username to upload to
17
+ # @option options Boolean :ask Prompt the user before uploading (default is true)
18
+ def initialize(options = {})
19
+ @options = {
20
+ :rsync => "rsync",
21
+ :remote_path => "",
22
+ :host => "",
23
+ :username => "",
24
+ :ask => true
25
+ }.update(options)
26
+ end
27
+
28
+ def call(release, options = {})
29
+ options = @options.dup.update(options)
30
+
31
+ # Validate options
32
+ validate_options!(release, options)
33
+
34
+ if !options[:ask] || (prompt("Do you wish to upload to #{options[:host]}? Type y[es]: ")) =~ /\Ay(es)?\Z/
35
+ begin
36
+ `#{@options[:rsync]} --version`
37
+ rescue Errno::ENOENT
38
+ raise RuntimeError, "Could not find rsync in #{@options[:rsync].inspect}"
39
+ end
40
+
41
+ local_path = release.build_path.to_s
42
+ remote_path = options[:remote_path]
43
+
44
+ local_path += "/" unless local_path =~ /\/\Z/
45
+ remote_path += "/" unless remote_path =~ /\/\Z/
46
+
47
+ release.log(self, "Starting upload of #{(release.build_path + "*")} to #{options[:host]}")
48
+
49
+ command = "#{options[:rsync]} -az #{Shellwords.escape(local_path)} #{Shellwords.escape(options[:username])}@#{Shellwords.escape(options[:host])}:#{Shellwords.escape(remote_path)}"
50
+
51
+ # Run r.js optimizer
52
+ output = `#{command}`
53
+
54
+ # Check if r.js succeeded
55
+ unless $?.success?
56
+ raise RuntimeError, "Rsync failed.\noutput:\n #{output}"
57
+ end
58
+ end
59
+ end
60
+
61
+ protected
62
+
63
+ def validate_options!(release, options)
64
+ must_have_keys = [:remote_path, :host, :username]
65
+ if (options.keys & must_have_keys).size != must_have_keys.size
66
+ release.log(self, "You must specify these options: #{(must_have_keys - options.keys).inspect}")
67
+ raise "Missing keys: #{(must_have_keys - options.keys).inspect}"
68
+ end
69
+ end
70
+
71
+ def prompt(question = 'Do you wish to continue?')
72
+ print(question)
73
+ return $stdin.gets.strip
74
+ end
75
+
76
+ end
77
+ end
@@ -0,0 +1,42 @@
1
+ module Roger::Release::Finalizers
2
+
3
+ class Zip < Base
4
+
5
+ attr_reader :release
6
+
7
+ # @option options :prefix Prefix to put before the version (default = "html")
8
+ # @option options :zip The zip command
9
+ def call(release, options = {})
10
+ if options
11
+ options = @options.dup.update(options)
12
+ else
13
+ options = @options
14
+ end
15
+
16
+ options = {
17
+ :zip => "zip",
18
+ :prefix => "html"
19
+ }.update(options)
20
+
21
+ name = [options[:prefix], release.scm.version].join("-") + ".zip"
22
+ release.log(self, "Finalizing release to #{release.target_path + name}")
23
+
24
+ if File.exist?(release.target_path + name)
25
+ release.log(self, "Removing existing target #{release.target_path + name}")
26
+ FileUtils.rm_rf(release.target_path + name)
27
+ end
28
+
29
+ begin
30
+ `#{options[:zip]} -v`
31
+ rescue Errno::ENOENT
32
+ raise RuntimeError, "Could not find zip in #{options[:zip].inspect}"
33
+ end
34
+
35
+ ::Dir.chdir(release.build_path) do
36
+ `#{options[:zip]} -r -9 "#{release.target_path + name}" ./*`
37
+ end
38
+ end
39
+
40
+
41
+ end
42
+ end
@@ -0,0 +1,19 @@
1
+ module Roger::Release::Finalizers
2
+ class Base
3
+
4
+ def initialize(options = {})
5
+ @options = {}
6
+ @options.update(options) if options
7
+ end
8
+
9
+ def call(release, options = {})
10
+ raise ArgumentError, "Implement in subclass"
11
+ end
12
+ end
13
+ end
14
+
15
+ require File.dirname(__FILE__) + "/finalizers/zip"
16
+ require File.dirname(__FILE__) + "/finalizers/dir"
17
+ require File.dirname(__FILE__) + "/finalizers/rsync"
18
+ require File.dirname(__FILE__) + "/finalizers/git_branch"
19
+
@@ -0,0 +1,99 @@
1
+ require 'tilt'
2
+ module Roger
3
+
4
+ # Inject VERSION / DATE (i.e. in TOC)
5
+ # r.inject({"VERSION" => release.version, "DATE" => release.date}, :into => %w{_doc/toc.html})
6
+
7
+ # Inject CHANGELOG
8
+ # r.inject({"CHANGELOG" => {:file => "", :filter => BlueCloth}}, :into => %w{_doc/changelog.html})
9
+
10
+ class Release::Injector
11
+
12
+ # @example Simple variable injection (replaces [VARIABLE] into all .css files)
13
+ # {"[VARIABLE]" => "replacement"}, :into => %w{**/*.css}
14
+ #
15
+ # @example Regex variable injection (replaces all matches into test.js files)
16
+ # {/\/\*\s*\[BANNER\]\s*\*\// => "replacement"}, :into => %w{javacripts/test.js}
17
+ #
18
+ # @example Simple variable injection with filtering (replaces [VARIABLE] with :content run through the markdown processor into all .html files)
19
+ # {"[VARIABLE]" => {:content => "# header one", :processor => "md"}, :into => %w{**/*.html}
20
+ #
21
+ # @example Full file injection (replaces all matches of [CHANGELOG] with the contents of "CHANGELOG.md" into _doc/changelog.html)
22
+ #
23
+ # {"CHANGELOG" => {:file => "CHANGELOG.md"}}, :into => %w{_doc/changelog.html}
24
+ #
25
+ # @example Full file injection with filtering (replaces all matches of [CHANGELOG] with the contents of "CHANGELOG" which ran through Markdown compresser into _doc/changelog.html)
26
+ #
27
+ # {"CHANGELOG" => {:file => "CHANGELOG", :processor => "md"}}, :into => %w{_doc/changelog.html}
28
+ #
29
+ # Processors are based on Tilt (https://github.com/rtomayko/tilt).
30
+ # Currently supported/tested processors are:
31
+ #
32
+ # * 'md' for Markdown (bluecloth)
33
+ #
34
+ # Injection files are relative to the :source_path
35
+ #
36
+ # @param [Hash] variables Variables to inject. See example for more info
37
+ # @option options [Array] :into An array of file globs relative to the build_path
38
+ def initialize(variables, options)
39
+ @variables = variables
40
+ @options = options
41
+ end
42
+
43
+ def call(release, options = {})
44
+ @options.update(options)
45
+ files = release.get_files(@options[:into])
46
+
47
+ files.each do |f|
48
+ c = File.read(f)
49
+ injected_vars = []
50
+ @variables.each do |variable, injection|
51
+ if c.gsub!(variable, get_content(injection, release))
52
+ injected_vars << variable
53
+ end
54
+ end
55
+ release.log(self, "Injected variables #{injected_vars.inspect} into #{f}") if injected_vars.size > 0
56
+ File.open(f,"w") { |fh| fh.write c }
57
+ end
58
+
59
+ end
60
+
61
+ def get_content(injection, release)
62
+ case injection
63
+ when String
64
+ injection
65
+ when Hash
66
+ get_complex_injection(injection, release)
67
+ else
68
+ if injection.respond_to?(:to_s)
69
+ injection.to_s
70
+ else
71
+ raise ArgumentError, "Woah, what's this? #{injection.inspect}"
72
+ end
73
+ end
74
+ end
75
+
76
+ def get_complex_injection(injection, release)
77
+
78
+ if injection[:file]
79
+ content = File.read(release.source_path + injection[:file])
80
+ else
81
+ content = injection[:content]
82
+ end
83
+
84
+ raise ArgumentError, "No :content or :file specified" if !content
85
+
86
+ if injection[:processor]
87
+ if tmpl = Tilt[injection[:processor]]
88
+ (tmpl.new{ content }).render
89
+ else
90
+ raise ArgumentError, "Unknown processor #{injection[:processor]}"
91
+ end
92
+ else
93
+ content
94
+ end
95
+
96
+ end
97
+
98
+ end
99
+ end
@@ -0,0 +1,93 @@
1
+ module Roger::Release::Processors
2
+ class Mockup < Base
3
+
4
+ attr_accessor :project
5
+
6
+ def initialize(options={})
7
+ @options = {
8
+ :env => {},
9
+ :match => ["**/*.{html,md,html.erb}"],
10
+ :skip => [/\Astylesheets/, /\Ajavascripts/]
11
+ }
12
+
13
+ @options.update(options) if options
14
+ end
15
+
16
+ def call(release, options={})
17
+ self.project = release.project
18
+
19
+ options = {}.update(@options).update(options)
20
+
21
+ options[:env].update("MOCKUP_PROJECT" => project)
22
+
23
+ release.log(self, "Processing mockup files")
24
+
25
+ release.log(self, " Matching: #{options[:match].inspect}", true)
26
+ release.log(self, " Skiping : #{options[:skip].inspect}", true)
27
+ release.log(self, " Env : #{options[:env].inspect}", true)
28
+ release.log(self, " Files :", true)
29
+
30
+ release.get_files(options[:match], options[:skip]).each do |file_path|
31
+ release.log(self, " Extract: #{file_path}", true)
32
+ self.run_on_file!(file_path, options[:env])
33
+ end
34
+ end
35
+
36
+
37
+ def run_on_file!(file_path, env = {})
38
+ template = Roger::Template.open(file_path, :partials_path => self.project.partial_path, :layouts_path => self.project.layouts_path)
39
+
40
+ # Clean up source file
41
+ FileUtils.rm(file_path)
42
+
43
+ # Write out new file
44
+ File.open(self.target_path(file_path, template),"w"){|f| f.write(template.render(env.dup)) }
45
+ end
46
+
47
+ # Runs the extractor on a single file and return processed source.
48
+ def extract_source_from_file(file_path, env = {})
49
+ Roger::Template.open(file_path, :partials_path => self.project.partial_path, :layouts_path => self.project.layouts_path).render(env.dup)
50
+ end
51
+
52
+ protected
53
+
54
+ def target_path(path, template)
55
+ # 1. If we have a double extension we rip of the template it's own extension and be done with it
56
+ parts = File.basename(path.to_s).split(".")
57
+ dir = Pathname.new(File.dirname(path.to_s))
58
+
59
+ # 2. Try to figure out the extension based on the template's mime-type
60
+ mime_types = {
61
+ "text/html" => "html",
62
+ "text/css" => "css",
63
+ "application/javascript" => "js",
64
+ "text/xml" => "xml",
65
+ "application/xml" => "xml",
66
+ "text/csv" => "csv",
67
+ "application/json" => "json"
68
+ }
69
+ extension = mime_types[template.template.class.default_mime_type]
70
+
71
+ # Always return .html directly as it will cause too much trouble otherwise
72
+ if parts.last == "html"
73
+ return path
74
+ end
75
+
76
+ if parts.size > 2
77
+ # Strip extension
78
+ dir + parts[0..-2].join(".")
79
+ else
80
+ return path if extension.nil?
81
+
82
+ if parts.size > 1
83
+ # Strip extension and replace with extension
84
+ dir + (parts[0..-2] << extension).join(".")
85
+ else
86
+ # Let's just add the extension
87
+ dir + (parts << extension).join(".")
88
+ end
89
+ end
90
+ end
91
+
92
+ end
93
+ end
@@ -0,0 +1,45 @@
1
+ require File.dirname(__FILE__) + '../../../resolver'
2
+
3
+ module Roger::Release::Processors
4
+ class UrlRelativizer < Base
5
+
6
+ def initialize(options={})
7
+ @options = {
8
+ :url_attributes => %w{src href action},
9
+ :match => ["**/*.html"],
10
+ :skip => []
11
+ }
12
+
13
+ @options.update(options) if options
14
+ end
15
+
16
+ def call(release, options={})
17
+ options = {}.update(@options).update(options)
18
+
19
+ release.log(self, "Relativizing all URLS in #{options[:match].inspect} files in attributes #{options[:url_attributes].inspect}, skipping #{options[:skip].any? ? options[:skip].inspect : "none" }")
20
+
21
+ @resolver = Roger::Resolver.new(release.build_path)
22
+ release.get_files(options[:match], options[:skip]).each do |file_path|
23
+ release.debug(self, "Relativizing URLS in #{file_path}") do
24
+ orig_source = File.read(file_path)
25
+ File.open(file_path,"w") do |f|
26
+ doc = Hpricot(orig_source)
27
+ options[:url_attributes].each do |attribute|
28
+ (doc/"*[@#{attribute}]").each do |tag|
29
+ converted_url = @resolver.url_to_relative_url(tag[attribute], file_path)
30
+ release.debug(self, "Converting '#{tag[attribute]}' to '#{converted_url}'")
31
+ case converted_url
32
+ when String
33
+ tag[attribute] = converted_url
34
+ when nil
35
+ release.log(self, "Could not resolve link #{tag[attribute]} in #{file_path}")
36
+ end
37
+ end
38
+ end
39
+ f.write(doc.to_original_html)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,17 @@
1
+ module Roger::Release::Processors
2
+ class Base
3
+
4
+ def initialize(options = {})
5
+ @options = {}
6
+ @options.update(options) if options
7
+ end
8
+
9
+
10
+ def call(release, options = {})
11
+ raise ArgumentError, "Implement in subclass"
12
+ end
13
+ end
14
+ end
15
+
16
+ require File.dirname(__FILE__) + "/processors/mockup"
17
+ require File.dirname(__FILE__) + "/processors/url_relativizer"
@@ -0,0 +1,101 @@
1
+ require 'pathname'
2
+
3
+ module Roger::Release::Scm
4
+ class Git < Base
5
+
6
+ # @option config [String] :ref Ref to use for current tag
7
+ # @option config [String, Pathname] :path Path to working dir
8
+ def initialize(config={})
9
+ super(config)
10
+ @config[:ref] ||= "HEAD"
11
+ end
12
+
13
+ # Version is either:
14
+ # - the tagged version number (first "v" will be stripped) or
15
+ # - the return value of "git describe --tags HEAD"
16
+ # - the short SHA1 if there hasn't been a previous tag
17
+ def version
18
+ get_scm_data if @_version.nil?
19
+ @_version
20
+ end
21
+
22
+ # Date will be Time.now if it can't be determined from GIT repository
23
+ def date
24
+ get_scm_data if @_date.nil?
25
+ @_date
26
+ end
27
+
28
+ def previous
29
+ self.class.new(@config.dup.update(:ref => get_previous_tag_name))
30
+ end
31
+
32
+ protected
33
+
34
+ def get_previous_tag_name
35
+ # Get list of SHA1 that have a ref
36
+ begin
37
+ sha1s = `git --git-dir=#{git_dir} log --pretty='%H' --simplify-by-decoration`.split("\n")
38
+ tags = []
39
+ while tags.size < 2 && sha1s.any?
40
+ sha1 = sha1s.shift
41
+ tag = `git --git-dir=#{git_dir} describe --tags --exact-match #{sha1} 2>/dev/null`.strip
42
+ tags << tag if !tag.empty?
43
+ end
44
+ tags.last
45
+ rescue
46
+ raise "Could not get previous tag"
47
+ end
48
+ end
49
+
50
+ def git_dir
51
+ @git_dir ||= find_git_dir(@config[:path])
52
+ end
53
+
54
+ # Some hackery to determine if we're on a tagged version or not
55
+ def get_scm_data(ref = @config[:ref])
56
+ @_version = ""
57
+ @_date = Time.now
58
+ begin
59
+ if File.exist?(git_dir)
60
+ @_version = `git --git-dir=#{git_dir} describe --tags #{ref} 2>&1`
61
+
62
+ if $?.to_i > 0
63
+ # HEAD is not a tagged verison, get the short SHA1 instead
64
+ @_version = `git --git-dir=#{git_dir} show #{ref} --format=format:"%h" -s 2>&1`
65
+ else
66
+ # HEAD is a tagged version, if version is prefixed with "v" it will be stripped off
67
+ @_version.gsub!(/^v/,"")
68
+ end
69
+ @_version.strip!
70
+
71
+ # Get the date in epoch time
72
+ date = `git --git-dir=#{git_dir} show #{ref} --format=format:"%ct" -s 2>&1`
73
+ if date =~ /\d+/
74
+ @_date = Time.at(date.to_i)
75
+ else
76
+ @_date = Time.now
77
+ end
78
+
79
+ end
80
+ rescue RuntimeError => e
81
+ end
82
+
83
+ end
84
+
85
+ # Find the git dir
86
+ def find_git_dir(path)
87
+ path = Pathname.new(path).realpath
88
+ while path.parent != path && !(path + ".git").directory?
89
+ path = path.parent
90
+ end
91
+
92
+ path = path + ".git"
93
+
94
+ raise "Could not find suitable .git dir in #{path}" if !path.directory?
95
+
96
+ path
97
+ end
98
+
99
+ end
100
+ end
101
+
@@ -0,0 +1,32 @@
1
+ module Roger::Release::Scm
2
+ class Base
3
+
4
+ attr_reader :config
5
+
6
+ def initialize(config={})
7
+ @config = config
8
+ end
9
+
10
+ # Returns the release version string from the SCM
11
+ #
12
+ # @return String The current version string
13
+ def version
14
+ raise "Implement in subclass"
15
+ end
16
+
17
+ # Returns the release version date from the SCM
18
+ def date
19
+ raise "Implement in subclass"
20
+ end
21
+
22
+ # Returns a Release::Scm object with the previous version's data
23
+ #
24
+ # @return Roger::Release::Scm The previous version
25
+ def previous
26
+ raise "Implement in subclass"
27
+ end
28
+
29
+ end
30
+ end
31
+
32
+ require File.dirname(__FILE__) + "/scm/git"