html_mockup 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,53 @@
1
+ # Use the ruby-yui-compressor gem.
2
+ require 'yui/compressor'
3
+
4
+ module HtmlMockup::Release::Processors
5
+ class Yuicompressor < Base
6
+
7
+ # Compresses all JS and CSS files, it will keep all lines before
8
+ #
9
+ # /* -------------------------------------------------------------------------------- */
10
+ #
11
+ # (80 dashes)
12
+ #
13
+ # @options options [Array] match Files to match, default to ["**/*.{css,js}"]
14
+ # @options options [Regexp] :delimiter An array of header delimiters. Defaults to the one above. The delimiter will be removed from the output.
15
+ # @options options [Array[Regexp]] :skip An array of file regular expressions to specifiy which files to skip. Defaults to [/javascripts\/vendor\/.\*.js\Z/, /_doc\/.*/]
16
+ def call(release, options={})
17
+ options = {
18
+ :match => ["**/*.{css,js}"],
19
+ :skip => [/javascripts\/vendor\/.*\.js\Z/, /_doc\/.*/],
20
+ :delimiter => Regexp.escape("/* -------------------------------------------------------------------------------- */")
21
+ }.update(options)
22
+
23
+ compressor_options = {:line_break => 80}
24
+ css_compressor = YUI::CssCompressor.new(compressor_options)
25
+ js_compressor = YUI::JavaScriptCompressor.new(compressor_options)
26
+
27
+ # Add version numbers and minify the files
28
+ release.get_files(options[:match], options[:skip]).each do |f|
29
+ type = f[/\.(.+)$/,1]
30
+
31
+ data = File.read(f);
32
+ File.open(f,"w") do |fh|
33
+
34
+ # Extract header and store for later use
35
+ header = data[/\A(.+?)\n#{options[:delimiter]}\s*\n/m,1]
36
+ minified = [header]
37
+
38
+ # Actual minification
39
+ release.log self, "Minifying #{f}"
40
+ case type
41
+ when "css"
42
+ minified << css_compressor.compress(data)
43
+ when "js"
44
+ minified << js_compressor.compress(data)
45
+ end
46
+
47
+ fh.write minified.join("\n")
48
+ end
49
+ end
50
+
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,11 @@
1
+ module HtmlMockup::Release::Processors
2
+ class Base
3
+ def call(release, options = {})
4
+ raise ArgumentError, "Implement in subclass"
5
+ end
6
+ end
7
+ end
8
+
9
+ require File.dirname(__FILE__) + "/finalizers/zip"
10
+ require File.dirname(__FILE__) + "/finalizers/dir"
11
+
@@ -0,0 +1,101 @@
1
+ require 'pathname'
2
+
3
+ module HtmlMockup::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" --quiet 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" --quiet 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 HtmlMockup::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 HtmlMockup::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"
@@ -0,0 +1,312 @@
1
+ require File.dirname(__FILE__) + "/cli"
2
+
3
+ module HtmlMockup
4
+ class Release
5
+
6
+ attr_reader :config, :project
7
+
8
+ attr_reader :finalizers, :injections, :stack, :cleanups
9
+
10
+ def self.default_stack
11
+ []
12
+ end
13
+
14
+ def self.default_finalizers
15
+ [[:dir, {}]]
16
+ end
17
+
18
+ # @option config [Symbol] :scm The SCM to use (default = :git)
19
+ # @option config [String, Pathname] :target_path The path/directory to put the release into
20
+ # @option config [String, Pathname]:build_path Temporary path used to build the release
21
+ # @option config [Boolean] :cleanup_build Wether or not to remove the build_path after we're done (default = true)
22
+ def initialize(project, config = {})
23
+ defaults = {
24
+ :scm => :git,
25
+ :source_path => Pathname.new(Dir.pwd) + "html",
26
+ :target_path => Pathname.new(Dir.pwd) + "releases",
27
+ :build_path => Pathname.new(Dir.pwd) + "build",
28
+ :cleanup_build => true
29
+ }
30
+
31
+ @config = {}.update(defaults).update(config)
32
+ @project = project
33
+ @finalizers = []
34
+ @injections = []
35
+ @stack = []
36
+ @cleanups = []
37
+ end
38
+
39
+ # Accessor for target_path
40
+ # The target_path is the path where the finalizers will put the release
41
+ #
42
+ # @return Pathname the target_path
43
+ def target_path
44
+ Pathname.new(self.config[:target_path])
45
+ end
46
+
47
+ # Accessor for build_path
48
+ # The build_path is a temporary directory where the release will be built
49
+ #
50
+ # @return Pathname the build_path
51
+ def build_path
52
+ Pathname.new(self.config[:build_path])
53
+ end
54
+
55
+ # Accessor for source_path
56
+ # The source path is the root of the mockup
57
+ #
58
+ # @return Pathanem the source_path
59
+ def source_path
60
+ Pathname.new(self.config[:source_path])
61
+ end
62
+
63
+ # Get the current SCM object
64
+ def scm(force = false)
65
+ return @_scm if @_scm && !force
66
+
67
+ case self.config[:scm]
68
+ when :git
69
+ @_scm = Release::Scm::Git.new(:path => self.source_path)
70
+ else
71
+ raise "Unknown SCM #{options[:scm].inspect}"
72
+ end
73
+ end
74
+
75
+ # Inject variables into files with an optional filter
76
+ #
77
+ # @examples
78
+ # release.inject({"VERSION" => release.version, "DATE" => release.date}, :into => %w{_doc/toc.html})
79
+ # release.inject({"CHANGELOG" => {:file => "", :filter => BlueCloth}}, :into => %w{_doc/changelog.html})
80
+ def inject(variables, options)
81
+ @injections << [variables, options]
82
+ end
83
+
84
+ # Use a certain pre-processor
85
+ #
86
+ # @examples
87
+ # release.use :sprockets, sprockets_config
88
+ def use(processor, options = {})
89
+ @stack << [processor, options]
90
+ end
91
+
92
+ # Write out the whole release into a directory, zip file or anything you can imagine
93
+ # #finalize can be called multiple times, it just will run all of them.
94
+ #
95
+ # The default finalizer is :dir
96
+ #
97
+ # @param [Symbol, Proc] Finalizer to use
98
+ #
99
+ # @examples
100
+ # release.finalize :zip
101
+ def finalize(finalizer, options = {})
102
+ @finalizers << [finalizer, options]
103
+ end
104
+
105
+ # Files to clean up in the build directory just before finalization happens
106
+ #
107
+ # @param [String] Pattern to glob within build directory
108
+ #
109
+ # @examples
110
+ # release.cleanup "**/.DS_Store"
111
+ def cleanup(pattern)
112
+ @cleanups << pattern
113
+ end
114
+
115
+ # Generates a banner if a block is given, or returns the currently set banner.
116
+ # It automatically takes care of adding comment marks around the banner.
117
+ #
118
+ # The default banner looks like this:
119
+ #
120
+ # =======================
121
+ # = Version : v1.0.0 =
122
+ # = Date : 2012-06-20 =
123
+ # =======================
124
+ #
125
+ #
126
+ # @option options [:css,:js,:html,false] :comment Wether or not to comment the output and in what style. (default=js)
127
+ def banner(options = {}, &block)
128
+ options = {
129
+ :comment => :js
130
+ }.update(options)
131
+
132
+ if block_given?
133
+ @_banner = yield.to_s
134
+ elsif !@_banner
135
+ banner = []
136
+ banner << "Version : #{self.scm.version}"
137
+ banner << "Date : #{self.scm.date.strftime("%Y-%m-%d")}"
138
+
139
+ size = banner.inject(0){|mem,b| b.size > mem ? b.size : mem }
140
+ banner.map!{|b| "= #{b.ljust(size)} =" }
141
+ div = "=" * banner.first.size
142
+ banner.unshift(div)
143
+ banner << div
144
+ @_banner = banner.join("\n")
145
+ end
146
+
147
+ if options[:comment]
148
+ self.comment(@_banner, :style => options[:comment])
149
+ else
150
+ @_banner
151
+ end
152
+ end
153
+
154
+ # Actually perform the release
155
+ def run!
156
+ # Validate paths
157
+ validate_paths!
158
+
159
+ # Extract valid mockup
160
+ extract!
161
+
162
+ # Run stack
163
+ run_stack!
164
+
165
+ # Run injections
166
+ run_injections!
167
+
168
+ # Run cleanups
169
+ run_cleanups!
170
+
171
+ # Run finalizers
172
+ run_finalizers!
173
+
174
+ # Cleanup
175
+ cleanup! if self.config[:cleanup_build]
176
+
177
+ end
178
+
179
+ def log(part, msg)
180
+ puts part.class.to_s + " : " + msg.to_s
181
+ end
182
+
183
+ # @param [Array] globs an array of file path globs that will be globbed against the build_path
184
+ # @param [Array] excludes an array of regexps that will be excluded from the result
185
+ def get_files(globs, excludes = [])
186
+ files = globs.map{|g| Dir.glob(self.build_path + g) }.flatten
187
+ if excludes.any?
188
+ files.reject{|c| excludes.detect{|e| e.match(c) } }
189
+ else
190
+ files
191
+ end
192
+ end
193
+
194
+ protected
195
+
196
+ # ==============
197
+ # = The runway =
198
+ # ==============
199
+
200
+ def validate_paths!
201
+ # Make sure the build_path is empty
202
+ raise ArgumentError, "Build path \"#{self.build_path}\" is not empty" if self.build_path.exist?
203
+
204
+ # Make sure the target_path exists
205
+ raise ArgumentError, "Target path \"#{self.target_path}\" does not exist" if !self.target_path.exist?
206
+ end
207
+
208
+ def extract!
209
+ extractor = Extractor.new(self.project, self.build_path)
210
+ extractor.run!
211
+ end
212
+
213
+ def run_stack!
214
+ @stack = self.class.default_stack.dup if @stack.empty?
215
+ @stack.each do |processor, options|
216
+ get_callable(processor, HtmlMockup::Release::Processors).call(self, options)
217
+ end
218
+ end
219
+
220
+ def run_injections!
221
+ @injections.each do |injection|
222
+ Injector.new(injection[0], injection[1]).call(self)
223
+ end
224
+ end
225
+
226
+ def run_finalizers!
227
+ @finalizers = self.class.default_finalizers.dup if @finalizers.empty?
228
+ @finalizers.each do |finalizer, options|
229
+ get_callable(finalizer, HtmlMockup::Release::Finalizers).call(self, options)
230
+ end
231
+ end
232
+
233
+ def run_cleanups!
234
+ # We switch to the build path and append the globbed files for safety, so even if you manage to sneak in a
235
+ # pattern like "/**/*" it won't do you any good as it will be reappended to the path
236
+ Dir.chdir(self.build_path.to_s) do
237
+ @cleanups.each do |pattern|
238
+ Dir.glob(pattern).each do |file|
239
+ path = File.join(self.build_path.to_s, file)
240
+ log(self, "Cleaning up \"#{path}\" in build")
241
+ rm(path)
242
+ end
243
+ end
244
+ end
245
+ end
246
+
247
+ def cleanup!
248
+ log(self, "Cleaning up build path #{self.build_path}")
249
+ rm_rf(self.build_path)
250
+ end
251
+
252
+ # Makes callable into a object that responds to call.
253
+ #
254
+ # @param [#call, Symbol, Class] callable If callable already responds to #call will just return callable, a Symbol will be searched for in the scope parameter, a class will be instantiated (and checked if it will respond to #call)
255
+ # @param [Module] scope The scope in which to search callable in if it's a Symbol
256
+ def get_callable(callable, scope)
257
+ return callable if callable.respond_to?(:call)
258
+
259
+ if callable.kind_of?(Symbol)
260
+ # TODO camelcase callable
261
+ callable = callable.to_s.capitalize.to_sym
262
+ if scope.constants.include?(callable)
263
+ c = scope.const_get(callable)
264
+ callable = c if c.is_a?(Class)
265
+ end
266
+ end
267
+
268
+ if callable.kind_of?(Class)
269
+ callable = callable.new
270
+ end
271
+
272
+ if callable.respond_to?(:call)
273
+ callable
274
+ else
275
+ raise ArgumentError, "Could not resolve #{callable.inspect}. Callable must be an object that responds to #call or a symbol that resolve to such an object or a class with a #call instance method."
276
+ end
277
+
278
+ end
279
+
280
+ # @param [String] string The string to comment
281
+ #
282
+ # @option options [:html, :css, :js] :style The comment style to use (default=:js, which is the same as :css)
283
+ # @option options [Boolean] :per_line Comment per line or make one block? (default=true)
284
+ def comment(string, options = {})
285
+ options = {
286
+ :style => :css,
287
+ :per_line => true
288
+ }.update(options)
289
+
290
+ commenters = {
291
+ :html => Proc.new{|s| "<!-- #{s} -->" },
292
+ :css => Proc.new{|s| "/* #{s} */" },
293
+ :js => Proc.new{|s| "/* #{s} */" }
294
+ }
295
+
296
+ commenter = commenters[options[:style]] || commenters[:js]
297
+
298
+ if options[:per_line]
299
+ string = string.split(/\r?\n/)
300
+ string.map{|s| commenter.call(s) }.join("\n")
301
+ else
302
+ commenter.call(s)
303
+ end
304
+ end
305
+ end
306
+ end
307
+
308
+ require File.dirname(__FILE__) + "/extractor"
309
+ require File.dirname(__FILE__) + "/release/scm"
310
+ require File.dirname(__FILE__) + "/release/injector"
311
+ require File.dirname(__FILE__) + "/release/finalizers"
312
+ require File.dirname(__FILE__) + "/release/processors"
@@ -6,17 +6,28 @@ require File.dirname(__FILE__) + "/rack/html_validator"
6
6
 
7
7
  module HtmlMockup
8
8
  class Server
9
- attr_accessor :options,:server_options, :root, :partial_path
10
9
 
11
- def initialize(root,partial_path,options={},server_options={})
10
+ attr_reader :options
11
+
12
+ attr_accessor :html_path, :partial_path
13
+
14
+ attr_accessor :port, :handler
15
+
16
+ def initialize(html_path, partial_path, options={})
12
17
  @stack = ::Rack::Builder.new
13
18
 
14
19
  @middleware = []
15
- @root = root
20
+ @html_path = html_path
16
21
  @partial_path = partial_path
17
- @options,@server_options = options,server_options
22
+ @options = {
23
+ :handler => nil, # Autodetect
24
+ :port => 9000
25
+ }.update(options)
26
+
27
+ @port = @options[:port]
28
+ @handler = @options[:handler]
18
29
  end
19
-
30
+
20
31
  # Use the specified Rack middleware
21
32
  def use(middleware, *args, &block)
22
33
  @middleware << [middleware, args, block]
@@ -25,7 +36,7 @@ module HtmlMockup
25
36
  def handler
26
37
  if self.options[:handler]
27
38
  begin
28
- @handler = ::Rack::Handler.get(self.options[:handler])
39
+ @handler = ::Rack::Handler.get(self.handler)
29
40
  rescue LoadError
30
41
  rescue NameError
31
42
  end
@@ -35,9 +46,9 @@ module HtmlMockup
35
46
  end
36
47
  @handler ||= detect_rack_handler
37
48
  end
38
-
39
- def run
40
- self.handler.run self.application, @server_options do |server|
49
+
50
+ def run!
51
+ self.handler.run self.application, self.server_options do |server|
41
52
  trap(:INT) do
42
53
  ## Use thins' hard #stop! if available, otherwise just #stop
43
54
  server.respond_to?(:stop!) ? server.stop! : server.stop
@@ -45,6 +56,7 @@ module HtmlMockup
45
56
  end
46
57
  end
47
58
  end
59
+ alias :run :run!
48
60
 
49
61
  def application
50
62
  return @app if @app
@@ -56,7 +68,7 @@ module HtmlMockup
56
68
  @middleware.each { |c,a,b| @stack.use(c, *a, &b) }
57
69
 
58
70
  @stack.use Rack::HtmlValidator if self.options["validate"]
59
- @stack.run Rack::HtmlMockup.new(self.root, self.partial_path)
71
+ @stack.run Rack::HtmlMockup.new(self.html_path, self.partial_path)
60
72
 
61
73
  @app = @stack.to_app
62
74
  end
@@ -64,6 +76,14 @@ module HtmlMockup
64
76
 
65
77
  protected
66
78
 
79
+ # Generate server options for handler
80
+ def server_options
81
+ {
82
+ :Port => self.port
83
+ }
84
+ end
85
+
86
+
67
87
  # Sinatra's detect_rack_handler
68
88
  def detect_rack_handler
69
89
  servers = %w[mongrel thin webrick]
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: html_mockup
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,22 +9,27 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-04-27 00:00:00.000000000Z
12
+ date: 2012-10-08 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: thor
16
- requirement: &70245084497540 !ruby/object:Gem::Requirement
16
+ requirement: !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ~>
20
20
  - !ruby/object:Gem::Version
21
- version: 0.12.0
21
+ version: 0.16.0
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70245084497540
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 0.16.0
25
30
  - !ruby/object:Gem::Dependency
26
31
  name: rack
27
- requirement: &70245084496900 !ruby/object:Gem::Requirement
32
+ requirement: !ruby/object:Gem::Requirement
28
33
  none: false
29
34
  requirements:
30
35
  - - ! '>='
@@ -32,7 +37,12 @@ dependencies:
32
37
  version: 1.0.0
33
38
  type: :runtime
34
39
  prerelease: false
35
- version_requirements: *70245084496900
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: 1.0.0
36
46
  description:
37
47
  email: flurin@digitpaint.nl
38
48
  executables:
@@ -48,8 +58,23 @@ files:
48
58
  - examples/partials/test.part.rhtml
49
59
  - examples/script/server
50
60
  - lib/html_mockup/cli.rb
61
+ - lib/html_mockup/extractor.rb
62
+ - lib/html_mockup/mockupfile.rb
63
+ - lib/html_mockup/project.rb
51
64
  - lib/html_mockup/rack/html_mockup.rb
52
65
  - lib/html_mockup/rack/html_validator.rb
66
+ - lib/html_mockup/rack/sleep.rb
67
+ - lib/html_mockup/release.rb
68
+ - lib/html_mockup/release/finalizers.rb
69
+ - lib/html_mockup/release/finalizers/dir.rb
70
+ - lib/html_mockup/release/finalizers/zip.rb
71
+ - lib/html_mockup/release/injector.rb
72
+ - lib/html_mockup/release/processors.rb
73
+ - lib/html_mockup/release/processors/requirejs.rb
74
+ - lib/html_mockup/release/processors/sass.rb
75
+ - lib/html_mockup/release/processors/yuicompressor.rb
76
+ - lib/html_mockup/release/scm.rb
77
+ - lib/html_mockup/release/scm/git.rb
53
78
  - lib/html_mockup/server.rb
54
79
  - lib/html_mockup/template.rb
55
80
  - lib/html_mockup/w3c_validator.rb
@@ -66,6 +91,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
66
91
  - - ! '>='
67
92
  - !ruby/object:Gem::Version
68
93
  version: '0'
94
+ segments:
95
+ - 0
96
+ hash: -1984595046850543499
69
97
  required_rubygems_version: !ruby/object:Gem::Requirement
70
98
  none: false
71
99
  requirements:
@@ -74,7 +102,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
74
102
  version: '0'
75
103
  requirements: []
76
104
  rubyforge_project:
77
- rubygems_version: 1.8.6
105
+ rubygems_version: 1.8.24
78
106
  signing_key:
79
107
  specification_version: 3
80
108
  summary: HTML Mockup is a set of tools to create self-containing HTML mockups.