Xataface 2.0
Xataface Application Framework
Dataface/JavascriptTool.php
Go to the documentation of this file.
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 }
 All Data Structures Namespaces Files Functions Variables Enumerations