XHTMLforum

XHTMLforum (http://xhtmlforum.de/index.php)
-   Serveradministration und serverseitige Scripte (http://xhtmlforum.de/forumdisplay.php?f=80)
-   -   [PHP] Funktion zum Optimieren von CSS (http://xhtmlforum.de/showthread.php?t=36358)

Floele 03.08.2005 17:43

[PHP] Funktion zum Optimieren von CSS
 
Gleich eines vorweg: Wer sich den Beitrag komplett durchlesen will braucht ein bisschen Zeit ;)

Ich arbeite im Moment wieder an meinem CSS Optimierer, und für die nächste Version würde ich gerne einen einzigsten Fehler beheben - leider ist das etwas komplizierter als es aussieht.

Beispiel:

Code:

b {color:red;}
c,b {color:green;}
a {color:red;}

Daraus wird anschließend

Code:

c,b {
color:green
}

b,a {
color:red
}

Offensichtlich stimmt das so nicht. Um diesen Fehler zu beheben müsste das Programm erstmal alle Selektoren zerlegen (am ",") und anschließend wieder richtig zusammenfassen (oder einfach die Option zum automatischen Zusammenfassen ausschalten, aber das ist nicht Sinn der Sache). Während das Zerlegen kein Problem darstellt, ist das richtige Zusammenfassen aber problematisch. Das Programm muss nämlich erstmal überprüfen wo Übereinstimmungen vorhanden und wie groß diese sind. Anhand dieser Informationen können die Eigenschaften dann sinnvoll zusammengefasst werden.

Dafür habe ich sogar auch schon eine Funktion geschrieben (die fast perfekt funktioniert), allerdings ist diese so aufwändig, dass damit schnell alle Zeit- und Speicherlimits überschritten werden (je nach Größe des Stylesheets). Und deswegen bitte ich hier um ein bisschen Hilfe, vielleicht hat ja jemand eine Idee wie man das sinnvoll machen kann (bzw. wie man meiner Funktion ein bisschen Feuer unterm Hintern macht).

Hier erstmal meine Funktion + Infos:

Struktur der Daten
Die Daten in denen das CSS gespeichert wird, sehen etwa so aus:
Code:

$css['standard']['a'][]['color'] = 'red';
$css['standard']['a'][]['size'] = '1em';
$css['standard']['b'][]['size'] = '1em';
// also $css[MEDIUM][SELEKTOR][ZAHL][EIGENSCHAFT] = WERT

Bisherhige Funktion
Das hier ist meine Funktion (das ultimative foreach-Chaos), hoffentlich gut genug kommentiert. Vorraussetzung ist, dass beim ersten Durchlauf keine Selektoren wie "a,b" oder "a,b#id,c" etc. vorhanden sind, bzw. diese zerlegt sind (dafür kann der Rest meines CSS Parsers garantieren).

Code:

function merge_selectors(&$css)
{
        $return = FALSE;

Das hier wird nacher auf TRUE gesetzt falls irgendetwas zusammengefasst wurde
Code:

        foreach($css as $medium => $vali)
        {
                $diff = array();
                $diff_c = array();

Zuerstmal geht die Funktion hier alle Medientypen durch und legt die Variablen $diff (Liste der Gemeinsamkeiten, $diff[Selektoren] => Größe der Gemeinsamkeit, wird aus der Zeichenlänge der gemeinsamen Eigenschaften und deren Werten bestimmt) und $diff_c an (speichert anstatt der Gemeinsamkeitsgröße die gemeinsamen Eigenschaften+Werte),
Code:

       
                foreach($vali as $selector => $valj)
                {

Anschließend geht sie alle Selektoren durch. Die Werte der Arrays werden immer in $val[Buchstable] gespeichert, je nach Verschachtelungstiefe.
Code:

                        foreach($valj as $num_key => $valk)
                        {
                                foreach($valk as $property => $value)
                                {

Danach folgen die Nummern der Eigenschaften und die Eigenschaften selbst.
Code:

                                        foreach($css[$medium] as $selector2 => $vall)
                                        {

Jetzt geht die Funktion innerhalb des gleichen Mediums die Selektoren durch (wieder von vorne anfangend), da wir ja alle Kombinationen durchlaufen müssen.
Code:

                                                $selector_combine_r = serialize(array($selector2,$selector));
So, jetzt wird's erstmal komisch. Da $diff bzw. $diff_c ja die Selektoren speichern, für die die Gemeinsamkeiten gelten kommen diese sinnvollerweise in den Schlüssel. Da es aber auch logischerweise zwei Selektoren sind, müssen diese so gespeichert werden, dass man sie nacher wieder mühelos trennen kann. Das wäre bei einem Array der Fall ( $dif[array('a','b')]). Das geht so natürlich nicht, also wird das array serialisiert. Damit überlasse ich PHP das Trennen der Selektoren und muss nicht irgendeine selbstgebaute Funktion die beiden Selektoren wieder trennen lassen (wir erinnern uns: So geht es nicht explode(',',$selektoren), da das "," auch als String in den Selektoren vorkommen könnte).

Hier wird also die serialisierte Form der Selektoren 1 und 2 in umgekehrter Reichenfolge gespeichert, womit wir...
Code:

                                               
                                                // Prevent duplicate comparisions (a-b, b-a) and self-comparisons (a-a)
                                                if($selector2 === $selector || isset($diff[$selector_combine_r]))
                                                {
                                                        continue;       
                                                }

...hier die Schleife überspringen falls wir für die gleichen Selektoren (nur in umgekehrter Reihenfolge) schon nach Übereinstimmungen gesucht haben. Außerdem überspringen wir alle Vergleiche eines Selektors mit sich selbst.
Code:

                                               
                                                $selector_combine = serialize(array($selector,$selector2));
                                                if(!isset($diff[$selector_combine]))
                                                {
                                                        $diff[$selector_combine] = 0;
                                                        $diff_[$selector_combine] = array();
                                                }

Hier legen wir jetzt, falls noch nicht vorhanden, die Speicherplätze für die Größe der Gemeinsamkeiten und für die Gemeinsamkeiten selbst an (und zwar mit einem Schlüssel, der die beiden Selektoren die wir grade vergleichen enthält).
Code:

                                                                               
                                                foreach($vall as $num_key2 => $valm)
                                                {
                                                        foreach($valm as $property2 => $value2)
                                                        {

Hier geht die Funktion jetzt auch durch die Eigenschaften des 2. (Vergleichs-)Selektors.
Code:

                                                                if($property2 === $property && $value2 === $value)
                                                                {
                                                                        $diff[$selector_combine] += (strlen($property)+strlen($value));
                                                                        $diff_c[$selector_combine][][$property] = $value;
                                                                }

Wenn Eigenschaft und Wert zwischen beiden Selektoren übereinstimmt, speichern wir die Zeichenlänge dieser (damit wir nacher wissen, wo die Übereinstimmungen am größten sind, diese müssen dann zuerst zusammengefasst werden) sowie Eigenschaft+Wert selbst ab.
Code:

                                                        }
                                                }
                                               
                                                if($diff[$selector_combine] === 0)
                                                {
                                                        unset($diff[$selector_combine]);
                                                        unset($diff_c[$selector_combine]);
                                                }

Um den Speicherverbrauch wenigstens ansatzweise in den Griff zu bekommen, löschen wir hier den Eintrag der Selektoren in $diff und $diff_ wenn es keine Übereinstimmungen hab.
Code:

                                        }
                                }
                        }
                }

So, wo sind wir hier? Genau, am Ende eines Mediums (alle Selektoren die nicht in einem @media drin sind, sind in 'standard'). Das heißt jetzt, nachdem wir wissen was zusammengefasst werden kann, fangen wir an zusammenzufassen. Das nachfolgende wird also für jeden Mediumtyp ausgeführt.
Code:

                // Move best matches to the top
                array_multisort($diff,SORT_NUMERIC,SORT_DESC);

Wie der Kommentar schon sagt werden die größten Übereinstimmungen nach oben gesetzt damit möglichst "intelligent" optimiert wird.
Code:

       
                foreach($diff as $key => $value)
                {
                        $key_u = unserialize($key);
                        foreach($key_u as $selector)
                        {
                                // If there are no matches or if selector is already dissolved
                                if(!isset($diff_c[$key]) || !isset($css[$medium][$selector]))
                                {
                                        continue;
                                }

Jetzt geht die Funktion die Variable $diff mit den Unterschieden durch. Dabei wird auch jeder Selektor einzeln behandelt, da wir andere Kombinationen mit diesem Selektor aus $diff entfernen müssen. Wenn wir nämlich einen Selektor mit einem anderen kombinieren, kann es ja sein, dass andere Kombinationen die vorher gefunden wurden gar nicht mehr möglich sind.
Code:

                               
                                foreach($diff_c[$key] as $num_key => $valj)
                                {
                                        foreach($valj as $property => $value)
                                        {
                                                rm_subkey($property,$css[$medium][$selector],$value);
                                        }
                                }

Hier löschen wir jetzt alle Eigenschaften eines Selektors, die in Kürze mit einem anderen Selektor zusammengefasst werden. Wenn a{color:red;} also mit b{color:red;} kombiniert wird, muss color:red; in beiden Selektoren gelöscht werden. rm_subkey ist selbstgebaut und funktioniert wie unset($css[$medium][$selector][?][$property]);.
Code:

                               
                                // If no properties are left, remove selector
                                if(empty($css[$medium][$selector]))
                                {
                                        unset($css[$medium][$selector]);
                                }

Und wenn dann wie im obigen Beispiel gar keine Eigenschaften in einem Selektor mehr drin sind, dann wird er komplett gelöscht.
Code:

                               
                                // Create the new selector if matches are left
                                if(!empty($diff_c[$key]))
                                {
                                        $css[$medium][implode(',',$key_u)] = $diff_c[$key];
                                        $return = TRUE;
                                }

Hier überprüft die Funktion nochmal ob dem Selektor überhaupt noch Eigenschaften die kombiniert werden können zur Verfügung stehen (und fügt dann den neuen Selektor ein. Zu beachten ist noch dass TRUE zurückgegeben wird falls eine Zusammenfassung möglich war), weil
Code:

                        }
                       
                        // Go through diff_c and remove no-more-shareable properties
                        // and possibly the complete entry
                        foreach($diff_c as $keyi => $valuei)
                        {
                                $selectors = unserialize($keyi);
                                if(in_array($key_u[0],$selectors) || in_array($key_u[1],$selectors))
                                {
                                        unset($diff_c[$keyi]);
                                }
                        }

hier alles aus $diff_c gelöscht wird was zum Zusammenfassen nicht mehr zur Verfügung steht.
Code:

                }
        }
        return $return;
}

Hier noch der Rückgabewert. Wichtig für die praktische Anwendung der Funktion die so aussieht:

Code:

while(csspp::merge_selectors($this->css)) {}
Das heißt, dass diese ohnehin schon aufwändige Optimierung so lange ausgeführt wird, bis es nirgendwo mehr was zum zusammenfassen gibt. Falls es nämlich nicht schon aufgefallen sein sollte, es werden immer nur 2 Selektoren zusammengefasst, allerdings sollte das mit 3 oder mehr Selektoren ebenfalls möglich sein.

Das einzigste was die Funktion noch beachten müsste ist, dass ein gewisses Verhältnis zwischen Größe der optimierten Eigenschaften und Anzahl der Selektoren erhalten bleibt (damit nacher nicht sowas wie
Code:

ein,ganz,langer,langer,................................,selektor{size:1em;}
bei rauskommt. Aber das ist erstmal nebensächlich.

Wer alles nochmal im Zusammenhang sehen möchte:
http://cdburnerxp.se/cssparse/css_parser.txt

Die Funktion ist da komplett drin, wer will kann den Parser damit mal durchlaufen lassen (in der Klasse die Funktion parse($string) [diese "Superoptimierung wird automatisch mit ausgeführt), das Ergebnis als Array befindet sich danach in der Variable $css).

Ich hoffe auf zahlreiche Antworten :D



Edit: Vielleicht nochmal ein kleines Beispiel damit ihr besser nachvollziehen könnt was die Funktion macht.

Das wird optimiert:
Code:

$css['standard']['a'][]['margin'] = '0';
$css['standard']['a'][]['color'] = 'red';
$css['standard']['a'][]['text'] = 'none';
$css['standard']['a'][]['text'] = '!important';

$css['standard']['b'][]['text'] = 'none';
$css['standard']['b'][]['color'] = 'red';

$css['standard']['d'][]['color'] = 'red';
$css['standard']['e'][]['margin'] = '2';

$diff sieht dann beim ersten Durchlauf so aus:

Code:

Array
(
    [a:2:{i:0;s:1:"a";i:1;s:1:"b";}] => 16
    [a:2:{i:0;s:1:"a";i:1;s:1:"d";}] => 8
    [a:2:{i:0;s:1:"b";i:1;s:1:"d";}] => 8
)

$diff_c so:

Code:

Array
(
    [a:2:{i:0;s:1:"a";i:1;s:1:"b";}] => Array
        (
            [0] => Array
                (
                    [color] => red
                )

            [1] => Array
                (
                    [text] => none
                )

        )

    [a:2:{i:0;s:1:"a";i:1;s:1:"d";}] => Array
        (
            [0] => Array
                (
                    [color] => red
                )

        )

    [a:2:{i:0;s:1:"b";i:1;s:1:"d";}] => Array
        (
            [0] => Array
                (
                    [color] => red
                )

        )

)

Das Ergebnis ist:

Code:

Array
(
    [standard] => Array
        (
            [a] => Array
                (
                    [0] => Array
                        (
                            [margin] => 0
                        )

                    [3] => Array
                        (
                            [text] => !important
                        )

                )

            [e] => Array
                (
                    [0] => Array
                        (
                            [margin] => 2
                        )

                )

            [a,b] => Array
                (
                    [1] => Array
                        (
                            [text] => none
                        )

                )

            [d,a,b] => Array
                (
                    [0] => Array
                        (
                            [color] => red
                        )

                )

        )

)


Lestat 13.08.2005 11:08

Hi!

Schlag das ganze doch als PEAR Paket vor? Schon daran gedacht?

http://pear.php.net/

Wenn du hier schaust, dann siehst du das es einen XML_Beautifier und einen PHP_Beautifier bereits gibt.

Ein CSS_Beautifier fehlt noch! Ich bin gespannt was du von meinem Vorschlag hälst ;)

mfg
Lestat

Floele 13.08.2005 11:31

Ich kann ja mal drüber nachdenken...aber erstmal muss das Ding fertig werden ;)


Alle Zeitangaben in WEZ +2. Es ist jetzt 17:02 Uhr.

Powered by vBulletin® Version 3.8.11 (Deutsch)
Copyright ©2000 - 2024, vBulletin Solutions, Inc.

© Dirk H. 2003 - 2023