Haml attributeの仕様

  • 6
    いいね
  • 0
    コメント

この記事はHaml 4.1.0.beta.1について書いています。最初は最新の安定板であるHaml 4.0.7について書いていたんですが、4.0.7だと書き方によってattributeが消えたりしてバグっぽいので、betaですが4.1.0について書くことにしました。

解説は全て自分用のメモです。

値がArray, Hash, true, false, nil以外のケース

以下のHamlのレンダリング結果は何でしょうか。haml -t ugly ファイル名のコマンドの出力(escape_attrs: true, escape_html: false, format: :html5, ugly: true)で答えてください。なお、Rubyとして有効な警告の出ないコードにコンパイルされるので、シンタックスエラーは不正解です。

- c = 'c'
- ::D = Struct.new(:id)
#a{ id: 'b' }(id=c)[D.new('e')]
.a{ :class=> 'b' }(class='c')[D.new('e')]
%div{ data: 'a' }(data='b')


答えはこちら

<div class='d' id='a_c_b_d_e'></div>
<div class='a b c d' id='d_e'></div>
<div data='a'></div>

解説

idのパース

#a{ id: 'b' }(id=c)[D.new('e')]

このコードのHaml::Parser#parse_tagの返り値の2つ目(attributes)は"#a"で、Haml::Parser.parse_class_and_id(attributes)の結果は:

{"id"=>"a"}

3つ目(attributes_hashes)は:

{:old=>" id: 'b' ", :new=>[{}, "{\"id\" => c,}"]}


attributes_hashesの詳細
attributes_hashes[:new] は、 static_attributes: {}, attributes_hash: "{\"id\" => c,}" になる
attributes_hashes[:old] は、 Haml::Parser#parse_static_hash の結果、static_attributes: nil になる

:id=> ならstatic hashという扱いになるが、id: のパースに対応していない。

4つ目(object_ref)は:

"[D.new('e')]"


object_refの詳細
#<struct D id="e"> は、 Haml::Buffer#parse_object_refの結果、 {"id"=>"d_e", "class"=>"d"} になる

になる。つまり

返り値/コード 結果
Haml::Parser.parse_class_and_id(attributes) {"id"=>"a"}
attributes_hashes {:old=>" id: 'b' ", :new=>[{}, "{\"id\" => c,}"]}
Haml::Parser#parse_static_hash(attributes_hashes[:old]) nil
object_ref "[D.new('e')]"

idのマージ

Haml::Buffer.merge_attrsは複数のidをマージする時_でjoinする。

  1. Parser.parse_class_and_id(attributes)の結果がattributesのベースになる
    • attributes: {"id"=>"a"}
  2. attributes_hashes[:new]の1つ目static_attributesがマージされる
    • 2つ目attributes_hashattributes_listに突っ込まれてコンパイラに渡される
    • attributes: {"id"=>"a"}, attributes_list: ["{\"id\" => c,}"]
  3. Haml::Parser#parse_static_hash(attributes_hashes[:old])の結果がマージされる
    • 結果がnilの場合attributes_listに突っ込まれてコンパイラに渡される
    • attributes: {"id"=>"a"}, attributes_list: ["{\"id\" => c,}", " id: 'b' "]
  4. Haml::Compiler#compile_tag がパーサーからattributes: attributes, attributes_list: attributes_list, object_ref: object_refを受け取る
    • object_ref が空(:nil)かつattributes_hashesも空で!preserve_scriptがtrue(~, &~, !~ではない)の場合、attributesだけでCompiler.build_attributesされる
    • その他の場合、"_hamlout.attributes(#{inspect_obj(attributes)}, #{object_ref},#{attributes_hashes.join(', ')})"がレンダリングされる
    • コード: _hamlout.attributes({"id"=>"a"}, [D.new('e')], {"id" => c,}, id: 'b' )
  5. Haml::Buffer#attributesattributes_hashesの1つ目をstringify_keysしたものをマージする
    • attributes: {"id"=>"a_c"}
  6. Haml::Buffer#attributesattributes_hashesの2つ目をstringify_keysしたものをマージする
    • attributes: {"id"=>"a_c_b"}
  7. obj_refが存在すればHaml::Buffer#parse_object_ref(obj_ref)の結果をマージする
    • attributes: {"id"=>"a_c_b_d_e", "class"=>"d"}

classのパース

これはidの例とは別の場所がstaticにパースされるような例にしてある。

.a{ :class=> 'b' }(class='c')[D.new('e')]

このコードのHaml::Parser#parse_tagの返り値の2つ目(attributes)は".a"で、Haml::Parser.parse_class_and_id(attributes)の結果は:

{"class"=>"a"}

3つ目(attributes_hashes)は:

{:old=>" :class=> 'b' ", :new=>[{"class"=>"c"}, nil]}


attributes_hashesの詳細
attributes_hashes[:new] は、 static_attributes: {"class"=>"c"}, attributes_hash: nil になる。これがidの例と逆。
attributes_hashes[:old] は、 Haml::Parser#parse_static_hash の結果、static_attributes: {"class"=>"b"} になる。
:class=> はパースできるので、staticという扱いになる。

4つ目(object_ref)は同じ。つまり

返り値/コード 結果
Haml::Parser.parse_class_and_id(attributes) {"class"=>"a"}
attributes_hashes {:old=>" :class=> 'b' ", :new=>[{"class"=>"c"}, nil]}
Haml::Parser#parse_static_hash(attributes_hashes[:old]) {"class"=>"b"}
object_ref "[D.new('e')]"

classのマージ

Haml::Buffer.merge_attrsは複数のclassをマージする時uniqやsortを行い、でjoinする。

  1. Parser.parse_class_and_id(attributes)の結果がattributesのベースになる
    • attributes: {"class"=>"a"}
  2. attributes_hashes[:new]の1つ目static_attributesがマージされる
    • 2つ目attributes_hashattributes_listに突っ込まれてコンパイラに渡される
    • attributes: {"class"=>"a c"}, attributes_list: [nil]
  3. Haml::Parser#parse_static_hash(attributes_hashes[:old])の結果がマージされる
    • 結果がnilの場合attributes_listに突っ込まれてコンパイラに渡される
    • attributes: {"class"=>"a b c"}, attributes_list: [nil]
  4. Haml::Compiler#compile_tag がパーサーからattributes: attributes, attributes_list: attributes_list, object_ref: object_refを受け取る
    • object_ref が空(:nil)かつattributes_hashesも空で!preserve_scriptがtrue(~, &~, !~ではない)の場合、attributesだけでCompiler.build_attributesされる
    • その他の場合、"_hamlout.attributes(#{inspect_obj(attributes)}, #{object_ref},#{attributes_hashes.join(', ')})"がレンダリングされる
    • コード: _hamlout.attributes({"class"=>"a b c"}, [D.new('e')])
  5. Haml::Buffer#attributesattributes_hashesの1つ目をstringify_keysしたものをマージする
    • attributes: {"class"=>"a b c"} (attributes_hashesが空なのでスキップ)
  6. Haml::Buffer#attributesattributes_hashesの2つ目をstringify_keysしたものをマージする
    • attributes: {"class"=>"a b c"} (attributes_hashesが空なのでスキップ)
  7. obj_refが存在すればHaml::Buffer#parse_object_ref(obj_ref)の結果をマージする
    • attributes: {"class"=>"a b c d", "id"=>"d_e"}

その他のパース

%div{ data: 'a' }(data='b')
返り値/コード 結果
Haml::Parser.parse_class_and_id(attributes) {}
attributes_hashes {:old=>" data: 'a' ", :new=>[{"data"=>"b"}, nil]}
Haml::Parser#parse_static_hash(attributes_hashes[:old]) nil
object_ref nil

その他のマージ

Haml::Buffer.merge_attrsは複数のid,class以外のキーをマージする時、後にマージされた方を優先する。

  1. Parser.parse_class_and_id(attributes)の結果がattributesのベースになる
    • attributes: {}
  2. attributes_hashes[:new]の1つ目static_attributesがマージされる
    • 2つ目attributes_hashattributes_listに突っ込まれてコンパイラに渡される
    • attributes: {"data"=>"b"}, attributes_list: [nil]
  3. Haml::Parser#parse_static_hash(attributes_hashes[:old])の結果がマージされる
    • 結果がnilの場合attributes_listに突っ込まれてコンパイラに渡される
    • attributes: {"data"=>"b"}, attributes_list: [nil, " data: 'a' "]
  4. Haml::Compiler#compile_tag がパーサーからattributes: attributes, attributes_list: attributes_list, object_ref: object_refを受け取る
    • object_ref が空(:nil)かつattributes_hashesも空で!preserve_scriptがtrue(~, &~, !~ではない)の場合、attributesだけでCompiler.build_attributesされる
    • その他の場合、"_hamlout.attributes(#{inspect_obj(attributes)}, #{object_ref},#{attributes_hashes.join(', ')})"がレンダリングされる
    • コード: _hamlout.attributes({"data"=>"b"}, nil, data: 'a' )
  5. Haml::Buffer#attributesattributes_hashesの1つ目をstringify_keysしたものをマージする
    • attributes: {"class"=>"a"}
  6. Haml::Buffer#attributesattributes_hashesの2つ目をstringify_keysしたものをマージする
    • attributes: {"class"=>"a"} (空なのでスキップ)
  7. obj_refが存在すればHaml::Buffer#parse_object_ref(obj_ref)の結果をマージする
    • attributes: {"class"=>"a"} (空なのでスキップ)

true, false, nilのケース

今度はこれはどうなるでしょうか。

- ::D = Struct.new(:id)
#static{ id: true }(id=false)[D.new(nil)]
%div{ id: true }(id=true)
%div{ id: true }(id=false)
%div{ id: false }(id=true)
%div{ id: false }(id=false)

.static{ class: true }(class=nil)[D.new(false)]
%div{ class: true }(class=true)
%div{ class: false }(class=true)
%div{ class: true }(class=false)
%div{ class: false }(class=false)

%div{ data: 'true' }(data=true)
%div{ data: true }(data=true)
%div{ data: false }(data=true)
%div{ data: true }(data=false)
%div{ data: false }(data=false)


答えはこちら

<div class='d' id='static_true_d_new'></div>
<div id='true_true'></div>
<div id='true'></div>
<div id='true'></div>
<div class='d static true' id='d_new'></div>
<div class='true'></div>
<div class='true'></div>
<div class='true'></div>
<div></div>
<div data='true'></div>
<div data></div>
<div></div>
<div data></div>
<div></div>

idの場合

  • false, nilが来てもなかったことにされる
  • trueが来てもto_sされ、"true"と同じに解釈される
  • 複数のマージは"_"でjoin
  • 全てnilかfalseならレンダリングされない
  • object_refは、値がnil/falseの時idに_newがつく

classの場合

  • false, nilが来てもなかったことにされる
  • trueが来てもto_sされ、"true"と同じに解釈される
  • 複数のマージはuniq & sort
  • object_refは、そもそもclassに関しては値が影響しない

その他

  • old attribute({...})の値がnew attribute((...))の値を上書きする
    • id, class以外は、単に後にマージした方が優先され、かつoldが後からマージされるため
  • true"true"は区別される
  • マージ結果がnil/falseならレンダリングされない
  • foo: trueの時、format=html5ならdata、format=xhtmlならdata='data'がレンダリングされる

値にHashを含むケース

くぅ〜w 疲れましたw

- str = 'str'
- hash1 = { 'k1' => 'v1', 'key1' => { 'nk1' => 'nv1' }}
- hash2 = { 'k1' => 'v2', 'key1' => { 'nk2' => 'nv2' }}
#static{ id: 'str' }(id=hash1)
#static{ id: hash1 }(id=str)
%div{ id: hash1 }
%div{ id: hash1 }(id=hash2)
%div{ id: hash2 }(id=hash1)

.static{ class: 'str' }(class=hash1)
.static{ class: hash1 }(class=str)
%div{ class: hash1 }
%div{ class: hash1 }(class=hash2)

%div{ data: hash1 }
%div(data=hash2)
%div{ data: hash1 }(data='str')
%div{ data: 'str' }(data=hash1)
%div{ data: hash1 }(data=hash2)
%div{ data: hash2 }(data=hash1)


答えはこちら

<div id='static_{"k1"=&gt;"v1", "key1"=&gt;{"nk1"=&gt;"nv1"}}_str'></div>
<div id='static_str_{"k1"=&gt;"v1", "key1"=&gt;{"nk1"=&gt;"nv1"}}'></div>
<div id='{"k1"=&gt;"v1", "key1"=&gt;{"nk1"=&gt;"nv1"}}'></div>
<div id='{"k1"=&gt;"v2", "key1"=&gt;{"nk2"=&gt;"nv2"}}_{"k1"=&gt;"v1", "key1"=&gt;{"nk1"=&gt;"nv1"}}'></div>
<div id='{"k1"=&gt;"v1", "key1"=&gt;{"nk1"=&gt;"nv1"}}_{"k1"=&gt;"v2", "key1"=&gt;{"nk2"=&gt;"nv2"}}'></div>
<div class='"key1"=&gt;{"nk1"=&gt;"nv1"}} static str {"k1"=&gt;"v1",'></div>
<div class='"key1"=&gt;{"nk1"=&gt;"nv1"}} static str {"k1"=&gt;"v1",'></div>
<div class='{"k1"=&gt;"v1", "key1"=&gt;{"nk1"=&gt;"nv1"}}'></div>
<div class='"key1"=&gt;{"nk1"=&gt;"nv1"}} "key1"=&gt;{"nk2"=&gt;"nv2"}} {"k1"=&gt;"v1", {"k1"=&gt;"v2",'></div>
<div data-k1='v1' data-key1-nk1='nv1'></div>
<div data-k1='v2' data-key1-nk2='nv2'></div>
<div data-k1='v1' data-key1-nk1='nv1' data='str'></div>
<div data-k1='v1' data-key1-nk1='nv1' data='str'></div>
<div data-k1='v1' data-key1-nk1='nv1' data='str'></div>
<div data-k1='v1' data-key1-nk1='nv1' data='str'></div>

idの場合

  • 値は全てto_sされる
  • キーが被っても、マージされた順序でto_sの結果を並べて"_"でjoinするだけ

classの場合

  • 値は全てto_sされる
  • キーが被った場合、to_s後uniq & sortする

その他

  • hyphenate(ネストしたレベルごとに"-"で連結)される
  • キーが被った場合
    • 片方がHashの場合は両方の値が保持され、Hashの方がキー名がhyphenateされ、そうでない方はキーがそのままのため両方個別にレンダリングされる
    • Hash同士のマージでは、Hash#merge!するだけなので浅いマージ
      • そういえば4.1.0.beta.1はバグってるから連続して書いてレンダリングすると壊れるけど、本当は後にマージした値(old attribute)が採用される意図のはず

Arrayを含むケース

まだあるんだった…idとclassだけ特別な挙動をするんですよ〜

- str = 'str'
- arr1 = ['arr1_1', 'arr1_2']
- arr2 = ['arr2_1', 'arr2_2']
#static{ id: 'str' }(id=arr1)
#static{ id: arr1 }(id=str)
%div{ id: arr1 }
%div{ id: arr1 }(id=arr2)
%div{ id: arr2 }(id=arr1)

.static{ class: 'str' }(class=arr1)
.static{ class: arr1 }(class=str)
%div{ class: arr1 }
%div{ class: arr1 }(class=arr2)
%div{ class: arr2 }(class=arr1)

%div{ data: arr1 }
%div{ data: arr1 }(data='str')
%div{ data: 'str' }(data=arr1)
%div{ data: arr1 }(data=arr2)
%div{ data: arr2 }(data=arr1)


答えはこちら

<div id='static_arr1_1_arr1_2_str'></div>
<div id='static_str_arr1_1_arr1_2'></div>
<div id='arr1_1_arr1_2'></div>
<div id='arr2_1_arr2_2_arr1_1_arr1_2'></div>
<div id='arr1_1_arr1_2_arr2_1_arr2_2'></div>
<div class='arr1_1 arr1_2 static str'></div>
<div class='arr1_1 arr1_2 static str'></div>
<div class='arr1_1 arr1_2'></div>
<div class='arr1_1 arr1_2 arr2_1 arr2_2'></div>
<div class='arr1_1 arr1_2 arr2_1 arr2_2'></div>
<div data='["arr1_1", "arr1_2"]'></div>
<div data='["arr1_1", "arr1_2"]'></div>
<div data='str'></div>
<div data='["arr1_1", "arr1_2"]'></div>
<div data='["arr2_1", "arr2_2"]'></div>

idの場合

  • マージされた順に"_"でjoinされるんじゃないだろうか
    • flattenされるので、ネストしたarrayは平になる。それ以外はto_sされる

classの場合

  • flatten, sort, uniqした上で" "でjoinされるんじゃないかな
    • flatten後の値は全てto_s

その他

  • 普通。単に後にマージされる方が採用される。
    • 後になるのは大体はold attributeだけど、old attributeのkeyとvalueが両方文字列リテラルかつハッシュロケットの場合、new attributeがarrayだったらそっちが後になる