source: spip-zone/_core_/plugins/compresseur/inc/compresseur.php @ 43372

Last change on this file since 43372 was 43372, checked in by cedric@…, 10 years ago

refactoring du compresseur :
concatener toutes les css en une seule, sans en modifier l'ordre, mais en prefixant leur contenu du @media correspondant a son media
De ce fait, reecriture des fonctions compacte_head_css et compacte_head_js en une seule fonction compacte_head_files commune
L'insertion dans le head du fichier compresse est deleguee aux fonctions
compacte_ecrire_balise_css et compacte_ecrire_balise_css qui peuvent decider de la forme et du point d'insertion les plus pertinents (balise simple, dom insertion par js etc ...)

File size: 18.0 KB
Line 
1<?php
2
3/***************************************************************************\
4 *  SPIP, Systeme de publication pour l'internet                           *
5 *                                                                         *
6 *  Copyright (c) 2001-2011                                                *
7 *  Arnaud Martin, Antoine Pitrou, Philippe Riviere, Emmanuel Saint-James  *
8 *                                                                         *
9 *  Ce programme est un logiciel libre distribue sous licence GNU/GPL.     *
10 *  Pour plus de details voir le fichier COPYING.txt ou l'aide en ligne.   *
11\***************************************************************************/
12
13if (!defined("_ECRIRE_INC_VERSION")) return;
14
15/**
16 * Ecrire la balise javascript pour inserer le fichier compresse
17 * C'est cette fonction qui decide ou il est le plus pertinent
18 * d'inserer le fichier, et dans quelle forme d'ecriture
19 *
20 * @param string $flux
21 *   contenu du head nettoye des fichiers qui ont ete compresse
22 * @param int $pos
23 *   position initiale du premier fichier inclu dans le fichier compresse
24 * @param string $src
25 *   nom du fichier compresse
26 * @param string $comments
27 *   commentaires a inserer devant
28 * @return string
29 */
30function compacte_ecrire_balise_js_dist(&$flux, $pos, $src, $comments = ""){
31        $comments .= "<script type='text/javascript' src='$src'></script>";
32  $flux = substr_replace($flux,$comments,$pos,0);
33  return $flux;
34}
35
36/**
37 * Ecrire la balise css pour inserer le fichier compresse
38 * C'est cette fonction qui decide ou il est le plus pertinent
39 * d'inserer le fichier, et dans quelle forme d'ecriture
40 *
41 * @param string $flux
42 *   contenu du head nettoye des fichiers qui ont ete compresse
43 * @param int $pos
44 *   position initiale du premier fichier inclu dans le fichier compresse
45 * @param string $src
46 *   nom du fichier compresse
47 * @param string $comments
48 *   commentaires a inserer devant
49 * @return string
50 */
51function compacte_ecrire_balise_css_dist(&$flux, $pos, $src, $comments = "", $media=""){
52        $comments .= "<link rel='stylesheet'".($media?" media='$media'":"")." href='$src' type='text/css' />";
53  $flux = substr_replace($flux,$comments,$pos,0);
54        return $flux;
55}
56
57/**
58 * Minifier un contenu CSS
59 * Si $options est vide
60 *      on utilise la methode regexp simple
61 * Si $options est une chaine non vide
62 *  elle definit un media a appliquer a la css
63 *      si la css ne contient aucun @media ni @import, on encapsule tout dans "@media $option {...}" et on utilise regexp
64 *  sinon on utilise csstidy pour ne pas faire d'erreur, mais c'est 12 fois plus lent
65 * Si $options sous forme de array()
66 *      on pass par csstidy pour parser le code
67 *  et produire un contenu plus compact et prefixe eventuellement par un @media
68 * options disponibles :
69 *  string media : media qui seront utilises pour encapsuler par @media
70 *        les selecteurs sans media
71 *  string template : format de sortie parmi 'low','default','high','highest'
72 * @param string $contenu  contenu css
73 * @param mixed $options options de minification
74 * @return string
75 */
76function compacte_css ($contenu, $options='') {
77        if (is_string($options) AND $options){
78                if ($options=="all") // facile : media all => ne rien preciser
79                        $options = "";
80                elseif (
81                                        strpos($contenu,"@media")==false
82                        AND strpos($contenu,"@import")==false
83                        ){
84                        $contenu = "@media $options {\n$contenu\n}\n";
85                        $options="";
86                }
87                else
88                        $options = array('media'=>$options);
89        }
90        if (!is_array($options)){
91
92                // nettoyer la css de tout ce qui sert pas
93                // pas de commentaires
94                $contenu = preg_replace(",/\*.*\*/,Ums","",$contenu);
95                $contenu = preg_replace(",\s//[^\n]*\n,Ums","",$contenu);
96                // espaces autour des retour lignes
97                $contenu = str_replace("\r\n","\n",$contenu);
98                $contenu = preg_replace(",\s+\n,ms","\n",$contenu);
99                $contenu = preg_replace(",\n\s+,ms","\n",$contenu);
100                // pas d'espaces consecutifs
101                $contenu = preg_replace(",\s(?=\s),Ums","",$contenu);
102                // pas d'espaces avant et apres { ; ,
103                $contenu = preg_replace("/\s?({|;|,)\s?/ms","$1",$contenu);
104                // supprimer les espaces devant : sauf si suivi d'une lettre (:after, :first...)
105                $contenu = preg_replace("/\s:([^a-z])/ims",":$1",$contenu);
106                // supprimer les espaces apres :
107                $contenu = preg_replace("/:\s/ms",":",$contenu);
108                // pas d'espaces devant }
109                $contenu = preg_replace("/\s}/ms","}",$contenu);
110
111                // ni de point virgule sur la derniere declaration
112                $contenu = preg_replace("/;}/ms","}",$contenu);
113                // pas d'espace avant !important
114                $contenu = preg_replace("/\s!\s?important/ms","!important",$contenu);
115                // passser les codes couleurs en 3 car si possible
116                // uniquement si non precedees d'un [="'] ce qui indique qu'on est dans un filter(xx=#?...)
117                $contenu = preg_replace(";([:\s,(])#([0-9a-f])(\\2)([0-9a-f])(\\4)([0-9a-f])(\\6)(?=[^\w\-]);i","$1#$2$4$6",$contenu);
118                // remplacer font-weight:bold par font-weight:700
119                $contenu = preg_replace("/font-weight:bold/ims","font-weight:700",$contenu);
120                // remplacer font-weight:normal par font-weight:400
121                $contenu = preg_replace("/font-weight:normal/ims","font-weight:400",$contenu);
122
123                // enlever le 0 des unites decimales
124                $contenu = preg_replace("/0[.]([0-9]+em)/ims",".$1",$contenu);
125                // supprimer les declarations vides
126                $contenu = preg_replace(",\s([^{}]*){},Ums"," ",$contenu);
127                // zero est zero, quelle que soit l'unite
128                $contenu = preg_replace("/([^0-9.]0)(em|px|pt|%)/ms","$1",$contenu);
129
130                // renommer les couleurs par leurs versions courtes quand c'est possible
131                $colors = array(
132                        'source'=>array('black','fuchsia','white','yellow','#800000','#ffa500','#808000','#800080','#008000','#000080','#008080','#c0c0c0','#808080','#f00'),
133                        'replace'=>array('#000' ,'#F0F'   ,'#FFF' ,'#FF0'  ,'maroon' ,'orange' ,'olive'  ,'purple' ,'green'  ,'navy'   ,'teal'   ,'silver' ,'gray'   ,'red')
134                );
135                foreach($colors['source'] as $k=>$v){
136                        $colors['source'][$k]=";([:\s,(])".$v."(?=[^\w\-]);ms";
137                        $colors['replace'][$k] = "$1".$colors['replace'][$k];
138                }
139                $contenu = preg_replace($colors['source'],$colors['replace'],$contenu);
140
141                // raccourcir les padding qui le peuvent (sur 3 ou 2 valeurs)
142                $contenu = preg_replace(",padding:([^\s;}]+)\s([^\s;}]+)\s([^\s;}]+)\s(\\2),ims","padding:$1 $2 $3",$contenu);
143                $contenu = preg_replace(",padding:([^\s;}]+)\s([^\s;}]+)\s(\\1)([;}!]),ims","padding:$1 $2$4",$contenu);
144
145                // raccourcir les margin qui le peuvent (sur 3 ou 2 valeurs)
146                $contenu = preg_replace(",margin:([^\s;}]+)\s([^\s;}]+)\s([^\s;}]+)\s(\\2),ims","margin:$1 $2 $3",$contenu);
147                $contenu = preg_replace(",margin:([^\s;}]+)\s([^\s;}]+)\s(\\1)([;}!]),ims","margin:$1 $2$4",$contenu);
148
149                $contenu = trim($contenu);
150
151                return $contenu;
152        }
153        else {
154                // compression avancee en utilisant csstidy
155                // beaucoup plus lent, mais necessaire pour placer des @media la ou il faut
156                // si il y a deja des @media ou des @import
157
158                // modele de sortie plus ou moins compact
159                $template = 'high';
160                if (isset($options['template']) AND in_array($options['template'],array('low','default','high','highest')))
161                        $template = $options['template'];
162                // @media eventuel pour prefixe toutes les css
163                // et regrouper plusieurs css entre elles
164                $media = "";
165                if (isset($options['media']))
166                        $media = "@media ".$options['media']." ";
167
168                include_spip("lib/csstidy/class.csstidy");
169                $css = new csstidy();
170
171                // essayer d'optimiser les font, margin, padding avec des ecritures raccourcies
172                $css->set_cfg('optimise_shorthands',2);
173                $css->set_cfg('template',$template);
174                $css->parse($contenu);
175                return $css->print->plain($media);
176        }
177}
178
179/**
180 * Extraire les balises CSS a compacter et retourner un tableau
181 * balise => src
182 *
183 * @param  $flux
184 * @param  $url_base
185 * @return array
186 */
187function compacte_js($flux) {
188        if (!strlen($flux))
189                return $flux;
190
191        include_spip('lib/JavascriptPacker/class.JavaScriptPacker');
192        $packer = new JavaScriptPacker($flux, 0, true, false);
193
194        // en cas d'echec (?) renvoyer l'original
195        if (!strlen($t = $packer->pack())) {
196                spip_log('erreur de compacte_js');
197                return $flux;
198        }
199        return $t;
200}
201
202/**
203 * Compacter du javascript plus intensivement
204 * grace au google closure compiler
205 *
206 * @param string $content
207 * @param bool $file
208 * @return string
209 */
210function compacte_js_more($content,$file=false) {
211        # Closure Compiler n'accepte pas des POST plus gros que 200 000 octets
212        # au-dela il faut stocker dans un fichier, et envoyer l'url du fichier
213        # dans code_url ; en localhost ca ne marche evidemment pas
214        if ($file) {
215                $nom = $content;
216                lire_fichier($nom, $content);
217                $dest = dirname($nom).'/'.md5($content).'.js';
218                if (file_exists($dest))
219                        if (filesize($dest))
220                                return $dest;
221                        else
222                                return $nom;
223        }
224
225        if (!$file AND strlen($content)>200000)
226                return $content;
227
228        include_spip('inc/distant');
229
230        $datas=array(
231                'output_format' => 'text',
232                'output_info' => 'compiled_code',
233                'compilation_level' => 'SIMPLE_OPTIMIZATIONS', // 'SIMPLE_OPTIMIZATIONS', 'WHITESPACE_ONLY', 'ADVANCED_OPTIMIZATIONS'
234        );
235        if (!$file OR strlen($content) < 200000)
236                $datas['js_code'] = $content;
237        else
238                $datas['url_code'] = url_absolue($nom);
239
240        $cc = recuperer_page('http://closure-compiler.appspot.com/compile',
241                $trans=false, $get_headers=false,
242                $taille_max = null,
243                $datas,
244                $boundary = -1);
245
246        if ($cc AND !preg_match(',^\s*Error,', $cc)) {
247                spip_log('Closure Compiler: success');
248                $cc = "/* $nom + Closure Compiler */\n".$cc;
249                if ($file){
250                        ecrire_fichier ($dest, $cc, true);
251                        ecrire_fichier ("$dest.gz", $cc, true);
252                        $content = $dest;
253                }
254                else
255                        $content = &$cc;
256        } else {
257                if ($file)
258                        ecrire_fichier ($dest, '', true);
259        }
260        return $content;
261}
262
263
264/**
265 * Extraire les balises CSS a compacter et retourner un tableau
266 * balise => src
267 *
268 * @param  $flux
269 * @param  $url_base
270 * @return array
271 */
272function extraire_balises_css_dist($flux, $url_base){
273        $balises = extraire_balises($flux,'link');
274        $files = array();
275        foreach ($balises as $s){
276                if (extraire_attribut($s, 'rel') === 'stylesheet'
277                        AND (!($type = extraire_attribut($s, 'type'))
278                                OR $type == 'text/css')
279                        AND is_null(extraire_attribut($s, 'name')) # css nommee : pas touche
280                        AND is_null(extraire_attribut($s, 'id'))   # idem
281                        AND !strlen(strip_tags($s))
282                        AND $src = preg_replace(",^$url_base,",_DIR_RACINE,extraire_attribut($s, 'href')))
283                        $files[$s] = $src;
284        }
285        return $files;
286}
287
288/**
289 * Extraire les balises JS a compacter et retoruner un tableau
290 * balise => src
291 * @param  $flux
292 * @param  $url_base
293 * @return array
294 */
295function extraire_balises_js_dist($flux, $url_base){
296        $balises = extraire_balises($flux,'script');
297        $files = array();
298        foreach ($balises as $s){
299                if (extraire_attribut($s, 'type') === 'text/javascript'
300                        AND $src = extraire_attribut($s, 'src')
301                        AND !strlen(strip_tags($s)))
302                        $files[$s] = $src;
303        }
304        return $files;
305}
306
307/**
308 * Compacter (concatener+minifier) les fichiers format css ou js
309 * du head. Reperer fichiers statiques vs url squelettes
310 * Compacte le tout dans un fichier statique pose dans local/
311 *
312 * @param string $flux
313 *  contenu du <head> de la page html
314 * @param string $format
315 *  css ou js
316 * @return string
317 */
318function compacte_head_files($flux,$format) {
319        $url_base = url_de_base();
320        $url_page = substr(generer_url_public('A'), 0, -1);
321        $dir = preg_quote($url_page,',').'|'.preg_quote(preg_replace(",^$url_base,",_DIR_RACINE,$url_page),',');
322
323        if (!$extraire_balises = charger_fonction("extraire_balises_$format",'',true))
324                return $flux;
325
326        $files = array();
327        $flux_nocomment = preg_replace(",<!--.*-->,Uims","",$flux);
328        foreach ($extraire_balises($flux_nocomment, $url_base) as $s=>$src) {
329                if (
330                        preg_match(',^('.$dir.')(.*)$,', $src, $r)
331                        OR (
332                                // ou si c'est un fichier
333                                $src = preg_replace(',^'.preg_quote(url_de_base(),',').',', '', $src)
334                                // enlever un timestamp eventuel derriere un nom de fichier statique
335                                AND $src2 = preg_replace(",[.]{$format}[?].+$,",".$format",$src)
336                                // verifier qu'il n'y a pas de ../ ni / au debut (securite)
337                                AND !preg_match(',(^/|\.\.),', substr($src,strlen(_DIR_RACINE)))
338                                // et si il est lisible
339                                AND @is_readable($src2)
340                        )
341                ) {
342                        if ($r)
343                                $files[$s] = explode('&', str_replace('&amp;', '&', $r[2]), 2);
344                        else
345                                $files[$s] = $src;
346                }
347        }
348
349        if (list($src,$comms) = filtre_cache_static($files,$format)){
350                $compacte_ecrire_balise = charger_fonction("compacte_ecrire_balise_$format",'');
351                $files = array_keys($files);
352                // retrouver la position du premier fichier compacte
353                $pos = strpos($flux,reset($files));
354                // supprimer tous les fichiers compactes du flux
355                $flux = str_replace($files,"",$flux);
356                // inserer la balise (deleguer a la fonction, en lui donnant le necessaire)
357                $flux = $compacte_ecrire_balise($flux, $pos, $src, $comms);
358        }
359
360        return $flux;
361}
362
363
364// http://doc.spip.org/@filtre_cache_static
365/**
366 * Retrouve ou genere le fichier statique correspondant a une liste de fichiers
367 * fournis en arguments
368 * @param  $files
369 * @param string $format
370 * @return array
371 */
372function filtre_cache_static($files,$format='js'){
373        $nom = "";
374        if (!is_array($files) && $files) $files = array($files);
375        if (count($files)){
376                $minifier = 'compacte_'.$format;
377         
378                // on trie la liste de files pour calculer le nom
379                // necessaire pour retomber sur le meme fichier
380                // si on renome une url a la volee pour enlever le var_mode=recalcul
381                // mais attention, il faut garder l'ordre initial pour la minification elle meme !
382                $s2 = $files;
383                ksort($s2);
384                $dir = sous_repertoire(_DIR_VAR,'cache-'.$format);
385                $nom = $dir . md5(serialize($s2)) . ".$format";
386                if (
387                        $GLOBALS['var_mode']=='recalcul'
388                        OR !file_exists($nom)
389                ) {
390                        $fichier = "";
391                        $comms = array();
392                        $total = 0;
393                        $s2 = false;
394                        foreach($files as $key=>$file){
395                                if (!is_array($file)) {
396                                        // c'est un fichier
397                                        $comm = $file;
398                                        // enlever le timestamp si besoin
399                                        $file = preg_replace(",[?].+$,",'',$file);
400                                        if ($format=='css'){
401                                                $fonctions = array('urls_absolues_css');
402                                                if (isset($GLOBALS['compresseur_filtres_css']) AND is_array($GLOBALS['compresseur_filtres_css']))
403                                                        $fonctions = $GLOBALS['compresseur_filtres_css'] + $fonctions;
404                                                $file = appliquer_fonctions_css_fichier($fonctions, $file);
405                                        }
406                                        lire_fichier($file, $contenu);
407                                }
408                                else {
409                                        // c'est un squelette
410                                        $comm = _SPIP_PAGE . "=$file[0]"
411                                                . (strlen($file[1])?"($file[1])":'');
412                                        parse_str($file[1],$contexte);
413                                        $contenu = recuperer_fond($file[0],$contexte);
414                                        if ($format=='css'){
415                                                $fonctions = array('urls_absolues_css');
416                                                if (isset($GLOBALS['compresseur_filtres_css']) AND is_array($GLOBALS['compresseur_filtres_css']))
417                                                        $fonctions = $GLOBALS['compresseur_filtres_css'] + $fonctions;
418                                                $contenu = appliquer_fonctions_css_contenu($fonctions, $contenu, self('&'));
419                                        }
420                                        // enlever le var_mode si present pour retrouver la css minifiee standard
421                                        if (strpos($file[1],'var_mode')!==false) {
422                                                if (!$s2) $s2 = $files;
423                                                unset($s2[$key]);
424                                                $key = preg_replace(',(&(amp;)?)?var_mode=[^&\'"]*,','',$key);
425                                                $file[1] = preg_replace(',&?var_mode=[^&\'"]*,','',$file[1]);
426                                                $s2[$key] = $file;
427                                        }
428                                }
429                                // minifier en passant le media en option si c'est une css
430                                // (ignore pour les js)
431                                $fichier .= "/* $comm */\n". $minifier($contenu, extraire_attribut($key,'media')) . "\n\n";
432                                $comms[] = $comm;
433                                $total += strlen($contenu);
434                        }
435
436                        // calcul du % de compactage
437                        $pc = intval(1000*strlen($fichier)/$total)/10;
438                        $comms = "compact [\n\t".join("\n\t", $comms)."\n] $pc%";
439                        $fichier = "/* $comms */\n\n".$fichier;
440
441                        if ($s2) {
442                                ksort($s2);
443                                $nom = $dir . md5(serialize($s2)) . ".$format";
444                        }
445
446                        // ecrire
447                        ecrire_fichier($nom,$fichier,true);
448                        // ecrire une version .gz pour content-negociation par apache, cf. [11539]
449                        ecrire_fichier("$nom.gz",$fichier,true);
450                        // closure compiler ou autre super-compresseurs
451                        // a appliquer sur le fichier final
452                        $nom = compresse_encore($nom, $format);
453                }
454
455
456        }
457
458        // Le commentaire detaille n'apparait qu'au recalcul, pour debug
459        return array($nom, (isset($comms) AND $comms) ? "<!-- $comms -->\n" : '');
460}
461
462/**
463 * Minification additionnelle :
464 * experimenter le Closure Compiler de Google
465 * @param string $nom
466 *   nom d'un fichier a minifier encore plus
467 * @param string $format
468 *   format css ou js
469 * @return string
470 */
471function compresse_encore (&$nom, $format) {
472        # Closure Compiler n'accepte pas des POST plus gros que 200 000 octets
473        # au-dela il faut stocker dans un fichier, et envoyer l'url du fichier
474        # dans code_url ; en localhost ca ne marche evidemment pas
475        if (
476        $GLOBALS['meta']['auto_compress_closure'] == 'oui'
477        AND $format=='js'
478        ) {
479                $nom = compacte_js_more($nom,true);
480        }
481        return $nom;
482}
483
484function appliquer_fonctions_css_fichier($fonctions,$css) {
485        if (!preg_match(',\.css$,i', $css, $r)) return $css;
486
487        $url_absolue_css = url_absolue($css);
488
489        // verifier qu'on a un array
490        if (is_string($fonctions))
491                $fonctions = array($fonctions);
492
493        $sign = implode(",",$fonctions);
494        $sign = substr(md5("$css-$sign"), 0,8);
495
496        $file = basename($css,'.css');
497        $file = sous_repertoire (_DIR_VAR, 'cache-css')
498                . preg_replace(",(.*?)(_rtl|_ltr)?$,","\\1-f-" . $sign . "\\2",$file)
499                . '.css';
500
501        if ((@filemtime($f) > @filemtime($css))
502        AND ($GLOBALS['var_mode'] != 'recalcul'))
503                return $f;
504
505        if ($url_absolue_css==$css){
506                if (strncmp($GLOBALS['meta']['adresse_site'],$css,$l=strlen($GLOBALS['meta']['adresse_site']))!=0
507                 OR !lire_fichier(_DIR_RACINE . substr($css,$l), $contenu)){
508                                include_spip('inc/distant');
509                                if (!$contenu = recuperer_page($css))
510                                        return $css;
511                }
512        }
513        elseif (!lire_fichier($css, $contenu))
514                return $css;
515
516        $contenu = appliquer_fonctions_css_contenu($fonctions, $contenu, $css);
517
518        // ecrire la css
519        if (!ecrire_fichier($file, $contenu))
520                return $css;
521
522        return $file;
523}
524
525function appliquer_fonctions_css_contenu($fonctions, &$contenu, $base) {
526        foreach($fonctions as $f)
527                if (function_exists($f))
528                        $contenu = $f($contenu, $base);
529        return $contenu;
530}
531
532
533function compresseur_embarquer_images_css($contenu, $source){
534        #$path = suivre_lien(url_absolue($source),'./');
535        $base = ((substr($source,-1)=='/')?$source:(dirname($source).'/'));
536
537        return preg_replace_callback(
538                ",url\s*\(\s*['\"]?([^'\"/][^:]*[.](png|gif|jpg))['\"]?\s*\),Uims",
539                create_function('$x',
540                        'return "url(\"".filtre_embarque_fichier($x[1],"'.$base.'")."\")";'
541                ), $contenu);
542}
Note: See TracBrowser for help on using the repository browser.