hamlet 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.md +15 -5
- data/hamlet.gemspec +5 -2
- data/lib/hamlet/forked_slim_parser.rb +0 -6
- data/lib/hamlet/parser.rb +114 -66
- data/test/slim/test_chain_manipulation.rb +2 -2
- data/test/slim/test_code_blocks.rb +10 -10
- data/test/slim/test_code_escaping.rb +4 -4
- data/test/slim/test_code_evaluation.rb +36 -36
- data/test/slim/test_code_output.rb +22 -20
- data/test/slim/test_code_structure.rb +21 -21
- data/test/slim/test_embedded_engines.rb +16 -16
- data/test/slim/test_html_escaping.rb +6 -7
- data/test/slim/test_html_structure.rb +51 -52
- data/test/slim/test_parser_errors.rb +29 -27
- data/test/slim/test_pretty.rb +10 -8
- data/test/slim/test_ruby_errors.rb +13 -11
- data/test/slim/test_sections.rb +5 -5
- data/test/slim/test_slim_template.rb +3 -3
- data/test/slim/test_text_interpolation.rb +5 -5
- data/test/slim/test_wrapper.rb +2 -2
- metadata +34 -24
data/README.md
CHANGED
@@ -29,22 +29,32 @@ see, it is just HTML! Designers love Hamlet because it is just HTML! Closing tag
|
|
29
29
|
You can see the [original hamlet templating langauge](http://www.yesodweb.com/book/templates) and the
|
30
30
|
[javascript port](hamlet: https://github.com/gregwebs/hamlet.js).
|
31
31
|
|
32
|
-
This Hamlet works on top of [slim](https://github.com/stonean/slim/). Please
|
32
|
+
This Hamlet works on top of [slim](https://github.com/stonean/slim/). Please take a look at the [slim documentation](http://slim-lang.com) if you are looking to see if a more advanced feature is supported.
|
33
33
|
|
34
|
-
|
34
|
+
## Difference with Slim
|
35
|
+
|
36
|
+
The most important difference is that hamlet always uses angle brackets. Hamlet also does not require attributes to be quoted - unquoted is considered a normal html attribute value and quotes will be added. Hamlet also uses a '#' for code comments and the normal <!-- for HTML comments. Hamlet also uses different whitespace indicators - see the next section.
|
37
|
+
|
38
|
+
In Slim you have:
|
39
|
+
|
40
|
+
/! HTML comment
|
41
|
+
p data-attr="foo" Text
|
35
42
|
| More Text
|
43
|
+
/ Comment
|
36
44
|
|
37
45
|
In hamlet you have:
|
38
46
|
|
47
|
+
<!-- HTML comment
|
39
48
|
<p data-attr=foo>Text
|
40
49
|
More Text
|
50
|
+
# Comment
|
41
51
|
|
42
52
|
## Whitespace
|
43
53
|
|
44
|
-
|
54
|
+
I added the same syntax that hamlet.js uses to indicate whitespace: a closing bracket to the left and a code comment to the right. I will probably take out some of the slim white space techniques.
|
45
55
|
|
46
|
-
<p> White space to the left
|
47
|
-
> White space to the left again
|
56
|
+
<p> White space to the left # and to the right
|
57
|
+
> White space to the left again# None to the right
|
48
58
|
|
49
59
|
|
50
60
|
## Limitations
|
data/hamlet.gemspec
CHANGED
@@ -4,7 +4,7 @@ require 'date'
|
|
4
4
|
|
5
5
|
Gem::Specification.new do |s|
|
6
6
|
s.name = 'hamlet'
|
7
|
-
s.version = '0.
|
7
|
+
s.version = '0.3.0'
|
8
8
|
s.date = Date.today.to_s
|
9
9
|
s.authors = ['Greg Weber']
|
10
10
|
s.email = ['greg@gregweber.info']
|
@@ -16,7 +16,7 @@ Gem::Specification.new do |s|
|
|
16
16
|
s.rubyforge_project = s.name
|
17
17
|
|
18
18
|
s.files = `git ls-files`.split("\n")
|
19
|
-
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
#s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
20
|
s.require_paths = %w(lib)
|
21
21
|
|
22
22
|
s.add_dependency('slim', ['~> 1.0.0'])
|
@@ -29,6 +29,9 @@ Gem::Specification.new do |s|
|
|
29
29
|
s.add_development_dependency('creole', ['>= 0'])
|
30
30
|
s.add_development_dependency('builder', ['>= 0'])
|
31
31
|
s.add_development_dependency('pry', ['>= 0'])
|
32
|
+
if RUBY_VERSION =~ /1.9/
|
33
|
+
s.add_development_dependency('ruby-debug19', ['>= 0'])
|
34
|
+
end
|
32
35
|
|
33
36
|
unless defined?(RUBY_ENGINE) && RUBY_ENGINE == 'rbx'
|
34
37
|
s.add_development_dependency('rcov', ['>= 0'])
|
@@ -74,12 +74,6 @@ module ForkedSlim
|
|
74
74
|
DELIMITER_REGEX = /\A[\(\[\{]/
|
75
75
|
ATTR_NAME_REGEX = '\A\s*(\w[:\w-]*)'
|
76
76
|
|
77
|
-
if RUBY_VERSION > '1.9'
|
78
|
-
CLASS_ID_REGEX = /\A(#|\.)([\w\u00c0-\uFFFF][\w:\u00c0-\uFFFF-]*)/
|
79
|
-
else
|
80
|
-
CLASS_ID_REGEX = /\A(#|\.)(\w[\w:-]*)/
|
81
|
-
end
|
82
|
-
|
83
77
|
def reset(lines = nil, stacks = nil)
|
84
78
|
# Since you can indent however you like in Slim, we need to keep a list
|
85
79
|
# of how deeply indented you are. For instance, in a template like this:
|
data/lib/hamlet/parser.rb
CHANGED
@@ -3,77 +3,114 @@ require 'hamlet/forked_slim_parser'
|
|
3
3
|
# @api private
|
4
4
|
module Hamlet
|
5
5
|
class Parser < ForkedSlim::Parser
|
6
|
-
|
6
|
+
if RUBY_VERSION > '1.9'
|
7
|
+
CLASS_ID_REGEX = /\A\s*(#|\.)([\w\u00c0-\uFFFF][\w:\u00c0-\uFFFF-]*)/
|
8
|
+
else
|
9
|
+
CLASS_ID_REGEX = /\A\s*(#|\.)(\w[\w:-]*)/
|
10
|
+
end
|
11
|
+
|
12
|
+
# Compile string to Temple expression
|
13
|
+
#
|
14
|
+
# @param [String] str Slim code
|
15
|
+
# @return [Array] Temple expression representing the code]]
|
16
|
+
def call(str)
|
17
|
+
# Set string encoding if option is set
|
18
|
+
if options[:encoding] && str.respond_to?(:encoding)
|
19
|
+
old = str.encoding
|
20
|
+
str = str.dup if str.frozen?
|
21
|
+
str.force_encoding(options[:encoding])
|
22
|
+
# Fall back to old encoding if new encoding is invalid
|
23
|
+
str.force_encoding(old_enc) unless str.valid_encoding?
|
24
|
+
end
|
25
|
+
|
26
|
+
result = [:multi]
|
27
|
+
reset(str.split($/), [result])
|
28
|
+
|
29
|
+
while @lines.first && @lines.first =~ /\A\s*\Z/
|
30
|
+
@stacks.last << [:newline]
|
31
|
+
next_line
|
32
|
+
end
|
33
|
+
if @lines.first and @lines.first =~ /\A<doctype\s+([^>]*)/i
|
34
|
+
if !$'.empty? and $'[0] !~ /\s*#/
|
35
|
+
fail("did not expect content after doctype")
|
36
|
+
end
|
37
|
+
@stacks.last << [:html, :doctype, $1]
|
38
|
+
next_line
|
39
|
+
end
|
40
|
+
|
41
|
+
parse_line while next_line
|
42
|
+
|
43
|
+
reset
|
44
|
+
result
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
7
48
|
|
8
49
|
def parse_line_indicators
|
9
50
|
case @line
|
10
|
-
when /\A
|
11
|
-
# Found a comment block.
|
12
|
-
if @line =~ %r{\A/!( ?)(.*)\Z}
|
13
|
-
# HTML comment
|
14
|
-
block = [:multi]
|
15
|
-
@stacks.last << [:html, :comment, block]
|
16
|
-
@stacks << block
|
17
|
-
@stacks.last << [:slim, :interpolate, $2] unless $2.empty?
|
18
|
-
parse_text_block($2.empty? ? nil : @indents.last + $1.size + 2)
|
19
|
-
elsif @line =~ %r{\A/\[\s*(.*?)\s*\]\s*\Z}
|
20
|
-
# HTML conditional comment
|
21
|
-
block = [:multi]
|
22
|
-
@stacks.last << [:slim, :condcomment, $1, block]
|
23
|
-
@stacks << block
|
24
|
-
else
|
25
|
-
# Slim comment
|
26
|
-
parse_comment_block
|
27
|
-
end
|
28
|
-
when /\A-/
|
29
|
-
# Found a code block.
|
30
|
-
# We expect the line to be broken or the next line to be indented.
|
51
|
+
when /\A-/ # code block.
|
31
52
|
block = [:multi]
|
32
53
|
@line.slice!(0)
|
33
54
|
@stacks.last << [:slim, :control, parse_broken_line, block]
|
34
55
|
@stacks << block
|
35
|
-
when /\A=/
|
36
|
-
# Found an output block.
|
37
|
-
# We expect the line to be broken or the next line to be indented.
|
56
|
+
when /\A=/ # output block.
|
38
57
|
@line =~ /\A=(=?)('?)/
|
39
58
|
@line = $'
|
40
59
|
block = [:multi]
|
41
60
|
@stacks.last << [:slim, :output, $1.empty?, parse_broken_line, block]
|
42
61
|
@stacks.last << [:static, ' '] unless $2.empty?
|
43
62
|
@stacks << block
|
44
|
-
when /\A<(\w+):\s*\Z/
|
45
|
-
# Embedded template detected. It is treated as block.
|
63
|
+
when /\A<(\w+):\s*\Z/ # Embedded template. It is treated as block.
|
46
64
|
block = [:multi]
|
47
65
|
@stacks.last << [:newline] << [:slim, :embedded, $1, block]
|
48
66
|
@stacks << block
|
49
|
-
parse_text_block
|
67
|
+
parse_text_block(nil, true)
|
50
68
|
return # Don't append newline, this has already been done before
|
51
|
-
when /\
|
52
|
-
# Found doctype declaration
|
53
|
-
@stacks.last << [:html, :doctype, $'.strip]
|
54
|
-
when /\A<([#\.]|\w[:\w-]*)/
|
55
|
-
# Found a HTML tag.
|
69
|
+
when /\A<([#\.]|\w[:\w-]*)/ # HTML tag.
|
56
70
|
parse_tag($1)
|
57
|
-
when /\A(
|
58
|
-
|
59
|
-
@stacks.last <<
|
60
|
-
|
71
|
+
when /\A<!--( ?)(.*)\Z/ # HTML comment
|
72
|
+
block = [:multi]
|
73
|
+
@stacks.last << [:html, :comment, block]
|
74
|
+
@stacks << block
|
75
|
+
@stacks.last << [:slim, :interpolate, $2] unless $2.empty?
|
76
|
+
parse_text_block($2.empty? ? nil : @indents.last + $1.size + 2)
|
77
|
+
when %r{\A#\[\s*(.*?)\s*\]\s*\Z} # HTML conditional comment
|
78
|
+
block = [:multi]
|
79
|
+
@stacks.last << [:slim, :condcomment, $1, block]
|
80
|
+
@stacks << block
|
81
|
+
when /\A(?:\s*>( *))?/ # text block.
|
82
|
+
@stacks.last << [:slim, :interpolate, $1 ? $1 << $' : $']
|
83
|
+
parse_text_block($'.empty? ? nil : @indents.last + $1.to_s.size)
|
61
84
|
else
|
62
85
|
syntax_error! 'Unknown line indicator'
|
63
86
|
end
|
64
87
|
@stacks.last << [:newline]
|
65
88
|
end
|
66
89
|
|
67
|
-
def parse_text_block(text_indent = nil)
|
90
|
+
def parse_text_block(text_indent = nil, special = nil)
|
68
91
|
empty_lines = 0
|
92
|
+
multi_line = false
|
93
|
+
if special == :from_tag
|
94
|
+
multi_line = true
|
95
|
+
special = nil
|
96
|
+
end
|
97
|
+
|
98
|
+
first_line = true
|
99
|
+
close_bracket = false
|
69
100
|
until @lines.empty?
|
70
|
-
if @lines.first =~ /\A\s*\Z/
|
101
|
+
if @lines.first =~ /\A\s*>?\s*\Z/
|
71
102
|
next_line
|
72
103
|
@stacks.last << [:newline]
|
73
104
|
empty_lines += 1 if text_indent
|
74
105
|
else
|
75
106
|
indent = get_indent(@lines.first)
|
76
107
|
break if indent <= @indents.last
|
108
|
+
if @lines.first =~ /\A\s*>/
|
109
|
+
indent += 1 #$1.size if $1
|
110
|
+
close_bracket = true
|
111
|
+
else
|
112
|
+
close_bracket = false
|
113
|
+
end
|
77
114
|
|
78
115
|
if empty_lines > 0
|
79
116
|
@stacks.last << [:slim, :interpolate, "\n" * empty_lines]
|
@@ -85,16 +122,28 @@ module Hamlet
|
|
85
122
|
# The text block lines must be at least indented
|
86
123
|
# as deep as the first line.
|
87
124
|
if text_indent && indent < text_indent
|
88
|
-
|
89
|
-
|
125
|
+
# special case for a leading '>' being back 1 char
|
126
|
+
unless first_line && close_bracket && (text_indent - indent == 1)
|
127
|
+
@line.lstrip!
|
128
|
+
syntax_error!('Unexpected text indentation')
|
129
|
+
end
|
90
130
|
end
|
91
131
|
|
92
132
|
@line.slice!(0, text_indent || indent)
|
133
|
+
@line = $' if @line =~ /\A>/
|
134
|
+
# a code comment
|
135
|
+
if @line =~ /(\A|[^\\])#([^{]|\Z)/
|
136
|
+
@line = $` + $1
|
137
|
+
end
|
138
|
+
@stacks.last << [:newline] if multi_line && !special
|
93
139
|
@stacks.last << [:slim, :interpolate, (text_indent ? "\n" : '') + @line] << [:newline]
|
94
140
|
|
95
141
|
# The indentation of first line of the text block
|
96
142
|
# determines the text base indentation.
|
97
143
|
text_indent ||= indent
|
144
|
+
|
145
|
+
first_line = false
|
146
|
+
multi_line = true
|
98
147
|
end
|
99
148
|
end
|
100
149
|
end
|
@@ -111,27 +160,25 @@ module Hamlet
|
|
111
160
|
@stacks.last << tag
|
112
161
|
|
113
162
|
case @line
|
114
|
-
when /\A
|
115
|
-
# Handle output code
|
163
|
+
when /\A=(=?)('?)/ # Handle output code
|
116
164
|
block = [:multi]
|
117
165
|
@line = $'
|
118
166
|
content = [:slim, :output, $1 != '=', parse_broken_line, block]
|
119
167
|
tag << content
|
120
168
|
@stacks.last << [:static, ' '] unless $2.empty?
|
121
169
|
@stacks << block
|
122
|
-
when /\A\s
|
123
|
-
# Closed tag. Do nothing
|
124
|
-
when /\A\s*>?\s*\Z/
|
170
|
+
when /\A\s*\Z/
|
125
171
|
# Empty content
|
126
172
|
content = [:multi]
|
127
173
|
tag << content
|
128
174
|
@stacks << content
|
129
|
-
when
|
130
|
-
#
|
131
|
-
|
175
|
+
when %r!\A/>!
|
176
|
+
# Do nothing for closing tag
|
177
|
+
else # Text content
|
178
|
+
content = [:multi, [:slim, :interpolate, @line]]
|
132
179
|
tag << content
|
133
180
|
@stacks << content
|
134
|
-
parse_text_block(@orig_line.size - @line.size
|
181
|
+
parse_text_block(@orig_line.size - @line.size, :from_tag)
|
135
182
|
end
|
136
183
|
end
|
137
184
|
|
@@ -147,44 +194,45 @@ module Hamlet
|
|
147
194
|
end
|
148
195
|
|
149
196
|
# Check to see if there is a delimiter right after the tag name
|
150
|
-
delimiter =
|
151
|
-
if @line =~ DELIMITER_REGEX
|
152
|
-
delimiter = DELIMITERS[$&]
|
153
|
-
@line.slice!(0)
|
154
|
-
end
|
197
|
+
delimiter = '>'
|
155
198
|
|
156
199
|
orig_line = @orig_line
|
157
200
|
lineno = @lineno
|
158
201
|
while true
|
159
202
|
# Parse attributes
|
160
|
-
|
161
|
-
while @line =~ attr_regex
|
162
|
-
@line = $'
|
203
|
+
while @line =~ /#{ATTR_NAME_REGEX}\s*(=\s*)?/
|
163
204
|
name = $1
|
164
|
-
|
205
|
+
@line = $'
|
206
|
+
if !$2
|
165
207
|
attributes << [:slim, :attr, name, false, 'true']
|
166
208
|
elsif @line =~ /\A["']/
|
167
209
|
# Value is quoted (static)
|
168
210
|
@line = $'
|
169
211
|
attributes << [:html, :attr, name, [:slim, :interpolate, parse_quoted_attribute($&)]]
|
170
|
-
|
171
|
-
@line =~ /[^ >]+/
|
212
|
+
elsif @line =~ /\A[^ >]+/
|
172
213
|
@line = $'
|
173
214
|
attributes << [:html, :attr, name, [:slim, :interpolate, $&]]
|
174
215
|
end
|
175
216
|
end
|
176
217
|
|
177
|
-
|
178
|
-
break unless delimiter
|
218
|
+
@line.lstrip!
|
179
219
|
|
180
220
|
# Find ending delimiter
|
181
|
-
if @line =~ /\A
|
221
|
+
if @line =~ /\A(>|\Z)/
|
182
222
|
@line = $'
|
183
223
|
break
|
224
|
+
elsif @line =~ %r!\A/>!
|
225
|
+
# Do nothing for closing tag
|
226
|
+
# don't eat the line either, we check for it again
|
227
|
+
if not $'.empty? and $' !~ /\s*#/
|
228
|
+
syntax_error!("Did not expect any content after self closing tag",
|
229
|
+
:orig_line => orig_line,
|
230
|
+
:lineno => lineno,
|
231
|
+
:column => orig_line.size)
|
232
|
+
end
|
233
|
+
break
|
184
234
|
end
|
185
235
|
|
186
|
-
# Found something where an attribute should be
|
187
|
-
@line.lstrip!
|
188
236
|
syntax_error!('Expected attribute') unless @line.empty?
|
189
237
|
|
190
238
|
# Attributes span multiple lines
|
@@ -19,7 +19,7 @@ p Test
|
|
19
19
|
p Test
|
20
20
|
}
|
21
21
|
chain = proc do |engine|
|
22
|
-
engine.before(
|
22
|
+
engine.before(Hamlet::Parser, :WrapInput) do |input|
|
23
23
|
"p Header\n#{input}\np Footer"
|
24
24
|
end
|
25
25
|
end
|
@@ -32,7 +32,7 @@ p Test
|
|
32
32
|
p Test
|
33
33
|
}
|
34
34
|
chain = proc do |engine|
|
35
|
-
engine.after(
|
35
|
+
engine.after(Hamlet::Parser, :ReplaceParsedExp) do |exp|
|
36
36
|
[:slim, :output, false, '1+1', [:multi]]
|
37
37
|
end
|
38
38
|
end
|
@@ -3,9 +3,9 @@ require 'helper'
|
|
3
3
|
class TestSlimCodeBlocks < TestSlim
|
4
4
|
def test_render_with_output_code_block
|
5
5
|
source = %q{
|
6
|
-
p
|
6
|
+
<p
|
7
7
|
= hello_world "Hello Ruby!" do
|
8
|
-
|
8
|
+
Hello from within a block!
|
9
9
|
}
|
10
10
|
|
11
11
|
assert_html '<p>Hello Ruby! Hello from within a block! Hello Ruby!</p>', source
|
@@ -13,7 +13,7 @@ p
|
|
13
13
|
|
14
14
|
def test_render_with_output_code_within_block
|
15
15
|
source = %q{
|
16
|
-
p
|
16
|
+
<p
|
17
17
|
= hello_world "Hello Ruby!" do
|
18
18
|
= hello_world "Hello from within a block!"
|
19
19
|
}
|
@@ -23,7 +23,7 @@ p
|
|
23
23
|
|
24
24
|
def test_render_with_output_code_within_block_2
|
25
25
|
source = %q{
|
26
|
-
p
|
26
|
+
<p
|
27
27
|
= hello_world "Hello Ruby!" do
|
28
28
|
= hello_world "Hello from within a block!" do
|
29
29
|
= hello_world "And another one!"
|
@@ -34,10 +34,10 @@ p
|
|
34
34
|
|
35
35
|
def test_output_block_with_arguments
|
36
36
|
source = %q{
|
37
|
-
p
|
37
|
+
<p
|
38
38
|
= define_macro :person do |first_name, last_name|
|
39
|
-
|
40
|
-
|
39
|
+
<.first_name>= first_name
|
40
|
+
<.last_name>= last_name
|
41
41
|
== call_macro :person, 'John', 'Doe'
|
42
42
|
== call_macro :person, 'Max', 'Mustermann'
|
43
43
|
}
|
@@ -48,9 +48,9 @@ p
|
|
48
48
|
|
49
49
|
def test_render_with_control_code_loop
|
50
50
|
source = %q{
|
51
|
-
p
|
51
|
+
<p
|
52
52
|
- 3.times do
|
53
|
-
|
53
|
+
Hey!
|
54
54
|
}
|
55
55
|
|
56
56
|
assert_html '<p>Hey!Hey!Hey!</p>', source
|
@@ -60,7 +60,7 @@ p
|
|
60
60
|
source = %q{
|
61
61
|
= hello_world "Hello Ruby!" do
|
62
62
|
- if true
|
63
|
-
|
63
|
+
Hello from within a block!
|
64
64
|
}
|
65
65
|
|
66
66
|
assert_html 'Hello Ruby! Hello from within a block! Hello Ruby!', source
|
@@ -3,7 +3,7 @@ require 'helper'
|
|
3
3
|
class TestSlimCodeEscaping < TestSlim
|
4
4
|
def test_escaping_evil_method
|
5
5
|
source = %q{
|
6
|
-
p
|
6
|
+
<p>= evil_method
|
7
7
|
}
|
8
8
|
|
9
9
|
assert_html '<p><script>do_something_evil();</script></p>', source
|
@@ -11,7 +11,7 @@ p = evil_method
|
|
11
11
|
|
12
12
|
def test_render_without_html_safe
|
13
13
|
source = %q{
|
14
|
-
p
|
14
|
+
<p>= "<strong>Hello World\\n, meet \\"Slim\\"</strong>."
|
15
15
|
}
|
16
16
|
|
17
17
|
assert_html "<p><strong>Hello World\n, meet \"Slim\"</strong>.</p>", source
|
@@ -19,7 +19,7 @@ p = "<strong>Hello World\\n, meet \\"Slim\\"</strong>."
|
|
19
19
|
|
20
20
|
def test_render_with_html_safe_false
|
21
21
|
source = %q{
|
22
|
-
p
|
22
|
+
<p>= HtmlUnsafeString.new("<strong>Hello World\\n, meet \\"Slim\\"</strong>.")
|
23
23
|
}
|
24
24
|
|
25
25
|
assert_html "<p><strong>Hello World\n, meet \"Slim\"</strong>.</p>", source, :use_html_safe => true
|
@@ -27,7 +27,7 @@ p = HtmlUnsafeString.new("<strong>Hello World\\n, meet \\"Slim\\"</strong>.")
|
|
27
27
|
|
28
28
|
def test_render_with_html_safe_true
|
29
29
|
source = %q{
|
30
|
-
p
|
30
|
+
<p>= HtmlSafeString.new("<strong>Hello World\\n, meet \\"Slim\\"</strong>.")
|
31
31
|
}
|
32
32
|
|
33
33
|
assert_html "<p><strong>Hello World\n, meet \"Slim\"</strong>.</p>", source, :use_html_safe => true
|