@extendの仕組み
2013年11月23日投稿者:Natalie Weizenbaum
これは元々、gistとして公開されました。.
Aaron Leungはlibsassに取り組んでおり、SassのRuby実装で@extend
がどのように実装されているのか疑問に思っていました。彼に伝えるだけでなく、Sassを移植している人や、その仕組みを知りたいと思っている人が誰でも見ることができるように、公開ドキュメントを作成することにしました。
この説明は、多くの点で簡略化されています。基本的な正しい@extend
変換の最も複雑な部分を説明することを目的としていますが、完全なSass互換性を求める場合に重要となる多くの詳細は省略されています。これは、完全なサポートを構築できる@extend
の基礎となる説明と考えるべきです。@extend
の完全な理解のためには、Ruby Sassコードとそのテストを参照する以外にありません。
このドキュメントは、セレクターレベル4仕様で定義されているセレクター用語の知識を前提としています。ドキュメント全体を通して、セレクターは、そのコンポーネントのリストまたは集合と同様に扱われます。たとえば、複雑なセレクターは、複合セレクターのリストまたは単純セレクターのリストのリストとして扱われる場合があります。
プリミティブプリミティブパーマリンク
以下は、@extend
を実装するために必要なプリミティブオブジェクト、定義、および演算子のセットです。これらを実装することは、読者の課題として残されています。
-
@extend
はセレクターに関するものであるため、セレクターオブジェクトは明らかに必要です。セレクターは、徹底的に意味的に解析する必要があります。実装では、さまざまな異なる形式のセレクターの背後にある意味をかなり理解する必要があります。 -
「サブセットマップ」と呼ばれるカスタムデータ構造も必要です。サブセットマップには、
Map.set(Set, Object)
とMap.get(Set) => [Object]
の2つの演算があります。前者は、マップ内のキーのセットに値を関連付けます。後者は、キーのセットの*サブセット*に関連付けられているすべての値を検索します。たとえばmap.set([1, 2], 'value1') map.set([2, 3], 'value2') map.set([3, 4], 'value3') map.get([1, 2, 3]) => ['value1', 'value2']
-
セレクター
S1
は、S2
によってマッチされたすべての要素がS1
によってもマッチされる場合、セレクターS2
の「スーパセレクター」です。たとえば、.foo
は.foo.bar
のスーパセレクターであり、a
はdiv a
のスーパセレクターであり、*
はすべてのスーパセレクターです。スーパセレクターの逆は「サブセレクター」です。 -
両方の入力セレクターによってマッチされた要素を正確にマッチするセレクターを返す演算
unify(複合セレクター、複合セレクター) => 複合セレクター
。たとえば、unify(.foo, .bar)
は.foo.bar
を返します。これは、複合セレクターまたはそれ以下の単純なセレクターに対してのみ機能する必要があります。この演算は失敗する可能性があります(例:unify(a, h1)
)。その場合はnull
を返す必要があります。 -
入力にある他の複合セレクターのサブセレクターである複合セレクターを削除する演算
trim([セレクターリスト]) => セレクターリスト
。これは入力を複数のセレクターリストとして受け取り、これらのリスト全体でサブセレクターのみをチェックします。なぜなら、前の@extend
プロセスではリスト内のサブセレクターは生成されないからです。たとえば、[[a], [.foo a]]
が渡された場合、.foo a
はa
のサブセレクターであるため、[a]
を返します。 -
各ステップの選択肢のリストをすべて返す演算
paths([[オブジェクト]]) => [[オブジェクト]]
。たとえば、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]]
を返します。
アルゴリズムアルゴリズムパーマリンク
@extend
アルゴリズムには、2つのパスが必要です。1つはスタイルシートで宣言されている@extend
を記録し、もう1つはそれらの@extend
を使用してセレクターを変換します。これは、@extend
がスタイルシートの前のセレクターにも影響を与える可能性があるため必要です。
記録パス記録パスパーマリンク
擬似コードでは、このパスは次のように記述できます。
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))
変換パス変換パスパーマリンク
変換パスは記録パスよりも複雑です。擬似コードで以下に説明します。
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
鋭い読者は、この擬似コードで使用されている未定義関数weave
に気づいたでしょう。weave
は他のプリミティブ演算よりもはるかに複雑であるため、詳細に説明したかったのです。
WeaveWeaveパーマリンク
高レベルでは、「weave」演算は非常に簡単に理解できます。「括弧付きセレクター」を展開していると考えるのが最適です。.foo (.bar a)
と書くと、.foo
親要素と.bar
親要素の両方を持つすべてのa
要素にマッチすると想像してください。weave
はこのように機能します。
このa
要素にマッチするには、.foo (.bar a)
を次のセレクターリストに展開する必要があります。.foo .bar a, .foo.bar a, .bar .foo a
。これは、a
が.foo
親と.bar
親の両方を持つ可能性のあるすべての方法にマッチします。しかし、weave
は実際には.foo.bar a
を出力しません。このようなマージされたセレクターを含めると、出力サイズが指数関数的に増加し、ほとんど役に立ちません。
この括弧付きセレクターは、複合セレクターのリストとしてweave
に渡されます。たとえば、.foo (.bar a)
は[.foo, .bar a]
として渡されます。同様に、(.foo div) (.bar a) (.baz h1 span)
は[.foo div, .bar a, .baz h1 span]
として渡されます。
weave
は、括弧付きセレクターを左から右に移動し、可能なすべてのプレフィックスのリストを作成し、各括弧付きコンポーネントが発生するたびにこのリストに追加することで機能します。擬似コードは次のとおりです。
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
これには、さらに別の未定義関数subweave
が含まれています。これには、セレクターを織り込むロジックの大部分が含まれています。これは、@extend
アルゴリズム全体の最も複雑なロジックの一部であり、セレクターコンバイナー、スーパセレクター、サブジェクトセレクターなどを処理します。ただし、セマンティクスは非常にシンプルであり、基本的なバージョンを作成するのは非常に簡単です。
weave
が多くの複合セレクターを織り込むのに対し、subweave
は2つだけ織り込みます。織り込む複合セレクターは、暗黙的に同一の末尾複合セレクターを持つと見なされます。たとえば、.foo .bar
と.x .y .z
が渡された場合、.foo .bar E
と.x .y .z E
であるかのように織り込みます。さらに、ほとんどの場合、2つのセレクターをマージしないため、この場合、.foo .bar .x .y .z, .x .y .z .foo .bar
を返すだけです。非常に単純な実装では、2つの引数の2つの順序を返すだけで、ほとんどの場合正しくなります。
このドキュメントでは、意図的に高度な機能を避けているため、subweave
の完全な複雑さについて詳しく説明することはできません。そのコードはlib/sass/selector/sequence.rb
にあり、本格的な実装を試みる場合は参照する必要があります。