html_minifier 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/.gitmodules +3 -0
- data/.travis.yml +19 -0
- data/Gemfile +16 -0
- data/README.md +43 -0
- data/Rakefile +61 -0
- data/html_minifier.gemspec +25 -0
- data/lib/html_minifier/linter.rb +0 -0
- data/lib/html_minifier/minifier.rb +52 -0
- data/lib/html_minifier/task.rb +0 -0
- data/lib/html_minifier/version.rb +4 -0
- data/lib/html_minifier.rb +6 -0
- data/lib/js/console.js +16 -0
- data/lib/js/exports.js +16 -0
- data/lib/js/htmllint.js +150 -0
- data/lib/js/htmlminifier.js +406 -0
- data/lib/js/htmlparser.js +327 -0
- data/spec/html_minifier_spec.rb +29 -0
- data/spec/qunit_helper.js +44 -0
- metadata +125 -0
data/.gitignore
ADDED
data/.gitmodules
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
rvm:
|
2
|
+
- 1.8.7
|
3
|
+
- 1.9.2
|
4
|
+
- 1.9.3
|
5
|
+
- jruby
|
6
|
+
before_script: "git submodule update --init --recursive"
|
7
|
+
env:
|
8
|
+
- EXECJS_RUNTIME=RubyRacer
|
9
|
+
- EXECJS_RUNTIME=RubyRhino
|
10
|
+
matrix:
|
11
|
+
exclude:
|
12
|
+
- rvm: 1.8.7
|
13
|
+
env: EXECJS_RUNTIME=RubyRhino
|
14
|
+
- rvm: 1.9.2
|
15
|
+
env: EXECJS_RUNTIME=RubyRhino
|
16
|
+
- rvm: 1.9.3
|
17
|
+
env: EXECJS_RUNTIME=RubyRhino
|
18
|
+
- rvm: jruby
|
19
|
+
env: EXECJS_RUNTIME=RubyRacer
|
data/Gemfile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in gemspec
|
4
|
+
gemspec
|
5
|
+
|
6
|
+
# Depend on defined ExecJS runtime
|
7
|
+
execjs_runtimes = {
|
8
|
+
"RubyRacer" => "therubyracer",
|
9
|
+
"RubyRhino" => "therubyrhino",
|
10
|
+
"Mustang" => "mustang"
|
11
|
+
}
|
12
|
+
|
13
|
+
if ENV["EXECJS_RUNTIME"] && execjs_runtimes[ENV["EXECJS_RUNTIME"]]
|
14
|
+
gem execjs_runtimes[ENV["EXECJS_RUNTIME"]], :group => :development
|
15
|
+
end
|
16
|
+
|
data/README.md
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
# HtmlMinifier
|
2
|
+
[![Build Status](https://secure.travis-ci.org/stereobooster/html_minifier.png?branch=master)](http://travis-ci.org/stereobooster/html_minifier)
|
3
|
+
|
4
|
+
Ruby wrapper for [html-minifier](https://github.com/kangax/html-minifier/).
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
`html_minifier` is available as ruby gem.
|
9
|
+
|
10
|
+
$ gem install html_minifier
|
11
|
+
|
12
|
+
Ensure that your environment has a JavaScript interpreter supported by [ExecJS](https://github.com/sstephenson/execjs). Usually, installing therubyracer gem is the best alternative.
|
13
|
+
|
14
|
+
## Usage
|
15
|
+
|
16
|
+
```ruby
|
17
|
+
require 'html_minifier'
|
18
|
+
|
19
|
+
HtmlMinifier.minify(File.read("source.html"))
|
20
|
+
```
|
21
|
+
|
22
|
+
Or you can use it with rake
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
require "html_minifier/task"
|
26
|
+
HtmlMinifier::Task.new :html do |t|
|
27
|
+
t.pattern = 'static/**/*.html'
|
28
|
+
end
|
29
|
+
```
|
30
|
+
|
31
|
+
When initializing `HtmlMinifier`, you can pass options
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
HtmlMinifier::minifier.new( <options> ).minify(source)
|
35
|
+
# Or
|
36
|
+
HtmlMinifier.minify(source, <options>)
|
37
|
+
```
|
38
|
+
|
39
|
+
## TODO
|
40
|
+
|
41
|
+
- add more tests
|
42
|
+
- add color reporter. Maybe [colorize](https://github.com/fazibear/colorize)
|
43
|
+
- add cli
|
data/Rakefile
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
|
3
|
+
[:build, :install, :release].each do |task_name|
|
4
|
+
Rake::Task[task_name].prerequisites << :spec
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'submodule'
|
8
|
+
Submodule::Task.new do |t|
|
9
|
+
t.branch = "gh-pages"
|
10
|
+
|
11
|
+
t.test do
|
12
|
+
js = []
|
13
|
+
js << File.open(File.expand_path("../lib/js/console.js", __FILE__), "r:UTF-8").read
|
14
|
+
%w{htmlparser htmllint htmlminifier}.each do |i|
|
15
|
+
js << File.open("src/#{i}.js", "r:UTF-8").read
|
16
|
+
end
|
17
|
+
js << File.open("tests/qunit.js", "r:UTF-8").read
|
18
|
+
js << File.open(File.expand_path("../spec/qunit_helper.js", __FILE__), "r:UTF-8").read
|
19
|
+
%w{minify_test lint_test}.each do |i|
|
20
|
+
js << File.open("tests/#{i}.js", "r:UTF-8").read
|
21
|
+
end
|
22
|
+
|
23
|
+
require "execjs"
|
24
|
+
context = ExecJS.compile js.join("\n")
|
25
|
+
result = context.exec "return QUnit.result();"
|
26
|
+
if result["fail"] > 0 && result["assertions"].respond_to?(:each)
|
27
|
+
puts "Failures:"
|
28
|
+
i = 1
|
29
|
+
result["assertions"].each do |test, details|
|
30
|
+
puts " #{i}) #{test}"
|
31
|
+
puts " Failure/Error: #{details[0]}"
|
32
|
+
puts " expected: #{details[1].inspect}"
|
33
|
+
puts " got: #{details[2].inspect}"
|
34
|
+
i+=1
|
35
|
+
end
|
36
|
+
end
|
37
|
+
# (#{result['pass_asserions']}) (#{result['fail_asserions']})
|
38
|
+
puts "Pass: #{result['pass']}, Fail: #{result['fail']}"
|
39
|
+
if result["fail"] > 0
|
40
|
+
abort
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
t.after_pull do
|
45
|
+
%w{htmlparser htmllint htmlminifier}.each do |i|
|
46
|
+
cp "vendor/html-minifier/src/#{i}.js", "lib/js/#{i}.js"
|
47
|
+
sh "git add lib/js/#{i}.js"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
require "rspec/core/rake_task"
|
53
|
+
RSpec::Core::RakeTask.new
|
54
|
+
|
55
|
+
task :default => [:spec, "submodule:test"]
|
56
|
+
|
57
|
+
#desc "Generate code coverage"
|
58
|
+
# RSpec::Core::RakeTask.new(:coverage) do |t|
|
59
|
+
# t.rcov = true
|
60
|
+
# t.rcov_opts = ["--exclude", "spec"]
|
61
|
+
# end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "html_minifier/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "html_minifier"
|
7
|
+
s.version = HtmlMinifier::VERSION
|
8
|
+
s.authors = ["slavic"]
|
9
|
+
s.email = ["stereobooster@gmail.com"]
|
10
|
+
s.homepage = "https://github.com/stereobooster/html_minifier"
|
11
|
+
s.summary = %q{Ruby wrapper for kangax html-minifier}
|
12
|
+
s.description = %q{Ruby wrapper for kangax html-minifier}
|
13
|
+
|
14
|
+
s.files = `git ls-files`.split("\n")
|
15
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
16
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
17
|
+
s.require_paths = ["lib"]
|
18
|
+
|
19
|
+
s.add_development_dependency "rspec"
|
20
|
+
s.add_development_dependency "submodule", ">= 0.1.0"
|
21
|
+
s.add_runtime_dependency "rake"
|
22
|
+
|
23
|
+
s.add_dependency "multi_json", ">= 1.3"
|
24
|
+
s.add_dependency "execjs"
|
25
|
+
end
|
File without changes
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require "execjs"
|
4
|
+
require "multi_json"
|
5
|
+
|
6
|
+
module HtmlMinifier
|
7
|
+
|
8
|
+
class Minifier
|
9
|
+
Error = ExecJS::Error
|
10
|
+
|
11
|
+
SourceBasePath = File.expand_path("../../js/", __FILE__)
|
12
|
+
|
13
|
+
def initialize(options = nil)
|
14
|
+
if options.instance_of? Hash then
|
15
|
+
@options = options.dup
|
16
|
+
@log = @options.delete :log
|
17
|
+
elsif options.nil?
|
18
|
+
@options = nil
|
19
|
+
else
|
20
|
+
raise 'Unsupported option for HtmlMinifier: ' + options.to_s
|
21
|
+
end
|
22
|
+
|
23
|
+
js = %w{console htmlparser htmllint htmlminifier}.map do |i|
|
24
|
+
File.open("#{SourceBasePath}/#{i}.js", "r:UTF-8").read
|
25
|
+
end.join("\n")
|
26
|
+
@context = ExecJS.compile(js)
|
27
|
+
end
|
28
|
+
|
29
|
+
def minify(source)
|
30
|
+
source = source.respond_to?(:read) ? source.read : source.to_s
|
31
|
+
js = []
|
32
|
+
if @options.nil? then
|
33
|
+
js << "var min = minify(#{MultiJson.dump(source)});"
|
34
|
+
else
|
35
|
+
js << "var min = minify(#{MultiJson.dump(source)}, #{MultiJson.dump(@options)});"
|
36
|
+
end
|
37
|
+
js << "return {min:min, logs:console.clear()};"
|
38
|
+
result = @context.exec js.join("\n")
|
39
|
+
if @log.respond_to?(:info)
|
40
|
+
result["logs"].each do |i|
|
41
|
+
@log.info i
|
42
|
+
end
|
43
|
+
end
|
44
|
+
result["min"]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.minify(source, options = nil)
|
49
|
+
Minifier.new(options).minify(source)
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
File without changes
|
data/lib/js/console.js
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
(function (global) {
|
2
|
+
var logs = [];
|
3
|
+
global.console = {
|
4
|
+
log: function (message) {
|
5
|
+
logs.push(message);
|
6
|
+
},
|
7
|
+
get: function () {
|
8
|
+
return logs;
|
9
|
+
},
|
10
|
+
clear: function () {
|
11
|
+
var ret = logs;
|
12
|
+
logs = [];
|
13
|
+
return ret;
|
14
|
+
}
|
15
|
+
};
|
16
|
+
}(this));
|
data/lib/js/exports.js
ADDED
data/lib/js/htmllint.js
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
/*!
|
2
|
+
* HTMLLint (to be used in conjunction with HTMLMinifier)
|
3
|
+
*
|
4
|
+
* Copyright (c) 2010 Juriy "kangax" Zaytsev
|
5
|
+
* Licensed under the MIT license.
|
6
|
+
*
|
7
|
+
*/
|
8
|
+
|
9
|
+
(function(global){
|
10
|
+
|
11
|
+
function isPresentationalElement(tag) {
|
12
|
+
return (/^(?:b|i|big|small|hr|blink|marquee)$/).test(tag);
|
13
|
+
}
|
14
|
+
function isDeprecatedElement(tag) {
|
15
|
+
return (/^(?:applet|basefont|center|dir|font|isindex|menu|s|strike|u)$/).test(tag);
|
16
|
+
}
|
17
|
+
function isEventAttribute(attrName) {
|
18
|
+
return (/^on[a-z]+/).test(attrName);
|
19
|
+
}
|
20
|
+
function isStyleAttribute(attrName) {
|
21
|
+
return ('style' === attrName.toLowerCase());
|
22
|
+
}
|
23
|
+
function isDeprecatedAttribute(tag, attrName) {
|
24
|
+
return (
|
25
|
+
(attrName === 'align' &&
|
26
|
+
(/^(?:caption|applet|iframe|img|imput|object|legend|table|hr|div|h[1-6]|p)$/).test(tag)) ||
|
27
|
+
(attrName === 'alink' && tag === 'body') ||
|
28
|
+
(attrName === 'alt' && tag === 'applet') ||
|
29
|
+
(attrName === 'archive' && tag === 'applet') ||
|
30
|
+
(attrName === 'background' && tag === 'body') ||
|
31
|
+
(attrName === 'bgcolor' && (/^(?:table|t[rdh]|body)$/).test(tag)) ||
|
32
|
+
(attrName === 'border' && (/^(?:img|object)$/).test(tag)) ||
|
33
|
+
(attrName === 'clear' && tag === 'br') ||
|
34
|
+
(attrName === 'code' && tag === 'applet') ||
|
35
|
+
(attrName === 'codebase' && tag === 'applet') ||
|
36
|
+
(attrName === 'color' && (/^(?:base(?:font)?)$/).test(tag)) ||
|
37
|
+
(attrName === 'compact' && (/^(?:dir|[dou]l|menu)$/).test(tag)) ||
|
38
|
+
(attrName === 'face' && (/^base(?:font)?$/).test(tag)) ||
|
39
|
+
(attrName === 'height' && (/^(?:t[dh]|applet)$/).test(tag)) ||
|
40
|
+
(attrName === 'hspace' && (/^(?:applet|img|object)$/).test(tag)) ||
|
41
|
+
(attrName === 'language' && tag === 'script') ||
|
42
|
+
(attrName === 'link' && tag === 'body') ||
|
43
|
+
(attrName === 'name' && tag === 'applet') ||
|
44
|
+
(attrName === 'noshade' && tag === 'hr') ||
|
45
|
+
(attrName === 'nowrap' && (/^t[dh]$/).test(tag)) ||
|
46
|
+
(attrName === 'object' && tag === 'applet') ||
|
47
|
+
(attrName === 'prompt' && tag === 'isindex') ||
|
48
|
+
(attrName === 'size' && (/^(?:hr|font|basefont)$/).test(tag)) ||
|
49
|
+
(attrName === 'start' && tag === 'ol') ||
|
50
|
+
(attrName === 'text' && tag === 'body') ||
|
51
|
+
(attrName === 'type' && (/^(?:li|ol|ul)$/).test(tag)) ||
|
52
|
+
(attrName === 'value' && tag === 'li') ||
|
53
|
+
(attrName === 'version' && tag === 'html') ||
|
54
|
+
(attrName === 'vlink' && tag === 'body') ||
|
55
|
+
(attrName === 'vspace' && (/^(?:applet|img|object)$/).test(tag)) ||
|
56
|
+
(attrName === 'width' && (/^(?:hr|td|th|applet|pre)$/).test(tag))
|
57
|
+
);
|
58
|
+
}
|
59
|
+
function isInaccessibleAttribute(attrName, attrValue) {
|
60
|
+
return (
|
61
|
+
attrName === 'href' &&
|
62
|
+
(/^\s*javascript\s*:\s*void\s*(\s+0|\(\s*0\s*\))\s*$/i).test(attrValue)
|
63
|
+
);
|
64
|
+
}
|
65
|
+
|
66
|
+
function Lint() {
|
67
|
+
this.log = [ ];
|
68
|
+
this._lastElement = null;
|
69
|
+
this._isElementRepeated = false;
|
70
|
+
}
|
71
|
+
|
72
|
+
Lint.prototype.testElement = function(tag) {
|
73
|
+
if (isDeprecatedElement(tag)) {
|
74
|
+
this.log.push(
|
75
|
+
'<li>Found <span class="deprecated-element">deprecated</span> <strong><code><' +
|
76
|
+
tag + '></code></strong> element</li>');
|
77
|
+
}
|
78
|
+
else if (isPresentationalElement(tag)) {
|
79
|
+
this.log.push(
|
80
|
+
'<li>Found <span class="presentational-element">presentational</span> <strong><code><' +
|
81
|
+
tag + '></code></strong> element</li>');
|
82
|
+
}
|
83
|
+
else {
|
84
|
+
this.checkRepeatingElement(tag);
|
85
|
+
}
|
86
|
+
};
|
87
|
+
|
88
|
+
Lint.prototype.checkRepeatingElement = function(tag) {
|
89
|
+
if (tag === 'br' && this._lastElement === 'br') {
|
90
|
+
this._isElementRepeated = true;
|
91
|
+
}
|
92
|
+
else if (this._isElementRepeated) {
|
93
|
+
this._reportRepeatingElement();
|
94
|
+
this._isElementRepeated = false;
|
95
|
+
}
|
96
|
+
this._lastElement = tag;
|
97
|
+
};
|
98
|
+
|
99
|
+
Lint.prototype._reportRepeatingElement = function() {
|
100
|
+
this.log.push('<li>Found <code><br></code> sequence. Try replacing it with styling.</li>');
|
101
|
+
};
|
102
|
+
|
103
|
+
Lint.prototype.testAttribute = function(tag, attrName, attrValue) {
|
104
|
+
if (isEventAttribute(attrName)) {
|
105
|
+
this.log.push(
|
106
|
+
'<li>Found <span class="event-attribute">event attribute</span> (<strong>',
|
107
|
+
attrName, '</strong>) on <strong><code><' + tag + '></code></strong> element</li>');
|
108
|
+
}
|
109
|
+
else if (isDeprecatedAttribute(tag, attrName)) {
|
110
|
+
this.log.push(
|
111
|
+
'<li>Found <span class="deprecated-attribute">deprecated</span> <strong>' +
|
112
|
+
attrName + '</strong> attribute on <strong><code><', tag, '></code></strong> element</li>');
|
113
|
+
}
|
114
|
+
else if (isStyleAttribute(attrName)) {
|
115
|
+
this.log.push(
|
116
|
+
'<li>Found <span class="style-attribute">style attribute</span> on <strong><code><', tag, '></code></strong> element</li>');
|
117
|
+
}
|
118
|
+
else if (isInaccessibleAttribute(attrName, attrValue)) {
|
119
|
+
this.log.push(
|
120
|
+
'<li>Found <span class="inaccessible-attribute">inaccessible attribute</span> '+
|
121
|
+
'(on <strong><code><', tag, '></code></strong> element)</li>');
|
122
|
+
}
|
123
|
+
};
|
124
|
+
|
125
|
+
Lint.prototype.testChars = function(chars) {
|
126
|
+
this._lastElement = '';
|
127
|
+
if (/( \s*){2,}/.test(chars)) {
|
128
|
+
this.log.push('<li>Found repeating <strong><code>&nbsp;</code></strong> sequence. Try replacing it with styling.</li>');
|
129
|
+
}
|
130
|
+
};
|
131
|
+
|
132
|
+
Lint.prototype.test = function(tag, attrName, attrValue) {
|
133
|
+
this.testElement(tag);
|
134
|
+
this.testAttribute(tag, attrName, attrValue);
|
135
|
+
};
|
136
|
+
|
137
|
+
Lint.prototype.populate = function(writeToElement) {
|
138
|
+
if (this._isElementRepeated) {
|
139
|
+
this._reportRepeatingElement();
|
140
|
+
}
|
141
|
+
var report;
|
142
|
+
if (this.log.length && writeToElement) {
|
143
|
+
report = '<ol>' + this.log.join('') + '</ol>';
|
144
|
+
writeToElement.innerHTML = report;
|
145
|
+
}
|
146
|
+
};
|
147
|
+
|
148
|
+
global.HTMLLint = Lint;
|
149
|
+
|
150
|
+
})(typeof exports === 'undefined' ? this : exports);
|
@@ -0,0 +1,406 @@
|
|
1
|
+
/*!
|
2
|
+
* HTMLMinifier v0.4.4
|
3
|
+
* http://kangax.github.com/html-minifier/
|
4
|
+
*
|
5
|
+
* Copyright (c) 2010 Juriy "kangax" Zaytsev
|
6
|
+
* Licensed under the MIT license.
|
7
|
+
*
|
8
|
+
*/
|
9
|
+
|
10
|
+
(function(global){
|
11
|
+
|
12
|
+
var log, HTMLParser;
|
13
|
+
if (global.console && global.console.log) {
|
14
|
+
log = function(message) {
|
15
|
+
// "preserving" `this`
|
16
|
+
global.console.log(message);
|
17
|
+
};
|
18
|
+
}
|
19
|
+
else {
|
20
|
+
log = function(){ };
|
21
|
+
}
|
22
|
+
|
23
|
+
if (global.HTMLParser) {
|
24
|
+
HTMLParser = global.HTMLParser;
|
25
|
+
}
|
26
|
+
else if (typeof require === 'function') {
|
27
|
+
HTMLParser = require('./htmlparser').HTMLParser;
|
28
|
+
}
|
29
|
+
|
30
|
+
function trimWhitespace(str) {
|
31
|
+
return str.replace(/^\s+/, '').replace(/\s+$/, '');
|
32
|
+
}
|
33
|
+
if (String.prototype.trim) {
|
34
|
+
trimWhitespace = function(str) {
|
35
|
+
return str.trim();
|
36
|
+
};
|
37
|
+
}
|
38
|
+
|
39
|
+
function collapseWhitespace(str) {
|
40
|
+
return str.replace(/\s+/g, ' ');
|
41
|
+
}
|
42
|
+
|
43
|
+
function isConditionalComment(text) {
|
44
|
+
return (/\[if[^\]]+\]/).test(text);
|
45
|
+
}
|
46
|
+
|
47
|
+
function isEventAttribute(attrName) {
|
48
|
+
return (/^on[a-z]+/).test(attrName);
|
49
|
+
}
|
50
|
+
|
51
|
+
function canRemoveAttributeQuotes(value) {
|
52
|
+
// http://www.w3.org/TR/html4/intro/sgmltut.html#attributes
|
53
|
+
// avoid \w, which could match unicode in some implementations
|
54
|
+
return (/^[a-zA-Z0-9-._:]+$/).test(value);
|
55
|
+
}
|
56
|
+
|
57
|
+
function attributesInclude(attributes, attribute) {
|
58
|
+
for (var i = attributes.length; i--; ) {
|
59
|
+
if (attributes[i].name.toLowerCase() === attribute) {
|
60
|
+
return true;
|
61
|
+
}
|
62
|
+
}
|
63
|
+
return false;
|
64
|
+
}
|
65
|
+
|
66
|
+
function isAttributeRedundant(tag, attrName, attrValue, attrs) {
|
67
|
+
attrValue = trimWhitespace(attrValue.toLowerCase());
|
68
|
+
return (
|
69
|
+
(tag === 'script' &&
|
70
|
+
attrName === 'language' &&
|
71
|
+
attrValue === 'javascript') ||
|
72
|
+
|
73
|
+
(tag === 'form' &&
|
74
|
+
attrName === 'method' &&
|
75
|
+
attrValue === 'get') ||
|
76
|
+
|
77
|
+
(tag === 'input' &&
|
78
|
+
attrName === 'type' &&
|
79
|
+
attrValue === 'text') ||
|
80
|
+
|
81
|
+
(tag === 'script' &&
|
82
|
+
attrName === 'charset' &&
|
83
|
+
!attributesInclude(attrs, 'src')) ||
|
84
|
+
|
85
|
+
(tag === 'a' &&
|
86
|
+
attrName === 'name' &&
|
87
|
+
attributesInclude(attrs, 'id')) ||
|
88
|
+
|
89
|
+
(tag === 'area' &&
|
90
|
+
attrName === 'shape' &&
|
91
|
+
attrValue === 'rect')
|
92
|
+
);
|
93
|
+
}
|
94
|
+
|
95
|
+
function isScriptTypeAttribute(tag, attrName, attrValue) {
|
96
|
+
return (
|
97
|
+
tag === 'script' &&
|
98
|
+
attrName === 'type' &&
|
99
|
+
trimWhitespace(attrValue.toLowerCase()) === 'text/javascript'
|
100
|
+
);
|
101
|
+
}
|
102
|
+
|
103
|
+
function isStyleLinkTypeAttribute(tag, attrName, attrValue) {
|
104
|
+
return (
|
105
|
+
(tag === 'style' || tag === 'link') &&
|
106
|
+
attrName === 'type' &&
|
107
|
+
trimWhitespace(attrValue.toLowerCase()) === 'text/css'
|
108
|
+
);
|
109
|
+
}
|
110
|
+
|
111
|
+
function isBooleanAttribute(attrName) {
|
112
|
+
return (/^(?:checked|disabled|selected|readonly)$/).test(attrName);
|
113
|
+
}
|
114
|
+
|
115
|
+
function isUriTypeAttribute(attrName, tag) {
|
116
|
+
return (
|
117
|
+
((/^(?:a|area|link|base)$/).test(tag) && attrName === 'href') ||
|
118
|
+
(tag === 'img' && (/^(?:src|longdesc|usemap)$/).test(attrName)) ||
|
119
|
+
(tag === 'object' && (/^(?:classid|codebase|data|usemap)$/).test(attrName)) ||
|
120
|
+
(tag === 'q' && attrName === 'cite') ||
|
121
|
+
(tag === 'blockquote' && attrName === 'cite') ||
|
122
|
+
((tag === 'ins' || tag === 'del') && attrName === 'cite') ||
|
123
|
+
(tag === 'form' && attrName === 'action') ||
|
124
|
+
(tag === 'input' && (attrName === 'src' || attrName === 'usemap')) ||
|
125
|
+
(tag === 'head' && attrName === 'profile') ||
|
126
|
+
(tag === 'script' && (attrName === 'src' || attrName === 'for'))
|
127
|
+
);
|
128
|
+
}
|
129
|
+
|
130
|
+
function isNumberTypeAttribute(attrName, tag) {
|
131
|
+
return (
|
132
|
+
((/^(?:a|area|object|button)$/).test(tag) && attrName === 'tabindex') ||
|
133
|
+
(tag === 'input' && (attrName === 'maxlength' || attrName === 'tabindex')) ||
|
134
|
+
(tag === 'select' && (attrName === 'size' || attrName === 'tabindex')) ||
|
135
|
+
(tag === 'textarea' && (/^(?:rows|cols|tabindex)$/).test(attrName)) ||
|
136
|
+
(tag === 'colgroup' && attrName === 'span') ||
|
137
|
+
(tag === 'col' && attrName === 'span') ||
|
138
|
+
((tag === 'th' || tag == 'td') && (attrName === 'rowspan' || attrName === 'colspan'))
|
139
|
+
);
|
140
|
+
}
|
141
|
+
|
142
|
+
function cleanAttributeValue(tag, attrName, attrValue) {
|
143
|
+
if (isEventAttribute(attrName)) {
|
144
|
+
return trimWhitespace(attrValue).replace(/^javascript:\s*/i, '').replace(/\s*;$/, '');
|
145
|
+
}
|
146
|
+
else if (attrName === 'class') {
|
147
|
+
return collapseWhitespace(trimWhitespace(attrValue));
|
148
|
+
}
|
149
|
+
else if (isUriTypeAttribute(attrName, tag) || isNumberTypeAttribute(attrName, tag)) {
|
150
|
+
return trimWhitespace(attrValue);
|
151
|
+
}
|
152
|
+
else if (attrName === 'style') {
|
153
|
+
return trimWhitespace(attrValue).replace(/\s*;\s*$/, '');
|
154
|
+
}
|
155
|
+
return attrValue;
|
156
|
+
}
|
157
|
+
|
158
|
+
function cleanConditionalComment(comment) {
|
159
|
+
return comment
|
160
|
+
.replace(/^(\[[^\]]+\]>)\s*/, '$1')
|
161
|
+
.replace(/\s*(<!\[endif\])$/, '$1');
|
162
|
+
}
|
163
|
+
|
164
|
+
function removeCDATASections(text) {
|
165
|
+
return text
|
166
|
+
// "/* <![CDATA[ */" or "// <![CDATA["
|
167
|
+
.replace(/^(?:\s*\/\*\s*<!\[CDATA\[\s*\*\/|\s*\/\/\s*<!\[CDATA\[.*)/, '')
|
168
|
+
// "/* ]]> */" or "// ]]>"
|
169
|
+
.replace(/(?:\/\*\s*\]\]>\s*\*\/|\/\/\s*\]\]>)\s*$/, '');
|
170
|
+
}
|
171
|
+
|
172
|
+
var reStartDelimiter = {
|
173
|
+
// account for js + html comments (e.g.: //<!--)
|
174
|
+
'script': /^\s*(?:\/\/)?\s*<!--.*\n?/,
|
175
|
+
'style': /^\s*<!--\s*/
|
176
|
+
};
|
177
|
+
var reEndDelimiter = {
|
178
|
+
'script': /\s*(?:\/\/)?\s*-->\s*$/,
|
179
|
+
'style': /\s*-->\s*$/
|
180
|
+
};
|
181
|
+
function removeComments(text, tag) {
|
182
|
+
return text.replace(reStartDelimiter[tag], '').replace(reEndDelimiter[tag], '');
|
183
|
+
}
|
184
|
+
|
185
|
+
function isOptionalTag(tag) {
|
186
|
+
return (/^(?:html|t?body|t?head|tfoot|tr|option)$/).test(tag);
|
187
|
+
}
|
188
|
+
|
189
|
+
var reEmptyAttribute = new RegExp(
|
190
|
+
'^(?:class|id|style|title|lang|dir|on(?:focus|blur|change|click|dblclick|mouse(' +
|
191
|
+
'?:down|up|over|move|out)|key(?:press|down|up)))$');
|
192
|
+
|
193
|
+
function canDeleteEmptyAttribute(tag, attrName, attrValue) {
|
194
|
+
var isValueEmpty = /^(["'])?\s*\1$/.test(attrValue);
|
195
|
+
if (isValueEmpty) {
|
196
|
+
return (
|
197
|
+
(tag === 'input' && attrName === 'value') ||
|
198
|
+
reEmptyAttribute.test(attrName));
|
199
|
+
}
|
200
|
+
return false;
|
201
|
+
}
|
202
|
+
|
203
|
+
function canRemoveElement(tag) {
|
204
|
+
return tag !== 'textarea';
|
205
|
+
}
|
206
|
+
|
207
|
+
function canCollapseWhitespace(tag) {
|
208
|
+
return !(/^(?:script|style|pre|textarea)$/.test(tag));
|
209
|
+
}
|
210
|
+
|
211
|
+
function canTrimWhitespace(tag) {
|
212
|
+
return !(/^(?:pre|textarea)$/.test(tag));
|
213
|
+
}
|
214
|
+
|
215
|
+
function normalizeAttribute(attr, attrs, tag, options) {
|
216
|
+
|
217
|
+
var attrName = attr.name.toLowerCase(),
|
218
|
+
attrValue = attr.escaped,
|
219
|
+
attrFragment;
|
220
|
+
|
221
|
+
if ((options.removeRedundantAttributes &&
|
222
|
+
isAttributeRedundant(tag, attrName, attrValue, attrs))
|
223
|
+
||
|
224
|
+
(options.removeScriptTypeAttributes &&
|
225
|
+
isScriptTypeAttribute(tag, attrName, attrValue))
|
226
|
+
||
|
227
|
+
(options.removeStyleLinkTypeAttributes &&
|
228
|
+
isStyleLinkTypeAttribute(tag, attrName, attrValue))) {
|
229
|
+
return '';
|
230
|
+
}
|
231
|
+
|
232
|
+
attrValue = cleanAttributeValue(tag, attrName, attrValue);
|
233
|
+
|
234
|
+
if (!options.removeAttributeQuotes ||
|
235
|
+
!canRemoveAttributeQuotes(attrValue)) {
|
236
|
+
attrValue = '"' + attrValue + '"';
|
237
|
+
}
|
238
|
+
|
239
|
+
if (options.removeEmptyAttributes &&
|
240
|
+
canDeleteEmptyAttribute(tag, attrName, attrValue)) {
|
241
|
+
return '';
|
242
|
+
}
|
243
|
+
|
244
|
+
if (options.collapseBooleanAttributes &&
|
245
|
+
isBooleanAttribute(attrName)) {
|
246
|
+
attrFragment = attrName;
|
247
|
+
}
|
248
|
+
else {
|
249
|
+
attrFragment = attrName + '=' + attrValue;
|
250
|
+
}
|
251
|
+
|
252
|
+
return (' ' + attrFragment);
|
253
|
+
}
|
254
|
+
|
255
|
+
|
256
|
+
function setDefaultTesters(options) {
|
257
|
+
|
258
|
+
var defaultTesters = ['canCollapseWhitespace','canTrimWhitespace'];
|
259
|
+
|
260
|
+
for (var i = 0, len = defaultTesters.length; i < len; i++) {
|
261
|
+
if (!options[defaultTesters[i]]) {
|
262
|
+
options[defaultTesters[i]] = function() {
|
263
|
+
return false;
|
264
|
+
}
|
265
|
+
}
|
266
|
+
}
|
267
|
+
}
|
268
|
+
|
269
|
+
function minify(value, options) {
|
270
|
+
|
271
|
+
options = options || { };
|
272
|
+
value = trimWhitespace(value);
|
273
|
+
setDefaultTesters(options);
|
274
|
+
|
275
|
+
var results = [ ],
|
276
|
+
buffer = [ ],
|
277
|
+
currentChars = '',
|
278
|
+
currentTag = '',
|
279
|
+
currentAttrs = [],
|
280
|
+
stackNoTrimWhitespace = [],
|
281
|
+
stackNoCollapseWhitespace = [],
|
282
|
+
lint = options.lint,
|
283
|
+
t = new Date()
|
284
|
+
|
285
|
+
function _canCollapseWhitespace(tag, attrs) {
|
286
|
+
return canCollapseWhitespace(tag) || options.canTrimWhitespace(tag, attrs);
|
287
|
+
}
|
288
|
+
|
289
|
+
function _canTrimWhitespace(tag, attrs) {
|
290
|
+
return canTrimWhitespace(tag) || options.canTrimWhitespace(tag, attrs);
|
291
|
+
}
|
292
|
+
|
293
|
+
HTMLParser(value, {
|
294
|
+
start: function( tag, attrs, unary ) {
|
295
|
+
tag = tag.toLowerCase();
|
296
|
+
currentTag = tag;
|
297
|
+
currentChars = '';
|
298
|
+
currentAttrs = attrs;
|
299
|
+
|
300
|
+
// set whitespace flags for nested tags (eg. <code> within a <pre>)
|
301
|
+
if (options.collapseWhitespace) {
|
302
|
+
if (!_canTrimWhitespace(tag, attrs)) {
|
303
|
+
stackNoTrimWhitespace.push(tag);
|
304
|
+
}
|
305
|
+
if (!_canCollapseWhitespace(tag, attrs)) {
|
306
|
+
stackNoCollapseWhitespace.push(tag);
|
307
|
+
}
|
308
|
+
}
|
309
|
+
|
310
|
+
buffer.push('<', tag);
|
311
|
+
|
312
|
+
lint && lint.testElement(tag);
|
313
|
+
|
314
|
+
for ( var i = 0, len = attrs.length; i < len; i++ ) {
|
315
|
+
lint && lint.testAttribute(tag, attrs[i].name.toLowerCase(), attrs[i].escaped);
|
316
|
+
buffer.push(normalizeAttribute(attrs[i], attrs, tag, options));
|
317
|
+
}
|
318
|
+
|
319
|
+
buffer.push('>');
|
320
|
+
},
|
321
|
+
end: function( tag ) {
|
322
|
+
// check if current tag is in a whitespace stack
|
323
|
+
if (options.collapseWhitespace) {
|
324
|
+
if (stackNoTrimWhitespace.length &&
|
325
|
+
tag == stackNoTrimWhitespace[stackNoTrimWhitespace.length - 1]) {
|
326
|
+
stackNoTrimWhitespace.pop();
|
327
|
+
}
|
328
|
+
if (stackNoCollapseWhitespace.length &&
|
329
|
+
tag == stackNoCollapseWhitespace[stackNoCollapseWhitespace.length - 1]) {
|
330
|
+
stackNoCollapseWhitespace.pop();
|
331
|
+
}
|
332
|
+
}
|
333
|
+
|
334
|
+
var isElementEmpty = currentChars === '' && tag === currentTag;
|
335
|
+
if ((options.removeEmptyElements && isElementEmpty && canRemoveElement(tag))) {
|
336
|
+
// remove last "element" from buffer, return
|
337
|
+
buffer.splice(buffer.lastIndexOf('<'));
|
338
|
+
return;
|
339
|
+
}
|
340
|
+
else if (options.removeOptionalTags && isOptionalTag(tag)) {
|
341
|
+
// noop, leave start tag in buffer
|
342
|
+
return;
|
343
|
+
}
|
344
|
+
else {
|
345
|
+
// push end tag to buffer
|
346
|
+
buffer.push('</', tag.toLowerCase(), '>');
|
347
|
+
results.push.apply(results, buffer);
|
348
|
+
}
|
349
|
+
// flush buffer
|
350
|
+
buffer.length = 0;
|
351
|
+
currentChars = '';
|
352
|
+
},
|
353
|
+
chars: function( text ) {
|
354
|
+
if (currentTag === 'script' || currentTag === 'style') {
|
355
|
+
if (options.removeCommentsFromCDATA) {
|
356
|
+
text = removeComments(text, currentTag);
|
357
|
+
}
|
358
|
+
if (options.removeCDATASectionsFromCDATA) {
|
359
|
+
text = removeCDATASections(text);
|
360
|
+
}
|
361
|
+
}
|
362
|
+
if (options.collapseWhitespace) {
|
363
|
+
if (!stackNoTrimWhitespace.length && _canTrimWhitespace(currentTag, currentAttrs)) {
|
364
|
+
text = trimWhitespace(text);
|
365
|
+
}
|
366
|
+
if (!stackNoCollapseWhitespace.length && _canCollapseWhitespace(currentTag, currentAttrs)) {
|
367
|
+
text = collapseWhitespace(text);
|
368
|
+
}
|
369
|
+
}
|
370
|
+
currentChars = text;
|
371
|
+
lint && lint.testChars(text);
|
372
|
+
buffer.push(text);
|
373
|
+
},
|
374
|
+
comment: function( text ) {
|
375
|
+
if (options.removeComments) {
|
376
|
+
if (isConditionalComment(text)) {
|
377
|
+
text = '<!--' + cleanConditionalComment(text) + '-->';
|
378
|
+
}
|
379
|
+
else {
|
380
|
+
text = '';
|
381
|
+
}
|
382
|
+
}
|
383
|
+
else {
|
384
|
+
text = '<!--' + text + '-->';
|
385
|
+
}
|
386
|
+
buffer.push(text);
|
387
|
+
},
|
388
|
+
doctype: function(doctype) {
|
389
|
+
buffer.push(options.useShortDoctype ? '<!DOCTYPE html>' : collapseWhitespace(doctype));
|
390
|
+
}
|
391
|
+
});
|
392
|
+
|
393
|
+
results.push.apply(results, buffer)
|
394
|
+
var str = results.join('');
|
395
|
+
log('minified in: ' + (new Date() - t) + 'ms');
|
396
|
+
return str;
|
397
|
+
}
|
398
|
+
|
399
|
+
// for CommonJS enviroments, export everything
|
400
|
+
if ( typeof exports !== "undefined" ) {
|
401
|
+
exports.minify = minify;
|
402
|
+
} else {
|
403
|
+
global.minify = minify;
|
404
|
+
}
|
405
|
+
|
406
|
+
}(this));
|
@@ -0,0 +1,327 @@
|
|
1
|
+
/*
|
2
|
+
* HTML Parser By John Resig (ejohn.org)
|
3
|
+
* Modified by Juriy "kangax" Zaytsev
|
4
|
+
* Original code by Erik Arvidsson, Mozilla Public License
|
5
|
+
* http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
|
6
|
+
*
|
7
|
+
* // Use like so:
|
8
|
+
* HTMLParser(htmlString, {
|
9
|
+
* start: function(tag, attrs, unary) {},
|
10
|
+
* end: function(tag) {},
|
11
|
+
* chars: function(text) {},
|
12
|
+
* comment: function(text) {}
|
13
|
+
* });
|
14
|
+
*
|
15
|
+
* // or to get an XML string:
|
16
|
+
* HTMLtoXML(htmlString);
|
17
|
+
*
|
18
|
+
* // or to get an XML DOM Document
|
19
|
+
* HTMLtoDOM(htmlString);
|
20
|
+
*
|
21
|
+
* // or to inject into an existing document/DOM node
|
22
|
+
* HTMLtoDOM(htmlString, document);
|
23
|
+
* HTMLtoDOM(htmlString, document.body);
|
24
|
+
*
|
25
|
+
*/
|
26
|
+
|
27
|
+
(function(global){
|
28
|
+
|
29
|
+
// Regular Expressions for parsing tags and attributes
|
30
|
+
var startTag = /^<(\w+)((?:\s*[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/,
|
31
|
+
endTag = /^<\/(\w+)[^>]*>/,
|
32
|
+
attr = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g,
|
33
|
+
doctype = /^<!DOCTYPE [^>]+>/i;
|
34
|
+
|
35
|
+
// Empty Elements - HTML 4.01
|
36
|
+
var empty = makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");
|
37
|
+
|
38
|
+
// Block Elements - HTML 4.01
|
39
|
+
var block = makeMap("address,applet,blockquote,button,center,dd,del,dir,div,dl,dt,fieldset,form,frameset,hr,iframe,ins,isindex,li,map,menu,noframes,noscript,object,ol,p,pre,script,table,tbody,td,tfoot,th,thead,tr,ul");
|
40
|
+
|
41
|
+
// Inline Elements - HTML 4.01
|
42
|
+
var inline = makeMap("a,abbr,acronym,applet,b,basefont,bdo,big,br,button,cite,code,del,dfn,em,font,i,iframe,img,input,ins,kbd,label,map,object,q,s,samp,script,select,small,span,strike,strong,sub,sup,textarea,tt,u,var");
|
43
|
+
|
44
|
+
// Elements that you can, intentionally, leave open
|
45
|
+
// (and which close themselves)
|
46
|
+
var closeSelf = makeMap("colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr");
|
47
|
+
|
48
|
+
// Attributes that have their values filled in disabled="disabled"
|
49
|
+
var fillAttrs = makeMap("checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected");
|
50
|
+
|
51
|
+
// Special Elements (can contain anything)
|
52
|
+
var special = makeMap("script,style");
|
53
|
+
|
54
|
+
var reCache = { }, stackedTag, re;
|
55
|
+
|
56
|
+
var HTMLParser = global.HTMLParser = function( html, handler ) {
|
57
|
+
var index, chars, match, stack = [], last = html;
|
58
|
+
stack.last = function(){
|
59
|
+
return this[ this.length - 1 ];
|
60
|
+
};
|
61
|
+
|
62
|
+
while ( html ) {
|
63
|
+
chars = true;
|
64
|
+
|
65
|
+
// Make sure we're not in a script or style element
|
66
|
+
if ( !stack.last() || !special[ stack.last() ] ) {
|
67
|
+
|
68
|
+
// Comment
|
69
|
+
if ( html.indexOf("<!--") === 0 ) {
|
70
|
+
index = html.indexOf("-->");
|
71
|
+
|
72
|
+
if ( index >= 0 ) {
|
73
|
+
if ( handler.comment )
|
74
|
+
handler.comment( html.substring( 4, index ) );
|
75
|
+
html = html.substring( index + 3 );
|
76
|
+
chars = false;
|
77
|
+
}
|
78
|
+
}
|
79
|
+
else if ( match = doctype.exec( html )) {
|
80
|
+
if ( handler.doctype )
|
81
|
+
handler.doctype( match[0] );
|
82
|
+
html = html.substring( match[0].length );
|
83
|
+
chars = false;
|
84
|
+
|
85
|
+
// end tag
|
86
|
+
} else if ( html.indexOf("</") === 0 ) {
|
87
|
+
match = html.match( endTag );
|
88
|
+
|
89
|
+
if ( match ) {
|
90
|
+
html = html.substring( match[0].length );
|
91
|
+
match[0].replace( endTag, parseEndTag );
|
92
|
+
chars = false;
|
93
|
+
}
|
94
|
+
|
95
|
+
// start tag
|
96
|
+
} else if ( html.indexOf("<") === 0 ) {
|
97
|
+
match = html.match( startTag );
|
98
|
+
|
99
|
+
if ( match ) {
|
100
|
+
html = html.substring( match[0].length );
|
101
|
+
match[0].replace( startTag, parseStartTag );
|
102
|
+
chars = false;
|
103
|
+
}
|
104
|
+
}
|
105
|
+
|
106
|
+
if ( chars ) {
|
107
|
+
index = html.indexOf("<");
|
108
|
+
|
109
|
+
var text = index < 0 ? html : html.substring( 0, index );
|
110
|
+
html = index < 0 ? "" : html.substring( index );
|
111
|
+
|
112
|
+
if ( handler.chars )
|
113
|
+
handler.chars( text );
|
114
|
+
}
|
115
|
+
|
116
|
+
} else {
|
117
|
+
|
118
|
+
stackedTag = stack.last().toLowerCase();
|
119
|
+
reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp("([\\s\\S]*?)<\/" + stackedTag + "[^>]*>", "i"));
|
120
|
+
|
121
|
+
html = html.replace(reStackedTag, function(all, text) {
|
122
|
+
if (stackedTag !== 'script' && stackedTag !== 'style') {
|
123
|
+
text = text
|
124
|
+
.replace(/<!--([\s\S]*?)-->/g, "$1")
|
125
|
+
.replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, "$1");
|
126
|
+
}
|
127
|
+
|
128
|
+
if ( handler.chars )
|
129
|
+
handler.chars( text );
|
130
|
+
|
131
|
+
return "";
|
132
|
+
});
|
133
|
+
|
134
|
+
parseEndTag( "", stackedTag );
|
135
|
+
}
|
136
|
+
|
137
|
+
if ( html == last )
|
138
|
+
throw "Parse Error: " + html;
|
139
|
+
last = html;
|
140
|
+
}
|
141
|
+
|
142
|
+
// Clean up any remaining tags
|
143
|
+
parseEndTag();
|
144
|
+
|
145
|
+
function parseStartTag( tag, tagName, rest, unary ) {
|
146
|
+
if ( block[ tagName ] ) {
|
147
|
+
while ( stack.last() && inline[ stack.last() ] ) {
|
148
|
+
parseEndTag( "", stack.last() );
|
149
|
+
}
|
150
|
+
}
|
151
|
+
|
152
|
+
if ( closeSelf[ tagName ] && stack.last() == tagName ) {
|
153
|
+
parseEndTag( "", tagName );
|
154
|
+
}
|
155
|
+
|
156
|
+
unary = empty[ tagName ] || !!unary;
|
157
|
+
|
158
|
+
if ( !unary )
|
159
|
+
stack.push( tagName );
|
160
|
+
|
161
|
+
if ( handler.start ) {
|
162
|
+
var attrs = [];
|
163
|
+
|
164
|
+
rest.replace(attr, function(match, name) {
|
165
|
+
var value = arguments[2] ? arguments[2] :
|
166
|
+
arguments[3] ? arguments[3] :
|
167
|
+
arguments[4] ? arguments[4] :
|
168
|
+
fillAttrs[name] ? name : "";
|
169
|
+
attrs.push({
|
170
|
+
name: name,
|
171
|
+
value: value,
|
172
|
+
escaped: value.replace(/(^|[^\\])"/g, '$1\\\"') //"
|
173
|
+
});
|
174
|
+
});
|
175
|
+
|
176
|
+
if ( handler.start )
|
177
|
+
handler.start( tagName, attrs, unary );
|
178
|
+
}
|
179
|
+
}
|
180
|
+
|
181
|
+
function parseEndTag( tag, tagName ) {
|
182
|
+
// If no tag name is provided, clean shop
|
183
|
+
if ( !tagName )
|
184
|
+
var pos = 0;
|
185
|
+
|
186
|
+
// Find the closest opened tag of the same type
|
187
|
+
else
|
188
|
+
for ( var pos = stack.length - 1; pos >= 0; pos-- )
|
189
|
+
if ( stack[ pos ] == tagName )
|
190
|
+
break;
|
191
|
+
|
192
|
+
if ( pos >= 0 ) {
|
193
|
+
// Close all the open elements, up the stack
|
194
|
+
for ( var i = stack.length - 1; i >= pos; i-- )
|
195
|
+
if ( handler.end )
|
196
|
+
handler.end( stack[ i ] );
|
197
|
+
|
198
|
+
// Remove the open elements from the stack
|
199
|
+
stack.length = pos;
|
200
|
+
}
|
201
|
+
}
|
202
|
+
};
|
203
|
+
|
204
|
+
global.HTMLtoXML = function( html ) {
|
205
|
+
var results = "";
|
206
|
+
|
207
|
+
HTMLParser(html, {
|
208
|
+
start: function( tag, attrs, unary ) {
|
209
|
+
results += "<" + tag;
|
210
|
+
|
211
|
+
for ( var i = 0; i < attrs.length; i++ )
|
212
|
+
results += " " + attrs[i].name + '="' + attrs[i].escaped + '"';
|
213
|
+
|
214
|
+
results += (unary ? "/" : "") + ">";
|
215
|
+
},
|
216
|
+
end: function( tag ) {
|
217
|
+
results += "</" + tag + ">";
|
218
|
+
},
|
219
|
+
chars: function( text ) {
|
220
|
+
results += text;
|
221
|
+
},
|
222
|
+
comment: function( text ) {
|
223
|
+
results += "<!--" + text + "-->";
|
224
|
+
}
|
225
|
+
});
|
226
|
+
|
227
|
+
return results;
|
228
|
+
};
|
229
|
+
|
230
|
+
global.HTMLtoDOM = function( html, doc ) {
|
231
|
+
// There can be only one of these elements
|
232
|
+
var one = makeMap("html,head,body,title");
|
233
|
+
|
234
|
+
// Enforce a structure for the document
|
235
|
+
var structure = {
|
236
|
+
link: "head",
|
237
|
+
base: "head"
|
238
|
+
};
|
239
|
+
|
240
|
+
if ( !doc ) {
|
241
|
+
if ( typeof DOMDocument != "undefined" )
|
242
|
+
doc = new DOMDocument();
|
243
|
+
else if ( typeof document != "undefined" && document.implementation && document.implementation.createDocument )
|
244
|
+
doc = document.implementation.createDocument("", "", null);
|
245
|
+
else if ( typeof ActiveX != "undefined" )
|
246
|
+
doc = new ActiveXObject("Msxml.DOMDocument");
|
247
|
+
|
248
|
+
} else
|
249
|
+
doc = doc.ownerDocument ||
|
250
|
+
doc.getOwnerDocument && doc.getOwnerDocument() ||
|
251
|
+
doc;
|
252
|
+
|
253
|
+
var elems = [],
|
254
|
+
documentElement = doc.documentElement ||
|
255
|
+
doc.getDocumentElement && doc.getDocumentElement();
|
256
|
+
|
257
|
+
// If we're dealing with an empty document then we
|
258
|
+
// need to pre-populate it with the HTML document structure
|
259
|
+
if ( !documentElement && doc.createElement ) (function(){
|
260
|
+
var html = doc.createElement("html");
|
261
|
+
var head = doc.createElement("head");
|
262
|
+
head.appendChild( doc.createElement("title") );
|
263
|
+
html.appendChild( head );
|
264
|
+
html.appendChild( doc.createElement("body") );
|
265
|
+
doc.appendChild( html );
|
266
|
+
})();
|
267
|
+
|
268
|
+
// Find all the unique elements
|
269
|
+
if ( doc.getElementsByTagName )
|
270
|
+
for ( var i in one )
|
271
|
+
one[ i ] = doc.getElementsByTagName( i )[0];
|
272
|
+
|
273
|
+
// If we're working with a document, inject contents into
|
274
|
+
// the body element
|
275
|
+
var curParentNode = one.body;
|
276
|
+
|
277
|
+
HTMLParser( html, {
|
278
|
+
start: function( tagName, attrs, unary ) {
|
279
|
+
// If it's a pre-built element, then we can ignore
|
280
|
+
// its construction
|
281
|
+
if ( one[ tagName ] ) {
|
282
|
+
curParentNode = one[ tagName ];
|
283
|
+
return;
|
284
|
+
}
|
285
|
+
|
286
|
+
var elem = doc.createElement( tagName );
|
287
|
+
|
288
|
+
for ( var attr in attrs )
|
289
|
+
elem.setAttribute( attrs[ attr ].name, attrs[ attr ].value );
|
290
|
+
|
291
|
+
if ( structure[ tagName ] && typeof one[ structure[ tagName ] ] != "boolean" )
|
292
|
+
one[ structure[ tagName ] ].appendChild( elem );
|
293
|
+
|
294
|
+
else if ( curParentNode && curParentNode.appendChild )
|
295
|
+
curParentNode.appendChild( elem );
|
296
|
+
|
297
|
+
if ( !unary ) {
|
298
|
+
elems.push( elem );
|
299
|
+
curParentNode = elem;
|
300
|
+
}
|
301
|
+
},
|
302
|
+
end: function( tag ) {
|
303
|
+
elems.length -= 1;
|
304
|
+
|
305
|
+
// Init the new parentNode
|
306
|
+
curParentNode = elems[ elems.length - 1 ];
|
307
|
+
},
|
308
|
+
chars: function( text ) {
|
309
|
+
curParentNode.appendChild( doc.createTextNode( text ) );
|
310
|
+
},
|
311
|
+
comment: function( text ) {
|
312
|
+
// create comment node
|
313
|
+
}
|
314
|
+
});
|
315
|
+
|
316
|
+
return doc;
|
317
|
+
};
|
318
|
+
|
319
|
+
function makeMap(str){
|
320
|
+
var obj = {}, items = str.split(",");
|
321
|
+
for ( var i = 0; i < items.length; i++ ) {
|
322
|
+
obj[ items[i] ] = true;
|
323
|
+
obj[ items[i].toUpperCase() ] = true;
|
324
|
+
}
|
325
|
+
return obj;
|
326
|
+
}
|
327
|
+
})(typeof exports === 'undefined' ? this : exports);
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require "html_minifier"
|
3
|
+
|
4
|
+
class TestLogger
|
5
|
+
attr_accessor :logs
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@logs = []
|
9
|
+
end
|
10
|
+
|
11
|
+
def info message
|
12
|
+
@logs << message
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "HtmlMinifier" do
|
17
|
+
|
18
|
+
it "it works" do
|
19
|
+
html = '<p>foo</p>'
|
20
|
+
HtmlMinifier.minify(html).should eq html
|
21
|
+
end
|
22
|
+
|
23
|
+
it "it logs" do
|
24
|
+
html = '<p>foo</p>'
|
25
|
+
log = TestLogger.new
|
26
|
+
HtmlMinifier.minify(html, :log => log).should eq html
|
27
|
+
log.logs.length.should eq 1
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
(function(QUnit) {
|
2
|
+
|
3
|
+
QUnit.init();
|
4
|
+
QUnit.config.blocking = false;
|
5
|
+
QUnit.config.autorun = true;
|
6
|
+
QUnit.config.updateRate = 0;
|
7
|
+
|
8
|
+
var assertions,
|
9
|
+
result = {
|
10
|
+
pass: 0,
|
11
|
+
fail: 0,
|
12
|
+
pass_asserions: 0,
|
13
|
+
fail_asserions: 0,
|
14
|
+
assertions: {},
|
15
|
+
tests: 0
|
16
|
+
};
|
17
|
+
|
18
|
+
QUnit.testDone(function (r) {
|
19
|
+
if (r.failed > 0) {
|
20
|
+
result.fail += 1;
|
21
|
+
result.assertions[(r.module ? r.module + ':' : '') + r.name] = assertions;
|
22
|
+
assertions = null;
|
23
|
+
} else {
|
24
|
+
result.pass += 1;
|
25
|
+
}
|
26
|
+
result.fail_asserions += r.failed;
|
27
|
+
result.pass_asserions += r.passed;
|
28
|
+
result.tests++;
|
29
|
+
});
|
30
|
+
|
31
|
+
QUnit.log(function (r) {
|
32
|
+
if (!r.result) {
|
33
|
+
assertions = [r.message, r.actual, r.expected];
|
34
|
+
}
|
35
|
+
});
|
36
|
+
|
37
|
+
QUnit.result = function () {
|
38
|
+
// run the tests
|
39
|
+
// QUnit.begin();
|
40
|
+
// QUnit.start();
|
41
|
+
return result;
|
42
|
+
};
|
43
|
+
|
44
|
+
}( QUnit ));
|
metadata
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: html_minifier
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- slavic
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-05-22 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: &28832952 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *28832952
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: submodule
|
27
|
+
requirement: &28832652 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 0.1.0
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *28832652
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: rake
|
38
|
+
requirement: &28832400 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :runtime
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *28832400
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: multi_json
|
49
|
+
requirement: &28832076 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.3'
|
55
|
+
type: :runtime
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *28832076
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: execjs
|
60
|
+
requirement: &28831824 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
type: :runtime
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *28831824
|
69
|
+
description: Ruby wrapper for kangax html-minifier
|
70
|
+
email:
|
71
|
+
- stereobooster@gmail.com
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- .gitignore
|
77
|
+
- .gitmodules
|
78
|
+
- .travis.yml
|
79
|
+
- Gemfile
|
80
|
+
- README.md
|
81
|
+
- Rakefile
|
82
|
+
- html_minifier.gemspec
|
83
|
+
- lib/html_minifier.rb
|
84
|
+
- lib/html_minifier/linter.rb
|
85
|
+
- lib/html_minifier/minifier.rb
|
86
|
+
- lib/html_minifier/task.rb
|
87
|
+
- lib/html_minifier/version.rb
|
88
|
+
- lib/js/console.js
|
89
|
+
- lib/js/exports.js
|
90
|
+
- lib/js/htmllint.js
|
91
|
+
- lib/js/htmlminifier.js
|
92
|
+
- lib/js/htmlparser.js
|
93
|
+
- spec/html_minifier_spec.rb
|
94
|
+
- spec/qunit_helper.js
|
95
|
+
homepage: https://github.com/stereobooster/html_minifier
|
96
|
+
licenses: []
|
97
|
+
post_install_message:
|
98
|
+
rdoc_options: []
|
99
|
+
require_paths:
|
100
|
+
- lib
|
101
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
102
|
+
none: false
|
103
|
+
requirements:
|
104
|
+
- - ! '>='
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: '0'
|
107
|
+
segments:
|
108
|
+
- 0
|
109
|
+
hash: -143587247
|
110
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
111
|
+
none: false
|
112
|
+
requirements:
|
113
|
+
- - ! '>='
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '0'
|
116
|
+
segments:
|
117
|
+
- 0
|
118
|
+
hash: -143587247
|
119
|
+
requirements: []
|
120
|
+
rubyforge_project:
|
121
|
+
rubygems_version: 1.8.15
|
122
|
+
signing_key:
|
123
|
+
specification_version: 3
|
124
|
+
summary: Ruby wrapper for kangax html-minifier
|
125
|
+
test_files: []
|