🕌

CSS大解剖 9日目: 「構文 2/2」

に公開

本稿は、2024年2月頃に書き溜めていたシリーズです。最後まで温存させるのが勿体ないので、未完成ですがそのまま公開します(公開日: 2025/9/24)。そのため、内容の重複や記述方針の不一致があるかもしれませんが、ご理解ください。


CSSの仕様を理解するために、1日ごとにテーマを決めて説明する企画9日目です。今日のテーマは前回に引き続き「構文」です。

括弧の対応

字句解析が終わると、括弧の対応を調べることができるようになります。開き括弧トークンは4種類、閉じ括弧トークンは3種類あり、括弧の正しい対応関係は以下の通りです。

  • <(-token><)-token>
    • (...)
  • <function-token><)-token>
    • foo(...)
  • <[-token><]-token>
    • [...]
  • <{-token><}-token>
    • {...}

CSSではメインの構文解析に入る前に、括弧の対応付けを行い、トークン列を要素値 (component value)の列に変換します。[1]

要素値 (component value) は以下の4種類からなります。

ブロックは再帰的に要素値のリストを記録しています。したがって、要素値は木構造をなしていることになります。

括弧の対応付けアルゴリズムはいたってシンプルです。開き括弧を見つけたら、対応する閉じ括弧が見つかるかEOFに遭遇するまで、再帰的に対応付けを続けます。括弧の対応がおかしい場合は、以下の2つの結果が生じえます。

  • 閉じ括弧が余る。
    • → 余った閉じ括弧は要素値として残されます。
  • 開き括弧が正しく終端されず、EOFで強制的に閉じられる。
    • → パースエラーが記録されますが、パーサーの出力には影響はありません。

たとえば以下の例を考えます。

p {
  color: red;
  font-size: calc(2 * var(--rem);/* 括弧の閉じ忘れ! */
  padding: 2px;
}

この場合、括弧の対応付けは以下のような要素値の列を生成します。

  1. <ident-token> (p)
  2. <whitespace-token>
  3. {}-ブロック; 中身は:
    1. <whitespace-token>
    2. <ident-token> (color)
    3. <colon-token>
    4. <whitespace-token>
    5. <ident-token> (red)
    6. <semicolon-token>
    7. <whitespace-token>
    8. <ident-token> (font-size)
    9. <colon-token>
    10. <whitespace-token>
    11. 関数ブロック (calc); 中身は:
      1. <number-token> (2; integer)
      2. <whitespace-token>
      3. <delim-token> (*)
      4. <whitespace-token>
      5. 関数ブロック (var); 中身は:
        1. <ident-token> (--rem)
      6. <semicolon-token> ← ここから括弧の対応が壊れる
      7. <whitespace-token>
      8. <ident-token> (padding)
      9. <colon-token>
      10. <whitespace-token>
      11. <dimension-token> (2; integer; px)
      12. <semicolon-token>
      13. <}-token>

コア構文

スタイルシートを要素値の列に分解することができたら、ここからコア構文をパースすることができます。

コア構文では、まだカスケードなどの具体的な概念に紐付かないレベルでの基礎的な解析のみを行います。

主要な構文要素

まずは主要な構文要素を確認しておきます。

宣言 (declaration) は、識別子と値のペアです。セミコロンは含みません。 !important がある場合は、それも含みます。プロパティや記述子を定義するのに使われます。

/* 宣言 */
color: red

修飾ルール (qualified rule) は、0個以上の要素値と {}-ブロックの並びです。ほとんどの場合、前者はセレクタ、後者は宣言列として解釈されます。

/* 修飾ルール */
p { color: red; }

at-rule は、<at-keyword-token> で始まり、 {}-ブロックまたは ; で終わる並びです。修飾ルールで表せないプラグマ的な機能のために使われます。

/* at-ruleの例1 */
@import url(https://example.com/style.css);

/* at-ruleの例2 */
@media (min-width: 20em) { p { color: red; } }
種類 名前 imporant プレリュード ブロック
宣言 color: red color red false - -
宣言 color: red !important color red true - -
修飾ルール p { color: red; } - - - p { color: red; }
at-rule @import url(https://example.com/style.css); import - - url(https://example.com/style.css) -
at-rule @media (min-width: 20em) { p { color: red; } } media - - (min-width: 20em) { p { color: red; } }

宣言

宣言 (declaration) は以下の3つの属性からなります。

  • 宣言の名前は、1文字以上の文字列です。
  • 宣言のは、0個以上の要素値の列です。ただし、以下の制限があります。
    • <whitespace-token> は先頭や末尾には出現できません。
    • importantフラグがfalseの場合、 ! (<delim-token>) と important (<ident-token>) の連続した並びは、末尾には出現できません。
    • また、通常 <semicolon-token> はトップレベルには出現できません。
  • 宣言のimportantフラグは真理値です。

たとえば、 color: red の名前は color, 値は red という単一のトークンからなり、importantフラグはfalseです。

宣言は以下の順で記述されます。宣言のパースはその逆です。

  1. 名前 (<ident-token> として)
  2. コロン (<colon-token>)
  3. importantフラグがある場合は、 !important

解釈

宣言は文脈により、プロパティまたは記述子として解釈されます。

p {
  /* プロパティとして解釈される宣言 */
  color: red;
}
@page {
  /* ページ記述子として解釈される宣言 */
  margin: 10%;
}

いずれの場合でも、値の解釈は個々のプロパティないし記述子に委ねられています。プロパティ値として解釈する場合、実質的には<declaration-value> の範囲内で書ける内容に制限されているとみなせます。

  • 閉じ括弧トークン、 <bad-string-token>, <bad-url-token>, <semicolon-token> および ![2] という値を持つ <delim-token> がトップレベルに出現してはいけない。 (出現する場合は無効扱い)

修飾ルール

修飾ルール (qualified rule) は以下の2つの属性からなります。

  • 修飾ルールの プレリュード (prelude) は、0個以上の要素値の列です。ただし、以下の制限があります。
    • {}-ブロックは最上位には出現できません。
    • <whitespace-token> は先頭には出現できません。 (末尾には出現できます)
    • <at-keyword-token> は先頭に出現できません。
    • このルールがネストしたルールである場合は、 <ident-token> は先頭に出現できません。
  • 修飾ルールのブロック は、 {}-ブロックです。

たとえば p:hover { color: red; } のプレリュードは p, :, hover, の4つのトークンからなり、ブロックは { color: red; } です。

修飾ルールのパースは、 {}-ブロックが見つかるまで要素列を消費し続けるだけです。もし {}-ブロックが見つからなければ構文エラーであり、途中結果は破棄されます。

修飾ルールは、スタイルルールとして解釈されることがほとんどですが、キーフレームブロックなど他の用例も存在します。

/* スタイルルールとして解釈される修飾ルール (プレリュードは "p") */
p {
  color: red;
}

@keyframes anim {
  /* キーフレームブロックとして解釈される修飾ルール (プレリュードは "from") */
  from {
    transform: translateX(0%);
  }
  to {
    transform: translateX(100%);
  }
}

at-rule

at-rule は以下の3つの属性からなります。

  • at-ruleの名前は、1文字以上の文字列です。
  • at-ruleの プレリュード (prelude) は、0個以上の要素値の列です。ただし、以下の制限があります。
    • {}-ブロックは最上位には出現できません。
    • <semicolon-token> は最上位には出現できません。
  • at-ruleのブロック は、0個または1個の {}-ブロックです。

たとえば @import url(https://example.com/style.css); の名前は import, プレリュードは url(https://example.com/style.css) の2つのトークンからなり、ブロックはありません。

at-ruleのパースは、 {}-ブロックまたは ; が見つかるまで要素列を消費し続けるだけです。

at-ruleのプレリュードとブロックの解釈は各々のat-ruleの定義に委ねられています。

宣言とルールの列のパース

スタイルシートやHTMLの style 属性、それから各種 {}-ブロックの内部は通常、「宣言」「修飾ルール」「at-rule」(のうち全部または一部)の列としてパースされます。

「宣言」「修飾ルール」「at-rule」の列をパースする際は、空白を適切に読み飛ばしながら以下の優先度で次にパースするものを決めます。

  • <semicolon-token> は読み飛ばす。 (宣言がパース対象に含まれている場合に限る)
  • <at-keyword-token> から始まる場合は、at-ruleのパースを試みる。
  • <ident-token> から始まる場合は、宣言のパースを試みる。
  • 上記のいずれにも該当しない場合は、修飾ルールのパースを試みる。

非常に端的にまとめてしまうと、これは宣言やルールがセミコロンまたは {}-ブロックの終端によって区切られると考えてよいでしょう。ただし厳密には以下のような違いがあります。

  • 修飾ルール中にはセミコロンが出現しえます。
  • 宣言中には {}-ブロックが出現しえます。
  • 宣言がパース対象に含まれていない場合は、余分なセミコロンは読み飛ばされず、修飾ルールの一部になってしまいます。たとえば、
    p {
      color: red;; /* 余分なセミコロン */
      font-size: 200%;
    }
    
    において、余分なセミコロンは完全にルールに準拠しており無害ですが、
    @import url(https://example.com/style.css);; /* 余分なセミコロン */
    p {
      color: red;
    }
    
    において、余分なセミコロンは続く修飾ルールの一部と解釈されてしまい、このルールは ; p という無効なセレクタを持つことになってしまいます。
  • 宣言と修飾ルールの両方がパース対象に含まれているときは、曖昧性解消のために修飾ルールは <ident-token> で開始できません。つまり、
    p {
      color: red;
    }
    
    は有効な修飾ルールですが、CSS Nestingにおいて
    article {
      p {
        color: red;
      }
    }
    
    の内部の修飾ルールは意図通りにパースされず、不完全な宣言と不完全な修飾ルールの並びになってしまいます。

どの宣言/ルールが対象に含まれるかは、その文脈によって異なります。この違いを反映するために、4種類の異なる非終端記号が定義されてます。

記号 宣言 修飾ルール at-rule
<style-block> ✔️ ✔️ ✔️
<declaration-list> ✔️ × ✔️
<rule-list> × ✔️ ✔️
<stylesheet> × ✔️ ✔️

いずれが使われるかは、そのコンテキストによって異なります。たとえばスタイルシートは <stylesheet> としてパースされ、 style 属性は <declaration-list> としてパースされます。

ネストしたルールについて

ネストしたルールについて、現行(記事執筆時点)のCSS Syntaxでは& で始まる修飾ルールだけがネストできるとしていますが、Working DraftであるCSS Nestingでは<ident-token> で始まらなければネストできるとされています。ここではCSS Nestingの記述のほうが新しく、こちらが現在の仕様上の意図であると考えられます。

スタイルシート全体の構文

スタイルシート全体の構文では、トップレベルに修飾ルールとat-ruleの2種類を書くことができます。

トップレベルの修飾ルールはスタイルルールとして解釈されます。

スタイルルールのプレリュードはセレクタリストとして解釈されます。

スタイルルールのブロック内には宣言と修飾ルールとat-ruleの全てを書くことができます。宣言はプロパティ宣言として解釈され、修飾ルールはネストしたスタイルルールとして解釈されます。ネストしたスタイルルールの挙動はWorking DraftであるCSS Nestingで規定されています。

マイクロ構文

色々な事情から、CSSではCSS Syntaxのトークン構造にマッチしない不可思議な構造を持つ値構文がいくつか定義されています。これらはグローバルに適用されると思わぬ曖昧性によるバグを生むため、CSS Syntaxの字句解析器の定義には含められていません。

さいわい、CSSは空白トークンを維持することから、分離されたトークンを再結合して解釈しなおすことはある程度可能です。そこで、これらの特殊な構文は、分割済みのトークンを必要なときに再解釈したとして定義されています。

  • CSS SyntaxではAn+BUnicoe-Rangeという2つのマイクロ構文を定義しています。
  • CSS Colorが定義する<quirky-color>も一種のマイクロ構文であるとみなせます。

たとえば An+B 形式は n を変数とする最大1次の整数係数多項式として書けるものを受理します[3]。これには以下のパターンがあります。

  • (+ | -)? digits
  • (+ | -)? digits? (n | N)
  • (+ | -)? digits? (n | N) (+ | -) digits

ただし、空白は二項演算子の前後にのみ許可されます。

これをCSSの字句で表現しようとすると、以下のようなバリエーションを考える必要が出てきます。

  • +n+ / n に分けられるが、 -n は単一トークン。
  • +n+ / n に分けられるが、 +1n は単一トークン。
  • n+ 0n / + / / 0 に分けられるが、 n- 0n- / / 0 の3トークン。
  • n+0n / +0 に分けられるが、 n-0 は単一トークン。
  • n + 0n / / + / / 0 に分けられるが、 n +0n / / +0 の3トークン。

本稿で説明していないこと

  • セレクタリストの構文については別の機会に説明します。
  • 各at-ruleの構文については省略します。
  • プロパティや記述子の値の構文はそれぞれに異なるため、本稿では説明していません。

脚注
  1. 仕様書では構文解析の中で括弧の対応付けを行っていますが、別ステップとして分けたほうがわかりやすいでしょう。 ↩︎

  2. したがって、些細なことですが、 foo: !important !important のようにして !important をプロパティ内に出現させてもそのプロパティを有効なプロパティ宣言とすることはできないということになります。 ↩︎

  3. 他に oddeven も受理します。 ↩︎

Discussion