Wie @extend funktioniert
Gepostet am 23. November 2013 von Natalie Weizenbaum
Dies wurde ursprünglich als ein Gist veröffentlicht.
Aaron Leung arbeitet an libsass und fragte sich, wie @extend in der Ruby-Implementierung von Sass implementiert wird. Anstatt ihm nur zu sagen, dachte ich, ich schreibe ein öffentliches Dokument darüber, damit jeder, der Sass portiert oder einfach nur neugierig ist, wie es funktioniert, es sehen kann.
Beachten Sie, dass diese Erklärung auf vielfältige Weise vereinfacht ist. Sie soll die komplexesten Teile einer grundlegend korrekten @extend-Transformation erklären, lässt jedoch zahlreiche Details aus, die für eine vollständige Sass-Kompatibilität wichtig sind. Dies sollte als Erläuterung des Fundaments für @extend betrachtet werden, auf dem eine vollständige Unterstützung aufgebaut werden kann. Für ein vollständiges Verständnis von @extend gibt es keinen Ersatz, als den Ruby Sass-Code und seine Tests zu konsultieren.
Dieses Dokument setzt Vertrautheit mit der Terminologie von Selektoren voraus, wie sie in der Spezifikation Selectors Level 4 definiert ist. Im gesamten Dokument werden Selektoren austauschbar mit Listen oder Mengen ihrer Komponenten behandelt. Beispielsweise kann ein komplexer Selektor als Liste von zusammengesetzten Selektoren oder als Liste von Listen einfacher Selektoren behandelt werden.
PrimitivePrimitive Permalien
Im Folgenden sind eine Reihe von primitiven Objekten, Definitionen und Operationen aufgeführt, die für die Implementierung von @extend notwendig sind. Die Implementierung dieser ist als Übung für den Leser überlassen.
-
Ein Selektorobjekt ist offensichtlich notwendig, da
@extendalles mit Selektoren zu tun hat. Selektoren müssen gründlich und semantisch analysiert werden. Es ist notwendig, dass die Implementierung viel über die Bedeutung der verschiedenen Formen von Selektoren weiß. -
Eine benutzerdefinierte Datenstruktur, die ich "Subset-Map" nenne, ist ebenfalls notwendig. Eine Subset-Map hat zwei Operationen:
Map.set(Set, Object)undMap.get(Set) => [Object]. Die erste ordnet einem Set von Schlüsseln in der Map einen Wert zu. Die zweite sucht alle Werte nach, die mit *Teilmengen* eines Sets von Schlüsseln assoziiert sind. Zum Beispielmap.set([1, 2], 'value1') map.set([2, 3], 'value2') map.set([3, 4], 'value3') map.get([1, 2, 3]) => ['value1', 'value2'] -
Ein Selektor
S1ist ein "Superselektor" eines SelektorsS2, wenn jedes vonS2gematchte Element auch vonS1gematcht wird. Zum Beispiel ist.fooein Superselektor von.foo.bar,aist ein Superselektor vondiv aund*ist ein Superselektor von allem. Das Gegenteil eines Superselektors ist ein "Subselektor". -
Eine Operation
unify(Compound Selector, Compound Selector) => Compound Selector, die einen Selektor zurückgibt, der genau die von beiden Eingangsselektoren gematchten Elemente matcht. Zum Beispiel gibtunify(.foo, .bar).foo.barzurück. Dies muss nur für zusammengesetzte oder einfachere Selektoren funktionieren. Diese Operation kann fehlschlagen (z.B.unify(a, h1)), in diesem Fall sollte sienullzurückgeben. -
Eine Operation
trim([Selector List]) => Selector List, die komplexe Selektoren entfernt, die Subselektoren anderer komplexer Selektoren in der Eingabe sind. Sie nimmt die Eingabe als mehrere Selektorlisten und prüft nur auf Subselektoren über diese Listen hinweg, da der vorherige@extend-Prozess keine Subselektoren innerhalb einer Liste erzeugt. Wenn ihr beispielsweise[[a], [.foo a]]übergeben wird, gibt sie[a]zurück, da.foo aein Subselektor vonaist. -
Eine Operation
paths([[Object]]) => [[Object]], die eine Liste aller möglichen Pfade durch eine Liste von Auswahlmöglichkeiten für jeden Schritt zurückgibt. Zum Beispiel gibtpaths([[1, 2], [3], [4, 5, 6]])[[1, 3, 4], [1, 3, 5], [1, 3, 6], [2, 3, 4], [2, 3, 5], [2, 3, 6]]zurück.
Der AlgorithmusDer Algorithmus Permalien
Der @extend-Algorithmus erfordert zwei Durchläufe: einen, um die im Stylesheet deklarierten @extends aufzuzeichnen, und einen weiteren, um Selektoren mithilfe dieser @extends zu transformieren. Dies ist notwendig, da @extends auch Selektoren früher im Stylesheet beeinflussen können.
AufzeichnungsdurchlaufAufzeichnungsdurchlauf Permalien
In Pseudocode kann dieser Durchlauf wie folgt beschrieben werden
let MAP be an empty subset map from simple selectors to (complex selector, compound selector) pairs
for each @extend in the document:
let EXTENDER be the complex selector of the CSS rule containing the @extend
let TARGET be the compound selector being @extended
MAP.set(TARGET, (EXTENDER, TARGET))
TransformationsdurchlaufTransformationsdurchlauf Permalien
Der Transformationsdurchlauf ist komplizierter als der Aufzeichnungsdurchlauf. Er wird im Pseudocode unten beschrieben
let MAP be the subset map from the recording pass
define extend_complex(COMPLEX, SEEN) to be:
let CHOICES be an empty list of lists of complex selectors
for each compound selector COMPOUND in COMPLEX:
let EXTENDED be extend_compound(COMPOUND, SEEN)
if no complex selector in EXTENDED is a superselector of COMPOUND:
add a complex selector composed only of COMPOUND to EXTENDED
add EXTENDED to CHOICES
let WEAVES be an empty list of selector lists
for each list of complex selectors PATH in paths(CHOICES):
add weave(PATH) to WEAVES
return trim(WEAVES)
define extend_compound(COMPOUND, SEEN) to be:
let RESULTS be an empty list of complex selectors
for each (EXTENDER, TARGET) in MAP.get(COMPOUND):
if SEEN contains TARGET, move to the next iteration
let COMPOUND_WITHOUT_TARGET be COMPOUND without any of the simple selectors in TARGET
let EXTENDER_COMPOUND be the last compound selector in EXTENDER
let UNIFIED be unify(EXTENDER_COMPOUND, COMPOUND_WITHOUT_TARGET)
if UNIFIED is null, move to the next iteration
let UNIFIED_COMPLEX be EXTENDER with the last compound selector replaced with UNIFIED
with TARGET in SEEN:
add each complex selector in extend_complex(UNIFIED_COMPLEX, SEEN) to RESULTS
return RESULTS
for each selector COMPLEX in the document:
let SEEN be an empty set of compound selectors
let LIST be a selector list comprised of the complex selectors in extend_complex(COMPLEX, SEEN)
replace COMPLEX with LIST
Ein scharfer Leser wird eine undefinierte Funktion bemerkt haben, die in diesem Pseudocode verwendet wird: weave. weave ist wesentlich komplizierter als die anderen primitiven Operationen, daher wollte ich sie im Detail erklären.
WeaveWeave Permalien
Auf hoher Ebene ist die "weave"-Operation ziemlich einfach zu verstehen. Am besten stellt man sie sich als Erweiterung eines "klammernden Selektors" vor. Stellen Sie sich vor, Sie könnten .foo (.bar a) schreiben und es würde jedes a-Element matchen, das sowohl ein .foo-Elternelement *als auch* ein .bar-Elternelement hat. weave lässt dies geschehen.
Um dieses a-Element zu matchen, müssen Sie .foo (.bar a) in die folgende Selektorliste erweitern: .foo .bar a, .foo.bar a, .bar .foo a. Dies matcht alle möglichen Wege, wie a sowohl einen .foo- als auch einen .bar-Elternteil haben könnte. weave gibt jedoch nicht tatsächlich .foo.bar a aus; das Einbeziehen von zusammengeführten Selektoren wie diesem würde zu einer exponentiellen Ausgabegröße führen und sehr wenig Nutzen bringen.
Dieser klammernde Selektor wird an weave als Liste von komplexen Selektoren übergeben. Zum Beispiel würde .foo (.bar a) als [.foo, .bar a] übergeben. Ebenso würde (.foo div) (.bar a) (.baz h1 span) als [.foo div, .bar a, .baz h1 span] übergeben.
weave arbeitet, indem es von links nach rechts durch den klammernden Selektor geht und eine Liste aller möglichen Präfixe aufbaut und diese Liste bei jedem Auftreten einer klammernden Komponente erweitert. Hier ist der Pseudocode
let PAREN_SELECTOR be the argument to weave(), a list of complex selectors
let PREFIXES be an empty list of complex selectors
for each complex selector COMPLEX in PAREN_SELECTOR:
if PREFIXES is empty:
add COMPLEX to PREFIXES
move to the next iteration
let COMPLEX_SUFFIX be the final compound selector in COMPLEX
let COMPLEX_PREFIX be COMPLEX without COMPLEX_SUFFIX
let NEW_PREFIXES be an empty list of complex selectors
for each complex selector PREFIX in PREFIXES:
let WOVEN be subweave(PREFIX, COMPLEX_PREFIX)
if WOVEN is null, move to the next iteration
for each complex selector WOVEN_COMPLEX in WOVEN:
append COMPLEX_SUFFIX to WOVEN_COMPLEX
add WOVEN_COMPLEX to NEW_PREFIXES
let PREFIXES be NEW_PREFIXES
return PREFIXES
Dies beinhaltet eine weitere undefinierte Funktion, subweave, die den Großteil der Logik zum Verweben von Selektoren enthält. Sie ist eines der kompliziertesten Logikteile im gesamten @extend-Algorithmus – sie behandelt Selektorkombinatoren, Superselektoren, Subjektselektoren und mehr. Die Semantik ist jedoch extrem einfach, und das Schreiben einer Basisversion davon ist sehr einfach.
Wo weave viele komplexe Selektoren verwebt, verwebt subweave nur zwei. Die komplexen Selektoren, die es verwebt, werden so betrachtet, als hätten sie einen impliziten identischen nachfolgenden zusammengesetzten Selektor; wenn ihm beispielsweise .foo .bar und .x .y .z übergeben werden, verwebt es sie, als ob sie .foo .bar E und .x .y .z E wären. Darüber hinaus führt es in den meisten Fällen keine Zusammenführung der beiden Selektoren durch, sodass es in diesem Fall einfach .foo .bar .x .y .z, .x .y .z .foo .bar zurückgeben würde. Eine extrem naive Implementierung könnte einfach die beiden Reihenfolgen der beiden Argumente zurückgeben und wäre die meiste Zeit korrekt.
Die volle Komplexität von subweave zu ergründen, liegt hier außerhalb des Rahmens, da sie fast vollständig in die Kategorie der erweiterten Funktionalität fällt, die dieses Dokument absichtlich vermeidet. Der Code dafür befindet sich in lib/sass/selector/sequence.rb und sollte bei dem Versuch einer ernsthaften Implementierung konsultiert werden.