この記事は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')
答えはこちら
``` ```解説
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する。
-
Parser.parse_class_and_id(attributes)
の結果がattributes
のベースになる
- attributes:
{"id"=>"a"}
-
attributes_hashes[:new]
の1つ目static_attributes
がマージされる
- 2つ目
attributes_hash
がattributes_list
に突っ込まれてコンパイラに渡される - attributes:
{"id"=>"a"}
, attributes_list:["{\"id\" => c,}"]
-
Haml::Parser#parse_static_hash(attributes_hashes[:old])
の結果がマージされる
- 結果がnilの場合
attributes_list
に突っ込まれてコンパイラに渡される - attributes:
{"id"=>"a"}
, attributes_list:["{\"id\" => c,}", " id: 'b' "]
-
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' )
-
Haml::Buffer#attributes
がattributes_hashes
の1つ目をstringify_keysしたものをマージする
- attributes:
{"id"=>"a_c"}
-
Haml::Buffer#attributes
がattributes_hashes
の2つ目をstringify_keysしたものをマージする
- attributes:
{"id"=>"a_c_b"}
- 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する。
-
Parser.parse_class_and_id(attributes)
の結果がattributes
のベースになる
- attributes:
{"class"=>"a"}
-
attributes_hashes[:new]
の1つ目static_attributes
がマージされる
- 2つ目
attributes_hash
がattributes_list
に突っ込まれてコンパイラに渡される - attributes:
{"class"=>"a c"}
, attributes_list:[nil]
-
Haml::Parser#parse_static_hash(attributes_hashes[:old])
の結果がマージされる
- 結果がnilの場合
attributes_list
に突っ込まれてコンパイラに渡される - attributes:
{"class"=>"a b c"}
, attributes_list:[nil]
-
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')])
-
Haml::Buffer#attributes
がattributes_hashes
の1つ目をstringify_keysしたものをマージする
- attributes:
{"class"=>"a b c"}
(attributes_hashesが空なのでスキップ)
-
Haml::Buffer#attributes
がattributes_hashes
の2つ目をstringify_keysしたものをマージする
- attributes:
{"class"=>"a b c"}
(attributes_hashesが空なのでスキップ)
- 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以外のキーをマージする時、後にマージされた方を優先する。
-
Parser.parse_class_and_id(attributes)
の結果がattributes
のベースになる
- attributes:
{}
-
attributes_hashes[:new]
の1つ目static_attributes
がマージされる
- 2つ目
attributes_hash
がattributes_list
に突っ込まれてコンパイラに渡される - attributes:
{"data"=>"b"}
, attributes_list:[nil]
-
Haml::Parser#parse_static_hash(attributes_hashes[:old])
の結果がマージされる
- 結果がnilの場合
attributes_list
に突っ込まれてコンパイラに渡される - attributes:
{"data"=>"b"}
, attributes_list:[nil, " data: 'a' "]
-
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' )
-
Haml::Buffer#attributes
がattributes_hashes
の1つ目をstringify_keysしたものをマージする
- attributes:
{"class"=>"a"}
-
Haml::Buffer#attributes
がattributes_hashes
の2つ目をstringify_keysしたものをマージする
- attributes:
{"class"=>"a"}
(空なのでスキップ)
- 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)
答えはこちら
``` ```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)
答えはこちら
``` ```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)
答えはこちら
``` ```idの場合
- マージされた順に
"_"
でjoinされるんじゃないだろうか- flattenされるので、ネストしたarrayは平になる。それ以外はto_sされる
classの場合
- flatten, sort, uniqした上で
" "
でjoinされるんじゃないかな- flatten後の値は全てto_s
その他
- 普通。単に後にマージされる方が採用される。
- 後になるのは大体はold attributeだけど、old attributeのkeyとvalueが両方文字列リテラルかつハッシュロケットの場合、new attributeがarrayだったらそっちが後になる