diff --git a/core/Requirements.php b/core/Requirements.php index 4654e472b..f93a0e600 100644 --- a/core/Requirements.php +++ b/core/Requirements.php @@ -6,14 +6,36 @@ * @subpackage view */ class Requirements { + private static $javascript = array(); + private static $css = array(); + private static $customScript = array(); + private static $customCSS = array(); + private static $customHeadTags = ""; + private static $disabled = array(); + private static $blocked = array(); + /** + * See {@link combine_files()}. + * + * @var array $files_to_combine + */ + public static $files_to_combine = array(); + + /** + * Using the JSMin library to minify any + * javascript file passed to {@link combine_files()}. + * + * @var boolean + */ + public static $combine_js_with_jsmin = true; + /** * Register the given javascript file as required. * Filenames should be relative to the base, eg, 'sapphire/javascript/loader.js' @@ -175,16 +197,26 @@ class Requirements { /** * Update the given HTML content with the appropriate include tags for the registered - * requirements. + * requirements. Needs to receive a valid HTML/XHTML template in the $content parameter, + * including a
tag. The requirements will insert before the closing tag automatically. + * * @todo Calculate $prefix properly + * + * @param string $templateFilePath Absolute path for the *.ss template file + * @param string $content HTML content that has already been parsed from the $templateFilePath through {@link SSViewer}. + * @return string HTML content thats augumented with the requirements before the closing tag. */ - static function includeInHTML($templateFile, $content) { + static function includeInHTML($templateFilePath, $content) { if(isset($_GET['debug_profile'])) Profiler::mark("Requirements::includeInHTML"); if(strpos($content, ' $dummy) { if(substr($file,0,7) == 'http://' || Director::fileExists($file)) { $requirements .= "\n"; @@ -198,6 +230,9 @@ class Requirements { $requirements .= "\n//]]>\n\n"; } } + + $jsRequirements=$requirements; + foreach(array_diff_key(self::$css,self::$blocked) as $file => $params) { if(Director::fileExists($file)) { $media = (isset($params['media']) && !empty($params['media'])) ? " media=\"{$params['media']}\"" : ""; @@ -219,6 +254,139 @@ class Requirements { } } + /** + * Concatenate several css or javascript files into a single dynamically generated + * file (stored in {@link Director::baseFolder()}). This increases performance + * by fewer HTTP requests. + * + * The combined file is regenerated + * based on every file modification time. Optionally a rebuild can be triggered + * by appending ?flush=1 to the URL. + * If all files to be combined are javascript, we use the external JSMin library + * to minify the javascript. This can be controlled by {@link $combine_js_with_jsmin}. + * + * All combined files will have a comment on the start of each concatenated file + * denoting their original position. For easier debugging, we recommend to only + * minify javascript if not in development mode ({@link Director::isDev()}). + * + * CAUTION: You're responsible for ensuring that the load order for combined files + * is retained - otherwise combining javascript files can lead to functional errors + * in the javascript logic, and combining css can lead to wrong styling inheritance. + * Depending on the javascript logic, you also have to ensure that files are not included + * in more than one combine_files() call. + * + * Example for combined JavaScript: + *
+ * Requirements::combine_files(
+ * 'foobar.js',
+ * array(
+ * 'mysite/javascript/foo.js',
+ * 'mysite/javascript/bar.js',
+ * )
+ * );
+ *
+ *
+ * Example for combined CSS:
+ *
+ * Requirements::combine_files(
+ * 'foobar.css',
+ * array(
+ * 'mysite/javascript/foo.css',
+ * 'mysite/javascript/bar.css',
+ * )
+ * );
+ *
+ *
+ * @see http://code.google.com/p/jsmin-php/
+ *
+ * @param string $combinedFileName Filename of the combined file (will be stored in {@link Director::baseFolder()} by default)
+ * @param array $files Array of filenames relative to the webroot
+ */
+ static function combine_files($combinedFileName, $files){
+ self::$files_to_combine[$combinedFileName] = $files;
+ }
+
+ /**
+ * See {@link combine_files()}.
+ */
+ static function process_combined_includes() {
+ // Make a map of files that could be potentially combined
+ $combinerCheck = array();
+ foreach(self::$files_to_combine as $combinedFile => $sourceItems) {
+ foreach($sourceItems as $sourceItem) {
+ if(isset($combinerCheck[$sourceItem]) && $combinerCheck[$sourceItem] != $combinedFile){
+ user_error("Requirements::process_combined_includes - file '$sourceItem' appears in two combined files:" . " '{$combinerCheck[$sourceItem]}' and '$combinedFile'", E_USER_WARNING);
+ }
+ $combinerCheck[$sourceItem] = $combinedFile;
+
+ }
+ }
+
+ // Figure out which ones apply to this pageview
+ $combinedFiles = array();
+ $newJSRequirements = array();
+ $newCSSRequirements = array();
+ foreach(Requirements::$javascript as $file => $dummy) {
+ if(isset($combinerCheck[$file])) {
+ $newJSRequirements[$combinerCheck[$file]] = true;
+ $combinedFiles[$combinerCheck[$file]] = true;
+ } else {
+ $newJSRequirements[$file] = true;
+ }
+ }
+
+ foreach(Requirements::$css as $file => $params) {
+ if(isset($combinerCheck[$file])) {
+ $newCSSRequirements[$combinerCheck[$file]] = true;
+ $combinedFiles[$combinerCheck[$file]] = true;
+ } else {
+ $newCSSRequirements[$file] = $params;
+ }
+ }
+
+ Requirements::$javascript = $newJSRequirements;
+ Requirements::$css = $newCSSRequirements;
+
+ // Process the combined files
+ if($combinedFiles) {
+ $base = Director::baseFolder() . '/';
+ foreach($combinedFiles as $combinedFile => $dummy) {
+ $fileList = self::$files_to_combine[$combinedFile];
+
+ // Determine if we need to build the combined include
+ if(file_exists($base . $combinedFile) && !isset($_GET['flush'])) {
+ $srcLastMod = 0;
+ foreach($fileList as $file) {
+ $srcLastMod = max(filemtime($base . $file), $srcLastMod);
+ }
+ $refresh = $srcLastMod > filemtime($base . $combinedFile);
+ } else {
+ $refresh = true;
+ }
+
+ // Rebuild, if necessary
+ if($refresh) {
+ $combinedData = "";
+ foreach($fileList as $file) {
+ $fileContent = file_get_contents($base . $file);
+ if(stripos($file, '.js') && self::$combine_js_with_jsmin) {
+ $fileContent = JSMin::minify($fileContent);
+ }
+ $combinedData .= "/****** FILE: $file *****/\n" . $fileContent . "\n";
+ }
+ if(!file_exists(dirname($base . $combinedFile)))
+ mkdir(dirname($base . $combinedFile), Filesystem::$folder_create_mask, true);
+
+ $fh = fopen($base . $combinedFile, 'w');
+ fwrite($fh, $combinedData);
+ fclose($fh);
+ }
+ }
+ }
+
+ }
+
+
static function get_custom_scripts() {
$requirements = "";
diff --git a/tests/forms/RequirementsTest.php b/tests/forms/RequirementsTest.php
new file mode 100644
index 000000000..f812ca157
--- /dev/null
+++ b/tests/forms/RequirementsTest.php
@@ -0,0 +1,53 @@
+';
+
+ function testCombinedJavascript() {
+ // require files normally (e.g. called from a FormField instance)
+ Requirements::javascript('sapphire/tests/forms/a.js');
+ Requirements::javascript('sapphire/tests/forms/b.js');
+ Requirements::javascript('sapphire/tests/forms/c.js');
+
+ // require two of those files as combined includes
+ Requirements::combine_files(
+ 'bc.js',
+ array(
+ 'sapphire/tests/forms/b.js',
+ 'sapphire/tests/forms/c.js'
+ )
+ );
+
+ $combinedFilePath = Director::baseFolder() . '/' . 'bc.js';
+
+ $html = Requirements::includeInHTML(false, self::$html_template);
+
+ /* COMBINED JAVASCRIPT FILE IS INCLUDED IN HTML HEADER */
+ $this->assertTrue((bool)preg_match('/src=".*\/bc\.js"/', $html));
+
+ /* COMBINED JAVASCRIPT FILE EXISTS */
+ $this->assertTrue(file_exists($combinedFilePath));
+
+ /* COMBINED JAVASCRIPT HAS CORRECT CONTENT */
+ $this->assertTrue((strpos(file_get_contents($combinedFilePath), "alert('b')") !== false));
+ $this->assertTrue((strpos(file_get_contents($combinedFilePath), "alert('c')") !== false));
+
+ /* COMBINED FILES ARE NOT INCLUDED TWICE */
+ $this->assertFalse((bool)preg_match('/src=".*\/b\.js"/', $html));
+ $this->assertFalse((bool)preg_match('/src=".*\/c\.js"/', $html));
+
+ /* NORMAL REQUIREMENTS ARE STILL INCLUDED */
+ $this->assertTrue((bool)preg_match('/src=".*\/a\.js"/', $html));
+
+ unlink($combinedFilePath);
+
+ }
+
+}
+?>
\ No newline at end of file
diff --git a/tests/forms/a.css b/tests/forms/a.css
new file mode 100644
index 000000000..730bd9eb3
--- /dev/null
+++ b/tests/forms/a.css
@@ -0,0 +1 @@
+.a {color: #f00;}
\ No newline at end of file
diff --git a/tests/forms/a.js b/tests/forms/a.js
new file mode 100644
index 000000000..829a65243
--- /dev/null
+++ b/tests/forms/a.js
@@ -0,0 +1 @@
+alert('a');
\ No newline at end of file
diff --git a/tests/forms/b.css b/tests/forms/b.css
new file mode 100644
index 000000000..e11f7eebf
--- /dev/null
+++ b/tests/forms/b.css
@@ -0,0 +1 @@
+.b {color: #0ff;}
\ No newline at end of file
diff --git a/tests/forms/b.js b/tests/forms/b.js
new file mode 100644
index 000000000..3d8670ca8
--- /dev/null
+++ b/tests/forms/b.js
@@ -0,0 +1 @@
+alert('b');
\ No newline at end of file
diff --git a/tests/forms/c.css b/tests/forms/c.css
new file mode 100644
index 000000000..ae49dd202
--- /dev/null
+++ b/tests/forms/c.css
@@ -0,0 +1 @@
+.c {color: #0ff;}
\ No newline at end of file
diff --git a/tests/forms/c.js b/tests/forms/c.js
new file mode 100644
index 000000000..1e0f7b3ce
--- /dev/null
+++ b/tests/forms/c.js
@@ -0,0 +1 @@
+alert('c');
\ No newline at end of file