roger 0.0.1 → 0.10.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 (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"