Back to index

plone3  3.1.7
packer.py
Go to the documentation of this file.
00001 #
00002 # packer.py
00003 #
00004 # Copyright (c) 2006-2007 Florian Schulze <florian.schulze@gmx.net>
00005 #
00006 # Permission is hereby granted, free of charge, to any person obtaining a copy
00007 # of this software and associated documentation files (the "Software"), to
00008 # deal in the Software without restriction, including without limitation the
00009 # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
00010 # sell copies of the Software, and to permit persons to whom the Software is
00011 # furnished to do so, subject to the following conditions:
00012 #
00013 # The above copyright notice and this permission notice shall be included in
00014 # all copies or substantial portions of the Software.
00015 #
00016 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
00017 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
00018 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
00019 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
00020 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
00021 # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
00022 # IN THE SOFTWARE.
00023 
00024 import re, unittest, textwrap
00025 
00026 
00027 class KeywordMapper:
00028     def __init__(self, regexp, encoder):
00029         if isinstance(regexp, (str, unicode)):
00030             self.regexp = re.compile(regexp)
00031         else:
00032             self.regexp = regexp
00033         self.encoder = encoder
00034         self.mapping = {}
00035 
00036     def analyseKeywords(self, input):
00037         matches = self.regexp.findall(input)
00038 
00039         protected = {}
00040         keyword_count = {}
00041         index = 0
00042         for match in matches:
00043             if match not in keyword_count:
00044                 keyword_count[match] = 0
00045                 protected[self.encoder(index)] = index
00046                 index = index + 1
00047             keyword_count[match] = keyword_count[match] + 1
00048 
00049         for match in matches:
00050             if match in protected and keyword_count[match]:
00051                 keyword_count[match] = 0
00052 
00053         protected = {}
00054         for match in keyword_count:
00055             if not keyword_count[match]:
00056                 protected[match] = None
00057 
00058         ## sorted_matches = [(c,len(v),v) for v,c in keyword_count.iteritems()]
00059         # the above line implements the original behaviour, the code below
00060         # removes keywords which have not enough weight to be encoded, in total
00061         # this saves some bytes, because the total length of the generated
00062         # codes is a bit smaller. This needs corresponding code in the
00063         # fast_decode javascript function of the decoder, see comment there
00064         sorted_matches = []
00065         for value, count in keyword_count.iteritems():
00066             weight = count * len(value)
00067             if len(value) >= weight:
00068                 keyword_count[value] = 0
00069                 sorted_matches.append((0, value))
00070             else:
00071                 sorted_matches.append((weight, value))
00072         sorted_matches.sort()
00073         sorted_matches.reverse()
00074         sorted_matches = [x[-1] for x in sorted_matches]
00075 
00076         index = 0
00077         mapping = {}
00078         for match in sorted_matches:
00079             if not keyword_count[match]:
00080                 if match not in protected:
00081                     mapping[match] = (-1, match)
00082                 continue
00083             while 1:
00084                 encoded = self.encoder(index)
00085                 index = index + 1
00086                 if encoded in protected:
00087                     mapping[encoded] = (index-1, encoded)
00088                     continue
00089                 else:
00090                     break
00091             mapping[match] = (index-1, encoded)
00092 
00093         return mapping
00094 
00095     def analyse(self, input):
00096         self.mapping = self.analyseKeywords(input)
00097 
00098     def getKeywords(self):
00099         sorted = zip(self.mapping.itervalues(), self.mapping.iterkeys())
00100         sorted.sort()
00101         keywords = []
00102         for (index, encoded), value in sorted:
00103             if index >= 0:
00104                 if encoded != value:
00105                     keywords.append(value)
00106                 else:
00107                     keywords.append('')
00108         return keywords
00109 
00110     def sub(self, input):
00111         def repl(m):
00112             return self.mapping.get(m.group(0), ('', m.group(0)))[1]
00113         return self.regexp.sub(repl, input)
00114 
00115 
00116 class JavascriptKeywordMapper(KeywordMapper):
00117     def __init__(self, regexp=None, encoder=None):
00118         if regexp is None:
00119             self.regexp = re.compile(r'\w+')
00120         elif isinstance(regexp, (str, unicode)):
00121             self.regexp = re.compile(regexp)
00122         else:
00123             self.regexp = regexp
00124         if encoder is None:
00125             self.encoder = self._encode
00126         else:
00127             self.encoder = encoder
00128         self.mapping = {}
00129 
00130     def _encode(self, charCode,
00131                 mapping="0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"):
00132         result = []
00133         quotient = charCode
00134         while quotient or not len(result):
00135             quotient, remainder = divmod(quotient, 62)
00136             result.append(mapping[remainder])
00137         result.reverse()
00138         return "".join(result)
00139 
00140     def getDecodeFunction(self, fast=True, name=None):
00141         jspacker = JavascriptPacker('full')
00142 
00143         # fast boot function
00144         fast_decoder = r"""
00145             // does the browser support String.replace where the
00146             //  replacement value is a function?
00147             if (!''.replace(/^/, String)) {
00148                 // decode all the values we need
00149                 // we have to add the dollar prefix, because $encoded can be
00150                 // any keyword in the decode function below. For example
00151                 // 'constructor' is an attribute of any object and it would
00152                 // return a false positive match in that case.
00153                 while ($count--) $decode["$"+$encode($count)] = $keywords[$count] || $encode($count);
00154                 // global replacement function
00155                 $keywords = [function($encoded){$result = $decode["$"+$encoded]; return $result!=undefined?$result:$encoded}];
00156                 // generic match
00157                 $encode = function(){return'\\w+'};
00158                 // reset the loop counter -  we are now doing a global replace
00159                 $count = 1;
00160             };"""
00161 
00162         if name is None:
00163             # boot function
00164             decoder = r"""
00165                 function($packed, $ascii, $count, $keywords, $encode, $decode) {
00166                     $encode = function($charCode) {
00167                         return ($charCode < $ascii ? "" : $encode(parseInt($charCode / $ascii))) +
00168                             (($charCode = $charCode % $ascii) > 35 ? String.fromCharCode($charCode + 29) : $charCode.toString(36));
00169                     };
00170                     // fastDecodePlaceholder
00171                     while ($count--)
00172                         if ($keywords[$count])
00173                             $packed = $packed.replace(new RegExp("\\b" + $encode($count) + "\\b", "g"), $keywords[$count]);
00174                     return $packed;
00175                 }"""
00176 
00177             if fast:
00178                 decoder = decoder.replace('// fastDecodePlaceholder', fast_decoder)
00179 
00180             decoder = jspacker.pack(decoder)
00181 
00182         else:
00183             decoder = r"""
00184                 var %s = function($ascii, $count, $keywords, $encode, $decode) {
00185                     $encode = function($charCode) {
00186                         return ($charCode < $ascii ? "" : $encode(parseInt($charCode / $ascii))) +
00187                             (($charCode = $charCode %% $ascii) > 35 ? String.fromCharCode($charCode + 29) : $charCode.toString(36));
00188                     };
00189                     // fastDecodePlaceholder
00190                     var decoder = function($packed, $ascii1, $count1, $keywords1, $encode1, $decode1) {
00191                         $count1 = $count;
00192                         while ($count1--)
00193                             if ($keywords[$count1])
00194                                 $packed = $packed.replace(new RegExp("\\b" + $encode($count1) + "\\b", "g"), $keywords[$count1]);
00195                         return $packed;
00196                     };
00197                     return decoder;
00198                 }""" % name
00199 
00200             if fast:
00201                 decoder = decoder.replace('// fastDecodePlaceholder', fast_decoder)
00202 
00203             decoder = jspacker.pack(decoder)
00204 
00205             keywords = self.getKeywords()
00206             decoder = "%s(62, %i, '%s'.split('|'), 0, {});" % (decoder, len(keywords), "|".join(keywords))
00207 
00208         return decoder
00209 
00210     def getDecoder(self, input, keyword_var=None, decode_func=None):
00211         if keyword_var is None:
00212             keywords = self.getKeywords()
00213             num_keywords = len(keywords)
00214             keywords = "|".join(keywords)
00215             keywords = "'%s'.split('|')" % keywords
00216         else:
00217             keywords = keyword_var
00218             num_keywords = len(self.getKeywords())
00219 
00220         if decode_func is None:
00221             decode_func = self.getDecodeFunction()
00222 
00223         escaped_single = input.replace("\\","\\\\").replace("'","\\'").replace('\n','\\n')
00224         escaped_double = input.replace("\\","\\\\").replace('"','\\"').replace('\n','\\n')
00225         if len(escaped_single) < len(escaped_double):
00226             script = "'%s'" % escaped_single
00227         else:
00228             script = '"%s"' % escaped_double
00229         return "eval(%s(%s,62,%i,%s,0,{}))" % (decode_func, script,
00230                                                num_keywords,
00231                                                keywords)
00232 
00233 
00234 class Packer:
00235     def __init__(self):
00236         self.patterns = []
00237 
00238     def copy(self):
00239         result = Packer()
00240         result.patterns = self.patterns[:]
00241         return result
00242 
00243     def _repl(self, match):
00244         # store protected part
00245         self.replacelist.append(match.group(1))
00246         # return escaped index
00247         return "\x00%i" % len(self.replacelist)
00248 
00249     def pack(self, input):
00250         # list of protected parts
00251         self.replacelist = []
00252         # escape the escapechar
00253         output = input.replace('\x00','\x00\x00')
00254         for regexp, replacement, keyword_encoder in self.patterns:
00255             if replacement is None:
00256                 if keyword_encoder is None:
00257                     # protect the matched parts
00258                     output = regexp.sub(self._repl, output)
00259                 else:
00260                     mapper = KeywordMapper(regexp=regexp,
00261                                            encoder=keyword_encoder)
00262                     # get keywords
00263                     mapper.analyse(output)
00264                     # replace keywords
00265                     output = mapper.sub(output)
00266             else:
00267                 # substitute
00268                 output = regexp.sub(replacement, output)
00269         # restore protected parts
00270         replacelist = list(enumerate(self.replacelist))
00271         replacelist.reverse() # from back to front, so 1 doesn't break 10 etc.
00272         for index, replacement in replacelist:
00273             # we use lambda in here, so the real string is used and no escaping
00274             # is done on it
00275             before = len(output)
00276             regexp = re.compile('(?<!\x00)\x00%i' % (index+1))
00277             output = regexp.sub(lambda m:replacement, output)
00278         # unescape
00279         output = output.replace('\x00\x00','\x00')
00280         # done
00281         return output
00282 
00283     def protect(self, pattern, flags=None):
00284         self.keywordSub(pattern, None, flags)
00285 
00286     def sub(self, pattern, replacement, flags=None):
00287         if flags is None:
00288             self.patterns.append((re.compile(pattern), replacement, None))
00289         else:
00290             self.patterns.append((re.compile(pattern, flags), replacement, None))
00291 
00292     def keywordSub(self, pattern, keyword_encoder, flags=None):
00293         if flags is None:
00294             self.patterns.append((re.compile(pattern), None, keyword_encoder))
00295         else:
00296             self.patterns.append((re.compile(pattern, flags), None, keyword_encoder))
00297 
00298 
00299 class JavascriptPacker(Packer):
00300     def __init__(self, level='safe'):
00301         Packer.__init__(self)
00302         if level == 'full':
00303             # encode local variables. those are preceeded by dollar signs
00304             # the amount of dollar signs says how many characters are preserved
00305             # any trailing digits are preserved as well
00306             # $name -> n, $$name -> na, $top1 -> t1, $top2 -> t2
00307             def _dollar_replacement(match):
00308                 length = len(match.group(2))
00309                 start = length - max(length - len(match.group(3)), 0)
00310                 result = match.group(1)[start:start+length] + match.group(4)
00311                 return result
00312             self.sub(r"""((\$+)([a-zA-Z\$_]+))(\d*)\b""", _dollar_replacement)
00313             
00314             self.keywordSub(r"""\b_[A-Za-z\d]\w*""", lambda i: "_%i" % i)
00315     
00316             # protect strings
00317             # this is more correct, but needs more testing
00318             # it has to be more accurate because of the more aggresive packing later
00319             self.protect(r"""(?<=return|..case|.....[=\[|(,?:+])\s*((?P<quote>['"])(?:\\(?P=quote)|\\\n|.)*?(?P=quote))""", re.DOTALL)
00320         else:
00321             # protect strings
00322             # these sometimes catch to much, but in safe mode this doesn't hurt
00323             self.protect(r"""('(?:\\'|\\\n|.)*?')""")
00324             self.protect(r'''("(?:\\"|\\\n|.)*?")''')
00325         # protect regular expressions
00326         self.protect(r"""\s+(\/[^\/\n\r\*][^\/\n\r]*\/g?i?)""")
00327         self.protect(r"""([^\w\$\/'"*)\?:]\/[^\/\n\r\*][^\/\n\r]*\/g?i?)""")
00328         # multiline comments
00329         self.sub(r'/\*(?!@).*?\*/', '', re.DOTALL)
00330         # one line comments
00331         self.sub(r'\s*//.*$', '', re.MULTILINE)
00332         # strip whitespace at the beginning and end of each line
00333         self.sub(r'^[ \t\r\f\v]*(.*?)[ \t\r\f\v]*$', r'\1', re.MULTILINE)
00334         # whitespace after some special chars but not
00335         # before function declaration
00336         self.sub(r'([{;\[(,=&|\?:<>%!/])\s+(?!function)', r'\1')
00337         # after an equal sign a function definition is ok
00338         self.sub(r'=\s+(?=function)', r'=')
00339         if level == 'full':
00340             # whitespace after some more special chars
00341             self.sub(r'([};\):,])\s+', r'\1')
00342         # whitespace before some special chars
00343         self.sub(r'\s+([={},&|\?:\.()<>%!/\]])', r'\1')
00344         # whitespace before plus chars if no other plus char before it
00345         self.sub(r'(?<!\+)\s+\+', '+')
00346         # whitespace after plus chars if no other plus char after it
00347         self.sub(r'\+\s+(?!\+)', '+')
00348         # whitespace before minus chars if no other minus char before it
00349         self.sub(r'(?<!-)\s+-', '-')
00350         # whitespace after minus chars if no other minus char after it
00351         self.sub(r'-\s+(?!-)', '-')
00352         # remove redundant semi-colons
00353         self.sub(r';+\s*([};])', r'\1')
00354         # remove any excessive whitespace left except newlines
00355         self.sub(r'[ \t\r\f\v]+', ' ')
00356         # excessive newlines
00357         self.sub(r'\n+', '\n')
00358         # first newline
00359         self.sub(r'^\n', '')
00360 
00361 
00362 class CSSPacker(Packer):
00363     def __init__(self, level='safe'):
00364         Packer.__init__(self)
00365         # protect strings
00366         # these sometimes catch to much, but in safe mode this doesn't hurt
00367         self.protect(r"""('(?:\\'|\\\n|.)*?')""")
00368         self.protect(r'''("(?:\\"|\\\n|.)*?")''')
00369         # strip whitespace
00370         self.sub(r'^[ \t\r\f\v]*(.*?)[ \t\r\f\v]*$', r'\1', re.MULTILINE)
00371         if level == 'full':
00372             # remove comments
00373             self.sub(r'/\*.*? ?[\\/*]*\*/', r'', re.DOTALL)
00374             #remove more whitespace
00375             self.sub(r'\s*([{,;:])\s+', r'\1')
00376         else:
00377             # remove comment contents
00378             self.sub(r'/\*.*?( ?[\\/*]*\*/)', r'/*\1', re.DOTALL)
00379             # remove lines with comments only (consisting of stars only)
00380             self.sub(r'^/\*+\*/$', '', re.MULTILINE)
00381         # excessive newlines
00382         self.sub(r'\n+', '\n')
00383         # first newline
00384         self.sub(r'^\n', '')
00385 
00386 
00387 
00388 # be aware that the initial indentation gets removed in the following tests,
00389 # the inner indentation is preserved though (see textwrap.dedent)
00390 js_compression_tests = (
00391     (
00392         'standardJS',
00393         """\
00394             /* a comment */
00395 
00396             function dummy() {
00397 
00398                 var localvar = 10 // one line comment
00399 
00400                 document.write(localvar);
00401                 return 'bar'
00402             }
00403         """, 
00404         """\
00405             function dummy(){var localvar=10
00406             document.write(localvar);return 'bar'}
00407         """,
00408         'safe'
00409     ),
00410     (
00411         'standardJS',
00412         """\
00413             /* a comment */
00414 
00415             function dummy() {
00416 
00417                 var localvar = 10 // one line comment
00418 
00419                 document.write(localvar);
00420                 return 'bar'
00421             }
00422         """, 
00423         """\
00424             function dummy(){var localvar=10
00425             document.write(localvar);return'bar'}""",
00426         'full'
00427     ),
00428     (
00429         'stringProtection',
00430         """
00431             var leafnode = this.shared.xmldata.selectSingleNode('//*[@selected]');
00432             var portal_url = 'http://127.0.0.1:9080/plone';
00433         """,
00434         """var leafnode=this.shared.xmldata.selectSingleNode('//*[@selected]');var portal_url='http://127.0.0.1:9080/plone';"""
00435     ),
00436     (
00437         'newlinesInStrings',
00438         r"""var message = "foo: " + foo + "\nbar: " + bar;""",
00439         r"""var message="foo: "+foo+"\nbar: "+bar;"""
00440     ),
00441     (
00442         'escapedStrings',
00443         r"""var message = "foo: \"something in quotes\"" + foo + "\nbar: " + bar;""",
00444         r"""var message="foo: \"something in quotes\""+foo+"\nbar: "+bar;"""
00445     ),
00446     (
00447         'whitspaceAroundPlus',
00448         """\
00449             var message = foo + bar;
00450             message = foo++ + bar;
00451             message = foo + ++bar;
00452         """,
00453         """\
00454             var message=foo+bar;message=foo++ +bar;message=foo+ ++bar;"""
00455     ),
00456     (
00457         'whitspaceAroundMinus',
00458         """\
00459             var message = foo - bar;
00460             message = foo-- - bar;
00461             message = foo - --bar;
00462         """,
00463         """\
00464             var message=foo-bar;message=foo-- -bar;message=foo- --bar;"""
00465     ),
00466     (
00467         'missingSemicolon',
00468         """\
00469             var x = function() {
00470  
00471             } /* missing ; here */
00472             next_instr;
00473         """,
00474         """\
00475             var x=function(){}
00476             next_instr;""",
00477         'safe'
00478     ),
00479     # be aware that the following produces invalid code. You *have* to add
00480     # a semicolon after a '}' followed by a normal instruction
00481     (
00482         'missingSemicolon',
00483         """\
00484             var x = function() {
00485  
00486             } /* missing ; here */
00487             next_instr;
00488         """,
00489         """\
00490             var x=function(){}next_instr;""",
00491         'full'
00492     ),
00493     # excessive semicolons after curly brackets get removed
00494     (
00495         'nestedCurlyBracketsWithSemicolons',
00496         """\
00497             function dummy(a, b) {
00498                 if (a > b) {
00499                     do something
00500                 } else {
00501                     do something else
00502                 };
00503             };
00504             next_instr;
00505         """,
00506         """\
00507             function dummy(a,b){if(a>b){do something} else{do something else}};next_instr;""",
00508         'safe'
00509     ),
00510     (
00511         'nestedCurlyBracketsWithSemicolons',
00512         """\
00513             function dummy(a, b) {
00514                 if (a > b) {
00515                     do something
00516                 } else {
00517                     do something else
00518                 };
00519             };
00520             next_instr;
00521         """,
00522         """\
00523             function dummy(a,b){if(a>b){do something}else{do something else}};next_instr;""",
00524         'full'
00525     ),
00526 )
00527 
00528 
00529 css_safe_compression_tests = (
00530     (
00531         'commentCompression',
00532         """
00533             /* this is a comment */
00534             #testElement {
00535                 property: value; /* another comment */
00536             }
00537             /**********/
00538             /* this is a multi
00539                line comment */
00540             #testElement {
00541                 /* yet another comment */
00542                 property: value;
00543             }
00544         """,
00545         """\
00546             /* */
00547             #testElement {
00548             property: value; /* */
00549             }
00550             /* */
00551             #testElement {
00552             /* */
00553             property: value;
00554             }
00555         """
00556     ),
00557     (
00558         'newlineCompression',
00559         """
00560         
00561         
00562         /* this is a comment */
00563         
00564         #testElement {
00565             property: value; /* another comment */
00566         }
00567         
00568         /* this is a multi
00569            line comment */
00570         #testElement {
00571         
00572             /* yet another comment */
00573             property: value;
00574             
00575         }
00576         
00577         
00578         """,
00579         """\
00580             /* */
00581             #testElement {
00582             property: value; /* */
00583             }
00584             /* */
00585             #testElement {
00586             /* */
00587             property: value;
00588             }
00589         """
00590     ),
00591     # see http://www.dithered.com/css_filters/index.html
00592     (
00593         'commentHacks1',
00594         """
00595             #testElement {
00596                 property/**/: value;
00597                 property/* */: value;
00598                 property /**/: value;
00599                 property: /**/value;
00600             }
00601         """,
00602         """\
00603             #testElement {
00604             property/**/: value;
00605             property/* */: value;
00606             property /**/: value;
00607             property: /**/value;
00608             }
00609         """
00610     ),
00611     (
00612         'commentHacks2',
00613         """
00614             selector/* */ {  }
00615         """,
00616         """\
00617             selector/* */ {  }
00618         """
00619     ),
00620     (
00621         'commentHacks3',
00622         """
00623             selector/* foobar */ {  }
00624         """,
00625         """\
00626             selector/* */ {  }
00627         """
00628     ),
00629     (
00630         'commentHacks4',
00631         """
00632             selector/**/ {  }
00633         """,
00634         """\
00635             selector/**/ {  }
00636         """
00637     ),
00638     (
00639         'commentHacks5',
00640         """
00641             /* \*/
00642             rules
00643             /* */
00644         """,
00645         """\
00646             /* \*/
00647             rules
00648             /* */
00649         """
00650     ),
00651     (
00652         'commentHacks6',
00653         """
00654             /* foobar \*/
00655             rules
00656             /* */
00657         """,
00658         """\
00659             /* \*/
00660             rules
00661             /* */
00662         """
00663     ),
00664     (
00665         'commentHacks7',
00666         """
00667             /*/*/
00668             rules
00669             /* */
00670         """,
00671         """\
00672             /*/*/
00673             rules
00674             /* */
00675         """
00676     ),
00677     (
00678         'commentHacks8',
00679         """
00680             /*/*//*/
00681             rules
00682             /* */
00683         """,
00684         """\
00685             /*/*//*/
00686             rules
00687             /* */
00688         """
00689     ),
00690     (
00691         'stringProtection',
00692         """
00693             /* test string protection */
00694             #selector,
00695             #another {
00696                 content: 'foo; bar';
00697             }
00698         """,
00699         """\
00700             /* */
00701             #selector,
00702             #another {
00703             content: 'foo; bar';
00704             }
00705         """
00706     ),
00707 )
00708 
00709 css_full_compression_tests = (
00710     (
00711         'commentCompression',
00712         """
00713             /* this is a comment */
00714             #testElement {
00715                 property: value; /* another comment */
00716             }
00717             /**********/
00718             /* this is a multi
00719                line comment */
00720             #testElement {
00721                 /* yet another comment */
00722                 property: value;
00723             }
00724         """,
00725         """\
00726             #testElement{property:value;}
00727             #testElement{property:value;}
00728         """
00729     ),
00730     (
00731         'newlineCompression',
00732         """
00733         
00734         
00735         /* this is a comment */
00736         
00737         #testElement {
00738             property: value; /* another comment */
00739         }
00740         
00741         /* this is a multi
00742            line comment */
00743         #testElement {
00744         
00745             /* yet another comment */
00746             property: value;
00747             
00748         }
00749         
00750         
00751         """,
00752         """\
00753             #testElement{property:value;}
00754             #testElement{property:value;}
00755         """
00756     ),
00757     # see http://www.dithered.com/css_filters/index.html
00758     # in full compression all hacks get removed
00759     (
00760         'commentHacks1',
00761         """
00762             #testElement {
00763                 property/**/: value;
00764                 property/* */: value;
00765                 property /**/: value;
00766                 property: /**/value;
00767             }
00768         """,
00769         """\
00770             #testElement{property:value;property:value;property:value;property:value;}
00771         """
00772     ),
00773     (
00774         'commentHacks2',
00775         """
00776             selector/* */ {  }
00777         """,
00778         """\
00779             selector{}
00780         """
00781     ),
00782     (
00783         'commentHacks3',
00784         """
00785             selector/* foobar */ {  }
00786         """,
00787         """\
00788             selector{}
00789         """
00790     ),
00791     (
00792         'commentHacks4',
00793         """
00794             selector/**/ {  }
00795         """,
00796         """\
00797             selector{}
00798         """
00799     ),
00800     (
00801         'commentHacks5',
00802         """
00803             /* \*/
00804             rules
00805             /* */
00806         """,
00807         """\
00808             rules
00809         """
00810     ),
00811     (
00812         'commentHacks6',
00813         """
00814             /* foobar \*/
00815             rules
00816             /* */
00817         """,
00818         """\
00819             rules
00820         """
00821     ),
00822     (
00823         'commentHacks7',
00824         """
00825             /*/*/
00826             rules
00827             /* */
00828         """,
00829         """\
00830             rules
00831         """
00832     ),
00833     (
00834         'commentHacks8',
00835         """
00836             /*/*//*/
00837             rules
00838             /* */
00839         """,
00840         """\
00841             rules
00842         """
00843     ),
00844     (
00845         'stringProtection',
00846         """
00847             /* test string protection and full compression */
00848             #selector,
00849             #another {
00850                 content: 'foo; bar';
00851             }
00852         """,
00853         """\
00854             #selector,#another{content:'foo; bar';}
00855         """
00856     ),
00857 )
00858 
00859 class PackerTestCase(unittest.TestCase):
00860     def __init__(self, name, input, output, packer):
00861         unittest.TestCase.__init__(self)
00862         self.name = name
00863         self.input = input
00864         self.output = output
00865         self.packer = packer
00866 
00867     def __str__(self):
00868         return self.name
00869 
00870     def runTest(self):
00871         self.assertEqual(self.packer.pack(self.input), self.output)
00872 
00873 
00874 def test_suite():
00875     suite = unittest.TestSuite()
00876 
00877     jspacker = {
00878         'safe': JavascriptPacker('safe'),
00879         'full': JavascriptPacker('full'),
00880     }
00881     csspacker = {
00882         'safe': CSSPacker('safe'),
00883         'full': CSSPacker('full'),
00884     }
00885 
00886     for info in js_compression_tests:
00887         name = info[0]
00888         input = textwrap.dedent(info[1])
00889         output = textwrap.dedent(info[2])
00890         if (len(info) == 4):
00891             compression = info[3].split(",")
00892         else:
00893             compression = ("safe", "full")
00894 
00895         for packer in compression:
00896             suite.addTest(PackerTestCase("%s (%s)" % (name, packer),
00897                                          input, output,
00898                                          jspacker[packer]))
00899 
00900     packer = "safe"
00901     for name, input, output in css_safe_compression_tests:
00902         input = textwrap.dedent(input)
00903         output = textwrap.dedent(output)
00904 
00905         suite.addTest(PackerTestCase("%s (%s)" % (name, packer),
00906                                      input, output,
00907                                      csspacker[packer]))
00908 
00909     packer = "full"
00910     for name, input, output in css_full_compression_tests:
00911         input = textwrap.dedent(input)
00912         output = textwrap.dedent(output)
00913 
00914         suite.addTest(PackerTestCase("%s (%s)" % (name, packer),
00915                                      input, output,
00916                                      csspacker[packer]))
00917 
00918     return suite
00919 
00920 if __name__ == '__main__':
00921     unittest.main(defaultTest='test_suite')