@@ -43,10 +43,14 @@ class Template {
4343 // used by html_head smarty block to add content before </head>
4444 var $ html_head_elements = array ();
4545
46+ var $ scriptLoader ;
47+ var $ html_footer_raw_script = array ();
48+
4649 function Template ($ root = ". " , $ theme = "" , $ path = "template " )
4750 {
4851 global $ conf , $ lang_info ;
4952
53+ $ this ->scriptLoader = new ScriptLoader ;
5054 $ this ->smarty = new Smarty ;
5155 $ this ->smarty ->debugging = $ conf ['debug_template ' ];
5256 $ this ->smarty ->compile_check = $ conf ['template_compile_check ' ];
@@ -82,6 +86,9 @@ function Template($root = ".", $theme= "", $path = "template")
8286 $ this ->smarty ->register_modifier ( 'explode ' , array ('Template ' , 'mod_explode ' ) );
8387 $ this ->smarty ->register_modifier ( 'get_extent ' , array (&$ this , 'get_extent ' ) );
8488 $ this ->smarty ->register_block ('html_head ' , array (&$ this , 'block_html_head ' ) );
89+ $ this ->smarty ->register_function ('combine_script ' , array (&$ this , 'func_combine_script ' ) );
90+ $ this ->smarty ->register_function ('get_combined_scripts ' , array (&$ this , 'func_get_combined_scripts ' ) );
91+ $ this ->smarty ->register_block ('footer_script ' , array (&$ this , 'block_footer_script ' ) );
8592 $ this ->smarty ->register_function ('known_script ' , array (&$ this , 'func_known_script ' ) );
8693 $ this ->smarty ->register_prefilter ( array ('Template ' , 'prefilter_white_space ' ) );
8794 if ( $ conf ['compiled_template_cache_language ' ] )
@@ -376,6 +383,26 @@ function pparse($handle)
376383
377384 function flush ()
378385 {
386+ if (!$ this ->scriptLoader ->did_head ())
387+ {
388+ $ search = "\n</head> " ;
389+ $ pos = strpos ( $ this ->output , $ search );
390+ if ($ pos !== false )
391+ {
392+ $ scripts = $ this ->scriptLoader ->get_head_scripts ();
393+ $ content = array ();
394+ foreach ($ scripts as $ id => $ script )
395+ {
396+ $ content []=
397+ '<script type="text/javascript" src=" '
398+ . Template::make_script_src ($ script )
399+ .'"></script> ' ;
400+ }
401+
402+ $ this ->output = substr_replace ( $ this ->output , "\n" .implode ( "\n" , $ content ), $ pos , 0 );
403+ } //else maybe error or warning ?
404+ }
405+
379406 if ( count ($ this ->html_head_elements ) )
380407 {
381408 $ search = "\n</head> " ;
@@ -472,6 +499,119 @@ function func_known_script($params, &$smarty )
472499 $ this ->block_html_head (null , $ content , $ smarty , $ repeat );
473500 }
474501 }
502+
503+ function func_combine_script ($ params , &$ smarty )
504+ {
505+ if (!isset ($ params ['id ' ]))
506+ {
507+ $ smarty ->trigger_error ("combine_script: missing 'id' parameter " , E_USER_ERROR );
508+ }
509+ $ load = 0 ;
510+ if (isset ($ params ['load ' ]))
511+ {
512+ switch ($ params ['load ' ])
513+ {
514+ case 'header ' : break ;
515+ case 'footer ' : $ load =1 ; break ;
516+ case 'async ' : $ load =2 ; break ;
517+ default : $ smarty ->trigger_error ("combine_script: invalid 'load' parameter " , E_USER_ERROR );
518+ }
519+ }
520+ $ this ->scriptLoader ->add ( $ params ['id ' ], $ load ,
521+ empty ($ params ['require ' ]) ? array () : explode ( ', ' , $ params ['require ' ] ),
522+ @$ params ['path ' ],
523+ isset ($ params ['version ' ]) ? $ params ['version ' ] : 0 );
524+ }
525+
526+
527+ function func_get_combined_scripts ($ params , &$ smarty )
528+ {
529+ if (!isset ($ params ['load ' ]))
530+ {
531+ $ smarty ->trigger_error ("get_combined_scripts: missing 'load' parameter " , E_USER_ERROR );
532+ }
533+ $ load = $ params ['load ' ]=='header ' ? 0 : 1 ;
534+ $ content = array ();
535+
536+ if ($ load ==0 )
537+ {
538+ if ($ this ->scriptLoader ->did_head ())
539+ fatal_error ('get_combined_scripts several times header ' );
540+
541+ $ scripts = $ this ->scriptLoader ->get_head_scripts ();
542+ foreach ($ scripts as $ id => $ script )
543+ {
544+ $ content []=
545+ '<script type="text/javascript" src=" '
546+ . Template::make_script_src ($ script )
547+ .'"></script> ' ;
548+ }
549+ }
550+ else
551+ {
552+ if (!$ this ->scriptLoader ->did_head ())
553+ fatal_error ('get_combined_scripts didn \'t call header ' );
554+ $ scripts = $ this ->scriptLoader ->get_footer_scripts ();
555+ foreach ($ scripts [0 ] as $ id => $ script )
556+ {
557+ $ content []=
558+ '<script type="text/javascript" src=" '
559+ . Template::make_script_src ($ script )
560+ .'"></script> ' ;
561+ }
562+ if (count ($ this ->html_footer_raw_script ))
563+ {
564+ $ content []= '<script type="text/javascript"> ' ;
565+ $ content = array_merge ($ content , $ this ->html_footer_raw_script );
566+ $ content []= '</script> ' ;
567+ }
568+
569+ if (count ($ scripts [1 ]))
570+ {
571+ $ content []= '<script type="text/javascript"> ' ;
572+ $ content []= '(function() {
573+ var after = document.getElementsByTagName( \'script \')[document.getElementsByTagName( \'script \').length-1];
574+ var s; ' ;
575+ foreach ($ scripts [1 ] as $ id => $ script )
576+ {
577+ $ content []=
578+ 's=document.createElement( \'script \'); s.type = \'text/javascript \'; s.async = true; s.src = \''
579+ . Template::make_script_src ($ script )
580+ .'\'; ' ;
581+ $ content []= 'after = after.parentNode.insertBefore(s, after); ' ;
582+ }
583+ $ content []= '})(); ' ;
584+ $ content []= '</script> ' ;
585+ }
586+ }
587+ return implode ("\n" , $ content );
588+ }
589+
590+
591+ private static function make_script_src ( $ script )
592+ {
593+ $ ret = '' ;
594+ if ( url_is_remote ($ script ->path ) )
595+ $ ret = $ script ->path ;
596+ else
597+ {
598+ $ ret = get_root_url ().$ script ->path ;
599+ if ($ script ->version !==false )
600+ {
601+ $ ret .= '?v ' . ($ script ->version ? $ script ->version : PHPWG_VERSION );
602+ }
603+ }
604+ return $ ret ;
605+ }
606+
607+ function block_footer_script ($ params , $ content , &$ smarty , &$ repeat )
608+ {
609+ $ content = trim ($ content );
610+ if ( !empty ($ content ) )
611+ { // second call
612+ $ this ->html_footer_raw_script [] = $ content ;
613+ }
614+ }
475615
476616 /**
477617 * This function allows to declare a Smarty prefilter from a plugin, thus allowing
@@ -644,4 +784,199 @@ function sprintf()
644784 }
645785}
646786
787+
788+ final class Script
789+ {
790+ public $ load_mode ;
791+ public $ precedents = array ();
792+ public $ path ;
793+ public $ version ;
794+ public $ extra = array ();
795+
796+ function Script ($ load_mode , $ precedents , $ path , $ version )
797+ {
798+ $ this ->load_mode = $ load_mode ;
799+ $ this ->precedents = $ precedents ;
800+ $ this ->path = $ path ;
801+ $ this ->version = $ version ;
802+ }
803+
804+ function set_path ($ path )
805+ {
806+ if (!empty ($ path ))
807+ $ this ->path = $ path ;
808+ }
809+ }
810+
811+
812+ /** Manage a list of required scripts for a page, by optimizing their loading location (head, bottom, async)
813+ and later on by combining them in a unique file respecting at the same time dependencies.*/
814+ class ScriptLoader
815+ {
816+ private $ registered_scripts ;
817+ private $ did_head ;
818+ private static $ known_paths = array (
819+ 'core.scripts ' => 'themes/default/js/scripts.js ' ,
820+ 'jquery ' => 'themes/default/js/jquery.min.js ' ,
821+ 'jquery.ui ' => 'themes/default/js/ui/packed/ui.core.packed.js '
822+ );
823+
824+ function __construct ()
825+ {
826+ $ this ->clear ();
827+ }
828+
829+ function clear ()
830+ {
831+ $ this ->registered_scripts = array ();
832+ $ this ->did_head = false ;
833+ }
834+
835+ function add ($ id , $ load_mode , $ require , $ path , $ version =0 )
836+ {
837+ if ($ this ->did_head && $ load_mode ==0 )
838+ {
839+ trigger_error ("Attempt to add a new script $ id but the head has been written " , E_USER_WARNING );
840+ }
841+ if (! isset ( $ this ->registered_scripts [$ id ] ) )
842+ {
843+ $ script = new Script ($ load_mode , $ require , $ path , $ version );
844+ self ::fill_well_known ($ id , $ script );
845+ $ this ->registered_scripts [$ id ] = $ script ;
846+ }
847+ else
848+ {
849+ $ script = & $ this ->registered_scripts [$ id ];
850+ if (count ($ require ))
851+ {
852+ $ script ->precedents = array_unique ( array_merge ($ script ->precedents , $ require ) );
853+ }
854+ $ script ->set_path ($ path );
855+ if ($ version && version_compare ($ script ->version , $ version )<0 )
856+ $ script ->version = $ version ;
857+ if ($ load_mode < $ script ->load_mode )
858+ $ script ->load_mode = $ load_mode ;
859+ }
860+ }
861+
862+ function did_head ()
863+ {
864+ return $ this ->did_head ;
865+ }
866+
867+ private static function fill_well_known ($ id , $ script )
868+ {
869+ if ( empty ($ script ->path ) && isset (self ::$ known_paths [$ id ]))
870+ {
871+ $ script ->path = self ::$ known_paths [$ id ];
872+ }
873+ if ( strncmp ($ id , 'jquery. ' , 7 )==0 )
874+ {
875+ if ( !in_array ('jquery ' , $ script ->precedents ) )
876+ $ script ->precedents [] = 'jquery ' ;
877+ if ( strncmp ($ id , 'jquery.ui. ' , 10 )==0 && !in_array ('jquery.ui ' , $ script ->precedents ) )
878+ $ script ->precedents [] = 'jquery.ui ' ;
879+ }
880+ }
881+
882+ function get_head_scripts ()
883+ {
884+ do
885+ {
886+ $ changed = false ;
887+ foreach ( $ this ->registered_scripts as $ id => $ script )
888+ {
889+ $ load = $ script ->load_mode ;
890+ if ($ load ==0 )
891+ continue ;
892+ if ($ load ==2 )
893+ $ load =1 ; // we are async -> a predecessor cannot be async because the script execution order is not guaranteed
894+ foreach ( $ script ->precedents as $ precedent )
895+ {
896+ if ( !isset ($ this ->registered_scripts [$ precedent ] ) )
897+ {
898+ trigger_error ("Script $ id requires undefined script $ precedent " , E_USER_WARNING );
899+ continue ;
900+ }
901+ if ( $ this ->registered_scripts [$ precedent ]->load_mode > $ load )
902+ {
903+ $ this ->registered_scripts [$ precedent ]->load_mode = $ load ;
904+ $ changed = true ;
905+ }
906+ }
907+ }
908+ }
909+ while ($ changed );
910+
911+ foreach ( array_keys ($ this ->registered_scripts ) as $ id )
912+ {
913+ $ this ->compute_script_topological_order ($ id );
914+ }
915+
916+ uasort ($ this ->registered_scripts , array ('ScriptLoader ' , 'cmp_by_mode_and_order ' ));
917+
918+ $ result = array ();
919+ foreach ( $ this ->registered_scripts as $ id => $ script )
920+ {
921+ if ($ script ->load_mode > 0 )
922+ break ;
923+ if ( !empty ($ script ->path ) )
924+ $ result [$ id ] = $ script ;
925+ else
926+ trigger_error ("Script $ id has an undefined path " , E_USER_WARNING );
927+ }
928+ $ this ->did_head = true ;
929+ return $ result ;
930+ }
931+
932+ function get_footer_scripts ()
933+ {
934+ if (!$ this ->did_head )
935+ {
936+ trigger_error ("Attempt to write footer scripts without header scripts " , E_USER_WARNING );
937+ }
938+ $ result = array ( array (), array () );
939+ foreach ( $ this ->registered_scripts as $ id => $ script )
940+ {
941+ if ($ script ->load_mode > 0 )
942+ {
943+ if ( !empty ( $ script ->path ) )
944+ {
945+ $ result [$ script ->load_mode -1 ][$ id ] = $ script ;
946+ }
947+ else
948+ trigger_error ("Script $ id has an undefined path " , E_USER_WARNING );
949+ }
950+ }
951+ return $ result ;
952+ }
953+
954+ private function compute_script_topological_order ($ script_id )
955+ {
956+ if (!isset ($ this ->registered_scripts [$ script_id ]))
957+ {
958+ trigger_error ("Undefined script $ script_id is required by someone " , E_USER_WARNING );
959+ return 0 ;
960+ }
961+ $ script = & $ this ->registered_scripts [$ script_id ];
962+ if (isset ($ script ->extra ['order ' ]))
963+ return $ script ->extra ['order ' ];
964+ if (count ($ script ->precedents ) == 0 )
965+ return ($ script ->extra ['order ' ] = 0 );
966+ $ max = 0 ;
967+ foreach ( $ script ->precedents as $ precedent )
968+ $ max = max ($ max , $ this ->compute_script_topological_order ($ precedent ) );
969+ $ max ++;
970+ return ($ script ->extra ['order ' ] = $ max );
971+ }
972+
973+ private static function cmp_by_mode_and_order ($ s1 , $ s2 )
974+ {
975+ $ ret = $ s1 ->load_mode - $ s2 ->load_mode ;
976+ if (!$ ret )
977+ $ ret = $ s1 ->extra ['order ' ] - $ s2 ->extra ['order ' ];
978+ return $ ret ;
979+ }
980+ }
981+
647982?>
0 commit comments