LoginSignup
5

More than 5 years have passed since last update.

Haml attributeの仕様

Last updated at Posted at 2017-02-08

この記事は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だったらそっちが後になる

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5