improved cssmin and combine

This commit is contained in:
gtbu 2025-04-10 13:18:27 +02:00
parent 93955abbd7
commit 2f2c120ec0
5 changed files with 577 additions and 218 deletions

46
include/thirdparty/cssmin_v.1.0.1.php vendored Normal file
View file

@ -0,0 +1,46 @@
<?php
/**
* cssmin.php - A simple CSS minifier.
* --
*
* <code>
* include("cssmin.php");
* file_put_contents("path/to/target.css", cssmin::minify(file_get_contents("path/to/source.css")));
* </code>
* --
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
* --
*
* @package cssmin
* @author Joe Scylla <joe.scylla@gmail.com>
* @copyright 2008 Joe Scylla <joe.scylla@gmail.com>
* @license http://opensource.org/licenses/mit-license.php MIT License
* @version 1.0 (2008-01-31)
*/
class cssmin
{
/**
* Minifies stylesheet definitions
*
* @param string $v Stylesheet definitions as string
* @return string Minified stylesheet definitions
*/
static function minify($v)
{
$v = trim($v);
$v = str_replace("\r\n", "\n", $v);
$search = array("/\/\*[\d\D]*?\*\/|\t+/", "/\s+/", "/\}\s+/");
$replace = array(null, " ", "}\n");
$v = preg_replace($search, $replace, $v);
$search = array("/\\;\s/", "/\s*\{\\s*/", "/\\:\s+\\#/", "/,\s+/i", "/\\:\s+\\\'/i", "/\\:\s+([0-9A-Z\-]+)/i");
$replace = array(";", "{", ":#", ",", ":\'", ":$1");
$v = preg_replace($search, $replace, $v);
$v = str_replace("\n", "" , $v);
return $v;
}
}

View file

@ -1,11 +1,14 @@
<?php
/**
* cssmin.php - A simple CSS minifier.
* --
* Provides basic CSS minification by removing comments and unnecessary whitespace.
*
* <code>
* include("cssmin.php");
* file_put_contents("path/to/target.css", cssmin::minify(file_get_contents("path/to/source.css")));
* $minifiedCss = cssmin::minify(file_get_contents("path/to/source.css"));
* file_put_contents("path/to/target.css", $minifiedCss);
* </code>
* --
*
@ -16,31 +19,98 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
* --
*
* @package cssmin
* @author Joe Scylla <joe.scylla@gmail.com>
* @copyright 2008 Joe Scylla <joe.scylla@gmail.com>
* @license http://opensource.org/licenses/mit-license.php MIT License
* @version 1.0 (2008-01-31)
* @package cssmin
* @author Joe Scylla <joe.scylla@gmail.com>
* @copyright 2008 Joe Scylla <joe.scylla@gmail.com> (Modernized 2023)
* @license http://opensource.org/licenses/mit-license.php MIT License
* @version 1.0.2
* modified 2025 by github.com/gtbu
*/
class cssmin
{
/**
* Minifies stylesheet definitions
*
* @param string $v Stylesheet definitions as string
* @return string Minified stylesheet definitions
*/
static function minify($v)
{
$v = trim($v);
$v = str_replace("\r\n", "\n", $v);
$search = array("/\/\*[\d\D]*?\*\/|\t+/", "/\s+/", "/\}\s+/");
$replace = array(null, " ", "}\n");
$v = preg_replace($search, $replace, $v);
$search = array("/\\;\s/", "/\s*\{\\s*/", "/\\:\s+\\#/", "/,\s+/i", "/\\:\s+\\\'/i", "/\\:\s+([0-9A-Z\-]+)/i");
$replace = array(";", "{", ":#", ",", ":\'", ":$1");
$v = preg_replace($search, $replace, $v);
$v = str_replace("\n", "" , $v);
return $v;
}
}
{
/**
* Minifies CSS definitions.
*
* @param mixed $css CSS content as a string. Accepts mixed for basic type check.
* @return string Minified CSS definitions, or an empty string if input is invalid or empty.
*/
public static function minify($css)
{
// Basic input validation: Ensure it's a string.
if (!is_string($css)) {
// error_log('cssmin::minify() expected a string, got ' . gettype($css)); // Optional logging
return ''; // Return empty string for invalid input
}
// 1. Initial cleanup: Remove leading/trailing whitespace and normalize line endings.
$css = trim($css);
if ($css === '') {
return ''; // Return early if string is empty after trimming
}
$css = str_replace("\r\n", "\n", $css); // Normalize line endings to LF
// 2. First round of regex replacements:
// - Remove comments (/* ... */)
// - Remove tabs
// - Collapse multiple whitespace chars into a single space
// - Remove whitespace after '}' but add a newline (for structure before next step)
$search = array(
"/\/\*[\s\S]*?\*\//", // Remove /* ... */ comments. [\s\S] matches any char incl. newline. *? is non-greedy.
"/\t+/", // Remove tabs.
"/\s+/", // Collapse whitespace (includes space, tab, newline) into a single space.
"/\}\s+/" // Remove whitespace following a '}' and add a newline.
);
$replace = array(
"", // Remove comments.
"", // Remove tabs.
" ", // Collapse whitespace to a single space.
"}\n" // Add newline after closing brace.
);
$css = preg_replace($search, $replace, $css);
// Check if preg_replace failed (returned null)
if ($css === null) {
// error_log('cssmin::minify() preg_replace step 1 failed'); // Optional logging
return ''; // Return empty on regex error
}
// 3. Second round of regex replacements:
// - Remove whitespace around critical CSS characters: ;, {, :, #, ,, '
// - Remove whitespace between ':' and simple values (keywords, numbers).
$search = array(
"/;\s+/", // Remove whitespace after semicolons (e.g., "; " => ";").
"/\s*\{\s*/", // Remove whitespace around opening braces (e.g., " { " => "{").
"/:\s+#/", // Remove whitespace after colon before # (e.g., ": #" => ":#").
"/,\s+/", // Remove whitespace after commas (e.g., ", " => ",").
"/:\s+'/", // Remove whitespace after colon before single quotes (e.g., ": '" => ":'").
"/:\s+\"/", // Remove whitespace after colon before double quotes (e.g., ': "' => ':"'). (Added for consistency)
"/:\s+([a-zA-Z0-9\-]+)/i" // Remove whitespace after colon before common values (keywords, numbers, units like 'px'). Case-insensitive.
// (e.g., "color: red" => "color:red", "margin: 10px" => "margin:10px"). Uses backreference $1.
);
$replace = array(
";", // ;
"{", // {
":#", // :#
",", // ,
":'", // :'
":\"", // :" (Added)
":$1" // :value (using backreference)
);
$css = preg_replace($search, $replace, $css);
// Check if preg_replace failed (returned null)
if ($css === null) {
// error_log('cssmin::minify() preg_replace step 2 failed'); // Optional logging
return ''; // Return empty on regex error
}
// 4. Final step: Remove all remaining newline characters.
// This contradicts step 2 adding newlines after '}', but matches the original code's behavior
// resulting in a single-line output.
$css = str_replace("\n", "", $css);
// Final trim just in case (though unlikely needed after previous steps)
return trim($css);
}
}

View file

@ -0,0 +1,209 @@
<?php
namespace gp\tool\Output;
defined('is_running') or die('Not an entry point...');
/**
* Get the contents of $file and fix paths:
* - url(..)
* - @import
* - @import url(..)
*/
class CombineCSS{
public $content;
public $file;
public $full_path;
public $imported = array();
public $imports = '';
public function __construct($file){
global $dataDir;
includeFile('thirdparty/cssmin_v.1.0.php');
$this->file = $file;
$this->full_path = $dataDir.$file;
$this->content = file_get_contents($this->full_path);
$this->content = \cssmin::minify($this->content);
$this->CSS_Import();
$this->CSS_FixUrls();
}
/**
* Include the css from @imported css
*
* Will include the css from these
* @import "../styles.css";
* @import url("../styles.css");
* @import styles.css;
*
*
* Will preserve the @import rule for these
* @import "styles.css" screen,tv;
* @import url('http://ajax.googleapis.com/ajax/libs/jqueryui/1.10.2/themes/smoothness/jquery-ui.css.css');
*
*/
public function CSS_Import($offset=0){
global $dataDir;
$pos = strpos($this->content,'@import ',$offset);
if( !is_numeric($pos) ){
return;
}
$replace_start = $pos;
$pos += 8;
$replace_end = strpos($this->content,';',$pos);
if( !is_numeric($replace_end) ){
return;
}
$import_orig = substr($this->content,$pos,$replace_end-$pos);
$import_orig = trim($import_orig);
$replace_len = $replace_end-$replace_start+1;
//get url(..)
$media = '';
if( substr($import_orig,0,4) == 'url(' ){
$end_url_pos = strpos($import_orig,')');
$import = substr($import_orig,4, $end_url_pos-4);
$import = trim($import);
$import = trim($import,'"\'');
$media = substr($import_orig,$end_url_pos+1);
}elseif( $import_orig[0] == '"' || $import_orig[0] == "'" ){
$end_url_pos = strpos($import_orig,$import_orig[0],1);
$import = substr($import_orig,1, $end_url_pos-1);
$import = trim($import);
$media = substr($import_orig,$end_url_pos+1);
}
// keep @import when the file is on a remote server?
if( strpos($import,'//') !== false ){
$this->imports .= substr($this->content, $replace_start, $replace_len );
$this->content = substr_replace( $this->content, '', $replace_start, $replace_len);
$this->CSS_Import($offset);
return;
}
//if a media type is set, keep the @import
$media = trim($media);
if( !empty($media) ){
$import = \gp\tool::GetDir(dirname($this->file).'/'.$import);
$import = $this->ReduceUrl($import);
$this->imports .= '@import url("'.$import.'") '.$media.';';
$this->content = substr_replace( $this->content, '', $replace_start, $replace_len);
$this->CSS_Import($offset);
return;
}
//include the css
$full_path = false;
if( $import[0] != '/' ){
$import = dirname($this->file).'/'.$import;
$import = $this->ReduceUrl($import);
}
$full_path = $dataDir.$import;
if( file_exists($full_path) ){
$temp = new \gp\tool\Output\CombineCss($import);
$this->content = substr_replace($this->content,$temp->content,$replace_start,$replace_end-$replace_start+1);
$this->imported[] = $full_path;
$this->imported = array_merge($this->imported,$temp->imported);
$this->imports .= $temp->imports;
$this->CSS_Import($offset);
return;
}
$this->CSS_Import($pos);
}
public function CSS_FixUrls($offset=0){
$pos = strpos($this->content,'url(',$offset);
if( !is_numeric($pos) ){
return;
}
$pos += 4;
$pos2 = strpos($this->content,')',$pos);
if( !is_numeric($pos2) ){
return;
}
$url = substr($this->content,$pos,$pos2-$pos);
$this->CSS_FixUrl($url,$pos,$pos2);
return $this->CSS_FixUrls($pos2);
}
public function CSS_FixUrl($url,$pos,$pos2){
global $dataDir;
$url = trim($url);
$url = trim($url,'"\'');
if( empty($url) ){
return;
}
//relative url
if( $url[0] == '/' ){
return;
}elseif( strpos($url,'://') > 0 ){
return;
}elseif( preg_match('/^data:/i', $url) ){
return;
}
//use a relative path so sub.domain.com and domain.com/sub both work
$replacement = \gp\tool::GetDir(dirname($this->file).'/'.$url);
$replacement = $this->ReduceUrl($replacement);
$replacement = '"'.$replacement.'"';
$this->content = substr_replace($this->content,$replacement,$pos,$pos2-$pos);
}
/**
* Canonicalize a path by resolving references to '/./', '/../'
* Does not remove leading "../"
* @param string path or url
* @return string Canonicalized path
*
*/
public function ReduceUrl($url){
$temp = explode('/',$url);
$result = array();
foreach($temp as $i => $path){
if( $path == '.' ){
continue;
}
if( $path == '..' ){
for($j=$i-1;$j>0;$j--){
if( isset($result[$j]) ){
unset($result[$j]);
continue 2;
}
}
}
$result[$i] = $path;
}
return implode('/',$result);
}
}

View file

@ -4,206 +4,225 @@ namespace gp\tool\Output;
defined('is_running') or die('Not an entry point...');
includeFile('thirdparty/cssmin_v.1.0.php');
/**
* Get the contents of $file and fix paths:
* - url(..)
* - @import
* - @import url(..)
* Combines CSS files, handling @import rules and fixing relative url() paths.
*/
class CombineCSS{
class CombineCSS {
public $content;
public $file;
public $full_path;
public $imported = array();
public $imports = '';
private string $base_dir;
private string $entry_file_rel; // Relative path from base_dir
private array $processed_files = []; // Prevent infinite loops
private string $final_css = '';
private string $preserved_imports = ''; // For external or media-specific imports
public function __construct($file){
global $dataDir;
/**
* Constructor
*
* @param string $entry_file Relative path to the main CSS file from $dataDir.
*/
public function __construct(string $entry_file) {
global $dataDir; // Assuming $dataDir ends *without* a slash
includeFile('thirdparty/cssmin_v.1.0.php');
if (!class_exists('\cssmin')) {
throw new \Exception('cssmin class not found. Ensure thirdparty/cssmin_v.1.0.php is included correctly.');
}
if (empty($dataDir) || !is_dir($dataDir)) {
throw new \Exception('$dataDir is not a valid directory.');
}
$this->file = $file;
$this->full_path = $dataDir.$file;
$this->base_dir = rtrim($dataDir, '/');
$this->entry_file_rel = ltrim($entry_file, '/');
$full_entry_path = $this->base_dir . '/' . $this->entry_file_rel;
if (!file_exists($full_entry_path) || !is_readable($full_entry_path)) {
trigger_error('CombineCSS: Entry file not found or not readable: ' . $full_entry_path, E_USER_WARNING);
$this->final_css = '/* CombineCSS Error: Entry file not found: ' . htmlspecialchars($entry_file) . ' */';
return;
}
$this->processFile($this->entry_file_rel);
// Minify *after* all processing
$this->final_css = \cssmin::minify($this->preserved_imports . $this->final_css);
}
/**
* Public getter for the combined and minified CSS content.
*
* @return string
*/
public function getContent(): string {
return $this->final_css;
}
/**
* Recursively processes a CSS file, handling imports and fixing URLs.
* @param string $file_rel Relative path of the CSS file from base_dir.
* @return string The processed CSS content of this file and its imports.
*/
private function processFile(string $file_rel): string {
$full_path = $this->base_dir . '/' . $file_rel;
$real_path = realpath($full_path); // Resolve symlinks, etc. for accurate tracking
// Prevent infinite loops for circular imports
if (!$real_path || isset($this->processed_files[$real_path])) {
trigger_error('CombineCSS: Skipped duplicate or invalid import: ' . $file_rel, E_USER_NOTICE);
return '/* CombineCSS Warning: Skipped duplicate import of ' . htmlspecialchars($file_rel) . ' */' . "\n";
}
if (!is_readable($full_path)) {
trigger_error('CombineCSS: Could not read file: ' . $full_path, E_USER_WARNING);
return '/* CombineCSS Error: Could not read ' . htmlspecialchars($file_rel) . ' */' . "\n";
}
$this->processed_files[$real_path] = true; // Mark as processed
$content = file_get_contents($full_path);
if ($content === false) {
trigger_error('CombineCSS: Failed to get contents of: ' . $full_path, E_USER_WARNING);
return '/* CombineCSS Error: Failed read for ' . htmlspecialchars($file_rel) . ' */' . "\n";
}
// 1. Process @import rules first
$content = $this->processImports($content, $file_rel);
// 2. Fix relative url() paths
$content = $this->processUrls($content, $file_rel);
// Remove this file from processed list *after* processing its children
// Allows the same file to be imported via different paths if needed, though generally avoided
// unset($this->processed_files[$real_path]);
return $content;
}
/**
* Finds and processes @import rules within CSS content.
* @return string CSS content with local imports replaced.
*/
private function processImports(string $content, string $file_rel): string {
$regex = '/@import\s+(?:url\(\s*(?:(["\']?)([^)"\'\s]+)\1?)\s*\)|(["\'])([^"\']+)\3)\s*([^;]*)?;/i';
return preg_replace_callback($regex, function ($matches) use ($file_rel) {
$import_statement = $matches[0];
$url = !empty($matches[2]) ? trim($matches[2]) : trim($matches[4]);
$media_query = isset($matches[5]) ? trim($matches[5]) : '';
// --- Conditions to PRESERVE @import ---
// 1. External URL
if (str_contains($url, '//') || str_starts_with($url, 'http:') || str_starts_with($url, 'https:')) {
// Add unique preserved imports
if (strpos($this->preserved_imports, $import_statement) === false) {
$this->preserved_imports .= $import_statement . "\n";
}
return ''; // Remove from current content
}
// 2. Media Query is present
if (!empty($media_query)) {
// Resolve the path relative to the *current* file for the preserved import
$import_file_rel = $this->resolvePath(dirname($file_rel), $url);
$preserved_import_rule = '@import url("' . htmlspecialchars($import_file_rel) . '") ' . $media_query . ';';
if (strpos($this->preserved_imports, $preserved_import_rule) === false) {
$this->preserved_imports .= $preserved_import_rule . "\n";
}
return ''; // Remove from current content
}
// --- Condition to INLINE @import ---
// Local import without media query
$import_file_rel = $this->resolvePath(dirname($file_rel), $url);
$import_full_path = $this->base_dir . '/' . $import_file_rel;
if (!file_exists($import_full_path)) {
trigger_error('CombineCSS: Imported file not found: ' . $import_full_path . ' (referenced in ' . $file_rel . ')', E_USER_WARNING);
return '/* CombineCSS Error: Import not found: ' . htmlspecialchars($url) . ' */';
}
// Recursively process the imported file
return $this->processFile($import_file_rel);
}, $content);
}
$this->content = file_get_contents($this->full_path);
$this->content = \cssmin::minify($this->content);
/**
* Finds and fixes relative url() paths within CSS content.
* @return string CSS content with url() paths fixed.
*/
private function processUrls(string $content, string $file_rel): string {
$regex = '/url\(\s*(["\']?)(?!(?:["\']?(?:(?:[a-z]+:)?\/\/|\/|data:|#)))([^)"\']+)\1\s*\)/i';
$this->CSS_Import();
$this->CSS_FixUrls();
}
return preg_replace_callback($regex, function ($matches) use ($file_rel) {
$original_url = trim($matches[2]);
$quote = $matches[1]; // Preserve original quote style
// Resolve the path relative to the *current* file's directory
$absolute_path = $this->resolvePath(dirname($file_rel), $original_url);
// Return the corrected url() statement with an absolute path from base_dir
return 'url(' . $quote . '/' . ltrim($absolute_path,'/') . $quote . ')';
}, $content);
}
/**
* Include the css from @imported css
*
* Will include the css from these
* @import "../styles.css";
* @import url("../styles.css");
* @import styles.css;
*
*
* Will preserve the @import rule for these
* @import "styles.css" screen,tv;
* @import url('http://ajax.googleapis.com/ajax/libs/jqueryui/1.10.2/themes/smoothness/jquery-ui.css.css');
*
*/
public function CSS_Import($offset=0){
global $dataDir;
/**
* Resolves a relative path or URL against a base directory path.
* Handles "../", "./", and ensures the path is relative to the base_dir.
* @return string The canonicalized path relative to $this->base_dir.
*/
private function resolvePath(string $base_path, string $relative_path): string {
// Normalize base path (remove trailing '.', ensure it's a dir)
if ($base_path === '.') {
$base_path = '';
} else {
$base_path = rtrim($base_path, '/');
}
$pos = strpos($this->content,'@import ',$offset);
if( !is_numeric($pos) ){
return;
}
$replace_start = $pos;
$pos += 8;
// If relative path is already absolute (shouldn't happen with URL regex but check anyway)
if (str_starts_with($relative_path, '/')) {
return ltrim($relative_path, '/');
}
$replace_end = strpos($this->content,';',$pos);
if( !is_numeric($replace_end) ){
return;
}
$full_path = $base_path ? $base_path . '/' . $relative_path : $relative_path;
$import_orig = substr($this->content,$pos,$replace_end-$pos);
$import_orig = trim($import_orig);
$replace_len = $replace_end-$replace_start+1;
// Canonicalize the path (resolve ../ and ./) - Stack-based approach
$parts = explode('/', $full_path);
$absolutes = [];
foreach ($parts as $part) {
if ('.' == $part || '' == $part) {
continue;
}
if ('..' == $part) {
array_pop($absolutes);
} else {
$absolutes[] = $part;
}
}
return implode('/', $absolutes);
}
//get url(..)
$media = '';
if( substr($import_orig,0,4) == 'url(' ){
$end_url_pos = strpos($import_orig,')');
$import = substr($import_orig,4, $end_url_pos-4);
$import = trim($import);
$import = trim($import,'"\'');
$media = substr($import_orig,$end_url_pos+1);
}elseif( $import_orig[0] == '"' || $import_orig[0] == "'" ){
$end_url_pos = strpos($import_orig,$import_orig[0],1);
$import = substr($import_orig,1, $end_url_pos-1);
$import = trim($import);
$media = substr($import_orig,$end_url_pos+1);
}
/**
* Public access to the list of processed files (absolute paths).
* Useful for debugging or cache invalidation.
* @return array
*/
public function getProcessedFiles(): array {
return array_keys($this->processed_files);
}
// keep @import when the file is on a remote server?
if( strpos($import,'//') !== false ){
$this->imports .= substr($this->content, $replace_start, $replace_len );
$this->content = substr_replace( $this->content, '', $replace_start, $replace_len);
$this->CSS_Import($offset);
return;
}
//if a media type is set, keep the @import
$media = trim($media);
if( !empty($media) ){
$import = \gp\tool::GetDir(dirname($this->file).'/'.$import);
$import = $this->ReduceUrl($import);
$this->imports .= '@import url("'.$import.'") '.$media.';';
$this->content = substr_replace( $this->content, '', $replace_start, $replace_len);
$this->CSS_Import($offset);
return;
}
//include the css
$full_path = false;
if( $import[0] != '/' ){
$import = dirname($this->file).'/'.$import;
$import = $this->ReduceUrl($import);
}
$full_path = $dataDir.$import;
if( file_exists($full_path) ){
$temp = new \gp\tool\Output\CombineCss($import);
$this->content = substr_replace($this->content,$temp->content,$replace_start,$replace_end-$replace_start+1);
$this->imported[] = $full_path;
$this->imported = array_merge($this->imported,$temp->imported);
$this->imports .= $temp->imports;
$this->CSS_Import($offset);
return;
}
$this->CSS_Import($pos);
}
public function CSS_FixUrls($offset=0){
$pos = strpos($this->content,'url(',$offset);
if( !is_numeric($pos) ){
return;
}
$pos += 4;
$pos2 = strpos($this->content,')',$pos);
if( !is_numeric($pos2) ){
return;
}
$url = substr($this->content,$pos,$pos2-$pos);
$this->CSS_FixUrl($url,$pos,$pos2);
return $this->CSS_FixUrls($pos2);
}
public function CSS_FixUrl($url,$pos,$pos2){
global $dataDir;
$url = trim($url);
$url = trim($url,'"\'');
if( empty($url) ){
return;
}
//relative url
if( $url[0] == '/' ){
return;
}elseif( strpos($url,'://') > 0 ){
return;
}elseif( preg_match('/^data:/i', $url) ){
return;
}
//use a relative path so sub.domain.com and domain.com/sub both work
$replacement = \gp\tool::GetDir(dirname($this->file).'/'.$url);
$replacement = $this->ReduceUrl($replacement);
$replacement = '"'.$replacement.'"';
$this->content = substr_replace($this->content,$replacement,$pos,$pos2-$pos);
}
/**
* Canonicalize a path by resolving references to '/./', '/../'
* Does not remove leading "../"
* @param string path or url
* @return string Canonicalized path
*
*/
public function ReduceUrl($url){
$temp = explode('/',$url);
$result = array();
foreach($temp as $i => $path){
if( $path == '.' ){
continue;
}
if( $path == '..' ){
for($j=$i-1;$j>0;$j--){
if( isset($result[$j]) ){
unset($result[$j]);
continue 2;
}
}
}
$result[$i] = $path;
}
return implode('/',$result);
}
}
/**
* Public access to the preserved @import rules.
* @return string
*/
public function getPreservedImports(): string {
return $this->preserved_imports;
}
}

View file

@ -5,11 +5,25 @@ $GP_MENU_ELEMENTS = 'Bootstrap5_menu';
/**
* Generates menu link elements compatible with Bootstrap 5.2 navbars/dropdowns.
*
* @param string $node The HTML node type being processed (expects 'a').
* @param array $attributes Associative array of link attributes:
* 'href_text' => The URL (value for href attribute).
* 'attr' => String containing existing HTML attributes (e.g., 'id="my-link"').
* 'label' => The visible text label of the link.
* 'title' => The title attribute text (tooltip).
* 'class' => An array of classes assigned by the menu system (used to check for 'dropdown-toggle').
* @param int $level The depth level of the menu item (0 for top level).
* @param mixed $menu_id The ID of the menu being processed.
* @param int $item_position The position of the item within its level.
*
* @return string|null The generated HTML for the <a> tag, or null if $node is not 'a'.
*/
function Bootstrap5_menu($node, $attributes, $level, $menu_id, $item_position){
GLOBAL $GP_MENU_LINKS;
if( $node == 'a' ){
// --- Add Bootstrap specific classes ---
$strpos_class = strpos($attributes['attr'], 'class="');
$add_class = ( $level > 0 ) ? "dropdown-item" : "nav-link";
@ -17,9 +31,9 @@ function Bootstrap5_menu($node, $attributes, $level, $menu_id, $item_position){
$attributes['attr'] .= ' class="' . $add_class . '"';
$strpos_class = strpos($attributes['attr'], 'class="');
} else {
$attributes['attr'] = substr($attributes['attr'], 0, $strpos_class + 7)
. $add_class . ' '
. substr($attributes['attr'], $strpos_class + 7);
$attributes['attr'] = substr($attributes['attr'], 0, $strpos_class + 7) // Part before classes
. $add_class . ' ' // The new class + space
. substr($attributes['attr'], $strpos_class + 7); // The rest of the original classes
}
// Ensure 'title' is included if it might be used in $GP_MENU_LINKS
@ -34,7 +48,8 @@ function Bootstrap5_menu($node, $attributes, $level, $menu_id, $item_position){
$format = '<a {$attr} href="{$href_text}">{$label}</a>';
}
}
// --- End Determine the link format ---
return str_replace( $search, $attributes, $format );
}
return null;