haml 1.5.2 → 1.7.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of haml might be problematic. Click here for more details.
- data/MIT-LICENSE +1 -1
- data/Rakefile +1 -0
- data/VERSION +1 -1
- data/bin/css2sass +7 -0
- data/bin/html2haml +0 -82
- data/lib/haml.rb +43 -6
- data/lib/haml/buffer.rb +81 -72
- data/lib/haml/engine.rb +240 -110
- data/lib/haml/exec.rb +120 -5
- data/lib/haml/helpers.rb +88 -3
- data/lib/haml/helpers/action_view_extensions.rb +45 -0
- data/lib/haml/helpers/action_view_mods.rb +30 -17
- data/lib/haml/html.rb +173 -0
- data/lib/haml/template.rb +1 -26
- data/lib/haml/util.rb +18 -0
- data/lib/sass.rb +181 -3
- data/lib/sass/constant.rb +38 -9
- data/lib/sass/constant/color.rb +25 -1
- data/lib/sass/constant/literal.rb +10 -8
- data/lib/sass/css.rb +197 -0
- data/lib/sass/engine.rb +239 -68
- data/lib/sass/error.rb +2 -2
- data/lib/sass/plugin.rb +11 -3
- data/lib/sass/tree/attr_node.rb +25 -17
- data/lib/sass/tree/comment_node.rb +14 -0
- data/lib/sass/tree/node.rb +18 -1
- data/lib/sass/tree/rule_node.rb +17 -5
- data/lib/sass/tree/value_node.rb +4 -0
- data/test/haml/engine_test.rb +42 -25
- data/test/haml/helper_test.rb +28 -3
- data/test/haml/results/eval_suppressed.xhtml +6 -0
- data/test/haml/results/helpers.xhtml +26 -2
- data/test/haml/results/helpful.xhtml +2 -0
- data/test/haml/results/just_stuff.xhtml +17 -2
- data/test/haml/results/standard.xhtml +1 -1
- data/test/haml/results/whitespace_handling.xhtml +1 -11
- data/test/haml/rhtml/standard.rhtml +1 -1
- data/test/haml/template_test.rb +7 -2
- data/test/haml/templates/eval_suppressed.haml +7 -2
- data/test/haml/templates/helpers.haml +16 -1
- data/test/haml/templates/helpful.haml +2 -0
- data/test/haml/templates/just_stuff.haml +23 -4
- data/test/haml/templates/standard.haml +3 -3
- data/test/haml/templates/whitespace_handling.haml +0 -50
- data/test/sass/engine_test.rb +35 -10
- data/test/sass/plugin_test.rb +10 -6
- data/test/sass/results/alt.css +4 -0
- data/test/sass/results/complex.css +4 -3
- data/test/sass/results/constants.css +3 -3
- data/test/sass/results/import.css +27 -0
- data/test/sass/results/nested.css +7 -0
- data/test/sass/results/parent_ref.css +13 -0
- data/test/sass/results/subdir/nested_subdir/nested_subdir.css +1 -0
- data/test/sass/results/subdir/subdir.css +1 -0
- data/test/sass/templates/alt.sass +16 -0
- data/test/sass/templates/bork2.sass +2 -0
- data/test/sass/templates/complex.sass +19 -1
- data/test/sass/templates/constants.sass +8 -0
- data/test/sass/templates/import.sass +8 -0
- data/test/sass/templates/importee.sass +10 -0
- data/test/sass/templates/nested.sass +8 -0
- data/test/sass/templates/parent_ref.sass +25 -0
- data/test/sass/templates/subdir/nested_subdir/nested_subdir.sass +3 -0
- data/test/sass/templates/subdir/subdir.sass +6 -0
- metadata +95 -75
- data/test/haml/results/semantic.cache +0 -15
data/MIT-LICENSE
CHANGED
data/Rakefile
CHANGED
@@ -103,6 +103,7 @@ unless ARGV[0] == 'benchmark'
|
|
103
103
|
rdoc.rdoc_files.include('README')
|
104
104
|
rdoc.rdoc_files.include('lib/**/*.rb')
|
105
105
|
rdoc.rdoc_files.exclude('lib/haml/buffer.rb')
|
106
|
+
rdoc.rdoc_files.exclude('lib/haml/util.rb')
|
106
107
|
rdoc.rdoc_files.exclude('lib/sass/tree/*')
|
107
108
|
end
|
108
109
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.
|
1
|
+
1.7.0
|
data/bin/css2sass
ADDED
data/bin/html2haml
CHANGED
@@ -1,88 +1,6 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
3
|
require File.dirname(__FILE__) + '/../lib/haml'
|
4
|
-
require 'haml/engine'
|
5
|
-
require 'rubygems'
|
6
|
-
require 'hpricot'
|
7
|
-
|
8
|
-
def tabulate(tabs)
|
9
|
-
' ' * tabs
|
10
|
-
end
|
11
|
-
|
12
|
-
TEXT_REGEXP = /^(\s*).*$/
|
13
|
-
|
14
|
-
def parse_text(text, tabs)
|
15
|
-
text.strip!
|
16
|
-
if text.empty?
|
17
|
-
String.new
|
18
|
-
else
|
19
|
-
lines = text.split("\n")
|
20
|
-
|
21
|
-
lines.map do |line|
|
22
|
-
line.strip!
|
23
|
-
"#{tabulate(tabs)}#{'\\' if Haml::Engine::SPECIAL_CHARACTERS.include?(line[0])}#{line}\n"
|
24
|
-
end.join
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
module Hpricot::Node
|
29
|
-
def to_haml(tabs)
|
30
|
-
parse_text(self.to_s, tabs)
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
class Hpricot::Doc
|
35
|
-
def to_haml
|
36
|
-
output = ''
|
37
|
-
children.each { |child| output += child.to_haml(0) }
|
38
|
-
output
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
class Hpricot::XMLDecl
|
43
|
-
def to_haml(tabs)
|
44
|
-
"#{tabulate(tabs)}!!! XML\n"
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
class Hpricot::DocType
|
49
|
-
def to_haml(tabs)
|
50
|
-
"#{tabulate(tabs)}!!!\n"
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
class Hpricot::Comment
|
55
|
-
def to_haml(tabs)
|
56
|
-
"#{tabulate(tabs)}/\n#{parse_text(self.content, tabs + 1)}"
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
|
-
class Hpricot::Elem
|
61
|
-
def to_haml(tabs)
|
62
|
-
output = "#{tabulate(tabs)}"
|
63
|
-
output += "%#{name}" unless name == 'div'
|
64
|
-
|
65
|
-
if attributes
|
66
|
-
output += "##{attributes['id']}" if attributes['id']
|
67
|
-
attributes['class'].split(' ').each { |c| output += ".#{c}" } if attributes['class']
|
68
|
-
attributes.delete("id")
|
69
|
-
attributes.delete("class")
|
70
|
-
output += attributes.inspect if attributes.length > 0
|
71
|
-
end
|
72
|
-
|
73
|
-
output += "/" if children.length == 0
|
74
|
-
output += "\n"
|
75
|
-
|
76
|
-
self.children.each do |child|
|
77
|
-
output += child.to_haml(tabs + 1)
|
78
|
-
end
|
79
|
-
|
80
|
-
output
|
81
|
-
end
|
82
|
-
end
|
83
|
-
|
84
|
-
# Must be required after Hpricot mods,
|
85
|
-
# so they're in scope
|
86
4
|
require 'haml/exec'
|
87
5
|
|
88
6
|
opts = Haml::Exec::HTML2Haml.new(ARGV)
|
data/lib/haml.rb
CHANGED
@@ -78,7 +78,7 @@ $LOAD_PATH << dir unless $LOAD_PATH.include?(dir)
|
|
78
78
|
#
|
79
79
|
# engine = Haml::Engine.new("%p Haml code!")
|
80
80
|
# engine.render #=> "<p>Haml code!</p>\n"
|
81
|
-
#
|
81
|
+
#
|
82
82
|
# == Characters with meaning to Haml
|
83
83
|
#
|
84
84
|
# Various characters, when placed at a certain point in a line,
|
@@ -181,6 +181,19 @@ $LOAD_PATH << dir unless $LOAD_PATH.include?(dir)
|
|
181
181
|
#
|
182
182
|
# <br />
|
183
183
|
# <meta http-equiv='Content-Type' content='text/html' />
|
184
|
+
#
|
185
|
+
# Some tags are automatically closed, as long as they have no content.
|
186
|
+
# +meta+, +img+, +link+, +script+, +br+, and +hr+ tags are closed by default.
|
187
|
+
# This list can be customized by setting the <tt>:autoclose</tt> option (see below).
|
188
|
+
# For example:
|
189
|
+
#
|
190
|
+
# %br
|
191
|
+
# %meta{'http-equiv' => 'Content-Type', :content => 'text/html'}
|
192
|
+
#
|
193
|
+
# is also compiled to:
|
194
|
+
#
|
195
|
+
# <br />
|
196
|
+
# <meta http-equiv='Content-Type' content='text/html' />
|
184
197
|
#
|
185
198
|
# ==== . and #
|
186
199
|
#
|
@@ -524,6 +537,20 @@ $LOAD_PATH << dir unless $LOAD_PATH.include?(dir)
|
|
524
537
|
# hi there reader!
|
525
538
|
# yo
|
526
539
|
# </p>
|
540
|
+
#
|
541
|
+
# You can also use two equal signs, <tt>==</tt>,
|
542
|
+
# along with conventional Ruby string-embedding syntax
|
543
|
+
# to easily embed Ruby code in otherwise static text.
|
544
|
+
# For example:
|
545
|
+
#
|
546
|
+
# %p
|
547
|
+
# == 1 + 1 = #{1 + 1}
|
548
|
+
#
|
549
|
+
# is compiled to:
|
550
|
+
#
|
551
|
+
# <p>
|
552
|
+
# 1 + 1 = 2
|
553
|
+
# </p>
|
527
554
|
#
|
528
555
|
# ==== -
|
529
556
|
#
|
@@ -595,8 +622,18 @@ $LOAD_PATH << dir unless $LOAD_PATH.include?(dir)
|
|
595
622
|
# <p>
|
596
623
|
# 2?
|
597
624
|
# </p>
|
625
|
+
#
|
626
|
+
# == Other Useful Things
|
627
|
+
#
|
628
|
+
# === Helpers
|
629
|
+
#
|
630
|
+
# Haml offers a bunch of helpers that are useful
|
631
|
+
# for doing stuff like preserving whitespace,
|
632
|
+
# creating nicely indented output for user-defined helpers,
|
633
|
+
# and other useful things.
|
634
|
+
# The helpers are all documented in the Haml::Helpers and Haml::Helpers::ActionViewExtensions modules.
|
598
635
|
#
|
599
|
-
#
|
636
|
+
# === Haml Options
|
600
637
|
#
|
601
638
|
# Options can be set by setting the hash <tt>Haml::Template.options</tt>
|
602
639
|
# from <tt>environment.rb</tt> in Rails,
|
@@ -608,10 +645,6 @@ $LOAD_PATH << dir unless $LOAD_PATH.include?(dir)
|
|
608
645
|
# evaluated. If this is true, said scripts are
|
609
646
|
# rendered as empty strings. Defaults to false.
|
610
647
|
#
|
611
|
-
# [<tt>:precompiled</tt>] A string containing a precompiled Haml template.
|
612
|
-
# If this is passed, <tt>template</tt> is ignored
|
613
|
-
# and no precompilation is done.
|
614
|
-
#
|
615
648
|
# [<tt>:attr_wrapper</tt>] The character that should wrap element attributes.
|
616
649
|
# This defaults to <tt>'</tt> (an apostrophe). Characters
|
617
650
|
# of this type within the attributes will be escaped
|
@@ -638,6 +671,10 @@ $LOAD_PATH << dir unless $LOAD_PATH.include?(dir)
|
|
638
671
|
# <tt>{ :foo => "bar" }</tt>, then within the template,
|
639
672
|
# <tt>= foo</tt> will produce <tt>bar</tt>.
|
640
673
|
#
|
674
|
+
# [<tt>:autoclose</tt>] A list of tag names that should be automatically self-closed
|
675
|
+
# if they have no content.
|
676
|
+
# Defaults to <tt>['meta', 'img', 'link', 'script', 'br', 'hr']</tt>.
|
677
|
+
#
|
641
678
|
module Haml; end
|
642
679
|
|
643
680
|
require 'haml/engine'
|
data/lib/haml/buffer.rb
CHANGED
@@ -15,9 +15,16 @@ module Haml
|
|
15
15
|
# _erbout for compatibility with ERB-specific code.
|
16
16
|
attr_accessor :buffer
|
17
17
|
|
18
|
-
#
|
19
|
-
|
20
|
-
|
18
|
+
# Gets the current tabulation of the document.
|
19
|
+
def tabulation
|
20
|
+
@real_tabs + @tabulation
|
21
|
+
end
|
22
|
+
|
23
|
+
# Sets the current tabulation of the document.
|
24
|
+
def tabulation=(val)
|
25
|
+
val = val - @real_tabs
|
26
|
+
@tabulation = val > -1 ? val : 0
|
27
|
+
end
|
21
28
|
|
22
29
|
# Creates a new buffer.
|
23
30
|
def initialize(options = {})
|
@@ -27,17 +34,16 @@ module Haml
|
|
27
34
|
@buffer = ""
|
28
35
|
@one_liner_pending = false
|
29
36
|
@tabulation = 0
|
37
|
+
|
38
|
+
# The number of tabs that Engine thinks we should have
|
39
|
+
# @real_tabs + @tabulation is the number of tabs actually output
|
40
|
+
@real_tabs = 0
|
30
41
|
end
|
31
42
|
|
32
43
|
# Renders +text+ with the proper tabulation. This also deals with
|
33
44
|
# making a possible one-line tag one line or not.
|
34
|
-
def push_text(text, tabulation
|
35
|
-
if
|
36
|
-
# In this case, tabulation is the number of spaces, rather
|
37
|
-
# than the number of tabs.
|
38
|
-
@buffer << "#{' ' * tabulation}#{flatten(text + "\n")}"
|
39
|
-
@one_liner_pending = true
|
40
|
-
elsif @one_liner_pending && one_liner?(text)
|
45
|
+
def push_text(text, tabulation)
|
46
|
+
if @one_liner_pending && Buffer.one_liner?(text)
|
41
47
|
@buffer << text
|
42
48
|
else
|
43
49
|
if @one_liner_pending
|
@@ -66,14 +72,21 @@ module Haml
|
|
66
72
|
end
|
67
73
|
nil
|
68
74
|
end
|
75
|
+
|
76
|
+
def open_prerendered_tag(tag, tabulation)
|
77
|
+
@buffer << "#{tabs(tabulation)}#{tag}"
|
78
|
+
@real_tabs += 1
|
79
|
+
end
|
69
80
|
|
70
81
|
# Takes the various information about the opening tag for an
|
71
82
|
# element, formats it, and adds it to the buffer.
|
72
|
-
def open_tag(name, tabulation, atomic, try_one_line, class_id,
|
73
|
-
attributes =
|
74
|
-
|
75
|
-
|
76
|
-
|
83
|
+
def open_tag(name, tabulation, atomic, try_one_line, class_id, obj_ref, attributes_hash)
|
84
|
+
attributes = class_id
|
85
|
+
if attributes_hash
|
86
|
+
attributes_hash.keys.each { |key| attributes_hash[key.to_s] = attributes_hash.delete(key) }
|
87
|
+
self.class.merge_attrs(attributes, attributes_hash)
|
88
|
+
end
|
89
|
+
self.class.merge_attrs(attributes, parse_object_ref(obj_ref)) if obj_ref
|
77
90
|
|
78
91
|
@one_liner_pending = false
|
79
92
|
if atomic
|
@@ -81,12 +94,24 @@ module Haml
|
|
81
94
|
elsif try_one_line
|
82
95
|
@one_liner_pending = true
|
83
96
|
str = ">"
|
84
|
-
elsif flattened
|
85
|
-
str = ">
"
|
86
97
|
else
|
87
98
|
str = ">\n"
|
88
99
|
end
|
89
100
|
@buffer << "#{tabs(tabulation)}<#{name}#{build_attributes(attributes)}#{str}"
|
101
|
+
@real_tabs += 1
|
102
|
+
end
|
103
|
+
|
104
|
+
def self.merge_attrs(to, from)
|
105
|
+
if to['id'] && from['id']
|
106
|
+
to['id'] << '_' << from.delete('id')
|
107
|
+
end
|
108
|
+
|
109
|
+
if to['class'] && from['class']
|
110
|
+
# Make sure we don't duplicate class names
|
111
|
+
from['class'] = (from['class'].split(' ') | to['class'].split(' ')).join(' ')
|
112
|
+
end
|
113
|
+
|
114
|
+
to.merge!(from)
|
90
115
|
end
|
91
116
|
|
92
117
|
# Creates a closing tag with the given name.
|
@@ -107,6 +132,7 @@ module Haml
|
|
107
132
|
@one_liner_pending = true
|
108
133
|
else
|
109
134
|
@buffer << "\n"
|
135
|
+
@real_tabs += 1
|
110
136
|
end
|
111
137
|
end
|
112
138
|
|
@@ -120,60 +146,9 @@ module Haml
|
|
120
146
|
push_text(close_tag, tabulation)
|
121
147
|
end
|
122
148
|
end
|
123
|
-
|
124
|
-
# Stops parsing a flat section.
|
125
|
-
def stop_flat
|
126
|
-
buffer.concat("\n")
|
127
|
-
@one_liner_pending = false
|
128
|
-
end
|
129
|
-
|
130
|
-
private
|
131
|
-
|
132
|
-
# Gets <tt>count</tt> tabs. Mostly for internal use.
|
133
|
-
def tabs(count)
|
134
|
-
' ' * (count + @tabulation)
|
135
|
-
end
|
136
|
-
|
137
|
-
# Iterates through the classes and ids supplied through <tt>.</tt>
|
138
|
-
# and <tt>#</tt> syntax, and returns a hash with them as attributes,
|
139
|
-
# that can then be merged with another attributes hash.
|
140
|
-
def parse_class_and_id(list)
|
141
|
-
attributes = {}
|
142
|
-
list.scan(/([#.])([-_a-zA-Z0-9]+)/) do |type, property|
|
143
|
-
case type
|
144
|
-
when '.'
|
145
|
-
if attributes[:class]
|
146
|
-
attributes[:class] += " "
|
147
|
-
else
|
148
|
-
attributes[:class] = ""
|
149
|
-
end
|
150
|
-
attributes[:class] += property
|
151
|
-
when '#'
|
152
|
-
attributes[:id] = property
|
153
|
-
end
|
154
|
-
end
|
155
|
-
attributes
|
156
|
-
end
|
157
|
-
|
158
|
-
# Takes an array of objects and uses the class and id of the first
|
159
|
-
# one to create an attributes hash.
|
160
|
-
def parse_object_ref(ref, old_id, old_class)
|
161
|
-
ref = ref[0]
|
162
|
-
# Let's make sure the value isn't nil. If it is, return the default Hash.
|
163
|
-
return {} if ref.nil?
|
164
|
-
class_name = ref.class.to_s.underscore
|
165
|
-
id = "#{class_name}_#{ref.id}"
|
166
|
-
|
167
|
-
if old_class
|
168
|
-
class_name += " #{old_class}"
|
169
|
-
end
|
170
149
|
|
171
|
-
|
172
|
-
|
173
|
-
end
|
174
|
-
|
175
|
-
{:id => id, :class => class_name}
|
176
|
-
end
|
150
|
+
# Some of these methods are exposed as public class methods
|
151
|
+
# so they can be re-used in helpers.
|
177
152
|
|
178
153
|
# Takes a hash and builds a list of XHTML attributes from it, returning
|
179
154
|
# the result.
|
@@ -194,12 +169,46 @@ module Haml
|
|
194
169
|
end
|
195
170
|
result.sort.join
|
196
171
|
end
|
197
|
-
|
172
|
+
|
198
173
|
# Returns whether or not the given value is short enough to be rendered
|
199
174
|
# on one line.
|
200
|
-
def one_liner?(value)
|
175
|
+
def self.one_liner?(value)
|
201
176
|
value.length <= ONE_LINER_LENGTH && value.scan(/\n/).empty?
|
202
177
|
end
|
178
|
+
|
179
|
+
private
|
180
|
+
|
181
|
+
@@tab_cache = {}
|
182
|
+
# Gets <tt>count</tt> tabs. Mostly for internal use.
|
183
|
+
def tabs(count)
|
184
|
+
@real_tabs = count
|
185
|
+
tabs = count + @tabulation
|
186
|
+
' ' * tabs
|
187
|
+
@@tab_cache[tabs] ||= ' ' * tabs
|
188
|
+
end
|
189
|
+
|
190
|
+
# Takes an array of objects and uses the class and id of the first
|
191
|
+
# one to create an attributes hash.
|
192
|
+
def parse_object_ref(ref)
|
193
|
+
ref = ref[0]
|
194
|
+
# Let's make sure the value isn't nil. If it is, return the default Hash.
|
195
|
+
return {} if ref.nil?
|
196
|
+
class_name = underscore(ref.class)
|
197
|
+
id = "#{class_name}_#{ref.id || 'new'}"
|
198
|
+
|
199
|
+
{'id' => id, 'class' => class_name}
|
200
|
+
end
|
201
|
+
|
202
|
+
# Changes a word from camel case to underscores.
|
203
|
+
# Based on the method of the same name in Rails' Inflector,
|
204
|
+
# but copied here so it'll run properly without Rails.
|
205
|
+
def underscore(camel_cased_word)
|
206
|
+
camel_cased_word.to_s.gsub(/::/, '_').
|
207
|
+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
208
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
209
|
+
tr("-", "_").
|
210
|
+
downcase
|
211
|
+
end
|
203
212
|
end
|
204
213
|
end
|
205
214
|
|
data/lib/haml/engine.rb
CHANGED
@@ -2,6 +2,7 @@ require 'haml/helpers'
|
|
2
2
|
require 'haml/buffer'
|
3
3
|
require 'haml/filters'
|
4
4
|
require 'haml/error'
|
5
|
+
require 'haml/util'
|
5
6
|
|
6
7
|
module Haml
|
7
8
|
# This is the class where all the parsing and processing of the Haml
|
@@ -13,9 +14,6 @@ module Haml
|
|
13
14
|
# output = haml_engine.to_html
|
14
15
|
# puts output
|
15
16
|
class Engine
|
16
|
-
# Allow access to the precompiled template
|
17
|
-
attr_reader :precompiled
|
18
|
-
|
19
17
|
# Allow reading and writing of the options hash
|
20
18
|
attr :options, true
|
21
19
|
|
@@ -93,13 +91,16 @@ module Haml
|
|
93
91
|
MID_BLOCK_KEYWORDS = ['else', 'elsif', 'rescue', 'ensure', 'when']
|
94
92
|
|
95
93
|
# The Regex that matches an HTML comment command.
|
96
|
-
COMMENT_REGEX = /\/(\[[
|
94
|
+
COMMENT_REGEX = /\/(\[[\w\s\.]*\])?(.*)/
|
97
95
|
|
98
96
|
# The Regex that matches a Doctype command.
|
99
|
-
DOCTYPE_REGEX = /(
|
97
|
+
DOCTYPE_REGEX = /(\d\.\d)?[\s]*([a-z]*)/i
|
100
98
|
|
101
99
|
# The Regex that matches an HTML tag command.
|
102
|
-
TAG_REGEX = /[%]([
|
100
|
+
TAG_REGEX = /[%]([-:\w]+)([-\w\.\#]*)(\{.*\})?(\[.*\])?([=\/\~]?)?(.*)?/
|
101
|
+
|
102
|
+
# The Regex that matches a literal string or symbol value
|
103
|
+
LITERAL_VALUE_REGEX = /^\s*(:(\w*)|(('|")([^\\\#]*?)\4))\s*$/
|
103
104
|
|
104
105
|
FLAT_WARNING = <<END
|
105
106
|
Haml deprecation warning:
|
@@ -109,7 +110,7 @@ helper instead.
|
|
109
110
|
END
|
110
111
|
|
111
112
|
# Creates a new instace of Haml::Engine that will compile the given
|
112
|
-
# template string when <tt>
|
113
|
+
# template string when <tt>render</tt> is called.
|
113
114
|
# See README for available options.
|
114
115
|
#
|
115
116
|
#--
|
@@ -117,16 +118,16 @@ END
|
|
117
118
|
# to README!
|
118
119
|
#++
|
119
120
|
#
|
120
|
-
def initialize(template,
|
121
|
+
def initialize(template, l_options = {})
|
121
122
|
@options = {
|
122
123
|
:suppress_eval => false,
|
123
124
|
:attr_wrapper => "'",
|
124
125
|
:locals => {},
|
126
|
+
:autoclose => ['meta', 'img', 'link', 'script', 'br', 'hr'],
|
125
127
|
:filters => {
|
126
128
|
'sass' => Sass::Engine,
|
127
129
|
'plain' => Haml::Filters::Plain,
|
128
|
-
'preserve' => Haml::Filters::Preserve
|
129
|
-
}
|
130
|
+
'preserve' => Haml::Filters::Preserve }
|
130
131
|
}
|
131
132
|
|
132
133
|
if !NOT_LOADED.include? 'redcloth'
|
@@ -140,7 +141,7 @@ END
|
|
140
141
|
@options[:filters]['markdown'] = Haml::Filters::Markdown
|
141
142
|
end
|
142
143
|
|
143
|
-
@options.rec_merge!
|
144
|
+
@options.rec_merge! l_options
|
144
145
|
|
145
146
|
unless @options[:suppress_eval]
|
146
147
|
@options[:filters].merge!({
|
@@ -148,9 +149,7 @@ END
|
|
148
149
|
'ruby' => Haml::Filters::Ruby
|
149
150
|
})
|
150
151
|
end
|
151
|
-
@options[:filters].rec_merge!
|
152
|
-
|
153
|
-
@precompiled = @options[:precompiled]
|
152
|
+
@options[:filters].rec_merge! l_options[:filters] if l_options[:filters]
|
154
153
|
|
155
154
|
@template = template.strip #String
|
156
155
|
@to_close_stack = []
|
@@ -165,7 +164,13 @@ END
|
|
165
164
|
begin
|
166
165
|
# Only do the first round of pre-compiling if we really need to.
|
167
166
|
# They might be passing in the precompiled string.
|
168
|
-
|
167
|
+
requires_precompile = true
|
168
|
+
if @@method_names[@template]
|
169
|
+
# Check that the compiled method supports a superset of the local assigns we want to do
|
170
|
+
supported_assigns = @@supported_local_assigns[@template]
|
171
|
+
requires_precompile = !@options[:locals].keys.all? {|var| supported_assigns.include? var}
|
172
|
+
end
|
173
|
+
do_precompile if requires_precompile
|
169
174
|
rescue Haml::Error => e
|
170
175
|
e.add_backtrace_entry(@index, @options[:filename])
|
171
176
|
raise e
|
@@ -177,17 +182,7 @@ END
|
|
177
182
|
@scope_object = scope
|
178
183
|
@buffer = Haml::Buffer.new(@options)
|
179
184
|
|
180
|
-
|
181
|
-
|
182
|
-
# Get inside the view object's world
|
183
|
-
@scope_object.instance_eval do
|
184
|
-
# Set all the local assigns
|
185
|
-
local_assigns.each do |key,val|
|
186
|
-
self.class.send(:define_method, key) { val }
|
187
|
-
end
|
188
|
-
end
|
189
|
-
|
190
|
-
# Compile the @precompiled buffer
|
185
|
+
# Run the compiled evaluator function
|
191
186
|
compile &block
|
192
187
|
|
193
188
|
# Return the result string
|
@@ -196,17 +191,36 @@ END
|
|
196
191
|
|
197
192
|
alias_method :to_html, :render
|
198
193
|
|
199
|
-
|
194
|
+
# This method is deprecated and shouldn't be used.
|
195
|
+
def precompiled
|
196
|
+
$stderr.puts <<END
|
197
|
+
The Haml precompiled method and :precompiled option
|
198
|
+
are deprecated and will be removed in version 2.0.
|
199
|
+
Haml::Engine now automatically handles caching.
|
200
|
+
END
|
201
|
+
nil
|
202
|
+
end
|
200
203
|
|
204
|
+
private
|
205
|
+
|
201
206
|
#Precompile each line
|
202
207
|
def do_precompile
|
208
|
+
@precompiled = ''
|
209
|
+
method_name = assign_method_name(@template, options[:filename])
|
203
210
|
push_silent <<-END
|
204
|
-
def
|
205
|
-
|
206
|
-
|
207
|
-
|
211
|
+
def #{method_name}(_haml_local_assigns)
|
212
|
+
@haml_is_haml = true
|
213
|
+
_hamlout = @haml_stack[-1]
|
214
|
+
_erbout = _hamlout.buffer
|
208
215
|
END
|
209
216
|
|
217
|
+
supported_local_assigns = {}
|
218
|
+
@@supported_local_assigns[@template] = supported_local_assigns
|
219
|
+
@options[:locals].each do |k,v|
|
220
|
+
supported_local_assigns[k] = true
|
221
|
+
push_silent "#{k} = _haml_local_assigns[:#{k}]"
|
222
|
+
end
|
223
|
+
|
210
224
|
old_line = nil
|
211
225
|
old_index = nil
|
212
226
|
old_spaces = nil
|
@@ -236,7 +250,7 @@ END
|
|
236
250
|
|
237
251
|
if flat
|
238
252
|
push_flat(old_uline, old_spaces)
|
239
|
-
elsif !line_empty
|
253
|
+
elsif !line_empty && !@haml_comment
|
240
254
|
process_line(old_line, old_index, block_opened)
|
241
255
|
end
|
242
256
|
|
@@ -266,8 +280,7 @@ END
|
|
266
280
|
# Close all the open tags
|
267
281
|
@template_tabs.times { close }
|
268
282
|
|
269
|
-
push_silent "@haml_is_haml = false"
|
270
|
-
push_silent "end"
|
283
|
+
push_silent "@haml_is_haml = false\nend\n"
|
271
284
|
end
|
272
285
|
|
273
286
|
# Processes and deals with lowering indentation.
|
@@ -302,12 +315,11 @@ END
|
|
302
315
|
when SCRIPT
|
303
316
|
sub_line = line[1..-1]
|
304
317
|
if sub_line[0] == SCRIPT
|
305
|
-
push_script(sub_line[1..-1].strip
|
318
|
+
push_script(unescape_interpolation(sub_line[1..-1].strip), false)
|
306
319
|
else
|
307
320
|
push_script(sub_line, false)
|
308
321
|
end
|
309
322
|
when FLAT_SCRIPT
|
310
|
-
warn(FLAT_WARNING) unless defined?(Test::Unit)
|
311
323
|
push_flat_script(line[1..-1])
|
312
324
|
when SILENT_SCRIPT
|
313
325
|
sub_line = line[1..-1]
|
@@ -317,6 +329,8 @@ END
|
|
317
329
|
if (@block_opened && !mbk) || line[1..-1].split(' ', 2)[0] == "case"
|
318
330
|
push_and_tabulate([:script])
|
319
331
|
end
|
332
|
+
else
|
333
|
+
start_haml_comment
|
320
334
|
end
|
321
335
|
when FILTER
|
322
336
|
name = line[1..-1].downcase
|
@@ -374,6 +388,23 @@ END
|
|
374
388
|
def is_multiline?(line) # ' '[0] == 32
|
375
389
|
line && line.length > 1 && line[-1] == MULTILINE_CHAR_VALUE && line[-2] == 32
|
376
390
|
end
|
391
|
+
|
392
|
+
# Method for generating compiled method names basically ripped out of ActiveView::Base
|
393
|
+
# If Haml is to be used as a standalone module without rails and still use the precompiled
|
394
|
+
# methods technique, it will end up duplicating this stuff. I can't decide whether
|
395
|
+
# checking compile times to decide whether to recompile a template belongs in here or
|
396
|
+
# out in template.rb
|
397
|
+
@@method_names = {}
|
398
|
+
@@supported_local_assigns = {}
|
399
|
+
@@render_method_count = 0
|
400
|
+
def assign_method_name(template, file_name)
|
401
|
+
@@render_method_count += 1
|
402
|
+
@@method_names[template] = "_render_haml_#{@@render_method_count}".intern
|
403
|
+
end
|
404
|
+
|
405
|
+
module CompiledTemplates
|
406
|
+
# holds compiled template code
|
407
|
+
end
|
377
408
|
|
378
409
|
# Takes <tt>@precompiled</tt>, a string buffer of Ruby code, and
|
379
410
|
# evaluates it in the context of <tt>@scope_object</tt>, after preparing
|
@@ -391,11 +422,17 @@ END
|
|
391
422
|
attr :haml_lineno # :nodoc:
|
392
423
|
end
|
393
424
|
end
|
425
|
+
@scope_object.class.instance_eval do
|
426
|
+
include CompiledTemplates
|
427
|
+
end
|
394
428
|
|
395
429
|
begin
|
396
|
-
|
397
|
-
|
398
|
-
@scope_object.
|
430
|
+
method_name = @@method_names[@template]
|
431
|
+
|
432
|
+
unless @scope_object.respond_to?(method_name)
|
433
|
+
CompiledTemplates.module_eval @precompiled
|
434
|
+
end
|
435
|
+
@scope_object.send(method_name, options[:locals], &block)
|
399
436
|
rescue Exception => e
|
400
437
|
class << e
|
401
438
|
include Haml::Error
|
@@ -408,9 +445,13 @@ END
|
|
408
445
|
compile_error = e.message.scan(/\(eval\):([0-9]*):in `[-_a-zA-Z]*': compile error/)[0]
|
409
446
|
|
410
447
|
if compile_error
|
411
|
-
|
412
|
-
|
413
|
-
|
448
|
+
if @precompiled
|
449
|
+
eval_line = compile_error[0].to_i
|
450
|
+
line_marker = @precompiled.split("\n")[0...eval_line].grep(/@haml_lineno = [0-9]*/)[-1]
|
451
|
+
lineno = line_marker.scan(/[0-9]+/)[0].to_i if line_marker
|
452
|
+
else
|
453
|
+
lineno = -1
|
454
|
+
end
|
414
455
|
end
|
415
456
|
|
416
457
|
e.add_backtrace_entry(lineno, @options[:filename])
|
@@ -426,7 +467,7 @@ END
|
|
426
467
|
# Evaluates <tt>text</tt> in the context of <tt>@scope_object</tt>, but
|
427
468
|
# does not output the result.
|
428
469
|
def push_silent(text, add_index = false, can_suppress = false)
|
429
|
-
unless can_suppress &&
|
470
|
+
unless (can_suppress && options[:suppress_eval])
|
430
471
|
if add_index
|
431
472
|
@precompiled << "@haml_lineno = #{@index}\n#{text}\n"
|
432
473
|
else
|
@@ -455,11 +496,7 @@ END
|
|
455
496
|
def push_flat(text, spaces)
|
456
497
|
tabulation = spaces - @flat_spaces
|
457
498
|
tabulation = tabulation > -1 ? tabulation : 0
|
458
|
-
|
459
|
-
@filter_buffer << "#{' ' * tabulation}#{text}\n"
|
460
|
-
else
|
461
|
-
@precompiled << "_hamlout.push_text(#{text.dump}, #{tabulation}, true)\n"
|
462
|
-
end
|
499
|
+
@filter_buffer << "#{' ' * tabulation}#{text}\n"
|
463
500
|
end
|
464
501
|
|
465
502
|
# Causes <tt>text</tt> to be evaluated in the context of
|
@@ -482,13 +519,17 @@ END
|
|
482
519
|
# Causes <tt>text</tt> to be evaluated, and Haml::Helpers#find_and_flatten
|
483
520
|
# to be run on it afterwards.
|
484
521
|
def push_flat_script(text)
|
485
|
-
|
486
|
-
|
522
|
+
if text.empty?
|
523
|
+
raise SyntaxError.new("Tag has no content.")
|
487
524
|
else
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
525
|
+
push_script(text, true)
|
526
|
+
end
|
527
|
+
end
|
528
|
+
|
529
|
+
def start_haml_comment
|
530
|
+
if @block_opened
|
531
|
+
@haml_comment = true
|
532
|
+
push_and_tabulate([:haml_comment])
|
492
533
|
end
|
493
534
|
end
|
494
535
|
|
@@ -502,12 +543,12 @@ END
|
|
502
543
|
close_comment value
|
503
544
|
when :element
|
504
545
|
close_tag value
|
505
|
-
when :flat
|
506
|
-
close_flat value
|
507
546
|
when :loud
|
508
547
|
close_loud value
|
509
548
|
when :filtered
|
510
549
|
close_filtered value
|
550
|
+
when :haml_comment
|
551
|
+
close_haml_comment
|
511
552
|
end
|
512
553
|
end
|
513
554
|
|
@@ -532,17 +573,6 @@ END
|
|
532
573
|
push_silent "_hamlout.close_comment(#{has_conditional}, #{@output_tabs})"
|
533
574
|
end
|
534
575
|
|
535
|
-
# Closes a flattened section.
|
536
|
-
def close_flat(in_tag)
|
537
|
-
@flat_spaces = -1
|
538
|
-
if in_tag
|
539
|
-
close
|
540
|
-
else
|
541
|
-
push_silent('_hamlout.stop_flat')
|
542
|
-
@template_tabs -= 1
|
543
|
-
end
|
544
|
-
end
|
545
|
-
|
546
576
|
# Closes a loud Ruby block.
|
547
577
|
def close_loud(command)
|
548
578
|
push_silent 'end', false, true
|
@@ -573,43 +603,167 @@ END
|
|
573
603
|
@template_tabs -= 1
|
574
604
|
end
|
575
605
|
|
606
|
+
def close_haml_comment
|
607
|
+
@haml_comment = false
|
608
|
+
@template_tabs -= 1
|
609
|
+
end
|
610
|
+
|
611
|
+
# Iterates through the classes and ids supplied through <tt>.</tt>
|
612
|
+
# and <tt>#</tt> syntax, and returns a hash with them as attributes,
|
613
|
+
# that can then be merged with another attributes hash.
|
614
|
+
def parse_class_and_id(list)
|
615
|
+
attributes = {}
|
616
|
+
list.scan(/([#.])([-_a-zA-Z0-9]+)/) do |type, property|
|
617
|
+
case type
|
618
|
+
when '.'
|
619
|
+
if attributes['class']
|
620
|
+
attributes['class'] += " "
|
621
|
+
else
|
622
|
+
attributes['class'] = ""
|
623
|
+
end
|
624
|
+
attributes['class'] += property
|
625
|
+
when '#'
|
626
|
+
attributes['id'] = property
|
627
|
+
end
|
628
|
+
end
|
629
|
+
attributes
|
630
|
+
end
|
631
|
+
|
632
|
+
def parse_literal_value(text)
|
633
|
+
text.match(LITERAL_VALUE_REGEX)
|
634
|
+
|
635
|
+
# $2 holds the value matched by a symbol, but is nil for a string match
|
636
|
+
# $5 holds the value matched by a string
|
637
|
+
$2 || $5
|
638
|
+
end
|
639
|
+
|
640
|
+
def parse_literal_hash(text)
|
641
|
+
unless text
|
642
|
+
return {}
|
643
|
+
end
|
644
|
+
|
645
|
+
attributes = {}
|
646
|
+
if inner = text.scan(/^\{(.*)\}$/)[0]
|
647
|
+
inner[0].split(',').each do |attrib|
|
648
|
+
key, value, more = attrib.split('=>')
|
649
|
+
|
650
|
+
# Make sure the key and value and only the key and value exist
|
651
|
+
# Otherwise, it's too complicated and we'll defer it to the actual Ruby parser
|
652
|
+
if more || (key = parse_literal_value(key)).nil? ||
|
653
|
+
(value = parse_literal_value(value)).nil?
|
654
|
+
return nil
|
655
|
+
end
|
656
|
+
|
657
|
+
attributes[key] = value
|
658
|
+
end
|
659
|
+
end
|
660
|
+
attributes
|
661
|
+
end
|
662
|
+
|
663
|
+
def build_attributes(attributes = {})
|
664
|
+
@quote_escape = @options[:attr_wrapper] == '"' ? """ : "'"
|
665
|
+
@other_quote_char = @options[:attr_wrapper] == '"' ? "'" : '"'
|
666
|
+
|
667
|
+
result = attributes.collect do |a,v|
|
668
|
+
v = v.to_s
|
669
|
+
unless v.nil? || v.empty?
|
670
|
+
attr_wrapper = @options[:attr_wrapper]
|
671
|
+
if v.include? attr_wrapper
|
672
|
+
if v.include? @other_quote_char
|
673
|
+
# An imperfection in LITERAL_VALUE_REGEX prevents this
|
674
|
+
# from ever actually being reached,
|
675
|
+
# but in case it becomes possible,
|
676
|
+
# I'm leaving it in.
|
677
|
+
v = v.gsub(attr_wrapper, @quote_escape)
|
678
|
+
else
|
679
|
+
attr_wrapper = @other_quote_char
|
680
|
+
end
|
681
|
+
end
|
682
|
+
" #{a}=#{attr_wrapper}#{v}#{attr_wrapper}"
|
683
|
+
end
|
684
|
+
end
|
685
|
+
result.sort.join
|
686
|
+
end
|
687
|
+
|
688
|
+
def prerender_tag(name, atomic, attributes)
|
689
|
+
if atomic
|
690
|
+
str = " />"
|
691
|
+
else
|
692
|
+
str = ">"
|
693
|
+
end
|
694
|
+
|
695
|
+
"<#{name}#{build_attributes(attributes)}#{str}"
|
696
|
+
end
|
697
|
+
|
576
698
|
# Parses a line that will render as an XHTML tag, and adds the code that will
|
577
699
|
# render that tag to <tt>@precompiled</tt>.
|
578
700
|
def render_tag(line)
|
579
701
|
matched = false
|
580
702
|
line.scan(TAG_REGEX) do |tag_name, attributes, attributes_hash, object_ref, action, value|
|
581
703
|
matched = true
|
582
|
-
value = value.to_s
|
704
|
+
value = value.to_s.strip
|
583
705
|
|
584
706
|
case action
|
585
707
|
when '/'
|
586
708
|
atomic = true
|
587
709
|
when '=', '~'
|
588
710
|
parse = true
|
589
|
-
|
590
|
-
value =
|
711
|
+
|
712
|
+
if value.first == '='
|
713
|
+
value = value[1..-1].strip.dump.gsub('\\#', '#')
|
714
|
+
end
|
591
715
|
end
|
592
716
|
|
593
717
|
flattened = (action == '~')
|
594
718
|
|
595
|
-
warn(FLAT_WARNING) if flattened && !defined?(Test::Unit)
|
596
|
-
|
597
719
|
value_exists = !value.empty?
|
598
|
-
|
720
|
+
literal_attributes = parse_literal_hash(attributes_hash)
|
721
|
+
attributes_hash = "{nil}" if attributes_hash.nil? || literal_attributes || @options[:suppress_eval]
|
599
722
|
object_ref = "nil" if object_ref.nil? || @options[:suppress_eval]
|
600
723
|
|
601
|
-
if
|
724
|
+
if !attributes.empty? && '.#'.include?(attributes)
|
725
|
+
raise SyntaxError.new("Illegal element: classes and ids must have values. Use %div instead.")
|
726
|
+
end
|
727
|
+
|
728
|
+
# Preparse the attributes hash
|
729
|
+
attributes = parse_class_and_id(attributes)
|
730
|
+
Buffer.merge_attrs(attributes, literal_attributes) if literal_attributes
|
731
|
+
|
732
|
+
if @block_opened
|
602
733
|
if atomic
|
603
734
|
raise SyntaxError.new("Illegal Nesting: Nesting within an atomic tag is illegal.")
|
604
735
|
elsif action == '=' || value_exists
|
605
736
|
raise SyntaxError.new("Illegal Nesting: Nesting within a tag that already has content is illegal.")
|
606
737
|
end
|
738
|
+
elsif atomic && value_exists
|
739
|
+
raise SyntaxError.new("Atomic tags can't have content.")
|
607
740
|
elsif parse && !value_exists
|
608
|
-
raise SyntaxError.new("
|
741
|
+
raise SyntaxError.new("Tag has no content.")
|
609
742
|
end
|
610
743
|
|
611
|
-
|
612
|
-
|
744
|
+
if !@block_opened && !value_exists && @options[:autoclose].include?(tag_name)
|
745
|
+
atomic = true
|
746
|
+
end
|
747
|
+
|
748
|
+
do_one_liner = value_exists && !parse && Buffer.one_liner?(value)
|
749
|
+
|
750
|
+
if object_ref == "nil" && attributes_hash == "{nil}" && !flattened && (do_one_liner || !value_exists)
|
751
|
+
# This means that we can render the tag directly to text and not process it in the buffer
|
752
|
+
open_tag = prerender_tag(tag_name, atomic, attributes)
|
753
|
+
|
754
|
+
if do_one_liner
|
755
|
+
open_tag += value
|
756
|
+
open_tag += "</#{tag_name}>"
|
757
|
+
end
|
758
|
+
|
759
|
+
open_tag += "\n"
|
760
|
+
|
761
|
+
push_silent "_hamlout.open_prerendered_tag(#{open_tag.dump}, #{@output_tabs})"
|
762
|
+
return if do_one_liner
|
763
|
+
else
|
764
|
+
push_silent "_hamlout.open_tag(#{tag_name.inspect}, #{@output_tabs}, #{atomic.inspect}, #{value_exists.inspect}, #{attributes.inspect}, #{object_ref}, #{attributes_hash[1...-1]})", true
|
765
|
+
end
|
766
|
+
|
613
767
|
unless atomic
|
614
768
|
push_and_tabulate([:element, tag_name])
|
615
769
|
@output_tabs += 1
|
@@ -622,7 +776,7 @@ END
|
|
622
776
|
end
|
623
777
|
close
|
624
778
|
elsif flattened
|
625
|
-
|
779
|
+
raise SyntaxError.new("Tag has no content.")
|
626
780
|
end
|
627
781
|
end
|
628
782
|
end
|
@@ -684,18 +838,6 @@ END
|
|
684
838
|
end
|
685
839
|
push_text doctype
|
686
840
|
end
|
687
|
-
|
688
|
-
# Starts a flattened block.
|
689
|
-
def start_flat(in_tag)
|
690
|
-
# @flat_spaces is the number of indentations in the template
|
691
|
-
# that forms the base of the flattened area
|
692
|
-
if in_tag
|
693
|
-
@to_close_stack.push([:flat, true])
|
694
|
-
else
|
695
|
-
push_and_tabulate([:flat])
|
696
|
-
end
|
697
|
-
@flat_spaces = @template_tabs * 2
|
698
|
-
end
|
699
841
|
|
700
842
|
# Starts a filtered block.
|
701
843
|
def start_filtered(filter)
|
@@ -707,10 +849,18 @@ END
|
|
707
849
|
@filter_buffer = String.new
|
708
850
|
end
|
709
851
|
|
852
|
+
def unescape_interpolation(str)
|
853
|
+
str.dump.gsub('\\#', '#').gsub(/\#\{[^\}]+\}/) do |substr|
|
854
|
+
substr.gsub('\\"', '"')
|
855
|
+
end
|
856
|
+
end
|
857
|
+
|
710
858
|
# Counts the tabulation of a line.
|
711
859
|
def count_soft_tabs(line)
|
712
860
|
spaces = line.index(/[^ ]/)
|
713
861
|
if line[spaces] == ?\t
|
862
|
+
return nil if line.strip.empty?
|
863
|
+
|
714
864
|
raise SyntaxError.new("Illegal Indentation: Only two space characters are allowed as tabulation.")
|
715
865
|
end
|
716
866
|
[spaces, spaces/2]
|
@@ -724,23 +874,3 @@ END
|
|
724
874
|
end
|
725
875
|
end
|
726
876
|
end
|
727
|
-
|
728
|
-
class Hash # :nodoc:
|
729
|
-
# Same as Hash#merge!, but recursively merges sub-hashes.
|
730
|
-
def rec_merge!(other)
|
731
|
-
other.each do |key, value|
|
732
|
-
myval = self[key]
|
733
|
-
if value.is_a?(Hash) && myval.is_a?(Hash)
|
734
|
-
myval.rec_merge!(value)
|
735
|
-
else
|
736
|
-
self[key] = value
|
737
|
-
end
|
738
|
-
end
|
739
|
-
self
|
740
|
-
end
|
741
|
-
|
742
|
-
def rec_merge(other)
|
743
|
-
toret = self.clone
|
744
|
-
toret.rec_merge! other
|
745
|
-
end
|
746
|
-
end
|