Package tdi :: Package tools :: Module javascript
[frames] | no frames]

Source Code for Module tdi.tools.javascript

  1  # -*- coding: ascii -*- 
  2  # 
  3  # Copyright 2006 - 2013 
  4  # Andr\xe9 Malo or his licensors, as applicable 
  5  # 
  6  # Licensed under the Apache License, Version 2.0 (the "License"); 
  7  # you may not use this file except in compliance with the License. 
  8  # You may obtain a copy of the License at 
  9  # 
 10  #     http://www.apache.org/licenses/LICENSE-2.0 
 11  # 
 12  # Unless required by applicable law or agreed to in writing, software 
 13  # distributed under the License is distributed on an "AS IS" BASIS, 
 14  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 15  # See the License for the specific language governing permissions and 
 16  # limitations under the License. 
 17  """ 
 18  ================== 
 19   Javascript Tools 
 20  ================== 
 21   
 22  Javascript Tools. 
 23  """ 
 24  __author__ = u"Andr\xe9 Malo" 
 25  __docformat__ = "restructuredtext en" 
 26   
 27  import re as _re 
 28   
 29  from tdi import filters as _filters 
 30  from tdi import _htmldecode 
 31  from tdi.tools._util import norm_enc as _norm_enc 
 32   
 33   
34 -def _make_big_sub_b():
35 """ Make bigsub """ 36 sub = _re.compile(r'(?<!\\)((?:\\\\)*)\\U([0-9a-fA-F]{8})').sub 37 int_ = int 38 return lambda v: sub(lambda m: "%s\\u%04x\\u%04x" % ( 39 m.group(1), 40 ((((int_(m.group(2), 16) - 0x10000) >> 10) & 0x3FF) + 0xD800), 41 ((int_(m.group(2), 16) & 0x3FF) + 0xDC00), 42 ), v)
43 44 _big_sub_b = _make_big_sub_b() 45 46
47 -def _make_big_sub():
48 """ Make bigsub """ 49 sub = _re.compile(ur'(?<!\\)((?:\\\\)*)\\U([0-9a-fA-F]{8})').sub 50 int_ = int 51 return lambda v: sub(lambda m: u"%s\\u%04x\\u%04x" % ( 52 m.group(1), 53 ((((int_(m.group(2), 16) - 0x10000) >> 10) & 0x3FF) + 0xD800), 54 ((int_(m.group(2), 16) & 0x3FF) + 0xDC00), 55 ), v)
56 57 _big_sub = _make_big_sub() 58 59
60 -def _make_small_sub():
61 """ Make small sub """ 62 sub = _re.compile(ur'(?<!\\)((?:\\\\)*)\\x([0-9a-fA-F]{2})').sub 63 return lambda v: sub(lambda m: u"%s\\u00%s" % ( 64 m.group(1), m.group(2) 65 ), v)
66 67 _small_sub = _make_small_sub() 68 69
70 -def _make_escape_inlined():
71 """ Make escape_inlined """ 72 dash_sub = _re.compile(ur'-(-+)').sub 73 dash_sub_b = _re.compile(r'-(-+)').sub 74 len_, str_, unicode_, isinstance_ = len, str, unicode, isinstance 75 norm_enc = _norm_enc 76 77 subber = lambda m: u'-' + u'\\-' * len_(m.group(1)) 78 subber_b = lambda m: '-' + '\\-' * len_(m.group(1)) 79 80 def escape_inlined(toescape, encoding=None): # pylint: disable = W0621 81 """ 82 Escape value for inlining 83 84 :Parameters: 85 `toescape` : ``basestring`` 86 The value to escape 87 88 `encoding` : ``str`` 89 Encoding in case that toescape is a ``str``. If omitted or 90 ``None``, no encoding is applied and `toescape` is expected to be 91 ASCII compatible. 92 93 :Return: The escaped value, typed as input 94 :Rtype: ``basestring`` 95 """ 96 if isinstance_(toescape, unicode_): 97 return (dash_sub(subber, toescape) 98 .replace(u'</', u'<\\/') 99 .replace(u']]>', u']\\]>') 100 ) 101 elif encoding is None: 102 return (dash_sub_b(subber_b, str_(toescape)) 103 .replace('</', '<\\/') 104 .replace(']]>', ']\\]>') 105 ) 106 # don't decode ascii, but latin-1. just in case, if it's a 107 # dumb default. Doesn't hurt here, but avoids failures. 108 if norm_enc(encoding) == 'ascii': 109 encoding = 'latin-1' 110 return (dash_sub(subber, str_(toescape).decode(encoding)) 111 .replace(u'</', u'<\\/') 112 .replace(u']]>', u']\\]>') 113 ).encode(encoding)
114 115 return escape_inlined 116 117 escape_inlined = _make_escape_inlined() 118 119
120 -def _make_escape_string():
121 """ Make escape_string function """ 122 big_sub_b = _big_sub_b 123 unicode_, str_, isinstance_ = unicode, str, isinstance 124 escape_inlined_, norm_enc = escape_inlined, _norm_enc 125 126 need_solid = '\\'.encode('string_escape') == '\\' 127 need_solid_u = u'\\'.encode('unicode_escape') == '\\' 128 need_apos = "'".encode('string_escape') == "'" 129 need_apos_u = u"'".encode('unicode_escape') == "'" 130 need_quote = '"'.encode('string_escape') == '"' 131 need_quote_u = u'"'.encode('unicode_escape') == '"' 132 133 def escape_string(toescape, inlined=True, encoding=None): 134 """ 135 Escape a string for JS output (to be inserted into a JS string) 136 137 This function is one of the building blocks of the 138 `tdi.tools.javascript.replace` function. You probably shouldn't 139 use it directly in your rendering code. 140 141 :See: 142 - `tdi.tools.javascript.fill` 143 - `tdi.tools.javascript.fill_attr` 144 - `tdi.tools.javascript.replace` 145 146 :Parameters: 147 `toescape` : ``basestring`` 148 The string to escape 149 150 `inlined` : ``bool`` 151 Do additional escapings (possibly needed for inlining the script 152 within a HTML page)? 153 154 `encoding` : ``str`` 155 Encoding in case that toescape is a ``str``. If omitted or 156 ``None``, no encoding is applied and `toescape` is expected to be 157 ASCII compatible. 158 159 :Return: The escaped string (ascii) 160 :Rtype: ``str`` 161 """ 162 # pylint: disable = W0621 163 164 isuni = isinstance_(toescape, unicode_) 165 if isuni or encoding is not None: 166 if not isuni: 167 # don't decode ascii, but latin-1. just in case, if it's a 168 # dumb default. The result is similar to encoding = None. 169 if norm_enc(encoding) == 'ascii': 170 encoding = 'latin-1' 171 toescape = str_(toescape).decode(encoding) 172 if need_solid_u: 173 toescape = toescape.replace(u'\\', u'\\\\') 174 result = big_sub_b(toescape.encode('unicode_escape')) 175 if need_apos_u: 176 result = result.replace("'", "\\'") 177 if need_quote_u: 178 result = result.replace('"', '\\"') 179 else: 180 result = str_(toescape) 181 if need_solid: 182 result = result.replace('\\', '\\\\') 183 result = result.encode('string_escape') 184 if need_apos: 185 result = result.replace("'", "\\'") 186 if need_quote: 187 result = result.replace('"', '\\"') 188 189 if inlined: 190 return escape_inlined_(result) 191 return result
192 193 return escape_string 194 195 escape_string = _make_escape_string() 196 197
198 -def _make_replace():
199 """ Make replace function """ 200 # pylint: disable = R0912 201 # (too many branches) 202 203 default_sub = _re.compile(ur'__(?P<name>[^_]*(?:_[^_]+)*)__').sub 204 escape_string_, getattr_, unicode_ = escape_string, getattr, unicode 205 isinstance_, escape_inlined_, str_ = isinstance, escape_inlined, str 206 big_sub, small_sub, norm_enc = _big_sub, _small_sub, _norm_enc 207 208 def replace(script, holders, pattern=None, as_json=True, inlined=True, 209 encoding=None): 210 """ 211 Replace javascript values 212 213 See `fill` and `fill_attr` for more specific usage. 214 215 This functions provides safe (single pass) javascript value 216 replacement:: 217 218 filled = javascript.replace(script_template, dict( 219 a=10, 220 b=u'Andr\\xe9', 221 c=javascript.SimpleJSON(dict(foo='bar')), 222 )) 223 224 Where script_template is something like:: 225 226 // more script... 227 var count = __a__; 228 var name = '__b__'; 229 var param = __c__; 230 // more script... 231 232 :See: 233 - `fill` 234 - `fill_attr` 235 236 :Parameters: 237 `script` : ``basestring`` 238 Script content to modify 239 240 `holders` : ``dict`` 241 Placeholders mappings (name -> value). If a placeholder is found 242 within the script which has no mapping, it's simply left as-is. 243 If `as_json` is true, the values are checked if they have an 244 ``as_json`` method. *If* they do have one, the method is called 245 and the result (of type ``unicode``) is used as replacement. 246 Otherwise the mapped value is piped through the `escape_string` 247 function and that result is used as replacement. ``as_json`` is 248 passed a boolean ``inlined`` parameter which indicates whether the 249 method should escape for inline usage or not. 250 251 Use the `LiteralJSON` class for passing any JSON content literally 252 to the script. There is also a `SimpleJSON` class for converting 253 complex structures to JSON using the simplejson converter. You may 254 pass your own classes as well, of course, as long as they provide 255 a proper ``as_json()`` method. 256 257 `pattern` : ``unicode`` or compiled ``re`` object 258 Placeholder name pattern. If omitted or ``None``, the pattern is 259 (simplified [#]_): ``__(?P<name>.+)__``, i.e. the placeholder name 260 enclosed in double-underscores. The name group is expected. 261 262 .. [#] The actual pattern is: ``__(?P<name>[^_]*(?:_[^_]+)*)__`` 263 264 `as_json` : ``bool`` 265 Check the placeholder values for an ``as_json`` method? See the 266 description of the `holders` parameter for details. 267 268 `inlined` : ``bool`` 269 Escape simple content for being inlined (e.g. 270 no CDATA endmarkers, ``</script>``). 271 272 `encoding` : ``str`` 273 Script encoding if `script` is a ``str``. If omitted or ``None``, 274 the script is expected to be ASCII compatible. 275 276 If ``script`` is of type ``unicode``, the encoding is applied to 277 ``as_json`` method return values. This is to make sure, the JSON 278 stuff is encoded safely. If omitted or ``None``, ASCII is assumed. 279 JSON result characters not fitting into the this encoding are 280 escaped (\\uxxxx). 281 282 :Return: The modified script, typed as input 283 :Rtype: ``basestring`` 284 """ 285 # pylint: disable = W0621 286 if not holders: 287 return script 288 isuni = isinstance_(script, unicode_) 289 if isuni: 290 if encoding is None: 291 json_encoding = 'ascii' 292 else: 293 json_encoding = encoding 294 else: 295 if encoding is None: 296 encoding = 'latin-1' 297 json_encoding = 'ascii' 298 else: 299 json_encoding = encoding 300 # don't decode ascii, but latin-1. just in case, if it's a 301 # dumb default. Doesn't hurt here, but avoids failures. 302 if norm_enc(encoding) == 'ascii': 303 encoding = 'latin-1' 304 script = str_(script).decode(encoding) 305 if pattern is None: 306 pattern = default_sub 307 else: 308 pattern = _re.compile(pattern).sub 309 310 def simple_subber(match): 311 """ Substitution function without checking .as_json() """ 312 name = match.group(u'name') 313 if name and name in holders: 314 return escape_string_(holders[name], 315 encoding=encoding, inlined=inlined 316 ).decode('ascii') 317 return match.group(0)
318 319 def json_subber(match): 320 """ Substitution function with .as_json() checking """ 321 name = match.group(u'name') 322 if name and name in holders: 323 value = holders[name] 324 method = getattr_(value, 'as_json', None) 325 if method is None: 326 return escape_string_(value, 327 encoding=encoding, inlined=inlined 328 ).decode('ascii') 329 value = small_sub(big_sub(unicode_(method(inlined=False)) 330 .encode(json_encoding, 'backslashreplace') 331 .decode(json_encoding) 332 )) 333 if inlined: 334 return escape_inlined_(value) 335 return value 336 return match.group(0) 337 338 script = pattern(as_json and json_subber or simple_subber, script) 339 if not isuni: 340 return script.encode(encoding) 341 return script 342 343 return replace 344 345 replace = _make_replace() 346 347
348 -def fill(node, holders, pattern=None, as_json=True):
349 """ 350 Replace javascript values in a script node 351 352 This functions provides safe (single pass) javascript value 353 replacement (utilizing the `replace` function):: 354 355 javascript.fill(node, dict( 356 a=10, 357 b=u'Andr\\xe9', 358 c=javascript.SimpleJSON(dict(foo='bar')), 359 )) 360 361 Where `node` is something like:: 362 363 <script tdi="name"> 364 var count = __a__; 365 var name = '__b__'; 366 var param = __c__; 367 </script> 368 369 :See: 370 - `fill_attr` 371 - `replace` 372 373 :Parameters: 374 `node` : TDI node 375 The script node 376 377 `holders` : ``dict`` 378 Placeholders mappings (name -> value). If a placeholder is found 379 within the script which has no mapping, it's simply left as-is. 380 If `as_json` is true, the values are checked if they have an 381 ``as_json`` method. *If* they do have one, the method is called 382 and the result (of type ``unicode``) is used as replacement. 383 Otherwise the mapped value is piped through the `escape_string` 384 function and the result is used as replacement. ``as_json`` is 385 passed a boolean ``inlined`` parameter which indicates whether the 386 method should escape for inline usage or not. 387 388 Use the `LiteralJSON` class for passing any JSON content literally 389 to the script. There is also a `SimpleJSON` class for converting 390 complex structures to JSON using the simplejson converter. You may 391 pass your own classes as well, of course, as long as they provide 392 a proper ``as_json()`` method. 393 394 `pattern` : ``unicode`` or compiled ``re`` object 395 Placeholder name pattern. If omitted or ``None``, the pattern is 396 (simplified [#]_): ``__(?P<name>.+)__``, i.e. the placeholder name 397 enclosed in double-underscores. The name group is expected. 398 399 .. [#] The actual pattern is: ``__(?P<name>[^_]*(?:_[^_]+)*)__`` 400 401 `as_json` : ``bool`` 402 Check the placeholder values for an ``as_json`` method? See the 403 description of the `holders` parameter for details. 404 """ 405 node.raw.content = replace(node.raw.content, holders, 406 pattern=pattern, 407 as_json=as_json, 408 inlined=True, 409 encoding=node.raw.encoder.encoding, 410 )
411 412
413 -def fill_attr(node, attr, holders, pattern=None, as_json=True):
414 """ 415 Replace javascript values in a script attribute 416 417 This functions provides safe (single pass) javascript value 418 replacement (utilizing the `replace` function):: 419 420 javascript.fill_attr(node, u'onclick', dict( 421 a=10, 422 b=u'Andr\\xe9', 423 c=javascript.SimpleJSON(dict(foo='bar')), 424 )) 425 426 Where `node` is something like:: 427 428 <div onclick="return foo(__a__)">...</div> 429 430 :See: 431 - `fill` 432 - `replace` 433 434 :Parameters: 435 `node` : TDI node 436 The script node 437 438 `attr` : ``basestring`` 439 The name of the attribute 440 441 `holders` : ``dict`` 442 Placeholders mappings (name -> value). If a placeholder is found 443 within the script which has no mapping, it's simply left as-is. 444 If `as_json` is true, the values are checked if they have an 445 ``as_json`` method. *If* they do have one, the method is called 446 and the result (of type ``unicode``) is used as replacement. 447 Otherwise the mapped value is piped through the `escape_string` 448 function and that result is used as replacement. ``as_json`` is 449 passed a boolean ``inlined`` parameter which indicates whether the 450 method should escape for inline usage or not. 451 452 Use the `LiteralJSON` class for passing any JSON content literally 453 to the script. There is also a `SimpleJSON` class for converting 454 complex structures to JSON using the simplejson converter. You may 455 pass your own classes as well, of course, as long as they provide 456 a proper ``as_json()`` method. 457 458 `pattern` : ``unicode`` or compiled ``re`` object 459 Placeholder name pattern. If omitted or ``None``, the pattern is 460 (simplified [#]_): ``__(?P<name>.+)__``, i.e. the placeholder name 461 enclosed in double-underscores. The name group is expected. 462 463 .. [#] The actual pattern is: ``__(?P<name>[^_]*(?:_[^_]+)*)__`` 464 465 `as_json` : ``bool`` 466 Check the placeholder values for an ``as_json`` method? See the 467 description of the `holders` parameter for details. 468 """ 469 encoding = node.raw.encoder.encoding 470 node[attr] = replace( 471 _htmldecode.decode(node[attr], encoding=encoding), holders, 472 pattern=pattern, 473 as_json=as_json, 474 inlined=False, 475 encoding=encoding, 476 )
477 478
479 -class LiteralJSON(object):
480 """ 481 Literal JSON container for use with `replace` or `fill` 482 483 The container just passes its input back through as_json(). 484 485 :IVariables: 486 `_json` : JSON input 487 JSON input 488 489 `_inlined` : ``bool`` 490 Escape for inlining? 491 492 `_encoding` : ``str`` 493 Encoding of `_json` 494 """ 495
496 - def __init__(self, json, inlined=False, encoding=None):
497 """ 498 Initialization 499 500 :Parameters: 501 `json` : ``basestring`` 502 JSON to output 503 504 `inlined` : ``bool`` 505 Escape for inlining? See `escape_inlined` for details. 506 507 `encoding` : ``str`` 508 Encoding of `json`, in case it's a ``str``. If omitted or ``None`` 509 and `json` is ``str``, `json` is expected to be UTF-8 encoded (or 510 ASCII only, which is compatible here) 511 """ 512 self._json = json 513 self._inlined = bool(inlined) 514 self._encoding = encoding
515
516 - def __repr__(self):
517 """ Debug representation """ 518 return "%s(%r, inlined=%r, encoding=%r)" % ( 519 self.__class__.__name__, 520 self._json, self._inlined, self._encoding 521 )
522
523 - def as_json(self, inlined=None):
524 """ 525 Content as JSON 526 527 :Parameters: 528 `inlined` : ``bool`` or ``None`` 529 escape for inlining? If omitted or ``None``, the default value 530 from construction is used. 531 532 :Return: JSON string 533 :Rtype: ``unicode`` 534 """ 535 json = self._json 536 if inlined is None: 537 inlined = self._inlined 538 if inlined: 539 json = escape_inlined(json, encoding=self._encoding) 540 if not isinstance(json, unicode): 541 encoding = self._encoding 542 if encoding is None: 543 encoding = 'utf-8' 544 json = json.decode(encoding) 545 return (json 546 .replace(u'\u2028', u'\\u2028') 547 .replace(u'\u2029', u'\\u2029') 548 )
549 550 __unicode__ = as_json 551
552 - def __str__(self):
553 """ JSON as ``str`` (UTF-8 encoded) """ 554 return self.as_json().encode('utf-8')
555 556
557 -class SimpleJSON(object):
558 """ 559 JSON generator for use with `replace` or `fill` 560 561 This class uses simplejson for generating JSON output. 562 563 The encoder looks for either the ``json`` module or, if that fails, for 564 the ``simplejson`` module. If both fail, an ImportError is raised from the 565 `as_json` method. 566 567 :IVariables: 568 `_content` : any 569 Wrapped content 570 571 `_inlined` : ``bool`` 572 Escape for inlining? 573 574 `_str_encoding` : ``str`` 575 Str encoding 576 """ 577
578 - def __init__(self, content, inlined=False, str_encoding='latin-1'):
579 """ 580 Initialization 581 582 :Parameters: 583 `content` : any 584 Content to wrap for json conversion 585 586 `inlined` : ``bool`` 587 Is it going to be inlined? Certain sequences are escaped then. 588 589 `str_encoding` : ``str`` 590 Encoding to be applied on ``str`` content parts. Latin-1 is 591 a failsafe default here, because it always translates. It may be 592 wrong though. 593 """ 594 self._content = content 595 self._inlined = bool(inlined) 596 self._str_encoding = str_encoding
597
598 - def __repr__(self):
599 """ Debug representation """ 600 return "%s(%r, %r)" % ( 601 self.__class__.__name__, self._content, bool(self._inlined) 602 )
603
604 - def as_json(self, inlined=None):
605 """ 606 Content as JSON 607 608 :Parameters: 609 `inlined` : ``bool`` or ``None`` 610 escape for inlining? If omitted or ``None``, the default value 611 from construction is used. 612 613 :Return: The JSON encoded content 614 :Rtype: ``unicode`` 615 """ 616 try: 617 import json as _json # pylint: disable = F0401 618 except ImportError: 619 import simplejson as _json # pylint: disable = F0401 620 json = _json.dumps(self._content, 621 separators=(',', ':'), 622 ensure_ascii=False, 623 encoding=self._str_encoding, 624 ) 625 if isinstance(json, str): 626 json = json.decode(self._str_encoding) 627 if inlined is None: 628 inlined = self._inlined 629 if inlined: 630 json = escape_inlined(json) 631 return (json 632 .replace(u'\u2028', u'\\u2028') 633 .replace(u'\u2029', u'\\u2029') 634 )
635 636 __unicode__ = as_json 637
638 - def __str__(self):
639 """ 640 JSON as ``str`` (UTF-8 encoded) 641 642 :Return: JSON string 643 :Rtype: ``str`` 644 """ 645 return self.as_json().encode('utf-8')
646 647
648 -def cleanup(script, encoding=None):
649 """ 650 Cleanup single JS buffer 651 652 This method attempts to remove CDATA and starting/finishing comment 653 containers. 654 655 :Parameters: 656 `script` : ``basestring`` 657 Buffer to cleanup 658 659 `encoding` : ``str`` 660 Encoding in case that toescape is a ``str``. If omitted or 661 ``None``, no encoding is applied and `script` is expected to be 662 ASCII compatible. 663 664 :Return: The cleaned up buffer, typed as input 665 :Rtype: ``basestring`` 666 """ 667 # pylint: disable = R0912 668 669 isuni = isinstance(script, unicode) 670 if not isuni: 671 # don't decode ascii, but latin-1. just in case, if it's a 672 # dumb default. Doesn't hurt here, but avoids failures. 673 if encoding is None or _norm_enc(encoding) == 'ascii': 674 encoding = 'latin-1' 675 script = str(script).decode(encoding) 676 script = script.strip() 677 if script.startswith(u'<!--'): 678 script = script[4:] 679 if script.endswith(u'-->'): 680 script = script[:-3] 681 script = script.strip() 682 if script.startswith(u'//'): 683 pos = script.find(u'\n') 684 if pos >= 0: 685 script = script[pos + 1:] 686 script = script[::-1] 687 pos = script.find(u'\n') 688 if pos >= 0: 689 line = script[:pos].strip() 690 else: 691 line = script.strip() 692 pos = len(line) 693 if line.endswith(u'//'): 694 script = script[pos + 1:] 695 script = script[::-1].strip() 696 if script.startswith(u'<![CDATA['): 697 script = script[len(u'<![CDATA['):] 698 if script.endswith(u']]>'): 699 script = script[:-3] 700 script = script.strip() 701 if script.endswith(u'-->'): 702 script = script[:-3] 703 script = script.strip() 704 if isuni: 705 return script 706 return script.encode(encoding)
707 708
709 -def cdata(script, encoding=None):
710 """ 711 Add a failsafe CDATA container around a script 712 713 See <http://lists.w3.org/Archives/Public/www-html/2002Apr/0053.html> 714 for details. 715 716 :Parameters: 717 `script` : ``basestring`` 718 JS to contain 719 720 `encoding` : ``str`` 721 Encoding in case that toescape is a ``str``. If omitted or 722 ``None``, no encoding is applied and `script` is expected to be 723 ASCII compatible. 724 725 :Return: The contained JS, typed as input 726 :Rtype: ``basestring`` 727 """ 728 isuni = isinstance(script, unicode) 729 if not isuni: 730 # don't decode ascii, but latin-1. just in case, if it's a 731 # dumb default. Doesn't hurt here, but avoids failures. 732 if encoding is None or _norm_enc(encoding) == 'ascii': 733 encoding = 'latin-1' 734 script = str(script).decode(encoding) 735 script = cleanup(script) 736 if script: 737 script = u'<!--//--><![CDATA[//><!--\n%s\n//--><!]]>' % script 738 if isuni: 739 return script 740 return script.encode(encoding)
741 742
743 -def minify(script, encoding=None):
744 """ 745 Minify a script (using `rjsmin`_) 746 747 .. _rjsmin: http://opensource.perlig.de/rjsmin/ 748 749 :Parameters: 750 `script` : ``basestring`` 751 JS to minify 752 753 `encoding` : ``str`` 754 Encoding in case that toescape is a ``str``. If omitted or 755 ``None``, no encoding is applied and `script` is expected to be 756 ASCII compatible. 757 758 :Return: The minified JS, typed as input 759 :Rtype: ``basestring`` 760 """ 761 from tdi.tools import rjsmin as _rjsmin 762 763 isuni = isinstance(script, unicode) 764 if not isuni and encoding is not None: 765 # don't decode ascii, but latin-1. just in case, if it's a 766 # dumb default. Doesn't hurt here, but avoids failures. 767 if _norm_enc(encoding) == 'ascii': 768 encoding = 'latin-1' 769 return _rjsmin.jsmin(script.decode(encoding)).encode(encoding) 770 return _rjsmin.jsmin(script)
771 772
773 -class JSInlineFilter(_filters.BaseEventFilter):
774 """ 775 TDI filter for modifying inline javascript 776 777 :IVariables: 778 `_collecting` : ``bool`` 779 Currently collecting javascript text? 780 781 `_buffer` : ``list`` 782 Collection buffer 783 784 `_starttag` : ``tuple`` or ``None`` 785 Original script starttag parameters 786 787 `_modify` : callable 788 Modifier function 789 790 `_attribute` : ``str`` 791 ``tdi`` attribute name or ``None`` (if standalone) 792 793 `_strip` : ``bool`` 794 Strip empty script elements? 795 """ 796
797 - def __init__(self, builder, modifier, strip_empty=True, standalone=False):
798 """ 799 Initialization 800 801 :Parameters: 802 `builder` : `tdi.interfaces.BuildingListenerInterface` 803 Builder 804 805 `modifier` : callable 806 Modifier function. Takes a script and returns the (possibly) 807 modified result. 808 809 `strip_empty` : ``bool`` 810 Strip empty script elements? 811 812 `standalone` : ``bool`` 813 Standalone context? In this case, we won't watch out for TDI 814 attributes. 815 """ 816 super(JSInlineFilter, self).__init__(builder) 817 self._collecting = False 818 self._buffer = [] 819 self._starttag = None 820 self._modify = modifier 821 self._normalize = self.builder.decoder.normalize 822 if standalone: 823 self._attribute = None 824 else: 825 self._attribute = self._normalize( 826 self.builder.analyze.attribute 827 ) 828 self._strip = strip_empty
829
830 - def handle_starttag(self, name, attr, closed, data):
831 """ 832 Handle starttag 833 834 Script starttags are delayed until the endtag is found. The whole 835 element is then evaluated (and possibly thrown away). 836 837 :See: `tdi.interfaces.ListenerInterface` 838 """ 839 if not closed and self._normalize(name) == 'script': 840 self._collecting = True 841 self._buffer = [] 842 self._starttag = name, attr, closed, data 843 else: 844 self.builder.handle_starttag(name, attr, closed, data)
845
846 - def handle_endtag(self, name, data):
847 """ 848 Handle endtag 849 850 When currently collecting, it must be a script endtag. The script 851 element content is then modified (using the modifiy function passed 852 during initialization). The result replaces the original. If it's 853 empty and the starttag neither provides ``src`` nor ``tdi`` attributes 854 and the filter was configured to do so: the whole element is thrown 855 away. 856 857 :See: `tdi.interfaces.ListenerInterface` 858 """ 859 normalize = self._normalize 860 if self._collecting: 861 if normalize(name) != 'script': 862 raise AssertionError("Invalid event stream") 863 864 self._collecting = False 865 script, self._buffer = ''.join(self._buffer), [] 866 script = self._modify(script) 867 868 if not script and self._strip: 869 attrdict = dict((normalize(name), val) 870 for name, val in self._starttag[1] 871 ) 872 if normalize('src') not in attrdict: 873 if self._attribute is None or \ 874 self._attribute not in attrdict: 875 return 876 877 self.builder.handle_starttag(*self._starttag) 878 self._starttag = None 879 self.builder.handle_text(script) 880 881 self.builder.handle_endtag(name, data)
882
883 - def handle_text(self, data):
884 """ 885 Handle text 886 887 While collecting javascript text, the received data is buffered. 888 Otherwise the event is just passed through. 889 890 :See: `tdi.interfaces.ListenerInterface` 891 """ 892 if not self._collecting: 893 return self.builder.handle_text(data) 894 self._buffer.append(data)
895 896
897 -def MinifyFilter(builder, minifier=None, standalone=False):
898 """ 899 TDI Filter for minifying inline javascript 900 901 :Parameters: 902 `minifier` : callable 903 Minifier function. If omitted or ``None``, the builtin minifier (see 904 `minify`) is applied. 905 906 `standalone` : ``bool`` 907 Standalone context? In this case, we won't watch out for TDI 908 attributes. 909 """ 910 # pylint: disable = C0103, C0322 911 if minifier is None: 912 minifier = minify 913 work = lambda x, m=minifier, c=cleanup: m(c(x)) 914 return JSInlineFilter(builder, work, standalone=standalone)
915 916
917 -def CDATAFilter(builder, standalone=False): # pylint: disable = C0103
918 """ 919 TDI filter for adding failsafe CDATA containers around scripts 920 921 :Parameters: 922 `standalone` : ``bool`` 923 Standalone context? In this case, we won't watch out for TDI 924 attributes. 925 926 See <http://lists.w3.org/Archives/Public/www-html/2002Apr/0053.html> 927 for details. 928 """ 929 return JSInlineFilter(builder, cdata, standalone=standalone) 930