-
Notifications
You must be signed in to change notification settings - Fork 0
/
asset_packer.php
1170 lines (1020 loc) · 42.3 KB
/
asset_packer.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?php
/**
* AssetPacker - Support für REDAXO-Addons
*
* @author Christoph Böcker <https://github.com/christophboecker/>
* @version 1.3.1
* @copyright Christoph Böcker
* @license Die AssetPacker-Klassen: MIT-License <https://opensource.org/licenses/MIT>
* Die JS-Minifier-Klasse: BSD 3-Clause License <https://github.com/tedivm/JShrink/blob/master/LICENSE>
* @source Die Dateien liegen auf Github: <https://github.com/christophboecker/AssetPacker/>
* @see Handbuch auf Github: <https://github.com/christophboecker/AssetPacker/blob/master/readme.md>
*
* JShrink: https://github.com/tedivm/JShrink
* Adaptierter Code, Copyright usw. im hinteren Teil dieser Datei (suche: class Minifier)
* Bitte beachten.
*
* Die Klasse "AssetPacker" stellt Methoden zum erzeugen komprimierter (.min.) Assets wie CSS- und
* JS-Dateien aus mehreren Einzelkomponenten (via URL, lokale Dateien, Codeblöcke) zur Verfügung
*
* Wie man AssetPacker benutzt steht im Handbuch auf Github (Link siehe oben @see)
*
* ------------------------------------------------------------------------------------------------
*/
namespace Project\AssetPacker;
/**
* @package AssetPacker
* @method AssetPacker target( string $targetPath )
* @method AssetPacker overwrite( bool $overwrite = true )
* @method AssetPacker addFile( string $assetPath, bool $minified = false )
* @method AssetPacker addOptionalFile( string $assetPath, bool $minified = false )
* @method AssetPacker addCode( string $code )
* @method AssetPacker replace( string $marker, string $replacement='' )
* @method AssetPacker regReplace( string pattern, string $replacement='' )
* @method AssetPacker create()
* @method string getTag ( )
* @method string minify ( string $content )
* @method string _minify( string $content, string $name = '' )
* @method string|false _getComment( string &$content )
* @method string|false fileinfo( $filename )
*/
abstract class AssetPacker
{
// Um einen einleitenden Kommentar zu identifizieren
// in AssetPacker_xyz überschreiben falls andere Kommentarzeichen benutzt werden.
public $remOn = '/*';
public $remOff = '*/';
public $validExtensions = [];
// Fehlermeldungen
const ERR_NO_TARGET = 'AssetPacker: missing valid target-filename! Found "%s"';
const ERR_TARGET_EXT_INVALID = 'AssetPacker: target-type "%s" is not supported!';
const ERR_NO_FILE = 'AssetPacker: missing or invalid ressource-name "%s"! Please check the manual.';
const ERR_FILE_TYPE = 'AssetPacker:ressource "%s" not of type "%s"!';
const ERR_FILE_NOT_FOUND = 'AssetPacker: "%s" not found! Minificaton stopped.';
const ERR_MINIFY = 'AssetPacker: "%s" not minimized [%s]! Minificaton stopped.';
const ERR_REGEX = 'AssetPacker: regex-pattern error in "%s"! Minificaton stopped.';
// Source-Typen
const CODE = 1; // Direkt angegebener Code
const HTTP = 2; // URL zu einer abrufbaren Datei; wird nicht minifiziert
const FILE = 3; // Pfad zu einer lokalen, nicht minifizierten Datei
const COMPRESSED = 4; // Pfad zu einer lokalen, minifizierten Datei
// interne Variablen
protected $content = []; // Liste der Quellen
protected $overwrite = false; // Zieldatei überschreiben?
protected $timestamp = false; // false = keine Zieldatei, sonst deren Timestamp
protected $type = ''; // Bearbeiteter Dateityp (.css, .js)
protected $target = false; // Pfadname der Zieldatei
protected $current = null; // Pointer auf das letzte zugefühte Element in $content
public function __construct( array $fileinfo )
{
if( $fileinfo ) {
$this->target = implode('',$fileinfo);
$this->timestamp = @filemtime( $this->target );
$this->overwrite = false === $this->timestamp;
array_unshift( $this->validExtensions,strtolower($fileinfo['ext']) );
}
}
/**
* Getter um die Instanz entsprechend des Dateityps anzulegen.
* Öffnet die Klasse AssetPacker_«ext» wenn vorhanden
*
* @param string Pfadname der Zieldatei
* @return AssetPacker|null die Asset-Packer-Instanz oder NULL
*
* @throws \AssetPacker\TargetError
* Abbruch wenn keine oder ein fehlerhafter Zielname angegeben wurde
* Abbruch wenn der Dateityp nicht unterstützt wird
*/
public static function target( string $targetPath ) : AssetPacker
{
$fileinfo = self::fileinfo( $targetPath );
if( !$fileinfo || $fileinfo['http'] ) {
throw new TargetError(sprintf(self::ERR_NO_TARGET,$targetPath),1);
}
$class = self::class . '_' . strtolower(substr($fileinfo['ext'],1));
if( !is_subclass_of($class,self::class) ) {
throw new TargetError(sprintf(self::ERR_TARGET_EXT_INVALID,$fileinfo['ext']),1);
}
return new $class( $fileinfo );
}
/** Aktiviert den Overwrite-Modus.
* Damit kann erzwungen werden, dass eine schon existierende Target-Datei neu angelegt wird.
*
* @var bool TRUE|false legt fest, ob eine gecachte .min-Version überschreiben werden soll
* @return AssetPacker die Asset-Packer-Instanz
*/
public function overwrite( bool $overwrite = true ) : AssetPacker
{
if( $this->timestamp ) {
$this->overwrite = $overwrite;
}
return $this;
}
/** Fügt der Quellenliste eine Datei hinzu
* Sofern es sich um eine URL handelt, wird die URL ohne weitere Prüfung akzeptiert.
* Dateien mit .min. im Namen werden nicht komprimiert.
* Fügt das Element der Content-Liste hinzu; Verarbeitung erst mit create()
* setzt einen Pointer (current) auf das zuletzt hinzugefügte Element
*
* @var string Pfad der AssetDatei bzw. URL zum Abruf
* @var bool FALSE|true legt fest, dass die Datei auch ohne .min im Namen als
* minifiziert behandelt wird
* @return AssetPacker die Asset-Packer-Instanz
*
* @throws \AssetPacker\SourceError
* Abbruch wenn keine oder ein fehlerhafter Dateiname angegeben wurde
* Abbruch wenn der Dateityp nicht der Zieldatei entspricht
*/
public function addFile( string $assetPath, bool $minified = false ) : AssetPacker
{
// Pfadname muss formal richtig sein
$fileinfo = self::fileinfo( $assetPath );
if( !$fileinfo ) {
throw new SourceError(sprintf(self::ERR_NO_FILE,$assetPath),1);
}
// Suffix muss dem den aktuellen Typen der Klasse entsprechen
if( !in_array(strtolower($fileinfo['ext']),$this->validExtensions) ) {
throw new SourceError(sprintf(self::ERR_FILE_TYPE,$assetPath,implode('|',$this->validExtensions)),1);
}
// Den Eintrag in die Liste übernehmen und beenden
$this->content[] = [
'type' => ($fileinfo['http'] ? self::HTTP : ( $fileinfo['min'] || true === $minified? self::COMPRESSED : self::FILE ) ),
'source' => $fileinfo,
'name' => $assetPath,
'replace' => [],
'regreplace' => [],
'content' => '',
'optional' => false,
];
$this->current = &$this->content[array_key_last($this->content)];
return $this;
}
/** Fügt der Quellenliste eine optionale Datei hinzu
* Im Unterschied zu addFile wird ihr fehlen keinen Fehlermeldung bzw. Warnung auslösen
*
* @var string Pfad der AssetDatei bzw. URL zum Abruf
* @var bool FALSE|true legt fest, dass die Datei auch ohne .min im Namen als
* minifiziert behandelt wird
* @return AssetPacker die Asset-Packer-Instanz
*
* @throws \AssetPacker\SourceError
* Abbruch wenn keine oder ein fehlerhafter Dateiname angegeben wurde
* Abbruch wenn der Dateityp nicht der Zieldatei entspricht
*/
public function addOptionalFile( string $assetPath, bool $minified = false ) : AssetPacker
{
$this->addFile( $assetPath, $minified );
$this->current['optional'] = true;
return $this;
}
/** Fügt der Quellenliste einen Codeblock hinzu
* Codeblöcke werden immer neu comprimiert.
* Fügt das Element der Content-Liste hinzu; Verarbeitung erst mit create()
* setzt einen Pointer (current) auf das zuletzt hinzugefügte Element
*
* @var string Codeblock
* @return AssetPacker die Asset-Packer-Instanz
*/
public function addCode( string $code ) : AssetPacker
{
$this->content[] = [
'type' => self::CODE,
'source' => null,
'name' => 'code-block',
'replace' => [],
'regreplace' => [],
'content' => trim($code),
];
$this->current = &$this->content[array_key_last($this->content)];
return $this;
}
/** Fügt dem aktuell letzten Element Ersetzungen hinzu
* z.B. um in einer Datei Platzhalter gegen aktuelle Werte zu tauschen
* Die Werte werden getauscht BEVOR die Datei minifiziert wird.
* Gedacht um Parameter und Vorgabewerte einzutragen, nicht für größerer Textblöcke.
* auch nicht für self::HTTP gedacht
*
* @var string Needle
* @var string Statt Needle einzusetzender Text
* @return AssetPacker die Asset-Packer-Instanz
*/
public function replace( string $marker, string $replacement='' ) : AssetPacker
{
if( $marker && null !== $this->current && self::HTTP != $this->current['type'] ) {
$this->current['replace'][$marker] = $replacement;
}
return $this;
}
/** Fügt dem aktuell letzten Element Ersetzungen hinzu
* z.B. um in einer Datei Regex-Patterns gegen aktuelle Werte zu tauschen
* Die Werte werden getauscht BEVOR die Datei minifiziert wird.
* Gedacht um Parameter und Vorgabewerte einzutragen, nicht für größerer Textblöcke.
* auch nicht für self::HTTP gedacht
*
* @var string Regex-Pattern
* @var string An der Fundstelle einzusetzender Text
* @var string Anzahl Ersetzungen (0=alle)
* @return AssetPacker die Asset-Packer-Instanz
*/
public function regReplace( string $pattern, string $replacement='', int $limit=-1 ) : AssetPacker
{
if( $pattern && null !== $this->current && self::HTTP != $this->current['type'] ) {
$this->current['regreplace'][] = [
'pattern' => $pattern,
'replacement' => $replacement,
'limit' => $limit
];
}
return $this;
}
/** legt die Zieldatei aus den angegebenen Elementen an
* keine Elemente -> leere Zieldatei
* Der Vorgang startet nur, wenn "overwrite" angefordert wurde
* (Zieldatei nicht vorhanden oder overwrite() )
* Fehler z.B. vom jeweiligen Minifizierer werden intern abgefangen und ausgewertet
* Kritische Fehler (Konstruktionsfehler) bzw. Fehler im Backend bei einem Admin-User
* zu einem Whoops. Alles andere wird still beendet mit einem Log-Eintrag.
*
* @return AssetPacker die Asset-Packer-Instanz
*
* @throws \AssetPacker\MinifyError
* @throws \AssetPacker\SourceError
*/
public function create() : AssetPacker
{
// keine Zieldatei angegeben; Abbruch
if( !$this->overwrite ) return $this;
try {
$bundle = [];
foreach( $this->content as $k=>$item ){
$filename = $item['name'];
// Außer wenn Code: Daten abrufen (HTTP) oder einlesen (File)
if( self::CODE !== $item['type'] ){
$item['content'] = \rex_file::get( $filename );
// Content kann nicht abgerufen werden; Meldung und Abbruch
if( null === $item['content'] ) {
if( true === $item['optional'] ){
$item['content'] = '';
} else {
throw new SourceError(sprintf(self::ERR_FILE_NOT_FOUND,$filename), 2);
}
}
}
// Variablen ersetzen
if( $item['content'] ){
if( $item['replace'] ) {
$item['content'] = str_replace( array_keys($item['replace']), $item['replace'], $item['content'] );
}
foreach( $item['regreplace'] as $replace ) {
$item['content'] = preg_replace( $replace['pattern'], $replace['replacement'], $item['content'], $replace['limit'] );
if( null === $item['content'] ){
throw new SourceError(sprintf(self::ERR_REGEX,$filename), 1);
}
}
}
// Außer wenn schon comprimiert: Code comprimieren
if( self::COMPRESSED !== $item['type'] ){
$item['content'] = $this->_minify( $item['content'], $item['name'] );
}
// Dem Paket hinzufügen
if( $item['content'] ) {
$bundle[] = (count($bundle)?(PHP_EOL.PHP_EOL):'') . $item['content'];
}
}
\rex_file::put( $this->target, implode(" \n\r",$bundle) );
$this->timestamp = @filemtime( $this->target );
} catch (\Exception $e) {
// Fehlermeldung je nach Context "still" oder als "Whoops"
if( 1 == $e->getCode() || \rex::isBackend() || \rex::getUser()->isAdmin() ){
throw $e;
} else {
\rex_logger::logError(E_ERROR, $e->getMessage(), $e->getFile(), $e->getLine());
}
}
return $this;
}
/** Minifiziert den Code
* Ein einleitender Kommentar z.B. mit Versions- und Copyright-Informationen bleibt enthalten
* und wird nach dem Minifizieren dem minifizierten Code vorangestellt.
*
* Fehler beim Minifizieren führen in minity() zu einer Exception. Die wird in create()
* abgefangen, nicht hier.
*
* @var string Der Quellcode
* @var string Der DAteiname für ggf.vom Minifizierer erzeugte Fehlermeldungen
* @return string Der minifizierte Code.
*
* @throws \AssetPacker\MinifyError
* Nimmt eine Fehlermeldung vom Minifier auf und leitet sie mit
* erweitertem Text weiter
*/
private function _minify( string $content, string $name = '' ) : string
{
// einleitender Kommentar /*...*/ bleibt erhalten
$rem = $this->_getComment( $content );
// Übrigen Text minifizieren
try {
if( $rem ){
$content = $this->minify( substr($content,strlen($rem)) );
} else {
$content = $this->minify( $content );
}
} catch (\Exception $e) {
throw new MinifyError(sprintf(self::ERR_MINIFY,$name,$e->getMessage()), 2);
}
// ggf. einleitenden Kommentar wieder einfügen
return $rem ? $rem . PHP_EOL . $content : $content;
}
/** Ermittelt den einleitenden Kommentar im Quellcode
* Der Kommentar darf z.B. Versions- und Copyright-Informationen enthalten, die
* später wieder vor der minifizierten Datei eingefügt werden.
*
* @var string Der Quellcode als Referenz
* @return string|bool Entweder der Kommentar oder false für "kein Kommentar"
*/
public function _getComment( string &$content ) : string
{
$pattern = '/^'.preg_quote($this->remOn,'/').'.*?'.preg_quote($this->remOff,'/').'/s';
if( preg_match( $pattern, $content, $matches ) && $matches ){
return substr( $content, 0, strlen($matches[0]));
}
return false;
}
/** Zerlegt einen Pfadnamen ähnlich wie PHPs pathinfo in seine Bestandteile und wirft auch aus,
* ob es eine minifizierte Datei ist (.min als vorletztes Element im Dateinamen)
* Beispiel: /abc/def/ghi/lorem.min.js abc/def/ghi/lorem.max.js lorem.min.js lorem.min
* http:
* path: /abc/def/ghi/ abc/def/ghi/
* name: lorem lorem.max lorem lorem
* min: .min «leer» .min
* ext: .js .js .js .min
* Rückgabe ist entweder ein Array mit den vier Bestandteilen oder false für
* formal falsch (ohne Extension) bzw. leer
* Auch möglich: URL, die mit http(s):// beginnt.
*
* @param string Der zu untersuchenede Pfad
* @return array|false [http=>,path=>,name=>,min=>,ext=>] oder false
*/
static function fileinfo( $filename )
{
$pattern = '/^((?<http>https?\:\/\/)|(?<ws>.*?\:\/\/))?(?<path>(.*?\/)*)(?<name>.*?)(?<min>\.min)?(?<ext>\.\w+)$/';
if( preg_match( $pattern, trim($filename), $pathinfo ) && !$pathinfo['ws'] ) {
unset( $pathinfo['ws'] );
return array_filter($pathinfo,'is_string',ARRAY_FILTER_USE_KEY);
}
return false;
}
/** Führt die dateityp-spezifische Minimierung eines Codeblocks durch
*
* Wenn die Minifizierung misslingt muss mit einer Exceptionabgebrochen werden.
* Die Exception bearbeitet die aufrufende Methode (hier: _minifty())
*
* @param string der umkomprimierte Codeblock
* @return string der komprimierte Codeblock
*/
abstract public function minify ( string $content ) : string;
/** Erzeugt für die Target-Datei den passenden HTML-Tag zum Einbinden der Ressource
*
* @return string der HTML-Tag
*/
abstract public function getTag ( ) : string;
} // end of class AssetPacker
class TargetError extends \Exception {}
class SourceError extends \Exception {}
class MinifyError extends \Exception {}
/** Variante für CSS-Dateien
*
* Der Minifier für CSS nutzt das im READXO enthaltene ScssPhp als Minifizierer.
* Als zusätzicher Benefit die CSS-Dateien LESS/SASS-Format haben
* Die Redaxo-LESS/SASS-Variablen aus be_style/plugins/redaxo/scss/_variables werden stets
* automatisch bereitgestellt.
*
* @package AssetPacker
*/
class AssetPacker_css extends AssetPacker
{
public $validExtensions = ['.scss'];
public function minify( string $content ) : string
{
$scss_compiler = new \ScssPhp\ScssPhp\Compiler();
$scss_compiler->setNumberPrecision(10);
$scss_compiler->setFormatter(\ScssPhp\ScssPhp\Formatter\Compressed::class);
$styles = '@import \''.\rex_path::addon('be_style','plugins/redaxo/scss/_variables').'\';';
// SCSSPHP mag nicht mit Windows-Backslash (@dtpop), daher ...
$styles = str_replace('\\','/',$styles);
return $scss_compiler->compile($styles.$content);
}
public function getTag( string $media = 'all' ) : string
{
// Pathname relativ zu rex_path
$asset = \rex_url::base( \rex_path::relative( $this->target ) );
if (!\rex::isDebugMode() && \rex::isBackend() && $this->timestamp)
{
$asset = \rex_url::backendController(['asset' => $asset, 'buster' => $this->timestamp]);
}
elseif ($this->timestamp )
{
$asset .= '?buster=' . $this->timestamp;
}
return ' <link rel="stylesheet" type="text/css" media="' . $media . '" href="' . $asset .'" />';
}
} // end of class AssetPacker_css
/** Variante für JS-Dateien
*
* Komprimiert den JS-Code recht einfach durch Entfernen der Kommentare und unnötiger Leerzeichen.
*
* @package AssetPacker
*/
class AssetPacker_js extends AssetPacker
{
public function minify( string $content = '' ) : string
{
return Minifier::minify($content);
}
public function getTag( array $options = [] ) : string
{
// Pathname relativ zu rex_path
$asset = \rex_url::base( \rex_path::relative( $this->target ) );
if (array_key_exists(\rex_view::JS_IMMUTABLE, $options) && $options[\rex_view::JS_IMMUTABLE])
{
if (!\rex::isDebugMode() && \rex::backendController() && $this->timestamp)
{
$asset = \rex_url::$controller(['asset' => $asset, 'buster' => $this->timestamp]);
}
}
elseif ( $this->timestamp )
{
$asset .= '?buster=' . $this->timestamp;
}
$attributes = [];
if (array_key_exists(\rex_view::JS_ASYNC, $options) && $options[\rex_view::JS_ASYNC]) {
$attributes[] = 'async="async"';
}
if (array_key_exists(\rex_view::JS_DEFERED, $options) && $options[\rex_view::JS_DEFERED]) {
$attributes[] = 'defer="defer"';
}
return "\n" . ' <script type="text/javascript" src="' . $asset .'" '. implode(' ', $attributes) .'></script>';
}
} // end of class AssetPacker_js
/*
* The following code is from the JShrink package. (https://github.com/tedivm/JShrink)
*
* > Erweiterung von Christiph Böcker für nettere Fehlermeldungen
* > function cb_line_and_col( Rechnet eine Char-Position wieder in Zeile/Spalte um
* > In den Exceptions statt Position den Wert aus cb_line_and_col() einsetzen
*
* (c) Robert Hafner <tedivm@tedivm.com>
*
* BSD 3-Clause License
*
* Copyright (c) 2009, Robert Hafner
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/**
* JShrink
*
*
* @package JShrink
* @author Robert Hafner <tedivm@tedivm.com>
*/
/**
* Minifier
*
* Usage - Minifier::minify($js);
* Usage - Minifier::minify($js, $options);
* Usage - Minifier::minify($js, array('flaggedComments' => false));
*
* @package JShrink
* @author Robert Hafner <tedivm@tedivm.com>
* @license http://www.opensource.org/licenses/bsd-license.php BSD License
* @version 1.4.0
*/
class Minifier
{
/**
* The input javascript to be minified.
*
* @var string
*/
protected $input;
/**
* Length of input javascript.
*
* @var int
*/
protected $len = 0;
/**
* The location of the character (in the input string) that is next to be
* processed.
*
* @var int
*/
protected $index = 0;
/**
* The first of the characters currently being looked at.
*
* @var string
*/
protected $a = '';
/**
* The next character being looked at (after a);
*
* @var string
*/
protected $b = '';
/**
* This character is only active when certain look ahead actions take place.
*
* @var string
*/
protected $c;
/**
* Contains the options for the current minification process.
*
* @var array
*/
protected $options;
/**
* These characters are used to define strings.
*/
protected $stringDelimiters = ['\'' => true, '"' => true, '`' => true];
/**
* Contains the default options for minification. This array is merged with
* the one passed in by the user to create the request specific set of
* options (stored in the $options attribute).
*
* @var array
*/
protected static $defaultOptions = ['flaggedComments' => true];
/**
* Contains lock ids which are used to replace certain code patterns and
* prevent them from being minified
*
* @var array
*/
protected $locks = [];
/**
* Takes a string containing javascript and removes unneeded characters in
* order to shrink the code without altering it's functionality.
*
* @param string $js The raw javascript to be minified
* @param array $options Various runtime options in an associative array
* @throws \Exception
* @return bool|string
*/
public static function minify($js, $options = [])
{
try {
ob_start();
$jshrink = new Minifier();
$js = $jshrink->lock($js);
$jshrink->minifyDirectToOutput($js, $options);
// Sometimes there's a leading new line, so we trim that out here.
$js = ltrim(ob_get_clean());
$js = $jshrink->unlock($js);
unset($jshrink);
return $js;
} catch (\Exception $e) {
if (isset($jshrink)) {
// Since the breakdownScript function probably wasn't finished
// we clean it out before discarding it.
$jshrink->clean();
unset($jshrink);
}
// without this call things get weird, with partially outputted js.
ob_end_clean();
throw $e;
}
}
/**
* Processes a javascript string and outputs only the required characters,
* stripping out all unneeded characters.
*
* @param string $js The raw javascript to be minified
* @param array $options Various runtime options in an associative array
*/
protected function minifyDirectToOutput($js, $options)
{
$this->initialize($js, $options);
$this->loop();
$this->clean();
}
/**
* Initializes internal variables, normalizes new lines,
*
* @param string $js The raw javascript to be minified
* @param array $options Various runtime options in an associative array
*/
protected function initialize($js, $options)
{
$this->options = array_merge(static::$defaultOptions, $options);
$this->input = str_replace(["\r\n", '/**/', "\r"], ["\n", "", "\n"], $js);
// We add a newline to the end of the script to make it easier to deal
// with comments at the bottom of the script- this prevents the unclosed
// comment error that can otherwise occur.
$this->input .= PHP_EOL;
// save input length to skip calculation every time
$this->len = strlen($this->input);
// Populate "a" with a new line, "b" with the first character, before
// entering the loop
$this->a = "\n";
$this->b = $this->getReal();
}
/**
* Characters that can't stand alone preserve the newline.
*
* @var array
*/
protected $noNewLineCharacters = [
'(' => true,
'-' => true,
'+' => true,
'[' => true,
'@' => true];
/**
* The primary action occurs here. This function loops through the input string,
* outputting anything that's relevant and discarding anything that is not.
*/
protected function loop()
{
while ($this->a !== false && !is_null($this->a) && $this->a !== '') {
switch ($this->a) {
// new lines
case "\n":
// if the next line is something that can't stand alone preserve the newline
if ($this->b !== false && isset($this->noNewLineCharacters[$this->b])) {
echo $this->a;
$this->saveString();
break;
}
// if B is a space we skip the rest of the switch block and go down to the
// string/regex check below, resetting $this->b with getReal
if ($this->b === ' ') {
break;
}
// otherwise we treat the newline like a space
// no break
case ' ':
if (static::isAlphaNumeric($this->b)) {
echo $this->a;
}
$this->saveString();
break;
default:
switch ($this->b) {
case "\n":
if (strpos('}])+-"\'', $this->a) !== false) {
echo $this->a;
$this->saveString();
break;
} else {
if (static::isAlphaNumeric($this->a)) {
echo $this->a;
$this->saveString();
}
}
break;
case ' ':
if (!static::isAlphaNumeric($this->a)) {
break;
}
// no break
default:
// check for some regex that breaks stuff
if ($this->a === '/' && ($this->b === '\'' || $this->b === '"')) {
$this->saveRegex();
continue 3;
}
echo $this->a;
$this->saveString();
break;
}
}
// do reg check of doom
$this->b = $this->getReal();
if (($this->b == '/' && strpos('(,=:[!&|?', $this->a) !== false)) {
$this->saveRegex();
}
}
}
/**
* Resets attributes that do not need to be stored between requests so that
* the next request is ready to go. Another reason for this is to make sure
* the variables are cleared and are not taking up memory.
*/
protected function clean()
{
unset($this->input);
$this->len = 0;
$this->index = 0;
$this->a = $this->b = '';
unset($this->c);
unset($this->options);
}
/**
* Returns the next string for processing based off of the current index.
*
* @return string
*/
protected function getChar()
{
// Check to see if we had anything in the look ahead buffer and use that.
if (isset($this->c)) {
$char = $this->c;
unset($this->c);
} else {
// Otherwise we start pulling from the input.
$char = $this->index < $this->len ? $this->input[$this->index] : false;
// If the next character doesn't exist return false.
if (isset($char) && $char === false) {
return false;
}
// Otherwise increment the pointer and use this char.
$this->index++;
}
// Normalize all whitespace except for the newline character into a
// standard space.
if ($char !== "\n" && $char < "\x20") {
return ' ';
}
return $char;
}
/**
* This function gets the next "real" character. It is essentially a wrapper
* around the getChar function that skips comments. This has significant
* performance benefits as the skipping is done using native functions (ie,
* c code) rather than in script php.
*
*
* @return string Next 'real' character to be processed.
* @throws \RuntimeException
*/
protected function getReal()
{
$startIndex = $this->index;
$char = $this->getChar();
// Check to see if we're potentially in a comment
if ($char !== '/') {
return $char;
}
$this->c = $this->getChar();
if ($this->c === '/') {
$this->processOneLineComments($startIndex);
return $this->getReal();
} elseif ($this->c === '*') {
$this->processMultiLineComments($startIndex);
return $this->getReal();
}
return $char;
}
/**
* Removed one line comments, with the exception of some very specific types of
* conditional comments.
*
* @param int $startIndex The index point where "getReal" function started
* @return void
*/
protected function processOneLineComments($startIndex)
{
$thirdCommentString = $this->index < $this->len ? $this->input[$this->index] : false;
// kill rest of line
$this->getNext("\n");
unset($this->c);
if ($thirdCommentString == '@') {
$endPoint = $this->index - $startIndex;
$this->c = "\n" . substr($this->input, $startIndex, $endPoint);
}
}
/**
* Skips multiline comments where appropriate, and includes them where needed.
* Conditional comments and "license" style blocks are preserved.
*
* @param int $startIndex The index point where "getReal" function started
* @return void
* @throws \RuntimeException Unclosed comments will throw an error
*/
protected function processMultiLineComments($startIndex)
{
$this->getChar(); // current C
$thirdCommentString = $this->getChar();
// kill everything up to the next */ if it's there
if ($this->getNext('*/')) {
$this->getChar(); // get *
$this->getChar(); // get /
$char = $this->getChar(); // get next real character
// Now we reinsert conditional comments and YUI-style licensing comments
if (($this->options['flaggedComments'] && $thirdCommentString === '!')
|| ($thirdCommentString === '@')) {
// If conditional comments or flagged comments are not the first thing in the script
// we need to echo a and fill it with a space before moving on.
if ($startIndex > 0) {
echo $this->a;
$this->a = " ";
// If the comment started on a new line we let it stay on the new line
if ($this->input[($startIndex - 1)] === "\n") {
echo "\n";
}
}
$endPoint = ($this->index - 1) - $startIndex;
echo substr($this->input, $startIndex, $endPoint);
$this->c = $char;
return;
}
} else {
$char = false;
}
if ($char === false) {
throw new \RuntimeException('Unclosed multiline comment ' . $this->cb_line_and_col($this->index - 2));
}
// if we're here c is part of the comment and therefore tossed
$this->c = $char;
}
/**
* Pushes the index ahead to the next instance of the supplied string. If it
* is found the first character of the string is returned and the index is set
* to it's position.
*
* @param string $string
* @return string|false Returns the first character of the string or false.
*/
protected function getNext($string)
{
// Find the next occurrence of "string" after the current position.
$pos = strpos($this->input, $string, $this->index);
// If it's not there return false.
if ($pos === false) {
return false;
}
// Adjust position of index to jump ahead to the asked for string
$this->index = $pos;
// Return the first character of that string.
return $this->index < $this->len ? $this->input[$this->index] : false;
}
/**
* When a javascript string is detected this function crawls for the end of