├── lib ├── html_minifier │ ├── linter.rb │ ├── task.rb │ ├── version.rb │ └── minifier.rb ├── html_minifier.rb └── js │ ├── exports.js │ ├── console.js │ ├── htmllint.js │ ├── htmlparser.js │ └── htmlminifier.js ├── .gitignore ├── .gitmodules ├── .travis.yml ├── Gemfile ├── spec ├── html_minifier_spec.rb └── qunit_helper.js ├── html_minifier.gemspec ├── README.md └── Rakefile /lib/html_minifier/linter.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/html_minifier/task.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | Gemfile.lock 4 | pkg/* 5 | /.project 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/html-minifier"] 2 | path = vendor/html-minifier 3 | url = https://github.com/kangax/html-minifier.git 4 | -------------------------------------------------------------------------------- /lib/html_minifier/version.rb: -------------------------------------------------------------------------------- 1 | module HtmlMinifier 2 | VERSION = "0.0.4" 3 | SUBMODULE = "7032d3d3d97aabf0a2c96c8155ed077b455aca31" 4 | end 5 | -------------------------------------------------------------------------------- /lib/html_minifier.rb: -------------------------------------------------------------------------------- 1 | require "html_minifier/version" 2 | require "html_minifier/minifier" 3 | 4 | module HtmlMinifier 5 | # Your code goes here... 6 | end 7 | -------------------------------------------------------------------------------- /lib/js/exports.js: -------------------------------------------------------------------------------- 1 | var exports = (function () { 2 | var logs = []; 3 | return { 4 | console: { 5 | log: function (message) { 6 | logs.push(message); 7 | }, 8 | get: function () { 9 | return logs; 10 | }, 11 | clear: function () { 12 | logs = []; 13 | } 14 | } 15 | }; 16 | })(); 17 | -------------------------------------------------------------------------------- /lib/js/console.js: -------------------------------------------------------------------------------- 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)); 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 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 | - EXECJS_RUNTIME=Node 11 | matrix: 12 | exclude: 13 | - rvm: 1.8.7 14 | env: EXECJS_RUNTIME=RubyRhino 15 | - rvm: 1.9.2 16 | env: EXECJS_RUNTIME=RubyRhino 17 | - rvm: 1.9.3 18 | env: EXECJS_RUNTIME=RubyRhino 19 | - rvm: jruby 20 | env: EXECJS_RUNTIME=RubyRacer 21 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 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 | } 11 | 12 | if ENV["EXECJS_RUNTIME"] && execjs_runtimes[ENV["EXECJS_RUNTIME"]] 13 | gem execjs_runtimes[ENV["EXECJS_RUNTIME"]], :group => :development 14 | if execjs_runtimes[ENV["EXECJS_RUNTIME"]] == "therubyracer" 15 | gem 'libv8', '~> 3.11.8', :group => :development 16 | end 17 | end 18 | 19 | -------------------------------------------------------------------------------- /spec/html_minifier_spec.rb: -------------------------------------------------------------------------------- 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 = '

foo

' 20 | HtmlMinifier.minify(html).should eq html 21 | end 22 | 23 | it "it logs" do 24 | html = '

foo

' 25 | log = TestLogger.new 26 | HtmlMinifier.minify(html, :log => log).should eq html 27 | log.logs.length.should eq 1 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /html_minifier.gemspec: -------------------------------------------------------------------------------- 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 = ["stereobooster"] 9 | s.email = ["stereobooster@gmail.com"] 10 | s.homepage = "https://github.com/stereobooster/html_minifier" 11 | s.summary = %q{Ruby wrapper for kangax js library html-minifier. If you want pure ruby use html_press} 12 | s.description = %q{Ruby wrapper for kangax js library html-minifier. If you want pure ruby use html_press} 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_development_dependency "rake" 22 | 23 | s.add_dependency "multi_json", ">= 1.3" 24 | s.add_dependency "execjs" 25 | end 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 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 js library [html-minifier](https://github.com/kangax/html-minifier/). If you want pure ruby use [html_press](https://github.com/stereobooster/html_press) 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 | When initializing `HtmlMinifier`, you can pass options 23 | 24 | ```ruby 25 | HtmlMinifier::minifier.new( ).minify(source) 26 | # Or 27 | HtmlMinifier.minify(source, ) 28 | ``` 29 | 30 | ## TODO 31 | 32 | - add Rake task 33 | - add color reporter. Maybe [colorize](https://github.com/fazibear/colorize) 34 | - add cli 35 | -------------------------------------------------------------------------------- /spec/qunit_helper.js: -------------------------------------------------------------------------------- 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 | }( this.QUnit )); 45 | -------------------------------------------------------------------------------- /lib/html_minifier/minifier.rb: -------------------------------------------------------------------------------- 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 | js = "function globe(){#{js};return this};var global = new globe();" 27 | @context = ExecJS.compile(js) 28 | end 29 | 30 | def minify(source) 31 | source = source.respond_to?(:read) ? source.read : source.to_s 32 | js = [] 33 | if @options.nil? then 34 | js << "var min = global.minify(#{MultiJson.dump(source)});" 35 | else 36 | js << "var min = global.minify(#{MultiJson.dump(source)}, #{MultiJson.dump(@options)});" 37 | end 38 | js << "return {min:min, logs:global.console.clear()};" 39 | 40 | result = @context.exec js.join("\n") 41 | if @log.respond_to?(:info) 42 | result["logs"].each do |i| 43 | @log.info i 44 | end 45 | end 46 | result["min"] 47 | end 48 | end 49 | 50 | def self.minify(source, options = nil) 51 | Minifier.new(options).minify(source) 52 | end 53 | 54 | end 55 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 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.gsub('}( (function() {return this;}.call()) ));', '}( this ));') 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 | js = js.join("\n") 24 | js = "function globe(){#{js};return this};var global = new globe();" 25 | 26 | require "execjs" 27 | context = ExecJS.compile js 28 | result = context.exec "return global.QUnit.result();" 29 | if result["fail"] > 0 && result["assertions"].respond_to?(:each) 30 | puts "Failures:" 31 | i = 1 32 | result["assertions"].each do |test, details| 33 | puts " #{i}) #{test}" 34 | puts " Failure/Error: #{details[0]}" 35 | puts " expected: #{details[1].inspect}" 36 | puts " got: #{details[2].inspect}" 37 | i+=1 38 | end 39 | end 40 | # (#{result['pass_asserions']}) (#{result['fail_asserions']}) 41 | puts "Pass: #{result['pass']}, Fail: #{result['fail']}" 42 | if result["fail"] > 0 43 | abort 44 | end 45 | end 46 | 47 | t.after_pull do 48 | %w{htmlparser htmllint htmlminifier}.each do |i| 49 | cp "vendor/html-minifier/src/#{i}.js", "lib/js/#{i}.js" 50 | sh "git add lib/js/#{i}.js" 51 | end 52 | end 53 | end 54 | 55 | require "rspec/core/rake_task" 56 | RSpec::Core::RakeTask.new 57 | 58 | task :default => [:spec, "submodule:test"] 59 | 60 | #desc "Generate code coverage" 61 | # RSpec::Core::RakeTask.new(:coverage) do |t| 62 | # t.rcov = true 63 | # t.rcov_opts = ["--exclude", "spec"] 64 | # end 65 | -------------------------------------------------------------------------------- /lib/js/htmllint.js: -------------------------------------------------------------------------------- 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 | '
  • Found deprecated <' + 76 | tag + '> element
  • '); 77 | } 78 | else if (isPresentationalElement(tag)) { 79 | this.log.push( 80 | '
  • Found presentational <' + 81 | tag + '> element
  • '); 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('
  • Found <br> sequence. Try replacing it with styling.
  • '); 101 | }; 102 | 103 | Lint.prototype.testAttribute = function(tag, attrName, attrValue) { 104 | if (isEventAttribute(attrName)) { 105 | this.log.push( 106 | '
  • Found event attribute (', 107 | attrName, ') on <' + tag + '> element
  • '); 108 | } 109 | else if (isDeprecatedAttribute(tag, attrName)) { 110 | this.log.push( 111 | '
  • Found deprecated ' + 112 | attrName + ' attribute on <', tag, '> element
  • '); 113 | } 114 | else if (isStyleAttribute(attrName)) { 115 | this.log.push( 116 | '
  • Found style attribute on <', tag, '> element
  • '); 117 | } 118 | else if (isInaccessibleAttribute(attrName, attrValue)) { 119 | this.log.push( 120 | '
  • Found inaccessible attribute '+ 121 | '(on <', tag, '> element)
  • '); 122 | } 123 | }; 124 | 125 | Lint.prototype.testChars = function(chars) { 126 | this._lastElement = ''; 127 | if (/( \s*){2,}/.test(chars)) { 128 | this.log.push('
  • Found repeating &nbsp; sequence. Try replacing it with styling.
  • '); 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 = '
      ' + this.log.join('') + '
    '; 144 | writeToElement.innerHTML = report; 145 | } 146 | }; 147 | 148 | global.HTMLLint = Lint; 149 | 150 | })(typeof exports === 'undefined' ? this : exports); -------------------------------------------------------------------------------- /lib/js/htmlparser.js: -------------------------------------------------------------------------------- 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 = /^]+>/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(""); 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("]*>", "i")); 120 | 121 | html = html.replace(reStackedTag, function(all, text) { 122 | if (stackedTag !== 'script' && stackedTag !== 'style') { 123 | text = text 124 | .replace(//g, "$1") 125 | .replace(//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 += ""; 218 | }, 219 | chars: function( text ) { 220 | results += text; 221 | }, 222 | comment: function( text ) { 223 | results += ""; 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); -------------------------------------------------------------------------------- /lib/js/htmlminifier.js: -------------------------------------------------------------------------------- 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*( */" or "// ]]>" 169 | .replace(/(?:\/\*\s*\]\]>\s*\*\/|\/\/\s*\]\]>)\s*$/, ''); 170 | } 171 | 172 | var reStartDelimiter = { 173 | // account for js + html comments (e.g.: //\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. within a
    )
    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('');
    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 = '';
    378 |           }
    379 |           else {
    380 |             text = '';
    381 |           }
    382 |         }
    383 |         else {
    384 |           text = '';
    385 |         }
    386 |         buffer.push(text);
    387 |       },
    388 |       doctype: function(doctype) {
    389 |         buffer.push(options.useShortDoctype ? '' : 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));
    
    
    --------------------------------------------------------------------------------