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 @extend alles 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) und Map.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 Beispiel

    map.set([1, 2], 'value1')
    map.set([2, 3], 'value2')
    map.set([3, 4], 'value3')
    map.get([1, 2, 3]) => ['value1', 'value2']
  • Ein Selektor S1 ist ein "Superselektor" eines Selektors S2, wenn jedes von S2 gematchte Element auch von S1 gematcht wird. Zum Beispiel ist .foo ein Superselektor von .foo.bar, a ist ein Superselektor von div a und * 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 gibt unify(.foo, .bar) .foo.bar zurück. Dies muss nur für zusammengesetzte oder einfachere Selektoren funktionieren. Diese Operation kann fehlschlagen (z.B. unify(a, h1)), in diesem Fall sollte sie null zurü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 a ein Subselektor von a ist.

  • 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 gibt paths([[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.