![]() |
Xataface 2.0
Xataface Application Framework
|
00001 <?php 00002 if ( !class_exists('Dataface_JavascriptTool')){ 00003 require_once 'Dataface/CSSTool.php'; 00004 class Dataface_JavascriptTool { 00005 00010 private static $instance=null; 00011 00017 private $includePath = array(); 00018 00025 private $scripts = array(); 00026 00031 private $included = array(); 00032 00037 private $css = null; 00038 00042 private $minify = true; 00043 00047 private $useCache = true; 00048 00052 private $dependencies = array(); 00053 00058 private $dependencyContents = array(); 00059 00060 00061 private $cssIncludes = array(); 00062 00063 00071 private $ignoreScripts = array(); 00072 00073 00074 public function ignore($script){ 00075 $this->ignoreScripts[$script] = 1; 00076 if ( isset($this->scripts[$script]) ) unset($this->scripts[$script]); 00077 } 00078 00079 public function isIgnored($script){ 00080 return @$this->ignoreScripts[$script]; 00081 } 00082 00083 public function ignoreCss($stylesheet){ 00084 $this->css->ignore($stylesheet); 00085 } 00086 00087 00092 public static function getInstance($type = null){ 00093 if ( !isset($type) ){ 00094 if ( isset(self::$instance) ){ 00095 $type = get_class(self::$instance); 00096 } else { 00097 $type = 'Dataface_JavascriptTool'; 00098 } 00099 00100 00101 } 00102 if ( !isset(self::$instance) or get_class(self::$instance) != $type ){ 00103 self::$instance = new $type; 00104 } 00105 return self::$instance; 00106 } 00107 00111 public function __construct(){ 00112 $this->css = new Dataface_CSSTool(); 00113 00114 } 00115 00120 public function setMinify($minify){ 00121 $this->minify = $minify; 00122 } 00123 00124 00129 public function getMinify(){ 00130 return $this->minify; 00131 } 00132 00136 public function setUseCache($cache){ 00137 $this->useCache = $cache; 00138 } 00139 00143 public function getUseCache(){ 00144 return $this->useCache; 00145 } 00146 00152 public function mergeCSSPaths(){ 00153 $css = Dataface_CSSTool::getInstance(); 00154 foreach ($this->css->getPaths() as $k=>$v){ 00155 $this->css->removePath($k); 00156 } 00157 foreach ($css->getPaths() as $k=>$v){ 00158 $this->css->addPath($k, $v); 00159 } 00160 } 00161 00168 public function getScripts(){ 00169 return $this->scripts; 00170 } 00171 00172 public function clearScripts(){ 00173 foreach ($this->scripts as $k=>$v){ 00174 unset($this->scripts[$k]); 00175 } 00176 } 00177 00178 public function copyTo(Dataface_JavascriptTool $target){ 00179 foreach ($this->includePath as $key=>$val){ 00180 $target->addPath($key, $val); 00181 } 00182 00183 foreach ($this->scripts as $key=>$val){ 00184 $target->import($key); 00185 } 00186 $target->setUseCache($this->useCache); 00187 $target->setMinify($this->minify); 00188 00189 } 00190 00191 00197 public function addPath($path, $url){ 00198 $this->includePath[$path] = $url; 00199 } 00200 00201 00206 public function removePath($path){ 00207 unset($this->includePath[$path]); 00208 } 00209 00210 00215 public function getPaths(){ 00216 return $this->includePath; 00217 } 00218 00219 public function clearPaths(){ 00220 $this->includePath = array(); 00221 } 00222 00227 public function import($path){ 00228 if ( @$this->ignoreScripts[$path] ) return; 00229 $this->scripts[$path] = 1; 00230 } 00231 00232 public function whereis($script){ 00233 $out = array(); 00234 foreach ($this->getPaths() as $path=>$url){ 00235 if ( is_readable($path.DIRECTORY_SEPARATOR.$script) ){ 00236 $out[] = $path.DIRECTORY_SEPARATOR.$script; 00237 } 00238 } 00239 00240 return $out; 00241 } 00242 00243 public function which($script){ 00244 foreach ($this->getPaths() as $path=>$url){ 00245 if ( is_readable($path.DIRECTORY_SEPARATOR.$script) ){ 00246 return $path.DIRECTORY_SEPARATOR.$script; 00247 } 00248 } 00249 return null; 00250 } 00251 00257 public function getURL(){ 00258 $this->compile(); 00259 return DATAFACE_SITE_HREF.'?-action=js&--id='.$this->generateCacheKeyForScripts(array_keys($this->scripts)); 00260 } 00261 00265 public function getContents(){ 00266 $this->compile(); 00267 return file_get_contents($this->getJavascriptCachePath(array_keys($this->scripts))); 00268 } 00269 00270 public function getHtml(){ 00271 $this->compile(); 00272 $out = array(); 00273 //print_r($this->dependencies); 00274 $clazz = get_class($this); 00275 $js = new $clazz; 00276 foreach ($this->dependencies as $script=>$path){ 00277 $js->import($script); 00278 $out[] = sprintf('<script src="%s"></script>', htmlspecialchars($js->getURL())); 00279 $js->unimport($script); 00280 } 00281 $out[] = sprintf('<script src="%s"></script>', htmlspecialchars($this->getURL())); 00282 return implode("\r\n", $out); 00283 } 00284 00285 public function unimport($script){ 00286 unset($this->scripts[$script]); 00287 } 00288 00293 private function generateCacheKeyForScripts(){ 00294 //$this->sortScripts(); 00295 $scripts = array_keys($this->scripts); 00296 $base = basename($scripts[0]); 00297 $base = substr($base, 0, 10); 00298 return $base.'-'.md5(implode(PATH_SEPARATOR, $scripts)); 00299 } 00300 00304 private function writeJavascript($contents){ 00305 $path = $this->getJavascriptCachePath(); 00306 return file_put_contents($path, $contents, LOCK_EX); 00307 } 00308 00312 private function getJavascriptCachePath(){ 00313 return DATAFACE_SITE_PATH.'/templates_c/'.$this->generateCacheKeyForScripts().'.js'; 00314 } 00315 00316 00320 private function getManifestPath(){ 00321 return DATAFACE_SITE_PATH.'/templates_c/'.$this->generateCacheKeyForScripts().'.manifest.js'; 00322 } 00323 00324 00332 private function getManifestData(){ 00333 $path = $this->getManifestPath(); 00334 if ( is_readable($path) ){ 00335 return json_decode(file_get_contents($path), true); 00336 } else { 00337 return array(); 00338 } 00339 } 00340 00344 private function prepareManifest(){ 00345 00346 return array( 00347 'included'=> $this->included, 00348 'dependencies' => $this->dependencies, 00349 'dependencyContents' => $this->dependencyContents, 00350 'cssIncludes' => $this->css->getIncluded() 00351 ); 00352 } 00353 00354 00367 private function writeManifest(){ 00368 $data = $this->prepareManifest(); 00369 $path = $this->getManifestPath(); 00370 return file_put_contents($path, json_encode($data), LOCK_EX); 00371 } 00372 00373 00380 private function isCacheDirty(){ 00381 $jspath = $this->getJavascriptCachePath(); 00382 $manifest = $this->getManifestData(); 00383 00384 if ( !file_exists($jspath) ) return true; 00385 if ( !$manifest ) return true; 00386 if ( !$manifest['dependencyContents'] ) return true; 00387 $mtime = filemtime($jspath); 00388 00389 $deps = $manifest['dependencyContents']; 00390 foreach ($deps as $script=>$file){ 00391 if ( filemtime($file) > $mtime ){ 00392 return true; 00393 } 00394 } 00395 00396 00397 return false; 00398 00399 00400 } 00401 00402 00403 00404 00405 00406 public function compile($clean=false){ 00407 if ( !$this->useCache ) $clean = true; 00408 $scripts = array_keys($this->scripts); 00409 00410 00411 if ( $clean or $this->isCacheDirty() ){ 00412 $this->included = array(); 00413 $this->dependencies = array(); 00414 $this->dependencyContents = array(); 00415 $this->cssIncludes = array(); 00416 00417 $this->mergeCSSPaths(); 00418 $contents = $this->_compile($scripts); 00419 00420 00421 00422 $css = $this->css; 00423 if ( $css->getStylesheets() ){ 00424 00425 00426 $contents = sprintf("\r\n".'(function(){ 00427 var headtg = document.getElementsByTagName("head")[0]; 00428 if ( !headtg ) return; 00429 var linktg = document.createElement("link"); 00430 linktg.type = "text/css"; 00431 linktg.rel = "stylesheet"; 00432 linktg.href="%s"; 00433 linktg.title="Styles"; 00434 headtg.appendChild(linktg); 00435 })();', $css->getURL()) 00436 00437 .$contents; 00438 00439 } 00440 00441 00442 00443 $contents = "if ( typeof(window.__xatajax_included__) != 'object' ){window.__xatajax_included__={};};" 00444 .$contents.' 00445 if ( typeof(XataJax) != "undefined" ) XataJax.ready(); 00446 '; 00447 00448 if ( $this->minify ) $contents = JSMin::minify($contents); 00449 $res = file_put_contents($this->getJavascriptCachePath(), $contents, LOCK_EX); 00450 if ( $res === false ){ 00451 throw new Exception("JavascriptTool failed cache the request's javascript file. Please check that your application has a templates_c directory and that it is writable."); 00452 00453 } 00454 //$res = file_put_contents($this->getManifestPath(), json_encode(array_merge($this->included, $css->getIncluded())), LOCK_EX); 00455 $res = $this->writeManifest(); 00456 if ( $res === false ){ 00457 throw new Exception("JavascriptTool failed cache the request's manifest file. Please check that your application has a templates_c directory and that it is writable."); 00458 00459 } 00460 } 00461 00462 00463 } 00464 00465 00466 00467 00468 private function processDependency($script){ 00469 if ( @$this->ignoreScripts[$script] ) return; 00470 $scriptPath = $this->which($script); 00471 if ( !$scriptPath ) throw new Exception(sprintf('Dependency "%s" could not be found in include path.', $script)); 00472 $this->dependencies[$script] = $scriptPath; 00473 00474 $clazz = get_class($this); 00475 $js = new $clazz; 00476 $js->clearPaths(); 00477 foreach ($this->getPaths() as $k=>$v){ 00478 $js->addPath($k,$v); 00479 } 00480 //echo "PRocessing dependency $script"; 00481 $js->import($script); 00482 $js->compile(); 00483 $data = $js->getManifestData(); 00484 00485 $contents = $data['dependencyContents']; 00486 foreach ($contents as $k=>$v){ 00487 $this->dependencyContents[$k] = $v; 00488 } 00489 foreach ( $js->getScripts() as $k=>$v){ 00490 $this->dependencies[$k] = $v; 00491 } 00492 foreach ($data['dependencies'] as $k=>$v){ 00493 $this->dependencies[$k] = $v; 00494 } 00495 00496 } 00497 00498 protected function decorateContents($contents, $script){ 00499 return $contents; 00500 } 00501 00502 00517 protected function _compile($scripts, $passthru=false, $onceOnly=true){ 00518 //$included = array(); 00519 $out=array(); 00520 if ( !is_array($scripts) ) $scripts = array($scripts); 00521 $included =& $this->dependencyContents; 00522 00523 // Go through each script 00524 foreach ($scripts as $script){ 00525 $contents = null; 00526 if ( $onceOnly and isset($included[$script]) or @$this->ignoreScripts[$script] ) continue; 00527 00528 foreach ($this->includePath as $path=>$url){ 00529 $filepath = $path.DIRECTORY_SEPARATOR.$script; 00530 //echo "\nChecking $filepath\n"; 00531 if ( is_readable($filepath) ){ 00532 $contents = file_get_contents($filepath); 00533 if ( !$passthru ){ 00534 $contents = $this->decorateContents($contents, $script); 00535 00536 if ( preg_match_all('#//load <(.*?)>#', $contents, $matches, PREG_SET_ORDER) ){ 00537 00538 foreach ($matches as $match){ 00539 00540 $this->processDependency($match[1]); 00541 } 00542 } 00543 } 00544 $included[$script] = $filepath; 00545 $this->included[$script] = $filepath; 00546 break; 00547 } 00548 } 00549 00550 if ( !isset($contents) ) { 00551 throw new Exception(sprintf("Could not find script %s", $script)); 00552 } 00553 00554 if ( !$passthru ){ 00555 try { 00556 $contents = preg_replace_callback('#//(require|include|require-css) <(.*)>#', array($this, '_importCallback'), $contents); 00557 $contents = preg_replace_callback('#@@\((.*?)\)#', array($this, '_includeStringCallback'), $contents); 00558 } catch (Exception $ex){ 00559 //die('here'); 00560 error_log($ex->getMessage()); 00561 echo $ex->getMessage(); 00562 throw new Exception( 00563 'Server-side Javascript directive failed in script "'.$script.'"', 00564 0 00565 00566 ); 00567 00568 } 00569 $contents = "\r\n//START ".$script."\r\n" 00570 .sprintf("if ( typeof(window.__xatajax_included__['%s']) == 'undefined'){window.__xatajax_included__['%s'] = true;\r\n", addslashes($script), addslashes($script)) 00571 .$contents."\r\n//END ".$script."\r\n" 00572 ."\r\n}"; 00573 } 00574 00575 $out[] = $contents; 00576 00577 00578 } 00579 return implode("\r\n", $out); 00580 } 00581 00582 public function _includeStringCallback($matches){ 00583 return json_encode($this->_compile($matches[1], true, false)); 00584 } 00585 00586 public function _importCallback($matches){ 00587 switch ($matches[1]){ 00588 case 'require': 00589 if ( isset($this->dependencyContents[$matches[2]]) ){ 00590 return ''; 00591 } 00592 return "\r\n".$this->_compile($matches[2]); 00593 break; 00594 case 'include': 00595 return "\r\n".$this->_compile($matches[2]); 00596 case 'require-css': 00597 $css = $this->css; 00598 $css->import($matches[2]); 00599 00600 return ''; 00601 default: 00602 throw new Exception("Handling import callback but no valid directive found"); 00603 } 00604 } 00605 00606 public function clearCache(){ 00607 $files = glob(DATAFACE_SITE_PATH.'/templates_c/*.js'); 00608 foreach($files as $f){ 00609 unlink($f); 00610 } 00611 $files = glob(DATAFACE_SITE_PATH.'/templates_c/*.manifest.js'); 00612 foreach($files as $f){ 00613 unlink($f); 00614 } 00615 } 00616 00617 00618 } 00619 00620 00672 class JSMin { 00673 const ORD_LF = 10; 00674 const ORD_SPACE = 32; 00675 const ACTION_KEEP_A = 1; 00676 const ACTION_DELETE_A = 2; 00677 const ACTION_DELETE_A_B = 3; 00678 00679 protected $a = "\n"; 00680 protected $b = ''; 00681 protected $input = ''; 00682 protected $inputIndex = 0; 00683 protected $inputLength = 0; 00684 protected $lookAhead = null; 00685 protected $output = ''; 00686 00693 public static function minify($js) 00694 { 00695 $jsmin = new JSMin($js); 00696 return $jsmin->min(); 00697 } 00698 00702 public function __construct($input) 00703 { 00704 $this->input = str_replace("\r\n", "\n", $input); 00705 $this->inputLength = strlen($this->input); 00706 } 00707 00711 public function min() 00712 { 00713 if ($this->output !== '') { // min already run 00714 return $this->output; 00715 } 00716 $this->action(self::ACTION_DELETE_A_B); 00717 00718 while ($this->a !== null) { 00719 // determine next command 00720 $command = self::ACTION_KEEP_A; // default 00721 if ($this->a === ' ') { 00722 if (! $this->isAlphaNum($this->b)) { 00723 $command = self::ACTION_DELETE_A; 00724 } 00725 } elseif ($this->a === "\n") { 00726 if ($this->b === ' ') { 00727 $command = self::ACTION_DELETE_A_B; 00728 } elseif (false === strpos('{[(+-', $this->b) 00729 && ! $this->isAlphaNum($this->b)) { 00730 $command = self::ACTION_DELETE_A; 00731 } 00732 } elseif (! $this->isAlphaNum($this->a)) { 00733 if ($this->b === ' ' 00734 || ($this->b === "\n" 00735 && (false === strpos('}])+-"\'', $this->a)))) { 00736 $command = self::ACTION_DELETE_A_B; 00737 } 00738 } 00739 $this->action($command); 00740 } 00741 $this->output = trim($this->output); 00742 return $this->output; 00743 } 00744 00750 protected function action($command) 00751 { 00752 switch ($command) { 00753 case self::ACTION_KEEP_A: 00754 $this->output .= $this->a; 00755 // fallthrough 00756 case self::ACTION_DELETE_A: 00757 $this->a = $this->b; 00758 if ($this->a === "'" || $this->a === '"') { // string literal 00759 $str = $this->a; // in case needed for exception 00760 while (true) { 00761 $this->output .= $this->a; 00762 $this->a = $this->get(); 00763 if ($this->a === $this->b) { // end quote 00764 break; 00765 } 00766 if (ord($this->a) <= self::ORD_LF) { 00767 throw new JSMin_UnterminatedStringException( 00768 'Unterminated String: ' . var_export($str, true)); 00769 } 00770 $str .= $this->a; 00771 if ($this->a === '\\') { 00772 $this->output .= $this->a; 00773 $this->a = $this->get(); 00774 $str .= $this->a; 00775 } 00776 } 00777 } 00778 // fallthrough 00779 case self::ACTION_DELETE_A_B: 00780 $this->b = $this->next(); 00781 if ($this->b === '/' && $this->isRegexpLiteral()) { // RegExp literal 00782 $this->output .= $this->a . $this->b; 00783 $pattern = '/'; // in case needed for exception 00784 while (true) { 00785 $this->a = $this->get(); 00786 $pattern .= $this->a; 00787 if ($this->a === '/') { // end pattern 00788 break; // while (true) 00789 } elseif ($this->a === '\\') { 00790 $this->output .= $this->a; 00791 $this->a = $this->get(); 00792 $pattern .= $this->a; 00793 } elseif (ord($this->a) <= self::ORD_LF) { 00794 throw new JSMin_UnterminatedRegExpException( 00795 'Unterminated RegExp: '. var_export($pattern, true)); 00796 } 00797 $this->output .= $this->a; 00798 } 00799 $this->b = $this->next(); 00800 } 00801 // end case ACTION_DELETE_A_B 00802 } 00803 } 00804 00805 protected function isRegexpLiteral() 00806 { 00807 if (false !== strpos("\n{;(,=:[!&|?", $this->a)) { // we aren't dividing 00808 return true; 00809 } 00810 if (' ' === $this->a) { 00811 $length = strlen($this->output); 00812 if ($length < 2) { // weird edge case 00813 return true; 00814 } 00815 // you can't divide a keyword 00816 if (preg_match('/(?:case|else|in|return|typeof)$/', $this->output, $m)) { 00817 if ($this->output === $m[0]) { // odd but could happen 00818 return true; 00819 } 00820 // make sure it's a keyword, not end of an identifier 00821 $charBeforeKeyword = substr($this->output, $length - strlen($m[0]) - 1, 1); 00822 if (! $this->isAlphaNum($charBeforeKeyword)) { 00823 return true; 00824 } 00825 } 00826 } 00827 return false; 00828 } 00829 00833 protected function get() 00834 { 00835 $c = $this->lookAhead; 00836 $this->lookAhead = null; 00837 if ($c === null) { 00838 if ($this->inputIndex < $this->inputLength) { 00839 $c = $this->input[$this->inputIndex]; 00840 $this->inputIndex += 1; 00841 } else { 00842 return null; 00843 } 00844 } 00845 if ($c === "\r" || $c === "\n") { 00846 return "\n"; 00847 } 00848 if (ord($c) < self::ORD_SPACE) { // control char 00849 return ' '; 00850 } 00851 return $c; 00852 } 00853 00857 protected function peek() 00858 { 00859 $this->lookAhead = $this->get(); 00860 return $this->lookAhead; 00861 } 00862 00866 protected function isAlphaNum($c) 00867 { 00868 return (preg_match('/^[0-9a-zA-Z_\\$\\\\]$/', $c) || ord($c) > 126); 00869 } 00870 00871 protected function singleLineComment() 00872 { 00873 $comment = ''; 00874 while (true) { 00875 $get = $this->get(); 00876 $comment .= $get; 00877 if (ord($get) <= self::ORD_LF) { // EOL reached 00878 // if IE conditional comment 00879 if (preg_match('/^\\/@(?:cc_on|if|elif|else|end)\\b/', $comment)) { 00880 return "/{$comment}"; 00881 } 00882 return $get; 00883 } 00884 } 00885 } 00886 00887 protected function multipleLineComment() 00888 { 00889 $this->get(); 00890 $comment = ''; 00891 while (true) { 00892 $get = $this->get(); 00893 if ($get === '*') { 00894 if ($this->peek() === '/') { // end of comment reached 00895 $this->get(); 00896 // if comment preserved by YUI Compressor 00897 if (0 === strpos($comment, '!')) { 00898 return "\n/*" . substr($comment, 1) . "*/\n"; 00899 } 00900 // if IE conditional comment 00901 if (preg_match('/^@(?:cc_on|if|elif|else|end)\\b/', $comment)) { 00902 return "/*{$comment}*/"; 00903 } 00904 return ' '; 00905 } 00906 } elseif ($get === null) { 00907 throw new JSMin_UnterminatedCommentException('Unterminated Comment: ' . var_export('/*' . $comment, true)); 00908 } 00909 $comment .= $get; 00910 } 00911 } 00912 00917 protected function next() 00918 { 00919 $get = $this->get(); 00920 if ($get !== '/') { 00921 return $get; 00922 } 00923 switch ($this->peek()) { 00924 case '/': return $this->singleLineComment(); 00925 case '*': return $this->multipleLineComment(); 00926 default: return $get; 00927 } 00928 } 00929 } 00930 00931 class JSMin_UnterminatedStringException extends Exception {} 00932 class JSMin_UnterminatedCommentException extends Exception {} 00933 class JSMin_UnterminatedRegExpException extends Exception {} 00934 00935 }