diff --git a/Makefile b/Makefile index 6a71722be..2e6848d49 100644 --- a/Makefile +++ b/Makefile @@ -7,11 +7,8 @@ BUILD_DIR = build PREFIX = . DIST_DIR = ${PREFIX}/dist -RHINO ?= java -jar ${BUILD_DIR}/js.jar - -CLOSURE_COMPILER = ${BUILD_DIR}/google-compiler-20100917.jar - -MINJAR ?= java -jar ${CLOSURE_COMPILER} +JS_ENGINE ?= `which node nodejs` +COMPILER = ${JS_ENGINE} ${BUILD_DIR}/uglify.js --unsafe BASE_FILES = ${SRC_DIR}/core.js\ ${SRC_DIR}/support.js\ @@ -93,17 +90,13 @@ ${SRC_DIR}/selector.js: ${SIZZLE_DIR}/sizzle.js lint: ${JQ} @@echo "Checking jQuery against JSLint..." - @@${RHINO} build/jslint-check.js + @@${JS_ENGINE} build/jslint-check.js min: ${JQ_MIN} ${JQ_MIN}: ${JQ} @@echo "Building" ${JQ_MIN} - - @@head -15 ${JQ} > ${JQ_MIN} - @@${MINJAR} --js ${JQ} --warning_level QUIET --js_output_file ${JQ_MIN}.tmp - @@cat ${JQ_MIN}.tmp >> ${JQ_MIN} - @@rm -f ${JQ_MIN}.tmp + @@${COMPILER} ${JQ} > ${JQ_MIN} clean: @@echo "Removing Distribution directory:" ${DIST_DIR} diff --git a/README.md b/README.md index 384e2d190..d56576c81 100644 --- a/README.md +++ b/README.md @@ -1,85 +1,64 @@ [jQuery](http://jquery.com/) - New Wave Javascript -================================ +================================================== What you need to build your own jQuery ---------------------------------------- -* Make sure that you have Java installed (if you want to build a minified version of jQuery). -If not, [go to this page](http://java.sun.com/javase/downloads/index.jsp) and download "Java Runtime Environment (JRE) 5.0" +-------------------------------------- -Build Options --------------- +In order to build jQuery, you need to have GNU make 3.8 or later, Node.js 0.2 or later, and git 1.7 or later. +(Earlier versions might work OK, but are not tested.) -You now have **three** options for building jQuery: +Windows users have two options: -* **`make`**: If you have access to common UNIX commands (like `make`, `mkdir`, `rm`, `cat`, and `echo`) then simply type `make` to build all the components. +1. Install [msysgit](https://code.google.com/p/msysgit/) (Full installer for official Git), + [GNU make for Windows](http://gnuwin32.sourceforge.net/packages/make.htm), and a + [binary version of Node.js](http://node-js.prcn.co.cc/). Make sure all three packages are installed to the same + location (by default, this is C:\Program Files\Git). +2. Install [Cygwin](http://cygwin.com/) (make sure you install the git, make, and which packages), then either follow + the [Node.js build instructions](https://github.com/ry/node/wiki/Building-node.js-on-Cygwin-%28Windows%29) or install + the [binary version of Node.js](http://node-js.prcn.co.cc/). -* **`rake`**: If you have Ruby Rake installed (on either Windows or UNIX/Linux), you can simply type `rake` to build all the components. +Mac OS users should install Xcode (comes on your Mac OS install DVD, or downloadable from +[Apple's Xcode site](http://developer.apple.com/technologies/xcode.html)) and +[http://mxcl.github.com/homebrew/](Homebrew). Once Homebrew is installed, run `brew install git` to install git, +and `brew install node` to install Node.js. + +Linux/BSD users should use their appropriate package managers to install make, git, and node, or build from source +if you swing that way. Easy-peasy. -* **`ant`**: If you have Ant installed (or are on Windows and don't have access to make). You can download Ant from here: [http://ant.apache.org/bindownload.cgi]. How to build your own jQuery ------------------------------ +---------------------------- -*Note: If you are using either `rake` or `ant`, substitute your chosen method in place of `make` in the examples below. They work identically for all intents and purposes. Quick reference is also available for `rake` by typing `rake -T` in the `jquery` directory.* +First, clone a copy of the main jQuery git repo by running `git clone git://github.com/jquery/jquery.git`. -In the main directory of the distribution (the one that this file is in), type -the following to make all versions of jQuery: +Then, to get a complete, minified, jslinted version of jQuery, simply `cd` to the `jquery` directory and type +`make`. If you don't have Node installed and/or want to make a basic, uncompressed, unlinted version of jQuery, use +`make jquery` instead of `make`. - make +The built version of jQuery will be put in the `dist/` subdirectory. -*Here are the individual items that are buildable from the Makefile:* +To remove all built files, run `make clean`. - make init - -Pull in all the external dependencies (QUnit, Sizzle) for the project. - - make jquery - -The standard, uncompressed, jQuery code. -Makes: `./dist/jquery.js` - - make min - -A compressed version of jQuery (made the Closure Compiler). -Makes: `./dist/jquery.min.js` - - make lint - -Tests a build of jQuery against JSLint, looking for potential errors or bits of confusing code. - - make selector - -Builds the selector library for jQuery from Sizzle. -Makes: `./src/selector.js` - -Finally, you can remove all the built files using the command: - - make clean Building to a different directory ----------------------------------- +--------------------------------- -If you want to build jQuery to a directory that is different from the default location, you can... +If you want to build jQuery to a directory that is different from the default location, you can specify the PREFIX +directory: `make PREFIX=/home/jquery/test/ [command]` -**Make only:** Specify the PREFIX directory, for example: - - make PREFIX=/home/john/test/ [command] - -With this example, the output files would be contained in `/home/john/test/dist/` +With this example, the output files would end up in `/home/jquery/test/dist/`. -**Rake only:** Define the DIST_DIR directory, for example: - rake DIST_DIR=/home/john/test/ [command] - -With this example, the output files would be contained in `/home/john/test/` +Troubleshooting +--------------- -*In both examples, `[command]` is optional.* +Sometimes, the various git repositories get into an inconsistent state where builds don't complete properly +(usually this results in the jquery.js or jquery.min.js being 0 bytes). If this happens, run `make clean`, then +run `make` again. -**Ant only:** You cannot currently build to another directory when using Ant. Questions? ---------- -If you have any questions, please feel free to ask them on the Developing jQuery Core -forum, which can be found here: -[http://forum.jquery.com/developing-jquery-core](http://forum.jquery.com/developing-jquery-core) +If you have any questions, please feel free to ask on the +[Developing jQuery Core forum](http://forum.jquery.com/developing-jquery-core) or in #jquery on irc.freenode.net. diff --git a/Rakefile b/Rakefile deleted file mode 100644 index bf7ee2b58..000000000 --- a/Rakefile +++ /dev/null @@ -1,162 +0,0 @@ -prefix = File.dirname( __FILE__ ) - -# Directory variables -src_dir = File.join( prefix, 'src' ) -build_dir = File.join( prefix, 'build' ) -test_dir = File.join( prefix, 'test' ) - -# A different destination directory can be set by -# setting DIST_DIR before calling rake -dist_dir = ENV['DIST_DIR'] || File.join( prefix, 'dist' ) - -base_files = %w{ - intro - core - support - data - queue - attributes - event - selector - traversing - manipulation - css - ajax - ajax/jsonp - ajax/script - ajax/xhr - effects - offset - dimensions - outro -}.map { |js| File.join( src_dir, "#{js}.js" ) } - -# Sizzle, QUnit and jQuery files/dirs -sizzle_dir = File.join( src_dir, "sizzle" ) -sizzle = File.join( sizzle_dir, "sizzle.js" ) -selector = File.join( src_dir, "selector.js" ) - -qunit_dir = File.join( test_dir, "qunit" ) -qunit = File.join( qunit_dir, "qunit", "qunit.js" ) - -jq = File.join( dist_dir, "jquery.js" ) -jq_min = File.join( dist_dir, "jquery.min.js" ) - -# General Variables -date = `git log -1`[/^Date:\s+(.+)$/, 1] -version = File.read( File.join( prefix, 'version.txt' ) ).strip - -# Build tools -rhino = "java -jar #{build_dir}/js.jar" -minfier = "java -jar #{build_dir}/google-compiler-20100917.jar" - -# Turn off output other than needed from `sh` and file commands -verbose(false) - -# Tasks -task :default => "all" - -desc "Builds jQuery; Tests with JSLint; Minifies jQuery" -task :all => [:jquery, :lint, :min] do - puts "jQuery build complete." -end - -desc "Builds jQuery: jquery.js (Default task)" -task :jquery => [:selector, jq] - -desc "Builds a minified version of jQuery: jquery.min.js" -task :min => jq_min - - -task :init => [sizzle, qunit] do - sizzle_git = File.join(sizzle_dir, '.git') - qunit_git = File.join(qunit_dir, '.git') - - puts "Updating SizzleJS with latest..." - sh "git --git-dir=#{sizzle_git} pull -q origin master" - - puts "Updating QUnit with latest..." - sh "git --git-dir=#{qunit_git} pull -q origin master" -end - -desc "Removes dist folder, selector.js, and Sizzle/QUnit" -task :clean do - puts "Removing Distribution directory: #{dist_dir}..." - rm_rf dist_dir - - puts "Removing built copy of Sizzle..." - rm_rf selector - - puts "Removing cloned directories..." - rm_rf qunit_dir - rm_rf sizzle_dir -end - -desc "Rebuilds selector.js from SizzleJS" -task :selector => [:init, selector] - -desc "Tests built jquery.js against JSLint" -task :lint => jq do - puts "Checking jQuery against JSLint..." - sh "#{rhino} " + File.join(build_dir, 'jslint-check.js') -end - - -# File and Directory Dependencies -directory dist_dir - -file jq => [dist_dir, base_files].flatten do - puts "Building jquery.js..." - - File.open(jq, 'w') do |f| - f.write cat(base_files). - gsub(/@DATE/, date). - gsub(/@VERSION/, version). - gsub(/.function..jQuery...\{/, ''). - gsub(/\}...jQuery..;/, '') - end -end - -file jq_min => jq do - puts "Building jquery.min.js..." - - sh "#{minfier} --js #{jq} --warning_level QUIET --js_output_file #{jq_min}" - - min = File.read( jq_min ) - - # Equivilent of "head" - File.open(jq_min, 'w') do |f| - f.write File.readlines(jq)[0..14].join() - f.write min - end -end - -file selector => [sizzle, :init] do - puts "Building selector code from Sizzle..." - - File.open(selector, 'w') do |f| - f.write File.read(sizzle).gsub( - /^.+EXPOSE$\n/, - '\0' + File.read( File.join( src_dir, 'sizzle-jquery.js' )) - ).gsub( - /^window.Sizzle.+$\n/, '' - ) - end -end - -file sizzle do - puts "Retrieving SizzleJS from Github..." - sh "git clone git://github.com/jeresig/sizzle.git #{sizzle_dir}" -end - -file qunit do - puts "Retrieving QUnit from Github..." - sh "git clone git://github.com/jquery/qunit.git #{qunit_dir}" -end - - -def cat( files ) - files.map do |file| - File.read(file) - end.join('') -end diff --git a/build.xml b/build.xml deleted file mode 100644 index 87b31e192..000000000 --- a/build.xml +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/build/google-compiler-20100917.jar b/build/google-compiler-20100917.jar deleted file mode 100644 index 4dfa5ad0b..000000000 Binary files a/build/google-compiler-20100917.jar and /dev/null differ diff --git a/build/js.jar b/build/js.jar deleted file mode 100644 index a76cc7c6b..000000000 Binary files a/build/js.jar and /dev/null differ diff --git a/build/jslint-check.js b/build/jslint-check.js index 976975a26..72d670187 100644 --- a/build/jslint-check.js +++ b/build/jslint-check.js @@ -1,6 +1,6 @@ -load("build/jslint.js"); - -var src = readFile("dist/jquery.js"); +var JSLINT = require("./lib/jslint").JSLINT, + print = require("sys").print, + src = require("fs").readFileSync("dist/jquery.js", "utf8"); JSLINT(src, { evil: true, forin: true, maxerr: 100 }); @@ -29,8 +29,8 @@ for ( var i = 0; i < e.length; i++ ) { } if ( found > 0 ) { - print( "\n" + found + " Error(s) found." ); + print( "\n" + found + " Error(s) found.\n" ); } else { - print( "JSLint check passed." ); + print( "JSLint check passed.\n" ); } diff --git a/build/jslint.js b/build/lib/jslint.js similarity index 99% rename from build/jslint.js rename to build/lib/jslint.js index f629fec03..f563292bf 100644 --- a/build/jslint.js +++ b/build/lib/jslint.js @@ -5495,6 +5495,10 @@ loop: for (;;) { itself.edition = '2010-02-20'; + if (typeof exports !== "undefined") { + exports.JSLINT = itself; + } + return itself; -}()); \ No newline at end of file +}()); diff --git a/build/lib/parse-js.js b/build/lib/parse-js.js new file mode 100644 index 000000000..7e4fd0e27 --- /dev/null +++ b/build/lib/parse-js.js @@ -0,0 +1,1239 @@ +/*********************************************************************** + + A JavaScript tokenizer / parser / beautifier / compressor. + + This version is suitable for Node.js. With minimal changes (the + exports stuff) it should work on any JS platform. + + This file contains the tokenizer/parser. It is a port to JavaScript + of parse-js [1], a JavaScript parser library written in Common Lisp + by Marijn Haverbeke. Thank you Marijn! + + [1] http://marijn.haverbeke.nl/parse-js/ + + Exported functions: + + - tokenizer(code) -- returns a function. Call the returned + function to fetch the next token. + + - parse(code) -- returns an AST of the given JavaScript code. + + -------------------------------- (C) --------------------------------- + + Author: Mihai Bazon + + http://mihai.bazon.net/blog + + Distributed under the BSD license: + + Copyright 2010 (c) Mihai Bazon + Based on parse-js (http://marijn.haverbeke.nl/parse-js/). + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + * Redistributions of source code must retain the above + copyright notice, this list of conditions and the following + disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials + provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER “AS IS” AND ANY + EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + SUCH DAMAGE. + + ***********************************************************************/ + +/* -----[ Tokenizer (constants) ]----- */ + +var KEYWORDS = array_to_hash([ + "break", + "case", + "catch", + "const", + "continue", + "default", + "delete", + "do", + "else", + "finally", + "for", + "function", + "if", + "in", + "instanceof", + "new", + "return", + "switch", + "throw", + "try", + "typeof", + "var", + "void", + "while", + "with" +]); + +var RESERVED_WORDS = array_to_hash([ + "abstract", + "boolean", + "byte", + "char", + "class", + "debugger", + "double", + "enum", + "export", + "extends", + "final", + "float", + "goto", + "implements", + "import", + "int", + "interface", + "long", + "native", + "package", + "private", + "protected", + "public", + "short", + "static", + "super", + "synchronized", + "throws", + "transient", + "volatile" +]); + +var KEYWORDS_BEFORE_EXPRESSION = array_to_hash([ + "return", + "new", + "delete", + "throw", + "else", + "case" +]); + +var KEYWORDS_ATOM = array_to_hash([ + "false", + "null", + "true", + "undefined" +]); + +var OPERATOR_CHARS = array_to_hash(characters("+-*&%=<>!?|~^")); + +var RE_HEX_NUMBER = /^0x[0-9a-f]+$/i; +var RE_OCT_NUMBER = /^0[0-7]+$/; +var RE_DEC_NUMBER = /^\d*\.?\d*(?:e[+-]?\d*(?:\d\.?|\.?\d)\d*)?$/i; + +var OPERATORS = array_to_hash([ + "in", + "instanceof", + "typeof", + "new", + "void", + "delete", + "++", + "--", + "+", + "-", + "!", + "~", + "&", + "|", + "^", + "*", + "/", + "%", + ">>", + "<<", + ">>>", + "<", + ">", + "<=", + ">=", + "==", + "===", + "!=", + "!==", + "?", + "=", + "+=", + "-=", + "/=", + "*=", + "%=", + ">>=", + "<<=", + ">>>=", + "~=", + "%=", + "|=", + "^=", + "&=", + "&&", + "||" +]); + +var WHITESPACE_CHARS = array_to_hash(characters(" \n\r\t")); + +var PUNC_BEFORE_EXPRESSION = array_to_hash(characters("[{}(,.;:")); + +var PUNC_CHARS = array_to_hash(characters("[]{}(),;:")); + +var REGEXP_MODIFIERS = array_to_hash(characters("gmsiy")); + +/* -----[ Tokenizer ]----- */ + +function is_alphanumeric_char(ch) { + ch = ch.charCodeAt(0); + return (ch >= 48 && ch <= 57) || + (ch >= 65 && ch <= 90) || + (ch >= 97 && ch <= 122); +}; + +function is_identifier_char(ch) { + return is_alphanumeric_char(ch) || ch == "$" || ch == "_"; +}; + +function is_digit(ch) { + ch = ch.charCodeAt(0); + return ch >= 48 && ch <= 57; +}; + +function parse_js_number(num) { + if (RE_HEX_NUMBER.test(num)) { + return parseInt(num.substr(2), 16); + } else if (RE_OCT_NUMBER.test(num)) { + return parseInt(num.substr(1), 8); + } else if (RE_DEC_NUMBER.test(num)) { + return parseFloat(num); + } +}; + +function JS_Parse_Error(message, line, col, pos) { + this.message = message; + this.line = line; + this.col = col; + this.pos = pos; + try { + ({})(); + } catch(ex) { + this.stack = ex.stack; + }; +}; + +JS_Parse_Error.prototype.toString = function() { + return this.message + " (line: " + this.line + ", col: " + this.col + ", pos: " + this.pos + ")" + "\n\n" + this.stack; +}; + +function js_error(message, line, col, pos) { + throw new JS_Parse_Error(message, line, col, pos); +}; + +function is_token(token, type, val) { + return token.type == type && (val == null || token.value == val); +}; + +var EX_EOF = {}; + +function tokenizer($TEXT, skip_comments) { + + var S = { + text : $TEXT.replace(/\r\n?|[\n\u2028\u2029]/g, "\n").replace(/^\uFEFF/, ''), + pos : 0, + tokpos : 0, + line : 0, + tokline : 0, + col : 0, + tokcol : 0, + newline_before : false, + regex_allowed : false + }; + + function peek() { return S.text.charAt(S.pos); }; + + function next(signal_eof) { + var ch = S.text.charAt(S.pos++); + if (signal_eof && !ch) + throw EX_EOF; + if (ch == "\n") { + S.newline_before = true; + ++S.line; + S.col = 0; + } else { + ++S.col; + } + return ch; + }; + + function eof() { + return !S.peek(); + }; + + function find(what, signal_eof) { + var pos = S.text.indexOf(what, S.pos); + if (signal_eof && pos == -1) throw EX_EOF; + return pos; + }; + + function start_token() { + S.tokline = S.line; + S.tokcol = S.col; + S.tokpos = S.pos; + }; + + function token(type, value) { + S.regex_allowed = ((type == "operator" && !HOP(UNARY_POSTFIX, value)) || + (type == "keyword" && HOP(KEYWORDS_BEFORE_EXPRESSION, value)) || + (type == "punc" && HOP(PUNC_BEFORE_EXPRESSION, value))); + var ret = { + type : type, + value : value, + line : S.tokline, + col : S.tokcol, + pos : S.tokpos, + nlb : S.newline_before + }; + S.newline_before = false; + return ret; + }; + + function skip_whitespace() { + while (HOP(WHITESPACE_CHARS, peek())) + next(); + }; + + function read_while(pred) { + var ret = "", ch = peek(), i = 0; + while (ch && pred(ch, i++)) { + ret += next(); + ch = peek(); + } + return ret; + }; + + function parse_error(err) { + js_error(err, S.tokline, S.tokcol, S.tokpos); + }; + + function read_num(prefix) { + var has_e = false, after_e = false, has_x = false, has_dot = prefix == "."; + var num = read_while(function(ch, i){ + if (ch == "x" || ch == "X") { + if (has_x) return false; + return has_x = true; + } + if (!has_x && (ch == "E" || ch == "e")) { + if (has_e) return false; + return has_e = after_e = true; + } + if (ch == "-") { + if (after_e || (i == 0 && !prefix)) return true; + return false; + } + if (ch == "+") return after_e; + after_e = false; + if (ch == ".") { + if (!has_dot) + return has_dot = true; + return false; + } + return is_alphanumeric_char(ch); + }); + if (prefix) + num = prefix + num; + var valid = parse_js_number(num); + if (!isNaN(valid)) { + return token("num", valid); + } else { + parse_error("Invalid syntax: " + num); + } + }; + + function read_escaped_char() { + var ch = next(true); + switch (ch) { + case "n" : return "\n"; + case "r" : return "\r"; + case "t" : return "\t"; + case "b" : return "\b"; + case "v" : return "\v"; + case "f" : return "\f"; + case "0" : return "\0"; + case "x" : return String.fromCharCode(hex_bytes(2)); + case "u" : return String.fromCharCode(hex_bytes(4)); + default : return ch; + } + }; + + function hex_bytes(n) { + var num = 0; + for (; n > 0; --n) { + var digit = parseInt(next(true), 16); + if (isNaN(digit)) + parse_error("Invalid hex-character pattern in string"); + num = (num << 4) | digit; + } + return num; + }; + + function read_string() { + return with_eof_error("Unterminated string constant", function(){ + var quote = next(), ret = ""; + for (;;) { + var ch = next(true); + if (ch == "\\") ch = read_escaped_char(); + else if (ch == quote) break; + ret += ch; + } + return token("string", ret); + }); + }; + + function read_line_comment() { + next(); + var i = find("\n"), ret; + if (i == -1) { + ret = S.text.substr(S.pos); + S.pos = S.text.length; + } else { + ret = S.text.substring(S.pos, i); + S.pos = i; + } + return token("comment1", ret); + }; + + function read_multiline_comment() { + next(); + return with_eof_error("Unterminated multiline comment", function(){ + var i = find("*/", true), + text = S.text.substring(S.pos, i), + tok = token("comment2", text); + S.pos = i + 2; + S.line += text.split("\n").length - 1; + S.newline_before = text.indexOf("\n") >= 0; + return tok; + }); + }; + + function read_regexp() { + return with_eof_error("Unterminated regular expression", function(){ + var prev_backslash = false, regexp = "", ch, in_class = false; + while ((ch = next(true))) if (prev_backslash) { + regexp += "\\" + ch; + prev_backslash = false; + } else if (ch == "[") { + in_class = true; + regexp += ch; + } else if (ch == "]" && in_class) { + in_class = false; + regexp += ch; + } else if (ch == "/" && !in_class) { + break; + } else if (ch == "\\") { + prev_backslash = true; + } else { + regexp += ch; + } + var mods = read_while(function(ch){ + return HOP(REGEXP_MODIFIERS, ch); + }); + return token("regexp", [ regexp, mods ]); + }); + }; + + function read_operator(prefix) { + function grow(op) { + var bigger = op + peek(); + if (HOP(OPERATORS, bigger)) { + next(); + return grow(bigger); + } else { + return op; + } + }; + return token("operator", grow(prefix || next())); + }; + + var handle_slash = skip_comments ? function() { + next(); + var regex_allowed = S.regex_allowed; + switch (peek()) { + case "/": read_line_comment(); S.regex_allowed = regex_allowed; return next_token(); + case "*": read_multiline_comment(); S.regex_allowed = regex_allowed; return next_token(); + } + return S.regex_allowed ? read_regexp() : read_operator("/"); + } : function() { + next(); + switch (peek()) { + case "/": return read_line_comment(); + case "*": return read_multiline_comment(); + } + return S.regex_allowed ? read_regexp() : read_operator("/"); + }; + + function handle_dot() { + next(); + return is_digit(peek()) + ? read_num(".") + : token("punc", "."); + }; + + function read_word() { + var word = read_while(is_identifier_char); + return !HOP(KEYWORDS, word) + ? token("name", word) + : HOP(OPERATORS, word) + ? token("operator", word) + : HOP(KEYWORDS_ATOM, word) + ? token("atom", word) + : token("keyword", word); + }; + + function with_eof_error(eof_error, cont) { + try { + return cont(); + } catch(ex) { + if (ex === EX_EOF) parse_error(eof_error); + else throw ex; + } + }; + + function next_token(force_regexp) { + if (force_regexp) + return read_regexp(); + skip_whitespace(); + start_token(); + var ch = peek(); + if (!ch) return token("eof"); + if (is_digit(ch)) return read_num(); + if (ch == '"' || ch == "'") return read_string(); + if (HOP(PUNC_CHARS, ch)) return token("punc", next()); + if (ch == ".") return handle_dot(); + if (ch == "/") return handle_slash(); + if (HOP(OPERATOR_CHARS, ch)) return read_operator(); + if (is_identifier_char(ch)) return read_word(); + parse_error("Unexpected character '" + ch + "'"); + }; + + next_token.context = function(nc) { + if (nc) S = nc; + return S; + }; + + return next_token; + +}; + +/* -----[ Parser (constants) ]----- */ + +var UNARY_PREFIX = array_to_hash([ + "typeof", + "void", + "delete", + "--", + "++", + "!", + "~", + "-", + "+" +]); + +var UNARY_POSTFIX = array_to_hash([ "--", "++" ]); + +var ASSIGNMENT = (function(a, ret, i){ + while (i < a.length) { + ret[a[i]] = a[i].substr(0, a[i].length - 1); + i++; + } + return ret; +})( + ["+=", "-=", "/=", "*=", "%=", ">>=", "<<=", ">>>=", "~=", "%=", "|=", "^=", "&="], + { "=": true }, + 0 +); + +var PRECEDENCE = (function(a, ret){ + for (var i = 0, n = 1; i < a.length; ++i, ++n) { + var b = a[i]; + for (var j = 0; j < b.length; ++j) { + ret[b[j]] = n; + } + } + return ret; +})( + [ + ["||"], + ["&&"], + ["|"], + ["^"], + ["&"], + ["==", "===", "!=", "!=="], + ["<", ">", "<=", ">=", "in", "instanceof"], + [">>", "<<", ">>>"], + ["+", "-"], + ["*", "/", "%"] + ], + {} +); + +var STATEMENTS_WITH_LABELS = array_to_hash([ "for", "do", "while", "switch" ]); + +var ATOMIC_START_TOKEN = array_to_hash([ "atom", "num", "string", "regexp", "name" ]); + +/* -----[ Parser ]----- */ + +function NodeWithToken(str, start, end) { + this.name = str; + this.start = start; + this.end = end; +}; + +NodeWithToken.prototype.toString = function() { return this.name; }; + +function parse($TEXT, strict_mode, embed_tokens) { + + var S = { + input: tokenizer($TEXT, true), + token: null, + prev: null, + peeked: null, + in_function: 0, + in_loop: 0, + labels: [] + }; + + S.token = next(); + + function is(type, value) { + return is_token(S.token, type, value); + }; + + function peek() { return S.peeked || (S.peeked = S.input()); }; + + function next() { + S.prev = S.token; + if (S.peeked) { + S.token = S.peeked; + S.peeked = null; + } else { + S.token = S.input(); + } + return S.token; + }; + + function prev() { + return S.prev; + }; + + function croak(msg, line, col, pos) { + var ctx = S.input.context(); + js_error(msg, + line != null ? line : ctx.tokline, + col != null ? col : ctx.tokcol, + pos != null ? pos : ctx.tokpos); + }; + + function token_error(token, msg) { + croak(msg, token.line, token.col); + }; + + function unexpected(token) { + if (token == null) + token = S.token; + token_error(token, "Unexpected token: " + token.type + " (" + token.value + ")"); + }; + + function expect_token(type, val) { + if (is(type, val)) { + return next(); + } + token_error(S.token, "Unexpected token " + S.token.type + ", expected " + type); + }; + + function expect(punc) { return expect_token("punc", punc); }; + + function can_insert_semicolon() { + return !strict_mode && ( + S.token.nlb || is("eof") || is("punc", "}") + ); + }; + + function semicolon() { + if (is("punc", ";")) next(); + else if (!can_insert_semicolon()) unexpected(); + }; + + function as() { + return slice(arguments); + }; + + function parenthesised() { + expect("("); + var ex = expression(); + expect(")"); + return ex; + }; + + function add_tokens(str, start, end) { + return new NodeWithToken(str, start, end); + }; + + var statement = embed_tokens ? function() { + var start = S.token; + var stmt = $statement(); + stmt[0] = add_tokens(stmt[0], start, prev()); + return stmt; + } : $statement; + + function $statement() { + if (is("operator", "/")) { + S.peeked = null; + S.token = S.input(true); // force regexp + } + switch (S.token.type) { + case "num": + case "string": + case "regexp": + case "operator": + case "atom": + return simple_statement(); + + case "name": + return is_token(peek(), "punc", ":") + ? labeled_statement(prog1(S.token.value, next, next)) + : simple_statement(); + + case "punc": + switch (S.token.value) { + case "{": + return as("block", block_()); + case "[": + case "(": + return simple_statement(); + case ";": + next(); + return as("block"); + default: + unexpected(); + } + + case "keyword": + switch (prog1(S.token.value, next)) { + case "break": + return break_cont("break"); + + case "continue": + return break_cont("continue"); + + case "debugger": + semicolon(); + return as("debugger"); + + case "do": + return (function(body){ + expect_token("keyword", "while"); + return as("do", prog1(parenthesised, semicolon), body); + })(in_loop(statement)); + + case "for": + return for_(); + + case "function": + return function_(true); + + case "if": + return if_(); + + case "return": + if (S.in_function == 0) + croak("'return' outside of function"); + return as("return", + is("punc", ";") + ? (next(), null) + : can_insert_semicolon() + ? null + : prog1(expression, semicolon)); + + case "switch": + return as("switch", parenthesised(), switch_block_()); + + case "throw": + return as("throw", prog1(expression, semicolon)); + + case "try": + return try_(); + + case "var": + return prog1(var_, semicolon); + + case "const": + return prog1(const_, semicolon); + + case "while": + return as("while", parenthesised(), in_loop(statement)); + + case "with": + return as("with", parenthesised(), statement()); + + default: + unexpected(); + } + } + }; + + function labeled_statement(label) { + S.labels.push(label); + var start = S.token, stat = statement(); + if (strict_mode && !HOP(STATEMENTS_WITH_LABELS, stat[0])) + unexpected(start); + S.labels.pop(); + return as("label", label, stat); + }; + + function simple_statement() { + return as("stat", prog1(expression, semicolon)); + }; + + function break_cont(type) { + var name = is("name") ? S.token.value : null; + if (name != null) { + next(); + if (!member(name, S.labels)) + croak("Label " + name + " without matching loop or statement"); + } + else if (S.in_loop == 0) + croak(type + " not inside a loop or switch"); + semicolon(); + return as(type, name); + }; + + function for_() { + expect("("); + var has_var = is("keyword", "var"); + if (has_var) + next(); + if (is("name") && is_token(peek(), "operator", "in")) { + // for (i in foo) + var name = S.token.value; + next(); next(); + var obj = expression(); + expect(")"); + return as("for-in", has_var, name, obj, in_loop(statement)); + } else { + // classic for + var init = is("punc", ";") ? null : has_var ? var_() : expression(); + expect(";"); + var test = is("punc", ";") ? null : expression(); + expect(";"); + var step = is("punc", ")") ? null : expression(); + expect(")"); + return as("for", init, test, step, in_loop(statement)); + } + }; + + function function_(in_statement) { + var name = is("name") ? prog1(S.token.value, next) : null; + if (in_statement && !name) + unexpected(); + expect("("); + return as(in_statement ? "defun" : "function", + name, + // arguments + (function(first, a){ + while (!is("punc", ")")) { + if (first) first = false; else expect(","); + if (!is("name")) unexpected(); + a.push(S.token.value); + next(); + } + next(); + return a; + })(true, []), + // body + (function(){ + ++S.in_function; + var loop = S.in_loop; + S.in_loop = 0; + var a = block_(); + --S.in_function; + S.in_loop = loop; + return a; + })()); + }; + + function if_() { + var cond = parenthesised(), body = statement(), belse; + if (is("keyword", "else")) { + next(); + belse = statement(); + } + return as("if", cond, body, belse); + }; + + function block_() { + expect("{"); + var a = []; + while (!is("punc", "}")) { + if (is("eof")) unexpected(); + a.push(statement()); + } + next(); + return a; + }; + + var switch_block_ = curry(in_loop, function(){ + expect("{"); + var a = [], cur = null; + while (!is("punc", "}")) { + if (is("eof")) unexpected(); + if (is("keyword", "case")) { + next(); + cur = []; + a.push([ expression(), cur ]); + expect(":"); + } + else if (is("keyword", "default")) { + next(); + expect(":"); + cur = []; + a.push([ null, cur ]); + } + else { + if (!cur) unexpected(); + cur.push(statement()); + } + } + next(); + return a; + }); + + function try_() { + var body = block_(), bcatch, bfinally; + if (is("keyword", "catch")) { + next(); + expect("("); + if (!is("name")) + croak("Name expected"); + var name = S.token.value; + next(); + expect(")"); + bcatch = [ name, block_() ]; + } + if (is("keyword", "finally")) { + next(); + bfinally = block_(); + } + if (!bcatch && !bfinally) + croak("Missing catch/finally blocks"); + return as("try", body, bcatch, bfinally); + }; + + function vardefs() { + var a = []; + for (;;) { + if (!is("name")) + unexpected(); + var name = S.token.value; + next(); + if (is("operator", "=")) { + next(); + a.push([ name, expression(false) ]); + } else { + a.push([ name ]); + } + if (!is("punc", ",")) + break; + next(); + } + return a; + }; + + function var_() { + return as("var", vardefs()); + }; + + function const_() { + return as("const", vardefs()); + }; + + function new_() { + var newexp = expr_atom(false), args; + if (is("punc", "(")) { + next(); + args = expr_list(")"); + } else { + args = []; + } + return subscripts(as("new", newexp, args), true); + }; + + function expr_atom(allow_calls) { + if (is("operator", "new")) { + next(); + return new_(); + } + if (is("operator") && HOP(UNARY_PREFIX, S.token.value)) { + return make_unary("unary-prefix", + prog1(S.token.value, next), + expr_atom(allow_calls)); + } + if (is("punc")) { + switch (S.token.value) { + case "(": + next(); + return subscripts(prog1(expression, curry(expect, ")")), allow_calls); + case "[": + next(); + return subscripts(array_(), allow_calls); + case "{": + next(); + return subscripts(object_(), allow_calls); + } + unexpected(); + } + if (is("keyword", "function")) { + next(); + return subscripts(function_(false), allow_calls); + } + if (HOP(ATOMIC_START_TOKEN, S.token.type)) { + var atom = S.token.type == "regexp" + ? as("regexp", S.token.value[0], S.token.value[1]) + : as(S.token.type, S.token.value); + return subscripts(prog1(atom, next), allow_calls); + } + unexpected(); + }; + + function expr_list(closing, allow_trailing_comma) { + var first = true, a = []; + while (!is("punc", closing)) { + if (first) first = false; else expect(","); + if (allow_trailing_comma && is("punc", closing)) + break; + a.push(expression(false)); + } + next(); + return a; + }; + + function array_() { + return as("array", expr_list("]", !strict_mode)); + }; + + function object_() { + var first = true, a = []; + while (!is("punc", "}")) { + if (first) first = false; else expect(","); + if (!strict_mode && is("punc", "}")) + // allow trailing comma + break; + var type = S.token.type; + var name = as_property_name(); + if (type == "name" && (name == "get" || name == "set") && !is("punc", ":")) { + a.push([ as_name(), function_(false), name ]); + } else { + expect(":"); + a.push([ name, expression(false) ]); + } + } + next(); + return as("object", a); + }; + + function as_property_name() { + switch (S.token.type) { + case "num": + case "string": + return prog1(S.token.value, next); + } + return as_name(); + }; + + function as_name() { + switch (S.token.type) { + case "name": + case "operator": + case "keyword": + case "atom": + return prog1(S.token.value, next); + default: + unexpected(); + } + }; + + function subscripts(expr, allow_calls) { + if (is("punc", ".")) { + next(); + return subscripts(as("dot", expr, as_name()), allow_calls); + } + if (is("punc", "[")) { + next(); + return subscripts(as("sub", expr, prog1(expression, curry(expect, "]"))), allow_calls); + } + if (allow_calls && is("punc", "(")) { + next(); + return subscripts(as("call", expr, expr_list(")")), true); + } + if (allow_calls && is("operator") && HOP(UNARY_POSTFIX, S.token.value)) { + return prog1(curry(make_unary, "unary-postfix", S.token.value, expr), + next); + } + return expr; + }; + + function make_unary(tag, op, expr) { + if ((op == "++" || op == "--") && !is_assignable(expr)) + croak("Invalid use of " + op + " operator"); + return as(tag, op, expr); + }; + + function expr_op(left, min_prec) { + var op = is("operator") ? S.token.value : null; + var prec = op != null ? PRECEDENCE[op] : null; + if (prec != null && prec > min_prec) { + next(); + var right = expr_op(expr_atom(true), prec); + return expr_op(as("binary", op, left, right), min_prec); + } + return left; + }; + + function expr_ops() { + return expr_op(expr_atom(true), 0); + }; + + function maybe_conditional() { + var expr = expr_ops(); + if (is("operator", "?")) { + next(); + var yes = expression(false); + expect(":"); + return as("conditional", expr, yes, expression(false)); + } + return expr; + }; + + function is_assignable(expr) { + switch (expr[0]) { + case "dot": + case "sub": + return true; + case "name": + return expr[1] != "this"; + } + }; + + function maybe_assign() { + var left = maybe_conditional(), val = S.token.value; + if (is("operator") && HOP(ASSIGNMENT, val)) { + if (is_assignable(left)) { + next(); + return as("assign", ASSIGNMENT[val], left, maybe_assign()); + } + croak("Invalid assignment"); + } + return left; + }; + + function expression(commas) { + if (arguments.length == 0) + commas = true; + var expr = maybe_assign(); + if (commas && is("punc", ",")) { + next(); + return as("seq", expr, expression()); + } + return expr; + }; + + function in_loop(cont) { + try { + ++S.in_loop; + return cont(); + } finally { + --S.in_loop; + } + }; + + return as("toplevel", (function(a){ + while (!is("eof")) + a.push(statement()); + return a; + })([])); + +}; + +/* -----[ Utilities ]----- */ + +function curry(f) { + var args = slice(arguments, 1); + return function() { return f.apply(this, args.concat(slice(arguments))); }; +}; + +function prog1(ret) { + if (ret instanceof Function) + ret = ret(); + for (var i = 1, n = arguments.length; --n > 0; ++i) + arguments[i](); + return ret; +}; + +function array_to_hash(a) { + var ret = {}; + for (var i = 0; i < a.length; ++i) + ret[a[i]] = true; + return ret; +}; + +function slice(a, start) { + return Array.prototype.slice.call(a, start == null ? 0 : start); +}; + +function characters(str) { + return str.split(""); +}; + +function member(name, array) { + for (var i = array.length; --i >= 0;) + if (array[i] === name) + return true; + return false; +}; + +function HOP(obj, prop) { + return Object.prototype.hasOwnProperty.call(obj, prop); +}; + +/* -----[ Exports ]----- */ + +exports.tokenizer = tokenizer; +exports.parse = parse; +exports.slice = slice; +exports.curry = curry; +exports.member = member; +exports.array_to_hash = array_to_hash; +exports.PRECEDENCE = PRECEDENCE; +exports.KEYWORDS_ATOM = KEYWORDS_ATOM; +exports.RESERVED_WORDS = RESERVED_WORDS; +exports.KEYWORDS = KEYWORDS; +exports.ATOMIC_START_TOKEN = ATOMIC_START_TOKEN; +exports.OPERATORS = OPERATORS; +exports.is_alphanumeric_char = is_alphanumeric_char; diff --git a/build/lib/process.js b/build/lib/process.js new file mode 100644 index 000000000..edcf599dc --- /dev/null +++ b/build/lib/process.js @@ -0,0 +1,1562 @@ +/*********************************************************************** + + A JavaScript tokenizer / parser / beautifier / compressor. + + This version is suitable for Node.js. With minimal changes (the + exports stuff) it should work on any JS platform. + + This file implements some AST processors. They work on data built + by parse-js. + + Exported functions: + + - ast_mangle(ast, include_toplevel) -- mangles the + variable/function names in the AST. Returns an AST. Pass true + as second argument to mangle toplevel names too. + + - ast_squeeze(ast) -- employs various optimizations to make the + final generated code even smaller. Returns an AST. + + - gen_code(ast, beautify) -- generates JS code from the AST. Pass + true (or an object, see the code for some options) as second + argument to get "pretty" (indented) code. + + -------------------------------- (C) --------------------------------- + + Author: Mihai Bazon + + http://mihai.bazon.net/blog + + Distributed under the BSD license: + + Copyright 2010 (c) Mihai Bazon + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + * Redistributions of source code must retain the above + copyright notice, this list of conditions and the following + disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials + provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER “AS IS” AND ANY + EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + SUCH DAMAGE. + + ***********************************************************************/ + +var jsp = require("./parse-js"), + slice = jsp.slice, + member = jsp.member, + PRECEDENCE = jsp.PRECEDENCE, + OPERATORS = jsp.OPERATORS; + +/* -----[ helper for AST traversal ]----- */ + +function ast_walker(ast) { + function _vardefs(defs) { + return MAP(defs, function(def){ + var a = [ def[0] ]; + if (def.length > 1) + a[1] = walk(def[1]); + return a; + }); + }; + var walkers = { + "string": function(str) { + return [ "string", str ]; + }, + "num": function(num) { + return [ "num", num ]; + }, + "name": function(name) { + return [ "name", name ]; + }, + "toplevel": function(statements) { + return [ "toplevel", MAP(statements, walk) ]; + }, + "block": function(statements) { + var out = [ "block" ]; + if (statements != null) + out.push(MAP(statements, walk)); + return out; + }, + "var": function(defs) { + return [ "var", _vardefs(defs) ]; + }, + "const": function(defs) { + return [ "const", _vardefs(defs) ]; + }, + "try": function(t, c, f) { + return [ + "try", + MAP(t, walk), + c != null ? [ c[0], MAP(c[1], walk) ] : null, + f != null ? MAP(f, walk) : null + ]; + }, + "throw": function(expr) { + return [ "throw", walk(expr) ]; + }, + "new": function(ctor, args) { + return [ "new", walk(ctor), MAP(args, walk) ]; + }, + "switch": function(expr, body) { + return [ "switch", walk(expr), MAP(body, function(branch){ + return [ branch[0] ? walk(branch[0]) : null, + MAP(branch[1], walk) ]; + }) ]; + }, + "break": function(label) { + return [ "break", label ]; + }, + "continue": function(label) { + return [ "continue", label ]; + }, + "conditional": function(cond, t, e) { + return [ "conditional", walk(cond), walk(t), walk(e) ]; + }, + "assign": function(op, lvalue, rvalue) { + return [ "assign", op, walk(lvalue), walk(rvalue) ]; + }, + "dot": function(expr) { + return [ "dot", walk(expr) ].concat(slice(arguments, 1)); + }, + "call": function(expr, args) { + return [ "call", walk(expr), MAP(args, walk) ]; + }, + "function": function(name, args, body) { + return [ "function", name, args.slice(), MAP(body, walk) ]; + }, + "defun": function(name, args, body) { + return [ "defun", name, args.slice(), MAP(body, walk) ]; + }, + "if": function(conditional, t, e) { + return [ "if", walk(conditional), walk(t), walk(e) ]; + }, + "for": function(init, cond, step, block) { + return [ "for", walk(init), walk(cond), walk(step), walk(block) ]; + }, + "for-in": function(has_var, key, hash, block) { + return [ "for-in", has_var, key, walk(hash), walk(block) ]; + }, + "while": function(cond, block) { + return [ "while", walk(cond), walk(block) ]; + }, + "do": function(cond, block) { + return [ "do", walk(cond), walk(block) ]; + }, + "return": function(expr) { + return [ "return", walk(expr) ]; + }, + "binary": function(op, left, right) { + return [ "binary", op, walk(left), walk(right) ]; + }, + "unary-prefix": function(op, expr) { + return [ "unary-prefix", op, walk(expr) ]; + }, + "unary-postfix": function(op, expr) { + return [ "unary-postfix", op, walk(expr) ]; + }, + "sub": function(expr, subscript) { + return [ "sub", walk(expr), walk(subscript) ]; + }, + "object": function(props) { + return [ "object", MAP(props, function(p){ + return p.length == 2 + ? [ p[0], walk(p[1]) ] + : [ p[0], walk(p[1]), p[2] ]; // get/set-ter + }) ]; + }, + "regexp": function(rx, mods) { + return [ "regexp", rx, mods ]; + }, + "array": function(elements) { + return [ "array", MAP(elements, walk) ]; + }, + "stat": function(stat) { + return [ "stat", walk(stat) ]; + }, + "seq": function() { + return [ "seq" ].concat(MAP(slice(arguments), walk)); + }, + "label": function(name, block) { + return [ "label", name, walk(block) ]; + }, + "with": function(expr, block) { + return [ "with", walk(expr), walk(block) ]; + }, + "atom": function(name) { + return [ "atom", name ]; + } + }; + + var user = {}; + var stack = []; + function walk(ast) { + if (ast == null) + return null; + try { + stack.push(ast); + var type = ast[0]; + var gen = user[type]; + if (gen) { + var ret = gen.apply(ast, ast.slice(1)); + if (ret != null) + return ret; + } + gen = walkers[type]; + return gen.apply(ast, ast.slice(1)); + } finally { + stack.pop(); + } + }; + + function with_walkers(walkers, cont){ + var save = {}, i; + for (i in walkers) if (HOP(walkers, i)) { + save[i] = user[i]; + user[i] = walkers[i]; + } + var ret = cont(); + for (i in save) if (HOP(save, i)) { + if (!save[i]) delete user[i]; + else user[i] = save[i]; + } + return ret; + }; + + return { + walk: walk, + with_walkers: with_walkers, + parent: function() { + return stack[stack.length - 2]; // last one is current node + }, + stack: function() { + return stack; + } + }; +}; + +/* -----[ Scope and mangling ]----- */ + +function Scope(parent) { + this.names = {}; // names defined in this scope + this.mangled = {}; // mangled names (orig.name => mangled) + this.rev_mangled = {}; // reverse lookup (mangled => orig.name) + this.cname = -1; // current mangled name + this.refs = {}; // names referenced from this scope + this.uses_with = false; // will become TRUE if eval() is detected in this or any subscopes + this.uses_eval = false; // will become TRUE if with() is detected in this or any subscopes + this.parent = parent; // parent scope + this.children = []; // sub-scopes + if (parent) { + this.level = parent.level + 1; + parent.children.push(this); + } else { + this.level = 0; + } +}; + +var base54 = (function(){ + var DIGITS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$_"; + return function(num) { + var ret = ""; + do { + ret = DIGITS.charAt(num % 54) + ret; + num = Math.floor(num / 54); + } while (num > 0); + return ret; + }; +})(); + +Scope.prototype = { + has: function(name) { + for (var s = this; s; s = s.parent) + if (HOP(s.names, name)) + return s; + }, + has_mangled: function(mname) { + for (var s = this; s; s = s.parent) + if (HOP(s.rev_mangled, mname)) + return s; + }, + toJSON: function() { + return { + names: this.names, + uses_eval: this.uses_eval, + uses_with: this.uses_with + }; + }, + + next_mangled: function() { + // we must be careful that the new mangled name: + // + // 1. doesn't shadow a mangled name from a parent + // scope, unless we don't reference the original + // name from this scope OR from any sub-scopes! + // This will get slow. + // + // 2. doesn't shadow an original name from a parent + // scope, in the event that the name is not mangled + // in the parent scope and we reference that name + // here OR IN ANY SUBSCOPES! + // + // 3. doesn't shadow a name that is referenced but not + // defined (possibly global defined elsewhere). + for (;;) { + var m = base54(++this.cname), prior; + + // case 1. + prior = this.has_mangled(m); + if (prior && this.refs[prior.rev_mangled[m]] === prior) + continue; + + // case 2. + prior = this.has(m); + if (prior && prior !== this && this.refs[m] === prior && !prior.has_mangled(m)) + continue; + + // case 3. + if (HOP(this.refs, m) && this.refs[m] == null) + continue; + + // I got "do" once. :-/ + if (!is_identifier(m)) + continue; + + return m; + } + }, + get_mangled: function(name, newMangle) { + if (this.uses_eval || this.uses_with) return name; // no mangle if eval or with is in use + var s = this.has(name); + if (!s) return name; // not in visible scope, no mangle + if (HOP(s.mangled, name)) return s.mangled[name]; // already mangled in this scope + if (!newMangle) return name; // not found and no mangling requested + + var m = s.next_mangled(); + s.rev_mangled[m] = name; + return s.mangled[name] = m; + }, + define: function(name) { + if (name != null) + return this.names[name] = name; + } +}; + +function ast_add_scope(ast) { + + var current_scope = null; + var w = ast_walker(), walk = w.walk; + var having_eval = []; + + function with_new_scope(cont) { + current_scope = new Scope(current_scope); + var ret = current_scope.body = cont(); + ret.scope = current_scope; + current_scope = current_scope.parent; + return ret; + }; + + function define(name) { + return current_scope.define(name); + }; + + function reference(name) { + current_scope.refs[name] = true; + }; + + function _lambda(name, args, body) { + return [ this[0], define(name), args, with_new_scope(function(){ + MAP(args, define); + return MAP(body, walk); + })]; + }; + + return with_new_scope(function(){ + // process AST + var ret = w.with_walkers({ + "function": _lambda, + "defun": _lambda, + "with": function(expr, block) { + for (var s = current_scope; s; s = s.parent) + s.uses_with = true; + }, + "var": function(defs) { + MAP(defs, function(d){ define(d[0]) }); + }, + "const": function(defs) { + MAP(defs, function(d){ define(d[0]) }); + }, + "try": function(t, c, f) { + if (c != null) return [ + "try", + MAP(t, walk), + [ define(c[0]), MAP(c[1], walk) ], + f != null ? MAP(f, walk) : null + ]; + }, + "name": function(name) { + if (name == "eval") + having_eval.push(current_scope); + reference(name); + }, + "for-in": function(has_var, name) { + if (has_var) define(name); + else reference(name); + } + }, function(){ + return walk(ast); + }); + + // the reason why we need an additional pass here is + // that names can be used prior to their definition. + + // scopes where eval was detected and their parents + // are marked with uses_eval, unless they define the + // "eval" name. + MAP(having_eval, function(scope){ + if (!scope.has("eval")) while (scope) { + scope.uses_eval = true; + scope = scope.parent; + } + }); + + // for referenced names it might be useful to know + // their origin scope. current_scope here is the + // toplevel one. + function fixrefs(scope, i) { + // do children first; order shouldn't matter + for (i = scope.children.length; --i >= 0;) + fixrefs(scope.children[i]); + for (i in scope.refs) if (HOP(scope.refs, i)) { + // find origin scope and propagate the reference to origin + for (var origin = scope.has(i), s = scope; s; s = s.parent) { + s.refs[i] = origin; + if (s === origin) break; + } + } + }; + fixrefs(current_scope); + + return ret; + }); + +}; + +/* -----[ mangle names ]----- */ + +function ast_mangle(ast, do_toplevel) { + var w = ast_walker(), walk = w.walk, scope; + + function get_mangled(name, newMangle) { + if (!do_toplevel && !scope.parent) return name; // don't mangle toplevel + return scope.get_mangled(name, newMangle); + }; + + function _lambda(name, args, body) { + if (name) name = get_mangled(name); + body = with_scope(body.scope, function(){ + args = MAP(args, function(name){ return get_mangled(name) }); + return MAP(body, walk); + }); + return [ this[0], name, args, body ]; + }; + + function with_scope(s, cont) { + var _scope = scope; + scope = s; + for (var i in s.names) if (HOP(s.names, i)) { + get_mangled(i, true); + } + var ret = cont(); + ret.scope = s; + scope = _scope; + return ret; + }; + + function _vardefs(defs) { + return MAP(defs, function(d){ + return [ get_mangled(d[0]), walk(d[1]) ]; + }); + }; + + return w.with_walkers({ + "function": _lambda, + "defun": function() { + // move function declarations to the top when + // they are not in some block. + var ast = _lambda.apply(this, arguments); + switch (w.parent()[0]) { + case "toplevel": + case "function": + case "defun": + return MAP.at_top(ast); + } + return ast; + }, + "var": function(defs) { + return [ "var", _vardefs(defs) ]; + }, + "const": function(defs) { + return [ "const", _vardefs(defs) ]; + }, + "name": function(name) { + return [ "name", get_mangled(name) ]; + }, + "try": function(t, c, f) { + return [ "try", + MAP(t, walk), + c != null ? [ get_mangled(c[0]), MAP(c[1], walk) ] : null, + f != null ? MAP(f, walk) : null ]; + }, + "toplevel": function(body) { + return with_scope(this.scope, function(){ + return [ "toplevel", MAP(body, walk) ]; + }); + }, + "for-in": function(has_var, name, obj, stat) { + return [ "for-in", has_var, get_mangled(name), walk(obj), walk(stat) ]; + } + }, function() { + return walk(ast_add_scope(ast)); + }); +}; + +/* -----[ + - compress foo["bar"] into foo.bar, + - remove block brackets {} where possible + - join consecutive var declarations + - various optimizations for IFs: + - if (cond) foo(); else bar(); ==> cond?foo():bar(); + - if (cond) foo(); ==> cond&&foo(); + - if (foo) return bar(); else return baz(); ==> return foo?bar():baz(); // also for throw + - if (foo) return bar(); else something(); ==> {if(foo)return bar();something()} + ]----- */ + +var warn = function(){}; + +function best_of(ast1, ast2) { + return gen_code(ast1).length > gen_code(ast2[0] == "stat" ? ast2[1] : ast2).length ? ast2 : ast1; +}; + +function last_stat(b) { + if (b[0] == "block" && b[1] && b[1].length > 0) + return b[1][b[1].length - 1]; + return b; +} + +function aborts(t) { + if (t) { + t = last_stat(t); + if (t[0] == "return" || t[0] == "break" || t[0] == "continue" || t[0] == "throw") + return true; + } +}; + +function negate(c) { + var not_c = [ "unary-prefix", "!", c ]; + switch (c[0]) { + case "unary-prefix": + return c[1] == "!" ? c[2] : not_c; + case "binary": + var op = c[1], left = c[2], right = c[3]; + switch (op) { + case "<=": return [ "binary", ">", left, right ]; + case "<": return [ "binary", ">=", left, right ]; + case ">=": return [ "binary", "<", left, right ]; + case ">": return [ "binary", "<=", left, right ]; + case "==": return [ "binary", "!=", left, right ]; + case "!=": return [ "binary", "==", left, right ]; + case "===": return [ "binary", "!==", left, right ]; + case "!==": return [ "binary", "===", left, right ]; + case "&&": return best_of(not_c, [ "binary", "||", negate(left), negate(right) ]); + case "||": return best_of(not_c, [ "binary", "&&", negate(left), negate(right) ]); + } + break; + } + return not_c; +}; + +function make_conditional(c, t, e) { + if (c[0] == "unary-prefix" && c[1] == "!") { + return e ? [ "conditional", c[2], e, t ] : [ "binary", "||", c[2], t ]; + } else { + return e ? [ "conditional", c, t, e ] : [ "binary", "&&", c, t ]; + } +}; + +function empty(b) { + return !b || (b[0] == "block" && (!b[1] || b[1].length == 0)); +}; + +function ast_squeeze(ast, options) { + options = defaults(options, { + make_seqs : true, + dead_code : true, + no_warnings : false, + extra : false + }); + + var w = ast_walker(), walk = w.walk, scope; + + function with_scope(s, cont) { + var _scope = scope; + scope = s; + var ret = cont(); + ret.scope = s; + scope = _scope; + return ret; + }; + + function is_constant(node) { + return node[0] == "string" || node[0] == "num"; + }; + + function find_first_execute(node) { + if (!node) + return false; + + switch (node[0]) { + case "num": + case "string": + case "name": + return node; + case "call": + case "conditional": + case "for": + case "if": + case "new": + case "return": + case "stat": + case "switch": + case "throw": + return find_first_execute(node[1]); + case "binary": + return find_first_execute(node[2]); + case "assign": + if (node[1] === true) + return find_first_execute(node[3]); + break; + case "var": + if (node[1][0].length > 1) + return find_first_execute(node[1][0][1]); + break; + } + return null; + } + + function find_assign_recursive(p, v) { + if (p[0] == "assign" && p[1] != true || p[0] == "unary-prefix") { + if (p[2][0] == "name" && v[0] == "name" && p[2][1] == v[1]) + return true; + return false; + } + + if (p[0] != "assign" || p[1] !== true) + return false; + + if ((is_constant(p[3]) && p[3][0] == v[0] && p[3][1] == v[1]) || + (p[3][0] == "name" && v[0] == "name" && p[3][1] == v[1]) || + (p[2][0] == "name" && v[0] == "name" && p[2][1] == v[1])) + return true; + + return find_assign_recursive(p[3], v); + }; + + function rmblock(block) { + if (block != null && block[0] == "block" && block[1] && block[1].length == 1) + block = block[1][0]; + return block; + }; + + function clone(obj) { + if (obj && obj.constructor == Array) + return MAP(obj, clone); + return obj; + }; + + function make_seq_to_statements(node) { + if (node[0] != "seq") { + switch (node[0]) { + case "var": + case "const": + return [ node ]; + default: + return [ [ "stat", node ] ]; + } + } + + var ret = []; + for (var i = 1; i < node.length; i++) + ret.push.apply(ret, make_seq_to_statements(node[i])); + + return ret; + }; + + function _lambda(name, args, body) { + return [ this[0], name, args, with_scope(body.scope, function(){ + return tighten(MAP(body, walk), "lambda"); + }) ]; + }; + + // we get here for blocks that have been already transformed. + // this function does a few things: + // 1. discard useless blocks + // 2. join consecutive var declarations + // 3. remove obviously dead code + // 4. transform consecutive statements using the comma operator + // 5. if block_type == "lambda" and it detects constructs like if(foo) return ... - rewrite like if (!foo) { ... } + function tighten(statements, block_type) { + statements = statements.reduce(function(a, stat){ + if (stat[0] == "block") { + if (stat[1]) { + a.push.apply(a, stat[1]); + } + } else { + a.push(stat); + } + return a; + }, []); + + if (options.extra) { + // Detightening things. We do this because then we can assume that the + // statements are structured in a specific way. + statements = (function(a, prev) { + statements.forEach(function(cur) { + switch (cur[0]) { + case "for": + if (cur[1] != null) { + a.push.apply(a, make_seq_to_statements(cur[1])); + cur[1] = null; + } + a.push(cur); + break; + case "stat": + var stats = make_seq_to_statements(cur[1]); + stats.forEach(function(s) { + if (s[1][0] == "unary-postfix") + s[1][0] = "unary-prefix"; + }); + a.push.apply(a, stats); + break; + default: + a.push(cur); + } + }); + return a; + })([]); + + statements = (function(a, prev) { + statements.forEach(function(cur) { + if (!(prev && prev[0] == "stat")) { + a.push(cur); + prev = cur; + return; + } + + var p = prev[1]; + var c = find_first_execute(cur); + if (c && find_assign_recursive(p, c)) { + var old_cur = clone(cur); + c.splice(0, c.length); + c.push.apply(c, p); + var tmp_cur = best_of(cur, [ "toplevel", [ prev, old_cur ] ]); + if (tmp_cur == cur) { + a[a.length -1] = cur; + } else { + cur = old_cur; + a.push(cur); + } + } else { + a.push(cur); + } + prev = cur; + }); + return a; + })([]); + } + + statements = (function(a, prev){ + statements.forEach(function(cur){ + if (prev && ((cur[0] == "var" && prev[0] == "var") || + (cur[0] == "const" && prev[0] == "const"))) { + prev[1] = prev[1].concat(cur[1]); + } else { + a.push(cur); + prev = cur; + } + }); + return a; + })([]); + + if (options.dead_code) statements = (function(a, has_quit){ + statements.forEach(function(st){ + if (has_quit) { + if (member(st[0], [ "function", "defun" , "var", "const" ])) { + a.push(st); + } + else if (!options.no_warnings) + warn("Removing unreachable code: " + gen_code(st, true)); + } + else { + a.push(st); + if (member(st[0], [ "return", "throw", "break", "continue" ])) + has_quit = true; + } + }); + return a; + })([]); + + if (options.make_seqs) statements = (function(a, prev) { + statements.forEach(function(cur){ + if (prev && prev[0] == "stat" && cur[0] == "stat") { + prev[1] = [ "seq", prev[1], cur[1] ]; + } else { + a.push(cur); + prev = cur; + } + }); + return a; + })([]); + + if (options.extra) { + statements = (function(a, prev){ + statements.forEach(function(cur){ + var replaced = false; + if (prev && cur[0] == "for" && cur[1] == null && (prev[0] == "var" || prev[0] == "const" || prev[0] == "stat")) { + cur[1] = prev; + a[a.length - 1] = cur; + } else { + a.push(cur); + } + prev = cur; + }); + return a; + })([]); + } + + if (block_type == "lambda") statements = (function(i, a, stat){ + while (i < statements.length) { + stat = statements[i++]; + if (stat[0] == "if" && !stat[3]) { + if (stat[2][0] == "return" && stat[2][1] == null) { + a.push(make_if(negate(stat[1]), [ "block", statements.slice(i) ])); + break; + } + var last = last_stat(stat[2]); + if (last[0] == "return" && last[1] == null) { + a.push(make_if(stat[1], [ "block", stat[2][1].slice(0, -1) ], [ "block", statements.slice(i) ])); + break; + } + } + a.push(stat); + } + return a; + })(0, []); + + return statements; + }; + + function make_if(c, t, e) { + c = walk(c); + t = walk(t); + e = walk(e); + + if (empty(t)) { + c = negate(c); + t = e; + e = null; + } else if (empty(e)) { + e = null; + } else { + // if we have both else and then, maybe it makes sense to switch them? + (function(){ + var a = gen_code(c); + var n = negate(c); + var b = gen_code(n); + if (b.length < a.length) { + var tmp = t; + t = e; + e = tmp; + c = n; + } + })(); + } + if (empty(e) && empty(t)) + return [ "stat", c ]; + var ret = [ "if", c, t, e ]; + if (t[0] == "stat") { + if (e) { + if (e[0] == "stat") { + ret = best_of(ret, [ "stat", make_conditional(c, t[1], e[1]) ]); + } + } + else { + ret = best_of(ret, [ "stat", make_conditional(c, t[1]) ]); + } + } + else if (e && t[0] == e[0] && (t[0] == "return" || t[0] == "throw")) { + ret = best_of(ret, [ t[0], make_conditional(c, t[1], e[1] ) ]); + } + else if (e && aborts(t)) { + ret = [ [ "if", c, t ] ]; + if (e[0] == "block") { + if (e[1]) ret = ret.concat(e[1]); + } + else { + ret.push(e); + } + ret = walk([ "block", ret ]); + } + else if (t && aborts(e)) { + ret = [ [ "if", negate(c), e ] ]; + if (t[0] == "block") { + if (t[1]) ret = ret.concat(t[1]); + } else { + ret.push(t); + } + ret = walk([ "block", ret ]); + } + return ret; + }; + + return w.with_walkers({ + "sub": function(expr, subscript) { + if (subscript[0] == "string") { + var name = subscript[1]; + if (is_identifier(name)) { + return [ "dot", walk(expr), name ]; + } + } + }, + "if": make_if, + "toplevel": function(body) { + return [ "toplevel", with_scope(this.scope, function(){ + return tighten(MAP(body, walk)); + }) ]; + }, + "switch": function(expr, body) { + var last = body.length - 1; + return [ "switch", walk(expr), MAP(body, function(branch, i){ + var block = tighten(MAP(branch[1], walk)); + if (i == last && block.length > 0) { + var node = block[block.length - 1]; + if (node[0] == "break" && !node[1]) + block.pop(); + } + return [ branch[0] ? walk(branch[0]) : null, block ]; + }) ]; + }, + "function": _lambda, + "defun": _lambda, + "block": function(body) { + if (body) return rmblock([ "block", tighten(MAP(body, walk)) ]); + }, + "binary": function(op, left, right) { + left = walk(left); + right = walk(right); + var best = [ "binary", op, left, right ]; + if (is_constant(right)) { + if (is_constant(left)) { + var val = null; + switch (op) { + case "+": val = left[1] + right[1]; break; + case "*": val = left[1] * right[1]; break; + case "/": val = left[1] / right[1]; break; + case "-": val = left[1] - right[1]; break; + case "<<": val = left[1] << right[1]; break; + case ">>": val = left[1] >> right[1]; break; + case ">>>": val = left[1] >>> right[1]; break; + } + if (val != null) { + best = best_of(best, [ typeof val == "string" ? "string" : "num", val ]); + } + } else if (left[0] == "binary" && left[1] == "+" && left[3][0] == "string") { + best = best_of(best, [ "binary", "+", left[2], [ "string", left[3][1] + right[1] ] ]); + } + } + return best; + }, + "conditional": function(c, t, e) { + return make_conditional(walk(c), walk(t), walk(e)); + }, + "try": function(t, c, f) { + return [ + "try", + tighten(MAP(t, walk)), + c != null ? [ c[0], tighten(MAP(c[1], walk)) ] : null, + f != null ? tighten(MAP(f, walk)) : null + ]; + }, + "unary-prefix": function(op, cond) { + if (op == "!") { + cond = walk(cond); + if (cond[0] == "unary-prefix" && cond[1] == "!") { + var p = w.parent(); + if (p[0] == "unary-prefix" && p[1] == "!") + return cond[2]; + return [ "unary-prefix", "!", cond ]; + } + return best_of(this, negate(cond)); + } + }, + "name": function(name) { + switch (name) { + case "true": return [ "unary-prefix", "!", [ "num", 0 ]]; + case "false": return [ "unary-prefix", "!", [ "num", 1 ]]; + } + }, + "new": function(ctor, args) { + if (ctor[0] == "name" && ctor[1] == "Array" && !scope.has("Array")) { + if (args.length != 1) { + return [ "array", args ]; + } else { + return [ "call", [ "name", "Array" ], args ]; + } + } + }, + "call": function(expr, args) { + if (expr[0] == "name" && expr[1] == "Array" && args.length != 1 && !scope.has("Array")) { + return [ "array", args ]; + } + } + }, function() { + return walk(ast_add_scope(ast)); + }); +}; + +/* -----[ re-generate code from the AST ]----- */ + +var DOT_CALL_NO_PARENS = jsp.array_to_hash([ + "name", + "array", + "string", + "dot", + "sub", + "call", + "regexp" +]); + +function make_string(str) { + var dq = 0, sq = 0; + str = str.replace(/[\\\b\f\n\r\t\x22\x27]/g, function(s){ + switch (s) { + case "\\": return "\\\\"; + case "\b": return "\\b"; + case "\f": return "\\f"; + case "\n": return "\\n"; + case "\r": return "\\r"; + case "\t": return "\\t"; + case '"': ++dq; return '"'; + case "'": ++sq; return "'"; + } + return s; + }); + if (dq > sq) { + return "'" + str.replace(/\x27/g, "\\'") + "'"; + } else { + return '"' + str.replace(/\x22/g, '\\"') + '"'; + } +}; + +function gen_code(ast, beautify) { + if (beautify) beautify = defaults(beautify, { + indent_start : 0, + indent_level : 4, + quote_keys : false, + space_colon : false + }); + var indentation = 0, + newline = beautify ? "\n" : "", + space = beautify ? " " : ""; + + function indent(line) { + if (line == null) + line = ""; + if (beautify) + line = repeat_string(" ", beautify.indent_start + indentation * beautify.indent_level) + line; + return line; + }; + + function with_indent(cont, incr) { + if (incr == null) incr = 1; + indentation += incr; + try { return cont.apply(null, slice(arguments, 1)); } + finally { indentation -= incr; } + }; + + function add_spaces(a) { + if (beautify) + return a.join(" "); + var b = []; + for (var i = 0; i < a.length; ++i) { + var next = a[i + 1]; + b.push(a[i]); + if (next && + ((/[a-z0-9_\x24]$/i.test(a[i].toString()) && /^[a-z0-9_\x24]/i.test(next.toString())) || + (/[\+\-]$/.test(a[i].toString()) && /^[\+\-]/.test(next.toString())))) { + b.push(" "); + } + } + return b.join(""); + }; + + function add_commas(a) { + return a.join("," + space); + }; + + function parenthesize(expr) { + var gen = make(expr); + for (var i = 1; i < arguments.length; ++i) { + var el = arguments[i]; + if ((el instanceof Function && el(expr)) || expr[0] == el) + return "(" + gen + ")"; + } + return gen; + }; + + function best_of(a) { + if (a.length == 1) { + return a[0]; + } + if (a.length == 2) { + var b = a[1]; + a = a[0]; + return a.length <= b.length ? a : b; + } + return best_of([ a[0], best_of(a.slice(1)) ]); + }; + + function needs_parens(expr) { + if (expr[0] == "function") { + // dot/call on a literal function requires the + // function literal itself to be parenthesized + // only if it's the first "thing" in a + // statement. This means that the parent is + // "stat", but it could also be a "seq" and + // we're the first in this "seq" and the + // parent is "stat", and so on. Messy stuff, + // but it worths the trouble. + var a = slice($stack), self = a.pop(), p = a.pop(); + while (p) { + if (p[0] == "stat") return true; + if ((p[0] == "seq" && p[1] === self) || + (p[0] == "call" && p[1] === self) || + (p[0] == "binary" && p[2] === self)) { + self = p; + p = a.pop(); + } else { + return false; + } + } + } + return !HOP(DOT_CALL_NO_PARENS, expr[0]); + }; + + function make_num(num) { + var str = num.toString(10), a = [ str.replace(/^0\./, ".") ], m; + if (Math.floor(num) === num) { + a.push("0x" + num.toString(16).toLowerCase(), // probably pointless + "0" + num.toString(8)); // same. + if ((m = /^(.*?)(0+)$/.exec(num))) { + a.push(m[1] + "e" + m[2].length); + } + } else if ((m = /^0?\.(0+)(.*)$/.exec(num))) { + a.push(m[2] + "e-" + (m[1].length + m[2].length), + str.substr(str.indexOf("."))); + } + return best_of(a); + }; + + var generators = { + "string": make_string, + "num": make_num, + "name": make_name, + "toplevel": function(statements) { + return make_block_statements(statements) + .join(newline + newline); + }, + "block": make_block, + "var": function(defs) { + return "var " + add_commas(MAP(defs, make_1vardef)) + ";"; + }, + "const": function(defs) { + return "const " + add_commas(MAP(defs, make_1vardef)) + ";"; + }, + "try": function(tr, ca, fi) { + var out = [ "try", make_block(tr) ]; + if (ca) out.push("catch", "(" + ca[0] + ")", make_block(ca[1])); + if (fi) out.push("finally", make_block(fi)); + return add_spaces(out); + }, + "throw": function(expr) { + return add_spaces([ "throw", make(expr) ]) + ";"; + }, + "new": function(ctor, args) { + args = args.length > 0 ? "(" + add_commas(MAP(args, make)) + ")" : ""; + return add_spaces([ "new", parenthesize(ctor, "seq", "binary", "conditional", "assign", function(expr){ + var w = ast_walker(), has_call = {}; + try { + w.with_walkers({ + "call": function() { throw has_call }, + "function": function() { return this } + }, function(){ + w.walk(expr); + }); + } catch(ex) { + if (ex === has_call) + return true; + throw ex; + } + }) + args ]); + }, + "switch": function(expr, body) { + return add_spaces([ "switch", "(" + make(expr) + ")", make_switch_block(body) ]); + }, + "break": function(label) { + var out = "break"; + if (label != null) + out += " " + make_name(label); + return out + ";"; + }, + "continue": function(label) { + var out = "continue"; + if (label != null) + out += " " + make_name(label); + return out + ";"; + }, + "conditional": function(co, th, el) { + return add_spaces([ parenthesize(co, "assign", "seq", "conditional"), "?", + parenthesize(th, "seq"), ":", + parenthesize(el, "seq") ]); + }, + "assign": function(op, lvalue, rvalue) { + if (op && op !== true) op += "="; + else op = "="; + return add_spaces([ make(lvalue), op, parenthesize(rvalue, "seq") ]); + }, + "dot": function(expr) { + var out = make(expr), i = 1; + if (expr[0] == "num") + out += "."; + else if (needs_parens(expr)) + out = "(" + out + ")"; + while (i < arguments.length) + out += "." + make_name(arguments[i++]); + return out; + }, + "call": function(func, args) { + var f = make(func); + if (needs_parens(func)) + f = "(" + f + ")"; + return f + "(" + add_commas(MAP(args, function(expr){ + return parenthesize(expr, "seq"); + })) + ")"; + }, + "function": make_function, + "defun": make_function, + "if": function(co, th, el) { + var out = [ "if", "(" + make(co) + ")", el ? make_then(th) : make(th) ]; + if (el) { + out.push("else", make(el)); + } + return add_spaces(out); + }, + "for": function(init, cond, step, block) { + var out = [ "for" ]; + init = (init != null ? make(init) : "").replace(/;*\s*$/, ";" + space); + cond = (cond != null ? make(cond) : "").replace(/;*\s*$/, ";" + space); + step = (step != null ? make(step) : "").replace(/;*\s*$/, ""); + var args = init + cond + step; + if (args == "; ; ") args = ";;"; + out.push("(" + args + ")", make(block)); + return add_spaces(out); + }, + "for-in": function(has_var, key, hash, block) { + var out = add_spaces([ "for", "(" ]); + if (has_var) + out += "var "; + out += add_spaces([ make_name(key) + " in " + make(hash) + ")", make(block) ]); + return out; + }, + "while": function(condition, block) { + return add_spaces([ "while", "(" + make(condition) + ")", make(block) ]); + }, + "do": function(condition, block) { + return add_spaces([ "do", make(block), "while", "(" + make(condition) + ")" ]) + ";"; + }, + "return": function(expr) { + var out = [ "return" ]; + if (expr != null) out.push(make(expr)); + return add_spaces(out) + ";"; + }, + "binary": function(operator, lvalue, rvalue) { + var left = make(lvalue), right = make(rvalue); + // XXX: I'm pretty sure other cases will bite here. + // we need to be smarter. + // adding parens all the time is the safest bet. + if (member(lvalue[0], [ "assign", "conditional", "seq" ]) || + lvalue[0] == "binary" && PRECEDENCE[operator] > PRECEDENCE[lvalue[1]]) { + left = "(" + left + ")"; + } + if (member(rvalue[0], [ "assign", "conditional", "seq" ]) || + rvalue[0] == "binary" && PRECEDENCE[operator] >= PRECEDENCE[rvalue[1]]) { + right = "(" + right + ")"; + } + return add_spaces([ left, operator, right ]); + }, + "unary-prefix": function(operator, expr) { + var val = make(expr); + if (!(expr[0] == "num" || (expr[0] == "unary-prefix" && !HOP(OPERATORS, operator + expr[1])) || !needs_parens(expr))) + val = "(" + val + ")"; + return operator + (jsp.is_alphanumeric_char(operator.charAt(0)) ? " " : "") + val; + }, + "unary-postfix": function(operator, expr) { + var val = make(expr); + if (!(expr[0] == "num" || (expr[0] == "unary-postfix" && !HOP(OPERATORS, operator + expr[1])) || !needs_parens(expr))) + val = "(" + val + ")"; + return val + operator; + }, + "sub": function(expr, subscript) { + var hash = make(expr); + if (needs_parens(expr)) + hash = "(" + hash + ")"; + return hash + "[" + make(subscript) + "]"; + }, + "object": function(props) { + if (props.length == 0) + return "{}"; + return "{" + newline + with_indent(function(){ + return MAP(props, function(p){ + if (p.length == 3) { + // getter/setter. The name is in p[0], the arg.list in p[1][2], the + // body in p[1][3] and type ("get" / "set") in p[2]. + return indent(make_function(p[0], p[1][2], p[1][3], p[2])); + } + var key = p[0], val = make(p[1]); + if (beautify && beautify.quote_keys) { + key = make_string(key); + } else if (typeof key == "number" || !beautify && +key + "" == key) { + key = make_num(+key); + } else if (!is_identifier(key)) { + key = make_string(key); + } + return indent(add_spaces(beautify && beautify.space_colon + ? [ key, ":", val ] + : [ key + ":", val ])); + }).join("," + newline); + }) + newline + indent("}"); + }, + "regexp": function(rx, mods) { + return "/" + rx + "/" + mods; + }, + "array": function(elements) { + if (elements.length == 0) return "[]"; + return add_spaces([ "[", add_commas(MAP(elements, function(el){ + return parenthesize(el, "seq"); + })), "]" ]); + }, + "stat": function(stmt) { + return make(stmt).replace(/;*\s*$/, ";"); + }, + "seq": function() { + return add_commas(MAP(slice(arguments), make)); + }, + "label": function(name, block) { + return add_spaces([ make_name(name), ":", make(block) ]); + }, + "with": function(expr, block) { + return add_spaces([ "with", "(" + make(expr) + ")", make(block) ]); + }, + "atom": function(name) { + return make_name(name); + }, + "comment1": function(text) { + return "//" + text + "\n"; + }, + "comment2": function(text) { + return "/*" + text + "*/"; + } + }; + + // The squeezer replaces "block"-s that contain only a single + // statement with the statement itself; technically, the AST + // is correct, but this can create problems when we output an + // IF having an ELSE clause where the THEN clause ends in an + // IF *without* an ELSE block (then the outer ELSE would refer + // to the inner IF). This function checks for this case and + // adds the block brackets if needed. + function make_then(th) { + if (th[0] == "do") { + // https://github.com/mishoo/UglifyJS/issues/#issue/57 + // IE croaks with "syntax error" on code like this: + // if (foo) do ... while(cond); else ... + // we need block brackets around do/while + return make([ "block", [ th ]]); + } + var b = th; + while (true) { + var type = b[0]; + if (type == "if") { + if (!b[3]) + // no else, we must add the block + return make([ "block", [ th ]]); + b = b[3]; + } + else if (type == "while" || type == "do") b = b[2]; + else if (type == "for" || type == "for-in") b = b[4]; + else break; + } + return make(th); + }; + + function make_function(name, args, body, keyword) { + var out = keyword || "function"; + if (name) { + out += " " + make_name(name); + } + out += "(" + add_commas(MAP(args, make_name)) + ")"; + return add_spaces([ out, make_block(body) ]); + }; + + function make_name(name) { + return name.toString(); + }; + + function make_block_statements(statements) { + for (var a = [], last = statements.length - 1, i = 0; i <= last; ++i) { + var stat = statements[i]; + var code = make(stat); + if (code != ";") { + if (!beautify && i == last) + code = code.replace(/;+\s*$/, ""); + a.push(code); + } + } + return MAP(a, indent); + }; + + function make_switch_block(body) { + var n = body.length; + if (n == 0) return "{}"; + return "{" + newline + MAP(body, function(branch, i){ + var has_body = branch[1].length > 0, code = with_indent(function(){ + return indent(branch[0] + ? add_spaces([ "case", make(branch[0]) + ":" ]) + : "default:"); + }, 0.5) + (has_body ? newline + with_indent(function(){ + return make_block_statements(branch[1]).join(newline); + }) : ""); + if (!beautify && has_body && i < n - 1) + code += ";"; + return code; + }).join(newline) + newline + indent("}"); + }; + + function make_block(statements) { + if (!statements) return ";"; + if (statements.length == 0) return "{}"; + return "{" + newline + with_indent(function(){ + return make_block_statements(statements).join(newline); + }) + newline + indent("}"); + }; + + function make_1vardef(def) { + var name = def[0], val = def[1]; + if (val != null) + name = add_spaces([ name, "=", make(val) ]); + return name; + }; + + var $stack = []; + + function make(node) { + var type = node[0]; + var gen = generators[type]; + if (!gen) + throw new Error("Can't find generator for \"" + type + "\""); + $stack.push(node); + var ret = gen.apply(type, node.slice(1)); + $stack.pop(); + return ret; + }; + + return make(ast); +}; + +/* -----[ Utilities ]----- */ + +function repeat_string(str, i) { + if (i <= 0) return ""; + if (i == 1) return str; + var d = repeat_string(str, i >> 1); + d += d; + if (i & 1) d += str; + return d; +}; + +function defaults(args, defs) { + var ret = {}; + if (args === true) + args = {}; + for (var i in defs) if (HOP(defs, i)) { + ret[i] = (args && HOP(args, i)) ? args[i] : defs[i]; + } + return ret; +}; + +function is_identifier(name) { + return /^[a-z_$][a-z0-9_$]*$/i.test(name) + && name != "this" + && !HOP(jsp.KEYWORDS_ATOM, name) + && !HOP(jsp.RESERVED_WORDS, name) + && !HOP(jsp.KEYWORDS, name); +}; + +function HOP(obj, prop) { + return Object.prototype.hasOwnProperty.call(obj, prop); +}; + +// some utilities + +var MAP; + +(function(){ + MAP = function(a, f, o) { + var ret = []; + for (var i = 0; i < a.length; ++i) { + var val = f.call(o, a[i], i); + if (val instanceof AtTop) ret.unshift(val.v); + else ret.push(val); + } + return ret; + }; + MAP.at_top = function(val) { return new AtTop(val) }; + function AtTop(val) { this.v = val }; +})(); + +/* -----[ Exports ]----- */ + +exports.ast_walker = ast_walker; +exports.ast_mangle = ast_mangle; +exports.ast_squeeze = ast_squeeze; +exports.gen_code = gen_code; +exports.ast_add_scope = ast_add_scope; +exports.ast_squeeze_more = require("./squeeze-more").ast_squeeze_more; +exports.set_logger = function(logger) { warn = logger }; diff --git a/build/lib/squeeze-more.js b/build/lib/squeeze-more.js new file mode 100644 index 000000000..12380af82 --- /dev/null +++ b/build/lib/squeeze-more.js @@ -0,0 +1,22 @@ +var jsp = require("./parse-js"), + pro = require("./process"), + slice = jsp.slice, + member = jsp.member, + PRECEDENCE = jsp.PRECEDENCE, + OPERATORS = jsp.OPERATORS; + +function ast_squeeze_more(ast) { + var w = pro.ast_walker(), walk = w.walk; + return w.with_walkers({ + "call": function(expr, args) { + if (expr[0] == "dot" && expr[2] == "toString" && args.length == 0) { + // foo.toString() ==> foo+"" + return [ "binary", "+", expr[1], [ "string", "" ]]; + } + } + }, function() { + return walk(ast); + }); +}; + +exports.ast_squeeze_more = ast_squeeze_more; diff --git a/build/uglify.js b/build/uglify.js new file mode 100644 index 000000000..943ddd806 --- /dev/null +++ b/build/uglify.js @@ -0,0 +1,199 @@ +#! /usr/bin/env node +// -*- js2 -*- + +global.sys = require(/^v0\.[012]/.test(process.version) ? "sys" : "util"); +var fs = require("fs"), + jsp = require("./lib/parse-js"), + pro = require("./lib/process"); + +pro.set_logger(function(msg){ + sys.debug(msg); +}); + +var options = { + ast: false, + mangle: true, + mangle_toplevel: false, + squeeze: true, + make_seqs: true, + dead_code: true, + beautify: false, + verbose: false, + show_copyright: true, + out_same_file: false, + extra: false, + unsafe: false, // XXX: extra & unsafe? but maybe we don't want both, so.... + beautify_options: { + indent_level: 4, + indent_start: 0, + quote_keys: false, + space_colon: false + }, + output: true // stdout +}; + +var args = jsp.slice(process.argv, 2); +var filename; + +out: while (args.length > 0) { + var v = args.shift(); + switch (v) { + case "-b": + case "--beautify": + options.beautify = true; + break; + case "-i": + case "--indent": + options.beautify_options.indent_level = args.shift(); + break; + case "-q": + case "--quote-keys": + options.beautify_options.quote_keys = true; + break; + case "-mt": + case "--mangle-toplevel": + options.mangle_toplevel = true; + break; + case "--no-mangle": + case "-nm": + options.mangle = false; + break; + case "--no-squeeze": + case "-ns": + options.squeeze = false; + break; + case "--no-seqs": + options.make_seqs = false; + break; + case "--no-dead-code": + options.dead_code = false; + break; + case "--no-copyright": + case "-nc": + options.show_copyright = false; + break; + case "-o": + case "--output": + options.output = args.shift(); + break; + case "--overwrite": + options.out_same_file = true; + break; + case "-v": + case "--verbose": + options.verbose = true; + break; + case "--ast": + options.ast = true; + break; + case "--extra": + options.extra = true; + break; + case "--unsafe": + options.unsafe = true; + break; + default: + filename = v; + break out; + } +} + +if (filename) { + fs.readFile(filename, "utf8", function(err, text){ + if (err) { + throw err; + } + output(squeeze_it(text)); + }); +} else { + var stdin = process.openStdin(); + stdin.setEncoding("utf8"); + var text = ""; + stdin.on("data", function(chunk){ + text += chunk; + }); + stdin.on("end", function() { + output(squeeze_it(text)); + }); +} + +function output(text) { + var out; + if (options.out_same_file && filename) + options.output = filename; + if (options.output === true) { + out = process.stdout; + } else { + out = fs.createWriteStream(options.output, { + flags: "w", + encoding: "utf8", + mode: 0644 + }); + } + out.write(text); + out.end(); +}; + +// --------- main ends here. + +function show_copyright(comments) { + var ret = ""; + for (var i = 0; i < comments.length; ++i) { + var c = comments[i]; + if (c.type == "comment1") { + ret += "//" + c.value + "\n"; + } else { + ret += "/*" + c.value + "*/"; + } + } + return ret; +}; + +function squeeze_it(code) { + var result = ""; + if (options.show_copyright) { + var initial_comments = []; + // keep first comment + var tok = jsp.tokenizer(code, false), c; + c = tok(); + var prev = null; + while (/^comment/.test(c.type) && (!prev || prev == c.type)) { + initial_comments.push(c); + prev = c.type; + c = tok(); + } + result += show_copyright(initial_comments); + } + try { + var ast = time_it("parse", function(){ return jsp.parse(code); }); + if (options.mangle) + ast = time_it("mangle", function(){ return pro.ast_mangle(ast, options.mangle_toplevel); }); + if (options.squeeze) + ast = time_it("squeeze", function(){ + ast = pro.ast_squeeze(ast, { + make_seqs : options.make_seqs, + dead_code : options.dead_code, + extra : options.extra + }); + if (options.unsafe) + ast = pro.ast_squeeze_more(ast); + return ast; + }); + if (options.ast) + return sys.inspect(ast, null, null); + result += time_it("generate", function(){ return pro.gen_code(ast, options.beautify && options.beautify_options) }); + return result; + } catch(ex) { + sys.debug(ex.stack); + sys.debug(sys.inspect(ex)); + sys.debug(JSON.stringify(ex)); + } +}; + +function time_it(name, cont) { + if (!options.verbose) + return cont(); + var t1 = new Date().getTime(); + try { return cont(); } + finally { sys.debug("// " + name + ": " + ((new Date().getTime() - t1) / 1000).toFixed(3) + " sec."); } +}; diff --git a/src/ajax.js b/src/ajax.js index d570fcccb..871481d01 100644 --- a/src/ajax.js +++ b/src/ajax.js @@ -61,26 +61,34 @@ jQuery.fn.extend({ type: type, dataType: "html", data: params, - complete: function( res, status ) { + // Complete callback (responseText is used internally) + complete: function( jXHR, status, responseText ) { + // Store the response as specified by the jXHR object + responseText = jXHR.responseText; // If successful, inject the HTML into all the matched elements - if ( status === "success" || status === "notmodified" ) { + if ( jXHR.isResolved() ) { + // #4825: Get the actual response in case + // a dataFilter is present in ajaxSettings + jXHR.done(function( r ) { + responseText = r; + }); // See if a selector was specified self.html( selector ? // Create a dummy div to hold the results jQuery("
") // inject the contents of the document in, removing the scripts // to avoid any 'Permission Denied' errors in IE - .append(res.responseText.replace(rscript, "")) + .append(responseText.replace(rscript, "")) // Locate the specified elements .find(selector) : // If not, just inject the full result - res.responseText ); + responseText ); } if ( callback ) { - self.each( callback, [res.responseText, status, res] ); + self.each( callback, [responseText, status, jXHR] ); } } }); @@ -152,7 +160,8 @@ jQuery.extend({ }, ajaxSetup: function( settings ) { - jQuery.extend( jQuery.ajaxSettings, settings ); + jQuery.extend( true, jQuery.ajaxSettings, settings ); + return this; }, ajaxSettings: { @@ -166,11 +175,12 @@ jQuery.extend({ timeout: 0, data: null, dataType: null, - dataTypes: null, username: null, password: null, cache: null, traditional: false, + headers: {}, + crossDomain: null, */ xhr: function() { return new window.XMLHttpRequest(); @@ -257,6 +267,8 @@ jQuery.extend({ // Deferreds deferred = jQuery.Deferred(), completeDeferred = jQuery._Deferred(), + // Status-dependent callbacks + statusCode = s.statusCode || {}, // Headers (they are sent all at once) requestHeaders = {}, // Response headers @@ -266,6 +278,9 @@ jQuery.extend({ transport, // timeout handle timeoutTimer, + // Cross-domain detection vars + loc = document.location, + parts, // The jXHR state state = 0, // Loop variable @@ -292,30 +307,35 @@ jQuery.extend({ // (match is used internally) getResponseHeader: function( key , match ) { - if ( state !== 2 ) { - return null; - } + if ( state === 2 ) { - if ( responseHeaders === undefined ) { + if ( responseHeaders === undefined ) { - responseHeaders = {}; + responseHeaders = {}; - if ( typeof responseHeadersString === "string" ) { + if ( typeof responseHeadersString === "string" ) { - while( ( match = rheaders.exec( responseHeadersString ) ) ) { - responseHeaders[ match[ 1 ].toLowerCase() ] = match[ 2 ]; + while( ( match = rheaders.exec( responseHeadersString ) ) ) { + responseHeaders[ match[ 1 ].toLowerCase() ] = match[ 2 ]; + } } } + match = responseHeaders[ key.toLowerCase() ]; + + } else { + + match = null; } - return responseHeaders[ key.toLowerCase() ]; + + return match; }, // Cancel the request abort: function( statusText ) { - if ( transport && state !== 2 ) { + if ( transport ) { transport.abort( statusText || "abort" ); - done( 0 , statusText ); } + done( 0 , statusText ); return this; } }; @@ -333,6 +353,10 @@ jQuery.extend({ // State is "done" now state = 2; + // Dereference transport for early garbage collection + // (no matter how long the jXHR transport will be used + transport = 0; + // Set readyState jXHR.readyState = status ? 4 : 0; @@ -354,17 +378,10 @@ jQuery.extend({ // Stored success success, // Stored error - error = statusText; - - // If not timeout, force a jQuery-compliant status text - if ( statusText != "timeout" ) { - statusText = ( status >= 200 && status < 300 ) ? - "success" : - ( status === 304 ? "notmodified" : "error" ); - } + error; // If successful, handle type chaining - if ( statusText === "success" || statusText === "notmodified" ) { + if ( status >= 200 && status < 300 || status === 304 ) { // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. if ( s.ifModified ) { @@ -380,91 +397,124 @@ jQuery.extend({ } } - if ( s.ifModified && statusText === "notmodified" ) { + // If not modified + if ( status === 304 ) { - success = null; + // Set the statusText accordingly + statusText = "notmodified"; + // Mark as a success isSuccess = 1; + // If we have data } else { + + // Set the statusText accordingly + statusText = "success"; + // Chain data conversions and determine the final value // (if an exception is thrown in the process, it'll be notified as an error) try { var i, + // Current dataType current, + // Previous dataType prev, - checker, + // Conversion function conv, + // Conversion functions (when text is used in-between) conv1, conv2, - convertion, + // Local references to dataTypes & converters dataTypes = s.dataTypes, converters = s.converters, + // DataType to responseXXX field mapping responses = { "xml": "XML", "text": "Text" }; + // For each dataType in the chain for( i = 0 ; i < dataTypes.length ; i++ ) { current = dataTypes[ i ]; + // If a responseXXX field for this dataType exists + // and if it hasn't been set yet if ( responses[ current ] ) { + // Set it jXHR[ "response" + responses[ current ] ] = response; + // Mark it as set responses[ current ] = 0; } + // If this is not the first element if ( i ) { + // Get the dataType to convert from prev = dataTypes[ i - 1 ]; + // If no catch-all and dataTypes are actually different if ( prev !== "*" && current !== "*" && prev !== current ) { - conv = converters[ ( conversion = prev + " " + current ) ] || + // Get the converter + conv = converters[ prev + " " + current ] || converters[ "* " + current ]; conv1 = conv2 = 0; + // If there is no direct converter and none of the dataTypes is text if ( ! conv && prev !== "text" && current !== "text" ) { + // Try with text in-between conv1 = converters[ prev + " text" ] || converters[ "* text" ]; conv2 = converters[ "text " + current ]; + // Revert back to a single converter + // if one of the converter is an equivalence if ( conv1 === true ) { conv = conv2; } else if ( conv2 === true ) { conv = conv1; } } - + // If we found no converter, dispatch an error if ( ! ( conv || conv1 && conv2 ) ) { throw conversion; } - + // If found converter is not an equivalence if ( conv !== true ) { + // Convert with 1 or 2 converters accordingly response = conv ? conv( response ) : conv2( conv1( response ) ); } } + // If it is the first element of the chain + // and we have a dataFilter } else if ( s.dataFilter ) { - - response = s.dataFilter( response ); + // Apply the dataFilter + response = s.dataFilter( response , current ); + // Get dataTypes again in case the filter changed them dataTypes = s.dataTypes; } } + // End of loop // We have a real success success = response; isSuccess = 1; + // If an exception was thrown } catch(e) { + // We have a parsererror statusText = "parsererror"; error = "" + e; } } - } else { // if not success, mark it as an error + // if not success, mark it as an error + } else { - error = error || statusText; + error = statusText = statusText || "error"; // Set responseText if needed if ( response ) { @@ -483,6 +533,9 @@ jQuery.extend({ deferred.fireReject( callbackContext , [ jXHR , statusText , error ] ); } + // Status-dependent callbacks + jXHR.statusCode( statusCode ); + if ( s.global ) { globalEventContext.trigger( "ajax" + ( isSuccess ? "Success" : "Error" ) , [ jXHR , s , isSuccess ? success : error ] ); @@ -506,23 +559,37 @@ jQuery.extend({ jXHR.error = jXHR.fail; jXHR.complete = completeDeferred.done; + // Status-dependent callbacks + jXHR.statusCode = function( map ) { + if ( map ) { + var resolved = jXHR.isResolved(), + tmp; + if ( resolved || jXHR.isRejected() ) { + tmp = map[ jXHR.status ]; + if ( tmp ) { + if ( map === statusCode ) { + delete statusCode[ jXHR.status ]; + } + jXHR[ resolved ? "done" : "fail" ]( tmp ); + } + } else { + for( tmp in map ) { + statusCode[ tmp ] = [ statusCode[ tmp ] , map[ tmp ] ]; + } + } + } + return this; + }; + // Remove hash character (#7531: and string promotion) s.url = ( "" + s.url ).replace( rhash , "" ); - // Uppercase the type - s.type = s.type.toUpperCase(); - - // Determine if request has content - s.hasContent = ! rnoContent.test( s.type ); - // Extract dataTypes list s.dataTypes = jQuery.trim( s.dataType || "*" ).toLowerCase().split( /\s+/ ); // Determine if a cross-domain request is in order - var parts = rurl.exec( s.url.toLowerCase() ), - loc = location; - if ( ! s.crossDomain ) { + parts = rurl.exec( s.url.toLowerCase() ); s.crossDomain = !!( parts && ( parts[ 1 ] && parts[ 1 ] != loc.protocol || @@ -532,88 +599,97 @@ jQuery.extend({ } // Convert data if not already a string - if ( s.data && s.processData && typeof s.data != "string" ) { + if ( s.data && s.processData && typeof s.data !== "string" ) { s.data = jQuery.param( s.data , s.traditional ); } - // Get transport - transport = jQuery.ajax.prefilter( s , options ).transport( s ); + // Apply prefilters + jQuery.ajaxPrefilter( s , options ); + + // Uppercase the type + s.type = s.type.toUpperCase(); + + // Determine if request has content + s.hasContent = ! rnoContent.test( s.type ); // Watch for a new set of requests if ( s.global && jQuery.active++ === 0 ) { jQuery.event.trigger( "ajaxStart" ); } - // If no transport, we auto-abort - if ( ! transport ) { + // More options handling for requests with no content + if ( ! s.hasContent ) { - done( 0 , "transport not found" ); - jXHR = false; + // If data is available, append data to url + if ( s.data ) { + s.url += ( rquery.test( s.url ) ? "&" : "?" ) + s.data; + } + + // Add anti-cache in url if needed + if ( s.cache === false ) { + + var ts = jQuery.now(), + // try replacing _= if it is there + ret = s.url.replace( rts , "$1_=" + ts ); + + // if nothing was replaced, add timestamp to the end + s.url = ret + ( (ret == s.url ) ? ( rquery.test( s.url ) ? "&" : "?" ) + "_=" + ts : ""); + } + } + + // Set the correct header, if data is being sent + if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) { + requestHeaders[ "content-type" ] = s.contentType; + } + + // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. + if ( s.ifModified ) { + if ( jQuery_lastModified[ s.url ] ) { + requestHeaders[ "if-modified-since" ] = jQuery_lastModified[ s.url ]; + } + if ( jQuery_etag[ s.url ] ) { + requestHeaders[ "if-none-match" ] = jQuery_etag[ s.url ]; + } + } + + // Set the Accepts header for the server, depending on the dataType + requestHeaders.accept = s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[ 0 ] ] ? + s.accepts[ s.dataTypes[ 0 ] ] + ( s.dataTypes[ 0 ] !== "*" ? ", */*; q=0.01" : "" ) : + s.accepts[ "*" ]; + + // Check for headers option + for ( i in s.headers ) { + requestHeaders[ i.toLowerCase() ] = s.headers[ i ]; + } + + // Allow custom headers/mimetypes and early abort + if ( s.beforeSend && ( s.beforeSend.call( callbackContext , jXHR , s ) === false || state === 2 ) ) { + + // Abort if not done already + done( 0 , "abort" ); + + // Return false + jXHR = false; } else { - // More options handling for requests with no content - if ( ! s.hasContent ) { - - // If data is available, append data to url - if ( s.data ) { - s.url += ( rquery.test( s.url ) ? "&" : "?" ) + s.data; - } - - // Add anti-cache in url if needed - if ( s.cache === false ) { - - var ts = jQuery.now(), - // try replacing _= if it is there - ret = s.url.replace( rts , "$1_=" + ts ); - - // if nothing was replaced, add timestamp to the end - s.url = ret + ( (ret == s.url ) ? ( rquery.test( s.url ) ? "&" : "?" ) + "_=" + ts : ""); - } + // Install callbacks on deferreds + for ( i in { success:1, error:1, complete:1 } ) { + jXHR[ i ]( s[ i ] ); } - // Set the correct header, if data is being sent - if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) { - requestHeaders[ "content-type" ] = s.contentType; - } + // Get transport + transport = jQuery.ajaxTransport( s ); - // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. - if ( s.ifModified ) { - if ( jQuery_lastModified[ s.url ] ) { - requestHeaders[ "if-modified-since" ] = jQuery_lastModified[ s.url ]; - } - if ( jQuery_etag[ s.url ] ) { - requestHeaders[ "if-none-match" ] = jQuery_etag[ s.url ]; - } - } + // If no transport, we auto-abort + if ( ! transport ) { - // Set the Accepts header for the server, depending on the dataType - requestHeaders.accept = s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[ 0 ] ] ? - s.accepts[ s.dataTypes[ 0 ] ] + ( s.dataTypes[ 0 ] !== "*" ? ", */*; q=0.01" : "" ) : - s.accepts[ "*" ]; - - // Check for headers option - for ( i in s.headers ) { - requestHeaders[ i.toLowerCase() ] = s.headers[ i ]; - } - - // Allow custom headers/mimetypes and early abort - if ( s.beforeSend && ( s.beforeSend.call( callbackContext , jXHR , s ) === false || state === 2 ) ) { - - // Abort if not done already - done( 0 , "abort" ); - jXHR = false; + done( 0 , "notransport" ); } else { // Set state as sending - state = 1; - jXHR.readyState = 1; - - // Install callbacks on deferreds - for ( i in { success:1, error:1, complete:1 } ) { - jXHR[ i ]( s[ i ] ); - } + state = jXHR.readyState = 1; // Send global event if ( s.global ) { @@ -787,7 +863,7 @@ function ajax_selectOrExecute( structure , s ) { } } - return noSelect ? jQuery.ajax : selected; + return noSelect ? jQuery : selected; } // Add an element to one of the structures in ajaxSettings @@ -846,13 +922,13 @@ function ajax_addElement( structure , args ) { } } - return jQuery.ajax; + return jQuery; } // Install prefilter & transport methods -jQuery.each( [ "prefilter" , "transport" ] , function( _ , name ) { - _ = name + "s"; - jQuery.ajax[ name ] = function() { +jQuery.each( [ "Prefilter" , "Transport" ] , function( _ , name ) { + _ = name.toLowerCase() + "s"; + jQuery[ "ajax" + name ] = function() { return ajax_addElement( _ , arguments ); }; } ); diff --git a/src/ajax/jsonp.js b/src/ajax/jsonp.js index f4b324e17..883876fc0 100644 --- a/src/ajax/jsonp.js +++ b/src/ajax/jsonp.js @@ -1,8 +1,7 @@ (function( jQuery ) { var jsc = jQuery.now(), - jsre = /\=(?:\?|%3F)(&|$)/i, - rquery_jsonp = /\?/; + jsre = /(\=)(?:\?|%3F)(&|$)|()(?:\?\?|%3F%3F)()/i; // Default jsonp settings jQuery.ajaxSetup({ @@ -10,75 +9,78 @@ jQuery.ajaxSetup({ jsonpCallback: function() { return "jsonp" + jsc++; } -}); -// Normalize jsonp queries -// 1) put callback parameter in url or data -// 2) sneakily ensure transportDataType is always jsonp for jsonp requests -jQuery.ajax.prefilter("json jsonp", function(s, originalSettings) { +// Detect, normalize options and install callbacks for jsonp requests +// (dataIsString is used internally) +}).ajaxPrefilter("json jsonp", function(s, originalSettings, dataIsString) { + + dataIsString = ( typeof(s.data) === "string" ); if ( s.dataTypes[ 0 ] === "jsonp" || - originalSettings.jsonp || originalSettings.jsonpCallback || - jsre.test(s.url) || - typeof(s.data) === "string" && jsre.test(s.data) ) { + originalSettings.jsonp != null || + s.jsonp !== false && ( jsre.test( s.url ) || + dataIsString && jsre.test( s.data ) ) ) { - var jsonpCallback = s.jsonpCallback = + var responseContainer, + jsonpCallback = s.jsonpCallback = jQuery.isFunction( s.jsonpCallback ) ? s.jsonpCallback() : s.jsonpCallback, - url = s.url.replace(jsre, "=" + jsonpCallback + "$1"), - data = s.url === url && typeof(s.data) === "string" ? s.data.replace(jsre, "=" + jsonpCallback + "$1") : s.data; + previous = window[ jsonpCallback ], + url = s.url, + data = s.data, + replace = "$1" + jsonpCallback + "$2"; - if ( url === s.url && data === s.data ) { - url += (rquery_jsonp.test( url ) ? "&" : "?") + s.jsonp + "=" + jsonpCallback; + if ( s.jsonp !== false ) { + url = url.replace( jsre, replace ); + if ( s.url === url ) { + if ( dataIsString ) { + data = data.replace( jsre, replace ); + } + if ( s.data === data ) { + // Add callback manually + url += (/\?/.test( url ) ? "&" : "?") + s.jsonp + "=" + jsonpCallback; + } + } } s.url = url; s.data = data; - s.dataTypes[ 0 ] = "jsonp"; - } -// Bind transport to jsonp dataType -}).transport("jsonp", function(s) { + window [ jsonpCallback ] = function( response ) { + responseContainer = [response]; + }; - // Put callback in place - var responseContainer, - jsonpCallback = s.jsonpCallback, - previous = window[ jsonpCallback ]; + s.complete = [function() { - window [ jsonpCallback ] = function( response ) { - responseContainer = [response]; - }; + // Set callback back to previous value + window[ jsonpCallback ] = previous; - s.complete = [function() { - - // Set callback back to previous value - window[ jsonpCallback ] = previous; - - // Call if it was a function and we have a response - if ( previous) { - if ( responseContainer && jQuery.isFunction ( previous ) ) { - window[ jsonpCallback ] ( responseContainer[0] ); + // Call if it was a function and we have a response + if ( previous) { + if ( responseContainer && jQuery.isFunction ( previous ) ) { + window[ jsonpCallback ] ( responseContainer[0] ); + } + } else { + // else, more memory leak avoidance + try{ delete window[ jsonpCallback ]; } catch(e){} } - } else { - // else, more memory leak avoidance - try{ delete window[ jsonpCallback ]; } catch(e){} - } - }, s.complete ]; + }, s.complete ]; - // Sneakily ensure this will be handled as json - s.dataTypes[ 0 ] = "json"; + // Use data converter to retrieve json after script execution + s.converters["script json"] = function() { + if ( ! responseContainer ) { + jQuery.error( jsonpCallback + " was not called" ); + } + return responseContainer[ 0 ]; + }; - // Use data converter to retrieve json after script execution - s.converters["script json"] = function() { - if ( ! responseContainer ) { - jQuery.error( jsonpCallback + " was not called" ); - } - return responseContainer[ 0 ]; - }; + // force json dataType + s.dataTypes[ 0 ] = "json"; - // Delegate to script transport - return "script"; + // Delegate to script + return "script"; + } }); })( jQuery ); diff --git a/src/ajax/script.js b/src/ajax/script.js index 0db0de6c7..b0e576f27 100644 --- a/src/ajax/script.js +++ b/src/ajax/script.js @@ -1,7 +1,7 @@ (function( jQuery ) { -// Install text to script executor -jQuery.extend( true, jQuery.ajaxSettings , { +// Install script dataType +jQuery.ajaxSetup({ accepts: { script: "text/javascript, application/javascript" @@ -14,20 +14,24 @@ jQuery.extend( true, jQuery.ajaxSettings , { converters: { "text script": jQuery.globalEval } -} ); -// Bind script tag hack transport -jQuery.ajax.transport("script", function(s) { +// Handle cache's special case and global +}).ajaxPrefilter("script", function(s) { - // Handle cache special case if ( s.cache === undefined ) { s.cache = false; } - // This transport only deals with cross domain get requests - if ( s.crossDomain && s.async && ( s.type === "GET" || ! s.data ) ) { - + if ( s.crossDomain ) { + s.type = "GET"; s.global = false; + } + +// Bind script tag hack transport +}).ajaxTransport("script", function(s) { + + // This transport only deals with cross domain requests + if ( s.crossDomain ) { var script, head = document.getElementsByTagName("head")[0] || document.documentElement; @@ -47,7 +51,7 @@ jQuery.ajax.transport("script", function(s) { script.src = s.url; // Attach handlers for all browsers - script.onload = script.onreadystatechange = function( _ , statusText) { + script.onload = script.onreadystatechange = function( _ , isAbort ) { if ( ! script.readyState || /loaded|complete/.test( script.readyState ) ) { @@ -59,10 +63,13 @@ jQuery.ajax.transport("script", function(s) { head.removeChild( script ); } + // Dereference the script script = 0; - // Callback - callback( statusText ? 0 : 200, statusText || "success" ); + // Callback if not abort + if ( ! isAbort ) { + callback( 200, "success" ); + } } }; // Use insertBefore instead of appendChild to circumvent an IE6 bug. @@ -70,9 +77,9 @@ jQuery.ajax.transport("script", function(s) { head.insertBefore( script, head.firstChild ); }, - abort: function(statusText) { + abort: function() { if ( script ) { - script.onload( 0 , statusText ); + script.onload(0,1); } } }; diff --git a/src/ajax/xhr.js b/src/ajax/xhr.js index 26e91ce79..34aa832fe 100644 --- a/src/ajax/xhr.js +++ b/src/ajax/xhr.js @@ -1,19 +1,16 @@ (function( jQuery ) { -var // Next fake timer id - xhrPollingId = jQuery.now(), +var // Next active xhr id + xhrId = jQuery.now(), - // Callbacks hashtable + // active xhrs xhrs = {}, - // XHR pool - xhrPool = [], - - // #5280: see end of file - xhrUnloadAbortMarker; + // #5280: see below + xhrUnloadAbortInstalled; -jQuery.ajax.transport( function( s , determineDataType ) { +jQuery.ajaxTransport( function( s , determineDataType ) { // Cross domain only allowed if supported through XMLHttpRequest if ( ! s.crossDomain || jQuery.support.cors ) { @@ -25,26 +22,24 @@ jQuery.ajax.transport( function( s , determineDataType ) { send: function(headers, complete) { // #5280: we need to abort on unload or IE will keep connections alive - if ( ! xhrUnloadAbortMarker ) { + if ( ! xhrUnloadAbortInstalled ) { - xhrUnloadAbortMarker = []; + xhrUnloadAbortInstalled = 1; jQuery(window).bind( "unload" , function() { // Abort all pending requests jQuery.each(xhrs, function(_, xhr) { if ( xhr.onreadystatechange ) { - xhr.onreadystatechange( xhrUnloadAbortMarker ); + xhr.onreadystatechange( 1 ); } }); - // Reset polling structure to be safe - xhrs = {}; - }); } - var xhr = xhrPool.pop() || s.xhr(), + // Get a new xhr + var xhr = s.xhr(), handle; // Open the socket @@ -58,7 +53,7 @@ jQuery.ajax.transport( function( s , determineDataType ) { // Requested-With header // Not set for crossDomain requests with no content // (see why at http://trac.dojotoolkit.org/ticket/9486) - // Won't change header if already provided in beforeSend + // Won't change header if already provided if ( ! ( s.crossDomain && ! s.hasContent ) && ! headers["x-requested-with"] ) { headers["x-requested-with"] = "XMLHttpRequest"; } @@ -76,49 +71,40 @@ jQuery.ajax.transport( function( s , determineDataType ) { try { xhr.send( ( s.hasContent && s.data ) || null ); } catch(e) { - // Store back in pool - xhrPool.push( xhr ); complete(0, "error", "" + e); return; } // Listener - callback = function ( abortStatusText ) { + callback = function( _ , isAbort ) { // Was never called and is aborted or complete - if ( callback && ( abortStatusText || xhr.readyState === 4 ) ) { + if ( callback && ( isAbort || xhr.readyState === 4 ) ) { - // Do not listen anymore - // and Store back in pool + // Only called once + callback = 0; + + // Do not keep as active anymore + // and store back into pool if (handle) { xhr.onreadystatechange = jQuery.noop; delete xhrs[ handle ]; - handle = undefined; - xhrPool.push( xhr ); } - callback = 0; - - // Get info - var status, statusText, response, responseHeaders; - - if ( abortStatusText ) { + // If it's an abort + if ( isAbort ) { + // Abort it manually if needed if ( xhr.readyState !== 4 ) { xhr.abort(); } - - // Stop here if unloadAbort - if ( abortStatusText === xhrUnloadAbortMarker ) { - return; - } - - status = 0; - statusText = abortStatusText; - } else { - status = xhr.status; + // Get info + var status = xhr.status, + statusText, + response, + responseHeaders = xhr.getAllResponseHeaders(); try { // Firefox throws an exception when accessing statusText for faulty cross-domain requests @@ -130,8 +116,6 @@ jQuery.ajax.transport( function( s , determineDataType ) { } - responseHeaders = xhr.getAllResponseHeaders(); - // Filter status for non standard behaviours // (so many they seem to be the actual "standard") status = @@ -164,10 +148,10 @@ jQuery.ajax.transport( function( s , determineDataType ) { xhr.getResponseHeader("content-type"), xhr.responseText, xhr.responseXML ); - } - // Call complete - complete(status,statusText,response,responseHeaders); + // Call complete + complete(status,statusText,response,responseHeaders); + } } }; @@ -180,18 +164,16 @@ jQuery.ajax.transport( function( s , determineDataType ) { } else { - // Listener is externalized to handle abort on unload - handle = xhrPollingId++; + // Add to list of active xhrs + handle = xhrId++; xhrs[ handle ] = xhr; - xhr.onreadystatechange = function() { - callback(); - }; + xhr.onreadystatechange = callback; } }, - abort: function(statusText) { + abort: function() { if ( callback ) { - callback(statusText); + callback(0,1); } } }; diff --git a/src/attributes.js b/src/attributes.js index fec132340..d37400a68 100644 --- a/src/attributes.js +++ b/src/attributes.js @@ -133,11 +133,11 @@ jQuery.fn.extend({ } else if ( type === "undefined" || type === "boolean" ) { if ( this.className ) { // store className if set - jQuery.data( this, "__className__", this.className ); + jQuery._data( this, "__className__", this.className ); } // toggle whole className - this.className = this.className || value === false ? "" : jQuery.data( this, "__className__" ) || ""; + this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || ""; } }); }, diff --git a/src/core.js b/src/core.js index b9e6d816f..4311e3103 100644 --- a/src/core.js +++ b/src/core.js @@ -3,7 +3,7 @@ var jQuery = (function() { // Define a local copy of jQuery var jQuery = function( selector, context ) { // The jQuery object is actually just the init constructor 'enhanced' - return new jQuery.fn.init( selector, context ); + return new jQuery.fn.init( selector, context, rootjQuery ); }, // Map over jQuery in case of overwrite @@ -63,6 +63,9 @@ var jQuery = function( selector, context ) { // The deferred used on DOM ready readyList, + // Promise methods + promiseMethods = "then done fail isResolved isRejected promise".split( " " ), + // The ready event handler DOMContentLoaded, @@ -78,7 +81,8 @@ var jQuery = function( selector, context ) { class2type = {}; jQuery.fn = jQuery.prototype = { - init: function( selector, context ) { + constructor: jQuery, + init: function( selector, context, rootjQuery ) { var match, elem, ret, doc; // Handle $(""), $(null), or $(undefined) @@ -112,6 +116,7 @@ jQuery.fn = jQuery.prototype = { // HANDLE: $(html) -> $(array) if ( match[1] ) { + context = context instanceof jQuery ? context[0] : context; doc = (context ? context.ownerDocument || context : document); // If a single string is passed in and it's a single tag @@ -129,7 +134,7 @@ jQuery.fn = jQuery.prototype = { } else { ret = jQuery.buildFragment( [ match[1] ], [ doc ] ); - selector = (ret.cacheable ? ret.fragment.cloneNode(true) : ret.fragment).childNodes; + selector = (ret.cacheable ? jQuery(ret.fragment).clone()[0] : ret.fragment).childNodes; } return jQuery.merge( this, selector ); @@ -171,7 +176,7 @@ jQuery.fn = jQuery.prototype = { // HANDLE: $(expr, context) // (which is just equivalent to: $(context).find(expr) } else { - return jQuery( context ).find( selector ); + return this.constructor( context ).find( selector ); } // HANDLE: $(function) @@ -222,7 +227,7 @@ jQuery.fn = jQuery.prototype = { // (returning the new matched element set) pushStack: function( elems, name, selector ) { // Build a new jQuery matched element set - var ret = jQuery(); + var ret = this.constructor(); if ( jQuery.isArray( elems ) ) { push.apply( ret, elems ); @@ -287,7 +292,7 @@ jQuery.fn = jQuery.prototype = { }, end: function() { - return this.prevObject || jQuery(null); + return this.prevObject || this.constructor(null); }, // For internal use only. @@ -578,7 +583,7 @@ jQuery.extend({ script.type = "text/javascript"; - if ( jQuery.support.scriptEval ) { + if ( jQuery.support.scriptEval() ) { script.appendChild( document.createTextNode( data ) ); } else { script.text = data; @@ -896,9 +901,10 @@ jQuery.extend({ Deferred: function( func ) { var deferred = jQuery._Deferred(), - failDeferred = jQuery._Deferred(); + failDeferred = jQuery._Deferred(), + promise; - // Add errorDeferred methods and redefine cancel + // Add errorDeferred methods, then and promise jQuery.extend( deferred , { then: function( doneCallbacks , failCallbacks ) { @@ -911,14 +917,18 @@ jQuery.extend({ isRejected: failDeferred.isResolved, // Get a promise for this deferred // If obj is provided, the promise aspect is added to the object - promise: function( obj ) { - obj = obj || {}; - jQuery.each( "then done fail isResolved isRejected".split( " " ) , function( _ , method ) { - obj[ method ] = deferred[ method ]; - }); - obj.promise = function() { - return obj; - }; + // (i is used internally) + promise: function( obj , i ) { + if ( obj == null ) { + if ( promise ) { + return promise; + } + promise = obj = {}; + } + i = promiseMethods.length; + while( i-- ) { + obj[ promiseMethods[ i ] ] = deferred[ promiseMethods[ i ] ]; + } return obj; } @@ -940,10 +950,32 @@ jQuery.extend({ // Deferred helper when: function( object ) { - object = object && jQuery.isFunction( object.promise ) ? - object : - jQuery.Deferred().resolve( object ); - return object.promise(); + var args = arguments, + length = args.length, + deferred = length <= 1 && object && jQuery.isFunction( object.promise ) ? + object : + jQuery.Deferred(), + promise = deferred.promise(), + resolveArray; + + if ( length > 1 ) { + resolveArray = new Array( length ); + jQuery.each( args, function( index, element, args ) { + jQuery.when( element ).done( function( value ) { + args = arguments; + resolveArray[ index ] = args.length > 1 ? slice.call( args , 0 ) : value; + if( ! --length ) { + deferred.fire( promise, resolveArray ); + } + }).fail( function() { + deferred.fireReject( promise, arguments ); + }); + return !deferred.isRejected(); + }); + } else if ( deferred !== object ) { + deferred.resolve( object ); + } + return promise; }, // Use of jQuery.browser is frowned upon. @@ -960,6 +992,25 @@ jQuery.extend({ return { browser: match[1] || "", version: match[2] || "0" }; }, + subclass: function(){ + function jQuerySubclass( selector, context ) { + return new jQuerySubclass.fn.init( selector, context ); + } + jQuerySubclass.superclass = this; + jQuerySubclass.fn = jQuerySubclass.prototype = this(); + jQuerySubclass.fn.constructor = jQuerySubclass; + jQuerySubclass.subclass = this.subclass; + jQuerySubclass.fn.init = function init( selector, context ) { + if (context && context instanceof jQuery && !(context instanceof jQuerySubclass)){ + context = jQuerySubclass(context); + } + return jQuery.fn.init.call( this, selector, context, rootjQuerySubclass ); + }; + jQuerySubclass.fn.init.prototype = jQuerySubclass.fn; + var rootjQuerySubclass = jQuerySubclass(document); + return jQuerySubclass; + }, + browser: {} }); diff --git a/src/css.js b/src/css.js index 8a83c6072..19c6342d2 100644 --- a/src/css.js +++ b/src/css.js @@ -263,8 +263,9 @@ if ( document.defaultView && document.defaultView.getComputedStyle ) { if ( document.documentElement.currentStyle ) { currentStyle = function( elem, name ) { - var left, rsLeft, + var left, ret = elem.currentStyle && elem.currentStyle[ name ], + rsLeft = elem.runtimeStyle && elem.runtimeStyle[ name ], style = elem.style; // From the awesome hack by Dean Edwards @@ -275,16 +276,19 @@ if ( document.documentElement.currentStyle ) { if ( !rnumpx.test( ret ) && rnum.test( ret ) ) { // Remember the original values left = style.left; - rsLeft = elem.runtimeStyle.left; // Put in the new values to get a computed value out - elem.runtimeStyle.left = elem.currentStyle.left; + if ( rsLeft ) { + elem.runtimeStyle.left = elem.currentStyle.left; + } style.left = name === "fontSize" ? "1em" : (ret || 0); ret = style.pixelLeft + "px"; // Revert the changed values style.left = left; - elem.runtimeStyle.left = rsLeft; + if ( rsLeft ) { + elem.runtimeStyle.left = rsLeft; + } } return ret === "" ? "auto" : ret; diff --git a/src/data.js b/src/data.js index 4d1d1bd55..21f0e3a55 100644 --- a/src/data.js +++ b/src/data.js @@ -1,7 +1,6 @@ (function( jQuery ) { -var windowData = {}, - rbrace = /^(?:\{.*\}|\[.*\])$/; +var rbrace = /^(?:\{.*\}|\[.*\])$/; jQuery.extend({ cache: {}, @@ -23,108 +22,168 @@ jQuery.extend({ }, hasData: function( elem ) { - if ( elem.nodeType ) { - elem = jQuery.cache[ elem[jQuery.expando] ]; - } + elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; return !!elem && !jQuery.isEmptyObject(elem); }, - data: function( elem, name, data ) { + data: function( elem, name, data, pvt /* Internal Use Only */ ) { if ( !jQuery.acceptData( elem ) ) { return; } - elem = elem == window ? - windowData : - elem; + var internalKey = jQuery.expando, getByName = typeof name === "string", thisCache, - var isNode = elem.nodeType, - id = isNode ? elem[ jQuery.expando ] : null, - cache = jQuery.cache, thisCache; + // We have to handle DOM nodes and JS objects differently because IE6-7 + // can't GC object references properly across the DOM-JS boundary + isNode = elem.nodeType, - if ( isNode && !id && typeof name === "string" && data === undefined ) { + // Only DOM nodes need the global jQuery cache; JS object data is + // attached directly to the object so GC can occur automatically + cache = isNode ? jQuery.cache : elem, + + // Only defining an ID for JS objects if its cache already exists allows + // the code to shortcut on the same path as a DOM node with no cache + id = isNode ? elem[ jQuery.expando ] : elem[ jQuery.expando ] && jQuery.expando; + + // Avoid doing any more work than we need to when trying to get data on an + // object that has no data at all + if ( (!id || (pvt && id && !cache[ id ][ internalKey ])) && getByName && data === undefined ) { return; } - // Get the data from the object directly - if ( !isNode ) { - cache = elem; - - // Compute a unique ID for the element - } else if ( !id ) { - elem[ jQuery.expando ] = id = ++jQuery.uuid; + if ( !id ) { + // Only DOM nodes need a new unique ID for each element since their data + // ends up in the global cache + if ( isNode ) { + elem[ jQuery.expando ] = id = ++jQuery.uuid; + } else { + id = jQuery.expando; + } } - // Avoid generating a new cache unless none exists and we - // want to manipulate it. - if ( typeof name === "object" ) { - if ( isNode ) { - cache[ id ] = jQuery.extend(cache[ id ], name); - - } else { - jQuery.extend( cache, name ); - } - - } else if ( isNode && !cache[ id ] ) { + if ( !cache[ id ] ) { cache[ id ] = {}; } - thisCache = isNode ? cache[ id ] : cache; + // An object can be passed to jQuery.data instead of a key/value pair; this gets + // shallow copied over onto the existing cache + if ( typeof name === "object" ) { + if ( pvt ) { + cache[ id ][ internalKey ] = jQuery.extend(cache[ id ][ internalKey ], name); + } else { + cache[ id ] = jQuery.extend(cache[ id ], name); + } + } + + thisCache = cache[ id ]; + + // Internal jQuery data is stored in a separate object inside the object's data + // cache in order to avoid key collisions between internal data and user-defined + // data + if ( pvt ) { + if ( !thisCache[ internalKey ] ) { + thisCache[ internalKey ] = {}; + } + + thisCache = thisCache[ internalKey ]; + } - // Prevent overriding the named cache with undefined values if ( data !== undefined ) { thisCache[ name ] = data; } - return typeof name === "string" ? thisCache[ name ] : thisCache; + // TODO: This is a hack for 1.5 ONLY. It will be removed in 1.6. Users should + // not attempt to inspect the internal events object using jQuery.data, as this + // internal data object is undocumented and subject to change. + if ( name === "events" && !thisCache[name] ) { + return thisCache[ internalKey ] && thisCache[ internalKey ].events; + } + + return getByName ? thisCache[ name ] : thisCache; }, - removeData: function( elem, name ) { + removeData: function( elem, name, pvt /* Internal Use Only */ ) { if ( !jQuery.acceptData( elem ) ) { return; } - elem = elem == window ? - windowData : - elem; + var internalKey = jQuery.expando, isNode = elem.nodeType, - var isNode = elem.nodeType, - id = isNode ? elem[ jQuery.expando ] : elem, - cache = jQuery.cache, - thisCache = isNode ? cache[ id ] : id; + // See jQuery.data for more information + cache = isNode ? jQuery.cache : elem, + + // See jQuery.data for more information + id = isNode ? elem[ jQuery.expando ] : jQuery.expando; + + // If there is already no cache entry for this object, there is no + // purpose in continuing + if ( !cache[ id ] ) { + return; + } - // If we want to remove a specific section of the element's data if ( name ) { + var thisCache = pvt ? cache[ id ][ internalKey ] : cache[ id ]; + if ( thisCache ) { - // Remove the section of cache data delete thisCache[ name ]; - // If we've removed all the data, remove the element's cache - if ( isNode && jQuery.isEmptyObject(thisCache) ) { - jQuery.removeData( elem ); - } - } - - // Otherwise, we want to remove all of the element's data - } else { - if ( isNode && jQuery.support.deleteExpando ) { - delete elem[ jQuery.expando ]; - - } else if ( elem.removeAttribute ) { - elem.removeAttribute( jQuery.expando ); - - // Completely remove the data cache - } else if ( isNode ) { - delete cache[ id ]; - - // Remove all fields from the object - } else { - for ( var n in elem ) { - delete elem[ n ]; + // If there is no data left in the cache, we want to continue + // and let the cache object itself get destroyed + if ( !jQuery.isEmptyObject(thisCache) ) { + return; } } } + + // See jQuery.data for more information + if ( pvt ) { + delete cache[ id ][ internalKey ]; + + // Don't destroy the parent cache unless the internal data object + // had been the only thing left in it + if ( !jQuery.isEmptyObject(cache[ id ]) ) { + return; + } + } + + var internalCache = cache[ id ][ internalKey ]; + + // Browsers that fail expando deletion also refuse to delete expandos on + // the window, but it will allow it on all other JS objects; other browsers + // don't care + if ( jQuery.support.deleteExpando || cache != window ) { + delete cache[ id ]; + } else { + cache[ id ] = null; + } + + // We destroyed the entire user cache at once because it's faster than + // iterating through each key, but we need to continue to persist internal + // data if it existed + if ( internalCache ) { + cache[ id ] = {}; + cache[ id ][ internalKey ] = internalCache; + + // Otherwise, we need to eliminate the expando on the node to avoid + // false lookups in the cache for entries that no longer exist + } else if ( isNode ) { + // IE does not allow us to delete expando properties from nodes, + // nor does it have a removeAttribute function on Document nodes; + // we must handle all of these cases + if ( jQuery.support.deleteExpando ) { + delete elem[ jQuery.expando ]; + } else if ( elem.removeAttribute ) { + elem.removeAttribute( jQuery.expando ); + } else { + elem[ jQuery.expando ] = null; + } + } + }, + + // For internal use only. + _data: function( elem, name, data ) { + return jQuery.data( elem, name, data, true ); }, // A method for determining if a DOM node can handle the data expando diff --git a/src/effects.js b/src/effects.js index bd57ffc3d..b0675395f 100644 --- a/src/effects.js +++ b/src/effects.js @@ -27,7 +27,7 @@ jQuery.fn.extend({ // Reset the inline display of this element to learn if it is // being hidden by cascaded rules or not - if ( !jQuery.data(elem, "olddisplay") && display === "none" ) { + if ( !jQuery._data(elem, "olddisplay") && display === "none" ) { display = elem.style.display = ""; } @@ -35,7 +35,7 @@ jQuery.fn.extend({ // in a stylesheet to whatever the default browser style is // for such an element if ( display === "" && jQuery.css( elem, "display" ) === "none" ) { - jQuery.data(elem, "olddisplay", defaultDisplay(elem.nodeName)); + jQuery._data(elem, "olddisplay", defaultDisplay(elem.nodeName)); } } @@ -46,7 +46,7 @@ jQuery.fn.extend({ display = elem.style.display; if ( display === "" || display === "none" ) { - elem.style.display = jQuery.data(elem, "olddisplay") || ""; + elem.style.display = jQuery._data(elem, "olddisplay") || ""; } } @@ -62,8 +62,8 @@ jQuery.fn.extend({ for ( var i = 0, j = this.length; i < j; i++ ) { var display = jQuery.css( this[i], "display" ); - if ( display !== "none" && !jQuery.data( this[i], "olddisplay" ) ) { - jQuery.data( this[i], "olddisplay", display ); + if ( display !== "none" && !jQuery._data( this[i], "olddisplay" ) ) { + jQuery._data( this[i], "olddisplay", display ); } } diff --git a/src/event.js b/src/event.js index 675e5fff3..2ddf28812 100644 --- a/src/event.js +++ b/src/event.js @@ -8,7 +8,8 @@ var rnamespaces = /\.(.*)$/, fcleanup = function( nm ) { return nm.replace(rescape, "\\$&"); }, - focusCounts = { focusin: 0, focusout: 0 }; + focusCounts = { focusin: 0, focusout: 0 }, + eventKey = "events"; /* * A number of helper functions used for managing events. @@ -50,7 +51,7 @@ jQuery.event = { } // Init the element's event structure - var elemData = jQuery.data( elem ); + var elemData = jQuery._data( elem ); // If no elemData is found then we must be trying to bind to one of the // banned noData elements @@ -58,10 +59,7 @@ jQuery.event = { return; } - // Use a key less likely to result in collisions for plain JS objects. - // Fixes bug #7150. - var eventKey = elem.nodeType ? "events" : "__events__", - events = elemData[ eventKey ], + var events = elemData[ eventKey ], eventHandle = elemData.handle; if ( typeof events === "function" ) { @@ -177,8 +175,7 @@ jQuery.event = { } var ret, type, fn, j, i = 0, all, namespaces, namespace, special, eventType, handleObj, origType, - eventKey = elem.nodeType ? "events" : "__events__", - elemData = jQuery.data( elem ), + elemData = jQuery.hasData( elem ) && jQuery._data( elem ), events = elemData && elemData[ eventKey ]; if ( !elemData || !events ) { @@ -290,10 +287,10 @@ jQuery.event = { delete elemData.handle; if ( typeof elemData === "function" ) { - jQuery.removeData( elem, eventKey ); + jQuery.removeData( elem, eventKey, true ); } else if ( jQuery.isEmptyObject( elemData ) ) { - jQuery.removeData( elem ); + jQuery.removeData( elem, undefined, true ); } } }, @@ -325,9 +322,16 @@ jQuery.event = { // Only trigger if we've ever bound an event for it if ( jQuery.event.global[ type ] ) { + // XXX This code smells terrible. event.js should not be directly + // inspecting the data cache jQuery.each( jQuery.cache, function() { - if ( this.events && this.events[type] ) { - jQuery.event.trigger( event, data, this.handle.elem ); + // internalKey variable is just used to make it easier to find + // and potentially change this stuff later; currently it just + // points to jQuery.expando + var internalKey = jQuery.expando, + internalCache = this[ internalKey ]; + if ( internalCache && internalCache.events && internalCache.events[type] ) { + jQuery.event.trigger( event, data, internalCache.handle.elem ); } }); } @@ -353,8 +357,8 @@ jQuery.event = { // Trigger the event, it is assumed that "handle" is a function var handle = elem.nodeType ? - jQuery.data( elem, "handle" ) : - (jQuery.data( elem, "__events__" ) || {}).handle; + jQuery._data( elem, "handle" ) : + (jQuery._data( elem, eventKey ) || {}).handle; if ( handle ) { handle.apply( elem, data ); @@ -432,7 +436,7 @@ jQuery.event = { event.namespace = event.namespace || namespace_sort.join("."); - events = jQuery.data(this, this.nodeType ? "events" : "__events__"); + events = jQuery._data(this, eventKey); if ( typeof events === "function" ) { events = events.events; @@ -603,7 +607,7 @@ jQuery.Event = function( src ) { // Events bubbling up the document may have been marked as prevented // by a handler lower down the tree; reflect the correct value. - this.isDefaultPrevented = (src.defaultPrevented || src.returnValue === false || + this.isDefaultPrevented = (src.defaultPrevented || src.returnValue === false || src.getPreventDefault && src.getPreventDefault()) ? returnTrue : returnFalse; // Event type @@ -787,12 +791,12 @@ if ( !jQuery.support.changeBubbles ) { return; } - data = jQuery.data( elem, "_change_data" ); + data = jQuery._data( elem, "_change_data" ); val = getVal(elem); // the current data will be also retrieved by beforeactivate if ( e.type !== "focusout" || elem.type !== "radio" ) { - jQuery.data( elem, "_change_data", val ); + jQuery._data( elem, "_change_data", val ); } if ( data === undefined || val === data ) { @@ -837,7 +841,7 @@ if ( !jQuery.support.changeBubbles ) { // information beforeactivate: function( e ) { var elem = e.target; - jQuery.data( elem, "_change_data", getVal(elem) ); + jQuery._data( elem, "_change_data", getVal(elem) ); } }, @@ -986,8 +990,8 @@ jQuery.fn.extend({ return this.click( jQuery.proxy( fn, function( event ) { // Figure out which function to execute - var lastToggle = ( jQuery.data( this, "lastToggle" + fn.guid ) || 0 ) % i; - jQuery.data( this, "lastToggle" + fn.guid, lastToggle + 1 ); + var lastToggle = ( jQuery._data( this, "lastToggle" + fn.guid ) || 0 ) % i; + jQuery._data( this, "lastToggle" + fn.guid, lastToggle + 1 ); // Make sure that clicks stop event.preventDefault(); @@ -1075,7 +1079,7 @@ function liveHandler( event ) { var stop, maxLevel, related, match, handleObj, elem, j, i, l, data, close, namespace, ret, elems = [], selectors = [], - events = jQuery.data( this, this.nodeType ? "events" : "__events__" ); + events = jQuery._data( this, eventKey ); if ( typeof events === "function" ) { events = events.events; diff --git a/src/manipulation.js b/src/manipulation.js index 493082212..596a45736 100644 --- a/src/manipulation.js +++ b/src/manipulation.js @@ -346,7 +346,7 @@ jQuery.fn.extend({ table ? root(this[i], first) : this[i], - i > 0 || results.cacheable || this.length > 1 ? + i > 0 || results.cacheable || (this.length > 1 && i > 0) ? jQuery(fragment).clone(true)[0] : fragment ); @@ -381,17 +381,24 @@ function cloneCopyEvent(orig, ret) { throw "Cloned data mismatch"; } - var oldData = jQuery.data( orig[nodeIndex] ), - curData = jQuery.data( this, oldData ), - events = oldData && oldData.events; + var internalKey = jQuery.expando, + oldData = jQuery.data( orig[nodeIndex] ), + curData = jQuery.data( this, oldData ); - if ( events ) { - delete curData.handle; - curData.events = {}; + // Switch to use the internal data object, if it exists, for the next + // stage of data copying + if ( (oldData = oldData[ internalKey ]) ) { + var events = oldData.events; + curData = curData[ internalKey ] = jQuery.extend({}, oldData); - for ( var type in events ) { - for ( var i = 0, l = events[ type ].length; i < l; i++ ) { - jQuery.event.add( this, type, events[ type ][ i ], events[ type ][ i ].data ); + if ( events ) { + delete curData.handle; + curData.events = {}; + + for ( var type in events ) { + for ( var i = 0, l = events[ type ].length; i < l; i++ ) { + jQuery.event.add( this, type, events[ type ][ i ], events[ type ][ i ].data ); + } } } } @@ -420,15 +427,29 @@ function cloneFixAttributes(src, dest) { if ( nodeName === "object" ) { dest.outerHTML = src.outerHTML; - // IE6-8 fails to persist the checked state of a cloned checkbox - // or radio button - } else if ( nodeName === "input" && src.checked ) { - dest.defaultChecked = dest.checked = src.checked; + } else if ( nodeName === "input" && (src.type === "checkbox" || src.type === "radio") ) { + // IE6-8 fails to persist the checked state of a cloned checkbox + // or radio button. Worse, IE6-7 fail to give the cloned element + // a checked appearance if the defaultChecked value isn't also set + if ( src.checked ) { + dest.defaultChecked = dest.checked = src.checked; + } + + // IE6-7 get confused and end up setting the value of a cloned + // checkbox/radio button to an empty string instead of "on" + if ( dest.value !== src.value ) { + dest.value = src.value; + } // IE6-8 fails to return the selected option to the default selected // state when cloning options } else if ( nodeName === "option" ) { dest.selected = src.defaultSelected; + + // IE6-8 fails to set the defaultValue to the correct value when + // cloning other types of input fields + } else if ( nodeName === "input" || nodeName === "textarea" ) { + dest.defaultValue = src.defaultValue; } // Event data gets referenced instead of copied if the expando @@ -594,8 +615,7 @@ jQuery.extend({ }, cleanData: function( elems ) { - var data, id, cache = jQuery.cache, - special = jQuery.event.special, + var data, id, cache = jQuery.cache, internalKey = jQuery.expando, special = jQuery.event.special, deleteExpando = jQuery.support.deleteExpando; for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) { @@ -606,13 +626,14 @@ jQuery.extend({ id = elem[ jQuery.expando ]; if ( id ) { - data = cache[ id ]; + data = cache[ id ] && cache[ id ][ internalKey ]; if ( data && data.events ) { for ( var type in data.events ) { if ( special[ type ] ) { jQuery.event.remove( elem, type ); + // This is a shortcut to avoid jQuery.event.remove's overhead } else { jQuery.removeEvent( elem, type, data.handle ); } diff --git a/src/queue.js b/src/queue.js index 735b0e189..9e3e2fb52 100644 --- a/src/queue.js +++ b/src/queue.js @@ -7,7 +7,7 @@ jQuery.extend({ } type = (type || "fx") + "queue"; - var q = jQuery.data( elem, type ); + var q = jQuery._data( elem, type ); // Speed up dequeue by getting out quickly if this is just a lookup if ( !data ) { @@ -15,7 +15,7 @@ jQuery.extend({ } if ( !q || jQuery.isArray(data) ) { - q = jQuery.data( elem, type, jQuery.makeArray(data) ); + q = jQuery._data( elem, type, jQuery.makeArray(data) ); } else { q.push( data ); @@ -46,6 +46,10 @@ jQuery.extend({ jQuery.dequeue(elem, type); }); } + + if ( !queue.length ) { + jQuery.removeData( elem, type + "queue", true ); + } } }); diff --git a/src/support.js b/src/support.js index e4c3ea916..f502811ae 100644 --- a/src/support.js +++ b/src/support.js @@ -4,10 +4,7 @@ jQuery.support = {}; - var root = document.documentElement, - script = document.createElement("script"), - div = document.createElement("div"), - id = "script" + jQuery.now(); + var div = document.createElement("div"); div.style.display = "none"; div.innerHTML = "
a"; @@ -64,7 +61,7 @@ deleteExpando: true, optDisabled: false, checkClone: false, - scriptEval: false, + _scriptEval: null, noCloneEvent: true, boxModel: null, inlineBlockNeedsLayout: false, @@ -77,32 +74,45 @@ select.disabled = true; jQuery.support.optDisabled = !opt.disabled; - script.type = "text/javascript"; - try { - script.appendChild( document.createTextNode( "window." + id + "=1;" ) ); - } catch(e) {} + jQuery.support.scriptEval = function() { + if ( jQuery.support._scriptEval === null) { + var root = document.documentElement, + script = document.createElement("script"), + id = "script" + jQuery.now(); - root.insertBefore( script, root.firstChild ); + script.type = "text/javascript"; + try { + script.appendChild( document.createTextNode( "window." + id + "=1;" ) ); + } catch(e) {} - // Make sure that the execution of code works by injecting a script - // tag with appendChild/createTextNode - // (IE doesn't support this, fails, and uses .text instead) - if ( window[ id ] ) { - jQuery.support.scriptEval = true; - delete window[ id ]; - } + root.insertBefore( script, root.firstChild ); + + // Make sure that the execution of code works by injecting a script + // tag with appendChild/createTextNode + // (IE doesn't support this, fails, and uses .text instead) + if ( window[ id ] ) { + jQuery.support._scriptEval = true; + delete window[ id ]; + } else { + jQuery.support._scriptEval = false; + } + + root.removeChild( script ); + // release memory in IE + root = script = id = null; + } + return jQuery.support._scriptEval; + }; // Test to see if it's possible to delete an expando from an element // Fails in Internet Explorer try { - delete script.test; + delete div.test; } catch(e) { jQuery.support.deleteExpando = false; } - root.removeChild( script ); - if ( div.attachEvent && div.fireEvent ) { div.attachEvent("onclick", function click() { // Cloning a node shouldn't copy over any @@ -191,6 +201,6 @@ jQuery.support.changeBubbles = eventSupported("change"); // release memory in IE - root = script = div = all = a = null; + div = all = a = null; })(); })( jQuery ); diff --git a/src/traversing.js b/src/traversing.js index 689e90196..b36ce3db8 100644 --- a/src/traversing.js +++ b/src/traversing.js @@ -6,7 +6,14 @@ var runtil = /Until$/, rmultiselector = /,/, isSimple = /^.[^:#\[\.,]*$/, slice = Array.prototype.slice, - POS = jQuery.expr.match.POS; + POS = jQuery.expr.match.POS, + // methods guaranteed to produce a unique set when starting from a unique set + guaranteedUnique = { + children: true, + contents: true, + next: true, + prev: true + }; jQuery.fn.extend({ find: function( selector ) { @@ -196,7 +203,8 @@ jQuery.each({ } }, function( name, fn ) { jQuery.fn[ name ] = function( until, selector ) { - var ret = jQuery.map( this, fn, until ); + var ret = jQuery.map( this, fn, until ), + args = slice.call(arguments); if ( !runtil.test( name ) ) { selector = until; @@ -206,13 +214,13 @@ jQuery.each({ ret = jQuery.filter( selector, ret ); } - ret = this.length > 1 ? jQuery.unique( ret ) : ret; + ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret; if ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) { ret = ret.reverse(); } - return this.pushStack( ret, name, slice.call(arguments).join(",") ); + return this.pushStack( ret, name, args.join(",") ); }; }); diff --git a/test/data/jsonp.php b/test/data/jsonp.php index 9ae1d8487..6c13d72e9 100644 --- a/test/data/jsonp.php +++ b/test/data/jsonp.php @@ -1,6 +1,10 @@
slideToggleOut
slideToggleOut
fadeToggleIn
fadeToggleIn
-
fadeToggleOut
fadeToggleOut
+
fadeToggleOut
fadeToggleOut
fadeTo
fadeTo
diff --git a/test/unit/ajax.js b/test/unit/ajax.js index 773088fc9..3f672ae04 100644 --- a/test/unit/ajax.js +++ b/test/unit/ajax.js @@ -1,4 +1,4 @@ -module("ajax"); +module("ajax", { teardown: moduleTeardown }); // Safari 3 randomly crashes when running these tests, // but only in the full suite - you can run just the Ajax @@ -276,7 +276,7 @@ test(".ajax() - retry with jQuery.ajax( this )", function() { start(); } } - }) + }); }); @@ -316,6 +316,28 @@ test(".ajax() - headers" , function() { }); +test(".ajax() - Accept header" , function() { + + expect( 1 ); + + stop(); + + jQuery.ajax(url("data/headers.php?keys=accept"), { + headers: { + Accept: "very wrong accept value" + }, + beforeSend: function( xhr ) { + xhr.setRequestHeader( "Accept", "*/*" ); + }, + success: function( data ) { + strictEqual( data , "accept: */*\n" , "Test Accept header is set to last value provided" ); + start(); + }, + error: function(){ ok(false, "error"); } + }); + +}); + test(".ajax() - contentType" , function() { expect( 2 ); @@ -1025,6 +1047,18 @@ test("load(String, Function) - check file with only a script tag", function() { }); }); +test("load(String, Function) - dataFilter in ajaxSettings", function() { + expect(2); + stop(); + jQuery.ajaxSetup({ dataFilter: function() { return "Hello World"; } }); + var div = jQuery("
").load(url("data/name.html"), function(responseText) { + strictEqual( div.html(), "Hello World" , "Test div was filled with filtered data" ); + strictEqual( responseText, "Hello World" , "Test callback receives filtered data" ); + jQuery.ajaxSetup({ dataFilter: 0 }); + start(); + }); +}); + test("load(String, Object, Function)", function() { expect(2); stop(); @@ -1080,233 +1114,249 @@ test("jQuery.getScript(String, Function) - no callback", function() { }); }); -test("jQuery.ajax() - JSONP, Local", function() { - expect(10); +jQuery.each( [ "Same Domain", "Cross Domain" ] , function( crossDomain , label ) { - var count = 0; - function plus(){ if ( ++count == 9 ) start(); } + test("jQuery.ajax() - JSONP, " + label, function() { + expect(17); - stop(); + var count = 0; + function plus(){ if ( ++count == 17 ) start(); } - jQuery.ajax({ - url: "data/jsonp.php", - dataType: "jsonp", - success: function(data){ - ok( data.data, "JSON results returned (GET, no callback)" ); - plus(); - }, - error: function(data){ - ok( false, "Ajax error JSON (GET, no callback)" ); - plus(); - } - }); + stop(); - jQuery.ajax({ - url: "data/jsonp.php?callback=?", - dataType: "jsonp", - success: function(data){ - ok( data.data, "JSON results returned (GET, url callback)" ); - plus(); - }, - error: function(data){ - ok( false, "Ajax error JSON (GET, url callback)" ); - plus(); - } - }); + jQuery.ajax({ + url: "data/jsonp.php", + dataType: "jsonp", + crossDomain: crossDomain, + success: function(data){ + ok( data.data, "JSON results returned (GET, no callback)" ); + plus(); + }, + error: function(data){ + ok( false, "Ajax error JSON (GET, no callback)" ); + plus(); + } + }); - jQuery.ajax({ - url: "data/jsonp.php", - dataType: "jsonp", - data: "callback=?", - success: function(data){ - ok( data.data, "JSON results returned (GET, data callback)" ); - plus(); - }, - error: function(data){ - ok( false, "Ajax error JSON (GET, data callback)" ); - plus(); - } - }); + jQuery.ajax({ + url: "data/jsonp.php?callback=?", + dataType: "jsonp", + crossDomain: crossDomain, + success: function(data){ + ok( data.data, "JSON results returned (GET, url callback)" ); + plus(); + }, + error: function(data){ + ok( false, "Ajax error JSON (GET, url callback)" ); + plus(); + } + }); - jQuery.ajax({ - url: "data/jsonp.php", - dataType: "jsonp", - data: { - callback: "?" - }, - success: function(data){ - ok( data.data, "JSON results returned (GET, processed data callback)" ); - plus(); - }, - error: function(data){ - ok( false, "Ajax error JSON (GET, processed data callback)" ); - plus(); - } - }); + jQuery.ajax({ + url: "data/jsonp.php", + dataType: "jsonp", + crossDomain: crossDomain, + data: "callback=?", + success: function(data){ + ok( data.data, "JSON results returned (GET, data callback)" ); + plus(); + }, + error: function(data){ + ok( false, "Ajax error JSON (GET, data callback)" ); + plus(); + } + }); - jQuery.ajax({ - url: "data/jsonp.php", - dataType: "jsonp", - jsonp: "callback", - success: function(data){ - ok( data.data, "JSON results returned (GET, data obj callback)" ); - plus(); - }, - error: function(data){ - ok( false, "Ajax error JSON (GET, data obj callback)" ); - plus(); - } - }); + jQuery.ajax({ + url: "data/jsonp.php?callback=??", + dataType: "jsonp", + crossDomain: crossDomain, + success: function(data){ + ok( data.data, "JSON results returned (GET, url context-free callback)" ); + plus(); + }, + error: function(data){ + ok( false, "Ajax error JSON (GET, url context-free callback)" ); + plus(); + } + }); - jQuery.ajax({ - url: "data/jsonp.php", - dataType: "jsonp", - jsonpCallback: "jsonpResults", - success: function(data){ - ok( data.data, "JSON results returned (GET, custom callback name)" ); - plus(); - }, - error: function(data){ - ok( false, "Ajax error JSON (GET, custom callback name)" ); - plus(); - } - }); + jQuery.ajax({ + url: "data/jsonp.php", + dataType: "jsonp", + crossDomain: crossDomain, + data: "callback=??", + success: function(data){ + ok( data.data, "JSON results returned (GET, data context-free callback)" ); + plus(); + }, + error: function(data){ + ok( false, "Ajax error JSON (GET, data context-free callback)" ); + plus(); + } + }); - jQuery.ajax({ - type: "POST", - url: "data/jsonp.php", - dataType: "jsonp", - success: function(data){ - ok( data.data, "JSON results returned (POST, no callback)" ); - plus(); - }, - error: function(data){ - ok( false, "Ajax error JSON (GET, data obj callback)" ); - plus(); - } - }); + jQuery.ajax({ + url: "data/jsonp.php/??", + dataType: "jsonp", + crossDomain: crossDomain, + success: function(data){ + ok( data.data, "JSON results returned (GET, REST-like)" ); + plus(); + }, + error: function(data){ + ok( false, "Ajax error JSON (GET, REST-like)" ); + plus(); + } + }); - jQuery.ajax({ - type: "POST", - url: "data/jsonp.php", - data: "callback=?", - dataType: "jsonp", - success: function(data){ - ok( data.data, "JSON results returned (POST, data callback)" ); - plus(); - }, - error: function(data){ - ok( false, "Ajax error JSON (POST, data callback)" ); - plus(); - } - }); + jQuery.ajax({ + url: "data/jsonp.php/???json=1", + dataType: "jsonp", + crossDomain: crossDomain, + success: function(data){ + strictEqual( jQuery.type(data), "array", "JSON results returned (GET, REST-like with param)" ); + plus(); + }, + error: function(data){ + ok( false, "Ajax error JSON (GET, REST-like with param)" ); + plus(); + } + }); - jQuery.ajax({ - type: "POST", - url: "data/jsonp.php", - jsonp: "callback", - dataType: "jsonp", - success: function(data){ - ok( data.data, "JSON results returned (POST, data obj callback)" ); + jQuery.ajax({ + url: "data/jsonp.php", + dataType: "jsonp", + crossDomain: crossDomain, + data: { + callback: "?" + }, + success: function(data){ + ok( data.data, "JSON results returned (GET, processed data callback)" ); + plus(); + }, + error: function(data){ + ok( false, "Ajax error JSON (GET, processed data callback)" ); + plus(); + } + }); + + jQuery.ajax({ + url: "data/jsonp.php", + dataType: "jsonp", + crossDomain: crossDomain, + jsonp: "callback", + success: function(data){ + ok( data.data, "JSON results returned (GET, data obj callback)" ); + plus(); + }, + error: function(data){ + ok( false, "Ajax error JSON (GET, data obj callback)" ); + plus(); + } + }); + + window.jsonpResults = function(data) { + ok( data.data, "JSON results returned (GET, custom callback function)" ); + window.jsonpResults = undefined; plus(); - }, - error: function(data){ - ok( false, "Ajax error JSON (POST, data obj callback)" ); - plus(); - } - }); + }; - //#7578 - jQuery.ajax({ - url: "data/jsonp.php", - dataType: "jsonp", - beforeSend: function(){ - strictEqual( this.cache, false, "cache must be false on JSON request" ); - plus(); - return false; - } - }); -}); + jQuery.ajax({ + url: "data/jsonp.php", + dataType: "jsonp", + crossDomain: crossDomain, + jsonpCallback: "jsonpResults", + success: function(data){ + ok( data.data, "JSON results returned (GET, custom callback name)" ); + plus(); + }, + error: function(data){ + ok( false, "Ajax error JSON (GET, custom callback name)" ); + plus(); + } + }); -test("jQuery.ajax() - JSONP - Custom JSONP Callback", function() { - expect(1); - stop(); + jQuery.ajax({ + type: "POST", + url: "data/jsonp.php", + dataType: "jsonp", + crossDomain: crossDomain, + success: function(data){ + ok( data.data, "JSON results returned (POST, no callback)" ); + plus(); + }, + error: function(data){ + ok( false, "Ajax error JSON (GET, data obj callback)" ); + plus(); + } + }); - window.jsonpResults = function(data) { - ok( data.data, "JSON results returned (GET, custom callback function)" ); - window.jsonpResults = undefined; - start(); - }; + jQuery.ajax({ + type: "POST", + url: "data/jsonp.php", + data: "callback=?", + dataType: "jsonp", + crossDomain: crossDomain, + success: function(data){ + ok( data.data, "JSON results returned (POST, data callback)" ); + plus(); + }, + error: function(data){ + ok( false, "Ajax error JSON (POST, data callback)" ); + plus(); + } + }); - jQuery.ajax({ - url: "data/jsonp.php", - dataType: "jsonp", - jsonpCallback: "jsonpResults" - }); -}); + jQuery.ajax({ + type: "POST", + url: "data/jsonp.php", + jsonp: "callback", + dataType: "jsonp", + crossDomain: crossDomain, + success: function(data){ + ok( data.data, "JSON results returned (POST, data obj callback)" ); + plus(); + }, + error: function(data){ + ok( false, "Ajax error JSON (POST, data obj callback)" ); + plus(); + } + }); -test("jQuery.ajax() - JSONP, Remote", function() { - expect(4); + //#7578 + jQuery.ajax({ + url: "data/jsonp.php", + dataType: "jsonp", + crossDomain: crossDomain, + beforeSend: function(){ + strictEqual( this.cache, false, "cache must be false on JSON request" ); + plus(); + return false; + } + }); - var count = 0; - function plus(){ if ( ++count == 4 ) start(); } + jQuery.ajax({ + url: "data/jsonp.php?callback=XXX", + dataType: "jsonp", + jsonp: false, + jsonpCallback: "XXX", + crossDomain: crossDomain, + beforeSend: function() { + ok( /^data\/jsonp.php\?callback=XXX&_=\d+$/.test( this.url ) , + "The URL wasn't messed with (GET, custom callback name with no url manipulation)" ); + plus(); + }, + success: function(data){ + ok( data.data, "JSON results returned (GET, custom callback name with no url manipulation)" ); + plus(); + }, + error: function(data){ + ok( false, "Ajax error JSON (GET, custom callback name with no url manipulation)" ); + plus(); + } + }); - var base = window.location.href.replace(/[^\/]*$/, ""); - - stop(); - - jQuery.ajax({ - url: base + "data/jsonp.php", - dataType: "jsonp", - success: function(data){ - ok( data.data, "JSON results returned (GET, no callback)" ); - plus(); - }, - error: function(data){ - ok( false, "Ajax error JSON (GET, no callback)" ); - plus(); - } - }); - - jQuery.ajax({ - url: base + "data/jsonp.php?callback=?", - dataType: "jsonp", - success: function(data){ - ok( data.data, "JSON results returned (GET, url callback)" ); - plus(); - }, - error: function(data){ - ok( false, "Ajax error JSON (GET, url callback)" ); - plus(); - } - }); - - jQuery.ajax({ - url: base + "data/jsonp.php", - dataType: "jsonp", - data: "callback=?", - success: function(data){ - ok( data.data, "JSON results returned (GET, data callback)" ); - plus(); - }, - error: function(data){ - ok( false, "Ajax error JSON (GET, data callback)" ); - plus(); - } - }); - - jQuery.ajax({ - url: base + "data/jsonp.php", - dataType: "jsonp", - jsonp: "callback", - success: function(data){ - ok( data.data, "JSON results returned (GET, data obj callback)" ); - plus(); - }, - error: function(data){ - ok( false, "Ajax error JSON (GET, data obj callback)" ); - plus(); - } }); }); @@ -1778,25 +1828,19 @@ test("jQuery ajax - failing cross-domain", function() { var i = 2; - if ( jQuery.ajax({ + jQuery.ajax({ url: 'http://somewebsitethatdoesnotexist-67864863574657654.com', success: function(){ ok( false , "success" ); }, error: function(xhr,_,e){ ok( true , "file not found: " + xhr.status + " => " + e ); }, complete: function() { if ( ! --i ) start(); } - }) === false ) { - ok( true , "no transport" ); - if ( ! --i ) start(); - } + }); - if ( jQuery.ajax({ + jQuery.ajax({ url: 'http://www.google.com', success: function(){ ok( false , "success" ); }, error: function(xhr,_,e){ ok( true , "access denied: " + xhr.status + " => " + e ); }, complete: function() { if ( ! --i ) start(); } - }) === false ) { - ok( true , "no transport" ); - if ( ! --i ) start(); - } + }); }); @@ -1828,6 +1872,76 @@ test( "jQuery.ajax - Location object as url (#7531)", 1, function () { ok( success, "document.location did not generate exception" ); }); +test( "jQuery.ajax - statusCode" , function() { + + var count = 10; + + expect( 16 ); + stop(); + + function countComplete() { + if ( ! --count ) { + start(); + } + } + + function createStatusCodes( name , isSuccess ) { + name = "Test " + name + " " + ( isSuccess ? "success" : "error" ); + return { + 200: function() { + ok( isSuccess , name ); + }, + 404: function() { + ok( ! isSuccess , name ); + } + }; + } + + jQuery.each( { + "data/name.html": true, + "data/someFileThatDoesNotExist.html": false + } , function( uri , isSuccess ) { + + jQuery.ajax( url( uri ) , { + statusCode: createStatusCodes( "in options" , isSuccess ), + complete: countComplete + }); + + jQuery.ajax( url( uri ) , { + complete: countComplete + }).statusCode( createStatusCodes( "immediately with method" , isSuccess ) ); + + jQuery.ajax( url( uri ) , { + complete: function(jXHR) { + jXHR.statusCode( createStatusCodes( "on complete" , isSuccess ) ); + countComplete(); + } + }); + + jQuery.ajax( url( uri ) , { + complete: function(jXHR) { + setTimeout( function() { + jXHR.statusCode( createStatusCodes( "very late binding" , isSuccess ) ); + countComplete(); + } , 100 ); + } + }); + + jQuery.ajax( url( uri ) , { + statusCode: createStatusCodes( "all (options)" , isSuccess ), + complete: function(jXHR) { + jXHR.statusCode( createStatusCodes( "all (on complete)" , isSuccess ) ); + setTimeout( function() { + jXHR.statusCode( createStatusCodes( "all (very late binding)" , isSuccess ) ); + countComplete(); + } , 100 ); + } + }).statusCode( createStatusCodes( "all (immediately with method)" , isSuccess ) ); + + }); + +}); + } //} \ No newline at end of file diff --git a/test/unit/attributes.js b/test/unit/attributes.js index a1ab58179..c58111de1 100644 --- a/test/unit/attributes.js +++ b/test/unit/attributes.js @@ -1,4 +1,4 @@ -module("attributes"); +module("attributes", { teardown: moduleTeardown }); var bareObj = function(value) { return value; }; var functionReturningObj = function(value) { return (function() { return value; }); }; @@ -703,12 +703,12 @@ var testToggleClass = function(valueObj) { // toggleClass storage e.toggleClass(true); - ok( e.get(0).className === "", "Assert class is empty (data was empty)" ); + ok( e[0].className === "", "Assert class is empty (data was empty)" ); e.addClass("testD testE"); ok( e.is(".testD.testE"), "Assert class present" ); e.toggleClass(); ok( !e.is(".testD.testE"), "Assert class not present" ); - ok( e.data('__className__') === 'testD testE', "Assert data was stored" ); + ok( jQuery._data(e[0], '__className__') === 'testD testE', "Assert data was stored" ); e.toggleClass(); ok( e.is(".testD.testE"), "Assert class present (restored from data)" ); e.toggleClass(false); @@ -720,11 +720,9 @@ var testToggleClass = function(valueObj) { e.toggleClass(); ok( e.is(".testD.testE"), "Assert class present (restored from data)" ); - - // Cleanup e.removeClass("testD"); - e.removeData('__className__'); + jQuery.removeData(e[0], '__className__', true); }; test("toggleClass(String|boolean|undefined[, boolean])", function() { @@ -785,7 +783,7 @@ test("toggleClass(Fucntion[, boolean]) with incoming value", function() { // Cleanup e.removeClass("test"); - e.removeData('__className__'); + jQuery.removeData(e[0], '__className__', true); }); test("addClass, removeClass, hasClass", function() { diff --git a/test/unit/core.js b/test/unit/core.js index 8fd060578..8e57edf95 100644 --- a/test/unit/core.js +++ b/test/unit/core.js @@ -1,4 +1,4 @@ -module("core"); +module("core", { teardown: moduleTeardown }); test("Basic requirements", function() { expect(7); @@ -12,7 +12,7 @@ test("Basic requirements", function() { }); test("jQuery()", function() { - expect(23); + expect(24); // Basic constructor's behavior @@ -21,7 +21,7 @@ test("jQuery()", function() { equals( jQuery(null).length, 0, "jQuery(null) === jQuery([])" ); equals( jQuery("").length, 0, "jQuery('') === jQuery([])" ); - var obj = jQuery("div") + var obj = jQuery("div"); equals( jQuery(obj).selector, "div", "jQuery(jQueryObj) == jQueryObj" ); // can actually yield more than one, when iframes are included, the window is an array as well @@ -84,6 +84,17 @@ test("jQuery()", function() { exec = true; elem.click(); + + // manually clean up detached elements + elem.remove(); + + for ( var i = 0; i < 3; ++i ) { + elem = jQuery(""); + } + equals( elem[0].defaultValue, "TEST", "Ensure cached nodes are cloned properly (Bug #6655)" ); + + // manually clean up detached elements + elem.remove(); }); test("selector state", function() { @@ -998,7 +1009,7 @@ test("jQuery._Deferred()", function() { test("jQuery.Deferred()", function() { - expect( 4 ); + expect( 10 ); jQuery.Deferred( function( defer ) { strictEqual( this , defer , "Defer passed as this & first argument" ); @@ -1018,11 +1029,35 @@ test("jQuery.Deferred()", function() { }, function() { ok( true , "Error on reject" ); }); + + ( new jQuery.Deferred( function( defer ) { + strictEqual( this , defer , "Defer passed as this & first argument (new)" ); + this.resolve( "done" ); + }) ).then( function( value ) { + strictEqual( value , "done" , "Passed function executed (new)" ); + }); + + ( new jQuery.Deferred() ).resolve().then( function() { + ok( true , "Success on resolve (new)" ); + }, function() { + ok( false , "Error on resolve (new)" ); + }); + + ( new jQuery.Deferred() ).reject().then( function() { + ok( false , "Success on reject (new)" ); + }, function() { + ok( true , "Error on reject (new)" ); + }); + + var tmp = jQuery.Deferred(); + + strictEqual( tmp.promise() , tmp.promise() , "Test deferred always return same promise" ); + strictEqual( tmp.promise() , tmp.promise().promise() , "Test deferred's promise always return same promise as deferred" ); }); test("jQuery.when()", function() { - expect( 21 ); + expect( 23 ); // Some other objects jQuery.each( { @@ -1045,6 +1080,10 @@ test("jQuery.when()", function() { } ); + ok( jQuery.isFunction( jQuery.when().then( function( resolveValue ) { + strictEqual( resolveValue , undefined , "Test the promise was resolved with no parameter" ); + } ).promise ) , "Test calling when with no parameter triggers the creation of a new Promise" ); + var cache, i; for( i = 1 ; i < 4 ; i++ ) { @@ -1058,3 +1097,106 @@ test("jQuery.when()", function() { }); } }); + +test("jQuery.when() - joined", function() { + + expect(8); + + jQuery.when( 1, 2, 3 ).done( function( a, b, c ) { + strictEqual( a , 1 , "Test first param is first resolved value - non-observables" ); + strictEqual( b , 2 , "Test second param is second resolved value - non-observables" ); + strictEqual( c , 3 , "Test third param is third resolved value - non-observables" ); + }).fail( function() { + ok( false , "Test the created deferred was resolved - non-observables"); + }); + + var successDeferred = jQuery.Deferred().resolve( 1 , 2 , 3 ), + errorDeferred = jQuery.Deferred().reject( "error" , "errorParam" ); + + jQuery.when( 1 , successDeferred , 3 ).done( function( a, b, c ) { + strictEqual( a , 1 , "Test first param is first resolved value - resolved observable" ); + same( b , [ 1 , 2 , 3 ] , "Test second param is second resolved value - resolved observable" ); + strictEqual( c , 3 , "Test third param is third resolved value - resolved observable" ); + }).fail( function() { + ok( false , "Test the created deferred was resolved - resolved observable"); + }); + + jQuery.when( 1 , errorDeferred , 3 ).done( function() { + ok( false , "Test the created deferred was rejected - rejected observable"); + }).fail( function( error , errorParam ) { + strictEqual( error , "error" , "Test first param is first rejected value - rejected observable" ); + strictEqual( errorParam , "errorParam" , "Test second param is second rejected value - rejected observable" ); + }); +}); + +test("jQuery.subclass", function(){ + expect(378); + + var Subclass = jQuery.subclass(), + SubclassSubclass = Subclass.subclass(), + jQueryDocument = jQuery(document), + selectors, contexts, methods, method, arg, description; + + jQueryDocument.toString = function(){ return 'jQueryDocument'; }; + + Subclass.fn.subclassMethod = function(){}; + SubclassSubclass.fn.subclassSubclassMethod = function(){}; + + selectors = [ + 'body', + 'html, body', + '
' + ]; + + methods = [ // all methods that return a new jQuery instance + ['eq', 1], + ['add', document], + ['end'], + ['has'], + ['closest', 'div'], + ['filter', document], + ['find', 'div'] + ]; + + contexts = [undefined, document, jQueryDocument]; + + jQuery.each(selectors, function(i, selector){ + + jQuery.each(methods, function(){ + method = this[0]; + arg = this[1]; + + jQuery.each(contexts, function(i, context){ + + description = '("'+selector+'", '+context+').'+method+'('+(arg||'')+')'; + + same( + jQuery(selector, context)[method](arg).subclassMethod, undefined, + 'jQuery'+description+' doesnt have Subclass methods' + ); + same( + jQuery(selector, context)[method](arg).subclassSubclassMethod, undefined, + 'jQuery'+description+' doesnt have SubclassSubclass methods' + ); + same( + Subclass(selector, context)[method](arg).subclassMethod, Subclass.fn.subclassMethod, + 'Subclass'+description+' has Subclass methods' + ); + same( + Subclass(selector, context)[method](arg).subclassSubclassMethod, undefined, + 'Subclass'+description+' doesnt have SubclassSubclass methods' + ); + same( + SubclassSubclass(selector, context)[method](arg).subclassMethod, Subclass.fn.subclassMethod, + 'SubclassSubclass'+description+' has Subclass methods' + ); + same( + SubclassSubclass(selector, context)[method](arg).subclassSubclassMethod, SubclassSubclass.fn.subclassSubclassMethod, + 'SubclassSubclass'+description+' has SubclassSubclass methods' + ); + + }); + }); + }); + +}); diff --git a/test/unit/css.js b/test/unit/css.js index fbbf937ca..555f13575 100644 --- a/test/unit/css.js +++ b/test/unit/css.js @@ -1,4 +1,4 @@ -module("css"); +module("css", { teardown: moduleTeardown }); test("css(String|Hash)", function() { expect(41); @@ -320,3 +320,16 @@ test(":visible selector works properly on children with a hidden parent (bug #45 jQuery('#table').css('display', 'none').html('cellcell'); equals(jQuery('#table td:visible').length, 0, "hidden cell children not perceived as visible"); }); + +test("internal ref to elem.runtimeStyle (bug #7608)", function () { + expect(1); + var result = true; + + try { + jQuery("#foo").css( { width: "0%" } ).css("width"); + } catch (e) { + result = false; + } + + ok( result, "elem.runtimeStyle does not throw exception" ); +}); diff --git a/test/unit/data.js b/test/unit/data.js index 310cd6bc4..889fc2da3 100644 --- a/test/unit/data.js +++ b/test/unit/data.js @@ -1,22 +1,159 @@ -module("data"); +module("data", { teardown: moduleTeardown }); test("expando", function(){ - expect(6); + expect(1); equals("expando" in jQuery, true, "jQuery is exposing the expando"); +}); - var obj = {}; - equals( jQuery.data(obj), obj, "jQuery.data(obj) returns the object"); - equals( jQuery.expando in obj, false, "jQuery.data(obj) did not add an expando to the object" ); +function dataTests (elem) { + // expect(32) - obj = {}; - jQuery.data(obj, 'test'); - equals( jQuery.expando in obj, false, "jQuery.data(obj,key) did not add an expando to the object" ); + function getCacheLength() { + var cacheLength = 0; + for (var i in jQuery.cache) { + ++cacheLength; + } - obj = {}; - jQuery.data(obj, "foo", "bar"); - equals( jQuery.expando in obj, false, "jQuery.data(obj,key,value) did not add an expando to the object" ); - equals( obj.foo, "bar", "jQuery.data(obj,key,value) sets fields directly on the object." ); + return cacheLength; + } + + equals( jQuery.data(elem, "foo"), undefined, "No data exists initially" ); + strictEqual( jQuery.hasData(elem), false, "jQuery.hasData agrees no data exists initially" ); + + var dataObj = jQuery.data(elem); + equals( typeof dataObj, "object", "Calling data with no args gives us a data object reference" ); + strictEqual( jQuery.data(elem), dataObj, "Calling jQuery.data returns the same data object when called multiple times" ); + + strictEqual( jQuery.hasData(elem), false, "jQuery.hasData agrees no data exists even when an empty data obj exists" ); + + dataObj.foo = "bar"; + equals( jQuery.data(elem, "foo"), "bar", "Data is readable by jQuery.data when set directly on a returned data object" ); + + strictEqual( jQuery.hasData(elem), true, "jQuery.hasData agrees data exists when data exists" ); + + jQuery.data(elem, "foo", "baz"); + equals( jQuery.data(elem, "foo"), "baz", "Data can be changed by jQuery.data" ); + equals( dataObj.foo, "baz", "Changes made through jQuery.data propagate to referenced data object" ); + + jQuery.data(elem, "foo", undefined); + equals( jQuery.data(elem, "foo"), "baz", "Data is not unset by passing undefined to jQuery.data" ); + + jQuery.data(elem, "foo", null); + strictEqual( jQuery.data(elem, "foo"), null, "Setting null using jQuery.data works OK" ); + + jQuery.data(elem, "foo", "foo1"); + + jQuery.data(elem, { "bar" : "baz", "boom" : "bloz" }); + strictEqual( jQuery.data(elem, "foo"), "foo1", "Passing an object extends the data object instead of replacing it" ); + equals( jQuery.data(elem, "boom"), "bloz", "Extending the data object works" ); + + jQuery._data(elem, "foo", "foo2"); + equals( jQuery._data(elem, "foo"), "foo2", "Setting internal data works" ); + equals( jQuery.data(elem, "foo"), "foo1", "Setting internal data does not override user data" ); + + var internalDataObj = jQuery.data(elem, jQuery.expando); + strictEqual( jQuery._data(elem), internalDataObj, "Internal data object is accessible via jQuery.expando property" ); + notStrictEqual( dataObj, internalDataObj, "Internal data object is not the same as user data object" ); + + strictEqual( elem.boom, undefined, "Data is never stored directly on the object" ); + + jQuery.removeData(elem, "foo"); + strictEqual( jQuery.data(elem, "foo"), undefined, "jQuery.removeData removes single properties" ); + + jQuery.removeData(elem); + strictEqual( jQuery.data(elem, jQuery.expando), internalDataObj, "jQuery.removeData does not remove internal data if it exists" ); + + jQuery.removeData(elem, undefined, true); + + strictEqual( jQuery.data(elem, jQuery.expando), undefined, "jQuery.removeData on internal data works" ); + strictEqual( jQuery.hasData(elem), false, "jQuery.hasData agrees all data has been removed from object" ); + + jQuery._data(elem, "foo", "foo2"); + strictEqual( jQuery.hasData(elem), true, "jQuery.hasData shows data exists even if it is only internal data" ); + + jQuery.data(elem, "foo", "foo1"); + equals( jQuery._data(elem, "foo"), "foo2", "Setting user data does not override internal data" ); + + jQuery.removeData(elem, undefined, true); + equals( jQuery.data(elem, "foo"), "foo1", "jQuery.removeData for internal data does not remove user data" ); + + if (elem.nodeType) { + var oldCacheLength = getCacheLength(); + jQuery.removeData(elem, "foo"); + + equals( getCacheLength(), oldCacheLength - 1, "Removing the last item in the data object destroys it" ); + } + else { + jQuery.removeData(elem, "foo"); + var expected, actual; + + if (jQuery.support.deleteExpando) { + expected = false; + actual = jQuery.expando in elem; + } + else { + expected = null; + actual = elem[ jQuery.expando ]; + } + + equals( actual, expected, "Removing the last item in the data object destroys it" ); + } + + jQuery.data(elem, "foo", "foo1"); + jQuery._data(elem, "foo", "foo2"); + + equals( jQuery.data(elem, "foo"), "foo1", "(sanity check) Ensure data is set in user data object" ); + equals( jQuery._data(elem, "foo"), "foo2", "(sanity check) Ensure data is set in internal data object" ); + + jQuery.removeData(elem, "foo", true); + + strictEqual( jQuery.data(elem, jQuery.expando), undefined, "Removing the last item in internal data destroys the internal data object" ); + + jQuery._data(elem, "foo", "foo2"); + equals( jQuery._data(elem, "foo"), "foo2", "(sanity check) Ensure data is set in internal data object" ); + + jQuery.removeData(elem, "foo"); + equals( jQuery._data(elem, "foo"), "foo2", "(sanity check) jQuery.removeData for user data does not remove internal data" ); + + if (elem.nodeType) { + oldCacheLength = getCacheLength(); + jQuery.removeData(elem, "foo", true); + equals( getCacheLength(), oldCacheLength - 1, "Removing the last item in the internal data object also destroys the user data object when it is empty" ); + } + else { + jQuery.removeData(elem, "foo", true); + + if (jQuery.support.deleteExpando) { + expected = false; + actual = jQuery.expando in elem; + } + else { + expected = null; + actual = elem[ jQuery.expando ]; + } + + equals( actual, expected, "Removing the last item in the internal data object also destroys the user data object when it is empty" ); + } +} + +test("jQuery.data", function() { + expect(128); + + var div = document.createElement("div"); + + dataTests(div); + dataTests({}); + + // remove bound handlers from window object to stop potential false positives caused by fix for #5280 in + // transports/xhr.js + jQuery(window).unbind("unload"); + + dataTests(window); + dataTests(document); + + // clean up unattached element + jQuery(div).remove(); }); test("jQuery.acceptData", function() { @@ -37,68 +174,11 @@ test("jQuery.acceptData", function() { ok( !jQuery.acceptData( applet ), "applet" ); }); -test("jQuery.data", function() { - expect(15); - var div = document.createElement("div"); - - ok( jQuery.data(div, "test") === undefined, "Check for no data exists" ); - - jQuery.data(div, "test", "success"); - equals( jQuery.data(div, "test"), "success", "Check for added data" ); - - ok( jQuery.data(div, "notexist") === undefined, "Check for no data exists" ); - - var data = jQuery.data(div); - same( data, { "test": "success" }, "Return complete data set" ); - - jQuery.data(div, "test", "overwritten"); - equals( jQuery.data(div, "test"), "overwritten", "Check for overwritten data" ); - - jQuery.data(div, "test", undefined); - equals( jQuery.data(div, "test"), "overwritten", "Check that data wasn't removed"); - - jQuery.data(div, "test", null); - ok( jQuery.data(div, "test") === null, "Check for null data"); - - jQuery.data(div, "test3", "orig"); - jQuery.data(div, { "test": "in", "test2": "in2" }); - equals( jQuery.data(div, "test"), "in", "Verify setting an object in data" ); - equals( jQuery.data(div, "test2"), "in2", "Verify setting an object in data" ); - equals( jQuery.data(div, "test3"), "orig", "Verify original not overwritten" ); - - var obj = {}; - jQuery.data( obj, "prop", true ); - - ok( obj.prop, "Data is being stored on the object" ); - equals( jQuery.data( obj, "prop" ), true, "Make sure the right value is retrieved" ); - - jQuery.data( window, "BAD", true ); - ok( !window[ jQuery.expando ], "Make sure there is no expando on the window object." ); - ok( !window.BAD, "And make sure that the property wasn't set directly on the window." ); - ok( jQuery.data( window, "BAD" ), "Make sure that the value was set." ); -}); - -test("jQuery.hasData", function() { - expect(6); - - function testData(obj) { - equals( jQuery.hasData(obj), false, "No data exists" ); - jQuery.data( obj, "foo", "bar" ); - equals( jQuery.hasData(obj), true, "Data exists" ); - jQuery.removeData( obj, "foo" ); - equals( jQuery.hasData(obj), false, "Data was removed" ); - } - - testData(document.createElement('div')); - testData({}); -}); - test(".data()", function() { expect(5); var div = jQuery("#foo"); strictEqual( div.data("foo"), undefined, "Make sure that missing result is undefined" ); - div.data("test", "success"); same( div.data(), {test: "success"}, "data() get the entire data object" ); strictEqual( div.data("foo"), undefined, "Make sure that missing result is still undefined" ); @@ -107,7 +187,7 @@ test(".data()", function() { equals( nodiv.data(), null, "data() on empty set returns null" ); var obj = { foo: "bar" }; - equals( jQuery(obj).data(), obj, "Retrieve data object from a wrapped JS object (#7524)" ); + deepEqual( jQuery(obj).data(), {}, "Retrieve data object from a wrapped JS object (#7524)" ); }) test(".data(String) and .data(String, Object)", function() { @@ -194,11 +274,14 @@ test(".data(String) and .data(String, Object)", function() { equals( $elem.data('null',null).data('null'), null, "null's are preserved"); equals( $elem.data('emptyString','').data('emptyString'), '', "Empty strings are preserved"); equals( $elem.data('false',false).data('false'), false, "false's are preserved"); - equals( $elem.data('exists'), true, "Existing data is returned" ); + equals( $elem.data('exists'), undefined, "Existing data is not returned" ); // Clean up $elem.removeData(); - ok( jQuery.isEmptyObject( $elem[0] ), "removeData clears the object" ); + deepEqual( $elem[0], {exists:true}, "removeData does not clear the object" ); + + // manually clean up detached elements + parent.remove(); }); test("data-* attributes", function() { @@ -218,6 +301,8 @@ test("data-* attributes", function() { div.data("attr", "internal").attr("data-attr", "external"); equals( div.data("attr"), "internal", "Check for .data('attr') precedence (internal > external data-* attribute)" ); + div.remove(); + child.appendTo('#main'); equals( child.data("myobj"), "old data", "Value accessed from data-* attribute"); @@ -229,6 +314,8 @@ test("data-* attributes", function() { var obj = child.data(), obj2 = dummy.data(), check = [ "myobj", "ignored", "other" ], num = 0, num2 = 0; + dummy.remove(); + for ( var i = 0, l = check.length; i < l; i++ ) { ok( obj[ check[i] ], "Make sure data- property exists when calling data-." ); ok( obj2[ check[i] ], "Make sure data- property exists when calling data-." ); @@ -323,13 +410,17 @@ test(".data(Object)", function() { var obj = {test:"unset"}, jqobj = jQuery(obj); + jqobj.data("test", "unset"); jqobj.data({ "test": "in", "test2": "in2" }); - equals( obj.test, "in", "Verify setting an object on an object extends the object" ); - equals( obj.test2, "in2", "Verify setting an object on an object extends the object" ); + equals( jQuery.data(obj).test, "in", "Verify setting an object on an object extends the data object" ); + equals( obj.test2, undefined, "Verify setting an object on an object does not extend the object" ); + + // manually clean up detached elements + div.remove(); }); test("jQuery.removeData", function() { - expect(7); + expect(6); var div = jQuery("#foo")[0]; jQuery.data(div, "test", "testing"); jQuery.removeData(div, "test"); @@ -342,10 +433,9 @@ test("jQuery.removeData", function() { var obj = {}; jQuery.data(obj, "test", "testing"); - equals( obj.test, "testing", "verify data on plain object"); + equals( jQuery(obj).data("test"), "testing", "verify data on plain object"); jQuery.removeData(obj, "test"); equals( jQuery.data(obj, "test"), undefined, "Check removal of data on plain object" ); - equals( obj.test, undefined, "Check removal of data directly from plain object" ); jQuery.data( window, "BAD", true ); jQuery.removeData( window, "BAD" ); @@ -371,4 +461,4 @@ test(".removeData()", function() { div.removeData("test.foo"); equals( div.data("test.foo"), undefined, "Make sure data is intact" ); -}); +}); \ No newline at end of file diff --git a/test/unit/dimensions.js b/test/unit/dimensions.js index b38e73bba..fa59a9f77 100644 --- a/test/unit/dimensions.js +++ b/test/unit/dimensions.js @@ -1,4 +1,4 @@ -module("dimensions"); +module("dimensions", { teardown: moduleTeardown }); function pass( val ) { return val; @@ -33,6 +33,8 @@ function testWidth( val ) { var blah = jQuery("blah"); equals( blah.width( val(10) ), blah, "Make sure that setting a width on an empty set returns the set." ); equals( blah.width(), null, "Make sure 'null' is returned on an empty set"); + + jQuery.removeData($div[0], 'olddisplay', true); } test("width()", function() { @@ -80,6 +82,8 @@ function testHeight( val ) { var blah = jQuery("blah"); equals( blah.height( val(10) ), blah, "Make sure that setting a height on an empty set returns the set." ); equals( blah.height(), null, "Make sure 'null' is returned on an empty set"); + + jQuery.removeData($div[0], 'olddisplay', true); } test("height()", function() { @@ -126,6 +130,9 @@ test("innerWidth()", function() { // Temporarily require 0 for backwards compat - should be auto equals( div.innerWidth(), 0, "Make sure that disconnected nodes are handled." ); + + div.remove(); + jQuery.removeData($div[0], 'olddisplay', true); }); test("innerHeight()", function() { @@ -152,6 +159,9 @@ test("innerHeight()", function() { // Temporarily require 0 for backwards compat - should be auto equals( div.innerHeight(), 0, "Make sure that disconnected nodes are handled." ); + + div.remove(); + jQuery.removeData($div[0], 'olddisplay', true); }); test("outerWidth()", function() { @@ -179,6 +189,9 @@ test("outerWidth()", function() { // Temporarily require 0 for backwards compat - should be auto equals( div.outerWidth(), 0, "Make sure that disconnected nodes are handled." ); + + div.remove(); + jQuery.removeData($div[0], 'olddisplay', true); }); test("outerHeight()", function() { @@ -205,4 +218,7 @@ test("outerHeight()", function() { // Temporarily require 0 for backwards compat - should be auto equals( div.outerHeight(), 0, "Make sure that disconnected nodes are handled." ); + + div.remove(); + jQuery.removeData($div[0], 'olddisplay', true); }); diff --git a/test/unit/effects.js b/test/unit/effects.js index b7b60abbe..b1dd28840 100644 --- a/test/unit/effects.js +++ b/test/unit/effects.js @@ -1,4 +1,4 @@ -module("effects"); +module("effects", { teardown: moduleTeardown }); test("sanity check", function() { expect(1); @@ -14,7 +14,7 @@ test("show()", function() { equals( hiddendiv.css("display"), "block", "Make sure a pre-hidden div is visible." ); - var div = jQuery("
").hide().appendTo("body").show(); + var div = jQuery("
").hide().appendTo("#main").show(); equal( div.css("display"), "block", "Make sure pre-hidden divs show" ); @@ -403,13 +403,16 @@ test("animate duration 0", function() { $elem.hide(0, function(){ ok(true, "Hide callback with no duration"); }); + + // manually clean up detached elements + $elem.remove(); }); test("animate hyphenated properties", function(){ expect(1); stop(); - jQuery("#nothiddendiv") + jQuery("#foo") .css("font-size", 10) .animate({"font-size": 20}, 200, function(){ equals( this.style.fontSize, "20px", "The font-size property was animated." ); @@ -433,7 +436,7 @@ test("stop()", function() { expect(3); stop(); - var $foo = jQuery("#nothiddendiv"); + var $foo = jQuery("#foo"); var w = 0; $foo.hide().width(200).width(); @@ -446,6 +449,8 @@ test("stop()", function() { nw = $foo.width(); notEqual( nw, w, "Stop didn't reset the animation " + nw + "px " + w + "px"); setTimeout(function(){ + $foo.removeData(); + $foo.removeData(undefined, true); equals( nw, $foo.width(), "The animation didn't continue" ); start(); }, 100); @@ -456,7 +461,7 @@ test("stop() - several in queue", function() { expect(3); stop(); - var $foo = jQuery("#nothiddendivchild"); + var $foo = jQuery("#foo"); var w = 0; $foo.hide().width(200).width(); @@ -481,7 +486,7 @@ test("stop(clearQueue)", function() { expect(4); stop(); - var $foo = jQuery("#nothiddendiv"); + var $foo = jQuery("#foo"); var w = 0; $foo.hide().width(200).width(); @@ -508,7 +513,7 @@ test("stop(clearQueue, gotoEnd)", function() { expect(1); stop(); - var $foo = jQuery("#nothiddendivchild"); + var $foo = jQuery("#foo"); var w = 0; $foo.hide().width(200).width(); @@ -536,7 +541,7 @@ test("stop(clearQueue, gotoEnd)", function() { test("toggle()", function() { expect(6); - var x = jQuery("#nothiddendiv"); + var x = jQuery("#foo"); ok( x.is(":visible"), "is visible" ); x.toggle(); ok( x.is(":hidden"), "is hidden" ); @@ -737,6 +742,9 @@ jQuery.each( { } } + // manually remove generated element + jQuery(this).remove(); + start(); }); }); @@ -763,6 +771,10 @@ jQuery.checkState = function(){ var cur = self.style[ c ] || jQuery.css(self, c); equals( cur, v, "Make sure that " + c + " is reset (Old: " + v + " Cur: " + cur + ")"); }); + + // manually clean data on modified element + jQuery.removeData(this, 'olddisplay', true); + start(); } @@ -829,9 +841,6 @@ jQuery.makeTest = function( text ){ jQuery("

") .text( text ) .appendTo("#fx-tests") - .click(function(){ - jQuery(this).next().toggle(); - }) .after( elem ); return elem; @@ -895,7 +904,7 @@ test("hide hidden elements (bug #7141)", function() { var div = jQuery("
").appendTo("#main"); equals( div.css("display"), "none", "Element is hidden by default" ); div.hide(); - ok( !div.data("olddisplay"), "olddisplay is undefined after hiding an already-hidden element" ); + ok( !jQuery._data(div, "olddisplay"), "olddisplay is undefined after hiding an already-hidden element" ); div.show(); equals( div.css("display"), "block", "Show a double-hidden element" ); @@ -910,7 +919,7 @@ test("hide hidden elements, with animation (bug #7141)", function() { var div = jQuery("
").appendTo("#main"); equals( div.css("display"), "none", "Element is hidden by default" ); div.hide(1, function () { - ok( !div.data("olddisplay"), "olddisplay is undefined after hiding an already-hidden element" ); + ok( !jQuery._data(div, "olddisplay"), "olddisplay is undefined after hiding an already-hidden element" ); div.show(1, function () { equals( div.css("display"), "block", "Show a double-hidden element" ); start(); diff --git a/test/unit/event.js b/test/unit/event.js index b4672a8b8..02824a9a3 100644 --- a/test/unit/event.js +++ b/test/unit/event.js @@ -1,4 +1,4 @@ -module("event"); +module("event", { teardown: moduleTeardown }); test("null or undefined handler", function() { expect(2); @@ -28,7 +28,7 @@ test("bind(), with data", function() { }; jQuery("#firstp").bind("click", {foo: "bar"}, handler).click().unbind("click", handler); - ok( !jQuery.data(jQuery("#firstp")[0], "events"), "Event handler unbound when using data." ); + ok( !jQuery._data(jQuery("#firstp")[0], "events"), "Event handler unbound when using data." ); }); test("click(), with data", function() { @@ -39,7 +39,7 @@ test("click(), with data", function() { }; jQuery("#firstp").click({foo: "bar"}, handler).click().unbind("click", handler); - ok( !jQuery.data(jQuery("#firstp")[0], "events"), "Event handler unbound when using data." ); + ok( !jQuery._data(jQuery("#firstp")[0], "events"), "Event handler unbound when using data." ); }); test("bind(), with data, trigger with data", function() { @@ -80,6 +80,9 @@ test("bind(), multiple events at once and namespaces", function() { cur = "focusin"; div.trigger("focusin.a"); + // manually clean up detached elements + div.remove(); + div = jQuery("
").bind("click mouseover", obj, function(e) { equals( e.type, cur, "Verify right multi event was fired." ); equals( e.data, obj, "Make sure the data came in correctly." ); @@ -91,6 +94,9 @@ test("bind(), multiple events at once and namespaces", function() { cur = "mouseover"; div.trigger("mouseover"); + // manually clean up detached elements + div.remove(); + div = jQuery("
").bind("focusin.a focusout.b", function(e) { equals( e.type, cur, "Verify right multi event was fired." ); }); @@ -100,6 +106,9 @@ test("bind(), multiple events at once and namespaces", function() { cur = "focusout"; div.trigger("focusout.b"); + + // manually clean up detached elements + div.remove(); }); test("bind(), namespace with special add", function() { @@ -295,15 +304,15 @@ test("live/delegate immediate propagation", function() { $p.undelegate( "click" ); }); -test("bind/delegate bubbling, isDefaultPrevented (Bug #7793)", function() { +test("bind/delegate bubbling, isDefaultPrevented", function() { expect(2); var $anchor2 = jQuery( "#anchor2" ), $main = jQuery( "#main" ), fakeClick = function($jq) { // Use a native click so we don't get jQuery simulated bubbling if ( document.createEvent ) { - var e = document.createEvent( "MouseEvents" ); - e.initEvent( "click", true, true ); + var e = document.createEvent( 'MouseEvents' ); + e.initEvent( "click", true, true ); $jq[0].dispatchEvent(e); } else if ( $jq[0].click ) { @@ -314,7 +323,15 @@ test("bind/delegate bubbling, isDefaultPrevented (Bug #7793)", function() { e.preventDefault(); }); $main.delegate("#foo", "click", function(e) { - equals( e.isDefaultPrevented(), true, "isDefaultPrevented true passed to bubbled event" ); + var orig = e.originalEvent; + + if ( typeof(orig.defaultPrevented) === "boolean" || typeof(orig.returnValue) === "boolean" || orig.getPreventDefault ) { + equals( e.isDefaultPrevented(), true, "isDefaultPrevented true passed to bubbled event" ); + + } else { + // Opera < 11 doesn't implement any interface we can use, so give it a pass + ok( true, "isDefaultPrevented not supported by this browser, test skipped" ); + } }); fakeClick( $anchor2 ); $anchor2.unbind( "click" ); @@ -505,7 +522,7 @@ test("bind(), with different this object", function() { .bind("click", jQuery.proxy(handler1, thisObject)).click().unbind("click", handler1) .bind("click", data, jQuery.proxy(handler2, thisObject)).click().unbind("click", handler2); - ok( !jQuery.data(jQuery("#firstp")[0], "events"), "Event handler unbound when using different this object and data." ); + ok( !jQuery._data(jQuery("#firstp")[0], "events"), "Event handler unbound when using different this object and data." ); }); test("bind(name, false), unbind(name, false)", function() { @@ -525,6 +542,9 @@ test("bind(name, false), unbind(name, false)", function() { jQuery("#ap").unbind("click", false); jQuery("#ap").trigger("click"); equals( main, 1, "Verify that the trigger happened correctly." ); + + // manually clean up events from elements outside the fixture + jQuery("#main").unbind("click"); }); test("bind()/trigger()/unbind() on plain object", function() { @@ -547,7 +567,7 @@ test("bind()/trigger()/unbind() on plain object", function() { } }); - var events = jQuery(obj).data("__events__"); + var events = jQuery._data(obj, "events"); ok( events, "Object has events bound." ); equals( obj.events, undefined, "Events object on plain objects is not events" ); equals( typeof events, "function", "'events' expando is a function on plain objects." ); @@ -567,7 +587,9 @@ test("bind()/trigger()/unbind() on plain object", function() { // Make sure it doesn't complain when no events are found jQuery(obj).unbind("test"); - equals( obj.__events__, undefined, "Make sure events object is removed" ); + equals( obj && obj[ jQuery.expando ] && + obj[ jQuery.expando ][ jQuery.expando ] && + obj[ jQuery.expando ][ jQuery.expando ].events, undefined, "Make sure events object is removed" ); }); test("unbind(type)", function() { @@ -661,13 +683,18 @@ test("hover()", function() { test("trigger() shortcuts", function() { expect(6); - jQuery('
  • Change location
  • ').prependTo('#firstUL').find('a').bind('click', function() { + + var elem = jQuery('
  • Change location
  • ').prependTo('#firstUL'); + elem.find('a').bind('click', function() { var close = jQuery('spanx', this); // same with jQuery(this).find('span'); equals( close.length, 0, "Context element does not exist, length must be zero" ); ok( !close[0], "Context element does not exist, direct access to element must return undefined" ); return false; }).click(); + // manually clean up detached elements + elem.remove(); + jQuery("#check1").click(function() { ok( true, "click event handler for checkbox gets fired twice, see #815" ); }).click(); @@ -686,9 +713,12 @@ test("trigger() shortcuts", function() { jQuery('#simon1').click(); equals( clickCounter, 1, "Check that click, triggers onclick event handler on an a tag also" ); - jQuery('').load(function(){ + elem = jQuery('').load(function(){ ok( true, "Trigger the load event, using the shortcut .load() (#2819)"); }).load(); + + // manually clean up detached elements + elem.remove(); }); test("trigger() bubbling", function() { @@ -723,6 +753,10 @@ test("trigger() bubbling", function() { equals( body, 2, "ap bubble" ); equals( main, 1, "ap bubble" ); equals( ap, 1, "ap bubble" ); + + // manually clean up events from elements outside the fixture + jQuery(document).unbind("click"); + jQuery("html, body, #main").unbind("click"); }); test("trigger(type, [data], [fn])", function() { @@ -766,7 +800,7 @@ test("trigger(type, [data], [fn])", function() { pass = true; try { - jQuery('table:first').bind('test:test', function(){}).trigger('test:test'); + jQuery('#main table:first').bind('test:test', function(){}).trigger('test:test'); } catch (e) { pass = false; } @@ -947,9 +981,12 @@ test("toggle(Function, Function, ...)", function() { equals( turn, 2, "Trying toggle with 3 functions, attempt 5 yields 2"); $div.unbind('click',fns[0]); - var data = jQuery.data( $div[0], 'events' ); + var data = jQuery._data( $div[0], 'events' ); ok( !data, "Unbinding one function from toggle unbinds them all"); + // manually clean up detached elements + $div.remove(); + // Test Multi-Toggles var a = [], b = []; $div = jQuery("
    "); @@ -965,6 +1002,9 @@ test("toggle(Function, Function, ...)", function() { $div.click(); same( a, [1,2,1], "Check that a click worked with a second toggle, second click." ); same( b, [1,2], "Check that a click worked with a second toggle, second click." ); + + // manually clean up detached elements + $div.remove(); }); test(".live()/.die()", function() { @@ -1065,7 +1105,7 @@ test(".live()/.die()", function() { equals( clicked, 2, "live with a context" ); // Make sure the event is actually stored on the context - ok( jQuery.data(container, "events").live, "live with a context" ); + ok( jQuery._data(container, "events").live, "live with a context" ); // Test unbinding with a different context jQuery("#foo", container).die("click"); @@ -1275,6 +1315,9 @@ test("live with multiple events", function(){ div.trigger("submit"); equals( count, 2, "Make sure both the click and submit were triggered." ); + + // manually clean up events from elements outside the fixture + div.die(); }); test("live with namespaces", function(){ @@ -1578,7 +1621,7 @@ test(".delegate()/.undelegate()", function() { equals( clicked, 2, "delegate with a context" ); // Make sure the event is actually stored on the context - ok( jQuery.data(container, "events").live, "delegate with a context" ); + ok( jQuery._data(container, "events").live, "delegate with a context" ); // Test unbinding with a different context jQuery("#main").undelegate("#foo", "click"); @@ -1907,7 +1950,7 @@ test("window resize", function() { ok( true, "Resize event fired." ); }).resize().unbind("resize"); - ok( !jQuery(window).data("__events__"), "Make sure all the events are gone." ); + ok( !jQuery._data(window, "__events__"), "Make sure all the events are gone." ); }); test("focusin bubbles", function() { diff --git a/test/unit/manipulation.js b/test/unit/manipulation.js index e273cf08a..37234d86d 100644 --- a/test/unit/manipulation.js +++ b/test/unit/manipulation.js @@ -1,4 +1,4 @@ -module("manipulation"); +module("manipulation", { teardown: moduleTeardown }); // Ensure that an extended Array prototype doesn't break jQuery Array.prototype.arrayProtoFn = function(arg) { throw("arrayProtoFn should not be called"); }; @@ -115,12 +115,19 @@ var testWrap = function(val) { // Wrap an element with a jQuery set and event result = jQuery("
    ").click(function(){ ok(true, "Event triggered."); + + // Remove handlers on detached elements + result.unbind(); + jQuery(this).unbind(); }); j = jQuery("").wrap(result); equals( j[0].parentNode.nodeName.toLowerCase(), "div", "Wrapping works." ); j.parent().trigger("click"); + + // clean up attached elements + QUnit.reset(); } test("wrap(String|Element)", function() { @@ -395,7 +402,7 @@ test("append(Function) with incoming value", function() { }); test("append the same fragment with events (Bug #6997, 5566)", function () { - expect(4 + (document.fireEvent ? 1 : 0)); + expect(2 + (document.fireEvent ? 1 : 0)); stop(1000); var element; @@ -408,8 +415,12 @@ test("append the same fragment with events (Bug #6997, 5566)", function () { ok(true, "Event exists on original after being unbound on clone"); jQuery(this).unbind('click'); }); - element.clone(true).unbind('click')[0].fireEvent('onclick'); + var clone = element.clone(true).unbind('click'); + clone[0].fireEvent('onclick'); element[0].fireEvent('onclick'); + + // manually clean up detached elements + clone.remove(); } element = jQuery("").click(function () { @@ -426,14 +437,6 @@ test("append the same fragment with events (Bug #6997, 5566)", function () { jQuery("#listWithTabIndex li").before(element); jQuery("#listWithTabIndex li.test6997").eq(1).click(); - - element = jQuery(""); - - equals( element.clone().find("option:selected").val(), element.find("option:selected").val(), "Selected option cloned correctly" ); - - element = jQuery("").attr('checked', 'checked'); - - equals( element.clone().is(":checked"), element.is(":checked"), "Checked input cloned correctly" ); }); test("appendTo(String|Element|Array<Element>|jQuery)", function() { @@ -894,20 +897,36 @@ test("clone()", function() { ok( true, "Bound event still exists." ); }); - div = div.clone(true).clone(true); + clone = div.clone(true); + + // manually clean up detached elements + div.remove(); + + div = clone.clone(true); + + // manually clean up detached elements + clone.remove(); + equals( div.length, 1, "One element cloned" ); equals( div[0].nodeName.toUpperCase(), "DIV", "DIV element cloned" ); div.trigger("click"); + // manually clean up detached elements + div.remove(); + div = jQuery("
    ").append([ document.createElement("table"), document.createElement("table") ]); div.find("table").click(function(){ ok( true, "Bound event still exists." ); }); - div = div.clone(true); - equals( div.length, 1, "One element cloned" ); - equals( div[0].nodeName.toUpperCase(), "DIV", "DIV element cloned" ); - div.find("table:last").trigger("click"); + clone = div.clone(true); + equals( clone.length, 1, "One element cloned" ); + equals( clone[0].nodeName.toUpperCase(), "DIV", "DIV element cloned" ); + clone.find("table:last").trigger("click"); + + // manually clean up detached elements + div.remove(); + clone.remove(); // this is technically an invalid object, but because of the special // classid instantiation it is the only kind that IE has trouble with, @@ -928,12 +947,16 @@ test("clone()", function() { equals( clone[0].nodeName.toUpperCase(), "DIV", "DIV element cloned" ); div = jQuery("
    ").data({ a: true }); - var div2 = div.clone(true); - equals( div2.data("a"), true, "Data cloned." ); - div2.data("a", false); - equals( div2.data("a"), false, "Ensure cloned element data object was correctly modified" ); + clone = div.clone(true); + equals( clone.data("a"), true, "Data cloned." ); + clone.data("a", false); + equals( clone.data("a"), false, "Ensure cloned element data object was correctly modified" ); equals( div.data("a"), true, "Ensure cloned element data object is copied, not referenced" ); + // manually clean up detached elements + div.remove(); + clone.remove(); + var form = document.createElement("form"); form.action = "/test/"; var div = document.createElement("div"); @@ -945,6 +968,28 @@ test("clone()", function() { equal( jQuery("body").clone().children()[0].id, "qunit-header", "Make sure cloning body works" ); }); +test("clone(form element) (Bug #3879, #6655)", function() { + expect(6); + element = jQuery(""); + + equals( element.clone().find("option:selected").val(), element.find("option:selected").val(), "Selected option cloned correctly" ); + + element = jQuery("").attr('checked', 'checked'); + clone = element.clone(); + + equals( clone.is(":checked"), element.is(":checked"), "Checked input cloned correctly" ); + equals( clone[0].defaultValue, "foo", "Checked input defaultValue cloned correctly" ); + equals( clone[0].defaultChecked, !jQuery.support.noCloneEvent, "Checked input defaultChecked cloned correctly" ); + + element = jQuery(""); + clone = element.clone(); + equals( clone[0].defaultValue, "foo", "Text input defaultValue cloned correctly" ); + + element = jQuery(""); + clone = element.clone(); + equals( clone[0].defaultValue, "foo", "Textarea defaultValue cloned correctly" ); +}); + if (!isLocal) { test("clone() on XML nodes", function() { expect(2); @@ -1141,15 +1186,21 @@ var testRemove = function(method) { jQuery("#nonnodes").contents()[method](); equals( jQuery("#nonnodes").contents().length, 0, "Check node,textnode,comment remove works" ); + // manually clean up detached elements + if (method === "detach") { + first.remove(); + } + QUnit.reset(); var count = 0; var first = jQuery("#ap").children(":first"); - var cleanUp = first.click(function() { count++ })[method]().appendTo("body").click(); + var cleanUp = first.click(function() { count++ })[method]().appendTo("#main").click(); equals( method == "remove" ? 0 : 1, count ); - cleanUp.detach(); + // manually clean up detached elements + cleanUp.remove(); }; test("remove()", function() { diff --git a/test/unit/offset.js b/test/unit/offset.js index 69bf6d5d0..bac900d69 100644 --- a/test/unit/offset.js +++ b/test/unit/offset.js @@ -1,4 +1,4 @@ -module("offset"); +module("offset", { teardown: moduleTeardown }); test("disconnected node", function() { expect(2); diff --git a/test/unit/queue.js b/test/unit/queue.js index eada0eede..31e587db2 100644 --- a/test/unit/queue.js +++ b/test/unit/queue.js @@ -1,4 +1,4 @@ -module("queue"); +module("queue", { teardown: moduleTeardown }); test("queue() with other types",function() { expect(9); diff --git a/test/unit/selector.js b/test/unit/selector.js index 5b758c101..6a3832555 100644 --- a/test/unit/selector.js +++ b/test/unit/selector.js @@ -1,4 +1,4 @@ -module("selector"); +module("selector", { teardown: moduleTeardown }); test("element", function() { expect(21); @@ -58,13 +58,14 @@ if ( location.protocol != "file:" ) { } test("broken", function() { - expect(8); + expect(19); + function broken(name, selector) { try { jQuery(selector); ok( false, name + ": " + selector ); } catch(e){ - ok( typeof e === "string" && e.indexOf("Syntax error") >= 0, + ok( typeof e === "string" && e.indexOf("Syntax error") >= 0, name + ": " + selector ); } } @@ -77,10 +78,31 @@ test("broken", function() { broken( "Broken Selector", "<>", [] ); broken( "Broken Selector", "{}", [] ); broken( "Doesn't exist", ":visble", [] ); + broken( "Nth-child", ":nth-child", [] ); + broken( "Nth-child", ":nth-child(-)", [] ); + // Sigh. WebKit thinks this is a real selector in qSA + // They've already fixed this and it'll be coming into + // current browsers soon. + //broken( "Nth-child", ":nth-child(asdf)", [] ); + broken( "Nth-child", ":nth-child(2n+-0)", [] ); + broken( "Nth-child", ":nth-child(2+0)", [] ); + broken( "Nth-child", ":nth-child(- 1n)", [] ); + broken( "Nth-child", ":nth-child(-1 n)", [] ); + broken( "First-child", ":first-child(n)", [] ); + broken( "Last-child", ":last-child(n)", [] ); + broken( "Only-child", ":only-child(n)", [] ); + + // Make sure attribute value quoting works correctly. See: #6093 + var attrbad = jQuery('').appendTo("body"); + + broken( "Attribute not escaped", "input[name=foo.baz]", [] ); + broken( "Attribute not escaped", "input[name=foo[baz]]", [] ); + + attrbad.remove(); }); test("id", function() { - expect(28); + expect(29); t( "ID Selector", "#body", ["body"] ); t( "ID Selector w/ Element", "body#body", ["body"] ); t( "ID Selector w/ Element", "ul#first", [] ); @@ -116,6 +138,9 @@ test("id", function() { same( jQuery("body").find("div#form").get(), [], "ID selector within the context of another element" ); + //#7533 + equal( jQuery("

    foo

    ").find("p").length, 1, "Find where context root is a node and has an ID with CSS3 meta characters" ); + t( "Underscore ID", "#types_all", ["types_all"] ); t( "Dash ID", "#fx-queue", ["fx-queue"] ); @@ -150,7 +175,7 @@ test("class", function() { t( "Child escaped Class", "form > .test\\.foo\\[5\\]bar", ["test.foo[5]bar"] ); var div = document.createElement("div"); - div.innerHTML = "
    "; + div.innerHTML = "
    "; same( jQuery(".e", div).get(), [ div.firstChild ], "Finding a second class." ); div.lastChild.className = "e"; @@ -202,7 +227,7 @@ test("multiple", function() { }); test("child and adjacent", function() { - expect(27); + expect(31); t( "Child", "p > a", ["simon1","google","groups","mark","yahoo","simon"] ); t( "Child", "p> a", ["simon1","google","groups","mark","yahoo","simon"] ); t( "Child", "p >a", ["simon1","google","groups","mark","yahoo","simon"] ); @@ -210,20 +235,25 @@ test("child and adjacent", function() { t( "Child w/ Class", "p > a.blog", ["mark","simon"] ); t( "All Children", "code > *", ["anchor1","anchor2"] ); t( "All Grandchildren", "p > * > *", ["anchor1","anchor2"] ); - t( "Adjacent", "#main a + a", ["groups"] ); - t( "Adjacent", "#main a +a", ["groups"] ); - t( "Adjacent", "#main a+ a", ["groups"] ); - t( "Adjacent", "#main a+a", ["groups"] ); + t( "Adjacent", "a + a", ["groups"] ); + t( "Adjacent", "a +a", ["groups"] ); + t( "Adjacent", "a+ a", ["groups"] ); + t( "Adjacent", "a+a", ["groups"] ); t( "Adjacent", "p + p", ["ap","en","sap"] ); t( "Adjacent", "p#firstp + p", ["ap"] ); t( "Adjacent", "p[lang=en] + p", ["sap"] ); t( "Adjacent", "a.GROUPS + code + a", ["mark"] ); - t( "Comma, Child, and Adjacent", "#main a + a, code > a", ["groups","anchor1","anchor2"] ); + t( "Comma, Child, and Adjacent", "a + a, code > a", ["groups","anchor1","anchor2"] ); t( "Element Preceded By", "p ~ div", ["foo", "moretests","tabindex-tests", "liveHandlerOrder", "siblingTest"] ); t( "Element Preceded By", "#first ~ div", ["moretests","tabindex-tests", "liveHandlerOrder", "siblingTest"] ); t( "Element Preceded By", "#groups ~ a", ["mark"] ); t( "Element Preceded By", "#length ~ input", ["idTest"] ); t( "Element Preceded By", "#siblingfirst ~ em", ["siblingnext"] ); + same( jQuery("#siblingfirst").find("~ em").get(), q("siblingnext"), "Element Preceded By with a context." ); + same( jQuery("#siblingfirst").find("+ em").get(), q("siblingnext"), "Element Directly Preceded By with a context." ); + var a = jQuery("

    "); + same( jQuery("~ p", a[0]).get(), [a[1], a[2]], "Detached Element Directly Preceded By with a context." ); + same( jQuery("+ p", a[0]).get(), [a[1]], "Detached Element Preceded By with a context." ); t( "Verify deep class selector", "div.blah > p > a", [] ); @@ -237,7 +267,8 @@ test("child and adjacent", function() { }); test("attributes", function() { - expect(39); + expect(41); + t( "Attribute Exists", "a[title]", ["google"] ); t( "Attribute Exists", "*[title]", ["google"] ); t( "Attribute Exists", "[title]", ["google"] ); @@ -294,10 +325,18 @@ test("attributes", function() { t("Select options via :selected", "#select3 option:selected", ["option3b", "option3c"] ); t( "Grouped Form Elements", "input[name='foo[bar]']", ["hidden2"] ); + + // Make sure attribute value quoting works correctly. See: #6093 + var attrbad = jQuery('').appendTo("body"); + + t("Find escaped attribute value", "input[name=foo\\.baz]", ["attrbad1"]); + t("Find escaped attribute value", "input[name=foo\\[baz\\]]", ["attrbad2"]); + + attrbad.remove(); }); test("pseudo - child", function() { - expect(31); + expect(38); t( "First Child", "p:first-child", ["firstp","sndp"] ); t( "Last Child", "p:last-child", ["sap"] ); t( "Only Child", "#main a:only-child", ["simon1","anchor1","yahoo","anchor2","liveLink1","liveLink2"] ); @@ -306,6 +345,7 @@ test("pseudo - child", function() { t( "First Child", "p:first-child", ["firstp","sndp"] ); t( "Nth Child", "p:nth-child(1)", ["firstp","sndp"] ); + t( "Nth Child With Whitespace", "p:nth-child( 1 )", ["firstp","sndp"] ); t( "Not Nth Child", "p:not(:nth-child(1))", ["ap","en","sap","first"] ); // Verify that the child position isn't being cached improperly @@ -322,15 +362,19 @@ test("pseudo - child", function() { t( "Nth-child", "#main form#form > *:nth-child(2)", ["text1"] ); t( "Nth-child", "#main form#form > :nth-child(2)", ["text1"] ); + t( "Nth-child", "#form select:first option:nth-child(-1)", [] ); t( "Nth-child", "#form select:first option:nth-child(3)", ["option1c"] ); t( "Nth-child", "#form select:first option:nth-child(0n+3)", ["option1c"] ); t( "Nth-child", "#form select:first option:nth-child(1n+0)", ["option1a", "option1b", "option1c", "option1d"] ); t( "Nth-child", "#form select:first option:nth-child(1n)", ["option1a", "option1b", "option1c", "option1d"] ); t( "Nth-child", "#form select:first option:nth-child(n)", ["option1a", "option1b", "option1c", "option1d"] ); + t( "Nth-child", "#form select:first option:nth-child(+n)", ["option1a", "option1b", "option1c", "option1d"] ); t( "Nth-child", "#form select:first option:nth-child(even)", ["option1b", "option1d"] ); t( "Nth-child", "#form select:first option:nth-child(odd)", ["option1a", "option1c"] ); t( "Nth-child", "#form select:first option:nth-child(2n)", ["option1b", "option1d"] ); t( "Nth-child", "#form select:first option:nth-child(2n+1)", ["option1a", "option1c"] ); + t( "Nth-child", "#form select:first option:nth-child(2n + 1)", ["option1a", "option1c"] ); + t( "Nth-child", "#form select:first option:nth-child(+2n + 1)", ["option1a", "option1c"] ); t( "Nth-child", "#form select:first option:nth-child(3n)", ["option1c"] ); t( "Nth-child", "#form select:first option:nth-child(3n+1)", ["option1a", "option1d"] ); t( "Nth-child", "#form select:first option:nth-child(3n+2)", ["option1b"] ); @@ -339,7 +383,9 @@ test("pseudo - child", function() { t( "Nth-child", "#form select:first option:nth-child(3n-2)", ["option1a", "option1d"] ); t( "Nth-child", "#form select:first option:nth-child(3n-3)", ["option1c"] ); t( "Nth-child", "#form select:first option:nth-child(3n+0)", ["option1c"] ); + t( "Nth-child", "#form select:first option:nth-child(-1n+3)", ["option1a", "option1b", "option1c"] ); t( "Nth-child", "#form select:first option:nth-child(-n+3)", ["option1a", "option1b", "option1c"] ); + t( "Nth-child", "#form select:first option:nth-child(-1n + 3)", ["option1a", "option1b", "option1c"] ); }); test("pseudo - misc", function() { diff --git a/test/unit/traversing.js b/test/unit/traversing.js index 31cffc2eb..f0471d74e 100644 --- a/test/unit/traversing.js +++ b/test/unit/traversing.js @@ -1,4 +1,4 @@ -module("traversing"); +module("traversing", { teardown: moduleTeardown }); test("find(String)", function() { expect(5); diff --git a/version.txt b/version.txt index 7810cd6c7..574f968af 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.4.5pre \ No newline at end of file +1.5pre \ No newline at end of file