Ich stelle diese Frage im Profi-Forum weil ich hoffe dass sie für Profis anspruchsvoll genug ist. Ich habe diese Frage auch schonmal anderswo gestellt, aber da das hier ein auf PHP spezialisertes Forum ist sind meine Chancen auf Antworten vermutlich besser
Gleich eines vorweg: Dier hier ist ein etwas längerer Beitrag.
--- (so, jetzt geht's los)
Ich arbeite schon seit längerem an einem PHP-CSS Parser und Optimierer, wer sich den mal angucken möchte kann das tun.
Nun möchte ich für die nächste Version einen Fehler beheben, dieser ist allerdings etwas komplizierter als er aussieht.
Wenn man dem Optimierer jetzt folgendes zum Verarbeiten gibt...
...macht er daraus folgendes:
Wie alle CSS-Kenner schnell feststellen werden, stimmt das so nicht. Was der Optimierer hier versucht, ist das Zusammenfassen von Selektoren die die gleichen Eigenschaften (Deklarationen) haben.
Richtig wäre es natürlich so:
Um dieses Problem zu umschiffen könnte man jetzt die Option zum Zusammenfassen von Eigenschaften einfach ausschalten. Das ist natürlich nicht Sinn der Sache.
Um das Problem also richtig zu beheben, müsste der Optimierer zuerst die Selektoren zerlegen und anschließend wieder richtig zusammenfassen. Das Zerlegen stellt kein Problem dar, wohl aber das Zusammenfassen.
Kleiner Hinweis für alle nicht CSS-Experten: Selektoren können mit
nicht zuverlässig zerlegt werden, da in Selektoren auch einfache Strings vorkommen können (z.B. in Zusammenhang mit Attributselektoren).
Um das CSS dann sinnvoll zusammenzufassen habe ich mir gedacht, dass zuersteinmal die Größe der Gemeinsamkeiten mehrerer Selektoren herausgefunden werden muss (um "intelligent" das zu optimieren was den größten "Gewinn" bringt) und welche Übereinstimmungen das genau sind.
Dafür habe ich auch schon eine Funktion geschrieben, die zwar fast perfekt funktioniert, allerdings schnell jegliche Zeit- und Speicherlimits überschreitet (abhängig von der Größe des CSS Codes). Was ich also jetzt ganz gerne hätte wäre ein Vorschlag wie man meine Funktion deutlich beschleunigt oder einen komplett neuen Ansatz von mir aus auch.
Jetzt erstmal die konkreten Informationen:
Struktur der Daten
Die Daten in denen das CSS gespeichert wird, sehen etwa so aus:
ZAHL ist deswegen nötig, weil Eigenschaften auch mehrmals vorkommen dürfen sollen. Das ermöglicht das "beachten" einiger CSS-Hacks. Normalerweise würden die sich sonst einfach in Luft auflösen.
"standard" steht für keinen Medium-Typ.
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).
Wer alles 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).
Hier jetzt die Funktion mit Kommentar:
Das hier wird nacher auf TRUE gesetzt falls irgendetwas zusammengefasst wurde
Zuerstmal geht die Funktion hier alle Medientypen (wem das nichts sagt, gucke hier: http://www.w3.org/TR/CSS21/media.html) 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),
Anschließend geht sie alle Selektoren durch. Die Werte der Arrays werden immer in $val[Buchstable] gespeichert, je nach Verschachtelungstiefe.
Danach folgen die Nummern der Eigenschaften und die Eigenschaften selbst.
Jetzt geht die Funktion innerhalb des gleichen Mediums die Selektoren durch (wieder von vorne anfangend), da wir ja alle Kombinationen durchlaufen müssen.
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: mit explode() geht es nicht!).
Hier wird also die serialisierte Form der Selektoren 1 und 2 in umgekehrter Reichenfolge gespeichert, womit wir...
...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.
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).
Hier geht die Funktion jetzt auch durch die Eigenschaften des 2. (Vergleichs-)Selektors.
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.
Um den Speicherverbrauch wenigstens ansatzweise in den Griff zu bekommen, löschen wir hier den Eintrag der Selektoren in $diff und $diff_c wenn es keine Übereinstimmungen hab.
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.
Wie der Kommentar schon sagt werden die größten Übereinstimmungen nach oben gesetzt damit möglichst "intelligent" optimiert wird.
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.
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]);.
Und wenn dann wie im obigen Beispiel gar keine Eigenschaften in einem Selektor mehr drin sind, dann wird er komplett gelöscht.
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
hier alles aus $diff_c gelöscht wird was zum Zusammenfassen nicht mehr zur Verfügung steht.
Hier noch der Rückgabewert. Wichtig für die praktische Anwendung der Funktion die so aussieht:
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
bei rauskommt. Aber das ist erstmal nebensächlich.
Jetzt zum Abschluss nochmal ein kleines Beispiel für die Funktionsweise der Funktion:
Das wird optimiert:
$diff sieht dann beim ersten Durchlauf so aus:
$diff_c so:
Das Ergebnis ist:
Letzte Anmerkung: Die Variable heißt hier $diff. Das ist nicht ganz logisch, weil ich eigentlich nicht nach Unterschieden (difference) sondern Gemeinsamkeiten (intersection) suche. Daran habe ich beim programmieren nicht gedacht, aber das dürfte erstmal keine große Rolle spielen.
Ich hoffe auf zahlreiche Vorschläge
Gleich eines vorweg: Dier hier ist ein etwas längerer Beitrag.
--- (so, jetzt geht's los)
Ich arbeite schon seit längerem an einem PHP-CSS Parser und Optimierer, wer sich den mal angucken möchte kann das tun.
Nun möchte ich für die nächste Version einen Fehler beheben, dieser ist allerdings etwas komplizierter als er aussieht.
Wenn man dem Optimierer jetzt folgendes zum Verarbeiten gibt...
Code:
b {color:red;} c,b {color:green;} a {color:red;}
Code:
c,b { color:green } b,a { color:red }
Richtig wäre es natürlich so:
Code:
b,c {color:green;} a {color:red;}
Um das Problem also richtig zu beheben, müsste der Optimierer zuerst die Selektoren zerlegen und anschließend wieder richtig zusammenfassen. Das Zerlegen stellt kein Problem dar, wohl aber das Zusammenfassen.
Kleiner Hinweis für alle nicht CSS-Experten: Selektoren können mit
Code:
explode(',',$selectoren);
Um das CSS dann sinnvoll zusammenzufassen habe ich mir gedacht, dass zuersteinmal die Größe der Gemeinsamkeiten mehrerer Selektoren herausgefunden werden muss (um "intelligent" das zu optimieren was den größten "Gewinn" bringt) und welche Übereinstimmungen das genau sind.
Dafür habe ich auch schon eine Funktion geschrieben, die zwar fast perfekt funktioniert, allerdings schnell jegliche Zeit- und Speicherlimits überschreitet (abhängig von der Größe des CSS Codes). Was ich also jetzt ganz gerne hätte wäre ein Vorschlag wie man meine Funktion deutlich beschleunigt oder einen komplett neuen Ansatz von mir aus auch.
Jetzt erstmal die konkreten Informationen:
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
"standard" steht für keinen Medium-Typ.
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).
Wer alles 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).
Hier jetzt die Funktion mit Kommentar:
Code:
function merge_selectors(&$css) { $return = FALSE;
Code:
foreach($css as $medium => $vali) { $diff = array(); $diff_c = array();
Code:
foreach($vali as $selector => $valj) {
Code:
foreach($valj as $num_key => $valk) { foreach($valk as $property => $value) {
Code:
foreach($css[$medium] as $selector2 => $vall) {
Code:
$selector_combine_r = serialize(array($selector2,$selector));
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; }
Code:
$selector_combine = serialize(array($selector,$selector2)); if(!isset($diff[$selector_combine])) { $diff[$selector_combine] = 0; $diff_[$selector_combine] = array(); }
Code:
foreach($vall as $num_key2 => $valm) { foreach($valm as $property2 => $value2) {
Code:
if($property2 === $property && $value2 === $value) { $diff[$selector_combine] += (strlen($property)+strlen($value)); $diff_c[$selector_combine][][$property] = $value; }
Code:
} } if($diff[$selector_combine] === 0) { unset($diff[$selector_combine]); unset($diff_c[$selector_combine]); }
Code:
} } } }
Code:
// Move best matches to the top array_multisort($diff,SORT_NUMERIC,SORT_DESC);
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; }
Code:
foreach($diff_c[$key] as $num_key => $valj) { foreach($valj as $property => $value) { rm_subkey($property,$css[$medium][$selector],$value); } }
Code:
// If no properties are left, remove selector if(empty($css[$medium][$selector])) { unset($css[$medium][$selector]); }
Code:
// Create the new selector if matches are left if(!empty($diff_c[$key])) { $css[$medium][implode(',',$key_u)] = $diff_c[$key]; $return = TRUE; }
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]); } }
Code:
} } return $return; }
Code:
while(csspp::merge_selectors($this->css)) {}
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;(und ganz wenig Eigenschaften)}
Jetzt zum Abschluss nochmal ein kleines Beispiel für die Funktionsweise der Funktion:
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';
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 )
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 ) ) )
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 ) ) ) )
Ich hoffe auf zahlreiche Vorschläge
Kommentar