diff --git a/src/language/en-GB/tpl_moko-cassiopeia.ini b/src/language/en-GB/tpl_moko-cassiopeia.ini index 03d6e7e..8611efe 100644 --- a/src/language/en-GB/tpl_moko-cassiopeia.ini +++ b/src/language/en-GB/tpl_moko-cassiopeia.ini @@ -17,7 +17,7 @@ TPL_MOKO-CASSIOPEIA_XML_DESCRIPTION="

MOKO-CASSIOPEIA Template Description + + This file is part of a Moko Consulting project. + + SPDX-License-Identifier: GPL-3.0-or-later + + # FILE INFORMATION + DEFGROUP: Joomla.Template.Site + INGROUP: Moko-Cassiopeia + REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia + PATH: ./templates/moko-cassiopeia/AssetMinifier.php + VERSION: 03.05.00 + BRIEF: Asset minification helper for development mode toggle + */ + +defined('_JEXEC') or die; + +/** + * Asset Minifier Helper + * + * Handles minification and cleanup of CSS and JavaScript assets + * based on the development mode setting. + */ +class AssetMinifier +{ + /** + * Minify CSS content + * + * @param string $css CSS content to minify + * @return string Minified CSS + */ + public static function minifyCSS(string $css): string + { + // Remove comments + $css = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $css); + + // Remove whitespace + $css = str_replace(["\r\n", "\r", "\n", "\t", ' ', ' ', ' '], '', $css); + + // Remove spaces around selectors and properties + $css = preg_replace('/\s*([{}|:;,])\s*/', '$1', $css); + + // Remove trailing semicolons + $css = str_replace(';}', '}', $css); + + return trim($css); + } + + /** + * Minify JavaScript content + * + * @param string $js JavaScript content to minify + * @return string Minified JavaScript + */ + public static function minifyJS(string $js): string + { + // Remove single-line comments (but preserve URLs) + $js = preg_replace('~//[^\n]*\n~', "\n", $js); + + // Remove multi-line comments + $js = preg_replace('~/\*.*?\*/~s', '', $js); + + // Remove whitespace + $js = preg_replace('/\s+/', ' ', $js); + + // Remove spaces around operators and punctuation + $js = preg_replace('/\s*([\{\}\[\]\(\);,=<>!&|+\-*\/])\s*/', '$1', $js); + + return trim($js); + } + + /** + * Create minified version of a file + * + * @param string $sourcePath Path to source file + * @param string $destPath Path to minified file + * @return bool Success status + */ + public static function minifyFile(string $sourcePath, string $destPath): bool + { + if (!file_exists($sourcePath)) { + return false; + } + + $content = file_get_contents($sourcePath); + if ($content === false) { + return false; + } + + $ext = pathinfo($sourcePath, PATHINFO_EXTENSION); + + if ($ext === 'css') { + $minified = self::minifyCSS($content); + } elseif ($ext === 'js') { + $minified = self::minifyJS($content); + } else { + return false; + } + + return file_put_contents($destPath, $minified) !== false; + } + + /** + * Delete all minified files in a directory (recursive) + * + * @param string $dir Directory path + * @return int Number of files deleted + */ + public static function deleteMinifiedFiles(string $dir): int + { + $deleted = 0; + + if (!is_dir($dir)) { + return 0; + } + + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS), + RecursiveIteratorIterator::SELF_FIRST + ); + + foreach ($iterator as $file) { + if ($file->isFile() && preg_match('/\.min\.(css|js)$/', $file->getFilename())) { + if (unlink($file->getPathname())) { + $deleted++; + } + } + } + + return $deleted; + } + + /** + * Process assets based on development mode + * + * @param string $mediaPath Path to media directory + * @param bool $developmentMode Development mode flag + * @return array Status information + */ + public static function processAssets(string $mediaPath, bool $developmentMode): array + { + $result = [ + 'mode' => $developmentMode ? 'development' : 'production', + 'minified' => 0, + 'deleted' => 0, + 'errors' => [] + ]; + + if (!is_dir($mediaPath)) { + $result['errors'][] = "Media path does not exist: {$mediaPath}"; + return $result; + } + + if ($developmentMode) { + // Delete all .min files + $result['deleted'] = self::deleteMinifiedFiles($mediaPath); + } else { + // Create minified versions of CSS and JS files + $files = [ + 'css/template.css' => 'css/template.min.css', + 'css/user.css' => 'css/user.min.css', + 'css/editor.css' => 'css/editor.min.css', + 'css/colors/light/colors_standard.css' => 'css/colors/light/colors_standard.min.css', + 'css/colors/light/colors_alternative.css' => 'css/colors/light/colors_alternative.min.css', + 'css/colors/light/colors_custom.css' => 'css/colors/light/colors_custom.min.css', + 'css/colors/dark/colors_standard.css' => 'css/colors/dark/colors_standard.min.css', + 'css/colors/dark/colors_alternative.css' => 'css/colors/dark/colors_alternative.min.css', + 'css/colors/dark/colors_custom.css' => 'css/colors/dark/colors_custom.min.css', + 'js/template.js' => 'js/template.min.js', + 'js/theme-init.js' => 'js/theme-init.min.js', + 'js/darkmode-toggle.js' => 'js/darkmode-toggle.min.js', + 'js/gtm.js' => 'js/gtm.min.js', + ]; + + foreach ($files as $source => $dest) { + $sourcePath = $mediaPath . '/' . $source; + $destPath = $mediaPath . '/' . $dest; + + // Only minify if source exists and dest doesn't exist or is older + if (file_exists($sourcePath)) { + if (!file_exists($destPath) || filemtime($sourcePath) > filemtime($destPath)) { + if (self::minifyFile($sourcePath, $destPath)) { + $result['minified']++; + } else { + $result['errors'][] = "Failed to minify: {$source}"; + } + } + } + } + } + + return $result; + } +} diff --git a/src/templates/index.php b/src/templates/index.php index 4fcca69..90bc1ad 100644 --- a/src/templates/index.php +++ b/src/templates/index.php @@ -24,6 +24,9 @@ use Joomla\CMS\Uri\Uri; /** @var Joomla\CMS\Document\HtmlDocument $this */ +// Load Asset Minifier +require_once __DIR__ . '/AssetMinifier.php'; + $app = Factory::getApplication(); $input = $app->getInput(); $wa = $this->getWebAssetManager(); @@ -41,6 +44,10 @@ $params_custom_head_start = $this->params->get('custom_head_start', null); $params_custom_head_end = $this->params->get('custom_head_end', null); $params_developmentmode = $this->params->get('developmentmode', false); +// Process assets based on development mode +$mediaPath = JPATH_ROOT . '/media/templates/site/moko-cassiopeia'; +AssetMinifier::processAssets($mediaPath, $params_developmentmode); + // Bootstrap behaviors (assets handled via WAM) HTMLHelper::_('bootstrap.framework'); HTMLHelper::_('bootstrap.loadCss', true); @@ -80,16 +87,19 @@ $this->setTitle($final); // Template/Media path $templatePath = 'media/templates/site/moko-cassiopeia'; +// Asset suffix based on development mode +$assetSuffix = $params_developmentmode ? '' : '.min'; + // =========================== // Web Asset Manager (WAM) — matches your joomla.asset.json // =========================== // Core template CSS -$wa->useStyle('template.global.base'); // css/template.css -$wa->useStyle('template.global.social-media-demo'); // css/user.css +$wa->useStyle('template.global.base' . $assetSuffix); // css/template.css or template.min.css +$wa->useStyle('template.user' . $assetSuffix); // css/user.css or user.min.css // Optional vendor CSS -$wa->useStyle('vendor.bootstrap-toc'); +$wa->useStyle('vendor.bootstrap-toc' . $assetSuffix); // Optional demo/showcase CSS (available for use, not loaded by default) // To use: Add 'template.global.social-media-demo' to your article/module @@ -98,34 +108,34 @@ $wa->useStyle('vendor.bootstrap-toc'); // Color theme (light + optional dark) $colorLightKey = strtolower(preg_replace('/[^a-z0-9_.-]/i', '', $params_LightColorName)); $colorDarkKey = strtolower(preg_replace('/[^a-z0-9_.-]/i', '', $params_DarkColorName)); -$lightKey = 'template.light.' . $colorLightKey; -$darkKey = 'template.dark.' . $colorDarkKey; +$lightKey = 'template.light.' . $colorLightKey . $assetSuffix; +$darkKey = 'template.dark.' . $colorDarkKey . $assetSuffix; try { - $wa->useStyle('template.light.colors_standard'); + $wa->useStyle('template.light.colors_standard' . $assetSuffix); } catch (\Throwable $e) { - $wa->registerAndUseStyle('template.light.colors_standard', $templatePath . '/css/global/light/colors_standard.css'); + $wa->registerAndUseStyle('template.light.colors_standard', $templatePath . '/css/colors/light/colors_standard' . $assetSuffix . '.css'); } try { - $wa->useStyle('template.dark.colors_standard'); + $wa->useStyle('template.dark.colors_standard' . $assetSuffix); } catch (\Throwable $e) { - $wa->registerAndUseStyle('template.dark.colors_standard', $templatePath . '/css/global/dark/colors_standard.css'); + $wa->registerAndUseStyle('template.dark.colors_standard', $templatePath . '/css/colors/dark/colors_standard' . $assetSuffix . '.css'); } try { $wa->useStyle($lightKey); } catch (\Throwable $e) { - $wa->registerAndUseStyle('template.light.dynamic', $templatePath . '/css/global/light/' . $colorLightKey . '.css'); + $wa->registerAndUseStyle('template.light.dynamic', $templatePath . '/css/colors/light/' . $colorLightKey . $assetSuffix . '.css'); } try { $wa->useStyle($darkKey); } catch (\Throwable $e) { - $wa->registerAndUseStyle('template.dark.dynamic', $templatePath . '/css/global/dark/' . $colorDarkKey . '.css'); + $wa->registerAndUseStyle('template.dark.dynamic', $templatePath . '/css/colors/dark/' . $colorDarkKey . $assetSuffix . '.css'); } // Scripts -$wa->useScript('template.js'); -$wa->useScript('theme-init.js'); -$wa->useScript('darkmode-toggle.js'); -$wa->useScript('vendor.bootstrap-toc.js'); +$wa->useScript('template.js' . $assetSuffix); +$wa->useScript('theme-init' . $assetSuffix . '.js'); +$wa->useScript('darkmode-toggle' . $assetSuffix . '.js'); +$wa->useScript('vendor.bootstrap-toc.js' . $assetSuffix); // Font scheme (external or local) + CSS custom properties $params_FontScheme = $this->params->get('useFontScheme', false); @@ -205,40 +215,21 @@ if ($this->params->get('faKitCode')) { HTMLHelper::_('script', $faKit, ['crossorigin' => 'anonymous']); } else { try { - if($params_developmentmode){ - $wa->useStyle('vendor.fa7free.all'); - $wa->useStyle('vendor.fa7free.brands'); - $wa->useStyle('vendor.fa7free.fontawesome'); - $wa->useStyle('vendor.fa7free.regular'); - $wa->useStyle('vendor.fa7free.solid'); - } else { - $wa->useStyle('vendor.fa7free.all.min'); - $wa->useStyle('vendor.fa7free.brands.min'); - $wa->useStyle('vendor.fa7free.fontawesome.min'); - $wa->useStyle('vendor.fa7free.regular.min'); - $wa->useStyle('vendor.fa7free.solid.min'); - } + $wa->useStyle('vendor.fa7free.all' . $assetSuffix); + $wa->useStyle('vendor.fa7free.brands' . $assetSuffix); + $wa->useStyle('vendor.fa7free.fontawesome' . $assetSuffix); + $wa->useStyle('vendor.fa7free.regular' . $assetSuffix); + $wa->useStyle('vendor.fa7free.solid' . $assetSuffix); } catch (\Throwable $e) { - if($params_developmentmode){ - $wa->registerAndUseStyle('vendor.fa7free.all.dynamic', $templatePath . '/vendor/fa7free/css/all.css'); - $wa->registerAndUseStyle('vendor.fa7free.brands.dynamic', $templatePath . '/vendor/fa7free/css/brands.css'); - $wa->registerAndUseStyle('vendor.fa7free.fontawesome.dynamic', $templatePath . '/vendor/fa7free/css/fontawesome.css'); - $wa->registerAndUseStyle('vendor.fa7free.regular.dynamic', $templatePath . '/vendor/fa7free/css/regular.css'); - $wa->registerAndUseStyle('vendor.fa7free.solid.dynamic', $templatePath . '/vendor/fa7free/css/solid.css'); - } else { - $wa->registerAndUseStyle('vendor.fa7free.all.min.dynamic', $templatePath . '/vendor/fa7free/css/all.min.css'); - $wa->registerAndUseStyle('vendor.fa7free.brands.min.dynamic', $templatePath . '/vendor/fa7free/css/brands.min.css'); - $wa->registerAndUseStyle('vendor.fa7free.fontawesome.min.dynamic', $templatePath . '/vendor/fa7free/css/fontawesome.min.css'); - $wa->registerAndUseStyle('vendor.fa7free.regular.min.dynamic', $templatePath . '/vendor/fa7free/css/regular.min.css'); - $wa->registerAndUseStyle('vendor.fa7free.solid.min.dynamic', $templatePath . '/vendor/fa7free/css/solid.min.css'); - } - + $wa->registerAndUseStyle('vendor.fa7free.all.dynamic', $templatePath . '/vendor/fa7free/css/all' . $assetSuffix . '.css'); + $wa->registerAndUseStyle('vendor.fa7free.brands.dynamic', $templatePath . '/vendor/fa7free/css/brands' . $assetSuffix . '.css'); + $wa->registerAndUseStyle('vendor.fa7free.fontawesome.dynamic', $templatePath . '/vendor/fa7free/css/fontawesome' . $assetSuffix . '.css'); + $wa->registerAndUseStyle('vendor.fa7free.regular.dynamic', $templatePath . '/vendor/fa7free/css/regular' . $assetSuffix . '.css'); + $wa->registerAndUseStyle('vendor.fa7free.solid.dynamic', $templatePath . '/vendor/fa7free/css/solid' . $assetSuffix . '.css'); } } $params_leftIcon = htmlspecialchars($this->params->get('drawerLeftIcon', 'fa-solid fa-chevron-left'), ENT_COMPAT, 'UTF-8'); $params_rightIcon = htmlspecialchars($this->params->get('drawerRightIcon', 'fa-solid fa-chevron-right'), ENT_COMPAT, 'UTF-8'); - -$wa->useStyle('template.user'); // css/user.css ?> diff --git a/src/templates/offline.php b/src/templates/offline.php index 5c0aaad..66ce0ec 100644 --- a/src/templates/offline.php +++ b/src/templates/offline.php @@ -32,6 +32,9 @@ use Joomla\CMS\Uri\Uri; * @var string $this->direction */ +// Load Asset Minifier +require_once __DIR__ . '/AssetMinifier.php'; + $app = Factory::getApplication(); $doc = Factory::getDocument(); $params = $this->params ?: $app->getTemplate(true)->params; @@ -40,15 +43,23 @@ $direction = $this->direction ?: 'ltr'; /* ----------------------- Load ONLY template.css + colors_*.css (with min toggle) ------------------------ */ -$useMin = !((int) $params->get('development_mode', 0) === 1); +$useMin = !((int) $params->get('developmentmode', 0) === 1); $assetSuffix = $useMin ? '.min' : ''; -$base = rtrim(Uri::root(true), '/') . '/templates/' . $this->template . '/css/'; + +// Process assets based on development mode +$mediaPath = JPATH_ROOT . '/media/templates/site/moko-cassiopeia'; +AssetMinifier::processAssets($mediaPath, !$useMin); + +$base = rtrim(Uri::root(true), '/') . '/media/templates/site/moko-cassiopeia/css/'; $doc->addStyleSheet($base . 'template' . $assetSuffix . '.css', ['version' => 'auto'], ['id' => 'moko-template']); /* If you have a template param for color variant, set it here; defaults to 'standard' */ -$colorKey = (string) ($params->get('colors', 'standard') ?: 'standard'); +$colorKey = (string) ($params->get('colorLightName', 'colors_standard') ?: 'colors_standard'); $colorKey = preg_replace('~[^a-z0-9_-]~i', '', $colorKey); -$doc->addStyleSheet($base . 'colors_' . $colorKey . $assetSuffix . '.css', ['version' => 'auto'], ['id' => 'moko-colors']); +$doc->addStyleSheet($base . 'colors/light/' . $colorKey . $assetSuffix . '.css', ['version' => 'auto'], ['id' => 'moko-colors-light']); +$colorKeyDark = (string) ($params->get('colorDarkName', 'colors_standard') ?: 'colors_standard'); +$colorKeyDark = preg_replace('~[^a-z0-9_-]~i', '', $colorKeyDark); +$doc->addStyleSheet($base . 'colors/dark/' . $colorKeyDark . $assetSuffix . '.css', ['version' => 'auto'], ['id' => 'moko-colors-dark']); /* Bootstrap CSS/JS for accordion behavior; safe to keep. */ HTMLHelper::_('bootstrap.loadCss', true, $doc); diff --git a/src/templates/templateDetails.xml b/src/templates/templateDetails.xml index 787a309..4e6caab 100644 --- a/src/templates/templateDetails.xml +++ b/src/templates/templateDetails.xml @@ -87,12 +87,10 @@
-