Skip to content

Commit

Permalink
Selector: Introduce Sizzle.escape
Browse files Browse the repository at this point in the history
SemVer minor
Fixes jquerygh-363
Ref jquery/jquery#1761
  • Loading branch information
gibson042 committed Nov 9, 2015
1 parent 3750f57 commit 312584f
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 19 deletions.
34 changes: 26 additions & 8 deletions dist/sizzle.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* Released under the MIT license
* http://jquery.org/license
*
* Date: 2015-11-06
* Date: 2015-11-09
*/
(function( window ) {

Expand Down Expand Up @@ -75,7 +75,7 @@ var i,
whitespace = "[\\x20\\t\\r\\n\\f]",

// http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier
identifier = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",
identifier = "(?:\\\\.|[\\w-]|[^\0-\\xa0])+",

// Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors
attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace +
Expand Down Expand Up @@ -132,9 +132,9 @@ var i,
rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,

rsibling = /[+~]/,
rescape = /'|\\/g,

// CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters
// CSS escapes
// http://www.w3.org/TR/CSS21/syndata.html#escaped-characters
runescape = new RegExp( "\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig" ),
funescape = function( _, escaped, escapedWhitespace ) {
var high = "0x" + escaped - 0x10000;
Expand All @@ -150,6 +150,21 @@ var i,
String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 );
},

// CSS string/identifier serialization
// https://drafts.csswg.org/cssom/#common-serializing-idioms
rcssescape = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\x80-\uFFFF\w-]/g,
fcssescape = function( ch, numeric, i, str ) {
if ( ch === "\0" ) {
throw new Error( "Cannot escape U+0000: " + str );
}
if ( numeric ) {
// Preserve the prefix (if any), code-point-escape the offending character
return ch.slice( 0, -1 ) + "\\" + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + " ";
} else {
return "\\" + ch;
}
},

// Used for iframes
// See setDocument()
// Removing the function wrapper causes a "Permission Denied"
Expand Down Expand Up @@ -188,7 +203,7 @@ try {
}

function Sizzle( selector, context, results, seed ) {
var m, i, elem, nid, nidselect, match, groups, newSelector,
var m, i, elem, nid, match, groups, newSelector,
newContext = context && context.ownerDocument,

// nodeType defaults to 9, since context defaults to document
Expand Down Expand Up @@ -281,17 +296,16 @@ function Sizzle( selector, context, results, seed ) {

// Capture the context ID, setting it first if necessary
if ( (nid = context.getAttribute( "id" )) ) {
nid = nid.replace( rescape, "\\$&" );
nid = nid.replace( rcssescape, fcssescape );
} else {
context.setAttribute( "id", (nid = expando) );
}

// Prefix every selector in the list
groups = tokenize( selector );
i = groups.length;
nidselect = ridentifier.test( nid ) ? "#" + nid : "[id='" + nid + "']";
while ( i-- ) {
groups[i] = nidselect + " " + toSelector( groups[i] );
groups[i] = "#" + nid + " " + toSelector( groups[i] );
}
newSelector = groups.join( "," );

Expand Down Expand Up @@ -917,6 +931,10 @@ Sizzle.attr = function( elem, name ) {
null;
};

Sizzle.escape = function( sel ) {
return (sel + "").replace( rcssescape, fcssescape );
};

Sizzle.error = function( msg ) {
throw new Error( "Syntax error, unrecognized expression: " + msg );
};
Expand Down
2 changes: 1 addition & 1 deletion dist/sizzle.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/sizzle.min.map

Large diffs are not rendered by default.

32 changes: 25 additions & 7 deletions src/sizzle.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ var i,
whitespace = "[\\x20\\t\\r\\n\\f]",

// http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier
identifier = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",
identifier = "(?:\\\\.|[\\w-]|[^\0-\\xa0])+",

// Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors
attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace +
Expand Down Expand Up @@ -132,9 +132,9 @@ var i,
rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,

rsibling = /[+~]/,
rescape = /'|\\/g,

// CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters
// CSS escapes
// http://www.w3.org/TR/CSS21/syndata.html#escaped-characters
runescape = new RegExp( "\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig" ),
funescape = function( _, escaped, escapedWhitespace ) {
var high = "0x" + escaped - 0x10000;
Expand All @@ -150,6 +150,21 @@ var i,
String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 );
},

// CSS string/identifier serialization
// https://drafts.csswg.org/cssom/#common-serializing-idioms
rcssescape = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\x80-\uFFFF\w-]/g,
fcssescape = function( ch, numeric, i, str ) {
if ( ch === "\0" ) {
throw new Error( "Cannot escape U+0000: " + str );
}
if ( numeric ) {
// Preserve the prefix (if any), code-point-escape the offending character
return ch.slice( 0, -1 ) + "\\" + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + " ";
} else {
return "\\" + ch;
}
},

// Used for iframes
// See setDocument()
// Removing the function wrapper causes a "Permission Denied"
Expand Down Expand Up @@ -188,7 +203,7 @@ try {
}

function Sizzle( selector, context, results, seed ) {
var m, i, elem, nid, nidselect, match, groups, newSelector,
var m, i, elem, nid, match, groups, newSelector,
newContext = context && context.ownerDocument,

// nodeType defaults to 9, since context defaults to document
Expand Down Expand Up @@ -281,17 +296,16 @@ function Sizzle( selector, context, results, seed ) {

// Capture the context ID, setting it first if necessary
if ( (nid = context.getAttribute( "id" )) ) {
nid = nid.replace( rescape, "\\$&" );
nid = nid.replace( rcssescape, fcssescape );
} else {
context.setAttribute( "id", (nid = expando) );
}

// Prefix every selector in the list
groups = tokenize( selector );
i = groups.length;
nidselect = ridentifier.test( nid ) ? "#" + nid : "[id='" + nid + "']";
while ( i-- ) {
groups[i] = nidselect + " " + toSelector( groups[i] );
groups[i] = "#" + nid + " " + toSelector( groups[i] );
}
newSelector = groups.join( "," );

Expand Down Expand Up @@ -917,6 +931,10 @@ Sizzle.attr = function( elem, name ) {
null;
};

Sizzle.escape = function( sel ) {
return (sel + "").replace( rcssescape, fcssescape );
};

Sizzle.error = function( msg ) {
throw new Error( "Syntax error, unrecognized expression: " + msg );
};
Expand Down
2 changes: 1 addition & 1 deletion test/data/fixtures.html
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
<param name="p2" value="x2" />
</object>

<span id="台北Táiběi"></span>
<span id="台北Táiběi"><span id="台北Táiběi-child"></span></span>
<span id="台北" lang="中文"></span>
<span id="utf8class1" class="台北Táiběi 台北">"'台北Táiběi"'</span>
<span id="utf8class2" class="台北"></span>
Expand Down
16 changes: 15 additions & 1 deletion test/unit/selector.js
Original file line number Diff line number Diff line change
Expand Up @@ -1146,7 +1146,7 @@ test("pseudo - :lang", function() {
});

test("context", function() {
expect( 16 );
expect( 21 );

var context,
selector = ".blog",
Expand All @@ -1172,10 +1172,24 @@ test("context", function() {
deepEqual( Sizzle( "*", context ), q( "nothiddendivchild" ), "<div> context" );
deepEqual( Sizzle( "* > *", context ), [], "<div> context (no results)" );

context.removeAttribute( "id" );
deepEqual( Sizzle( "*", context ), q( "nothiddendivchild" ), "no-id element context" );
deepEqual( Sizzle( "* > *", context ), [], "no-id element context (no results)" );

// Support: IE<8 only
// ID attroperty is never really gone
strictEqual( context.getAttribute( "id" ) || "", "", "id not added by no-id selection" );

context = document.getElementById( "lengthtest" );
deepEqual( Sizzle( "input", context ), q( "length", "idTest" ), "<form> context");
deepEqual( Sizzle( "select", context ), [], "<form> context (no results)");

context = document.getElementById( "台北Táiběi" );
expected = q( "台北Táiběi-child" );
deepEqual( Sizzle( "span[id]", context ), expected, "context with non-ASCII id");
deepEqual( Sizzle( "#台北Táiběi span[id]", context.parentNode ), expected,
"context with non-ASCII id selector prefix");

context = document.createDocumentFragment();
// Capture *independent* expected nodes before they're detached from the page
expected = q( "siblingnext", "siblingspan" );
Expand Down
97 changes: 97 additions & 0 deletions test/unit/utilities.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,103 @@ test("Sizzle.attr (XML)", function() {
testAttr( jQuery.parseXML("<root/>") );
});

test("Sizzle.escape", function( assert ) {
// Edge cases
assert.equal( Sizzle.escape(), "undefined", "Converts undefined to string" );
assert.equal( Sizzle.escape("-"), "\\-", "Escapes standalone dash" );
assert.equal( Sizzle.escape("-a"), "-a", "Doesn't escape leading dash followed by non-number" );
assert.equal( Sizzle.escape("--"), "--", "Doesn't escape standalone double dash" );

// Derived from CSSOM tests
// https://test.csswg.org/harness/test/cssom-1_dev/section/7.1/

// String conversion
assert.equal( Sizzle.escape( true ), "true", "Converts boolean true to string" );
assert.equal( Sizzle.escape( false ), "false", "Converts boolean true to string" );
assert.equal( Sizzle.escape( null ), "null", "Converts null to string" );
assert.equal( Sizzle.escape( "" ), "", "Doesn't modify empty string" );

// Null bytes
assert.throws( function() { Sizzle.escape( "\0" ); }, /escape/,
"Throws on null-character input" );
assert.throws( function() { Sizzle.escape( "a\0" ); }, /escape/,
"Throws on trailing-null input" );
assert.throws( function() { Sizzle.escape( "\0b" ); }, /escape/,
"Throws on leading-null input" );
assert.throws( function() { Sizzle.escape( "a\0b" ); }, /escape/,
"Throws on embedded-null input" );

// Number prefix
assert.equal( Sizzle.escape( "0a" ), "\\30 a", "Escapes leading 0" );
assert.equal( Sizzle.escape( "1a" ), "\\31 a", "Escapes leading 1" );
assert.equal( Sizzle.escape( "2a" ), "\\32 a", "Escapes leading 2" );
assert.equal( Sizzle.escape( "3a" ), "\\33 a", "Escapes leading 3" );
assert.equal( Sizzle.escape( "4a" ), "\\34 a", "Escapes leading 4" );
assert.equal( Sizzle.escape( "5a" ), "\\35 a", "Escapes leading 5" );
assert.equal( Sizzle.escape( "6a" ), "\\36 a", "Escapes leading 6" );
assert.equal( Sizzle.escape( "7a" ), "\\37 a", "Escapes leading 7" );
assert.equal( Sizzle.escape( "8a" ), "\\38 a", "Escapes leading 8" );
assert.equal( Sizzle.escape( "9a" ), "\\39 a", "Escapes leading 9" );

// Letter-number prefix
assert.equal( Sizzle.escape( "a0b" ), "a0b", "Doesn't escape embedded 0" );
assert.equal( Sizzle.escape( "a1b" ), "a1b", "Doesn't escape embedded 1" );
assert.equal( Sizzle.escape( "a2b" ), "a2b", "Doesn't escape embedded 2" );
assert.equal( Sizzle.escape( "a3b" ), "a3b", "Doesn't escape embedded 3" );
assert.equal( Sizzle.escape( "a4b" ), "a4b", "Doesn't escape embedded 4" );
assert.equal( Sizzle.escape( "a5b" ), "a5b", "Doesn't escape embedded 5" );
assert.equal( Sizzle.escape( "a6b" ), "a6b", "Doesn't escape embedded 6" );
assert.equal( Sizzle.escape( "a7b" ), "a7b", "Doesn't escape embedded 7" );
assert.equal( Sizzle.escape( "a8b" ), "a8b", "Doesn't escape embedded 8" );
assert.equal( Sizzle.escape( "a9b" ), "a9b", "Doesn't escape embedded 9" );

// Dash-number prefix
assert.equal( Sizzle.escape( "-0a" ), "-\\30 a", "Escapes 0 after leading dash" );
assert.equal( Sizzle.escape( "-1a" ), "-\\31 a", "Escapes 1 after leading dash" );
assert.equal( Sizzle.escape( "-2a" ), "-\\32 a", "Escapes 2 after leading dash" );
assert.equal( Sizzle.escape( "-3a" ), "-\\33 a", "Escapes 3 after leading dash" );
assert.equal( Sizzle.escape( "-4a" ), "-\\34 a", "Escapes 4 after leading dash" );
assert.equal( Sizzle.escape( "-5a" ), "-\\35 a", "Escapes 5 after leading dash" );
assert.equal( Sizzle.escape( "-6a" ), "-\\36 a", "Escapes 6 after leading dash" );
assert.equal( Sizzle.escape( "-7a" ), "-\\37 a", "Escapes 7 after leading dash" );
assert.equal( Sizzle.escape( "-8a" ), "-\\38 a", "Escapes 8 after leading dash" );
assert.equal( Sizzle.escape( "-9a" ), "-\\39 a", "Escapes 9 after leading dash" );

// Double dash prefix
assert.equal( Sizzle.escape( "--a" ), "--a", "Doesn't escape leading double dash" );

// Miscellany
assert.equal( Sizzle.escape( "\x01\x02\x1E\x1F" ), "\\1 \\2 \\1e \\1f ",
"Escapes C0 control characters" );
assert.equal( Sizzle.escape( "\x80\x2D\x5F\xA9" ), "\x80\x2D\x5F\xA9",
"Doesn't escape general punctuation or non-ASCII ISO-8859-1 characters" );
assert.equal(
Sizzle.escape( "\x7F\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8A\x8B\x8C\x8D\x8E\x8F\x90" +
"\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9A\x9B\x9C\x9D\x9E\x9F" ),
"\\7f \x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8A\x8B\x8C\x8D\x8E\x8F\x90" +
"\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9A\x9B\x9C\x9D\x9E\x9F",
"Escapes DEL control character"
);
assert.equal( Sizzle.escape( "\xA0\xA1\xA2" ), "\xA0\xA1\xA2",
"Doesn't escape non-ASCII ISO-8859-1 characters" );
assert.equal( Sizzle.escape( "a0123456789b" ), "a0123456789b",
"Doesn't escape embedded numbers" );
assert.equal( Sizzle.escape( "abcdefghijklmnopqrstuvwxyz" ), "abcdefghijklmnopqrstuvwxyz",
"Doesn't escape lowercase ASCII letters" );
assert.equal( Sizzle.escape( "ABCDEFGHIJKLMNOPQRSTUVWXYZ" ), "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
"Doesn't escape uppercase ASCII letters" );
assert.equal( Sizzle.escape( "\x20\x21\x78\x79" ), "\\ \\!xy",
"Escapes non-word ASCII characters" );

// Astral symbol (U+1D306 TETRAGRAM FOR CENTRE)
assert.equal( Sizzle.escape( "\uD834\uDF06" ), "\uD834\uDF06",
"Doesn't escape astral characters" );

// Lone surrogates
assert.equal( Sizzle.escape( "\uDF06" ), "\uDF06", "Doesn't escape lone low surrogate" );
assert.equal( Sizzle.escape( "\uD834" ), "\uD834", "Doesn't escape lone high surrogate" );
});

test("Sizzle.contains", function() {
expect( 16 );

Expand Down

0 comments on commit 312584f

Please sign in to comment.