@extendの仕組み

2013年11月23日投稿者:Natalie Weizenbaum

これは元々、gistとして公開されました。.

Aaron Leunglibsassに取り組んでおり、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のスーパセレクターであり、adiv aのスーパセレクターであり、*はすべてのスーパセレクターです。スーパセレクターの逆は「サブセレクター」です。

  • 両方の入力セレクターによってマッチされた要素を正確にマッチするセレクターを返す演算unify(複合セレクター、複合セレクター) => 複合セレクター。たとえば、unify(.foo, .bar).foo.barを返します。これは、複合セレクターまたはそれ以下の単純なセレクターに対してのみ機能する必要があります。この演算は失敗する可能性があります(例:unify(a, h1))。その場合はnullを返す必要があります。

  • 入力にある他の複合セレクターのサブセレクターである複合セレクターを削除する演算trim([セレクターリスト]) => セレクターリスト。これは入力を複数のセレクターリストとして受け取り、これらのリスト全体でサブセレクターのみをチェックします。なぜなら、前の@extendプロセスではリスト内のサブセレクターは生成されないからです。たとえば、[[a], [.foo a]]が渡された場合、.foo aaのサブセレクターであるため、[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にあり、本格的な実装を試みる場合は参照する必要があります。